using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; 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 managing topic/category data and filtering. /// public class CategoryService { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly TimeSpan _topicsCacheDuration = TimeSpan.FromHours(24); private Dictionary? _topicsCache; private DateTime _topicsCacheExpiry = DateTime.MinValue; /// /// Initializes a new instance of the class. /// /// The logger factory. public CategoryService(ILoggerFactory loggerFactory) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); } /// /// Gets all topics for a business unit. /// /// The business unit. /// The cancellation token. /// List of topics. public async Task> GetTopicsAsync(string businessUnit, CancellationToken cancellationToken = default) { // Return cached topics if still valid if (_topicsCache != null && DateTime.UtcNow < _topicsCacheExpiry) { _logger.LogDebug("Returning cached topics for business unit: {BusinessUnit}", businessUnit); return _topicsCache.Values.ToList(); } _logger.LogInformation("Fetching topics for business unit: {BusinessUnit}", businessUnit); using var apiClient = new SRFApiClient(_loggerFactory); var topics = await apiClient.GetAllTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false); if (topics != null && topics.Count > 0) { // Cache topics by ID for quick lookups _topicsCache = topics .Where(t => !string.IsNullOrEmpty(t.Id)) .ToDictionary(t => t.Id!, t => t); _topicsCacheExpiry = DateTime.UtcNow.Add(_topicsCacheDuration); _logger.LogInformation("Cached {Count} topics for business unit: {BusinessUnit}", _topicsCache.Count, businessUnit); } return topics ?? new List(); } /// /// Gets a topic by ID. /// /// The topic ID. /// The business unit. /// The cancellation token. /// The topic, or null if not found. public async Task GetTopicByIdAsync(string topicId, string businessUnit, CancellationToken cancellationToken = default) { // Ensure topics are loaded if (_topicsCache == null || DateTime.UtcNow >= _topicsCacheExpiry) { await GetTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false); } return _topicsCache?.GetValueOrDefault(topicId); } /// /// Filters shows by topic ID. /// /// The shows to filter. /// The topic ID to filter by. /// Filtered list of shows. public IReadOnlyList FilterShowsByTopic(IReadOnlyList shows, string topicId) { if (string.IsNullOrEmpty(topicId)) { return shows; } return shows .Where(s => s.TopicList != null && s.TopicList.Contains(topicId)) .ToList(); } /// /// Groups shows by their topics. /// /// The shows to group. /// Dictionary mapping topic IDs to shows. public IReadOnlyDictionary> GroupShowsByTopics(IReadOnlyList shows) { var groupedShows = new Dictionary>(); foreach (var show in shows) { if (show.TopicList == null || show.TopicList.Count == 0) { continue; } foreach (var topicId in show.TopicList) { if (!groupedShows.TryGetValue(topicId, out var showList)) { showList = new List(); groupedShows[topicId] = showList; } showList.Add(show); } } return groupedShows; } /// /// Gets shows for a specific topic, sorted by number of episodes. /// /// The topic ID. /// The business unit. /// Maximum number of results to return. /// The cancellation token. /// List of shows for the topic. public async Task> GetShowsByTopicAsync( string topicId, string businessUnit, int maxResults = 50, CancellationToken cancellationToken = default) { using var apiClient = new SRFApiClient(_loggerFactory); 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; } /// /// Gets video count for each topic. /// /// The shows to analyze. /// Dictionary mapping topic IDs to video counts. public IReadOnlyDictionary GetVideoCountByTopic(IReadOnlyList shows) { var topicCounts = new Dictionary(); foreach (var show in shows) { if (show.TopicList == null || show.TopicList.Count == 0) { continue; } foreach (var topicId in show.TopicList) { if (!topicCounts.TryGetValue(topicId, out var count)) { count = 0; } topicCounts[topicId] = count + show.NumberOfEpisodes; } } return topicCounts; } /// /// Clears the topics cache. /// public void ClearCache() { _topicsCache = null; _topicsCacheExpiry = DateTime.MinValue; _logger.LogInformation("Topics cache cleared"); } }