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.Api.Models; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.SRFPlay.Services; /// /// Service for refreshing content from SRF API. /// public class ContentRefreshService { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly MetadataCache _metadataCache; /// /// Initializes a new instance of the class. /// /// The logger factory. /// The metadata cache. public ContentRefreshService( ILoggerFactory loggerFactory, MetadataCache metadataCache) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); _metadataCache = metadataCache; } /// /// 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 urns = new List(); try { var config = Plugin.Instance?.Configuration; if (config == null || !config.EnableLatestContent) { _logger.LogDebug("Latest content refresh is disabled"); return urns; } _logger.LogInformation("Refreshing latest content for business unit: {BusinessUnit}", config.BusinessUnit); using var apiClient = new SRFApiClient(_loggerFactory); var businessUnit = config.BusinessUnit.ToString().ToLowerInvariant(); // Get all shows from Play v3 API var shows = await apiClient.GetAllShowsAsync(businessUnit, cancellationToken).ConfigureAwait(false); if (shows == null || shows.Count == 0) { _logger.LogWarning("No shows found for business unit: {BusinessUnit}", config.BusinessUnit); return urns; } _logger.LogInformation("Found {Count} shows, fetching latest episodes from each", shows.Count); // Get latest episodes from each show (limit to 20 shows to avoid overwhelming) var showsToFetch = shows.Where(s => s.NumberOfEpisodes > 0) .OrderByDescending(s => s.NumberOfEpisodes) .Take(20) .ToList(); foreach (var show in showsToFetch) { if (show.Id == null) { continue; } try { var videos = await apiClient.GetVideosForShowAsync(businessUnit, show.Id, cancellationToken).ConfigureAwait(false); if (videos != null && videos.Count > 0) { _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 now = DateTime.UtcNow; var publishedVideos = videos.Where(v => v.ValidFrom == null || v.ValidFrom.Value.ToUniversalTime() <= now).ToList(); _logger.LogDebug("Show {Show}: {PublishedCount} published out of {TotalCount} videos", show.Title, publishedVideos.Count, videos.Count); if (publishedVideos.Count > 0) { // Take only the most recent published video from each show var latestVideo = publishedVideos.OrderByDescending(v => v.Date).FirstOrDefault(); if (latestVideo?.Urn != null) { urns.Add(latestVideo.Urn); _logger.LogInformation( "Added latest video from show {Show}: {Title} (URN: {Urn}, Date: {Date}, ValidFrom: {ValidFrom}, ValidTo: {ValidTo})", show.Title, latestVideo.Title, latestVideo.Urn, latestVideo.Date, latestVideo.ValidFrom, latestVideo.ValidTo); } else { _logger.LogWarning("Show {Show}: Latest video has null URN", show.Title); } } else { _logger.LogDebug("Show {Show} has no published videos yet", show.Title); } } else { _logger.LogDebug("Show {Show} ({ShowId}): No videos returned from API", show.Title, show.Id); } } catch (Exception ex) { _logger.LogWarning(ex, "Error fetching videos for show {ShowId}", show.Id); } // Respect cancellation if (cancellationToken.IsCancellationRequested) { break; } } _logger.LogInformation("Refreshed {Count} latest content items from {ShowCount} shows", urns.Count, showsToFetch.Count); } catch (Exception ex) { _logger.LogError(ex, "Error refreshing latest content"); } return urns; } /// /// 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 urns = new List(); try { var config = Plugin.Instance?.Configuration; if (config == null || !config.EnableTrendingContent) { _logger.LogDebug("Trending content refresh is disabled"); return urns; } _logger.LogInformation("Refreshing trending content for business unit: {BusinessUnit}", config.BusinessUnit); using var apiClient = new SRFApiClient(_loggerFactory); var businessUnit = config.BusinessUnit.ToString().ToLowerInvariant(); // Get all shows from Play v3 API var shows = await apiClient.GetAllShowsAsync(businessUnit, cancellationToken).ConfigureAwait(false); if (shows == null || shows.Count == 0) { _logger.LogWarning("No shows found for business unit: {BusinessUnit}", config.BusinessUnit); return urns; } _logger.LogInformation("Found {Count} shows, fetching popular content", shows.Count); // Get videos from popular shows (those with many episodes) var popularShows = shows.Where(s => s.NumberOfEpisodes > 10) .OrderByDescending(s => s.NumberOfEpisodes) .Take(15) .ToList(); foreach (var show in popularShows) { if (show.Id == null) { continue; } try { var videos = await apiClient.GetVideosForShowAsync(businessUnit, show.Id, cancellationToken).ConfigureAwait(false); if (videos != null && videos.Count > 0) { _logger.LogDebug("Show {Show} ({ShowId}): Found {Count} videos for trending", show.Title, show.Id, videos.Count); // Filter to videos that are actually published (validFrom in the past) var now = DateTime.UtcNow; var publishedVideos = videos.Where(v => v.ValidFrom == null || v.ValidFrom.Value.ToUniversalTime() <= now).ToList(); _logger.LogDebug("Show {Show}: {PublishedCount} published out of {TotalCount} videos for trending", show.Title, publishedVideos.Count, videos.Count); if (publishedVideos.Count > 0) { // Take 2 recent published videos from each popular show var recentVideos = publishedVideos.OrderByDescending(v => v.Date).Take(2); foreach (var video in recentVideos) { if (video.Urn != null) { urns.Add(video.Urn); _logger.LogInformation( "Added trending video from show {Show}: {Title} (URN: {Urn}, Date: {Date}, ValidFrom: {ValidFrom}, ValidTo: {ValidTo})", show.Title, video.Title, video.Urn, video.Date, video.ValidFrom, video.ValidTo); } else { _logger.LogWarning("Show {Show}: Trending video has null URN - {Title}", show.Title, video.Title); } } } } else { _logger.LogDebug("Show {Show} ({ShowId}): No videos returned from API for trending", show.Title, show.Id); } } catch (Exception ex) { _logger.LogWarning(ex, "Error fetching videos for show {ShowId}", show.Id); } // Respect cancellation if (cancellationToken.IsCancellationRequested) { break; } } _logger.LogInformation("Refreshed {Count} trending content items from {ShowCount} shows", urns.Count, popularShows.Count); } catch (Exception ex) { _logger.LogError(ex, "Error refreshing trending content"); } 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(); } }