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 Jellyfin.Plugin.SRFPlay.Services.Interfaces; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.SRFPlay.Services; /// /// Service for managing topic/category data and filtering. /// public class CategoryService : ICategoryService { private readonly ILogger _logger; private readonly ISRFApiClientFactory _apiClientFactory; private readonly TimeSpan _topicsCacheDuration = TimeSpan.FromHours(24); // Cache topics per business unit. A single shared cache would return one unit's // topics (e.g. German SRF) even after the user switched to another (e.g. French RTS). private readonly Dictionary Topics, DateTime Expiry)> _topicsCacheByUnit = new(StringComparer.OrdinalIgnoreCase); /// /// Initializes a new instance of the class. /// /// The logger. /// The API client factory. public CategoryService(ILogger logger, ISRFApiClientFactory apiClientFactory) { _logger = logger; _apiClientFactory = apiClientFactory; } /// public async Task> GetTopicsAsync(string businessUnit, CancellationToken cancellationToken = default) { // Return cached topics for this business unit if still valid if (_topicsCacheByUnit.TryGetValue(businessUnit, out var cached) && DateTime.UtcNow < cached.Expiry) { _logger.LogDebug("Returning cached topics for business unit: {BusinessUnit}", businessUnit); return cached.Topics.Values.ToList(); } _logger.LogInformation("Fetching topics for business unit: {BusinessUnit}", businessUnit); using var apiClient = _apiClientFactory.CreateClient(); var topics = await apiClient.GetAllTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false); if (topics != null && topics.Count > 0) { // Cache topics by ID for quick lookups, scoped to this business unit var byId = topics .Where(t => !string.IsNullOrEmpty(t.Id)) .ToDictionary(t => t.Id!, t => t); _topicsCacheByUnit[businessUnit] = (byId, DateTime.UtcNow.Add(_topicsCacheDuration)); _logger.LogInformation("Cached {Count} topics for business unit: {BusinessUnit}", byId.Count, businessUnit); } return topics ?? new List(); } /// public async Task GetTopicByIdAsync(string topicId, string businessUnit, CancellationToken cancellationToken = default) { // Ensure topics are loaded for this business unit if (!_topicsCacheByUnit.TryGetValue(businessUnit, out var cached) || DateTime.UtcNow >= cached.Expiry) { await GetTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false); _topicsCacheByUnit.TryGetValue(businessUnit, out cached); } return cached.Topics?.GetValueOrDefault(topicId); } /// public async Task> GetShowsByTopicAsync( string topicId, string businessUnit, int maxResults = 50, CancellationToken cancellationToken = default) { using var apiClient = _apiClientFactory.CreateClient(); var allShows = await apiClient.GetAllShowsAsync(businessUnit, cancellationToken).ConfigureAwait(false); if (allShows == null || allShows.Count == 0) { _logger.LogWarning("No shows available for business unit: {BusinessUnit}", businessUnit); return new List(); } var filteredShows = FilterShowsByTopic(allShows, topicId) .Where(s => s.NumberOfEpisodes > 0) .OrderByDescending(s => s.NumberOfEpisodes) .Take(maxResults) .ToList(); _logger.LogDebug("Found {Count} shows for topic {TopicId}", filteredShows.Count, topicId); return filteredShows; } /// public void ClearCache() { _topicsCacheByUnit.Clear(); _logger.LogInformation("Topics cache cleared"); } /// /// Filters shows by topic ID. /// /// The shows to filter. /// The topic ID to filter by. /// Filtered list of shows. private static IReadOnlyList FilterShowsByTopic(IReadOnlyList shows, string topicId) { if (string.IsNullOrEmpty(topicId)) { return shows; } return shows .Where(s => s.TopicList != null && s.TopicList.Contains(topicId)) .ToList(); } }