using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Services;
///
/// Service for managing content expiration.
///
public class ContentExpirationService
{
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
private readonly StreamUrlResolver _streamResolver;
private readonly MetadataCache _metadataCache;
///
/// Initializes a new instance of the class.
///
/// The logger factory.
/// The library manager.
/// The stream URL resolver.
/// The metadata cache.
public ContentExpirationService(
ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
StreamUrlResolver streamResolver,
MetadataCache metadataCache)
{
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger();
_libraryManager = libraryManager;
_streamResolver = streamResolver;
_metadataCache = metadataCache;
}
///
/// Checks for expired content and removes it from the library.
///
/// The cancellation token.
/// The number of items removed.
public async Task CheckAndRemoveExpiredContentAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting content expiration check");
var removedCount = 0;
try
{
// Get all items with SRF provider ID
var query = new InternalItemsQuery
{
HasAnyProviderId = new Dictionary { { "SRF", string.Empty } },
IsVirtualItem = false
};
var items = _libraryManager.GetItemList(query);
_logger.LogDebug("Found {Count} SRF items to check for expiration", items.Count);
foreach (var item in items)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
try
{
if (await IsItemExpiredAsync(item, cancellationToken).ConfigureAwait(false))
{
_logger.LogInformation("Removing expired item: {Name} (URN: {Urn})", item.Name, item.ProviderIds.GetValueOrDefault("SRF"));
// Delete the item from library
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
removedCount++;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking expiration for item: {Name}", item.Name);
}
}
_logger.LogInformation("Content expiration check completed. Removed {Count} expired items", removedCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during content expiration check");
}
return removedCount;
}
///
/// Checks if an item is expired.
///
/// The item to check.
/// The cancellation token.
/// True if the item is expired.
private async Task IsItemExpiredAsync(BaseItem item, CancellationToken cancellationToken)
{
var urn = item.ProviderIds.GetValueOrDefault("SRF");
if (string.IsNullOrEmpty(urn))
{
return false;
}
var config = Plugin.Instance?.Configuration;
if (config == null)
{
return false;
}
// Try cache first
var mediaComposition = _metadataCache.GetMediaComposition(urn, config.CacheDurationMinutes);
// If not in cache, fetch from API
if (mediaComposition == null)
{
using var apiClient = new SRFApiClient(_loggerFactory);
mediaComposition = await apiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).ConfigureAwait(false);
if (mediaComposition != null)
{
_metadataCache.SetMediaComposition(urn, mediaComposition);
}
}
if (mediaComposition?.ChapterList == null || mediaComposition.ChapterList.Count == 0)
{
// If we can't fetch the content, consider it expired
_logger.LogWarning("Could not fetch media composition for URN: {Urn}, treating as expired", urn);
return true;
}
var chapter = mediaComposition.ChapterList[0];
var isExpired = _streamResolver.IsContentExpired(chapter);
if (isExpired)
{
_logger.LogDebug("Item {Name} is expired (ValidTo: {ValidTo})", item.Name, chapter.ValidTo);
}
return isExpired;
}
///
/// Gets statistics about content expiration.
///
/// The cancellation token.
/// Tuple with total count, expired count, and items expiring soon.
public async Task<(int Total, int Expired, int ExpiringSoon)> GetExpirationStatisticsAsync(CancellationToken cancellationToken)
{
var total = 0;
var expired = 0;
var expiringSoon = 0;
var soonThreshold = DateTime.UtcNow.AddDays(7); // Items expiring within 7 days
try
{
var query = new InternalItemsQuery
{
HasAnyProviderId = new Dictionary { { "SRF", string.Empty } },
IsVirtualItem = false
};
var items = _libraryManager.GetItemList(query);
total = items.Count;
foreach (var item in items)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
try
{
var urn = item.ProviderIds.GetValueOrDefault("SRF");
if (string.IsNullOrEmpty(urn))
{
continue;
}
var config = Plugin.Instance?.Configuration;
if (config == null)
{
continue;
}
var mediaComposition = _metadataCache.GetMediaComposition(urn, config.CacheDurationMinutes);
if (mediaComposition == null)
{
using var apiClient = new SRFApiClient(_loggerFactory);
mediaComposition = await apiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).ConfigureAwait(false);
if (mediaComposition != null)
{
_metadataCache.SetMediaComposition(urn, mediaComposition);
}
}
if (mediaComposition?.ChapterList != null && mediaComposition.ChapterList.Count > 0)
{
var chapter = mediaComposition.ChapterList[0];
if (_streamResolver.IsContentExpired(chapter))
{
expired++;
}
else if (chapter.ValidTo.HasValue && chapter.ValidTo.Value.ToUniversalTime() <= soonThreshold)
{
expiringSoon++;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking expiration statistics for item: {Name}", item.Name);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting expiration statistics");
}
return (total, expired, expiringSoon);
}
}