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.
///