Remove 9 dead methods, 6 unused constants, and redundant ReaderWriterLockSlim from MetadataCache. Consolidate repeated patterns into HasChapters, IsPlayable, and ToLowerString helpers. Extract shared API methods in SRFApiClient. Move variant manifest rewriting from controller to StreamProxyService. Make Auto quality distinct from HD. Update README architecture section.
172 lines
6.3 KiB
C#
172 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Plugin.SRFPlay.Api;
|
|
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
|
|
using Jellyfin.Plugin.SRFPlay.Utilities;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.SRFPlay.Services;
|
|
|
|
/// <summary>
|
|
/// Service for refreshing content from SRF API.
|
|
/// </summary>
|
|
public class ContentRefreshService : IContentRefreshService
|
|
{
|
|
private readonly ILogger<ContentRefreshService> _logger;
|
|
private readonly ISRFApiClientFactory _apiClientFactory;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ContentRefreshService"/> class.
|
|
/// </summary>
|
|
/// <param name="loggerFactory">The logger factory.</param>
|
|
/// <param name="apiClientFactory">The API client factory.</param>
|
|
public ContentRefreshService(
|
|
ILoggerFactory loggerFactory,
|
|
ISRFApiClientFactory apiClientFactory)
|
|
{
|
|
_logger = loggerFactory.CreateLogger<ContentRefreshService>();
|
|
_apiClientFactory = apiClientFactory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes latest content from SRF API using Play v3.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>List of URNs for new content.</returns>
|
|
public async Task<List<string>> RefreshLatestContentAsync(CancellationToken cancellationToken)
|
|
{
|
|
var config = Plugin.Instance?.Configuration;
|
|
if (config == null || !config.EnableLatestContent)
|
|
{
|
|
_logger.LogDebug("Latest content refresh is disabled");
|
|
return new List<string>();
|
|
}
|
|
|
|
return await FetchVideosFromShowsAsync(
|
|
config.BusinessUnit.ToLowerString(),
|
|
minEpisodeCount: 0,
|
|
maxShows: 20,
|
|
videosPerShow: 1,
|
|
contentType: "latest",
|
|
cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes trending content from SRF API using Play v3.
|
|
/// Gets videos from shows with the most episodes.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>List of URNs for trending content.</returns>
|
|
public async Task<List<string>> RefreshTrendingContentAsync(CancellationToken cancellationToken)
|
|
{
|
|
var config = Plugin.Instance?.Configuration;
|
|
if (config == null || !config.EnableTrendingContent)
|
|
{
|
|
_logger.LogDebug("Trending content refresh is disabled");
|
|
return new List<string>();
|
|
}
|
|
|
|
return await FetchVideosFromShowsAsync(
|
|
config.BusinessUnit.ToLowerString(),
|
|
minEpisodeCount: 10,
|
|
maxShows: 15,
|
|
videosPerShow: 2,
|
|
contentType: "trending",
|
|
cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches videos from shows based on filter criteria.
|
|
/// </summary>
|
|
private async Task<List<string>> FetchVideosFromShowsAsync(
|
|
string businessUnit,
|
|
int minEpisodeCount,
|
|
int maxShows,
|
|
int videosPerShow,
|
|
string contentType,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var urns = new List<string>();
|
|
|
|
try
|
|
{
|
|
_logger.LogInformation("Refreshing {ContentType} content for business unit: {BusinessUnit}", contentType, businessUnit);
|
|
|
|
using var apiClient = _apiClientFactory.CreateClient();
|
|
var shows = await apiClient.GetAllShowsAsync(businessUnit, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (shows == null || shows.Count == 0)
|
|
{
|
|
_logger.LogWarning("No shows found for business unit: {BusinessUnit}", businessUnit);
|
|
return urns;
|
|
}
|
|
|
|
_logger.LogInformation("Found {Count} shows, fetching {ContentType} content", shows.Count, contentType);
|
|
|
|
var filteredShows = shows
|
|
.Where(s => s.NumberOfEpisodes > minEpisodeCount)
|
|
.OrderByDescending(s => s.NumberOfEpisodes)
|
|
.Take(maxShows)
|
|
.ToList();
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
foreach (var show in filteredShows)
|
|
{
|
|
if (show.Id == null || cancellationToken.IsCancellationRequested)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
var videos = await apiClient.GetVideosForShowAsync(businessUnit, show.Id, cancellationToken).ConfigureAwait(false);
|
|
if (videos == null || videos.Count == 0)
|
|
{
|
|
_logger.LogDebug("Show {Show} ({ShowId}): No videos returned from API", show.Title, show.Id);
|
|
continue;
|
|
}
|
|
|
|
_logger.LogDebug("Show {Show} ({ShowId}): Found {Count} videos", show.Title, show.Id, videos.Count);
|
|
|
|
// Filter to videos that are actually published (validFrom in the past)
|
|
var publishedVideos = videos
|
|
.Where(v => v.ValidFrom == null || v.ValidFrom.Value.ToUniversalTime() <= now)
|
|
.OrderByDescending(v => v.Date)
|
|
.Take(videosPerShow)
|
|
.ToList();
|
|
|
|
foreach (var video in publishedVideos)
|
|
{
|
|
if (video.Urn != null)
|
|
{
|
|
urns.Add(video.Urn);
|
|
_logger.LogDebug(
|
|
"Added {ContentType} video from show {Show}: {Title} (URN: {Urn})",
|
|
contentType,
|
|
show.Title,
|
|
video.Title,
|
|
video.Urn);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Error fetching videos for show {ShowId}", show.Id);
|
|
}
|
|
}
|
|
|
|
_logger.LogInformation("Refreshed {Count} {ContentType} content items from {ShowCount} shows", urns.Count, contentType, filteredShows.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error refreshing {ContentType} content", contentType);
|
|
}
|
|
|
|
return urns;
|
|
}
|
|
}
|