Duncan Tourolle ac6a3842dd
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
📝 Create/Update Release Draft & Release Bump PR / call (push) Failing after 0s
🧪 Test Plugin / call (push) Failing after 0s
🔬 Run CodeQL / call (push) Failing after 0s
first commit
2025-11-12 22:05:36 +01:00

237 lines
6.4 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Threading;
using Jellyfin.Plugin.SRFPlay.Api.Models;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Services;
/// <summary>
/// Service for caching metadata from SRF API.
/// </summary>
public sealed class MetadataCache : IDisposable
{
private readonly ILogger<MetadataCache> _logger;
private readonly ConcurrentDictionary<string, CacheEntry<MediaComposition>> _mediaCompositionCache;
private readonly ReaderWriterLockSlim _lock;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="MetadataCache"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
public MetadataCache(ILogger<MetadataCache> logger)
{
_logger = logger;
_mediaCompositionCache = new ConcurrentDictionary<string, CacheEntry<MediaComposition>>();
_lock = new ReaderWriterLockSlim();
}
/// <summary>
/// Disposes resources.
/// </summary>
public void Dispose()
{
if (!_disposed)
{
_lock?.Dispose();
_disposed = true;
}
}
/// <summary>
/// Gets cached media composition by URN.
/// </summary>
/// <param name="urn">The URN.</param>
/// <param name="cacheDurationMinutes">The cache duration in minutes.</param>
/// <returns>The cached media composition, or null if not found or expired.</returns>
public MediaComposition? GetMediaComposition(string urn, int cacheDurationMinutes)
{
if (string.IsNullOrEmpty(urn))
{
return null;
}
try
{
_lock.EnterReadLock();
try
{
if (_mediaCompositionCache.TryGetValue(urn, out var entry))
{
if (entry.IsValid(cacheDurationMinutes))
{
_logger.LogDebug("Cache hit for URN: {Urn}", urn);
return entry.Value;
}
_logger.LogDebug("Cache entry expired for URN: {Urn}", urn);
}
}
finally
{
_lock.ExitReadLock();
}
}
catch (ObjectDisposedException)
{
return null;
}
return null;
}
/// <summary>
/// Sets media composition in cache.
/// </summary>
/// <param name="urn">The URN.</param>
/// <param name="mediaComposition">The media composition to cache.</param>
public void SetMediaComposition(string urn, MediaComposition mediaComposition)
{
if (string.IsNullOrEmpty(urn) || mediaComposition == null)
{
return;
}
try
{
_lock.EnterWriteLock();
try
{
var entry = new CacheEntry<MediaComposition>(mediaComposition);
_mediaCompositionCache.AddOrUpdate(urn, entry, (key, oldValue) => entry);
_logger.LogDebug("Cached media composition for URN: {Urn}", urn);
}
finally
{
_lock.ExitWriteLock();
}
}
catch (ObjectDisposedException)
{
// Cache is disposed, ignore
}
}
/// <summary>
/// Removes media composition from cache.
/// </summary>
/// <param name="urn">The URN.</param>
public void RemoveMediaComposition(string urn)
{
if (string.IsNullOrEmpty(urn))
{
return;
}
try
{
_lock.EnterWriteLock();
try
{
if (_mediaCompositionCache.TryRemove(urn, out _))
{
_logger.LogDebug("Removed cached media composition for URN: {Urn}", urn);
}
}
finally
{
_lock.ExitWriteLock();
}
}
catch (ObjectDisposedException)
{
// Cache is disposed, ignore
}
}
/// <summary>
/// Clears all cached data.
/// </summary>
public void Clear()
{
try
{
_lock.EnterWriteLock();
try
{
_mediaCompositionCache.Clear();
_logger.LogInformation("Cleared metadata cache");
}
finally
{
_lock.ExitWriteLock();
}
}
catch (ObjectDisposedException)
{
// Cache is disposed, ignore
}
}
/// <summary>
/// Gets the cache statistics.
/// </summary>
/// <returns>A tuple with cache count and size estimate.</returns>
public (int Count, long SizeEstimate) GetStatistics()
{
try
{
_lock.EnterReadLock();
try
{
var count = _mediaCompositionCache.Count;
// Rough estimate: average 50KB per entry
var sizeEstimate = count * 50L * 1024;
return (count, sizeEstimate);
}
finally
{
_lock.ExitReadLock();
}
}
catch (ObjectDisposedException)
{
return (0, 0);
}
}
/// <summary>
/// Represents a cached entry with timestamp.
/// </summary>
/// <typeparam name="T">The type of cached value.</typeparam>
private sealed class CacheEntry<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="CacheEntry{T}"/> class.
/// </summary>
/// <param name="value">The value to cache.</param>
public CacheEntry(T value)
{
Value = value;
Timestamp = DateTime.UtcNow;
}
/// <summary>
/// Gets the cached value.
/// </summary>
public T Value { get; }
/// <summary>
/// Gets the timestamp when the entry was created.
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// Checks if the cache entry is still valid.
/// </summary>
/// <param name="cacheDurationMinutes">The cache duration in minutes.</param>
/// <returns>True if the entry is still valid.</returns>
public bool IsValid(int cacheDurationMinutes)
{
var expirationTime = Timestamp.AddMinutes(cacheDurationMinutes);
return DateTime.UtcNow < expirationTime;
}
}
}