246 lines
8.6 KiB
C#
246 lines
8.6 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Service for managing content expiration.
|
|
/// </summary>
|
|
public class ContentExpirationService
|
|
{
|
|
private readonly ILogger<ContentExpirationService> _logger;
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
private readonly ILibraryManager _libraryManager;
|
|
private readonly StreamUrlResolver _streamResolver;
|
|
private readonly MetadataCache _metadataCache;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ContentExpirationService"/> class.
|
|
/// </summary>
|
|
/// <param name="loggerFactory">The logger factory.</param>
|
|
/// <param name="libraryManager">The library manager.</param>
|
|
/// <param name="streamResolver">The stream URL resolver.</param>
|
|
/// <param name="metadataCache">The metadata cache.</param>
|
|
public ContentExpirationService(
|
|
ILoggerFactory loggerFactory,
|
|
ILibraryManager libraryManager,
|
|
StreamUrlResolver streamResolver,
|
|
MetadataCache metadataCache)
|
|
{
|
|
_loggerFactory = loggerFactory;
|
|
_logger = loggerFactory.CreateLogger<ContentExpirationService>();
|
|
_libraryManager = libraryManager;
|
|
_streamResolver = streamResolver;
|
|
_metadataCache = metadataCache;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks for expired content and removes it from the library.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The number of items removed.</returns>
|
|
public async Task<int> 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<string, string> { { "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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an item is expired.
|
|
/// </summary>
|
|
/// <param name="item">The item to check.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>True if the item is expired.</returns>
|
|
private async Task<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets statistics about content expiration.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>Tuple with total count, expired count, and items expiring soon.</returns>
|
|
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<string, string> { { "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);
|
|
}
|
|
}
|