diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs index 8027450..c4dcaa4 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/BlackFrameAnalyzer.cs +++ b/ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs index 88a121c..f3384af 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Analyzers/ChapterAnalyzer.cs +++ b/ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs index 61a0bd0..839dcca 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs +++ b/ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html index 0ec85b0..f8136f8 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html @@ -52,6 +52,15 @@ +
+ + +
If checked, movies will be included in analysis.
+
+

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

-

-
-
-
- - - +
+

+
+
+
+ + + +
+
+ + + +
-
- - - +
+
+ + + +
+
+ + + +
+
-
-
- - - -
-
- - - -
-
-
- +
@@ -502,70 +524,85 @@
-

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: +
+
+
+ + + +
+ +
+


+ + + +
- +
- -
- +
+
@@ -614,6 +651,7 @@ "MaximumIntroDuration", "MinimumCreditsDuration", "MaximumCreditsDuration", + "MaximumMovieCreditsDuration", "EdlAction", "ProcessPriority", "ProcessThreads", @@ -633,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"); @@ -643,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"); @@ -667,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"); @@ -778,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"); } @@ -800,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) { @@ -891,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); @@ -905,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); @@ -955,6 +1019,7 @@ Dashboard.showLoadingMsg(); timestampError.value = ""; + fingerprintVisualizer.style.display = "unset"; canvas.style.display = "none"; lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint"); @@ -964,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!"; @@ -986,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) => { @@ -1283,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/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs index 03a5c20..0d42f24 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs @@ -4,6 +4,7 @@ using System.Net.Mime; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs index 5d3ae9f..9965cbf 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs +++ b/ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs index bab97be..ee2c535 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/QueuedEpisode.cs +++ b/ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Data/ShowInfos.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/ShowInfos.cs index 99b0fa3..c630b10 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/ShowInfos.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/ShowInfos.cs @@ -23,6 +23,11 @@ namespace ConfusedPolarBear.Plugin.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/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs index e324d76..c07ac98 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Manager/QueueManager.cs @@ -6,6 +6,7 @@ using ConfusedPolarBear.Plugin.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 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager private double _analysisPercent; private List _selectedLibraries = []; private bool _selectAllLibraries; + private bool _analyzeMovies; /// /// Gets all media items on the server. @@ -90,6 +92,8 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager _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 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager // 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 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager 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 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager 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,38 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager 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/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs index 4c18a0b..bf859cf 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Providers/SegmentProvider.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; @@ -16,14 +15,14 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers /// public class SegmentProvider : IMediaSegmentProvider { - private readonly int _remainingSecondsOfIntro; + private readonly long _remainingTicks; /// /// Initializes a new instance of the class. /// public SegmentProvider() { - _remainingSecondsOfIntro = Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2; + _remainingTicks = TimeSpan.FromSeconds(Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2).Ticks; } /// @@ -39,7 +38,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers segments.Add(new MediaSegmentDto { StartTicks = TimeSpan.FromSeconds(introValue.Start).Ticks, - EndTicks = TimeSpan.FromSeconds(introValue.End - _remainingSecondsOfIntro).Ticks, + EndTicks = TimeSpan.FromSeconds(introValue.End).Ticks - _remainingTicks, ItemId = request.ItemId, Type = MediaSegmentType.Intro }); @@ -47,19 +46,31 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers if (Plugin.Instance!.Credits.TryGetValue(request.ItemId, out var creditValue)) { - segments.Add(new MediaSegmentDto + var outroSegment = new MediaSegmentDto { StartTicks = TimeSpan.FromSeconds(creditValue.Start).Ticks, - EndTicks = TimeSpan.FromSeconds(creditValue.End - _remainingSecondsOfIntro).Ticks, ItemId = request.ItemId, Type = MediaSegmentType.Outro - }); + }; + + var creditEndTicks = TimeSpan.FromSeconds(creditValue.End).Ticks; + + if (Plugin.Instance.GetItem(request.ItemId) is IHasMediaSources item && creditEndTicks >= item.RunTimeTicks - TimeSpan.TicksPerSecond) + { + outroSegment.EndTicks = item.RunTimeTicks ?? creditEndTicks; + } + else + { + outroSegment.EndTicks = creditEndTicks - _remainingTicks; + } + + segments.Add(outroSegment); } return Task.FromResult>(segments); } /// - public ValueTask Supports(BaseItem item) => ValueTask.FromResult(item is Episode); + public ValueTask Supports(BaseItem item) => ValueTask.FromResult(item is IHasMediaSources); } } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs index f2e05e5..c63c838 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/BaseItemAnalyzerTask.cs @@ -123,11 +123,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 @@ -189,7 +186,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; } @@ -212,7 +209,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())); } @@ -222,7 +219,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/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs index 394e86c..002b2d1 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.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 ConfusedPolarBear.Plugin.IntroSkipper.Data; diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs index aeca1a0..2516730 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosCreditsTask.cs +++ b/ConfusedPolarBear.Plugin.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 ConfusedPolarBear.Plugin.IntroSkipper.Data; diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs index 894c3b0..f79dd3c 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/DetectIntrosTask.cs +++ b/ConfusedPolarBear.Plugin.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 ConfusedPolarBear.Plugin.IntroSkipper.Data;