diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs index adf5835..877b8d7 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs @@ -64,8 +64,7 @@ public class TestAudioFingerprinting [Fact] public void TestIntroDetection() { - var logger = new Logger(new LoggerFactory()); - var task = new FingerprinterTask(logger); + var task = new FingerprinterTask(new LoggerFactory()); var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3"); var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3"); diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs index 88f0200..5c7543c 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs @@ -1,14 +1,8 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Library; using Microsoft.Extensions.Logging; namespace ConfusedPolarBear.Plugin.IntroSkipper; @@ -22,8 +16,7 @@ public class Entrypoint : IServerEntryPoint private readonly IUserViewManager _userViewManager; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; - - private readonly object _queueLock = new object(); + private readonly ILoggerFactory _loggerFactory; /// /// Initializes a new instance of the class. @@ -32,16 +25,19 @@ public class Entrypoint : IServerEntryPoint /// User view manager. /// Library manager. /// Logger. + /// Logger factory. public Entrypoint( IUserManager userManager, IUserViewManager userViewManager, ILibraryManager libraryManager, - ILogger logger) + ILogger logger, + ILoggerFactory loggerFactory) { _userManager = userManager; _userViewManager = userViewManager; _libraryManager = libraryManager; _logger = logger; + _loggerFactory = loggerFactory; } /// @@ -56,40 +52,15 @@ public class Entrypoint : IServerEntryPoint LogVersion(); #endif - // Assert that ffmpeg with chromaprint is installed - if (!Chromaprint.CheckFFmpegVersion()) - { - _logger.LogError("ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed"); - return Task.CompletedTask; - } + // TODO: when a new item is added to the server, immediately analyze the season it belongs to + // instead of waiting for the next task interval. The task start should be debounced by a few seconds. try { - // As soon as a new episode is added, queue it for later analysis. - _libraryManager.ItemAdded += ItemAdded; - - // For all TV show libraries, enqueue all contained items. - foreach (var folder in _libraryManager.GetVirtualFolders()) - { - if (folder.CollectionType != CollectionTypeOptions.TvShows) - { - continue; - } - - _logger.LogInformation( - "Running startup enqueue of items in library {Name} ({ItemId})", - folder.Name, - folder.ItemId); - - try - { - QueueLibraryContents(folder.ItemId); - } - catch (Exception ex) - { - _logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex); - } - } + // Enqueue all episodes at startup so the fingerprint visualizer works before the task is started. + _logger.LogInformation("Running startup enqueue"); + var queueManager = new QueueManager(_loggerFactory.CreateLogger(), _libraryManager); + queueManager.EnqueueAllEpisodes(); } catch (Exception ex) { @@ -101,114 +72,6 @@ public class Entrypoint : IServerEntryPoint return Task.CompletedTask; } - private void QueueLibraryContents(string rawId) - { - _logger.LogDebug("Constructing anonymous internal query"); - - var query = new InternalItemsQuery() - { - // Order by series name, season, and then episode number so that status updates are logged in order - ParentId = Guid.Parse(rawId), - OrderBy = new[] - { - ("SeriesSortName", SortOrder.Ascending), - ("ParentIndexNumber", SortOrder.Ascending), - ("IndexNumber", SortOrder.Ascending), - }, - IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode }, - Recursive = true, - }; - - _logger.LogDebug("Getting items"); - - var items = _libraryManager.GetItemList(query, false); - - if (items is null) - { - _logger.LogError("Library query result is null"); - return; - } - - // Queue all episodes on the server for fingerprinting. - _logger.LogDebug("Iterating through library items"); - - foreach (var item in items) - { - if (item is not Episode episode) - { - _logger.LogError("Item {Name} is not an episode", item.Name); - continue; - } - - QueueEpisode(episode); - } - - _logger.LogDebug("Queued {Count} episodes", items.Count); - } - - /// - /// Called when an item is added to the server. - /// - /// Sender. - /// ItemChangeEventArgs. - private void ItemAdded(object? sender, ItemChangeEventArgs e) - { - if (e.Item is not Episode episode) - { - return; - } - - _logger.LogDebug("Queuing fingerprint of new episode {Name}", episode.Name); - - QueueEpisode(episode); - } - - private void QueueEpisode(Episode episode) - { - if (Plugin.Instance is null) - { - throw new InvalidOperationException("plugin instance was null"); - } - - if (string.IsNullOrEmpty(episode.Path)) - { - _logger.LogWarning("Not queuing episode {Id} as no path was provided by Jellyfin", episode.Id); - return; - } - - lock (_queueLock) - { - var queue = Plugin.Instance.AnalysisQueue; - - // Allocate a new list for each new season - if (!queue.ContainsKey(episode.SeasonId)) - { - Plugin.Instance.AnalysisQueue[episode.SeasonId] = new List(); - } - - // Only fingerprint up to 25% of the episode and at most 10 minutes. - var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds; - if (duration >= 5 * 60) - { - duration /= 4; - } - - duration = Math.Min(duration, 10 * 60); - - Plugin.Instance.AnalysisQueue[episode.SeasonId].Add(new QueuedEpisode() - { - SeriesName = episode.SeriesName, - SeasonNumber = episode.AiredSeasonNumber ?? 0, - EpisodeId = episode.Id, - Name = episode.Name, - Path = episode.Path, - FingerprintDuration = Convert.ToInt32(duration) - }); - - Plugin.Instance!.TotalQueued++; - } - } - #if DEBUG /// /// Logs the exact commit that created this version of the plugin. Only used in unstable builds. @@ -262,7 +125,5 @@ public class Entrypoint : IServerEntryPoint { return; } - - _libraryManager.ItemAdded -= ItemAdded; } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs new file mode 100644 index 0000000..f241042 --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs @@ -0,0 +1,156 @@ +namespace ConfusedPolarBear.Plugin.IntroSkipper; + +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; + +/// +/// Manages enqueuing library items for analysis. +/// +public class QueueManager +{ + private ILibraryManager _libraryManager; + private ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Logger. + /// Library manager. + public QueueManager(ILogger logger, ILibraryManager libraryManager) + { + _logger = logger; + _libraryManager = libraryManager; + } + + /// + /// Iterates through all libraries on the server and queues all episodes for analysis. + /// + public void EnqueueAllEpisodes() + { + // Assert that ffmpeg with chromaprint is installed + if (!Chromaprint.CheckFFmpegVersion()) + { + throw new FingerprintException( + "ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed"); + } + + Plugin.Instance!.AnalysisQueue.Clear(); + + // For all TV show libraries, enqueue all contained items. + foreach (var folder in _libraryManager.GetVirtualFolders()) + { + if (folder.CollectionType != CollectionTypeOptions.TvShows) + { + continue; + } + + _logger.LogInformation( + "Running enqueue of items in library {Name} ({ItemId})", + folder.Name, + folder.ItemId); + + try + { + QueueLibraryContents(folder.ItemId); + } + catch (Exception ex) + { + _logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex); + } + } + } + + private void QueueLibraryContents(string rawId) + { + _logger.LogDebug("Constructing anonymous internal query"); + + var query = new InternalItemsQuery() + { + // Order by series name, season, and then episode number so that status updates are logged in order + ParentId = Guid.Parse(rawId), + OrderBy = new[] + { + ("SeriesSortName", SortOrder.Ascending), + ("ParentIndexNumber", SortOrder.Ascending), + ("IndexNumber", SortOrder.Ascending), + }, + IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode }, + Recursive = true, + }; + + _logger.LogDebug("Getting items"); + + var items = _libraryManager.GetItemList(query, false); + + if (items is null) + { + _logger.LogError("Library query result is null"); + return; + } + + // Queue all episodes on the server for fingerprinting. + _logger.LogDebug("Iterating through library items"); + + foreach (var item in items) + { + if (item is not Episode episode) + { + _logger.LogError("Item {Name} is not an episode", item.Name); + continue; + } + + QueueEpisode(episode); + } + + _logger.LogDebug("Queued {Count} episodes", items.Count); + } + + private void QueueEpisode(Episode episode) + { + if (Plugin.Instance is null) + { + throw new InvalidOperationException("plugin instance was null"); + } + + if (string.IsNullOrEmpty(episode.Path)) + { + _logger.LogWarning("Not queuing episode {Id} as no path was provided by Jellyfin", episode.Id); + return; + } + + var queue = Plugin.Instance.AnalysisQueue; + + // Allocate a new list for each new season + if (!queue.ContainsKey(episode.SeasonId)) + { + Plugin.Instance.AnalysisQueue[episode.SeasonId] = new List(); + } + + // Only fingerprint up to 25% of the episode and at most 10 minutes. + var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds; + if (duration >= 5 * 60) + { + duration /= 4; + } + + duration = Math.Min(duration, 10 * 60); + + Plugin.Instance.AnalysisQueue[episode.SeasonId].Add(new QueuedEpisode() + { + SeriesName = episode.SeriesName, + SeasonNumber = episode.AiredSeasonNumber ?? 0, + EpisodeId = episode.Id, + Name = episode.Name, + Path = episode.Path, + FingerprintDuration = Convert.ToInt32(duration) + }); + + Plugin.Instance!.TotalQueued++; + } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs index f3a2013..23e7f7b 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Numerics; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -47,6 +48,10 @@ public class FingerprinterTask : IScheduledTask private readonly ILogger _logger; + private readonly ILogger _queueLogger; + + private readonly ILibraryManager? _libraryManager; + /// /// Lock which guards the fingerprint cache dictionary. /// @@ -66,10 +71,22 @@ public class FingerprinterTask : IScheduledTask /// /// Initializes a new instance of the class. /// - /// Logger. - public FingerprinterTask(ILogger logger) + /// Logger factory. + /// Library manager. + public FingerprinterTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager) : this(loggerFactory) { - _logger = logger; + _libraryManager = libraryManager; + } + + /// + /// Initializes a new instance of the class. + /// + /// Logger factory. + public FingerprinterTask(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _queueLogger = loggerFactory.CreateLogger(); + _fingerprintCache = new Dictionary>(); } @@ -94,13 +111,22 @@ public class FingerprinterTask : IScheduledTask public string Key => "CPBIntroSkipperRunFingerprinter"; /// - /// Analyze all episodes in the queue. + /// 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(_queueLogger, _libraryManager); + queueManager.EnqueueAllEpisodes(); + var queue = Plugin.Instance!.AnalysisQueue; if (queue.Count == 0)