From 431aed58ffa8705de38a919f7804b3e4350ba9c3 Mon Sep 17 00:00:00 2001 From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Thu, 2 Feb 2023 01:20:11 -0600 Subject: [PATCH] Combine introduction and credits detection code --- .../ScheduledTasks/BaseItemAnalyzerTask.cs | 198 ++++++++++++++++++ .../ScheduledTasks/DetectCreditsTask.cs | 136 +----------- .../ScheduledTasks/DetectIntroductionsTask.cs | 164 +-------------- 3 files changed, 217 insertions(+), 281 deletions(-) create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs new file mode 100644 index 0000000..6a8f50e --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs @@ -0,0 +1,198 @@ +namespace ConfusedPolarBear.Plugin.IntroSkipper; + +using System; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using Microsoft.Extensions.Logging; + +/// +/// Common code shared by all media item analyzer tasks. +/// +public class BaseItemAnalyzerTask +{ + private readonly AnalysisMode _analysisMode; + + private readonly ILogger _logger; + + private readonly ILoggerFactory _loggerFactory; + + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Analysis mode. + /// Task logger. + /// Logger factory. + /// Library manager. + public BaseItemAnalyzerTask( + AnalysisMode mode, + ILogger logger, + ILoggerFactory loggerFactory, + ILibraryManager libraryManager) + { + _analysisMode = mode; + _logger = logger; + _loggerFactory = loggerFactory; + _libraryManager = libraryManager; + + if (mode == AnalysisMode.Introduction) + { + EdlManager.Initialize(_logger); + } + } + + /// + /// Analyze all media items on the server. + /// + /// Progress. + /// Cancellation token. + public void AnalyzeItems( + IProgress progress, + CancellationToken cancellationToken) + { + var queueManager = new QueueManager( + _loggerFactory.CreateLogger(), + _libraryManager); + + var queue = queueManager.GetMediaItems(); + + var totalQueued = 0; + foreach (var kvp in queue) + { + totalQueued += kvp.Value.Count; + } + + if (totalQueued == 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."); + } + + if (this._analysisMode == AnalysisMode.Introduction) + { + EdlManager.LogConfiguration(); + } + + var totalProcessed = 0; + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism + }; + + Parallel.ForEach(queue, options, (season) => + { + var writeEdl = false; + + // Since the first run of the task can run for multiple hours, ensure that none + // of the current media items were deleted from Jellyfin since the task was started. + var (episodes, unanalyzed) = queueManager.VerifyQueue( + season.Value.AsReadOnly(), + this._analysisMode); + + if (episodes.Count == 0) + { + return; + } + + var first = episodes[0]; + + if (!unanalyzed) + { + _logger.LogDebug( + "All episodes in {Name} season {Season} have already been analyzed", + first.SeriesName, + first.SeasonNumber); + + return; + } + + try + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var analyzed = AnalyzeItems(episodes, cancellationToken); + Interlocked.Add(ref totalProcessed, analyzed); + + writeEdl = analyzed > 0 || Plugin.Instance!.Configuration.RegenerateEdlFiles; + } + catch (FingerprintException ex) + { + _logger.LogWarning( + "Unable to analyze {Series} season {Season}: unable to fingerprint: {Ex}", + first.SeriesName, + first.SeasonNumber, + ex); + } + + if ( + writeEdl && + Plugin.Instance!.Configuration.EdlAction != EdlAction.None && + _analysisMode == AnalysisMode.Introduction) + { + EdlManager.UpdateEDLFiles(episodes); + } + + progress.Report((totalProcessed * 100) / totalQueued); + }); + + if ( + _analysisMode == AnalysisMode.Introduction && + Plugin.Instance!.Configuration.RegenerateEdlFiles) + { + _logger.LogInformation("Turning EDL file regeneration flag off"); + Plugin.Instance!.Configuration.RegenerateEdlFiles = false; + Plugin.Instance!.SaveConfiguration(); + } + } + + /// + /// Analyze a group of media items for skippable segments. + /// + /// Media items to analyze. + /// Cancellation token. + /// Number of items that were successfully analyzed. + private int AnalyzeItems( + ReadOnlyCollection items, + CancellationToken cancellationToken) + { + var totalItems = items.Count; + + // Only analyze specials (season 0) if the user has opted in. + var first = items[0]; + if (first.SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero) + { + return 0; + } + + _logger.LogInformation( + "Analyzing {Count} files from {Name} season {Season}", + items.Count, + first.SeriesName, + first.SeasonNumber); + + var analyzers = new Collection(); + + analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger())); + analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger())); + + if (this._analysisMode == AnalysisMode.Credits) + { + analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger())); + } + + // Use each analyzer to find skippable ranges in all media files, removing successfully + // analyzed items from the queue. + foreach (var analyzer in analyzers) + { + items = analyzer.AnalyzeMediaFiles(items, this._analysisMode, cancellationToken); + } + + return totalItems - items.Count; + } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs index fa05f34..8e3bd9c 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; @@ -11,14 +10,13 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper; /// /// Analyze all television episodes for credits. +/// TODO: analyze all media files. /// public class DetectCreditsTask : IScheduledTask { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly ILibraryManager? _libraryManager; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. @@ -27,19 +25,10 @@ public class DetectCreditsTask : IScheduledTask /// 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) + ILibraryManager libraryManager) { - _logger = loggerFactory.CreateLogger(); _loggerFactory = loggerFactory; + _libraryManager = libraryManager; } /// @@ -72,125 +61,20 @@ public Task ExecuteAsync(IProgress progress, CancellationToken cancellat { if (_libraryManager is null) { - throw new InvalidOperationException("Library manager must not be null"); + throw new InvalidOperationException("Library manager was null"); } - // Make sure the analysis queue matches what's currently in Jellyfin. - var queueManager = new QueueManager( - _loggerFactory.CreateLogger(), + var baseAnalyzer = new BaseItemAnalyzerTask( + AnalysisMode.Credits, + _loggerFactory.CreateLogger(), + _loggerFactory, _libraryManager); - var queue = queueManager.GetMediaItems(); - - 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 - }; - - // Analyze all episodes in the queue using the degrees of parallelism the user specified. - Parallel.ForEach(queue, options, (season) => - { - var (episodes, unanalyzed) = queueManager.VerifyQueue( - season.Value.AsReadOnly(), - AnalysisMode.Credits); - - if (episodes.Count == 0 || unanalyzed) - { - 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); - } - - var total = Plugin.Instance!.TotalQueued; - if (total > 0) - { - progress.Report((totalProcessed * 100) / total); - } - }); + baseAnalyzer.AnalyzeItems(progress, cancellationToken); 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[] - { - 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. /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntroductionsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntroductionsTask.cs index 23890a0..c0f1609 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntroductionsTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntroductionsTask.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; @@ -15,11 +13,9 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper; /// public class DetectIntroductionsTask : IScheduledTask { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly ILibraryManager? _libraryManager; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. @@ -28,21 +24,10 @@ public class DetectIntroductionsTask : IScheduledTask /// Library manager. public DetectIntroductionsTask( ILoggerFactory loggerFactory, - ILibraryManager libraryManager) : this(loggerFactory) + ILibraryManager libraryManager) { - _libraryManager = libraryManager; - } - - /// - /// Initializes a new instance of the class. - /// - /// Logger factory. - public DetectIntroductionsTask(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); _loggerFactory = loggerFactory; - - EdlManager.Initialize(_logger); + _libraryManager = libraryManager; } /// @@ -75,151 +60,20 @@ public Task ExecuteAsync(IProgress progress, CancellationToken cancellat { if (_libraryManager is null) { - throw new InvalidOperationException("Library manager must not be null"); + throw new InvalidOperationException("Library manager was null"); } - // Make sure the analysis queue matches what's currently in Jellyfin. - var queueManager = new QueueManager( - _loggerFactory.CreateLogger(), + var baseAnalyzer = new BaseItemAnalyzerTask( + AnalysisMode.Introduction, + _loggerFactory.CreateLogger(), + _loggerFactory, _libraryManager); - var queue = queueManager.GetMediaItems(); - - 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."); - } - - // Log EDL settings - EdlManager.LogConfiguration(); - - var totalProcessed = 0; - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism - }; - - // Analyze all episodes in the queue using the degrees of parallelism the user specified. - Parallel.ForEach(queue, options, (season) => - { - // Since the first run of the task can run for multiple hours, ensure that none - // of the current media items were deleted from Jellyfin since the task was started. - var (episodes, unanalyzed) = queueManager.VerifyQueue( - season.Value.AsReadOnly(), - AnalysisMode.Introduction); - - if (episodes.Count == 0) - { - return; - } - - var first = episodes[0]; - var writeEdl = false; - - if (!unanalyzed) - { - _logger.LogDebug( - "All episodes in {Name} season {Season} have already been analyzed", - first.SeriesName, - first.SeasonNumber); - - return; - } - - try - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - // Increment totalProcessed by the number of episodes in this season that were actually analyzed - // (instead of just using the number of episodes in the current season). - var analyzed = AnalyzeSeason(episodes, cancellationToken); - Interlocked.Add(ref totalProcessed, analyzed); - writeEdl = analyzed > 0 || Plugin.Instance!.Configuration.RegenerateEdlFiles; - } - 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); - } - - if (writeEdl && Plugin.Instance!.Configuration.EdlAction != EdlAction.None) - { - EdlManager.UpdateEDLFiles(episodes); - } - - var total = Plugin.Instance!.TotalQueued; - if (total > 0) - { - progress.Report((totalProcessed * 100) / total); - } - }); - - // Turn the regenerate EDL flag off after the scan completes. - if (Plugin.Instance!.Configuration.RegenerateEdlFiles) - { - _logger.LogInformation("Turning EDL file regeneration flag off"); - Plugin.Instance!.Configuration.RegenerateEdlFiles = false; - Plugin.Instance!.SaveConfiguration(); - } + baseAnalyzer.AnalyzeItems(progress, cancellationToken); return Task.CompletedTask; } - /// - /// Fingerprints all episodes in the provided season and stores the timestamps of all introductions. - /// - /// Episodes in this season. - /// Cancellation token provided by the scheduled task. - /// Number of episodes from the provided season that were analyzed. - private int AnalyzeSeason( - ReadOnlyCollection episodes, - CancellationToken cancellationToken) - { - // Skip seasons with an insufficient number of episodes. - if (episodes.Count <= 1) - { - return episodes.Count; - } - - // Only analyze specials (season 0) if the user has opted in. - var first = episodes[0]; - if (first.SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero) - { - return 0; - } - - _logger.LogInformation( - "Analyzing {Count} episodes from {Name} season {Season}", - episodes.Count, - first.SeriesName, - first.SeasonNumber); - - // Chapter analyzer - var chapter = new ChapterAnalyzer(_loggerFactory.CreateLogger()); - episodes = chapter.AnalyzeMediaFiles(episodes, AnalysisMode.Introduction, cancellationToken); - - // Analyze the season with Chromaprint - var chromaprint = new ChromaprintAnalyzer(_loggerFactory.CreateLogger()); - chromaprint.AnalyzeMediaFiles(episodes, AnalysisMode.Introduction, cancellationToken); - - return episodes.Count; - } - /// /// Get task triggers. ///