Use Jellyfins MediaSegmentType (#344)

* Use Jellyfins MediaSegmentType

* Use primary constructor

* fix autoskip

* fix skip button

* fix episodestate class

* Update configPage.html

* Update QueueManager.cs

---------

Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
Co-authored-by: Kilian von Pflugk <github@jumoog.io>
This commit is contained in:
rlauuzo 2024-10-16 14:47:20 +02:00 committed by GitHub
parent 73287b79a5
commit 29ee3e0bc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1010 additions and 1039 deletions

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Xunit; using Xunit;
@ -63,7 +64,7 @@ public class TestAudioFingerprinting
var actual = FFmpegWrapper.Fingerprint( var actual = FFmpegWrapper.Fingerprint(
QueueEpisode("audio/big_buck_bunny_intro.mp3"), QueueEpisode("audio/big_buck_bunny_intro.mp3"),
AnalysisMode.Introduction); MediaSegmentType.Intro);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -83,7 +84,7 @@ public class TestAudioFingerprinting
{77, 5}, {77, 5},
}; };
var actual = FFmpegWrapper.CreateInvertedIndex(Guid.NewGuid(), fpr, AnalysisMode.Introduction); var actual = FFmpegWrapper.CreateInvertedIndex(Guid.NewGuid(), fpr, MediaSegmentType.Intro);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -95,8 +96,8 @@ public class TestAudioFingerprinting
var lhsEpisode = QueueEpisode("audio/big_buck_bunny_intro.mp3"); var lhsEpisode = QueueEpisode("audio/big_buck_bunny_intro.mp3");
var rhsEpisode = QueueEpisode("audio/big_buck_bunny_clip.mp3"); var rhsEpisode = QueueEpisode("audio/big_buck_bunny_clip.mp3");
var lhsFingerprint = FFmpegWrapper.Fingerprint(lhsEpisode, AnalysisMode.Introduction); var lhsFingerprint = FFmpegWrapper.Fingerprint(lhsEpisode, MediaSegmentType.Intro);
var rhsFingerprint = FFmpegWrapper.Fingerprint(rhsEpisode, AnalysisMode.Introduction); var rhsFingerprint = FFmpegWrapper.Fingerprint(rhsEpisode, MediaSegmentType.Intro);
var (lhs, rhs) = chromaprint.CompareEpisodes( var (lhs, rhs) = chromaprint.CompareEpisodes(
lhsEpisode.EpisodeId, lhsEpisode.EpisodeId,

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Xunit; using Xunit;
@ -19,8 +20,8 @@ public class TestChapterAnalyzer
[InlineData("Introduction")] [InlineData("Introduction")]
public void TestIntroductionExpression(string chapterName) public void TestIntroductionExpression(string chapterName)
{ {
var chapters = CreateChapters(chapterName, AnalysisMode.Introduction); var chapters = CreateChapters(chapterName, MediaSegmentType.Intro);
var introChapter = FindChapter(chapters, AnalysisMode.Introduction); var introChapter = FindChapter(chapters, MediaSegmentType.Intro);
Assert.NotNull(introChapter); Assert.NotNull(introChapter);
Assert.Equal(60, introChapter.Start); Assert.Equal(60, introChapter.Start);
@ -35,34 +36,34 @@ public class TestChapterAnalyzer
[InlineData("Credits")] [InlineData("Credits")]
public void TestEndCreditsExpression(string chapterName) public void TestEndCreditsExpression(string chapterName)
{ {
var chapters = CreateChapters(chapterName, AnalysisMode.Credits); var chapters = CreateChapters(chapterName, MediaSegmentType.Outro);
var creditsChapter = FindChapter(chapters, AnalysisMode.Credits); var creditsChapter = FindChapter(chapters, MediaSegmentType.Outro);
Assert.NotNull(creditsChapter); Assert.NotNull(creditsChapter);
Assert.Equal(1890, creditsChapter.Start); Assert.Equal(1890, creditsChapter.Start);
Assert.Equal(2000, creditsChapter.End); Assert.Equal(2000, creditsChapter.End);
} }
private Segment? FindChapter(Collection<ChapterInfo> chapters, AnalysisMode mode) private Segment? FindChapter(Collection<ChapterInfo> chapters, MediaSegmentType mode)
{ {
var logger = new LoggerFactory().CreateLogger<ChapterAnalyzer>(); var logger = new LoggerFactory().CreateLogger<ChapterAnalyzer>();
var analyzer = new ChapterAnalyzer(logger); var analyzer = new ChapterAnalyzer(logger);
var config = new Configuration.PluginConfiguration(); var config = new Configuration.PluginConfiguration();
var expression = mode == AnalysisMode.Introduction ? var expression = mode == MediaSegmentType.Intro ?
config.ChapterAnalyzerIntroductionPattern : config.ChapterAnalyzerIntroductionPattern :
config.ChapterAnalyzerEndCreditsPattern; config.ChapterAnalyzerEndCreditsPattern;
return analyzer.FindMatchingChapter(new() { Duration = 2000 }, chapters, expression, mode); return analyzer.FindMatchingChapter(new() { Duration = 2000 }, chapters, expression, mode);
} }
private Collection<ChapterInfo> CreateChapters(string name, AnalysisMode mode) private Collection<ChapterInfo> CreateChapters(string name, MediaSegmentType mode)
{ {
var chapters = new[]{ var chapters = new[]{
CreateChapter("Cold Open", 0), CreateChapter("Cold Open", 0),
CreateChapter(mode == AnalysisMode.Introduction ? name : "Introduction", 60), CreateChapter(mode == MediaSegmentType.Intro ? name : "Introduction", 60),
CreateChapter("Main Episode", 90), CreateChapter("Main Episode", 90),
CreateChapter(mode == AnalysisMode.Credits ? name : "Credits", 1890) CreateChapter(mode == MediaSegmentType.Outro ? name : "Credits", 1890)
}; };
return new(new List<ChapterInfo>(chapters)); return new(new List<ChapterInfo>(chapters));

View File

@ -1,5 +1,6 @@
using System; using System;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using Xunit; using Xunit;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests; namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;

View File

@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers
{
/// <summary> /// <summary>
/// Analyzer Helper. /// Analyzer Helper.
/// </summary> /// </summary>
@ -36,7 +37,7 @@ public class AnalyzerHelper
public Dictionary<Guid, Segment> AdjustIntroTimes( public Dictionary<Guid, Segment> AdjustIntroTimes(
IReadOnlyList<QueuedEpisode> episodes, IReadOnlyList<QueuedEpisode> episodes,
IReadOnlyDictionary<Guid, Segment> originalIntros, IReadOnlyDictionary<Guid, Segment> originalIntros,
AnalysisMode mode) MediaSegmentType mode)
{ {
return episodes return episodes
.Where(episode => originalIntros.TryGetValue(episode.EpisodeId, out var _)) .Where(episode => originalIntros.TryGetValue(episode.EpisodeId, out var _))
@ -45,7 +46,7 @@ public class AnalyzerHelper
episode => AdjustIntroForEpisode(episode, originalIntros[episode.EpisodeId], mode)); episode => AdjustIntroForEpisode(episode, originalIntros[episode.EpisodeId], mode));
} }
private Segment AdjustIntroForEpisode(QueuedEpisode episode, Segment originalIntro, AnalysisMode mode) private Segment AdjustIntroForEpisode(QueuedEpisode episode, Segment originalIntro, MediaSegmentType mode)
{ {
_logger.LogTrace("{Name} original intro: {Start} - {End}", episode.Name, originalIntro.Start, originalIntro.End); _logger.LogTrace("{Name} original intro: {Start} - {End}", episode.Name, originalIntro.Start, originalIntro.End);
@ -53,7 +54,7 @@ public class AnalyzerHelper
var originalIntroStart = new TimeRange(Math.Max(0, (int)originalIntro.Start - 5), (int)originalIntro.Start + 10); 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)); 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) if (!AdjustIntroBasedOnChapters(episode, adjustedIntro, originalIntroStart, originalIntroEnd) && mode == MediaSegmentType.Intro)
{ {
AdjustIntroBasedOnSilence(episode, adjustedIntro, originalIntroEnd); AdjustIntroBasedOnSilence(episode, adjustedIntro, originalIntroEnd);
} }
@ -114,3 +115,4 @@ public class AnalyzerHelper
silenceRange.Start >= adjustedIntro.Start; silenceRange.Start >= adjustedIntro.Start;
} }
} }
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
@ -41,10 +42,10 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles( public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
IReadOnlyList<QueuedEpisode> analysisQueue, IReadOnlyList<QueuedEpisode> analysisQueue,
AnalysisMode mode, MediaSegmentType mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (mode != AnalysisMode.Credits) if (mode != MediaSegmentType.Outro)
{ {
throw new NotImplementedException("mode must equal Credits"); throw new NotImplementedException("mode must equal Credits");
} }

View File

@ -1,12 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -26,7 +26,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles( public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
IReadOnlyList<QueuedEpisode> analysisQueue, IReadOnlyList<QueuedEpisode> analysisQueue,
AnalysisMode mode, MediaSegmentType mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var skippableRanges = new Dictionary<Guid, Segment>(); var skippableRanges = new Dictionary<Guid, Segment>();
@ -34,7 +34,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
// Episode analysis queue. // Episode analysis queue.
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue); var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
var expression = mode == AnalysisMode.Introduction ? var expression = mode == MediaSegmentType.Intro ?
Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern : Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern :
Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern; Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern;
@ -83,7 +83,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
QueuedEpisode episode, QueuedEpisode episode,
IReadOnlyList<ChapterInfo> chapters, IReadOnlyList<ChapterInfo> chapters,
string expression, string expression,
AnalysisMode mode) MediaSegmentType mode)
{ {
var count = chapters.Count; var count = chapters.Count;
if (count == 0) if (count == 0)
@ -92,7 +92,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
} }
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
var reversed = mode != AnalysisMode.Introduction; var reversed = mode != MediaSegmentType.Intro;
var (minDuration, maxDuration) = reversed var (minDuration, maxDuration) = reversed
? (config.MinimumCreditsDuration, config.MaximumCreditsDuration) ? (config.MinimumCreditsDuration, config.MaximumCreditsDuration)
: (config.MinimumIntroDuration, config.MaximumIntroDuration); : (config.MinimumIntroDuration, config.MaximumIntroDuration);

View File

@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading; using System.Threading;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
@ -31,7 +32,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
private readonly ILogger<ChromaprintAnalyzer> _logger; private readonly ILogger<ChromaprintAnalyzer> _logger;
private AnalysisMode _analysisMode; private MediaSegmentType _mediaSegmentType;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChromaprintAnalyzer"/> class. /// Initializes a new instance of the <see cref="ChromaprintAnalyzer"/> class.
@ -51,7 +52,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles( public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
IReadOnlyList<QueuedEpisode> analysisQueue, IReadOnlyList<QueuedEpisode> analysisQueue,
AnalysisMode mode, MediaSegmentType mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// All intros for this season. // All intros for this season.
@ -66,7 +67,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
// Episodes that were analyzed and do not have an introduction. // Episodes that were analyzed and do not have an introduction.
var episodesWithoutIntros = episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)).ToList(); var episodesWithoutIntros = episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)).ToList();
_analysisMode = mode; _mediaSegmentType = mode;
if (episodesWithoutIntros.Count == 0 || episodeAnalysisQueue.Count <= 1) if (episodesWithoutIntros.Count == 0 || episodeAnalysisQueue.Count <= 1)
{ {
@ -96,7 +97,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
fingerprintCache[episode.EpisodeId] = FFmpegWrapper.Fingerprint(episode, mode); fingerprintCache[episode.EpisodeId] = FFmpegWrapper.Fingerprint(episode, mode);
// Use reversed fingerprints for credits // Use reversed fingerprints for credits
if (_analysisMode == AnalysisMode.Credits) if (_mediaSegmentType == MediaSegmentType.Outro)
{ {
Array.Reverse(fingerprintCache[episode.EpisodeId]); Array.Reverse(fingerprintCache[episode.EpisodeId]);
} }
@ -139,7 +140,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
// - the introduction exceeds the configured limit // - the introduction exceeds the configured limit
if ( if (
!remainingIntro.Valid || !remainingIntro.Valid ||
(_analysisMode == AnalysisMode.Introduction && remainingIntro.Duration > Plugin.Instance!.Configuration.MaximumIntroDuration)) (_mediaSegmentType == MediaSegmentType.Intro && remainingIntro.Duration > Plugin.Instance!.Configuration.MaximumIntroDuration))
{ {
continue; continue;
} }
@ -153,7 +154,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
* To fix this, the starting and ending times need to be switched, as they were previously reversed * To fix this, the starting and ending times need to be switched, as they were previously reversed
* and subtracted from the episode duration to get the reported time range. * and subtracted from the episode duration to get the reported time range.
*/ */
if (_analysisMode == AnalysisMode.Credits) if (_mediaSegmentType == MediaSegmentType.Outro)
{ {
// Calculate new values for the current intro // Calculate new values for the current intro
double currentOriginalIntroStart = currentIntro.Start; double currentOriginalIntroStart = currentIntro.Start;
@ -202,9 +203,9 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
// Adjust all introduction times. // Adjust all introduction times.
var analyzerHelper = new AnalyzerHelper(_logger); var analyzerHelper = new AnalyzerHelper(_logger);
seasonIntros = analyzerHelper.AdjustIntroTimes(analysisQueue, seasonIntros, _analysisMode); seasonIntros = analyzerHelper.AdjustIntroTimes(analysisQueue, seasonIntros, _mediaSegmentType);
Plugin.Instance!.UpdateTimestamps(seasonIntros, _analysisMode); Plugin.Instance!.UpdateTimestamps(seasonIntros, _mediaSegmentType);
return episodeAnalysisQueue; return episodeAnalysisQueue;
} }
@ -296,8 +297,8 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
var rhsRanges = new List<TimeRange>(); var rhsRanges = new List<TimeRange>();
// Generate inverted indexes for the left and right episodes. // Generate inverted indexes for the left and right episodes.
var lhsIndex = FFmpegWrapper.CreateInvertedIndex(lhsId, lhsPoints, _analysisMode); var lhsIndex = FFmpegWrapper.CreateInvertedIndex(lhsId, lhsPoints, _mediaSegmentType);
var rhsIndex = FFmpegWrapper.CreateInvertedIndex(rhsId, rhsPoints, _analysisMode); var rhsIndex = FFmpegWrapper.CreateInvertedIndex(rhsId, rhsPoints, _mediaSegmentType);
var indexShifts = new HashSet<int>(); var indexShifts = new HashSet<int>();
// For all audio points in the left episode, check if the right episode has a point which matches exactly. // For all audio points in the left episode, check if the right episode has a point which matches exactly.

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
@ -18,6 +19,6 @@ public interface IMediaFileAnalyzer
/// <returns>Collection of media files that were **unsuccessfully analyzed**.</returns> /// <returns>Collection of media files that were **unsuccessfully analyzed**.</returns>
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles( public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
IReadOnlyList<QueuedEpisode> analysisQueue, IReadOnlyList<QueuedEpisode> analysisQueue,
AnalysisMode mode, MediaSegmentType mode,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Microsoft.Extensions.Logging; using Jellyfin.Data.Enums;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
@ -10,21 +10,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
/// </summary> /// </summary>
public class SegmentAnalyzer : IMediaFileAnalyzer public class SegmentAnalyzer : IMediaFileAnalyzer
{ {
private readonly ILogger<SegmentAnalyzer> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SegmentAnalyzer"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
public SegmentAnalyzer(ILogger<SegmentAnalyzer> logger)
{
_logger = logger;
}
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles( public IReadOnlyList<QueuedEpisode> AnalyzeMediaFiles(
IReadOnlyList<QueuedEpisode> analysisQueue, IReadOnlyList<QueuedEpisode> analysisQueue,
AnalysisMode mode, MediaSegmentType mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return analysisQueue; return analysisQueue;

View File

@ -1035,16 +1035,16 @@
// Update the editor for the first and second episodes // Update the editor for the first and second episodes
timestampEditor.style.display = "unset"; timestampEditor.style.display = "unset";
document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text; document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start; document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Intro.Start;
document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End; document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Intro.End;
document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start; document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Outro.Start;
document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End; document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Outro.End;
document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text; document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
document.querySelector("#editRightIntroEpisodeStartEdit").value = rightEpisodeJson.Introduction.Start; document.querySelector("#editRightIntroEpisodeStartEdit").value = rightEpisodeJson.Intro.Start;
document.querySelector("#editRightIntroEpisodeEndEdit").value = rightEpisodeJson.Introduction.End; document.querySelector("#editRightIntroEpisodeEndEdit").value = rightEpisodeJson.Intro.End;
document.querySelector("#editRightCreditEpisodeStartEdit").value = rightEpisodeJson.Credits.Start; document.querySelector("#editRightCreditEpisodeStartEdit").value = rightEpisodeJson.Outro.Start;
document.querySelector("#editRightCreditEpisodeEndEdit").value = rightEpisodeJson.Credits.End; document.querySelector("#editRightCreditEpisodeEndEdit").value = rightEpisodeJson.Outro.End;
// Update display inputs // Update display inputs
const inputs = document.querySelectorAll('#timestampEditor input[type="number"]'); const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
@ -1259,11 +1259,11 @@
selectEpisode1.addEventListener("change", episodeChanged); selectEpisode1.addEventListener("change", episodeChanged);
selectEpisode2.addEventListener("change", episodeChanged); selectEpisode2.addEventListener("change", episodeChanged);
btnEraseIntroTimestamps.addEventListener("click", (e) => { btnEraseIntroTimestamps.addEventListener("click", (e) => {
eraseTimestamps("Introduction"); eraseTimestamps("Intro");
e.preventDefault(); e.preventDefault();
}); });
btnEraseCreditTimestamps.addEventListener("click", (e) => { btnEraseCreditTimestamps.addEventListener("click", (e) => {
eraseTimestamps("Credits"); eraseTimestamps("Outro");
e.preventDefault(); e.preventDefault();
}); });
btnSeasonEraseTimestamps.addEventListener("click", () => { btnSeasonEraseTimestamps.addEventListener("click", () => {
@ -1317,11 +1317,11 @@
const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value; const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value;
const newLhs = { const newLhs = {
Introduction: { Intro: {
Start: getEditValue("editLeftIntroEpisodeStartEdit"), Start: getEditValue("editLeftIntroEpisodeStartEdit"),
End: getEditValue("editLeftIntroEpisodeEndEdit"), End: getEditValue("editLeftIntroEpisodeEndEdit"),
}, },
Credits: { Outro: {
Start: getEditValue("editLeftCreditEpisodeStartEdit"), Start: getEditValue("editLeftCreditEpisodeStartEdit"),
End: getEditValue("editLeftCreditEpisodeEndEdit"), End: getEditValue("editLeftCreditEpisodeEndEdit"),
}, },
@ -1329,11 +1329,11 @@
const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value; const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
const newRhs = { const newRhs = {
Introduction: { Intro: {
Start: getEditValue("editRightIntroEpisodeStartEdit"), Start: getEditValue("editRightIntroEpisodeStartEdit"),
End: getEditValue("editRightIntroEpisodeEndEdit"), End: getEditValue("editRightIntroEpisodeEndEdit"),
}, },
Credits: { Outro: {
Start: getEditValue("editRightCreditEpisodeStartEdit"), Start: getEditValue("editRightCreditEpisodeStartEdit"),
End: getEditValue("editRightCreditEpisodeEndEdit"), End: getEditValue("editRightCreditEpisodeEndEdit"),
}, },

View File

@ -186,8 +186,8 @@ const introSkipper = {
<span class="material-icons skip_next"></span> <span class="material-icons skip_next"></span>
</button> </button>
`; `;
this.skipButton.dataset.Introduction = config.SkipButtonIntroText; this.skipButton.dataset.Intro = config.SkipButtonIntroText;
this.skipButton.dataset.Credits = config.SkipButtonEndCreditsText; this.skipButton.dataset.Outro = config.SkipButtonEndCreditsText;
const controls = document.querySelector("div#videoOsdPage"); const controls = document.querySelector("div#videoOsdPage");
controls.appendChild(this.skipButton); controls.appendChild(this.skipButton);
}, },
@ -270,7 +270,7 @@ const introSkipper = {
}, 500); }, 500);
}; };
this.videoPlayer.addEventListener('seeked', seekedHandler); this.videoPlayer.addEventListener('seeked', seekedHandler);
this.videoPlayer.currentTime = segment.SegmentType === "Credits" && this.videoPlayer.duration - segment.IntroEnd < 3 this.videoPlayer.currentTime = segment.SegmentType === "Outro" && this.videoPlayer.duration - segment.IntroEnd < 3
? this.videoPlayer.duration + 10 ? this.videoPlayer.duration + 10
: segment.IntroEnd; : segment.IntroEnd;
}, },
@ -391,11 +391,11 @@ const introSkipper = {
this.setTimeInputs(skipperFields); this.setTimeInputs(skipperFields);
}, },
updateSkipperFields(skipperFields) { updateSkipperFields(skipperFields) {
const { Introduction = {}, Credits = {} } = this.skipperData; const { Intro = {}, Outro = {} } = this.skipperData;
skipperFields.querySelector('#introStartEdit').value = Introduction.Start || 0; skipperFields.querySelector('#introStartEdit').value = Intro.Start || 0;
skipperFields.querySelector('#introEndEdit').value = Introduction.End || 0; skipperFields.querySelector('#introEndEdit').value = Intro.End || 0;
skipperFields.querySelector('#creditsStartEdit').value = Credits.Start || 0; skipperFields.querySelector('#creditsStartEdit').value = Outro.Start || 0;
skipperFields.querySelector('#creditsEndEdit').value = Credits.End || 0; skipperFields.querySelector('#creditsEndEdit').value = Outro.End || 0;
}, },
attachSaveListener(metadataFormFields) { attachSaveListener(metadataFormFields) {
const saveButton = metadataFormFields.querySelector('.formDialogFooter .btnSave'); const saveButton = metadataFormFields.querySelector('.formDialogFooter .btnSave');
@ -441,20 +441,20 @@ const introSkipper = {
}, },
async saveSkipperData() { async saveSkipperData() {
const newTimestamps = { const newTimestamps = {
Introduction: { Intro: {
Start: parseFloat(document.getElementById('introStartEdit').value || 0), Start: parseFloat(document.getElementById('introStartEdit').value || 0),
End: parseFloat(document.getElementById('introEndEdit').value || 0) End: parseFloat(document.getElementById('introEndEdit').value || 0)
}, },
Credits: { Outro: {
Start: parseFloat(document.getElementById('creditsStartEdit').value || 0), Start: parseFloat(document.getElementById('creditsStartEdit').value || 0),
End: parseFloat(document.getElementById('creditsEndEdit').value || 0) End: parseFloat(document.getElementById('creditsEndEdit').value || 0)
} }
}; };
const { Introduction = {}, Credits = {} } = this.skipperData; const { Intro = {}, Outro = {} } = this.skipperData;
if (newTimestamps.Introduction.Start !== (Introduction.Start || 0) || if (newTimestamps.Intro.Start !== (Intro.Start || 0) ||
newTimestamps.Introduction.End !== (Introduction.End || 0) || newTimestamps.Intro.End !== (Intro.End || 0) ||
newTimestamps.Credits.Start !== (Credits.Start || 0) || newTimestamps.Outro.Start !== (Outro.Start || 0) ||
newTimestamps.Credits.End !== (Credits.End || 0)) { newTimestamps.Outro.End !== (Outro.End || 0)) {
const response = await this.secureFetch(`Episode/${this.currentEpisodeId}/Timestamps`, "POST", JSON.stringify(newTimestamps)); const response = await this.secureFetch(`Episode/${this.currentEpisodeId}/Timestamps`, "POST", JSON.stringify(newTimestamps));
this.d(response.ok ? 'Timestamps updated successfully' : 'Failed to update timestamps:', response.status); this.d(response.ok ? 'Timestamps updated successfully' : 'Failed to update timestamps:', response.status);
} else { } else {

View File

@ -23,8 +23,4 @@
<EmbeddedResource Include="Configuration\visualizer.js" /> <EmbeddedResource Include="Configuration\visualizer.js" />
<EmbeddedResource Include="Configuration\inject.js" /> <EmbeddedResource Include="Configuration\inject.js" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Manager\" />
<Folder Include="Services\" />
</ItemGroup>
</Project> </Project>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Net.Mime; using System.Net.Mime;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -37,7 +38,7 @@ public class SkipIntroController : ControllerBase
[HttpGet("Episode/{id}/IntroTimestamps/v1")] [HttpGet("Episode/{id}/IntroTimestamps/v1")]
public ActionResult<Intro> GetIntroTimestamps( public ActionResult<Intro> GetIntroTimestamps(
[FromRoute] Guid id, [FromRoute] Guid id,
[FromQuery] AnalysisMode mode = AnalysisMode.Introduction) [FromQuery] MediaSegmentType mode = MediaSegmentType.Intro)
{ {
var intro = GetIntro(id, mode); var intro = GetIntro(id, mode);
@ -80,8 +81,8 @@ public class SkipIntroController : ControllerBase
Plugin.Instance!.Credits[id] = new Segment(id, cr); Plugin.Instance!.Credits[id] = new Segment(id, cr);
} }
Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction); Plugin.Instance!.SaveTimestamps(MediaSegmentType.Intro);
Plugin.Instance!.SaveTimestamps(AnalysisMode.Credits); Plugin.Instance!.SaveTimestamps(MediaSegmentType.Outro);
return NoContent(); return NoContent();
} }
@ -125,18 +126,18 @@ public class SkipIntroController : ControllerBase
/// <response code="200">Skippable segments dictionary.</response> /// <response code="200">Skippable segments dictionary.</response>
/// <returns>Dictionary of skippable segments.</returns> /// <returns>Dictionary of skippable segments.</returns>
[HttpGet("Episode/{id}/IntroSkipperSegments")] [HttpGet("Episode/{id}/IntroSkipperSegments")]
public ActionResult<Dictionary<AnalysisMode, Intro>> GetSkippableSegments([FromRoute] Guid id) public ActionResult<Dictionary<MediaSegmentType, Intro>> GetSkippableSegments([FromRoute] Guid id)
{ {
var segments = new Dictionary<AnalysisMode, Intro>(); var segments = new Dictionary<MediaSegmentType, Intro>();
if (GetIntro(id, AnalysisMode.Introduction) is Intro intro) if (GetIntro(id, MediaSegmentType.Intro) is Intro intro)
{ {
segments[AnalysisMode.Introduction] = intro; segments[MediaSegmentType.Intro] = intro;
} }
if (GetIntro(id, AnalysisMode.Credits) is Intro credits) if (GetIntro(id, MediaSegmentType.Outro) is Intro credits)
{ {
segments[AnalysisMode.Credits] = credits; segments[MediaSegmentType.Outro] = credits;
} }
return segments; return segments;
@ -146,7 +147,7 @@ public class SkipIntroController : ControllerBase
/// <param name="id">Unique identifier of this episode.</param> /// <param name="id">Unique identifier of this episode.</param>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
/// <returns>Intro object if the provided item has an intro, null otherwise.</returns> /// <returns>Intro object if the provided item has an intro, null otherwise.</returns>
private static Intro? GetIntro(Guid id, AnalysisMode mode) private static Intro? GetIntro(Guid id, MediaSegmentType mode)
{ {
try try
{ {
@ -187,13 +188,13 @@ public class SkipIntroController : ControllerBase
/// <returns>No content.</returns> /// <returns>No content.</returns>
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[HttpPost("Intros/EraseTimestamps")] [HttpPost("Intros/EraseTimestamps")]
public ActionResult ResetIntroTimestamps([FromQuery] AnalysisMode mode, [FromQuery] bool eraseCache = false) public ActionResult ResetIntroTimestamps([FromQuery] MediaSegmentType mode, [FromQuery] bool eraseCache = false)
{ {
if (mode == AnalysisMode.Introduction) if (mode == MediaSegmentType.Intro)
{ {
Plugin.Instance!.Intros.Clear(); Plugin.Instance!.Intros.Clear();
} }
else if (mode == AnalysisMode.Credits) else if (mode == MediaSegmentType.Outro)
{ {
Plugin.Instance!.Credits.Clear(); Plugin.Instance!.Credits.Clear();
} }

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -114,8 +115,8 @@ public class VisualizationController(ILogger<VisualizationController> logger) :
return new IgnoreListItem(Guid.Empty) return new IgnoreListItem(Guid.Empty)
{ {
IgnoreIntro = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Introduction)), IgnoreIntro = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, MediaSegmentType.Intro)),
IgnoreCredits = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Credits)) IgnoreCredits = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, MediaSegmentType.Outro))
}; };
} }
@ -158,7 +159,7 @@ public class VisualizationController(ILogger<VisualizationController> logger) :
{ {
if (needle.EpisodeId == id) if (needle.EpisodeId == id)
{ {
return FFmpegWrapper.Fingerprint(needle, AnalysisMode.Introduction); return FFmpegWrapper.Fingerprint(needle, MediaSegmentType.Intro);
} }
} }
} }
@ -201,7 +202,7 @@ public class VisualizationController(ILogger<VisualizationController> logger) :
} }
} }
Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction | AnalysisMode.Credits); Plugin.Instance!.SaveTimestamps(MediaSegmentType.Intro | MediaSegmentType.Outro);
return NoContent(); return NoContent();
} }
@ -281,7 +282,7 @@ public class VisualizationController(ILogger<VisualizationController> logger) :
{ {
var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd); var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd);
Plugin.Instance!.Intros[id] = new Segment(id, tr); Plugin.Instance!.Intros[id] = new Segment(id, tr);
Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction); Plugin.Instance.SaveTimestamps(MediaSegmentType.Intro);
} }
return NoContent(); return NoContent();

View File

@ -1,17 +0,0 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
/// <summary>
/// Type of media file analysis to perform.
/// </summary>
public enum AnalysisMode
{
/// <summary>
/// Detect introduction sequences.
/// </summary>
Introduction,
/// <summary>
/// Detect credits.
/// </summary>
Credits,
}

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Enums;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data; namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
@ -7,44 +10,61 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
/// </summary> /// </summary>
public class EpisodeState public class EpisodeState
{ {
private readonly bool[] _analyzedStates = new bool[2]; private readonly Dictionary<MediaSegmentType, (bool Analyzed, bool Blacklisted)> _states = [];
private readonly bool[] _blacklistedStates = new bool[2]; /// <summary>
/// Initializes a new instance of the <see cref="EpisodeState"/> class.
/// </summary>
public EpisodeState() =>
Array.ForEach(Enum.GetValues<MediaSegmentType>(), mode => _states[mode] = default);
/// <summary> /// <summary>
/// Checks if the specified analysis mode has been analyzed. /// Checks if the specified analysis mode has been analyzed.
/// </summary> /// </summary>
/// <param name="mode">The analysis mode to check.</param> /// <param name="mode">The analysis mode to check.</param>
/// <returns>True if the mode has been analyzed, false otherwise.</returns> /// <returns>True if the mode has been analyzed, false otherwise.</returns>
public bool IsAnalyzed(AnalysisMode mode) => _analyzedStates[(int)mode]; public bool IsAnalyzed(MediaSegmentType mode) => _states[mode].Analyzed;
/// <summary>
/// Sets the analyzed state for the specified analysis mode.
/// </summary>
/// <param name="mode">The analysis mode to set.</param>
/// <param name="value">The analyzed state to set.</param>
public void SetAnalyzed(AnalysisMode mode, bool value) => _analyzedStates[(int)mode] = value;
/// <summary> /// <summary>
/// Checks if the specified analysis mode has been blacklisted. /// Checks if the specified analysis mode has been blacklisted.
/// </summary> /// </summary>
/// <param name="mode">The analysis mode to check.</param> /// <param name="mode">The analysis mode to check.</param>
/// <returns>True if the mode has been blacklisted, false otherwise.</returns> /// <returns>True if the mode has been blacklisted, false otherwise.</returns>
public bool IsBlacklisted(AnalysisMode mode) => _blacklistedStates[(int)mode]; public bool IsBlacklisted(MediaSegmentType mode) => _states[mode].Blacklisted;
/// <summary>
/// Sets the analyzed state for the specified analysis mode.
/// </summary>
/// <param name="mode">The analysis mode to set.</param>
/// <param name="value">The analyzed state to set.</param>
public void SetAnalyzed(MediaSegmentType mode, bool value) =>
_states[mode] = (value, _states[mode].Blacklisted);
/// <summary> /// <summary>
/// Sets the blacklisted state for the specified analysis mode. /// Sets the blacklisted state for the specified analysis mode.
/// </summary> /// </summary>
/// <param name="mode">The analysis mode to set.</param> /// <param name="mode">The analysis mode to set.</param>
/// <param name="value">The blacklisted state to set.</param> /// <param name="value">The blacklisted state to set.</param>
public void SetBlacklisted(AnalysisMode mode, bool value) => _blacklistedStates[(int)mode] = value; public void SetBlacklisted(MediaSegmentType mode, bool value) =>
_states[mode] = (_states[mode].Analyzed, value);
/// <summary> /// <summary>
/// Resets the analyzed states. /// Resets all states to their default values.
/// </summary> /// </summary>
public void ResetStates() public void ResetStates() =>
{ Array.ForEach(Enum.GetValues<MediaSegmentType>(), mode => _states[mode] = default);
Array.Clear(_analyzedStates);
Array.Clear(_blacklistedStates); /// <summary>
} /// Gets all modes that have been analyzed.
/// </summary>
/// <returns>An IEnumerable of analyzed MediaSegmentTypes.</returns>
public IEnumerable<MediaSegmentType> GetAnalyzedModes() =>
_states.Where(kvp => kvp.Value.Analyzed).Select(kvp => kvp.Key);
/// <summary>
/// Gets all modes that have been blacklisted.
/// </summary>
/// <returns>An IEnumerable of blacklisted MediaSegmentTypes.</returns>
public IEnumerable<MediaSegmentType> GetBlacklistedModes() =>
_states.Where(kvp => kvp.Value.Blacklisted).Select(kvp => kvp.Key);
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Jellyfin.Data.Enums;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data; namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
@ -59,14 +60,14 @@ public class IgnoreListItem
/// </summary> /// </summary>
/// <param name="mode">Analysis mode.</param> /// <param name="mode">Analysis mode.</param>
/// <param name="value">Value to set.</param> /// <param name="value">Value to set.</param>
public void Toggle(AnalysisMode mode, bool value) public void Toggle(MediaSegmentType mode, bool value)
{ {
switch (mode) switch (mode)
{ {
case AnalysisMode.Introduction: case MediaSegmentType.Intro:
IgnoreIntro = value; IgnoreIntro = value;
break; break;
case AnalysisMode.Credits: case MediaSegmentType.Outro:
IgnoreCredits = value; IgnoreCredits = value;
break; break;
} }
@ -77,12 +78,12 @@ public class IgnoreListItem
/// </summary> /// </summary>
/// <param name="mode">Analysis mode.</param> /// <param name="mode">Analysis mode.</param>
/// <returns>True if ignored, false otherwise.</returns> /// <returns>True if ignored, false otherwise.</returns>
public bool IsIgnored(AnalysisMode mode) public bool IsIgnored(MediaSegmentType mode)
{ {
return mode switch return mode switch
{ {
AnalysisMode.Introduction => IgnoreIntro, MediaSegmentType.Intro => IgnoreIntro,
AnalysisMode.Credits => IgnoreCredits, MediaSegmentType.Outro => IgnoreCredits,
_ => false, _ => false,
}; };
} }

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper;
@ -33,7 +34,7 @@ public static partial class FFmpegWrapper
private static Dictionary<string, string> ChromaprintLogs { get; set; } = []; private static Dictionary<string, string> ChromaprintLogs { get; set; } = [];
private static ConcurrentDictionary<(Guid Id, AnalysisMode Mode), Dictionary<uint, int>> InvertedIndexCache { get; set; } = new(); private static ConcurrentDictionary<(Guid Id, MediaSegmentType Mode), Dictionary<uint, int>> InvertedIndexCache { get; set; } = new();
/// <summary> /// <summary>
/// Check that the installed version of ffmpeg supports chromaprint. /// Check that the installed version of ffmpeg supports chromaprint.
@ -109,16 +110,16 @@ public static partial class FFmpegWrapper
/// <param name="episode">Queued episode to fingerprint.</param> /// <param name="episode">Queued episode to fingerprint.</param>
/// <param name="mode">Portion of media file to fingerprint. Introduction = first 25% / 10 minutes and Credits = last 4 minutes.</param> /// <param name="mode">Portion of media file to fingerprint. Introduction = first 25% / 10 minutes and Credits = last 4 minutes.</param>
/// <returns>Numerical fingerprint points.</returns> /// <returns>Numerical fingerprint points.</returns>
public static uint[] Fingerprint(QueuedEpisode episode, AnalysisMode mode) public static uint[] Fingerprint(QueuedEpisode episode, MediaSegmentType mode)
{ {
int start, end; int start, end;
if (mode == AnalysisMode.Introduction) if (mode == MediaSegmentType.Intro)
{ {
start = 0; start = 0;
end = episode.IntroFingerprintEnd; end = episode.IntroFingerprintEnd;
} }
else if (mode == AnalysisMode.Credits) else if (mode == MediaSegmentType.Outro)
{ {
start = episode.CreditsFingerprintStart; start = episode.CreditsFingerprintStart;
end = episode.Duration; end = episode.Duration;
@ -138,7 +139,7 @@ public static partial class FFmpegWrapper
/// <param name="fingerprint">Chromaprint fingerprint.</param> /// <param name="fingerprint">Chromaprint fingerprint.</param>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
/// <returns>Inverted index.</returns> /// <returns>Inverted index.</returns>
public static Dictionary<uint, int> CreateInvertedIndex(Guid id, uint[] fingerprint, AnalysisMode mode) public static Dictionary<uint, int> CreateInvertedIndex(Guid id, uint[] fingerprint, MediaSegmentType mode)
{ {
if (InvertedIndexCache.TryGetValue((id, mode), out var cached)) if (InvertedIndexCache.TryGetValue((id, mode), out var cached))
{ {
@ -468,7 +469,7 @@ public static partial class FFmpegWrapper
/// <param name="start">Time (in seconds) relative to the start of the file to start fingerprinting from.</param> /// <param name="start">Time (in seconds) relative to the start of the file to start fingerprinting from.</param>
/// <param name="end">Time (in seconds) relative to the start of the file to stop fingerprinting at.</param> /// <param name="end">Time (in seconds) relative to the start of the file to stop fingerprinting at.</param>
/// <returns>Numerical fingerprint points.</returns> /// <returns>Numerical fingerprint points.</returns>
private static uint[] Fingerprint(QueuedEpisode episode, AnalysisMode mode, int start, int end) private static uint[] Fingerprint(QueuedEpisode episode, MediaSegmentType mode, int start, int end)
{ {
// Try to load this episode from cache before running ffmpeg. // Try to load this episode from cache before running ffmpeg.
if (LoadCachedFingerprint(episode, mode, out uint[] cachedFingerprint)) if (LoadCachedFingerprint(episode, mode, out uint[] cachedFingerprint))
@ -522,7 +523,7 @@ public static partial class FFmpegWrapper
/// <returns>true if the episode was successfully loaded from cache, false on any other error.</returns> /// <returns>true if the episode was successfully loaded from cache, false on any other error.</returns>
private static bool LoadCachedFingerprint( private static bool LoadCachedFingerprint(
QueuedEpisode episode, QueuedEpisode episode,
AnalysisMode mode, MediaSegmentType mode,
out uint[] fingerprint) out uint[] fingerprint)
{ {
fingerprint = Array.Empty<uint>(); fingerprint = Array.Empty<uint>();
@ -578,7 +579,7 @@ public static partial class FFmpegWrapper
/// <param name="fingerprint">Fingerprint of the episode to store.</param> /// <param name="fingerprint">Fingerprint of the episode to store.</param>
private static void CacheFingerprint( private static void CacheFingerprint(
QueuedEpisode episode, QueuedEpisode episode,
AnalysisMode mode, MediaSegmentType mode,
List<uint> fingerprint) List<uint> fingerprint)
{ {
// Bail out if caching isn't enabled. // Bail out if caching isn't enabled.
@ -627,11 +628,11 @@ public static partial class FFmpegWrapper
/// Remove cached fingerprints from disk by mode. /// Remove cached fingerprints from disk by mode.
/// </summary> /// </summary>
/// <param name="mode">Analysis mode.</param> /// <param name="mode">Analysis mode.</param>
public static void DeleteCacheFiles(AnalysisMode mode) public static void DeleteCacheFiles(MediaSegmentType mode)
{ {
foreach (var filePath in Directory.EnumerateFiles(Plugin.Instance!.FingerprintCachePath)) foreach (var filePath in Directory.EnumerateFiles(Plugin.Instance!.FingerprintCachePath))
{ {
var shouldDelete = (mode == AnalysisMode.Introduction) var shouldDelete = (mode == MediaSegmentType.Intro)
? !filePath.Contains("credit", StringComparison.OrdinalIgnoreCase) ? !filePath.Contains("credit", StringComparison.OrdinalIgnoreCase)
&& !filePath.Contains("blackframes", StringComparison.OrdinalIgnoreCase) && !filePath.Contains("blackframes", StringComparison.OrdinalIgnoreCase)
: filePath.Contains("credit", StringComparison.OrdinalIgnoreCase) : filePath.Contains("credit", StringComparison.OrdinalIgnoreCase)
@ -651,18 +652,18 @@ public static partial class FFmpegWrapper
/// <param name="episode">Episode.</param> /// <param name="episode">Episode.</param>
/// <param name="mode">Analysis mode.</param> /// <param name="mode">Analysis mode.</param>
/// <returns>Path.</returns> /// <returns>Path.</returns>
public static string GetFingerprintCachePath(QueuedEpisode episode, AnalysisMode mode) public static string GetFingerprintCachePath(QueuedEpisode episode, MediaSegmentType mode)
{ {
var basePath = Path.Join( var basePath = Path.Join(
Plugin.Instance!.FingerprintCachePath, Plugin.Instance!.FingerprintCachePath,
episode.EpisodeId.ToString("N")); episode.EpisodeId.ToString("N"));
if (mode == AnalysisMode.Introduction) if (mode == MediaSegmentType.Intro)
{ {
return basePath; return basePath;
} }
if (mode == AnalysisMode.Credits) if (mode == MediaSegmentType.Outro)
{ {
return basePath + "-credits"; return basePath + "-credits";
} }

View File

@ -6,7 +6,7 @@ using System.Runtime.Serialization;
using System.Xml; using System.Xml;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
namespace ConfusedPolarBear.Plugin.IntroSkipper namespace ConfusedPolarBear.Plugin.IntroSkipper.Helper
{ {
internal sealed class XmlSerializationHelper internal sealed class XmlSerializationHelper
{ {

View File

@ -4,8 +4,8 @@ using System.IO;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager
{
/// <summary> /// <summary>
/// Update EDL files associated with a list of episodes. /// Update EDL files associated with a list of episodes.
/// </summary> /// </summary>
@ -114,3 +114,4 @@ public static class EdlManager
return Path.ChangeExtension(mediaPath, "edl"); return Path.ChangeExtension(mediaPath, "edl");
} }
} }
}

View File

@ -10,8 +10,8 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager
{
/// <summary> /// <summary>
/// Manages enqueuing library items for analysis. /// Manages enqueuing library items for analysis.
/// </summary> /// </summary>
@ -241,56 +241,50 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
/// <param name="candidates">Queued media items.</param> /// <param name="candidates">Queued media items.</param>
/// <param name="modes">Analysis mode.</param> /// <param name="modes">Analysis mode.</param>
/// <returns>Media items that have been verified to exist in Jellyfin and in storage.</returns> /// <returns>Media items that have been verified to exist in Jellyfin and in storage.</returns>
public (IReadOnlyList<QueuedEpisode> VerifiedItems, IReadOnlyCollection<AnalysisMode> RequiredModes) public (IReadOnlyList<QueuedEpisode> VerifiedItems, IReadOnlyCollection<MediaSegmentType> RequiredModes)
VerifyQueue(IReadOnlyList<QueuedEpisode> candidates, IReadOnlyCollection<AnalysisMode> modes) VerifyQueue(IReadOnlyList<QueuedEpisode> candidates, IReadOnlyCollection<MediaSegmentType> modes)
{ {
var verified = new List<QueuedEpisode>(); var verified = new List<QueuedEpisode>();
var reqModes = new HashSet<AnalysisMode>(); var reqModes = new HashSet<MediaSegmentType>(modes);
foreach (var candidate in candidates) foreach (var candidate in candidates)
{ {
try try
{ {
var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId); if (!File.Exists(Plugin.Instance!.GetItemPath(candidate.EpisodeId)))
if (!File.Exists(path))
{ {
continue; continue;
} }
verified.Add(candidate); verified.Add(candidate);
reqModes.ExceptWith(candidate.State.GetAnalyzedModes());
reqModes.ExceptWith(candidate.State.GetBlacklistedModes());
foreach (var mode in modes) if (reqModes.Remove(MediaSegmentType.Intro) && Plugin.Instance.Intros.ContainsKey(candidate.EpisodeId))
{ {
if (candidate.State.IsAnalyzed(mode) || candidate.State.IsBlacklisted(mode)) candidate.State.SetAnalyzed(MediaSegmentType.Intro, true);
{
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 else
{ {
reqModes.Add(mode); reqModes.Add(MediaSegmentType.Intro);
} }
if (reqModes.Remove(MediaSegmentType.Outro) && Plugin.Instance.Credits.ContainsKey(candidate.EpisodeId))
{
candidate.State.SetAnalyzed(MediaSegmentType.Outro, true);
}
else
{
reqModes.Add(MediaSegmentType.Outro);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug( _logger.LogDebug("Skipping analysis of {Name} ({Id}): {Exception}", candidate.Name, candidate.EpisodeId, ex);
"Skipping analysis of {Name} ({Id}): {Exception}",
candidate.Name,
candidate.EpisodeId,
ex);
} }
} }
return (verified, reqModes); return (verified, reqModes);
} }
} }
}

View File

@ -6,6 +6,8 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Helper;
using Jellyfin.Data.Enums;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
@ -182,16 +184,16 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// Save timestamps to disk. /// Save timestamps to disk.
/// </summary> /// </summary>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
public void SaveTimestamps(AnalysisMode mode) public void SaveTimestamps(MediaSegmentType mode)
{ {
List<Segment> introList = []; List<Segment> introList = [];
var filePath = mode == AnalysisMode.Introduction var filePath = mode == MediaSegmentType.Intro
? _introPath ? _introPath
: _creditsPath; : _creditsPath;
lock (_introsLock) lock (_introsLock)
{ {
introList.AddRange(mode == AnalysisMode.Introduction introList.AddRange(mode == MediaSegmentType.Intro
? Instance!.Intros.Values ? Instance!.Intros.Values
: Instance!.Credits.Values); : Instance!.Credits.Values);
} }
@ -235,7 +237,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <param name="id">Item id.</param> /// <param name="id">Item id.</param>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
/// <returns>True if ignored, false otherwise.</returns> /// <returns>True if ignored, false otherwise.</returns>
public bool IsIgnored(Guid id, AnalysisMode mode) public bool IsIgnored(Guid id, MediaSegmentType mode)
{ {
return Instance!.IgnoreList.TryGetValue(id, out var item) && item.IsIgnored(mode); return Instance!.IgnoreList.TryGetValue(id, out var item) && item.IsIgnored(mode);
} }
@ -312,9 +314,9 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <param name="id">Item id.</param> /// <param name="id">Item id.</param>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
/// <returns>Intro.</returns> /// <returns>Intro.</returns>
internal static Segment GetIntroByMode(Guid id, AnalysisMode mode) internal static Segment GetIntroByMode(Guid id, MediaSegmentType mode)
{ {
return mode == AnalysisMode.Introduction return mode == MediaSegmentType.Intro
? Instance!.Intros[id] ? Instance!.Intros[id]
: Instance!.Credits[id]; : Instance!.Credits[id];
} }
@ -373,15 +375,15 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <returns>State of this item.</returns> /// <returns>State of this item.</returns>
internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState()); internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState());
internal void UpdateTimestamps(IReadOnlyDictionary<Guid, Segment> newTimestamps, AnalysisMode mode) internal void UpdateTimestamps(IReadOnlyDictionary<Guid, Segment> newTimestamps, MediaSegmentType mode)
{ {
foreach (var intro in newTimestamps) foreach (var intro in newTimestamps)
{ {
if (mode == AnalysisMode.Introduction) if (mode == MediaSegmentType.Intro)
{ {
Instance!.Intros.AddOrUpdate(intro.Key, intro.Value, (key, oldValue) => intro.Value); Instance!.Intros.AddOrUpdate(intro.Key, intro.Value, (key, oldValue) => intro.Value);
} }
else if (mode == AnalysisMode.Credits) else if (mode == MediaSegmentType.Outro)
{ {
Instance!.Credits.AddOrUpdate(intro.Key, intro.Value, (key, oldValue) => intro.Value); Instance!.Credits.AddOrUpdate(intro.Key, intro.Value, (key, oldValue) => intro.Value);
} }
@ -404,8 +406,8 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
} }
} }
SaveTimestamps(AnalysisMode.Introduction); SaveTimestamps(MediaSegmentType.Intro);
SaveTimestamps(AnalysisMode.Credits); SaveTimestamps(MediaSegmentType.Outro);
} }
private void MigrateRepoUrl(IServerConfigurationManager serverConfiguration) private void MigrateRepoUrl(IServerConfigurationManager serverConfiguration)

View File

@ -1,4 +1,5 @@
using ConfusedPolarBear.Plugin.IntroSkipper.Providers; using ConfusedPolarBear.Plugin.IntroSkipper.Providers;
using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;

View File

@ -6,6 +6,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers; using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -16,7 +18,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// </summary> /// </summary>
public class BaseItemAnalyzerTask public class BaseItemAnalyzerTask
{ {
private readonly IReadOnlyCollection<AnalysisMode> _analysisModes; private readonly IReadOnlyCollection<MediaSegmentType> _modes;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -32,12 +34,12 @@ public class BaseItemAnalyzerTask
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
public BaseItemAnalyzerTask( public BaseItemAnalyzerTask(
IReadOnlyCollection<AnalysisMode> modes, IReadOnlyCollection<MediaSegmentType> modes,
ILogger logger, ILogger logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
_analysisModes = modes; _modes = modes;
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_libraryManager = libraryManager; _libraryManager = libraryManager;
@ -79,7 +81,7 @@ public class BaseItemAnalyzerTask
queue = queue.Where(kvp => seasonsToAnalyze.Contains(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); queue = queue.Where(kvp => seasonsToAnalyze.Contains(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
} }
int totalQueued = queue.Sum(kvp => kvp.Value.Count) * _analysisModes.Count; int totalQueued = queue.Sum(kvp => kvp.Value.Count) * _modes.Count;
if (totalQueued == 0) if (totalQueued == 0)
{ {
throw new FingerprintException( throw new FingerprintException(
@ -105,7 +107,7 @@ public class BaseItemAnalyzerTask
// of the current media items were deleted from Jellyfin since the task was started. // of the current media items were deleted from Jellyfin since the task was started.
var (episodes, requiredModes) = queueManager.VerifyQueue( var (episodes, requiredModes) = queueManager.VerifyQueue(
season.Value, season.Value,
_analysisModes.Where(m => !Plugin.Instance!.IsIgnored(season.Key, m)).ToList()); _modes.Where(m => !Plugin.Instance!.IsIgnored(season.Key, m)).ToList());
if (episodes.Count == 0) if (episodes.Count == 0)
{ {
@ -120,13 +122,13 @@ public class BaseItemAnalyzerTask
first.SeriesName, first.SeriesName,
first.SeasonNumber); first.SeasonNumber);
Interlocked.Add(ref totalProcessed, episodes.Count * _analysisModes.Count); // Update total Processed directly Interlocked.Add(ref totalProcessed, episodes.Count * _modes.Count); // Update total Processed directly
progress.Report(totalProcessed * 100 / totalQueued); progress.Report(totalProcessed * 100 / totalQueued);
return; return;
} }
if (_analysisModes.Count != requiredModes.Count) if (_modes.Count != requiredModes.Count)
{ {
Interlocked.Add(ref totalProcessed, episodes.Count); Interlocked.Add(ref totalProcessed, episodes.Count);
progress.Report(totalProcessed * 100 / totalQueued); // Partial analysis some modes have already been analyzed progress.Report(totalProcessed * 100 / totalQueued); // Partial analysis some modes have already been analyzed
@ -139,7 +141,7 @@ public class BaseItemAnalyzerTask
return; return;
} }
foreach (AnalysisMode mode in requiredModes) foreach (MediaSegmentType mode in requiredModes)
{ {
var analyzed = AnalyzeItems(episodes, mode, cancellationToken); var analyzed = AnalyzeItems(episodes, mode, cancellationToken);
Interlocked.Add(ref totalProcessed, analyzed); Interlocked.Add(ref totalProcessed, analyzed);
@ -181,7 +183,7 @@ public class BaseItemAnalyzerTask
/// <returns>Number of items that were successfully analyzed.</returns> /// <returns>Number of items that were successfully analyzed.</returns>
private int AnalyzeItems( private int AnalyzeItems(
IReadOnlyList<QueuedEpisode> items, IReadOnlyList<QueuedEpisode> items,
AnalysisMode mode, MediaSegmentType mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var totalItems = items.Count; var totalItems = items.Count;
@ -216,7 +218,7 @@ public class BaseItemAnalyzerTask
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>())); analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
} }
if (mode == AnalysisMode.Credits) if (mode == MediaSegmentType.Outro)
{ {
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>())); analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
} }

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,29 +14,22 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// <summary> /// <summary>
/// Analyze all television episodes for introduction sequences. /// Analyze all television episodes for introduction sequences.
/// </summary> /// </summary>
public class CleanCacheTask : IScheduledTask /// <remarks>
{
private readonly ILogger<CleanCacheTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="CleanCacheTask"/> class. /// Initializes a new instance of the <see cref="CleanCacheTask"/> class.
/// </summary> /// </remarks>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param> /// <param name="logger">Logger.</param>
public CleanCacheTask( public class CleanCacheTask(
ILogger<CleanCacheTask> logger, ILogger<CleanCacheTask> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager) : IScheduledTask
{ {
_logger = logger; private readonly ILogger<CleanCacheTask> _logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager; private readonly ILoggerFactory _loggerFactory = loggerFactory;
}
private readonly ILibraryManager _libraryManager = libraryManager;
/// <summary> /// <summary>
/// Gets the task name. /// Gets the task name.

View File

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -14,29 +13,22 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// Analyze all television episodes for credits. /// Analyze all television episodes for credits.
/// TODO: analyze all media files. /// TODO: analyze all media files.
/// </summary> /// </summary>
public class DetectCreditsTask : IScheduledTask /// <remarks>
{
private readonly ILogger<DetectCreditsTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="DetectCreditsTask"/> class. /// Initializes a new instance of the <see cref="DetectCreditsTask"/> class.
/// </summary> /// </remarks>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param> /// <param name="logger">Logger.</param>
public DetectCreditsTask( public class DetectCreditsTask(
ILogger<DetectCreditsTask> logger, ILogger<DetectCreditsTask> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager) : IScheduledTask
{ {
_logger = logger; private readonly ILogger<DetectCreditsTask> _logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager; private readonly ILoggerFactory _loggerFactory = loggerFactory;
}
private readonly ILibraryManager _libraryManager = libraryManager;
/// <summary> /// <summary>
/// Gets the task name. /// Gets the task name.
@ -82,7 +74,7 @@ public class DetectCreditsTask : IScheduledTask
{ {
_logger.LogInformation("Scheduled Task is starting"); _logger.LogInformation("Scheduled Task is starting");
var modes = new List<AnalysisMode> { AnalysisMode.Credits }; var modes = new List<MediaSegmentType> { MediaSegmentType.Outro };
var baseCreditAnalyzer = new BaseItemAnalyzerTask( var baseCreditAnalyzer = new BaseItemAnalyzerTask(
modes, modes,

View File

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,29 +12,22 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// <summary> /// <summary>
/// Analyze all television episodes for introduction sequences. /// Analyze all television episodes for introduction sequences.
/// </summary> /// </summary>
public class DetectIntrosCreditsTask : IScheduledTask /// <remarks>
{
private readonly ILogger<DetectIntrosCreditsTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="DetectIntrosCreditsTask"/> class. /// Initializes a new instance of the <see cref="DetectIntrosCreditsTask"/> class.
/// </summary> /// </remarks>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param> /// <param name="logger">Logger.</param>
public DetectIntrosCreditsTask( public class DetectIntrosCreditsTask(
ILogger<DetectIntrosCreditsTask> logger, ILogger<DetectIntrosCreditsTask> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager) : IScheduledTask
{ {
_logger = logger; private readonly ILogger<DetectIntrosCreditsTask> _logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager; private readonly ILoggerFactory _loggerFactory = loggerFactory;
}
private readonly ILibraryManager _libraryManager = libraryManager;
/// <summary> /// <summary>
/// Gets the task name. /// Gets the task name.
@ -81,7 +73,7 @@ public class DetectIntrosCreditsTask : IScheduledTask
{ {
_logger.LogInformation("Scheduled Task is starting"); _logger.LogInformation("Scheduled Task is starting");
var modes = new List<AnalysisMode> { AnalysisMode.Introduction, AnalysisMode.Credits }; var modes = new List<MediaSegmentType> { MediaSegmentType.Intro, MediaSegmentType.Outro };
var baseIntroAnalyzer = new BaseItemAnalyzerTask( var baseIntroAnalyzer = new BaseItemAnalyzerTask(
modes, modes,

View File

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,29 +12,22 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// <summary> /// <summary>
/// Analyze all television episodes for introduction sequences. /// Analyze all television episodes for introduction sequences.
/// </summary> /// </summary>
public class DetectIntrosTask : IScheduledTask /// <remarks>
{
private readonly ILogger<DetectIntrosTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="DetectIntrosTask"/> class. /// Initializes a new instance of the <see cref="DetectIntrosTask"/> class.
/// </summary> /// </remarks>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param> /// <param name="logger">Logger.</param>
public DetectIntrosTask( public class DetectIntrosTask(
ILogger<DetectIntrosTask> logger, ILogger<DetectIntrosTask> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager) : IScheduledTask
{ {
_logger = logger; private readonly ILogger<DetectIntrosTask> _logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager; private readonly ILoggerFactory _loggerFactory = loggerFactory;
}
private readonly ILibraryManager _libraryManager = libraryManager;
/// <summary> /// <summary>
/// Gets the task name. /// Gets the task name.
@ -81,7 +73,7 @@ public class DetectIntrosTask : IScheduledTask
{ {
_logger.LogInformation("Scheduled Task is starting"); _logger.LogInformation("Scheduled Task is starting");
var modes = new List<AnalysisMode> { AnalysisMode.Introduction }; var modes = new List<MediaSegmentType> { MediaSegmentType.Intro };
var baseIntroAnalyzer = new BaseItemAnalyzerTask( var baseIntroAnalyzer = new BaseItemAnalyzerTask(
modes, modes,

View File

@ -15,8 +15,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
{
/// <summary> /// <summary>
/// Automatically skip past introduction sequences. /// Automatically skip past introduction sequences.
/// Commands clients to seek to the end of the intro as soon as they start playing it. /// Commands clients to seek to the end of the intro as soon as they start playing it.
@ -33,7 +33,6 @@ public class AutoSkip(
ILogger<AutoSkip> logger) : IHostedService, IDisposable ILogger<AutoSkip> logger) : IHostedService, IDisposable
{ {
private readonly object _sentSeekCommandLock = new(); private readonly object _sentSeekCommandLock = new();
private ILogger<AutoSkip> _logger = logger; private ILogger<AutoSkip> _logger = logger;
private IUserDataManager _userDataManager = userDataManager; private IUserDataManager _userDataManager = userDataManager;
private ISessionManager _sessionManager = sessionManager; private ISessionManager _sessionManager = sessionManager;
@ -228,3 +227,4 @@ public class AutoSkip(
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
}

View File

@ -15,8 +15,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
{
/// <summary> /// <summary>
/// Automatically skip past credit sequences. /// Automatically skip past credit sequences.
/// Commands clients to seek to the end of the credits as soon as they start playing it. /// Commands clients to seek to the end of the credits as soon as they start playing it.
@ -33,7 +33,6 @@ public class AutoSkipCredits(
ILogger<AutoSkipCredits> logger) : IHostedService, IDisposable ILogger<AutoSkipCredits> logger) : IHostedService, IDisposable
{ {
private readonly object _sentSeekCommandLock = new(); private readonly object _sentSeekCommandLock = new();
private ILogger<AutoSkipCredits> _logger = logger; private ILogger<AutoSkipCredits> _logger = logger;
private IUserDataManager _userDataManager = userDataManager; private IUserDataManager _userDataManager = userDataManager;
private ISessionManager _sessionManager = sessionManager; private ISessionManager _sessionManager = sessionManager;
@ -228,3 +227,4 @@ public class AutoSkipCredits(
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
}

View File

@ -3,8 +3,9 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data; using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks; using ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -261,18 +262,18 @@ public sealed class Entrypoint : IHostedService, IDisposable
_analyzeAgain = false; _analyzeAgain = false;
var progress = new Progress<double>(); var progress = new Progress<double>();
var modes = new List<AnalysisMode>(); var modes = new List<MediaSegmentType>();
var tasklogger = _loggerFactory.CreateLogger("DefaultLogger"); var tasklogger = _loggerFactory.CreateLogger("DefaultLogger");
if (_config.AutoDetectIntros) if (_config.AutoDetectIntros)
{ {
modes.Add(AnalysisMode.Introduction); modes.Add(MediaSegmentType.Intro);
tasklogger = _loggerFactory.CreateLogger<DetectIntrosTask>(); tasklogger = _loggerFactory.CreateLogger<DetectIntrosTask>();
} }
if (_config.AutoDetectCredits) if (_config.AutoDetectCredits)
{ {
modes.Add(AnalysisMode.Credits); modes.Add(MediaSegmentType.Outro);
tasklogger = modes.Count == 2 tasklogger = modes.Count == 2
? _loggerFactory.CreateLogger<DetectIntrosCreditsTask>() ? _loggerFactory.CreateLogger<DetectIntrosCreditsTask>()
: _loggerFactory.CreateLogger<DetectCreditsTask>(); : _loggerFactory.CreateLogger<DetectCreditsTask>();