jellyfin-srfPlay/Jellyfin.Plugin.SRFPlay/Services/ContentExpirationService.cs
Duncan Tourolle c1b5dea569
All checks were successful
🏗️ Build Plugin / build (push) Successful in 41s
🧪 Test Plugin / test (push) Successful in 34s
🚀 Release Plugin / build-and-release (push) Successful in 51s
Fixes for Livestreams
2026-02-28 13:11:02 +01:00

140 lines
5.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Services;
/// <summary>
/// Service for managing content expiration.
/// </summary>
public class ContentExpirationService : IContentExpirationService
{
private readonly ILogger<ContentExpirationService> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IStreamUrlResolver _streamResolver;
private readonly IMediaCompositionFetcher _compositionFetcher;
/// <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="compositionFetcher">The media composition fetcher.</param>
public ContentExpirationService(
ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
IStreamUrlResolver streamResolver,
IMediaCompositionFetcher compositionFetcher)
{
_logger = loggerFactory.CreateLogger<ContentExpirationService>();
_libraryManager = libraryManager;
_streamResolver = streamResolver;
_compositionFetcher = compositionFetcher;
}
/// <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 mediaComposition = await _compositionFetcher.GetMediaCompositionAsync(urn, cancellationToken).ConfigureAwait(false);
if (mediaComposition?.HasChapters != true)
{
// Don't treat API failures as expired - the content may still be available
// and a transient error (network issue, 403, API outage) shouldn't delete library items
_logger.LogWarning("Could not fetch media composition for URN: {Urn}, skipping (not treating as expired)", urn);
return false;
}
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;
}
}