analyze movies (#348)

* 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>
Co-authored-by: TwistedUmbrellaX <twistedumbrella@gmail.com>
This commit is contained in:
rlauuzo 2024-10-18 14:15:09 +02:00 committed by GitHub
parent f6c8fca28f
commit 5bc8913668
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 345 additions and 121 deletions

View File

@ -22,6 +22,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
private readonly int _maximumCreditsDuration;
private readonly int _maximumMovieCreditsDuration;
private readonly int _blackFrameMinimumPercentage;
/// <summary>
@ -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);

View File

@ -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<ChapterAnalyzer> 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

View File

@ -33,6 +33,11 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary>
public bool SelectAllLibraries { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether movies should be analyzed.
/// </summary>
public bool AnalyzeMovies { get; set; }
/// <summary>
/// Gets or sets the list of client to auto skip for.
/// </summary>
@ -107,7 +112,12 @@ public class PluginConfiguration : BasePluginConfiguration
/// <summary>
/// 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.
/// </summary>
public int MaximumCreditsDuration { get; set; } = 300;
public int MaximumCreditsDuration { get; set; } = 450;
/// <summary>
/// Gets or sets the upper limit (in seconds) on the length of a movie segment that will be analyzed when searching for ending credits.
/// </summary>
public int MaximumMovieCreditsDuration { get; set; } = 900;
/// <summary>
/// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame.

View File

@ -52,6 +52,15 @@
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AnalyzeMovies" type="checkbox" is="emby-checkbox" />
<span>Analyze Movies</span>
</label>
<div class="fieldDescription">If checked, movies will be included in analysis.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
@ -135,6 +144,13 @@
Increasing either of these settings will cause episode analysis to take much longer.
</p>
<div class="inputContainer" id="movieCreditsDuration">
<label class="inputLabel inputLabelUnfocused" for="MaximumMovieCreditsDuration"> Maximum movie credits duration (in seconds) </label>
<input id="MaximumMovieCreditsDuration" type="number" is="emby-input" min="1" />
<div class="fieldDescription">Segments longer than this duration will not be considered movie credits.</div>
<br />upda
</div>
</details>
<details id="edl">
@ -398,10 +414,12 @@
<summary>Manage Timestamps & Fingerprints</summary>
<br />
<label class="inputLabel" for="troubleshooterShow">Select TV Series to manage</label>
<label class="inputLabel" for="troubleshooterShow">Select TV series / movie to manage</label>
<select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
<label class="inputLabel" for="troubleshooterSeason">Select Season to manage</label>
<div id="seasonSelection">
<label class="inputLabel" for="troubleshooterSeason">Select season to manage</label>
<select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
</div>
<br />
<div id="ignorelistSection" style="display: none">
@ -424,16 +442,18 @@
</div>
<br />
<button id="saveIgnoreListSeries" class="raised button-submit block emby-button">Apply to series</button>
<button id="saveIgnoreListSeason" class="raised button-submit block emby-button" style="display: none">Apply to season</button>
<button is="emby-button" id="saveIgnoreListSeries" class="raised button-submit block emby-button">Apply to series / movie</button>
<button is="emby-button" id="saveIgnoreListSeason" class="raised button-submit block emby-button" style="display: none">Apply to season</button>
</div>
<br />
<div id="episodeSelection">
<label class="inputLabel" for="troubleshooterEpisode1">Select the first episode</label>
<select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
<label class="inputLabel" for="troubleshooterEpisode1">Select the second episode</label>
<select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
<br />
</div>
<div id="timestampEditor" style="display: none">
<h3 style="margin: 0">Introduction timestamp editor</h3>
@ -465,6 +485,7 @@
</div>
</div>
<br />
<div id="rightEpisodeEditor">
<h4 style="margin: 0" id="editRightEpisodeTitle"></h4>
<br />
<div class="inlineForm">
@ -492,7 +513,8 @@
</div>
</div>
<br />
<button is="emby-select" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
</div>
<button is="emby-button" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
<br />
</div>
@ -502,6 +524,8 @@
<br />
</div>
<div id="fingerprintVisualizer">
<h3>Fingerprint Visualizer</h3>
<p>
Interactively compare the audio fingerprints of two episodes. <br />
@ -538,34 +562,47 @@
<span>Shift amount:</span>
<input type="number" min="-3000" max="3000" value="0" id="offset" />
<br />
<span id="suggestedShifts">Suggested shifts:</span>
<span id="suggestedShifts">
<span>Suggested shifts:</span>
<br />
<br />
</span>
<canvas id="troubleshooter" style="display: none"></canvas>
<span id="timestampContainer">
<span id="timestamps"></span> <br />
<span id="intros"></span>
</span>
</div>
<br />
<br />
<div id="eraseSeasonContainer" style="display: none">
<button id="btnEraseSeasonTimestamps" type="button">Erase all timestamps for this season</button>
<button is="emby-button" id="btnEraseSeasonTimestamps" class="button-submit emby-button" type="button">Erase all timestamps for this season</button>
<input type="checkbox" id="eraseSeasonCacheCheckbox" style="margin-left: 10px" />
<label for="eraseSeasonCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
<br />
</div>
<div id="eraseMovieContainer" style="display: none">
<button is="emby-button" id="btnEraseMovieTimestamps" class="button-submit emby-button" type="button">Erase all timestamps for this movie</button>
<input type="checkbox" id="eraseMovieCacheCheckbox" style="margin-left: 10px" />
<label for="eraseMovieCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
<br />
</div>
<button is="emby-button" class="button-submit emby-button" id="btnEraseIntroTimestamps">Erase all introduction timestamps (globally)</button>
<br />
<button id="btnEraseIntroTimestamps">Erase all introduction timestamps (globally)</button>
<button is="emby-button" class="button-submit emby-button" id="btnEraseCreditTimestamps">Erase all end credits timestamps (globally)</button>
<br />
<button id="btnEraseCreditTimestamps">Erase all end credits timestamps (globally)</button>
<br />
<input type="checkbox" id="eraseModeCacheCheckbox" style="margin-left: 10px" />
<label for="eraseModeCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
<br />
<br />
</details>
<details id="storage">
@ -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();

View File

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

View File

@ -47,7 +47,7 @@ public class VisualizationController(ILogger<VisualizationController> 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<VisualizationController> 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)

View File

@ -47,6 +47,11 @@ public class QueuedEpisode
/// </summary>
public bool IsAnime { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an item is a movie.
/// </summary>
public bool IsMovie { get; set; }
/// <summary>
/// Gets or sets the timestamp (in seconds) to stop searching for an introduction at.
/// </summary>

View File

@ -23,6 +23,11 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers
/// </summary>
public required string LibraryName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether its a movie.
/// </summary>
public required bool IsMovie { get; set; }
/// <summary>
/// Gets the Seasons of the show.
/// </summary>

View File

@ -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<string> _selectedLibraries = [];
private bool _selectAllLibraries;
private bool _analyzeMovies;
/// <summary>
/// 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,14 +145,19 @@ 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);
}
}
_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

View File

@ -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
/// </summary>
public class SegmentProvider : IMediaSegmentProvider
{
private readonly int _remainingSecondsOfIntro;
private readonly long _remainingTicks;
/// <summary>
/// Initializes a new instance of the <see cref="SegmentProvider"/> class.
/// </summary>
public SegmentProvider()
{
_remainingSecondsOfIntro = Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2;
_remainingTicks = TimeSpan.FromSeconds(Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2).Ticks;
}
/// <inheritdoc/>
@ -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<IReadOnlyList<MediaSegmentDto>>(segments);
}
/// <inheritdoc/>
public ValueTask<bool> Supports(BaseItem item) => ValueTask.FromResult(item is Episode);
public ValueTask<bool> Supports(BaseItem item) => ValueTask.FromResult(item is IHasMediaSources);
}
}

View File

@ -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<ChapterAnalyzer>())
};
if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint)
if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie)
{
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
}
@ -222,7 +219,7 @@ public class BaseItemAnalyzerTask
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
}
if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint)
if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie)
{
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
}

View File

@ -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;

View File

@ -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;

View File

@ -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;