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); } }