From 9388f2a58336cc8bed2dd4021e357e726fc882ef Mon Sep 17 00:00:00 2001 From: rlauuzo <46294892+rlauuzo@users.noreply.github.com> Date: Sat, 15 Jun 2024 13:16:47 +0200 Subject: [PATCH] refactor item queue (#183) Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com> --- .../Analyzers/BlackFrameAnalyzer.cs | 18 ++--- .../Analyzers/ChapterAnalyzer.cs | 20 ++--- .../Analyzers/ChromaprintAnalyzer.cs | 44 ++++++++--- .../Configuration/PluginConfiguration.cs | 5 -- .../Controllers/SkipIntroController.cs | 5 +- .../Controllers/VisualizationController.cs | 1 + .../Data/EpisodeState.cs | 50 ++++++++++++ .../Data/QueuedEpisode.cs | 10 +++ .../Entrypoint.cs | 31 ++------ .../FFmpegWrapper.cs | 3 +- .../Plugin.cs | 25 ++++++ .../QueueManager.cs | 76 +++++++++---------- .../ScheduledTasks/BaseItemAnalyzerTask.cs | 50 ++++++++++-- .../ScheduledTasks/DetectCreditsTask.cs | 1 - .../ScheduledTasks/DetectIntrosCreditsTask.cs | 1 - .../ScheduledTasks/DetectIntrosTask.cs | 1 - 16 files changed, 231 insertions(+), 110 deletions(-) create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/Data/EpisodeState.cs diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs index 7c720b3..8925684 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs @@ -51,13 +51,15 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer var creditTimes = new Dictionary(); + var episodeAnalysisQueue = new List(analysisQueue); + bool isFirstEpisode = true; double searchStart = minimumCreditsDuration; var searchDistance = 2 * minimumCreditsDuration; - foreach (var episode in analysisQueue) + foreach (var episode in episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode))) { if (cancellationToken.IsCancellationRequested) { @@ -96,13 +98,13 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer isFirstEpisode = false; } - var intro = AnalyzeMediaFile( + var credit = AnalyzeMediaFile( episode, searchStart, searchDistance, blackFrameMinimumPercentage); - if (intro is null) + if (credit is null) { // If no credits were found, reset the first-episode search logic for the next episode in the sequence. searchStart = minimumCreditsDuration; @@ -110,17 +112,15 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer continue; } - searchStart = episode.Duration - intro.IntroStart + (0.5 * searchDistance); + searchStart = episode.Duration - credit.IntroStart + (0.5 * searchDistance); - creditTimes[episode.EpisodeId] = intro; + creditTimes.Add(episode.EpisodeId, credit); + episode.State.SetAnalyzed(mode, true); } Plugin.Instance!.UpdateTimestamps(creditTimes, mode); - return analysisQueue - .Where(x => !creditTimes.ContainsKey(x.EpisodeId)) - .ToList() - .AsReadOnly(); + return episodeAnalysisQueue.AsReadOnly(); } /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs index d479eeb..12509eb 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs @@ -35,6 +35,9 @@ public class ChapterAnalyzer : IMediaFileAnalyzer { var skippableRanges = new Dictionary(); + // Episode analysis queue. + var episodeAnalysisQueue = new List(analysisQueue); + var expression = mode == AnalysisMode.Introduction ? Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern : Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern; @@ -44,7 +47,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer return analysisQueue; } - foreach (var episode in analysisQueue) + foreach (var episode in episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode))) { if (cancellationToken.IsCancellationRequested) { @@ -63,14 +66,12 @@ public class ChapterAnalyzer : IMediaFileAnalyzer } skippableRanges.Add(episode.EpisodeId, skipRange); + episode.State.SetAnalyzed(mode, true); } Plugin.Instance.UpdateTimestamps(skippableRanges, mode); - return analysisQueue - .Where(x => !skippableRanges.ContainsKey(x.EpisodeId)) - .ToList() - .AsReadOnly(); + return episodeAnalysisQueue.AsReadOnly(); } /// @@ -92,10 +93,11 @@ public class ChapterAnalyzer : IMediaFileAnalyzer var config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); - var minDuration = config.MinimumIntroDuration; - int maxDuration = mode == AnalysisMode.Introduction ? - config.MaximumIntroDuration : - config.MaximumCreditsDuration; + var (minDuration, maxDuration) = mode switch + { + AnalysisMode.Introduction => (config.MinimumIntroDuration, config.MaximumIntroDuration), + _ => (config.MinimumCreditsDuration, config.MaximumCreditsDuration) + }; if (chapters.Count == 0) { diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChromaprintAnalyzer.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChromaprintAnalyzer.cs index 813c89d..5996794 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChromaprintAnalyzer.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChromaprintAnalyzer.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; +using System.Linq; using System.Numerics; using System.Threading; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; @@ -61,16 +63,36 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer // Cache of all fingerprints for this season. var fingerprintCache = new Dictionary(); - // Episode analysis queue. + // Episode analysis queue based on not analyzed episodes var episodeAnalysisQueue = new List(analysisQueue); // Episodes that were analyzed and do not have an introduction. - var episodesWithoutIntros = new List(); + var episodesWithoutIntros = episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)).ToList(); this._analysisMode = mode; + if (episodesWithoutIntros.Count == 0 || episodeAnalysisQueue.Count <= 1) + { + return analysisQueue; + } + + var episodesWithFingerprint = new List(episodesWithoutIntros); + + // Load fingerprints from cache if available. + episodesWithFingerprint.AddRange(episodeAnalysisQueue.Where(e => e.State.IsAnalyzed(mode) && File.Exists(FFmpegWrapper.GetFingerprintCachePath(e, mode)))); + + // Ensure at least two fingerprints are present. + if (episodesWithFingerprint.Count == 1) + { + var indexInAnalysisQueue = episodeAnalysisQueue.FindIndex(episode => episode == episodesWithoutIntros[0]); + episodesWithFingerprint.AddRange(episodeAnalysisQueue + .Where((episode, index) => Math.Abs(index - indexInAnalysisQueue) <= 1 && index != indexInAnalysisQueue)); + } + + seasonIntros = episodesWithFingerprint.Where(e => e.State.IsAnalyzed(mode)).ToDictionary(e => e.EpisodeId, e => Plugin.Instance!.GetIntroByMode(e.EpisodeId, mode)); + // Compute fingerprints for all episodes in the season - foreach (var episode in episodeAnalysisQueue) + foreach (var episode in episodesWithFingerprint) { try { @@ -98,14 +120,15 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer } // While there are still episodes in the queue - while (episodeAnalysisQueue.Count > 0) + while (episodesWithoutIntros.Count > 0) { // Pop the first episode from the queue - var currentEpisode = episodeAnalysisQueue[0]; - episodeAnalysisQueue.RemoveAt(0); + var currentEpisode = episodesWithoutIntros[0]; + episodesWithoutIntros.RemoveAt(0); + episodesWithFingerprint.Remove(currentEpisode); // Search through all remaining episodes. - foreach (var remainingEpisode in episodeAnalysisQueue) + foreach (var remainingEpisode in episodesWithFingerprint) { // Compare the current episode to all remaining episodes in the queue. var (currentIntro, remainingIntro) = CompareEpisodes( @@ -167,9 +190,10 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer } // If no intro is found at this point, the popped episode is not reinserted into the queue. - if (!seasonIntros.ContainsKey(currentEpisode.EpisodeId)) + if (seasonIntros.ContainsKey(currentEpisode.EpisodeId)) { - episodesWithoutIntros.Add(currentEpisode); + episodesWithFingerprint.Add(currentEpisode); + episodeAnalysisQueue.FirstOrDefault(x => x.EpisodeId == currentEpisode.EpisodeId)?.State.SetAnalyzed(mode, true); } } @@ -187,7 +211,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer Plugin.Instance!.UpdateTimestamps(seasonIntros, this._analysisMode); - return episodesWithoutIntros.AsReadOnly(); + return episodeAnalysisQueue.AsReadOnly(); } /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs index 43fd7b0..70a6d2e 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs @@ -28,11 +28,6 @@ public class PluginConfiguration : BasePluginConfiguration /// public string SelectedLibraries { get; set; } = string.Empty; - /// - /// Gets a temporary limitation on file paths to be analyzed. Should be empty when automatic scan is idle. - /// - public IList PathRestrictions { get; } = new List(); - /// /// Gets or sets a value indicating whether to scan for intros during a scheduled task. /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs index 9e60310..5c81f0a 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs @@ -80,9 +80,7 @@ public class SkipIntroController : ControllerBase { try { - var timestamp = mode == AnalysisMode.Introduction ? - Plugin.Instance!.Intros[id] : - Plugin.Instance!.Credits[id]; + var timestamp = Plugin.Instance!.GetIntroByMode(id, mode); // Operate on a copy to avoid mutating the original Intro object stored in the dictionary. var segment = new Intro(timestamp); @@ -135,6 +133,7 @@ public class SkipIntroController : ControllerBase FFmpegWrapper.DeleteCacheFiles(mode); } + Plugin.Instance!.EpisodeStates.Clear(); Plugin.Instance!.SaveTimestamps(mode); return NoContent(); } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs index a9e2569..e9287a1 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs @@ -143,6 +143,7 @@ public class VisualizationController : ControllerBase { Plugin.Instance!.Intros.TryRemove(e.EpisodeId, out _); Plugin.Instance!.Credits.TryRemove(e.EpisodeId, out _); + e.State.ResetStates(); if (eraseCache) { FFmpegWrapper.DeleteEpisodeCache(e.EpisodeId); diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/EpisodeState.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/EpisodeState.cs new file mode 100644 index 0000000..ebc809b --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/EpisodeState.cs @@ -0,0 +1,50 @@ +using System; + +namespace ConfusedPolarBear.Plugin.IntroSkipper; + +/// +/// Represents the state of an episode regarding analysis and blacklist status. +/// +public class EpisodeState +{ + private readonly bool[] _analyzedStates = new bool[2]; + + private readonly bool[] _blacklistedStates = new bool[2]; + + /// + /// Checks if the specified analysis mode has been analyzed. + /// + /// The analysis mode to check. + /// True if the mode has been analyzed, false otherwise. + public bool IsAnalyzed(AnalysisMode mode) => _analyzedStates[(int)mode]; + + /// + /// Sets the analyzed state for the specified analysis mode. + /// + /// The analysis mode to set. + /// The analyzed state to set. + public void SetAnalyzed(AnalysisMode mode, bool value) => _analyzedStates[(int)mode] = value; + + /// + /// Checks if the specified analysis mode has been blacklisted. + /// + /// The analysis mode to check. + /// True if the mode has been blacklisted, false otherwise. + public bool IsBlacklisted(AnalysisMode mode) => _blacklistedStates[(int)mode]; + + /// + /// Sets the blacklisted state for the specified analysis mode. + /// + /// The analysis mode to set. + /// The blacklisted state to set. + public void SetBlacklisted(AnalysisMode mode, bool value) => _blacklistedStates[(int)mode] = value; + + /// + /// Resets the analyzed states. + /// + public void ResetStates() + { + Array.Clear(_analyzedStates); + Array.Clear(_blacklistedStates); + } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs index bb65789..7f5a6a9 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs @@ -22,6 +22,11 @@ public class QueuedEpisode /// public Guid EpisodeId { get; set; } + /// + /// Gets the state of the episode. + /// + public EpisodeState State => Plugin.Instance!.GetState(EpisodeId); + /// /// Gets or sets the full path to episode. /// @@ -32,6 +37,11 @@ public class QueuedEpisode /// public string Name { get; set; } = string.Empty; + /// + /// Gets or sets a value indicating whether an episode is Anime. + /// + public bool IsAnime { get; set; } = false; + /// /// Gets or sets the timestamp (in seconds) to stop searching for an introduction at. /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs index 3d22d1c..169636c 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs @@ -23,10 +23,9 @@ public class Entrypoint : IHostedService, IDisposable private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly object _pathRestrictionsLock = new(); private Timer _queueTimer; private bool _analyzeAgain; - private List _pathRestrictions = new List(); + private HashSet _seasonsToAnalyze = new HashSet(); private static CancellationTokenSource? _cancellationTokenSource; private static ManualResetEventSlim _autoTaskCompletEvent = new ManualResetEventSlim(false); @@ -140,7 +139,7 @@ public class Entrypoint : IHostedService, IDisposable } // Don't do anything if it's not a supported media type - if (itemChangeEventArgs.Item is not Episode) + if (itemChangeEventArgs.Item is not Episode episode) { return; } @@ -150,10 +149,7 @@ public class Entrypoint : IHostedService, IDisposable return; } - lock (_pathRestrictionsLock) - { - _pathRestrictions.Add(itemChangeEventArgs.Item.ContainingFolderPath); - } + _seasonsToAnalyze.Add(episode.SeasonId); StartTimer(); } @@ -172,7 +168,7 @@ public class Entrypoint : IHostedService, IDisposable } // Don't do anything if it's not a supported media type - if (itemChangeEventArgs.Item is not Episode) + if (itemChangeEventArgs.Item is not Episode episode) { return; } @@ -182,10 +178,7 @@ public class Entrypoint : IHostedService, IDisposable return; } - lock (_pathRestrictionsLock) - { - _pathRestrictions.Add(itemChangeEventArgs.Item.ContainingFolderPath); - } + _seasonsToAnalyze.Add(episode.SeasonId); StartTimer(); } @@ -255,7 +248,6 @@ public class Entrypoint : IHostedService, IDisposable } // Clean up - Plugin.Instance!.Configuration.PathRestrictions.Clear(); _cancellationTokenSource = null; _autoTaskCompletEvent.Set(); } @@ -271,15 +263,8 @@ public class Entrypoint : IHostedService, IDisposable using (_cancellationTokenSource = new CancellationTokenSource()) using (ScheduledTaskSemaphore.Acquire(-1, _cancellationTokenSource.Token)) { - lock (_pathRestrictionsLock) - { - foreach (var path in _pathRestrictions) - { - Plugin.Instance!.Configuration.PathRestrictions.Add(path); - } - - _pathRestrictions.Clear(); - } + var seasonIds = new HashSet(_seasonsToAnalyze); + _seasonsToAnalyze.Clear(); _analyzeAgain = false; var progress = new Progress(); @@ -309,7 +294,7 @@ public class Entrypoint : IHostedService, IDisposable _loggerFactory, _libraryManager); - baseCreditAnalyzer.AnalyzeItems(progress, _cancellationTokenSource.Token); + baseCreditAnalyzer.AnalyzeItems(progress, _cancellationTokenSource.Token, seasonIds); // New item detected, start timer again if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested) diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs index dac33f2..e2e19db 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs @@ -646,7 +646,8 @@ public static class FFmpegWrapper /// /// Episode. /// Analysis mode. - private static string GetFingerprintCachePath(QueuedEpisode episode, AnalysisMode mode) + /// Path. + public static string GetFingerprintCachePath(QueuedEpisode episode, AnalysisMode mode) { var basePath = Path.Join( Plugin.Instance!.FingerprintCachePath, diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs index f2dcdf5..ba9a783 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs @@ -175,6 +175,11 @@ public class Plugin : BasePlugin, IHasWebPages /// public ConcurrentDictionary> QueuedMediaItems { get; } = new(); + /// + /// Gets all episode states. + /// + public ConcurrentDictionary EpisodeStates { get; } = new(); + /// /// Gets or sets the total number of episodes in the queue. /// @@ -316,6 +321,19 @@ public class Plugin : BasePlugin, IHasWebPages return commit; } + /// + /// Gets the Intro for this item. + /// + /// Item id. + /// Mode. + /// Intro. + internal Intro GetIntroByMode(Guid id, AnalysisMode mode) + { + return mode == AnalysisMode.Introduction + ? Instance!.Intros[id] + : Instance!.Credits[id]; + } + internal BaseItem? GetItem(Guid id) { return _libraryManager.GetItemById(id); @@ -357,6 +375,13 @@ public class Plugin : BasePlugin, IHasWebPages return _itemRepository.GetChapters(item); } + /// + /// Gets the state for this item. + /// + /// Item ID. + /// State of this item. + internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState()); + internal void UpdateTimestamps(Dictionary newTimestamps, AnalysisMode mode) { foreach (var intro in newTimestamps) diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs index 8b6e5fd..ad156e2 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs @@ -160,14 +160,6 @@ public class QueueManager continue; } - if (Plugin.Instance!.Configuration.PathRestrictions.Count > 0) - { - if (!Plugin.Instance.Configuration.PathRestrictions.Contains(item.ContainingFolderPath)) - { - continue; - } - } - QueueEpisode(episode); } @@ -176,10 +168,7 @@ public class QueueManager private void QueueEpisode(Episode episode) { - if (Plugin.Instance is null) - { - throw new InvalidOperationException("plugin instance was null"); - } + var pluginInstance = Plugin.Instance ?? throw new InvalidOperationException("Plugin instance was null"); if (string.IsNullOrEmpty(episode.Path)) { @@ -192,9 +181,13 @@ public class QueueManager } // Allocate a new list for each new season - _queuedEpisodes.TryAdd(episode.SeasonId, new List()); + if (!_queuedEpisodes.TryGetValue(episode.SeasonId, out var seasonEpisodes)) + { + seasonEpisodes = new List(); + _queuedEpisodes[episode.SeasonId] = seasonEpisodes; + } - if (_queuedEpisodes[episode.SeasonId].Any(e => e.EpisodeId == episode.Id)) + if (seasonEpisodes.Any(e => e.EpisodeId == episode.Id)) { _logger.LogDebug( "\"{Name}\" from series \"{Series}\" ({Id}) is already queued", @@ -207,32 +200,26 @@ public class QueueManager // Limit analysis to the first X% of the episode and at most Y minutes. // X and Y default to 25% and 10 minutes. var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds; - var fingerprintDuration = duration; - - if (fingerprintDuration >= 5 * 60) - { - fingerprintDuration *= analysisPercent; - } - - fingerprintDuration = Math.Min( - fingerprintDuration, - 60 * Plugin.Instance.Configuration.AnalysisLengthLimit); + var fingerprintDuration = Math.Min( + duration >= 5 * 60 ? duration * analysisPercent : duration, + 60 * pluginInstance.Configuration.AnalysisLengthLimit); // Queue the episode for analysis - var maxCreditsDuration = Plugin.Instance.Configuration.MaximumCreditsDuration; + var maxCreditsDuration = pluginInstance.Configuration.MaximumCreditsDuration; _queuedEpisodes[episode.SeasonId].Add(new QueuedEpisode { SeriesName = episode.SeriesName, SeasonNumber = episode.AiredSeasonNumber ?? 0, EpisodeId = episode.Id, Name = episode.Name, + IsAnime = episode.GetInheritedTags().Contains("anime", StringComparer.OrdinalIgnoreCase), Path = episode.Path, Duration = Convert.ToInt32(duration), IntroFingerprintEnd = Convert.ToInt32(fingerprintDuration), CreditsFingerprintStart = Convert.ToInt32(duration - maxCreditsDuration), }); - Plugin.Instance.TotalQueued++; + pluginInstance.TotalQueued++; } /// @@ -246,10 +233,7 @@ public class QueueManager VerifyQueue(ReadOnlyCollection candidates, ReadOnlyCollection modes) { var verified = new List(); - var reqModes = new List(); - - var requiresIntroAnalysis = modes.Contains(AnalysisMode.Introduction); - var requiresCreditsAnalysis = modes.Contains(AnalysisMode.Credits); + var reqModes = new HashSet(); foreach (var candidate in candidates) { @@ -257,34 +241,44 @@ public class QueueManager { var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId); - if (File.Exists(path)) + if (!File.Exists(path)) { - verified.Add(candidate); + continue; + } - if (requiresIntroAnalysis && (!Plugin.Instance!.Intros.TryGetValue(candidate.EpisodeId, out var intro) || !intro.Valid)) + verified.Add(candidate); + + foreach (var mode in modes) + { + if (candidate.State.IsAnalyzed(mode) || candidate.State.IsBlacklisted(mode)) { - reqModes.Add(AnalysisMode.Introduction); - requiresIntroAnalysis = false; // No need to check again + continue; } - if (requiresCreditsAnalysis && (!Plugin.Instance!.Credits.TryGetValue(candidate.EpisodeId, out var credit) || !credit.Valid)) + bool isAnalyzed = mode == AnalysisMode.Introduction + ? Plugin.Instance!.Intros.ContainsKey(candidate.EpisodeId) + : Plugin.Instance!.Credits.ContainsKey(candidate.EpisodeId); + + if (isAnalyzed) { - reqModes.Add(AnalysisMode.Credits); - requiresCreditsAnalysis = false; // No need to check again + candidate.State.SetAnalyzed(mode, true); + } + else + { + reqModes.Add(mode); } } } catch (Exception ex) { _logger.LogDebug( - "Skipping {Mode} analysis of {Name} ({Id}): {Exception}", - modes, + "Skipping analysis of {Name} ({Id}): {Exception}", candidate.Name, candidate.EpisodeId, ex); } } - return (verified.AsReadOnly(), reqModes.AsReadOnly()); + return (verified.AsReadOnly(), reqModes.ToList().AsReadOnly()); } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs index 0750659..7556dae 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; @@ -50,9 +51,11 @@ public class BaseItemAnalyzerTask /// /// Progress. /// Cancellation token. + /// Season Ids to analyze. public void AnalyzeItems( IProgress progress, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + HashSet? seasonsToAnalyze = null) { var ffmpegValid = FFmpegWrapper.CheckFFmpegVersion(); // Assert that ffmpeg with chromaprint is installed @@ -68,6 +71,13 @@ public class BaseItemAnalyzerTask var queue = queueManager.GetMediaItems(); + // Filter the queue based on seasonsToAnalyze + if (seasonsToAnalyze != null && seasonsToAnalyze.Count > 0) + { + queue = queue.Where(kvp => seasonsToAnalyze.Contains(kvp.Key)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value).AsReadOnly(); + } + var totalQueued = 0; foreach (var kvp in queue) { @@ -194,6 +204,12 @@ public class BaseItemAnalyzerTask return 0; } + // Remove from Blacklist + foreach (var item in items.Where(e => e.State.IsBlacklisted(mode))) + { + item.State.SetBlacklisted(mode, false); + } + _logger.LogInformation( "[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}", mode, @@ -204,14 +220,29 @@ public class BaseItemAnalyzerTask var analyzers = new Collection(); analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger())); - if (mode == AnalysisMode.Credits) + if (first.IsAnime) { - analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger())); - } + if (Plugin.Instance!.Configuration.UseChromaprint) + { + analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger())); + } - if (Plugin.Instance!.Configuration.UseChromaprint) + if (mode == AnalysisMode.Credits) + { + analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger())); + } + } + else { - analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger())); + if (mode == AnalysisMode.Credits) + { + analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger())); + } + + if (Plugin.Instance!.Configuration.UseChromaprint) + { + analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger())); + } } // Use each analyzer to find skippable ranges in all media files, removing successfully @@ -221,6 +252,13 @@ public class BaseItemAnalyzerTask items = analyzer.AnalyzeMediaFiles(items, mode, cancellationToken); } + // Add items without intros/credits to blacklist. + foreach (var item in items.Where(e => !e.State.IsAnalyzed(mode))) + { + item.State.SetBlacklisted(mode, true); + totalItems -= 1; + } + return totalItems; } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs index 825708e..1d642d3 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs @@ -81,7 +81,6 @@ public class DetectCreditsTask : IScheduledTask { _logger.LogInformation("Scheduled Task is starting"); - Plugin.Instance!.Configuration.PathRestrictions.Clear(); var modes = new List { AnalysisMode.Credits }; var baseCreditAnalyzer = new BaseItemAnalyzerTask( diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs index 126a232..d629937 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs @@ -80,7 +80,6 @@ public class DetectIntrosCreditsTask : IScheduledTask { _logger.LogInformation("Scheduled Task is starting"); - Plugin.Instance!.Configuration.PathRestrictions.Clear(); var modes = new List { AnalysisMode.Introduction, AnalysisMode.Credits }; var baseIntroAnalyzer = new BaseItemAnalyzerTask( diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs index 0f7cfbf..dbded69 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs @@ -80,7 +80,6 @@ public class DetectIntrosTask : IScheduledTask { _logger.LogInformation("Scheduled Task is starting"); - Plugin.Instance!.Configuration.PathRestrictions.Clear(); var modes = new List { AnalysisMode.Introduction }; var baseIntroAnalyzer = new BaseItemAnalyzerTask(