using System; using System.Collections.Concurrent; using System.Threading; using Jellyfin.Plugin.SRFPlay.Api.Models; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.SRFPlay.Services; /// /// Service for caching metadata from SRF API. /// public sealed class MetadataCache : IDisposable { private readonly ILogger _logger; private readonly ConcurrentDictionary> _mediaCompositionCache; private readonly ReaderWriterLockSlim _lock; private bool _disposed; /// /// Initializes a new instance of the class. /// /// The logger instance. public MetadataCache(ILogger logger) { _logger = logger; _mediaCompositionCache = new ConcurrentDictionary>(); _lock = new ReaderWriterLockSlim(); } /// /// Disposes resources. /// public void Dispose() { if (!_disposed) { _lock?.Dispose(); _disposed = true; } } /// /// Gets cached media composition by URN. /// /// The URN. /// The cache duration in minutes. /// The cached media composition, or null if not found or expired. 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; } /// /// Sets media composition in cache. /// /// The URN. /// The media composition to cache. public void SetMediaComposition(string urn, MediaComposition mediaComposition) { if (string.IsNullOrEmpty(urn) || mediaComposition == null) { return; } try { _lock.EnterWriteLock(); try { var entry = new CacheEntry(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 } } /// /// Removes media composition from cache. /// /// The URN. 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 } } /// /// Clears all cached data. /// public void Clear() { try { _lock.EnterWriteLock(); try { _mediaCompositionCache.Clear(); _logger.LogInformation("Cleared metadata cache"); } finally { _lock.ExitWriteLock(); } } catch (ObjectDisposedException) { // Cache is disposed, ignore } } /// /// Gets the cache statistics. /// /// A tuple with cache count and size estimate. 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); } } /// /// Represents a cached entry with timestamp. /// /// The type of cached value. private sealed class CacheEntry { /// /// Initializes a new instance of the class. /// /// The value to cache. public CacheEntry(T value) { Value = value; Timestamp = DateTime.UtcNow; } /// /// Gets the cached value. /// public T Value { get; } /// /// Gets the timestamp when the entry was created. /// public DateTime Timestamp { get; } /// /// Checks if the cache entry is still valid. /// /// The cache duration in minutes. /// True if the entry is still valid. public bool IsValid(int cacheDurationMinutes) { var expirationTime = Timestamp.AddMinutes(cacheDurationMinutes); return DateTime.UtcNow < expirationTime; } } }