From 1a731e3acc47e68433a1131464cabf6780327fa5 Mon Sep 17 00:00:00 2001 From: rlauuzo <46294892+rlauuzo@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:49:47 +0200 Subject: [PATCH] Update MediaSegments directly (#350) Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com> Co-authored-by: Kilian von Pflugk --- .../Configuration/PluginConfiguration.cs | 14 + .../Configuration/configPage.html | 25 +- .../Manager/MediaSegmentUpdateManager.cs | 63 +++++ .../Plugin.cs | 2 +- .../PluginServiceRegistrator.cs | 2 + .../Providers/SegmentProvider.cs | 14 +- .../ScheduledTasks/BaseItemAnalyzerTask.cs | 41 +-- .../ScheduledTasks/DetectCreditsTask.cs | 49 ++-- .../ScheduledTasks/DetectIntrosCreditsTask.cs | 49 ++-- .../ScheduledTasks/DetectIntrosTask.cs | 49 ++-- .../ScheduledTasks/ScheduledTaskSemaphore.cs | 5 +- .../Services/Entrypoint.cs | 244 ++++++------------ 12 files changed, 285 insertions(+), 272 deletions(-) create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/Manager/MediaSegmentUpdateManager.cs diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs index 839dcca..f5d6eac 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs @@ -68,6 +68,20 @@ public class PluginConfiguration : BasePluginConfiguration /// public bool WithChromaprint { get; set; } = true; + // ===== Media Segment handling ===== + + /// + /// Gets or sets a value indicating whether to update Media Segments. + /// + public bool UpdateMediaSegments { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to regenerate all EDL files during the next scan. + /// By default, EDL files are only written for a season if the season had at least one newly analyzed episode. + /// If this is set, all EDL files will be regenerated and overwrite any existing EDL file. + /// + public bool RegenerateMediaSegments { get; set; } = true; + // ===== EDL handling ===== /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html index f8136f8..bfba8c2 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html @@ -153,6 +153,29 @@ +
+ Jellyfin Mediasegment Generation + +
+
+ + +
Enable this option to update media segments for newly added files during a scan. Warning: This should be disabled if you're using media segment providers other than Intro Skipper.
+
+ +
+ + +
When enabled, this option will overwrite all existing media segments for your episodes with currently detected introduction and credit timestamps during the next scan.
+
+
+
EDL File Generation @@ -671,7 +694,7 @@ "AutoSkipCreditsNotificationText", ]; - var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"]; + var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "RegenerateMediaSegments", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"]; // visualizer elements var ignorelistSection = document.querySelector("div#ignorelistSection"); diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Manager/MediaSegmentUpdateManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/MediaSegmentUpdateManager.cs new file mode 100644 index 0000000..f94a923 --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/MediaSegmentUpdateManager.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ConfusedPolarBear.Plugin.IntroSkipper.Data; +using MediaBrowser.Controller; +using MediaBrowser.Model; +using Microsoft.Extensions.Logging; + +namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager +{ + /// + /// Initializes a new instance of the class. + /// + /// MediaSegmentManager. + /// logger. + /// segmentProvider. + public class MediaSegmentUpdateManager(IMediaSegmentManager mediaSegmentManager, ILogger logger, IMediaSegmentProvider segmentProvider) + { + private readonly IMediaSegmentManager _mediaSegmentManager = mediaSegmentManager; + private readonly ILogger _logger = logger; + private readonly IMediaSegmentProvider _segmentProvider = segmentProvider; + private readonly string _name = Plugin.Instance!.Name; + + /// + /// Updates all media items in a List. + /// + /// Queued media items. + /// CancellationToken. + /// A representing the asynchronous operation. + public async Task UpdateMediaSegmentsAsync(IReadOnlyList episodes, CancellationToken cancellationToken) + { + foreach (var episode in episodes) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + var existingSegments = await _mediaSegmentManager.GetSegmentsAsync(episode.EpisodeId, null).ConfigureAwait(false); + var deleteTasks = existingSegments.Select(s => _mediaSegmentManager.DeleteSegmentAsync(s.Id)); + await Task.WhenAll(deleteTasks).ConfigureAwait(false); + + var newSegments = await _segmentProvider.GetMediaSegments(new MediaSegmentGenerationRequest { ItemId = episode.EpisodeId }, cancellationToken).ConfigureAwait(false); + + if (newSegments.Count == 0) + { + _logger.LogDebug("No segments found for episode {EpisodeId}", episode.EpisodeId); + continue; + } + + var createTasks = newSegments.Select(s => _mediaSegmentManager.CreateSegmentAsync(s, _name)); + await Task.WhenAll(createTasks).ConfigureAwait(false); + + _logger.LogDebug("Updated {SegmentCount} segments for episode {EpisodeId}", newSegments.Count, episode.EpisodeId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing episode {EpisodeId}", episode.EpisodeId); + } + } + } + } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs index 1e32252..0a6c6fa 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs @@ -416,7 +416,7 @@ public class Plugin : BasePlugin, IHasWebPages List oldRepos = [ "https://raw.githubusercontent.com/intro-skipper/intro-skipper/master/manifest.json", - "https://raw.githubusercontent.com/jumoog/intro-skipper/master/manifest.json" + "https://raw.githubusercontent.com/jumoog/intro-skipper/master/manifest.json" ]; // Access the current server configuration var config = serverConfiguration.Configuration; diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs b/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs index 89a856d..1370a3e 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs @@ -1,3 +1,4 @@ +using ConfusedPolarBear.Plugin.IntroSkipper.Manager; using ConfusedPolarBear.Plugin.IntroSkipper.Providers; using ConfusedPolarBear.Plugin.IntroSkipper.Services; using MediaBrowser.Controller; @@ -18,6 +19,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper serviceCollection.AddHostedService(); serviceCollection.AddHostedService(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs index bf859cf..74b4805 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs @@ -15,15 +15,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers ///
public class SegmentProvider : IMediaSegmentProvider { - private readonly long _remainingTicks; - - /// - /// Initializes a new instance of the class. - /// - public SegmentProvider() - { - _remainingTicks = TimeSpan.FromSeconds(Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2).Ticks; - } + private static long RemainingTicks => TimeSpan.FromSeconds(Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2).Ticks; /// public string Name => Plugin.Instance!.Name; @@ -38,7 +30,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers segments.Add(new MediaSegmentDto { StartTicks = TimeSpan.FromSeconds(introValue.Start).Ticks, - EndTicks = TimeSpan.FromSeconds(introValue.End).Ticks - _remainingTicks, + EndTicks = TimeSpan.FromSeconds(introValue.End).Ticks - RemainingTicks, ItemId = request.ItemId, Type = MediaSegmentType.Intro }); @@ -61,7 +53,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers } else { - outroSegment.EndTicks = creditEndTicks - _remainingTicks; + outroSegment.EndTicks = creditEndTicks - RemainingTicks; } segments.Add(outroSegment); diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs index c63c838..8edb543 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs @@ -18,12 +18,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; public class BaseItemAnalyzerTask { private readonly IReadOnlyCollection _analysisModes; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly ILibraryManager _libraryManager; + private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager; /// /// Initializes a new instance of the class. @@ -32,16 +30,19 @@ public class BaseItemAnalyzerTask /// Task logger. /// Logger factory. /// Library manager. + /// MediaSegmentUpdateManager. public BaseItemAnalyzerTask( IReadOnlyCollection modes, ILogger logger, ILoggerFactory loggerFactory, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + MediaSegmentUpdateManager mediaSegmentUpdateManager) { _analysisModes = modes; _logger = logger; _loggerFactory = loggerFactory; _libraryManager = libraryManager; + _mediaSegmentUpdateManager = mediaSegmentUpdateManager; if (Plugin.Instance!.Configuration.EdlAction != EdlAction.None) { @@ -55,7 +56,8 @@ public class BaseItemAnalyzerTask /// Progress. /// Cancellation token. /// Season Ids to analyze. - public void AnalyzeItems( + /// A representing the asynchronous operation. + public async Task AnalyzeItems( IProgress progress, CancellationToken cancellationToken, IReadOnlyCollection? seasonsToAnalyze = null) @@ -95,12 +97,13 @@ public class BaseItemAnalyzerTask var totalProcessed = 0; var options = new ParallelOptions { - MaxDegreeOfParallelism = Plugin.Instance.Configuration.MaxParallelism + MaxDegreeOfParallelism = Plugin.Instance.Configuration.MaxParallelism, + CancellationToken = cancellationToken }; - Parallel.ForEach(queue, options, season => + await Parallel.ForEachAsync(queue, options, async (season, ct) => { - var writeEdl = false; + var updateManagers = 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. @@ -132,17 +135,17 @@ public class BaseItemAnalyzerTask try { - if (cancellationToken.IsCancellationRequested) + if (ct.IsCancellationRequested) { return; } foreach (AnalysisMode mode in requiredModes) { - var analyzed = AnalyzeItems(episodes, mode, cancellationToken); + var analyzed = AnalyzeItems(episodes, mode, ct); Interlocked.Add(ref totalProcessed, analyzed); - writeEdl = analyzed > 0 || Plugin.Instance.Configuration.RegenerateEdlFiles; + updateManagers = analyzed > 0 || updateManagers; progress.Report(totalProcessed * 100 / totalQueued); } @@ -156,15 +159,21 @@ public class BaseItemAnalyzerTask ex); } - if (writeEdl && Plugin.Instance.Configuration.EdlAction != EdlAction.None) + if (Plugin.Instance.Configuration.RegenerateMediaSegments || (updateManagers && Plugin.Instance.Configuration.UpdateMediaSegments)) + { + await _mediaSegmentUpdateManager.UpdateMediaSegmentsAsync(episodes, ct).ConfigureAwait(false); + } + + if (Plugin.Instance.Configuration.RegenerateEdlFiles || (updateManagers && Plugin.Instance.Configuration.EdlAction != EdlAction.None)) { EdlManager.UpdateEDLFiles(episodes); } - }); + }).ConfigureAwait(false); - if (Plugin.Instance.Configuration.RegenerateEdlFiles) + if (Plugin.Instance.Configuration.RegenerateMediaSegments || Plugin.Instance.Configuration.RegenerateEdlFiles) { - _logger.LogInformation("Turning EDL file regeneration flag off"); + _logger.LogInformation("Turning Mediasegment/EDL file regeneration flag off"); + Plugin.Instance.Configuration.RegenerateMediaSegments = false; Plugin.Instance.Configuration.RegenerateEdlFiles = false; Plugin.Instance.SaveConfiguration(); } @@ -182,7 +191,7 @@ public class BaseItemAnalyzerTask AnalysisMode mode, CancellationToken cancellationToken) { - var totalItems = items.Count; + var totalItems = items.Count(e => !e.State.IsAnalyzed(mode)); // Only analyze specials (season 0) if the user has opted in. var first = items[0]; diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs index 002b2d1..35d7856 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ConfusedPolarBear.Plugin.IntroSkipper.Data; +using ConfusedPolarBear.Plugin.IntroSkipper.Manager; using ConfusedPolarBear.Plugin.IntroSkipper.Services; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; @@ -14,29 +15,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; /// Analyze all television episodes for credits. /// TODO: analyze all media files. /// -public class DetectCreditsTask : IScheduledTask +/// +/// Initializes a new instance of the class. +/// +/// Logger factory. +/// Library manager. +/// Logger. +/// MediaSegment Update Manager. +public class DetectCreditsTask( + ILogger logger, + ILoggerFactory loggerFactory, + ILibraryManager libraryManager, + MediaSegmentUpdateManager mediaSegmentUpdateManager) : IScheduledTask { - private readonly ILogger _logger; + private readonly ILogger _logger = logger; - private readonly ILoggerFactory _loggerFactory; + private readonly ILoggerFactory _loggerFactory = loggerFactory; - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager = libraryManager; - /// - /// Initializes a new instance of the class. - /// - /// Logger factory. - /// Library manager. - /// Logger. - public DetectCreditsTask( - ILogger logger, - ILoggerFactory loggerFactory, - ILibraryManager libraryManager) - { - _logger = logger; - _loggerFactory = loggerFactory; - _libraryManager = libraryManager; - } + private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager; /// /// Gets the task name. @@ -64,7 +62,7 @@ public class DetectCreditsTask : IScheduledTask /// Task progress. /// Cancellation token. /// Task. - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { if (_libraryManager is null) { @@ -75,10 +73,10 @@ public class DetectCreditsTask : IScheduledTask if (Entrypoint.AutomaticTaskState == TaskState.Running || Entrypoint.AutomaticTaskState == TaskState.Cancelling) { _logger.LogInformation("Automatic Task is {0} and will be canceled.", Entrypoint.AutomaticTaskState); - Entrypoint.CancelAutomaticTask(cancellationToken); + await Entrypoint.CancelAutomaticTaskAsync(cancellationToken).ConfigureAwait(false); } - using (ScheduledTaskSemaphore.Acquire(cancellationToken)) + using (await ScheduledTaskSemaphore.AcquireAsync(cancellationToken).ConfigureAwait(false)) { _logger.LogInformation("Scheduled Task is starting"); @@ -88,11 +86,10 @@ public class DetectCreditsTask : IScheduledTask modes, _loggerFactory.CreateLogger(), _loggerFactory, - _libraryManager); + _libraryManager, + _mediaSegmentUpdateManager); - baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken); - - return Task.CompletedTask; + await baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken).ConfigureAwait(false); } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs index 2516730..af97903 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ConfusedPolarBear.Plugin.IntroSkipper.Data; +using ConfusedPolarBear.Plugin.IntroSkipper.Manager; using ConfusedPolarBear.Plugin.IntroSkipper.Services; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; @@ -13,29 +14,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; /// /// Analyze all television episodes for introduction sequences. /// -public class DetectIntrosCreditsTask : IScheduledTask +/// +/// Initializes a new instance of the class. +/// +/// Logger factory. +/// Library manager. +/// Logger. +/// MediaSegment Update Manager. +public class DetectIntrosCreditsTask( + ILogger logger, + ILoggerFactory loggerFactory, + ILibraryManager libraryManager, + MediaSegmentUpdateManager mediaSegmentUpdateManager) : IScheduledTask { - private readonly ILogger _logger; + private readonly ILogger _logger = logger; - private readonly ILoggerFactory _loggerFactory; + private readonly ILoggerFactory _loggerFactory = loggerFactory; - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager = libraryManager; - /// - /// Initializes a new instance of the class. - /// - /// Logger factory. - /// Library manager. - /// Logger. - public DetectIntrosCreditsTask( - ILogger logger, - ILoggerFactory loggerFactory, - ILibraryManager libraryManager) - { - _logger = logger; - _loggerFactory = loggerFactory; - _libraryManager = libraryManager; - } + private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager; /// /// Gets the task name. @@ -63,7 +61,7 @@ public class DetectIntrosCreditsTask : IScheduledTask /// Task progress. /// Cancellation token. /// Task. - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { if (_libraryManager is null) { @@ -74,10 +72,10 @@ public class DetectIntrosCreditsTask : IScheduledTask if (Entrypoint.AutomaticTaskState == TaskState.Running || Entrypoint.AutomaticTaskState == TaskState.Cancelling) { _logger.LogInformation("Automatic Task is {0} and will be canceled.", Entrypoint.AutomaticTaskState); - Entrypoint.CancelAutomaticTask(cancellationToken); + await Entrypoint.CancelAutomaticTaskAsync(cancellationToken).ConfigureAwait(false); } - using (ScheduledTaskSemaphore.Acquire(cancellationToken)) + using (await ScheduledTaskSemaphore.AcquireAsync(cancellationToken).ConfigureAwait(false)) { _logger.LogInformation("Scheduled Task is starting"); @@ -87,11 +85,10 @@ public class DetectIntrosCreditsTask : IScheduledTask modes, _loggerFactory.CreateLogger(), _loggerFactory, - _libraryManager); + _libraryManager, + _mediaSegmentUpdateManager); - baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken); - - return Task.CompletedTask; + await baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken).ConfigureAwait(false); } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs index f79dd3c..2e44c47 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ConfusedPolarBear.Plugin.IntroSkipper.Data; +using ConfusedPolarBear.Plugin.IntroSkipper.Manager; using ConfusedPolarBear.Plugin.IntroSkipper.Services; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; @@ -13,29 +14,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; /// /// Analyze all television episodes for introduction sequences. /// -public class DetectIntrosTask : IScheduledTask +/// +/// Initializes a new instance of the class. +/// +/// Logger factory. +/// Library manager. +/// Logger. +/// MediaSegment Update Manager. +public class DetectIntrosTask( + ILogger logger, + ILoggerFactory loggerFactory, + ILibraryManager libraryManager, + MediaSegmentUpdateManager mediaSegmentUpdateManager) : IScheduledTask { - private readonly ILogger _logger; + private readonly ILogger _logger = logger; - private readonly ILoggerFactory _loggerFactory; + private readonly ILoggerFactory _loggerFactory = loggerFactory; - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager = libraryManager; - /// - /// Initializes a new instance of the class. - /// - /// Logger factory. - /// Library manager. - /// Logger. - public DetectIntrosTask( - ILogger logger, - ILoggerFactory loggerFactory, - ILibraryManager libraryManager) - { - _logger = logger; - _loggerFactory = loggerFactory; - _libraryManager = libraryManager; - } + private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager; /// /// Gets the task name. @@ -63,7 +61,7 @@ public class DetectIntrosTask : IScheduledTask /// Task progress. /// Cancellation token. /// Task. - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { if (_libraryManager is null) { @@ -74,10 +72,10 @@ public class DetectIntrosTask : IScheduledTask if (Entrypoint.AutomaticTaskState == TaskState.Running || Entrypoint.AutomaticTaskState == TaskState.Cancelling) { _logger.LogInformation("Automatic Task is {0} and will be canceled.", Entrypoint.AutomaticTaskState); - Entrypoint.CancelAutomaticTask(cancellationToken); + await Entrypoint.CancelAutomaticTaskAsync(cancellationToken).ConfigureAwait(false); } - using (ScheduledTaskSemaphore.Acquire(cancellationToken)) + using (await ScheduledTaskSemaphore.AcquireAsync(cancellationToken).ConfigureAwait(false)) { _logger.LogInformation("Scheduled Task is starting"); @@ -87,11 +85,10 @@ public class DetectIntrosTask : IScheduledTask modes, _loggerFactory.CreateLogger(), _loggerFactory, - _libraryManager); + _libraryManager, + _mediaSegmentUpdateManager); - baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken); - - return Task.CompletedTask; + await baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken).ConfigureAwait(false); } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/ScheduledTaskSemaphore.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/ScheduledTaskSemaphore.cs index d89bbed..3508af9 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/ScheduledTaskSemaphore.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/ScheduledTaskSemaphore.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; @@ -11,9 +12,9 @@ internal sealed class ScheduledTaskSemaphore : IDisposable { } - public static IDisposable Acquire(CancellationToken cancellationToken) + public static async Task AcquireAsync(CancellationToken cancellationToken) { - _semaphore.Wait(cancellationToken); + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); return new ScheduledTaskSemaphore(); } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs index d6735c1..cbca925 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs @@ -6,6 +6,7 @@ using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Manager; using ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; @@ -25,9 +26,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; + private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager; private readonly HashSet _seasonsToAnalyze = []; private readonly Timer _queueTimer; - private static readonly ManualResetEventSlim _autoTaskCompletEvent = new(false); + private static readonly SemaphoreSlim _analysisSemaphore = new(1, 1); private PluginConfiguration _config; private bool _analyzeAgain; private static CancellationTokenSource? _cancellationTokenSource; @@ -39,16 +41,19 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services /// Task manager. /// Logger. /// Logger factory. + /// MediaSegment Update Manager. public Entrypoint( ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + MediaSegmentUpdateManager mediaSegmentUpdateManager) { _libraryManager = libraryManager; _taskManager = taskManager; _logger = logger; _loggerFactory = loggerFactory; + _mediaSegmentUpdateManager = mediaSegmentUpdateManager; _config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); _queueTimer = new Timer( @@ -61,42 +66,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services /// /// Gets State of the automatic task. /// - public static TaskState AutomaticTaskState + public static TaskState AutomaticTaskState => _cancellationTokenSource switch { - get - { - if (_cancellationTokenSource is not null) - { - return _cancellationTokenSource.IsCancellationRequested - ? TaskState.Cancelling - : TaskState.Running; - } - - return TaskState.Idle; - } - } + null => TaskState.Idle, + { IsCancellationRequested: true } => TaskState.Cancelling, + _ => TaskState.Running + }; /// public Task StartAsync(CancellationToken cancellationToken) { - _libraryManager.ItemAdded += OnItemAdded; - _libraryManager.ItemUpdated += OnItemModified; + _libraryManager.ItemAdded += OnItemChanged; + _libraryManager.ItemUpdated += OnItemChanged; _taskManager.TaskCompleted += OnLibraryRefresh; Plugin.Instance!.ConfigurationChanged += OnSettingsChanged; FFmpegWrapper.Logger = _logger; - try - { - // Enqueue all episodes at startup to ensure any FFmpeg errors appear as early as possible - _logger.LogInformation("Running startup enqueue"); - var queueManager = new QueueManager(_loggerFactory.CreateLogger(), _libraryManager); - queueManager?.GetMediaItems(); - } - catch (Exception ex) - { - _logger.LogError("Unable to run startup enqueue: {Exception}", ex); - } + // Enqueue all episodes at startup to ensure any FFmpeg errors appear as early as possible + _logger.LogInformation("Running startup enqueue"); + new QueueManager(_loggerFactory.CreateLogger(), _libraryManager).GetMediaItems(); return Task.CompletedTask; } @@ -104,75 +93,33 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services /// public Task StopAsync(CancellationToken cancellationToken) { - _libraryManager.ItemAdded -= OnItemAdded; - _libraryManager.ItemUpdated -= OnItemModified; + _libraryManager.ItemAdded -= OnItemChanged; + _libraryManager.ItemUpdated -= OnItemChanged; _taskManager.TaskCompleted -= OnLibraryRefresh; + Plugin.Instance!.ConfigurationChanged -= OnSettingsChanged; - // Stop the timer _queueTimer.Change(Timeout.Infinite, 0); return Task.CompletedTask; } - // Disclose source for inspiration - // Implementation based on the principles of jellyfin-plugin-media-analyzer: - // https://github.com/endrl/jellyfin-plugin-media-analyzer - /// /// Library item was added. /// /// The sending entity. /// The . - private void OnItemAdded(object? sender, ItemChangeEventArgs itemChangeEventArgs) + private void OnItemChanged(object? sender, ItemChangeEventArgs itemChangeEventArgs) { - // Don't do anything if auto detection is disabled - if (!_config.AutoDetectIntros && !_config.AutoDetectCredits) + if ((_config.AutoDetectIntros || _config.AutoDetectCredits) && + itemChangeEventArgs.Item is { LocationType: not LocationType.Virtual } item) { - return; + Guid? id = item is Episode episode ? episode.SeasonId : (item is Movie movie ? movie.Id : null); + + if (id.HasValue) + { + _seasonsToAnalyze.Add(id.Value); + StartTimer(); + } } - - // Don't do anything if it's not a supported media type - if (itemChangeEventArgs.Item is not Episode episode) - { - return; - } - - if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual) - { - return; - } - - _seasonsToAnalyze.Add(episode.SeasonId); - - StartTimer(); - } - - /// - /// Library item was modified. - /// - /// The sending entity. - /// The . - private void OnItemModified(object? sender, ItemChangeEventArgs itemChangeEventArgs) - { - // Don't do anything if auto detection is disabled - if (!_config.AutoDetectIntros && !_config.AutoDetectCredits) - { - return; - } - - // Don't do anything if it's not a supported media type - if (itemChangeEventArgs.Item is not Episode episode) - { - return; - } - - if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual) - { - return; - } - - _seasonsToAnalyze.Add(episode.SeasonId); - - StartTimer(); } /// @@ -182,31 +129,12 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services /// The . private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs) { - // Don't do anything if auto detection is disabled - if (!_config.AutoDetectIntros && !_config.AutoDetectCredits) + if ((_config.AutoDetectIntros || _config.AutoDetectCredits) && + eventArgs.Result is { Key: "RefreshLibrary", Status: TaskCompletionStatus.Completed } && + AutomaticTaskState != TaskState.Running) { - return; + StartTimer(); } - - var result = eventArgs.Result; - - if (result.Key != "RefreshLibrary") - { - return; - } - - if (result.Status != TaskCompletionStatus.Completed) - { - return; - } - - // Unless user initiated, this is likely an overlap - if (AutomaticTaskState == TaskState.Running) - { - return; - } - - StartTimer(); } private void OnSettingsChanged(object? sender, BasePluginConfiguration e) => _config = (PluginConfiguration)e; @@ -223,90 +151,80 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services else if (AutomaticTaskState == TaskState.Idle) { _logger.LogDebug("Media Library changed, analyzis will start soon!"); - _queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan); + _queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan); } } - /// - /// Wait for timer callback to be completed. - /// - private void OnTimerCallback(object? state) + private void OnTimerCallback(object? state) => + _ = RunAnalysisAsync(); + + private async Task RunAnalysisAsync() { try { - PerformAnalysis(); + await PerformAnalysisAsync().ConfigureAwait(false); } catch (Exception ex) { - _logger.LogError(ex, "Error in PerformAnalysis"); + _logger.LogError(ex, "Error in RunAnalysisAsync"); } - // Clean up _cancellationTokenSource = null; - _autoTaskCompletEvent.Set(); } - /// - /// Wait for timer to be completed. - /// - private void PerformAnalysis() + private async Task PerformAnalysisAsync() { - _logger.LogInformation("Initiate automatic analysis task."); - _autoTaskCompletEvent.Reset(); - - using (_cancellationTokenSource = new CancellationTokenSource()) - using (ScheduledTaskSemaphore.Acquire(_cancellationTokenSource.Token)) + await _analysisSemaphore.WaitAsync().ConfigureAwait(false); + try { - var seasonIds = new HashSet(_seasonsToAnalyze); - _seasonsToAnalyze.Clear(); - - _analyzeAgain = false; - var progress = new Progress(); - var modes = new List(); - var tasklogger = _loggerFactory.CreateLogger("DefaultLogger"); - - if (_config.AutoDetectIntros) + using (_cancellationTokenSource = new CancellationTokenSource()) + using (await ScheduledTaskSemaphore.AcquireAsync(_cancellationTokenSource.Token).ConfigureAwait(false)) { - modes.Add(AnalysisMode.Introduction); - tasklogger = _loggerFactory.CreateLogger(); - } - - if (_config.AutoDetectCredits) - { - modes.Add(AnalysisMode.Credits); - tasklogger = modes.Count == 2 - ? _loggerFactory.CreateLogger() - : _loggerFactory.CreateLogger(); - } - - var baseCreditAnalyzer = new BaseItemAnalyzerTask( - modes, - tasklogger, - _loggerFactory, - _libraryManager); - - baseCreditAnalyzer.AnalyzeItems(progress, _cancellationTokenSource.Token, seasonIds); - - // New item detected, start timer again - if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested) - { - _logger.LogInformation("Analyzing ended, but we need to analyze again!"); - StartTimer(); + _logger.LogInformation("Initiating automatic analysis task"); + var seasonIds = new HashSet(_seasonsToAnalyze); + _seasonsToAnalyze.Clear(); + _analyzeAgain = false; + + var modes = new List(); + + if (_config.AutoDetectIntros) + { + modes.Add(AnalysisMode.Introduction); + } + + if (_config.AutoDetectCredits) + { + modes.Add(AnalysisMode.Credits); + } + + var analyzer = new BaseItemAnalyzerTask(modes, _loggerFactory.CreateLogger(), _loggerFactory, _libraryManager, _mediaSegmentUpdateManager); + await analyzer.AnalyzeItems(new Progress(), _cancellationTokenSource.Token, seasonIds).ConfigureAwait(false); + + if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested) + { + _logger.LogInformation("Analyzing ended, but we need to analyze again!"); + _queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan); + } } } + finally + { + _analysisSemaphore.Release(); + } } /// /// Method to cancel the automatic task. /// /// Cancellation token. - public static void CancelAutomaticTask(CancellationToken cancellationToken) + /// A representing the asynchronous operation. + public static async Task CancelAutomaticTaskAsync(CancellationToken cancellationToken) { - if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) + if (_cancellationTokenSource is { IsCancellationRequested: false }) { try { - _cancellationTokenSource.Cancel(); + await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -314,7 +232,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services } } - _autoTaskCompletEvent.Wait(TimeSpan.FromSeconds(60), cancellationToken); // Wait for the signal + await _analysisSemaphore.WaitAsync(TimeSpan.FromSeconds(60), cancellationToken).ConfigureAwait(false); } /// @@ -322,7 +240,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services { _queueTimer.Dispose(); _cancellationTokenSource?.Dispose(); - _autoTaskCompletEvent.Dispose(); + _analysisSemaphore.Dispose(); } } }