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:
parent
73287b79a5
commit
29ee3e0bc8
@ -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,
|
||||||
|
@ -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));
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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"),
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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>()));
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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>();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user