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();
}
}
}