From 627ae05defcb8d3532a34cb94b0dc7e71672d822 Mon Sep 17 00:00:00 2001 From: rlauuzo <46294892+rlauuzo@users.noreply.github.com> Date: Fri, 18 Oct 2024 14:15:09 +0200 Subject: [PATCH] analyze movies (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * scan movies * Update ConfusedPolarBear.Plugin.IntroSkipper.csproj * fix * Update SegmentProvider.cs * fix * update * add movies to endpoints * Update * Update QueueManager.cs * revert * Update configPage.html Battery died. I’ll be back * “Borrow” show config to hide seasons * Add IsMovie to ShowInfos * remove unused usings * Add option to enable/disble movies * Use the left episode as movie editor * Timestamp erasure for movies * Add max credits duration for movies * Formatting and button style cleanup * remove fingerprint timings for movies * remove x2 from MaximumCreditsDuration in blackframe analyzer * Update SegmentProvider.cs * Update SegmentProvider.cs * Update SegmentProvider.cs * Update SegmentProvider.cs * Update BaseItemAnalyzerTask.cs --------- Co-Authored-By: rlauu <46294892+rlauu@users.noreply.github.com> Co-Authored-By: TwistedUmbrellaX <1173913+AbandonedCart@users.noreply.github.com> --- IntroSkipper/Analyzers/BlackFrameAnalyzer.cs | 29 +- IntroSkipper/Analyzers/ChapterAnalyzer.cs | 4 +- .../Configuration/PluginConfiguration.cs | 12 +- IntroSkipper/Configuration/configPage.html | 290 +++++++++++++----- .../Controllers/SkipIntroController.cs | 5 +- .../Controllers/VisualizationController.cs | 3 +- IntroSkipper/Data/QueuedEpisode.cs | 5 + IntroSkipper/Data/ShowInfos.cs | 5 + IntroSkipper/Manager/QueueManager.cs | 54 +++- .../ScheduledTasks/BaseItemAnalyzerTask.cs | 11 +- .../ScheduledTasks/DetectCreditsTask.cs | 1 - .../ScheduledTasks/DetectIntrosCreditsTask.cs | 1 - .../ScheduledTasks/DetectIntrosTask.cs | 1 - 13 files changed, 314 insertions(+), 107 deletions(-) diff --git a/IntroSkipper/Analyzers/BlackFrameAnalyzer.cs b/IntroSkipper/Analyzers/BlackFrameAnalyzer.cs index aad9eac..bb8f1ec 100644 --- a/IntroSkipper/Analyzers/BlackFrameAnalyzer.cs +++ b/IntroSkipper/Analyzers/BlackFrameAnalyzer.cs @@ -22,6 +22,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer private readonly int _maximumCreditsDuration; + private readonly int _maximumMovieCreditsDuration; + private readonly int _blackFrameMinimumPercentage; /// @@ -32,7 +34,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer { var config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); _minimumCreditsDuration = config.MinimumCreditsDuration; - _maximumCreditsDuration = 2 * config.MaximumCreditsDuration; + _maximumCreditsDuration = config.MaximumCreditsDuration; + _maximumMovieCreditsDuration = config.MaximumMovieCreditsDuration; _blackFrameMinimumPercentage = config.BlackFrameMinimumPercentage; _logger = logger; @@ -66,7 +69,21 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer break; } - // Pre-check to find reasonable starting point. + var creditDuration = episode.IsMovie ? _maximumMovieCreditsDuration : _maximumCreditsDuration; + + var chapters = Plugin.Instance!.GetChapters(episode.EpisodeId); + var lastSuitableChapter = chapters.LastOrDefault(c => + { + var start = TimeSpan.FromTicks(c.StartPositionTicks).TotalSeconds; + return start >= _minimumCreditsDuration && start <= creditDuration; + }); + + if (lastSuitableChapter is not null) + { + searchStart = TimeSpan.FromTicks(lastSuitableChapter.StartPositionTicks).TotalSeconds; + isFirstEpisode = false; + } + if (isFirstEpisode) { var scanTime = episode.Duration - searchStart; @@ -83,9 +100,9 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer frames = FFmpegWrapper.DetectBlackFrames(episode, tr, _blackFrameMinimumPercentage); - if (searchStart > _maximumCreditsDuration) + if (searchStart > creditDuration) { - searchStart = _maximumCreditsDuration; + searchStart = creditDuration; break; } } @@ -143,6 +160,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer var end = TimeSpan.FromSeconds(lowerLimit); var firstFrameTime = 0.0; + var creditDuration = episode.IsMovie ? _maximumMovieCreditsDuration : _maximumCreditsDuration; + // Continue bisecting the end of the file until the range that contains the first black // frame is smaller than the maximum permitted error. while (start - end > _maximumError) @@ -189,7 +208,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer if (TimeSpan.FromSeconds(upperLimit) - midpoint < _maximumError) { - upperLimit = Math.Min(upperLimit + (0.5 * searchDistance), _maximumCreditsDuration); + upperLimit = Math.Min(upperLimit + (0.5 * searchDistance), creditDuration); // Reset start for a new search with the increased duration start = TimeSpan.FromSeconds(upperLimit); diff --git a/IntroSkipper/Analyzers/ChapterAnalyzer.cs b/IntroSkipper/Analyzers/ChapterAnalyzer.cs index 5fe2ffa..6cf832b 100644 --- a/IntroSkipper/Analyzers/ChapterAnalyzer.cs +++ b/IntroSkipper/Analyzers/ChapterAnalyzer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -92,9 +91,10 @@ public class ChapterAnalyzer(ILogger logger) : IMediaFileAnalyz } var config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); + var creditDuration = episode.IsMovie ? config.MaximumMovieCreditsDuration : config.MaximumCreditsDuration; var reversed = mode != AnalysisMode.Introduction; var (minDuration, maxDuration) = reversed - ? (config.MinimumCreditsDuration, config.MaximumCreditsDuration) + ? (config.MinimumCreditsDuration, creditDuration) : (config.MinimumIntroDuration, config.MaximumIntroDuration); // Check all chapters diff --git a/IntroSkipper/Configuration/PluginConfiguration.cs b/IntroSkipper/Configuration/PluginConfiguration.cs index 852da07..968fd92 100644 --- a/IntroSkipper/Configuration/PluginConfiguration.cs +++ b/IntroSkipper/Configuration/PluginConfiguration.cs @@ -33,6 +33,11 @@ public class PluginConfiguration : BasePluginConfiguration /// public bool SelectAllLibraries { get; set; } = true; + /// + /// Gets or sets a value indicating whether movies should be analyzed. + /// + public bool AnalyzeMovies { get; set; } + /// /// Gets or sets the list of client to auto skip for. /// @@ -107,7 +112,12 @@ public class PluginConfiguration : BasePluginConfiguration /// /// Gets or sets the upper limit (in seconds) on the length of each episode's audio track that will be analyzed when searching for ending credits. /// - public int MaximumCreditsDuration { get; set; } = 300; + public int MaximumCreditsDuration { get; set; } = 450; + + /// + /// Gets or sets the upper limit (in seconds) on the length of a movie segment that will be analyzed when searching for ending credits. + /// + public int MaximumMovieCreditsDuration { get; set; } = 900; /// /// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame. diff --git a/IntroSkipper/Configuration/configPage.html b/IntroSkipper/Configuration/configPage.html index bd2965d..5d9a567 100644 --- a/IntroSkipper/Configuration/configPage.html +++ b/IntroSkipper/Configuration/configPage.html @@ -52,6 +52,15 @@ +
+ + +
If checked, movies will be included in analysis.
+
+

- - - - -
+
+ + + + +
+

-

-
-
-
- - - +
+

+
+
+
+ + + +
+
+ + + +
-
- - - +
+
+ + + +
+
+ + + +
+
-
-
- - - -
-
- - - -
-
-

@@ -502,51 +524,56 @@
-

Fingerprint Visualizer

-

- Interactively compare the audio fingerprints of two episodes.
- The blue and red bar to the right of the fingerprint diff turns blue when the corresponding fingerprint points are at least 80% similar. -

- - - - - - - - - - - - - - - - - - - - - - - - - -
KeyFunction
Up arrowShift the left episode up by 0.1238 seconds. Holding control will shift the episode by 10 seconds.
Down arrowShift the left episode down by 0.1238 seconds. Holding control will shift the episode by 10 seconds.
Right arrowAdvance to the next pair of episodes.
Left arrowGo back to the previous pair of episodes.
-
+
- Shift amount: - -
- Suggested shifts: -
-
+

Fingerprint Visualizer

+

+ Interactively compare the audio fingerprints of two episodes.
+ The blue and red bar to the right of the fingerprint diff turns blue when the corresponding fingerprint points are at least 80% similar. +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
Up arrowShift the left episode up by 0.1238 seconds. Holding control will shift the episode by 10 seconds.
Down arrowShift the left episode down by 0.1238 seconds. Holding control will shift the episode by 10 seconds.
Right arrowAdvance to the next pair of episodes.
Left arrowGo back to the previous pair of episodes.
+
- - -
- -
+ Shift amount: + +
+ + Suggested shifts: +
+
+
+ + + +
+ +
+


@@ -555,8 +582,16 @@ +
+
+ + -

@@ -616,6 +651,7 @@ "MaximumIntroDuration", "MinimumCreditsDuration", "MaximumCreditsDuration", + "MaximumMovieCreditsDuration", "EdlAction", "ProcessPriority", "ProcessThreads", @@ -635,7 +671,7 @@ "AutoSkipCreditsNotificationText", ]; - var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeSeasonZero", "SelectAllLibraries", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"]; + var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"]; // visualizer elements var ignorelistSection = document.querySelector("div#ignorelistSection"); @@ -645,20 +681,27 @@ var saveIgnoreListSeriesButton = ignorelistSection.querySelector("button#saveIgnoreListSeries"); var canvas = document.querySelector("canvas#troubleshooter"); var selectShow = document.querySelector("select#troubleshooterShow"); + var seasonSelection = document.getElementById("seasonSelection"); var selectSeason = document.querySelector("select#troubleshooterSeason"); + var episodeSelection = document.getElementById("episodeSelection"); var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1"); var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2"); var txtOffset = document.querySelector("input#offset"); var txtSuggested = document.querySelector("span#suggestedShifts"); var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps"); var eraseSeasonContainer = document.getElementById("eraseSeasonContainer"); + var btnMovieEraseTimestamps = document.querySelector("button#btnEraseMovieTimestamps"); + var eraseMovieContainer = document.getElementById("eraseMovieContainer"); var timestampError = document.querySelector("textarea#timestampError"); var timestampEditor = document.querySelector("#timestampEditor"); + var rightEpisodeEditor = document.getElementById("rightEpisodeEditor"); var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps"); var timeContainer = document.querySelector("span#timestampContainer"); + var fingerprintVisualizer = document.getElementById("fingerprintVisualizer"); var windowHashInterval = 0; + var analyzeMovies = document.getElementById("AnalyzeMovies"); var autoSkip = document.querySelector("input#AutoSkip"); var skipButtonVisible = document.getElementById("SkipButtonVisible"); var skipButtonVisibleLabel = document.getElementById("SkipButtonVisibleLabel"); @@ -669,6 +712,7 @@ var secondsOfIntroStartToPlay = document.querySelector("div#divSecondsOfIntroStartToPlay"); var autoSkipClientList = document.getElementById("AutoSkipClientList"); var secondsOfCreditsStartToPlay = document.querySelector("div#divSecondsOfCreditsStartToPlay"); + var movieCreditsDuration = document.getElementById("movieCreditsDuration"); var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText"); var autoSkipCredits = document.querySelector("input#AutoSkipCredits"); var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText"); @@ -780,7 +824,7 @@ async function populateLibraries() { const response = await getJson("Library/VirtualFolders"); - const tvLibraries = response.filter((item) => item.CollectionType === undefined || item.CollectionType === "tvshows"); + const tvLibraries = response.filter((item) => item.CollectionType === undefined || item.CollectionType === "tvshows" || item.CollectionType === "movies"); const libraryNames = tvLibraries.map((lib) => lib.Name || "Unnamed Library"); generateCheckboxList(libraryNames, "libraryCheckboxes", "SelectedLibraries"); } @@ -802,6 +846,16 @@ persistSkip.addEventListener("change", persistSkipChanged); + async function analyzeMoviesChanged() { + if (analyzeMovies.checked) { + movieCreditsDuration.style.display = "unset"; + } else { + movieCreditsDuration.style.display = "none"; + } + } + + analyzeMovies.addEventListener("change", analyzeMoviesChanged); + // when the fingerprint visualizer opens, populate show names async function visualizerToggled() { if (!visualizer.open) { @@ -893,8 +947,11 @@ // show changed, populate seasons async function showChanged() { + seasonSelection.style.display = "unset"; clearSelect(selectSeason); eraseSeasonContainer.style.display = "none"; + eraseMovieContainer.style.display = "none"; + episodeSelection.style.display = "unset"; clearSelect(selectEpisode1); clearSelect(selectEpisode2); @@ -907,6 +964,11 @@ saveIgnoreListSeasonButton.style.display = "none"; Dashboard.hideLoadingMsg(); + if (shows[selectShow.value].IsMovie) { + movieLoaded(); + return; + } + // add all seasons from this show to the season select for (const season in shows[selectShow.value].Seasons) { addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season); @@ -957,6 +1019,7 @@ Dashboard.showLoadingMsg(); timestampError.value = ""; + fingerprintVisualizer.style.display = "unset"; canvas.style.display = "none"; lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint"); @@ -966,6 +1029,8 @@ timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n"; } + rightEpisodeEditor.style.display = "unset"; + rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint"); if (rhs === undefined) { timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!"; @@ -988,6 +1053,57 @@ updateTimestampEditor(); } + async function movieLoaded() { + + Dashboard.showLoadingMsg(); + + seasonSelection.style.display = "none"; + episodeSelection.style.display = "none"; + eraseMovieContainer.style.display = "unset"; + + timestampError.value = ""; + fingerprintVisualizer.style.display = "none"; + + lhs = await getJson("Intros/Episode/" + selectShow.value + "/Chromaprint"); + if (lhs === undefined) { + timestampError.value += "Error: " + selectShow.value + " fingerprints failed!\n"; + } else if (lhs === null) { + timestampError.value += "Error: " + selectShow.value + " fingerprints missing!\n"; + } + + rightEpisodeEditor.style.display = "none"; + + if (timestampError.value == "") { + timestampErrorDiv.style.display = "none"; + } else { + timestampErrorDiv.style.display = "unset"; + } + + Dashboard.hideLoadingMsg(); + + txtOffset.value = "0"; + + // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found + const leftEpisodeJson = await getJson("Episode/" + selectShow.value + "/Timestamps"); + + // Update the editor for the first and second episodes + timestampEditor.style.display = "unset"; + document.querySelector("#editLeftEpisodeTitle").textContent = selectShow.value; + document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start; + document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End; + document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start; + document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End + + // Update display inputs + const inputs = document.querySelectorAll('#timestampEditor input[type="number"]'); + inputs.forEach((input) => { + const displayInput = document.getElementById(input.id.replace("Edit", "Display")); + displayInput.value = formatTime(parseFloat(input.value) || 0); + }); + + setupTimeInputs(); + } + function setupTimeInputs() { const timestampEditor = document.getElementById("timestampEditor"); timestampEditor.querySelectorAll(".inputContainer").forEach((container) => { @@ -1285,6 +1401,22 @@ document.getElementById("eraseSeasonCacheCheckbox").checked = false; }); }); + btnMovieEraseTimestamps.addEventListener("click", () => { + Dashboard.confirm("Are you sure you want to erase all timestamps for this movie?", "Confirm timestamp erasure", (result) => { + if (!result) { + return; + } + + const show = selectShow.value; + const eraseCacheChecked = document.getElementById("eraseMovieCacheCheckbox").checked; + + const url = "Intros/Show/" + encodeURIComponent(show); + fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null); + + Dashboard.alert("Erased timestamps for " + show); + document.getElementById("eraseMovieCacheCheckbox").checked = false; + }); + }); saveIgnoreListSeasonButton.addEventListener("click", () => { Dashboard.showLoadingMsg(); diff --git a/IntroSkipper/Controllers/SkipIntroController.cs b/IntroSkipper/Controllers/SkipIntroController.cs index c0537a6..b7d2276 100644 --- a/IntroSkipper/Controllers/SkipIntroController.cs +++ b/IntroSkipper/Controllers/SkipIntroController.cs @@ -4,6 +4,7 @@ using System.Net.Mime; using IntroSkipper.Configuration; using IntroSkipper.Data; using MediaBrowser.Common.Api; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -63,7 +64,7 @@ public class SkipIntroController : ControllerBase { // only update existing episodes var rawItem = Plugin.Instance!.GetItem(id); - if (rawItem == null || rawItem is not Episode episode) + if (rawItem == null || rawItem is not Episode and not Movie) { return NotFound(); } @@ -99,7 +100,7 @@ public class SkipIntroController : ControllerBase { // only get return content for episodes var rawItem = Plugin.Instance!.GetItem(id); - if (rawItem == null || rawItem is not Episode episode) + if (rawItem == null || rawItem is not Episode and not Movie) { return NotFound(); } diff --git a/IntroSkipper/Controllers/VisualizationController.cs b/IntroSkipper/Controllers/VisualizationController.cs index 4015d5d..5d2c78a 100644 --- a/IntroSkipper/Controllers/VisualizationController.cs +++ b/IntroSkipper/Controllers/VisualizationController.cs @@ -47,7 +47,7 @@ public class VisualizationController(ILogger logger) : var seasonNumber = first.SeasonNumber; if (!showSeasons.TryGetValue(seriesId, out var showInfo)) { - showInfo = new ShowInfos { SeriesName = first.SeriesName, ProductionYear = GetProductionYear(seriesId), LibraryName = GetLibraryName(seriesId), Seasons = [] }; + showInfo = new ShowInfos { SeriesName = first.SeriesName, ProductionYear = GetProductionYear(seriesId), LibraryName = GetLibraryName(seriesId), IsMovie = first.IsMovie, Seasons = [] }; showSeasons[seriesId] = showInfo; } @@ -65,6 +65,7 @@ public class VisualizationController(ILogger logger) : SeriesName = kvp.Value.SeriesName, ProductionYear = kvp.Value.ProductionYear, LibraryName = kvp.Value.LibraryName, + IsMovie = kvp.Value.IsMovie, Seasons = kvp.Value.Seasons .OrderBy(s => s.Value) .ToDictionary(s => s.Key, s => s.Value) diff --git a/IntroSkipper/Data/QueuedEpisode.cs b/IntroSkipper/Data/QueuedEpisode.cs index 1168e3a..28826d3 100644 --- a/IntroSkipper/Data/QueuedEpisode.cs +++ b/IntroSkipper/Data/QueuedEpisode.cs @@ -47,6 +47,11 @@ public class QueuedEpisode ///
public bool IsAnime { get; set; } + /// + /// Gets or sets a value indicating whether an item is a movie. + /// + public bool IsMovie { get; set; } + /// /// Gets or sets the timestamp (in seconds) to stop searching for an introduction at. /// diff --git a/IntroSkipper/Data/ShowInfos.cs b/IntroSkipper/Data/ShowInfos.cs index b48aa06..68a3b0a 100644 --- a/IntroSkipper/Data/ShowInfos.cs +++ b/IntroSkipper/Data/ShowInfos.cs @@ -23,6 +23,11 @@ namespace IntroSkipper.Controllers /// public required string LibraryName { get; set; } + /// + /// Gets or sets a value indicating whether its a movie. + /// + public required bool IsMovie { get; set; } + /// /// Gets the Seasons of the show. /// diff --git a/IntroSkipper/Manager/QueueManager.cs b/IntroSkipper/Manager/QueueManager.cs index 96ce0bb..c8dff77 100644 --- a/IntroSkipper/Manager/QueueManager.cs +++ b/IntroSkipper/Manager/QueueManager.cs @@ -6,6 +6,7 @@ using IntroSkipper.Data; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using Microsoft.Extensions.Logging; @@ -28,6 +29,7 @@ public class QueueManager(ILogger logger, ILibraryManager libraryM private double _analysisPercent; private List _selectedLibraries = []; private bool _selectAllLibraries; + private bool _analyzeMovies; /// /// Gets all media items on the server. @@ -90,6 +92,8 @@ public class QueueManager(ILogger logger, ILibraryManager libraryM _selectAllLibraries = config.SelectAllLibraries; + _analyzeMovies = config.AnalyzeMovies; + if (!_selectAllLibraries) { // Get the list of library names which have been selected for analysis, ignoring whitespace and empty entries. @@ -123,7 +127,7 @@ public class QueueManager(ILogger logger, ILibraryManager libraryM // 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], + IncludeItemTypes = [BaseItemKind.Episode, BaseItemKind.Movie], Recursive = true, IsVirtualItem = false }; @@ -141,13 +145,18 @@ public class QueueManager(ILogger logger, ILibraryManager libraryM foreach (var item in items) { - if (item is not Episode episode) + if (item is Episode episode) { - _logger.LogDebug("Item {Name} is not an episode", item.Name); - continue; + QueueEpisode(episode); + } + else if (_analyzeMovies && item is Movie movie) + { + QueueMovie(movie); + } + else + { + _logger.LogDebug("Item {Name} is not an episode or movie", item.Name); } - - QueueEpisode(episode); } _logger.LogDebug("Queued {Count} episodes", items.Count); @@ -197,8 +206,11 @@ public class QueueManager(ILogger logger, ILibraryManager libraryM duration >= 5 * 60 ? duration * _analysisPercent : duration, 60 * pluginInstance.Configuration.AnalysisLengthLimit); + var maxCreditsDuration = Math.Min( + duration >= 5 * 60 ? duration * _analysisPercent : duration, + 60 * pluginInstance.Configuration.MaximumCreditsDuration); + // Queue the episode for analysis - var maxCreditsDuration = pluginInstance.Configuration.MaximumCreditsDuration; seasonEpisodes.Add(new QueuedEpisode { SeriesName = episode.SeriesName, @@ -216,6 +228,34 @@ public class QueueManager(ILogger logger, ILibraryManager libraryM pluginInstance.TotalQueued++; } + private void QueueMovie(Movie movie) + { + var pluginInstance = Plugin.Instance ?? throw new InvalidOperationException("Plugin instance was null"); + if (string.IsNullOrEmpty(movie.Path)) + { + _logger.LogWarning( + "Not queuing movie \"{Name}\" ({Id}) as no path was provided by Jellyfin", + movie.Name, + movie.Id); + return; + } + + // Allocate a new list for each Movie + _queuedEpisodes.TryAdd(movie.Id, []); + var duration = TimeSpan.FromTicks(movie.RunTimeTicks ?? 0).TotalSeconds; + _queuedEpisodes[movie.Id].Add(new QueuedEpisode + { + SeriesName = movie.Name, + SeriesId = movie.Id, + EpisodeId = movie.Id, + Name = movie.Name, + Path = movie.Path, + Duration = Convert.ToInt32(duration), + IsMovie = true + }); + pluginInstance.TotalQueued++; + } + private Guid GetSeasonId(Episode episode) { if (episode.ParentIndexNumber == 0 && episode.AiredSeasonNumber != 0) // In-season special diff --git a/IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs b/IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs index b33556c..0a34c1e 100644 --- a/IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs +++ b/IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs @@ -122,11 +122,8 @@ public class BaseItemAnalyzerTask Interlocked.Add(ref totalProcessed, episodes.Count * _analysisModes.Count); // Update total Processed directly progress.Report(totalProcessed * 100 / totalQueued); - - return; } - - if (_analysisModes.Count != requiredModes.Count) + else if (_analysisModes.Count != requiredModes.Count) { Interlocked.Add(ref totalProcessed, episodes.Count); progress.Report(totalProcessed * 100 / totalQueued); // Partial analysis some modes have already been analyzed @@ -188,7 +185,7 @@ public class BaseItemAnalyzerTask // Only analyze specials (season 0) if the user has opted in. var first = items[0]; - if (first.SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero) + if (!first.IsMovie && first.SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero) { return 0; } @@ -211,7 +208,7 @@ public class BaseItemAnalyzerTask new ChapterAnalyzer(_loggerFactory.CreateLogger()) }; - if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint) + if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie) { analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger())); } @@ -221,7 +218,7 @@ public class BaseItemAnalyzerTask analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger())); } - if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint) + if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie) { analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger())); } diff --git a/IntroSkipper/ScheduledTasks/DetectCreditsTask.cs b/IntroSkipper/ScheduledTasks/DetectCreditsTask.cs index 674bcc3..41107ba 100644 --- a/IntroSkipper/ScheduledTasks/DetectCreditsTask.cs +++ b/IntroSkipper/ScheduledTasks/DetectCreditsTask.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using IntroSkipper.Data; diff --git a/IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs b/IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs index 687ad96..2516053 100644 --- a/IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs +++ b/IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using IntroSkipper.Data; diff --git a/IntroSkipper/ScheduledTasks/DetectIntrosTask.cs b/IntroSkipper/ScheduledTasks/DetectIntrosTask.cs index 2d6d679..29797cc 100644 --- a/IntroSkipper/ScheduledTasks/DetectIntrosTask.cs +++ b/IntroSkipper/ScheduledTasks/DetectIntrosTask.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using IntroSkipper.Data;