237 lines
6.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|