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 Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.SRFPlay.Services; /// /// Service for refreshing content from SRF API. /// public class ContentRefreshService : IContentRefreshService { private readonly ILogger _logger; private readonly ISRFApiClientFactory _apiClientFactory; /// /// Initializes a new instance of the class. /// /// The logger factory. /// The API client factory. public ContentRefreshService( ILoggerFactory loggerFactory, ISRFApiClientFactory apiClientFactory) { _logger = loggerFactory.CreateLogger(); _apiClientFactory = apiClientFactory; } /// /// Refreshes latest content from SRF API using Play v3. /// /// The cancellation token. /// List of URNs for new content. public async Task> RefreshLatestContentAsync(CancellationToken cancellationToken) { var config = Plugin.Instance?.Configuration; if (config == null || !config.EnableLatestContent) { _logger.LogDebug("Latest content refresh is disabled"); return new List(); } return await FetchVideosFromShowsAsync( config.BusinessUnit.ToString().ToLowerInvariant(), minEpisodeCount: 0, maxShows: 20, videosPerShow: 1, contentType: "latest", cancellationToken).ConfigureAwait(false); } /// /// Refreshes trending content from SRF API using Play v3. /// Gets videos from shows with the most episodes. /// /// The cancellation token. /// List of URNs for trending content. public async Task> RefreshTrendingContentAsync(CancellationToken cancellationToken) { var config = Plugin.Instance?.Configuration; if (config == null || !config.EnableTrendingContent) { _logger.LogDebug("Trending content refresh is disabled"); return new List(); } return await FetchVideosFromShowsAsync( config.BusinessUnit.ToString().ToLowerInvariant(), minEpisodeCount: 10, maxShows: 15, videosPerShow: 2, contentType: "trending", cancellationToken).ConfigureAwait(false); } /// /// Fetches videos from shows based on filter criteria. /// private async Task> FetchVideosFromShowsAsync( string businessUnit, int minEpisodeCount, int maxShows, int videosPerShow, string contentType, CancellationToken cancellationToken) { var urns = new List(); 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; } /// /// Refreshes all content (latest and trending). /// /// The cancellation token. /// Tuple with counts of latest and trending items. public async Task<(int LatestCount, int TrendingCount)> RefreshAllContentAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting full content refresh"); var latestUrns = await RefreshLatestContentAsync(cancellationToken).ConfigureAwait(false); var trendingUrns = await RefreshTrendingContentAsync(cancellationToken).ConfigureAwait(false); var latestCount = latestUrns.Count; var trendingCount = trendingUrns.Count; _logger.LogInformation( "Content refresh completed. Latest: {LatestCount}, Trending: {TrendingCount}", latestCount, trendingCount); return (latestCount, trendingCount); } /// /// Gets content recommendations (combines latest and trending). /// /// The cancellation token. /// List of recommended URNs. public async Task> GetRecommendedContentAsync(CancellationToken cancellationToken) { var recommendations = new HashSet(); var latestUrns = await RefreshLatestContentAsync(cancellationToken).ConfigureAwait(false); var trendingUrns = await RefreshTrendingContentAsync(cancellationToken).ConfigureAwait(false); foreach (var urn in latestUrns.Concat(trendingUrns)) { recommendations.Add(urn); } _logger.LogInformation("Generated {Count} content recommendations", recommendations.Count); return recommendations.ToList(); } }