From fc830a5e6f81849515cd0aa237433870e1c8f506 Mon Sep 17 00:00:00 2001
From: rlauu <46294892+rlauu@users.noreply.github.com>
Date: Wed, 16 Oct 2024 16:20:21 +0200
Subject: [PATCH] Use primary constructor everywhere
---
.../TestEdl.cs | 1 +
.../Analyzers/AnalyzerHelper.cs | 181 +++---
...nfusedPolarBear.Plugin.IntroSkipper.csproj | 4 -
.../Helper/XmlSerializationHelper.cs | 2 +-
.../Manager/EdlManager.cs | 169 ++---
.../Manager/QueueManager.cs | 513 ++++++++--------
.../Plugin.cs | 1 +
.../PluginServiceRegistrator.cs | 1 +
.../ScheduledTasks/BaseItemAnalyzerTask.cs | 1 +
.../ScheduledTasks/CleanCacheTask.cs | 1 +
.../ScheduledTasks/DetectCreditsTask.cs | 1 +
.../ScheduledTasks/DetectIntrosCreditsTask.cs | 1 +
.../ScheduledTasks/DetectIntrosTask.cs | 1 +
.../Services/AutoSkip.cs | 347 +++++------
.../Services/AutoSkipCredits.cs | 347 +++++------
.../Services/Entrypoint.cs | 580 +++++++++---------
16 files changed, 1081 insertions(+), 1070 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestEdl.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestEdl.cs
index fe86c00..6106b2c 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestEdl.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestEdl.cs
@@ -1,5 +1,6 @@
using System;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using Xunit;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/AnalyzerHelper.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/AnalyzerHelper.cs
index d7829de..3170689 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/AnalyzerHelper.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/AnalyzerHelper.cs
@@ -5,112 +5,113 @@ using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Microsoft.Extensions.Logging;
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Analyzer Helper.
-///
-public class AnalyzerHelper
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers
{
- private readonly ILogger _logger;
- private readonly double _silenceDetectionMinimumDuration;
-
///
- /// Initializes a new instance of the class.
+ /// Analyzer Helper.
///
- /// Logger.
- public AnalyzerHelper(ILogger logger)
+ public class AnalyzerHelper
{
- var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
- _silenceDetectionMinimumDuration = config.SilenceDetectionMinimumDuration;
- _logger = logger;
- }
+ private readonly ILogger _logger;
+ private readonly double _silenceDetectionMinimumDuration;
- ///
- /// Adjusts the end timestamps of all intros so that they end at silence.
- ///
- /// QueuedEpisodes to adjust.
- /// Original introductions.
- /// Analysis mode.
- /// Modified Intro Timestamps.
- public Dictionary AdjustIntroTimes(
- IReadOnlyList episodes,
- IReadOnlyDictionary originalIntros,
- AnalysisMode mode)
- {
- return episodes
- .Where(episode => originalIntros.TryGetValue(episode.EpisodeId, out var _))
- .ToDictionary(
- episode => episode.EpisodeId,
- episode => AdjustIntroForEpisode(episode, originalIntros[episode.EpisodeId], mode));
- }
-
- private Segment AdjustIntroForEpisode(QueuedEpisode episode, Segment originalIntro, AnalysisMode mode)
- {
- _logger.LogTrace("{Name} original intro: {Start} - {End}", episode.Name, originalIntro.Start, originalIntro.End);
-
- var adjustedIntro = new Segment(originalIntro);
- var originalIntroStart = new TimeRange(Math.Max(0, (int)originalIntro.Start - 5), (int)originalIntro.Start + 10);
- var originalIntroEnd = new TimeRange((int)originalIntro.End - 10, Math.Min(episode.Duration, (int)originalIntro.End + 5));
-
- if (!AdjustIntroBasedOnChapters(episode, adjustedIntro, originalIntroStart, originalIntroEnd) && mode == AnalysisMode.Introduction)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Logger.
+ public AnalyzerHelper(ILogger logger)
{
- AdjustIntroBasedOnSilence(episode, adjustedIntro, originalIntroEnd);
+ var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
+ _silenceDetectionMinimumDuration = config.SilenceDetectionMinimumDuration;
+ _logger = logger;
}
- return adjustedIntro;
- }
-
- private bool AdjustIntroBasedOnChapters(QueuedEpisode episode, Segment adjustedIntro, TimeRange originalIntroStart, TimeRange originalIntroEnd)
- {
- var chapters = Plugin.Instance?.GetChapters(episode.EpisodeId) ?? [];
- double previousTime = 0;
-
- for (int i = 0; i <= chapters.Count; i++)
+ ///
+ /// Adjusts the end timestamps of all intros so that they end at silence.
+ ///
+ /// QueuedEpisodes to adjust.
+ /// Original introductions.
+ /// Analysis mode.
+ /// Modified Intro Timestamps.
+ public Dictionary AdjustIntroTimes(
+ IReadOnlyList episodes,
+ IReadOnlyDictionary originalIntros,
+ AnalysisMode mode)
{
- double currentTime = i < chapters.Count
- ? TimeSpan.FromTicks(chapters[i].StartPositionTicks).TotalSeconds
- : episode.Duration;
-
- if (originalIntroStart.Start < previousTime && previousTime < originalIntroStart.End)
- {
- adjustedIntro.Start = previousTime;
- _logger.LogTrace("{Name} chapter found close to intro start: {Start}", episode.Name, previousTime);
- }
-
- if (originalIntroEnd.Start < currentTime && currentTime < originalIntroEnd.End)
- {
- adjustedIntro.End = currentTime;
- _logger.LogTrace("{Name} chapter found close to intro end: {End}", episode.Name, currentTime);
- return true;
- }
-
- previousTime = currentTime;
+ return episodes
+ .Where(episode => originalIntros.TryGetValue(episode.EpisodeId, out var _))
+ .ToDictionary(
+ episode => episode.EpisodeId,
+ episode => AdjustIntroForEpisode(episode, originalIntros[episode.EpisodeId], mode));
}
- return false;
- }
-
- private void AdjustIntroBasedOnSilence(QueuedEpisode episode, Segment adjustedIntro, TimeRange originalIntroEnd)
- {
- var silence = FFmpegWrapper.DetectSilence(episode, originalIntroEnd);
-
- foreach (var currentRange in silence)
+ private Segment AdjustIntroForEpisode(QueuedEpisode episode, Segment originalIntro, AnalysisMode mode)
{
- _logger.LogTrace("{Name} silence: {Start} - {End}", episode.Name, currentRange.Start, currentRange.End);
+ _logger.LogTrace("{Name} original intro: {Start} - {End}", episode.Name, originalIntro.Start, originalIntro.End);
- if (IsValidSilenceForIntroAdjustment(currentRange, originalIntroEnd, adjustedIntro))
+ var adjustedIntro = new Segment(originalIntro);
+ var originalIntroStart = new TimeRange(Math.Max(0, (int)originalIntro.Start - 5), (int)originalIntro.Start + 10);
+ var originalIntroEnd = new TimeRange((int)originalIntro.End - 10, Math.Min(episode.Duration, (int)originalIntro.End + 5));
+
+ if (!AdjustIntroBasedOnChapters(episode, adjustedIntro, originalIntroStart, originalIntroEnd) && mode == AnalysisMode.Introduction)
{
- adjustedIntro.End = currentRange.Start;
- break;
+ AdjustIntroBasedOnSilence(episode, adjustedIntro, originalIntroEnd);
+ }
+
+ return adjustedIntro;
+ }
+
+ private bool AdjustIntroBasedOnChapters(QueuedEpisode episode, Segment adjustedIntro, TimeRange originalIntroStart, TimeRange originalIntroEnd)
+ {
+ var chapters = Plugin.Instance?.GetChapters(episode.EpisodeId) ?? [];
+ double previousTime = 0;
+
+ for (int i = 0; i <= chapters.Count; i++)
+ {
+ double currentTime = i < chapters.Count
+ ? TimeSpan.FromTicks(chapters[i].StartPositionTicks).TotalSeconds
+ : episode.Duration;
+
+ if (originalIntroStart.Start < previousTime && previousTime < originalIntroStart.End)
+ {
+ adjustedIntro.Start = previousTime;
+ _logger.LogTrace("{Name} chapter found close to intro start: {Start}", episode.Name, previousTime);
+ }
+
+ if (originalIntroEnd.Start < currentTime && currentTime < originalIntroEnd.End)
+ {
+ adjustedIntro.End = currentTime;
+ _logger.LogTrace("{Name} chapter found close to intro end: {End}", episode.Name, currentTime);
+ return true;
+ }
+
+ previousTime = currentTime;
+ }
+
+ return false;
+ }
+
+ private void AdjustIntroBasedOnSilence(QueuedEpisode episode, Segment adjustedIntro, TimeRange originalIntroEnd)
+ {
+ var silence = FFmpegWrapper.DetectSilence(episode, originalIntroEnd);
+
+ foreach (var currentRange in silence)
+ {
+ _logger.LogTrace("{Name} silence: {Start} - {End}", episode.Name, currentRange.Start, currentRange.End);
+
+ if (IsValidSilenceForIntroAdjustment(currentRange, originalIntroEnd, adjustedIntro))
+ {
+ adjustedIntro.End = currentRange.Start;
+ break;
+ }
}
}
- }
- private bool IsValidSilenceForIntroAdjustment(TimeRange silenceRange, TimeRange originalIntroEnd, Segment adjustedIntro)
- {
- return originalIntroEnd.Intersects(silenceRange) &&
- silenceRange.Duration >= _silenceDetectionMinimumDuration &&
- silenceRange.Start >= adjustedIntro.Start;
+ private bool IsValidSilenceForIntroAdjustment(TimeRange silenceRange, TimeRange originalIntroEnd, Segment adjustedIntro)
+ {
+ return originalIntroEnd.Intersects(silenceRange) &&
+ silenceRange.Duration >= _silenceDetectionMinimumDuration &&
+ silenceRange.Start >= adjustedIntro.Start;
+ }
}
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj
index 0a5c4a9..ff595c2 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj
@@ -23,8 +23,4 @@
-
-
-
-
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Helper/XmlSerializationHelper.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Helper/XmlSerializationHelper.cs
index f02d126..db5f929 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Helper/XmlSerializationHelper.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Helper/XmlSerializationHelper.cs
@@ -6,7 +6,7 @@ using System.Runtime.Serialization;
using System.Xml;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
-namespace ConfusedPolarBear.Plugin.IntroSkipper
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Helper
{
internal sealed class XmlSerializationHelper
{
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Manager/EdlManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/EdlManager.cs
index dd8fc4b..a21bf15 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Manager/EdlManager.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/EdlManager.cs
@@ -4,113 +4,114 @@ using System.IO;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Microsoft.Extensions.Logging;
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Update EDL files associated with a list of episodes.
-///
-public static class EdlManager
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager
{
- private static ILogger? _logger;
-
///
- /// Initialize EDLManager with a logger.
+ /// Update EDL files associated with a list of episodes.
///
- /// ILogger.
- public static void Initialize(ILogger logger)
+ public static class EdlManager
{
- _logger = logger;
- }
+ private static ILogger? _logger;
- ///
- /// Logs the configuration that will be used during EDL file creation.
- ///
- public static void LogConfiguration()
- {
- if (_logger is null)
+ ///
+ /// Initialize EDLManager with a logger.
+ ///
+ /// ILogger.
+ public static void Initialize(ILogger logger)
{
- throw new InvalidOperationException("Logger must not be null");
+ _logger = logger;
}
- var config = Plugin.Instance!.Configuration;
-
- if (config.EdlAction == EdlAction.None)
+ ///
+ /// Logs the configuration that will be used during EDL file creation.
+ ///
+ public static void LogConfiguration()
{
- _logger.LogDebug("EDL action: None - taking no further action");
- return;
+ if (_logger is null)
+ {
+ throw new InvalidOperationException("Logger must not be null");
+ }
+
+ var config = Plugin.Instance!.Configuration;
+
+ if (config.EdlAction == EdlAction.None)
+ {
+ _logger.LogDebug("EDL action: None - taking no further action");
+ return;
+ }
+
+ _logger.LogDebug("EDL action: {Action}", config.EdlAction);
+ _logger.LogDebug("Regenerate EDL files: {Regenerate}", config.RegenerateEdlFiles);
}
- _logger.LogDebug("EDL action: {Action}", config.EdlAction);
- _logger.LogDebug("Regenerate EDL files: {Regenerate}", config.RegenerateEdlFiles);
- }
-
- ///
- /// If the EDL action is set to a value other than None, update EDL files for the provided episodes.
- ///
- /// Episodes to update EDL files for.
- public static void UpdateEDLFiles(IReadOnlyList episodes)
- {
- var regenerate = Plugin.Instance!.Configuration.RegenerateEdlFiles;
- var action = Plugin.Instance.Configuration.EdlAction;
- if (action == EdlAction.None)
+ ///
+ /// If the EDL action is set to a value other than None, update EDL files for the provided episodes.
+ ///
+ /// Episodes to update EDL files for.
+ public static void UpdateEDLFiles(IReadOnlyList episodes)
{
- _logger?.LogDebug("EDL action is set to none, not updating EDL files");
- return;
- }
-
- _logger?.LogDebug("Updating EDL files with action {Action}", action);
-
- foreach (var episode in episodes)
- {
- var id = episode.EpisodeId;
-
- bool hasIntro = Plugin.Instance!.Intros.TryGetValue(id, out var intro) && intro.Valid;
- bool hasCredit = Plugin.Instance!.Credits.TryGetValue(id, out var credit) && credit.Valid;
-
- if (!hasIntro && !hasCredit)
+ var regenerate = Plugin.Instance!.Configuration.RegenerateEdlFiles;
+ var action = Plugin.Instance.Configuration.EdlAction;
+ if (action == EdlAction.None)
{
- _logger?.LogDebug("Episode {Id} has neither a valid intro nor credit, skipping", id);
- continue;
+ _logger?.LogDebug("EDL action is set to none, not updating EDL files");
+ return;
}
- var edlPath = GetEdlPath(Plugin.Instance.GetItemPath(id));
+ _logger?.LogDebug("Updating EDL files with action {Action}", action);
- _logger?.LogTrace("Episode {Id} has EDL path {Path}", id, edlPath);
-
- if (!regenerate && File.Exists(edlPath))
+ foreach (var episode in episodes)
{
- _logger?.LogTrace("Refusing to overwrite existing EDL file {Path}", edlPath);
- continue;
- }
+ var id = episode.EpisodeId;
- var edlContent = string.Empty;
+ bool hasIntro = Plugin.Instance!.Intros.TryGetValue(id, out var intro) && intro.Valid;
+ bool hasCredit = Plugin.Instance!.Credits.TryGetValue(id, out var credit) && credit.Valid;
- if (hasIntro)
- {
- edlContent += intro?.ToEdl(action);
- }
-
- if (hasCredit)
- {
- if (edlContent.Length > 0)
+ if (!hasIntro && !hasCredit)
{
- edlContent += Environment.NewLine;
+ _logger?.LogDebug("Episode {Id} has neither a valid intro nor credit, skipping", id);
+ continue;
}
- edlContent += credit?.ToEdl(action);
- }
+ var edlPath = GetEdlPath(Plugin.Instance.GetItemPath(id));
- File.WriteAllText(edlPath, edlContent);
+ _logger?.LogTrace("Episode {Id} has EDL path {Path}", id, edlPath);
+
+ if (!regenerate && File.Exists(edlPath))
+ {
+ _logger?.LogTrace("Refusing to overwrite existing EDL file {Path}", edlPath);
+ continue;
+ }
+
+ var edlContent = string.Empty;
+
+ if (hasIntro)
+ {
+ edlContent += intro?.ToEdl(action);
+ }
+
+ if (hasCredit)
+ {
+ if (edlContent.Length > 0)
+ {
+ edlContent += Environment.NewLine;
+ }
+
+ edlContent += credit?.ToEdl(action);
+ }
+
+ File.WriteAllText(edlPath, edlContent);
+ }
+ }
+
+ ///
+ /// Given the path to an episode, return the path to the associated EDL file.
+ ///
+ /// Full path to episode.
+ /// Full path to EDL file.
+ public static string GetEdlPath(string mediaPath)
+ {
+ return Path.ChangeExtension(mediaPath, "edl");
}
}
-
- ///
- /// Given the path to an episode, return the path to the associated EDL file.
- ///
- /// Full path to episode.
- /// Full path to EDL file.
- public static string GetEdlPath(string mediaPath)
- {
- return Path.ChangeExtension(mediaPath, "edl");
- }
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs
index 13e981c..e324d76 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs
@@ -10,287 +10,288 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Manages enqueuing library items for analysis.
-///
-///
-/// Initializes a new instance of the class.
-///
-/// Logger.
-/// Library manager.
-public class QueueManager(ILogger logger, ILibraryManager libraryManager)
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager
{
- private readonly ILibraryManager _libraryManager = libraryManager;
- private readonly ILogger _logger = logger;
- private readonly Dictionary> _queuedEpisodes = [];
- private double _analysisPercent;
- private List _selectedLibraries = [];
- private bool _selectAllLibraries;
-
///
- /// Gets all media items on the server.
+ /// Manages enqueuing library items for analysis.
///
- /// Queued media items.
- public IReadOnlyDictionary> GetMediaItems()
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Logger.
+ /// Library manager.
+ public class QueueManager(ILogger logger, ILibraryManager libraryManager)
{
- Plugin.Instance!.TotalQueued = 0;
+ private readonly ILibraryManager _libraryManager = libraryManager;
+ private readonly ILogger _logger = logger;
+ private readonly Dictionary> _queuedEpisodes = [];
+ private double _analysisPercent;
+ private List _selectedLibraries = [];
+ private bool _selectAllLibraries;
- LoadAnalysisSettings();
-
- // For all selected libraries, enqueue all contained episodes.
- foreach (var folder in _libraryManager.GetVirtualFolders())
+ ///
+ /// Gets all media items on the server.
+ ///
+ /// Queued media items.
+ public IReadOnlyDictionary> GetMediaItems()
{
- // If libraries have been selected for analysis, ensure this library was selected.
- if (!_selectAllLibraries && !_selectedLibraries.Contains(folder.Name))
+ Plugin.Instance!.TotalQueued = 0;
+
+ LoadAnalysisSettings();
+
+ // For all selected libraries, enqueue all contained episodes.
+ foreach (var folder in _libraryManager.GetVirtualFolders())
{
- _logger.LogDebug("Not analyzing library \"{Name}\": not selected by user", folder.Name);
- continue;
- }
-
- _logger.LogInformation("Running enqueue of items in library {Name}", folder.Name);
-
- // Some virtual folders don't have a proper item id.
- if (!Guid.TryParse(folder.ItemId, out var folderId))
- {
- continue;
- }
-
- try
- {
- QueueLibraryContents(folderId);
- }
- catch (Exception ex)
- {
- _logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex);
- }
- }
-
- Plugin.Instance.TotalSeasons = _queuedEpisodes.Count;
- Plugin.Instance.QueuedMediaItems.Clear();
- foreach (var kvp in _queuedEpisodes)
- {
- Plugin.Instance.QueuedMediaItems.TryAdd(kvp.Key, kvp.Value);
- }
-
- return _queuedEpisodes;
- }
-
- ///
- /// Loads the list of libraries which have been selected for analysis and the minimum intro duration.
- /// Settings which have been modified from the defaults are logged.
- ///
- private void LoadAnalysisSettings()
- {
- var config = Plugin.Instance!.Configuration;
-
- // Store the analysis percent
- _analysisPercent = Convert.ToDouble(config.AnalysisPercent) / 100;
-
- _selectAllLibraries = config.SelectAllLibraries;
-
- if (!_selectAllLibraries)
- {
- // Get the list of library names which have been selected for analysis, ignoring whitespace and empty entries.
- _selectedLibraries = [.. config.SelectedLibraries.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
-
- // If any libraries have been selected for analysis, log their names.
- _logger.LogInformation("Limiting analysis to the following libraries: {Selected}", _selectedLibraries);
- }
- else
- {
- _logger.LogDebug("Not limiting analysis by library name");
- }
-
- // If analysis settings have been changed from the default, log the modified settings.
- if (config.AnalysisLengthLimit != 10 || config.AnalysisPercent != 25 || config.MinimumIntroDuration != 15)
- {
- _logger.LogInformation(
- "Analysis settings have been changed to: {Percent}% / {Minutes}m and a minimum of {Minimum}s",
- config.AnalysisPercent,
- config.AnalysisLengthLimit,
- config.MinimumIntroDuration);
- }
- }
-
- private void QueueLibraryContents(Guid id)
- {
- _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 = id,
- OrderBy = [(ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Ascending),],
- IncludeItemTypes = [BaseItemKind.Episode],
- Recursive = true,
- IsVirtualItem = false
- };
-
- 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.LogDebug("Item {Name} is not an episode", item.Name);
- continue;
- }
-
- QueueEpisode(episode);
- }
-
- _logger.LogDebug("Queued {Count} episodes", items.Count);
- }
-
- private void QueueEpisode(Episode episode)
- {
- var pluginInstance = Plugin.Instance ?? throw new InvalidOperationException("Plugin instance was null");
-
- if (string.IsNullOrEmpty(episode.Path))
- {
- _logger.LogWarning(
- "Not queuing episode \"{Name}\" from series \"{Series}\" ({Id}) as no path was provided by Jellyfin",
- episode.Name,
- episode.SeriesName,
- episode.Id);
- return;
- }
-
- // Allocate a new list for each new season
- var seasonId = GetSeasonId(episode);
- if (!_queuedEpisodes.TryGetValue(seasonId, out var seasonEpisodes))
- {
- seasonEpisodes = [];
- _queuedEpisodes[seasonId] = seasonEpisodes;
- }
-
- if (seasonEpisodes.Any(e => e.EpisodeId == episode.Id))
- {
- _logger.LogDebug(
- "\"{Name}\" from series \"{Series}\" ({Id}) is already queued",
- episode.Name,
- episode.SeriesName,
- episode.Id);
- return;
- }
-
- var isAnime = seasonEpisodes.FirstOrDefault()?.IsAnime ??
- (pluginInstance.GetItem(episode.SeriesId) is Series series &&
- (series.Tags.Contains("anime", StringComparison.OrdinalIgnoreCase) ||
- series.Genres.Contains("anime", StringComparison.OrdinalIgnoreCase)));
-
- // 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 = Math.Min(
- duration >= 5 * 60 ? duration * _analysisPercent : duration,
- 60 * pluginInstance.Configuration.AnalysisLengthLimit);
-
- // Queue the episode for analysis
- var maxCreditsDuration = pluginInstance.Configuration.MaximumCreditsDuration;
- seasonEpisodes.Add(new QueuedEpisode
- {
- SeriesName = episode.SeriesName,
- SeasonNumber = episode.AiredSeasonNumber ?? 0,
- SeriesId = episode.SeriesId,
- EpisodeId = episode.Id,
- Name = episode.Name,
- IsAnime = isAnime,
- Path = episode.Path,
- Duration = Convert.ToInt32(duration),
- IntroFingerprintEnd = Convert.ToInt32(fingerprintDuration),
- CreditsFingerprintStart = Convert.ToInt32(duration - maxCreditsDuration),
- });
-
- pluginInstance.TotalQueued++;
- }
-
- private Guid GetSeasonId(Episode episode)
- {
- if (episode.ParentIndexNumber == 0 && episode.AiredSeasonNumber != 0) // In-season special
- {
- foreach (var kvp in _queuedEpisodes)
- {
- var first = kvp.Value.FirstOrDefault();
- if (first?.SeriesId == episode.SeriesId &&
- first.SeasonNumber == episode.AiredSeasonNumber)
+ // If libraries have been selected for analysis, ensure this library was selected.
+ if (!_selectAllLibraries && !_selectedLibraries.Contains(folder.Name))
{
- return kvp.Key;
+ _logger.LogDebug("Not analyzing library \"{Name}\": not selected by user", folder.Name);
+ continue;
}
- }
- }
- return episode.SeasonId;
- }
+ _logger.LogInformation("Running enqueue of items in library {Name}", folder.Name);
- ///
- /// Verify that a collection of queued media items still exist in Jellyfin and in storage.
- /// This is done to ensure that we don't analyze items that were deleted between the call to GetMediaItems() and popping them from the queue.
- ///
- /// Queued media items.
- /// Analysis mode.
- /// Media items that have been verified to exist in Jellyfin and in storage.
- public (IReadOnlyList VerifiedItems, IReadOnlyCollection RequiredModes)
- VerifyQueue(IReadOnlyList candidates, IReadOnlyCollection modes)
- {
- var verified = new List();
- var reqModes = new HashSet();
-
- foreach (var candidate in candidates)
- {
- try
- {
- var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId);
-
- if (!File.Exists(path))
+ // Some virtual folders don't have a proper item id.
+ if (!Guid.TryParse(folder.ItemId, out var folderId))
{
continue;
}
- verified.Add(candidate);
-
- foreach (var mode in modes)
+ try
{
- if (candidate.State.IsAnalyzed(mode) || candidate.State.IsBlacklisted(mode))
+ QueueLibraryContents(folderId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex);
+ }
+ }
+
+ Plugin.Instance.TotalSeasons = _queuedEpisodes.Count;
+ Plugin.Instance.QueuedMediaItems.Clear();
+ foreach (var kvp in _queuedEpisodes)
+ {
+ Plugin.Instance.QueuedMediaItems.TryAdd(kvp.Key, kvp.Value);
+ }
+
+ return _queuedEpisodes;
+ }
+
+ ///
+ /// Loads the list of libraries which have been selected for analysis and the minimum intro duration.
+ /// Settings which have been modified from the defaults are logged.
+ ///
+ private void LoadAnalysisSettings()
+ {
+ var config = Plugin.Instance!.Configuration;
+
+ // Store the analysis percent
+ _analysisPercent = Convert.ToDouble(config.AnalysisPercent) / 100;
+
+ _selectAllLibraries = config.SelectAllLibraries;
+
+ if (!_selectAllLibraries)
+ {
+ // Get the list of library names which have been selected for analysis, ignoring whitespace and empty entries.
+ _selectedLibraries = [.. config.SelectedLibraries.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+
+ // If any libraries have been selected for analysis, log their names.
+ _logger.LogInformation("Limiting analysis to the following libraries: {Selected}", _selectedLibraries);
+ }
+ else
+ {
+ _logger.LogDebug("Not limiting analysis by library name");
+ }
+
+ // If analysis settings have been changed from the default, log the modified settings.
+ if (config.AnalysisLengthLimit != 10 || config.AnalysisPercent != 25 || config.MinimumIntroDuration != 15)
+ {
+ _logger.LogInformation(
+ "Analysis settings have been changed to: {Percent}% / {Minutes}m and a minimum of {Minimum}s",
+ config.AnalysisPercent,
+ config.AnalysisLengthLimit,
+ config.MinimumIntroDuration);
+ }
+ }
+
+ private void QueueLibraryContents(Guid id)
+ {
+ _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 = id,
+ OrderBy = [(ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Ascending),],
+ IncludeItemTypes = [BaseItemKind.Episode],
+ Recursive = true,
+ IsVirtualItem = false
+ };
+
+ 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.LogDebug("Item {Name} is not an episode", item.Name);
+ continue;
+ }
+
+ QueueEpisode(episode);
+ }
+
+ _logger.LogDebug("Queued {Count} episodes", items.Count);
+ }
+
+ private void QueueEpisode(Episode episode)
+ {
+ var pluginInstance = Plugin.Instance ?? throw new InvalidOperationException("Plugin instance was null");
+
+ if (string.IsNullOrEmpty(episode.Path))
+ {
+ _logger.LogWarning(
+ "Not queuing episode \"{Name}\" from series \"{Series}\" ({Id}) as no path was provided by Jellyfin",
+ episode.Name,
+ episode.SeriesName,
+ episode.Id);
+ return;
+ }
+
+ // Allocate a new list for each new season
+ var seasonId = GetSeasonId(episode);
+ if (!_queuedEpisodes.TryGetValue(seasonId, out var seasonEpisodes))
+ {
+ seasonEpisodes = [];
+ _queuedEpisodes[seasonId] = seasonEpisodes;
+ }
+
+ if (seasonEpisodes.Any(e => e.EpisodeId == episode.Id))
+ {
+ _logger.LogDebug(
+ "\"{Name}\" from series \"{Series}\" ({Id}) is already queued",
+ episode.Name,
+ episode.SeriesName,
+ episode.Id);
+ return;
+ }
+
+ var isAnime = seasonEpisodes.FirstOrDefault()?.IsAnime ??
+ (pluginInstance.GetItem(episode.SeriesId) is Series series &&
+ (series.Tags.Contains("anime", StringComparison.OrdinalIgnoreCase) ||
+ series.Genres.Contains("anime", StringComparison.OrdinalIgnoreCase)));
+
+ // 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 = Math.Min(
+ duration >= 5 * 60 ? duration * _analysisPercent : duration,
+ 60 * pluginInstance.Configuration.AnalysisLengthLimit);
+
+ // Queue the episode for analysis
+ var maxCreditsDuration = pluginInstance.Configuration.MaximumCreditsDuration;
+ seasonEpisodes.Add(new QueuedEpisode
+ {
+ SeriesName = episode.SeriesName,
+ SeasonNumber = episode.AiredSeasonNumber ?? 0,
+ SeriesId = episode.SeriesId,
+ EpisodeId = episode.Id,
+ Name = episode.Name,
+ IsAnime = isAnime,
+ Path = episode.Path,
+ Duration = Convert.ToInt32(duration),
+ IntroFingerprintEnd = Convert.ToInt32(fingerprintDuration),
+ CreditsFingerprintStart = Convert.ToInt32(duration - maxCreditsDuration),
+ });
+
+ pluginInstance.TotalQueued++;
+ }
+
+ private Guid GetSeasonId(Episode episode)
+ {
+ if (episode.ParentIndexNumber == 0 && episode.AiredSeasonNumber != 0) // In-season special
+ {
+ foreach (var kvp in _queuedEpisodes)
+ {
+ var first = kvp.Value.FirstOrDefault();
+ if (first?.SeriesId == episode.SeriesId &&
+ first.SeasonNumber == episode.AiredSeasonNumber)
+ {
+ return kvp.Key;
+ }
+ }
+ }
+
+ return episode.SeasonId;
+ }
+
+ ///
+ /// Verify that a collection of queued media items still exist in Jellyfin and in storage.
+ /// This is done to ensure that we don't analyze items that were deleted between the call to GetMediaItems() and popping them from the queue.
+ ///
+ /// Queued media items.
+ /// Analysis mode.
+ /// Media items that have been verified to exist in Jellyfin and in storage.
+ public (IReadOnlyList VerifiedItems, IReadOnlyCollection RequiredModes)
+ VerifyQueue(IReadOnlyList candidates, IReadOnlyCollection modes)
+ {
+ var verified = new List();
+ var reqModes = new HashSet();
+
+ foreach (var candidate in candidates)
+ {
+ try
+ {
+ var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId);
+
+ if (!File.Exists(path))
{
continue;
}
- bool isAnalyzed = mode == AnalysisMode.Introduction
- ? Plugin.Instance!.Intros.ContainsKey(candidate.EpisodeId)
- : Plugin.Instance!.Credits.ContainsKey(candidate.EpisodeId);
+ verified.Add(candidate);
- if (isAnalyzed)
+ foreach (var mode in modes)
{
- candidate.State.SetAnalyzed(mode, true);
- }
- else
- {
- reqModes.Add(mode);
+ if (candidate.State.IsAnalyzed(mode) || candidate.State.IsBlacklisted(mode))
+ {
+ continue;
+ }
+
+ bool isAnalyzed = mode == AnalysisMode.Introduction
+ ? Plugin.Instance!.Intros.ContainsKey(candidate.EpisodeId)
+ : Plugin.Instance!.Credits.ContainsKey(candidate.EpisodeId);
+
+ if (isAnalyzed)
+ {
+ candidate.State.SetAnalyzed(mode, true);
+ }
+ else
+ {
+ reqModes.Add(mode);
+ }
}
}
+ catch (Exception ex)
+ {
+ _logger.LogDebug(
+ "Skipping analysis of {Name} ({Id}): {Exception}",
+ candidate.Name,
+ candidate.EpisodeId,
+ ex);
+ }
}
- catch (Exception ex)
- {
- _logger.LogDebug(
- "Skipping analysis of {Name} ({Id}): {Exception}",
- candidate.Name,
- candidate.EpisodeId,
- ex);
- }
- }
- return (verified, reqModes);
+ return (verified, reqModes);
+ }
}
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
index 7050830..1e32252 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Helper;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs b/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs
index e5f6c42..89a856d 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/PluginServiceRegistrator.cs
@@ -1,4 +1,5 @@
using ConfusedPolarBear.Plugin.IntroSkipper.Providers;
+using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs
index 7f8c49f..f2e05e5 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/CleanCacheTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/CleanCacheTask.cs
index f644db1..6f096de 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/CleanCacheTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/CleanCacheTask.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs
index 7706da4..394e86c 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs
index 72e8fee..aeca1a0 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs
index d7b17b1..894c3b0 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkip.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkip.cs
index f019056..e733d31 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkip.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkip.cs
@@ -15,216 +15,217 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer;
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Automatically skip past introduction sequences.
-/// Commands clients to seek to the end of the intro as soon as they start playing it.
-///
-///
-/// Initializes a new instance of the class.
-///
-/// User data manager.
-/// Session manager.
-/// Logger.
-public class AutoSkip(
- IUserDataManager userDataManager,
- ISessionManager sessionManager,
- ILogger logger) : IHostedService, IDisposable
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
{
- private readonly object _sentSeekCommandLock = new();
-
- private ILogger _logger = logger;
- private IUserDataManager _userDataManager = userDataManager;
- private ISessionManager _sessionManager = sessionManager;
- private Timer _playbackTimer = new(1000);
- private Dictionary _sentSeekCommand = [];
- private HashSet _clientList = [];
-
- private void AutoSkipChanged(object? sender, BasePluginConfiguration e)
+ ///
+ /// Automatically skip past introduction sequences.
+ /// Commands clients to seek to the end of the intro as soon as they start playing it.
+ ///
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// User data manager.
+ /// Session manager.
+ /// Logger.
+ public class AutoSkip(
+ IUserDataManager userDataManager,
+ ISessionManager sessionManager,
+ ILogger logger) : IHostedService, IDisposable
{
- var configuration = (PluginConfiguration)e;
- _clientList = [.. configuration.ClientList.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
- var newState = configuration.AutoSkip || _clientList.Count > 0;
- _logger.LogDebug("Setting playback timer enabled to {NewState}", newState);
- _playbackTimer.Enabled = newState;
- }
+ private readonly object _sentSeekCommandLock = new();
- private void UserDataManager_UserDataSaved(object? sender, UserDataSaveEventArgs e)
- {
- var itemId = e.Item.Id;
- var newState = false;
- var episodeNumber = e.Item.IndexNumber.GetValueOrDefault(-1);
+ private ILogger _logger = logger;
+ private IUserDataManager _userDataManager = userDataManager;
+ private ISessionManager _sessionManager = sessionManager;
+ private Timer _playbackTimer = new(1000);
+ private Dictionary _sentSeekCommand = [];
+ private HashSet _clientList = [];
- // Ignore all events except playback start & end
- if (e.SaveReason != UserDataSaveReason.PlaybackStart && e.SaveReason != UserDataSaveReason.PlaybackFinished)
+ private void AutoSkipChanged(object? sender, BasePluginConfiguration e)
{
- return;
+ var configuration = (PluginConfiguration)e;
+ _clientList = [.. configuration.ClientList.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+ var newState = configuration.AutoSkip || _clientList.Count > 0;
+ _logger.LogDebug("Setting playback timer enabled to {NewState}", newState);
+ _playbackTimer.Enabled = newState;
}
- // Lookup the session for this item.
- SessionInfo? session = null;
-
- try
+ private void UserDataManager_UserDataSaved(object? sender, UserDataSaveEventArgs e)
{
- foreach (var needle in _sessionManager.Sessions)
- {
- if (needle.UserId == e.UserId && needle.NowPlayingItem.Id == itemId)
- {
- session = needle;
- break;
- }
- }
+ var itemId = e.Item.Id;
+ var newState = false;
+ var episodeNumber = e.Item.IndexNumber.GetValueOrDefault(-1);
- if (session == null)
+ // Ignore all events except playback start & end
+ if (e.SaveReason != UserDataSaveReason.PlaybackStart && e.SaveReason != UserDataSaveReason.PlaybackFinished)
{
- _logger.LogInformation("Unable to find session for {Item}", itemId);
return;
}
- }
- catch (Exception ex) when (ex is NullReferenceException || ex is ResourceNotFoundException)
- {
- return;
- }
- // If this is the first episode in the season, and SkipFirstEpisode is false, pretend that we've already sent the seek command for this playback session.
- if (Plugin.Instance!.Configuration.SkipFirstEpisode && episodeNumber == 1)
- {
- newState = true;
- }
+ // Lookup the session for this item.
+ SessionInfo? session = null;
- // Reset the seek command state for this device.
- lock (_sentSeekCommandLock)
- {
- var device = session.DeviceId;
-
- _logger.LogDebug("Resetting seek command state for session {Session}", device);
- _sentSeekCommand[device] = newState;
- }
- }
-
- private void PlaybackTimer_Elapsed(object? sender, ElapsedEventArgs e)
- {
- foreach (var session in _sessionManager.Sessions.Where(s => Plugin.Instance!.Configuration.AutoSkip || _clientList.Contains(s.Client, StringComparer.OrdinalIgnoreCase)))
- {
- var deviceId = session.DeviceId;
- var itemId = session.NowPlayingItem.Id;
- var position = session.PlayState.PositionTicks / TimeSpan.TicksPerSecond;
-
- // Don't send the seek command more than once in the same session.
- lock (_sentSeekCommandLock)
+ try
{
- if (_sentSeekCommand.TryGetValue(deviceId, out var sent) && sent)
+ foreach (var needle in _sessionManager.Sessions)
{
- _logger.LogTrace("Already sent seek command for session {Session}", deviceId);
- continue;
+ if (needle.UserId == e.UserId && needle.NowPlayingItem.Id == itemId)
+ {
+ session = needle;
+ break;
+ }
+ }
+
+ if (session == null)
+ {
+ _logger.LogInformation("Unable to find session for {Item}", itemId);
+ return;
}
}
-
- // Assert that an intro was detected for this item.
- if (!Plugin.Instance!.Intros.TryGetValue(itemId, out var intro) || !intro.Valid)
+ catch (Exception ex) when (ex is NullReferenceException || ex is ResourceNotFoundException)
{
- continue;
+ return;
}
- // Seek is unreliable if called at the very start of an episode.
- var adjustedStart = Math.Max(1, intro.Start + Plugin.Instance.Configuration.SecondsOfIntroStartToPlay);
- var adjustedEnd = intro.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
-
- _logger.LogTrace(
- "Playback position is {Position}, intro runs from {Start} to {End}",
- position,
- adjustedStart,
- adjustedEnd);
-
- if (position < adjustedStart || position > adjustedEnd)
+ // If this is the first episode in the season, and SkipFirstEpisode is false, pretend that we've already sent the seek command for this playback session.
+ if (Plugin.Instance!.Configuration.SkipFirstEpisode && episodeNumber == 1)
{
- continue;
+ newState = true;
}
- // Notify the user that an introduction is being skipped for them.
- var notificationText = Plugin.Instance.Configuration.AutoSkipNotificationText;
- if (!string.IsNullOrWhiteSpace(notificationText))
- {
- _sessionManager.SendMessageCommand(
- session.Id,
- session.Id,
- new MessageCommand
- {
- Header = string.Empty, // some clients require header to be a string instead of null
- Text = notificationText,
- TimeoutMs = 2000,
- },
- CancellationToken.None);
- }
-
- _logger.LogDebug("Sending seek command to {Session}", deviceId);
-
- _sessionManager.SendPlaystateCommand(
- session.Id,
- session.Id,
- new PlaystateRequest
- {
- Command = PlaystateCommand.Seek,
- ControllingUserId = session.UserId.ToString(),
- SeekPositionTicks = (long)adjustedEnd * TimeSpan.TicksPerSecond,
- },
- CancellationToken.None);
-
- // Flag that we've sent the seek command so that it's not sent repeatedly
+ // Reset the seek command state for this device.
lock (_sentSeekCommandLock)
{
- _logger.LogTrace("Setting seek command state for session {Session}", deviceId);
- _sentSeekCommand[deviceId] = true;
+ var device = session.DeviceId;
+
+ _logger.LogDebug("Resetting seek command state for session {Session}", device);
+ _sentSeekCommand[device] = newState;
}
}
- }
- ///
- /// Dispose.
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Protected dispose.
- ///
- /// Dispose.
- protected virtual void Dispose(bool disposing)
- {
- if (!disposing)
+ private void PlaybackTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
- return;
+ foreach (var session in _sessionManager.Sessions.Where(s => Plugin.Instance!.Configuration.AutoSkip || _clientList.Contains(s.Client, StringComparer.OrdinalIgnoreCase)))
+ {
+ var deviceId = session.DeviceId;
+ var itemId = session.NowPlayingItem.Id;
+ var position = session.PlayState.PositionTicks / TimeSpan.TicksPerSecond;
+
+ // Don't send the seek command more than once in the same session.
+ lock (_sentSeekCommandLock)
+ {
+ if (_sentSeekCommand.TryGetValue(deviceId, out var sent) && sent)
+ {
+ _logger.LogTrace("Already sent seek command for session {Session}", deviceId);
+ continue;
+ }
+ }
+
+ // Assert that an intro was detected for this item.
+ if (!Plugin.Instance!.Intros.TryGetValue(itemId, out var intro) || !intro.Valid)
+ {
+ continue;
+ }
+
+ // Seek is unreliable if called at the very start of an episode.
+ var adjustedStart = Math.Max(1, intro.Start + Plugin.Instance.Configuration.SecondsOfIntroStartToPlay);
+ var adjustedEnd = intro.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
+
+ _logger.LogTrace(
+ "Playback position is {Position}, intro runs from {Start} to {End}",
+ position,
+ adjustedStart,
+ adjustedEnd);
+
+ if (position < adjustedStart || position > adjustedEnd)
+ {
+ continue;
+ }
+
+ // Notify the user that an introduction is being skipped for them.
+ var notificationText = Plugin.Instance.Configuration.AutoSkipNotificationText;
+ if (!string.IsNullOrWhiteSpace(notificationText))
+ {
+ _sessionManager.SendMessageCommand(
+ session.Id,
+ session.Id,
+ new MessageCommand
+ {
+ Header = string.Empty, // some clients require header to be a string instead of null
+ Text = notificationText,
+ TimeoutMs = 2000,
+ },
+ CancellationToken.None);
+ }
+
+ _logger.LogDebug("Sending seek command to {Session}", deviceId);
+
+ _sessionManager.SendPlaystateCommand(
+ session.Id,
+ session.Id,
+ new PlaystateRequest
+ {
+ Command = PlaystateCommand.Seek,
+ ControllingUserId = session.UserId.ToString(),
+ SeekPositionTicks = (long)adjustedEnd * TimeSpan.TicksPerSecond,
+ },
+ CancellationToken.None);
+
+ // Flag that we've sent the seek command so that it's not sent repeatedly
+ lock (_sentSeekCommandLock)
+ {
+ _logger.LogTrace("Setting seek command state for session {Session}", deviceId);
+ _sentSeekCommand[deviceId] = true;
+ }
+ }
}
- _playbackTimer.Stop();
- _playbackTimer.Dispose();
- }
+ ///
+ /// Dispose.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- ///
- public Task StartAsync(CancellationToken cancellationToken)
- {
- _logger.LogDebug("Setting up automatic skipping");
+ ///
+ /// Protected dispose.
+ ///
+ /// Dispose.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
- _userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
- Plugin.Instance!.ConfigurationChanged += AutoSkipChanged;
+ _playbackTimer.Stop();
+ _playbackTimer.Dispose();
+ }
- // Make the timer restart automatically and set enabled to match the configuration value.
- _playbackTimer.AutoReset = true;
- _playbackTimer.Elapsed += PlaybackTimer_Elapsed;
+ ///
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Setting up automatic skipping");
- AutoSkipChanged(null, Plugin.Instance.Configuration);
+ _userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
+ Plugin.Instance!.ConfigurationChanged += AutoSkipChanged;
- return Task.CompletedTask;
- }
+ // Make the timer restart automatically and set enabled to match the configuration value.
+ _playbackTimer.AutoReset = true;
+ _playbackTimer.Elapsed += PlaybackTimer_Elapsed;
- ///
- public Task StopAsync(CancellationToken cancellationToken)
- {
- _userDataManager.UserDataSaved -= UserDataManager_UserDataSaved;
- return Task.CompletedTask;
+ AutoSkipChanged(null, Plugin.Instance.Configuration);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _userDataManager.UserDataSaved -= UserDataManager_UserDataSaved;
+ return Task.CompletedTask;
+ }
}
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkipCredits.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkipCredits.cs
index 6898417..b0e6068 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkipCredits.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Services/AutoSkipCredits.cs
@@ -15,216 +15,217 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer;
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Automatically skip past credit sequences.
-/// Commands clients to seek to the end of the credits as soon as they start playing it.
-///
-///
-/// Initializes a new instance of the class.
-///
-/// User data manager.
-/// Session manager.
-/// Logger.
-public class AutoSkipCredits(
- IUserDataManager userDataManager,
- ISessionManager sessionManager,
- ILogger logger) : IHostedService, IDisposable
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
{
- private readonly object _sentSeekCommandLock = new();
-
- private ILogger _logger = logger;
- private IUserDataManager _userDataManager = userDataManager;
- private ISessionManager _sessionManager = sessionManager;
- private Timer _playbackTimer = new(1000);
- private Dictionary _sentSeekCommand = [];
- private HashSet _clientList = [];
-
- private void AutoSkipCreditChanged(object? sender, BasePluginConfiguration e)
+ ///
+ /// Automatically skip past credit sequences.
+ /// Commands clients to seek to the end of the credits as soon as they start playing it.
+ ///
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// User data manager.
+ /// Session manager.
+ /// Logger.
+ public class AutoSkipCredits(
+ IUserDataManager userDataManager,
+ ISessionManager sessionManager,
+ ILogger logger) : IHostedService, IDisposable
{
- var configuration = (PluginConfiguration)e;
- _clientList = [.. configuration.ClientList.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
- var newState = configuration.AutoSkipCredits || _clientList.Count > 0;
- _logger.LogDebug("Setting playback timer enabled to {NewState}", newState);
- _playbackTimer.Enabled = newState;
- }
+ private readonly object _sentSeekCommandLock = new();
- private void UserDataManager_UserDataSaved(object? sender, UserDataSaveEventArgs e)
- {
- var itemId = e.Item.Id;
- var newState = false;
- var episodeNumber = e.Item.IndexNumber.GetValueOrDefault(-1);
+ private ILogger _logger = logger;
+ private IUserDataManager _userDataManager = userDataManager;
+ private ISessionManager _sessionManager = sessionManager;
+ private Timer _playbackTimer = new(1000);
+ private Dictionary _sentSeekCommand = [];
+ private HashSet _clientList = [];
- // Ignore all events except playback start & end
- if (e.SaveReason != UserDataSaveReason.PlaybackStart && e.SaveReason != UserDataSaveReason.PlaybackFinished)
+ private void AutoSkipCreditChanged(object? sender, BasePluginConfiguration e)
{
- return;
+ var configuration = (PluginConfiguration)e;
+ _clientList = [.. configuration.ClientList.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+ var newState = configuration.AutoSkipCredits || _clientList.Count > 0;
+ _logger.LogDebug("Setting playback timer enabled to {NewState}", newState);
+ _playbackTimer.Enabled = newState;
}
- // Lookup the session for this item.
- SessionInfo? session = null;
-
- try
+ private void UserDataManager_UserDataSaved(object? sender, UserDataSaveEventArgs e)
{
- foreach (var needle in _sessionManager.Sessions)
- {
- if (needle.UserId == e.UserId && needle.NowPlayingItem.Id == itemId)
- {
- session = needle;
- break;
- }
- }
+ var itemId = e.Item.Id;
+ var newState = false;
+ var episodeNumber = e.Item.IndexNumber.GetValueOrDefault(-1);
- if (session == null)
+ // Ignore all events except playback start & end
+ if (e.SaveReason != UserDataSaveReason.PlaybackStart && e.SaveReason != UserDataSaveReason.PlaybackFinished)
{
- _logger.LogInformation("Unable to find session for {Item}", itemId);
return;
}
- }
- catch (Exception ex) when (ex is NullReferenceException || ex is ResourceNotFoundException)
- {
- return;
- }
- // If this is the first episode in the season, and SkipFirstEpisode is false, pretend that we've already sent the seek command for this playback session.
- if (Plugin.Instance!.Configuration.SkipFirstEpisode && episodeNumber == 1)
- {
- newState = true;
- }
+ // Lookup the session for this item.
+ SessionInfo? session = null;
- // Reset the seek command state for this device.
- lock (_sentSeekCommandLock)
- {
- var device = session.DeviceId;
-
- _logger.LogDebug("Resetting seek command state for session {Session}", device);
- _sentSeekCommand[device] = newState;
- }
- }
-
- private void PlaybackTimer_Elapsed(object? sender, ElapsedEventArgs e)
- {
- foreach (var session in _sessionManager.Sessions.Where(s => Plugin.Instance!.Configuration.AutoSkipCredits || _clientList.Contains(s.Client, StringComparer.OrdinalIgnoreCase)))
- {
- var deviceId = session.DeviceId;
- var itemId = session.NowPlayingItem.Id;
- var position = session.PlayState.PositionTicks / TimeSpan.TicksPerSecond;
-
- // Don't send the seek command more than once in the same session.
- lock (_sentSeekCommandLock)
+ try
{
- if (_sentSeekCommand.TryGetValue(deviceId, out var sent) && sent)
+ foreach (var needle in _sessionManager.Sessions)
{
- _logger.LogTrace("Already sent seek command for session {Session}", deviceId);
- continue;
+ if (needle.UserId == e.UserId && needle.NowPlayingItem.Id == itemId)
+ {
+ session = needle;
+ break;
+ }
+ }
+
+ if (session == null)
+ {
+ _logger.LogInformation("Unable to find session for {Item}", itemId);
+ return;
}
}
-
- // Assert that credits were detected for this item.
- if (!Plugin.Instance!.Credits.TryGetValue(itemId, out var credit) || !credit.Valid)
+ catch (Exception ex) when (ex is NullReferenceException || ex is ResourceNotFoundException)
{
- continue;
+ return;
}
- // Seek is unreliable if called at the very end of an episode.
- var adjustedStart = credit.Start + Plugin.Instance.Configuration.SecondsOfCreditsStartToPlay;
- var adjustedEnd = credit.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
-
- _logger.LogTrace(
- "Playback position is {Position}, credits run from {Start} to {End}",
- position,
- adjustedStart,
- adjustedEnd);
-
- if (position < adjustedStart || position > adjustedEnd)
+ // If this is the first episode in the season, and SkipFirstEpisode is false, pretend that we've already sent the seek command for this playback session.
+ if (Plugin.Instance!.Configuration.SkipFirstEpisode && episodeNumber == 1)
{
- continue;
+ newState = true;
}
- // Notify the user that credits are being skipped for them.
- var notificationText = Plugin.Instance!.Configuration.AutoSkipCreditsNotificationText;
- if (!string.IsNullOrWhiteSpace(notificationText))
- {
- _sessionManager.SendMessageCommand(
- session.Id,
- session.Id,
- new MessageCommand
- {
- Header = string.Empty, // some clients require header to be a string instead of null
- Text = notificationText,
- TimeoutMs = 2000,
- },
- CancellationToken.None);
- }
-
- _logger.LogDebug("Sending seek command to {Session}", deviceId);
-
- _sessionManager.SendPlaystateCommand(
- session.Id,
- session.Id,
- new PlaystateRequest
- {
- Command = PlaystateCommand.Seek,
- ControllingUserId = session.UserId.ToString(),
- SeekPositionTicks = (long)adjustedEnd * TimeSpan.TicksPerSecond,
- },
- CancellationToken.None);
-
- // Flag that we've sent the seek command so that it's not sent repeatedly
+ // Reset the seek command state for this device.
lock (_sentSeekCommandLock)
{
- _logger.LogTrace("Setting seek command state for session {Session}", deviceId);
- _sentSeekCommand[deviceId] = true;
+ var device = session.DeviceId;
+
+ _logger.LogDebug("Resetting seek command state for session {Session}", device);
+ _sentSeekCommand[device] = newState;
}
}
- }
- ///
- /// Dispose.
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Protected dispose.
- ///
- /// Dispose.
- protected virtual void Dispose(bool disposing)
- {
- if (!disposing)
+ private void PlaybackTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
- return;
+ foreach (var session in _sessionManager.Sessions.Where(s => Plugin.Instance!.Configuration.AutoSkipCredits || _clientList.Contains(s.Client, StringComparer.OrdinalIgnoreCase)))
+ {
+ var deviceId = session.DeviceId;
+ var itemId = session.NowPlayingItem.Id;
+ var position = session.PlayState.PositionTicks / TimeSpan.TicksPerSecond;
+
+ // Don't send the seek command more than once in the same session.
+ lock (_sentSeekCommandLock)
+ {
+ if (_sentSeekCommand.TryGetValue(deviceId, out var sent) && sent)
+ {
+ _logger.LogTrace("Already sent seek command for session {Session}", deviceId);
+ continue;
+ }
+ }
+
+ // Assert that credits were detected for this item.
+ if (!Plugin.Instance!.Credits.TryGetValue(itemId, out var credit) || !credit.Valid)
+ {
+ continue;
+ }
+
+ // Seek is unreliable if called at the very end of an episode.
+ var adjustedStart = credit.Start + Plugin.Instance.Configuration.SecondsOfCreditsStartToPlay;
+ var adjustedEnd = credit.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
+
+ _logger.LogTrace(
+ "Playback position is {Position}, credits run from {Start} to {End}",
+ position,
+ adjustedStart,
+ adjustedEnd);
+
+ if (position < adjustedStart || position > adjustedEnd)
+ {
+ continue;
+ }
+
+ // Notify the user that credits are being skipped for them.
+ var notificationText = Plugin.Instance!.Configuration.AutoSkipCreditsNotificationText;
+ if (!string.IsNullOrWhiteSpace(notificationText))
+ {
+ _sessionManager.SendMessageCommand(
+ session.Id,
+ session.Id,
+ new MessageCommand
+ {
+ Header = string.Empty, // some clients require header to be a string instead of null
+ Text = notificationText,
+ TimeoutMs = 2000,
+ },
+ CancellationToken.None);
+ }
+
+ _logger.LogDebug("Sending seek command to {Session}", deviceId);
+
+ _sessionManager.SendPlaystateCommand(
+ session.Id,
+ session.Id,
+ new PlaystateRequest
+ {
+ Command = PlaystateCommand.Seek,
+ ControllingUserId = session.UserId.ToString(),
+ SeekPositionTicks = (long)adjustedEnd * TimeSpan.TicksPerSecond,
+ },
+ CancellationToken.None);
+
+ // Flag that we've sent the seek command so that it's not sent repeatedly
+ lock (_sentSeekCommandLock)
+ {
+ _logger.LogTrace("Setting seek command state for session {Session}", deviceId);
+ _sentSeekCommand[deviceId] = true;
+ }
+ }
}
- _playbackTimer.Stop();
- _playbackTimer.Dispose();
- }
+ ///
+ /// Dispose.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- ///
- public Task StartAsync(CancellationToken cancellationToken)
- {
- _logger.LogDebug("Setting up automatic credit skipping");
+ ///
+ /// Protected dispose.
+ ///
+ /// Dispose.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
- _userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
- Plugin.Instance!.ConfigurationChanged += AutoSkipCreditChanged;
+ _playbackTimer.Stop();
+ _playbackTimer.Dispose();
+ }
- // Make the timer restart automatically and set enabled to match the configuration value.
- _playbackTimer.AutoReset = true;
- _playbackTimer.Elapsed += PlaybackTimer_Elapsed;
+ ///
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Setting up automatic credit skipping");
- AutoSkipCreditChanged(null, Plugin.Instance.Configuration);
+ _userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
+ Plugin.Instance!.ConfigurationChanged += AutoSkipCreditChanged;
- return Task.CompletedTask;
- }
+ // Make the timer restart automatically and set enabled to match the configuration value.
+ _playbackTimer.AutoReset = true;
+ _playbackTimer.Elapsed += PlaybackTimer_Elapsed;
- ///
- public Task StopAsync(CancellationToken cancellationToken)
- {
- _userDataManager.UserDataSaved -= UserDataManager_UserDataSaved;
- return Task.CompletedTask;
+ AutoSkipCreditChanged(null, Plugin.Instance.Configuration);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _userDataManager.UserDataSaved -= UserDataManager_UserDataSaved;
+ return Task.CompletedTask;
+ }
}
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs
index 5fc196a..d6735c1 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Services/Entrypoint.cs
@@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
+using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -13,314 +14,315 @@ using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Server entrypoint.
-///
-public sealed class Entrypoint : IHostedService, IDisposable
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
{
- private readonly ITaskManager _taskManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
- private readonly ILoggerFactory _loggerFactory;
- private readonly HashSet _seasonsToAnalyze = [];
- private readonly Timer _queueTimer;
- private static readonly ManualResetEventSlim _autoTaskCompletEvent = new(false);
- private PluginConfiguration _config;
- private bool _analyzeAgain;
- private static CancellationTokenSource? _cancellationTokenSource;
-
///
- /// Initializes a new instance of the class.
+ /// Server entrypoint.
///
- /// Library manager.
- /// Task manager.
- /// Logger.
- /// Logger factory.
- public Entrypoint(
- ILibraryManager libraryManager,
- ITaskManager taskManager,
- ILogger logger,
- ILoggerFactory loggerFactory)
+ public sealed class Entrypoint : IHostedService, IDisposable
{
- _libraryManager = libraryManager;
- _taskManager = taskManager;
- _logger = logger;
- _loggerFactory = loggerFactory;
+ private readonly ITaskManager _taskManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly HashSet _seasonsToAnalyze = [];
+ private readonly Timer _queueTimer;
+ private static readonly ManualResetEventSlim _autoTaskCompletEvent = new(false);
+ private PluginConfiguration _config;
+ private bool _analyzeAgain;
+ private static CancellationTokenSource? _cancellationTokenSource;
- _config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
- _queueTimer = new Timer(
- OnTimerCallback,
- null,
- Timeout.InfiniteTimeSpan,
- Timeout.InfiniteTimeSpan);
- }
-
- ///
- /// Gets State of the automatic task.
- ///
- public static TaskState AutomaticTaskState
- {
- get
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Library manager.
+ /// Task manager.
+ /// Logger.
+ /// Logger factory.
+ public Entrypoint(
+ ILibraryManager libraryManager,
+ ITaskManager taskManager,
+ ILogger logger,
+ ILoggerFactory loggerFactory)
{
- if (_cancellationTokenSource is not null)
+ _libraryManager = libraryManager;
+ _taskManager = taskManager;
+ _logger = logger;
+ _loggerFactory = loggerFactory;
+
+ _config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
+ _queueTimer = new Timer(
+ OnTimerCallback,
+ null,
+ Timeout.InfiniteTimeSpan,
+ Timeout.InfiniteTimeSpan);
+ }
+
+ ///
+ /// Gets State of the automatic task.
+ ///
+ public static TaskState AutomaticTaskState
+ {
+ get
{
- return _cancellationTokenSource.IsCancellationRequested
- ? TaskState.Cancelling
- : TaskState.Running;
- }
+ if (_cancellationTokenSource is not null)
+ {
+ return _cancellationTokenSource.IsCancellationRequested
+ ? TaskState.Cancelling
+ : TaskState.Running;
+ }
- return TaskState.Idle;
- }
- }
-
- ///
- public Task StartAsync(CancellationToken cancellationToken)
- {
- _libraryManager.ItemAdded += OnItemAdded;
- _libraryManager.ItemUpdated += OnItemModified;
- _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);
- }
-
- return Task.CompletedTask;
- }
-
- ///
- public Task StopAsync(CancellationToken cancellationToken)
- {
- _libraryManager.ItemAdded -= OnItemAdded;
- _libraryManager.ItemUpdated -= OnItemModified;
- _taskManager.TaskCompleted -= OnLibraryRefresh;
-
- // 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)
- {
- // 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();
- }
-
- ///
- /// 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();
- }
-
- ///
- /// TaskManager task ended.
- ///
- /// The sending entity.
- /// The .
- private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs)
- {
- // Don't do anything if auto detection is disabled
- if (!_config.AutoDetectIntros && !_config.AutoDetectCredits)
- {
- return;
- }
-
- 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;
-
- ///
- /// Start timer to debounce analyzing.
- ///
- private void StartTimer()
- {
- if (AutomaticTaskState == TaskState.Running)
- {
- _analyzeAgain = true;
- }
- else if (AutomaticTaskState == TaskState.Idle)
- {
- _logger.LogDebug("Media Library changed, analyzis will start soon!");
- _queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
- }
- }
-
- ///
- /// Wait for timer callback to be completed.
- ///
- private void OnTimerCallback(object? state)
- {
- try
- {
- PerformAnalysis();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in PerformAnalysis");
- }
-
- // Clean up
- _cancellationTokenSource = null;
- _autoTaskCompletEvent.Set();
- }
-
- ///
- /// Wait for timer to be completed.
- ///
- private void PerformAnalysis()
- {
- _logger.LogInformation("Initiate automatic analysis task.");
- _autoTaskCompletEvent.Reset();
-
- using (_cancellationTokenSource = new CancellationTokenSource())
- using (ScheduledTaskSemaphore.Acquire(_cancellationTokenSource.Token))
- {
- 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)
- {
- 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();
+ return TaskState.Idle;
}
}
- }
- ///
- /// Method to cancel the automatic task.
- ///
- /// Cancellation token.
- public static void CancelAutomaticTask(CancellationToken cancellationToken)
- {
- if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
+ ///
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _libraryManager.ItemAdded += OnItemAdded;
+ _libraryManager.ItemUpdated += OnItemModified;
+ _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);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _libraryManager.ItemAdded -= OnItemAdded;
+ _libraryManager.ItemUpdated -= OnItemModified;
+ _taskManager.TaskCompleted -= OnLibraryRefresh;
+
+ // 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)
+ {
+ // 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();
+ }
+
+ ///
+ /// 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();
+ }
+
+ ///
+ /// TaskManager task ended.
+ ///
+ /// The sending entity.
+ /// The .
+ private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs)
+ {
+ // Don't do anything if auto detection is disabled
+ if (!_config.AutoDetectIntros && !_config.AutoDetectCredits)
+ {
+ return;
+ }
+
+ 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;
+
+ ///
+ /// Start timer to debounce analyzing.
+ ///
+ private void StartTimer()
+ {
+ if (AutomaticTaskState == TaskState.Running)
+ {
+ _analyzeAgain = true;
+ }
+ else if (AutomaticTaskState == TaskState.Idle)
+ {
+ _logger.LogDebug("Media Library changed, analyzis will start soon!");
+ _queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
+ }
+ }
+
+ ///
+ /// Wait for timer callback to be completed.
+ ///
+ private void OnTimerCallback(object? state)
{
try
{
- _cancellationTokenSource.Cancel();
+ PerformAnalysis();
}
- catch (ObjectDisposedException)
+ catch (Exception ex)
{
- _cancellationTokenSource = null;
+ _logger.LogError(ex, "Error in PerformAnalysis");
+ }
+
+ // Clean up
+ _cancellationTokenSource = null;
+ _autoTaskCompletEvent.Set();
+ }
+
+ ///
+ /// Wait for timer to be completed.
+ ///
+ private void PerformAnalysis()
+ {
+ _logger.LogInformation("Initiate automatic analysis task.");
+ _autoTaskCompletEvent.Reset();
+
+ using (_cancellationTokenSource = new CancellationTokenSource())
+ using (ScheduledTaskSemaphore.Acquire(_cancellationTokenSource.Token))
+ {
+ 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)
+ {
+ 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();
+ }
}
}
- _autoTaskCompletEvent.Wait(TimeSpan.FromSeconds(60), cancellationToken); // Wait for the signal
- }
+ ///
+ /// Method to cancel the automatic task.
+ ///
+ /// Cancellation token.
+ public static void CancelAutomaticTask(CancellationToken cancellationToken)
+ {
+ if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
+ {
+ try
+ {
+ _cancellationTokenSource.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+ _cancellationTokenSource = null;
+ }
+ }
- ///
- public void Dispose()
- {
- _queueTimer.Dispose();
- _cancellationTokenSource?.Dispose();
- _autoTaskCompletEvent.Dispose();
+ _autoTaskCompletEvent.Wait(TimeSpan.FromSeconds(60), cancellationToken); // Wait for the signal
+ }
+
+ ///
+ public void Dispose()
+ {
+ _queueTimer.Dispose();
+ _cancellationTokenSource?.Dispose();
+ _autoTaskCompletEvent.Dispose();
+ }
}
}