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();
}
}