using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; namespace ConfusedPolarBear.Plugin.IntroSkipper; #if !DEBUG #error Fix all FIXMEs introduced during initial credit implementation before release #endif /// /// Analyze all television episodes for credits. /// public class DetectCreditsTask : IScheduledTask { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly ILibraryManager? _libraryManager; /// /// Initializes a new instance of the class. /// /// Logger factory. /// Library manager. public DetectCreditsTask( ILoggerFactory loggerFactory, ILibraryManager libraryManager) : this(loggerFactory) { _libraryManager = libraryManager; } /// /// Initializes a new instance of the class. /// /// Logger factory. public DetectCreditsTask(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); _loggerFactory = loggerFactory; } /// /// Gets the task name. /// public string Name => "Detect Credits"; /// /// Gets the task category. /// public string Category => "Intro Skipper"; /// /// Gets the task description. /// public string Description => "Analyzes the audio and video of all television episodes to find credits."; /// /// Gets the task key. /// public string Key => "CPBIntroSkipperDetectCredits"; /// /// Analyze all episodes in the queue. Only one instance of this task should be run at a time. /// /// Task progress. /// Cancellation token. /// Task. public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { if (_libraryManager is null) { throw new InvalidOperationException("Library manager must not be null"); } // Make sure the analysis queue matches what's currently in Jellyfin. var queueManager = new QueueManager( _loggerFactory.CreateLogger(), _libraryManager); queueManager.EnqueueAllEpisodes(); var queue = Plugin.Instance!.AnalysisQueue; if (queue.Count == 0) { throw new FingerprintException( "No episodes to analyze. If you are limiting the list of libraries to analyze, check that all library names have been spelled correctly."); } var totalProcessed = 0; var options = new ParallelOptions() { MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism }; // TODO: FIXME: if the queue is modified while the task is running, the task will fail. // clone the queue before running the task to prevent this. // Analyze all episodes in the queue using the degrees of parallelism the user specified. Parallel.ForEach(queue, options, (season) => { // TODO: FIXME: use VerifyEpisodes var episodes = season.Value.AsReadOnly(); if (episodes.Count == 0) { return; } var first = episodes[0]; try { if (cancellationToken.IsCancellationRequested) { return; } AnalyzeSeason(episodes, cancellationToken); Interlocked.Add(ref totalProcessed, episodes.Count); } catch (FingerprintException ex) { _logger.LogWarning( "Unable to analyze {Series} season {Season}: unable to fingerprint: {Ex}", first.SeriesName, first.SeasonNumber, ex); } catch (KeyNotFoundException ex) { _logger.LogWarning( "Unable to analyze {Series} season {Season}: cache miss: {Ex}", first.SeriesName, first.SeasonNumber, ex); } progress.Report((totalProcessed * 100) / Plugin.Instance!.TotalQueued); }); return Task.CompletedTask; } /// /// Analyzes all episodes in the season for end credits. /// /// Episodes in this season. /// Cancellation token provided by the scheduled task. private void AnalyzeSeason( ReadOnlyCollection episodes, CancellationToken cancellationToken) { // Only analyze specials (season 0) if the user has opted in. if (episodes[0].SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero) { return; } // Analyze with Chromaprint first and fall back to the black frame detector var analyzers = new IMediaFileAnalyzer[] { // TODO: FIXME: new ChromaprintAnalyzer(_loggerFactory.CreateLogger()), new BlackFrameAnalyzer(_loggerFactory.CreateLogger()) }; // Use each analyzer to find credits in all media files, removing successfully analyzed files // from the queue. var remaining = new ReadOnlyCollection(episodes); foreach (var analyzer in analyzers) { remaining = AnalyzeFiles(remaining, analyzer, cancellationToken); } } private ReadOnlyCollection AnalyzeFiles( ReadOnlyCollection episodes, IMediaFileAnalyzer analyzer, CancellationToken cancellationToken) { _logger.LogInformation( "Analyzing {Count} episodes from {Name} season {Season} with {Analyzer}", episodes.Count, episodes[0].SeriesName, episodes[0].SeasonNumber, analyzer.GetType().Name); return analyzer.AnalyzeMediaFiles(episodes, AnalysisMode.Credits, cancellationToken); } /// /// Get task triggers. /// /// Task triggers. public IEnumerable GetDefaultTriggers() { return Array.Empty(); } }