From 4558b59e9578584e58fca98f090ef41ca0573327 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 25 Aug 2022 01:28:41 -0500
Subject: [PATCH 01/29] Only use inverted indexes to search
---
.../Data/AnalysisStatistics.cs | 19 +-
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 233 +-----------------
2 files changed, 4 insertions(+), 248 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
index d0f1e97..522dbdf 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
@@ -25,30 +25,15 @@ public class AnalysisStatistics
///
public ThreadSafeInteger IndexSearches { get; } = new ThreadSafeInteger();
- ///
- /// Gets the number of times a quick scan successfully located a pair of introductions.
- ///
- public ThreadSafeInteger QuickScans { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets the number of times a full scan successfully located a pair of introductions.
- ///
- public ThreadSafeInteger FullScans { get; } = new ThreadSafeInteger();
-
///
/// Gets the total CPU time spent waiting for audio fingerprints to be generated.
///
public ThreadSafeInteger FingerprintCPUTime { get; } = new ThreadSafeInteger();
///
- /// Gets the total CPU time spent analyzing fingerprints in the initial pass.
+ /// Gets the total CPU time spent analyzing fingerprints.
///
- public ThreadSafeInteger FirstPassCPUTime { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets the total CPU time spent analyzing fingerprints in the second pass.
- ///
- public ThreadSafeInteger SecondPassCPUTime { get; } = new ThreadSafeInteger();
+ public ThreadSafeInteger AnalysisCPUTime { get; } = new ThreadSafeInteger();
///
/// Gets the total task runtime across all threads.
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index 47033e4..0d4507f 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -30,16 +30,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
///
private const double SamplesToSeconds = 0.128;
- ///
- /// Bucket size used in the reanalysis histogram.
- ///
- private const int ReanalysisBucketWidth = 5;
-
- ///
- /// Maximum time (in seconds) that an intro's duration can be different from a typical intro's duration before marking it for reanalysis.
- ///
- private const double ReanalysisTolerance = ReanalysisBucketWidth * 1.5;
-
private readonly ILogger _logger;
private readonly ILogger _queueLogger;
@@ -375,17 +365,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
}
}
- // Only run the second pass if the user hasn't requested cancellation and we found an intro
- if (!cancellationToken.IsCancellationRequested && everFoundIntro)
- {
- var start = DateTime.Now;
-
- // Run a second pass over this season to remove outliers and fix episodes that failed in the first pass.
- RunSecondPass(season.Value);
-
- analysisStatistics.SecondPassCPUTime.AddDuration(start);
- }
-
lock (_introsLock)
{
Plugin.Instance!.SaveTimestamps();
@@ -441,7 +420,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
// If this isn't running as part of the first analysis pass, don't count this CPU time as first pass time.
var start = isFirstPass ? DateTime.Now : DateTime.MinValue;
- // ===== Method 1: Inverted indexes =====
// Creates an inverted fingerprint point index for both episodes.
// For every point which is a 100% match, search for an introduction at that point.
var (lhsRanges, rhsRanges) = SearchInvertedIndex(lhsPoints, rhsPoints);
@@ -450,46 +428,17 @@ public class AnalyzeEpisodesTask : IScheduledTask
{
_logger.LogTrace("Index search successful");
analysisStatistics.IndexSearches.Increment();
- analysisStatistics.FirstPassCPUTime.AddDuration(start);
+ analysisStatistics.AnalysisCPUTime.AddDuration(start);
return GetLongestTimeRange(lhsId, lhsRanges, rhsId, rhsRanges);
}
- // ===== Method 2: Quick scan =====
- // Tests if an intro can be found within the first 5 seconds of the episodes. ±5/0.128 = ±40 samples.
- (lhsRanges, rhsRanges) = ShiftEpisodes(lhsPoints, rhsPoints, -40, 40);
-
- if (lhsRanges.Count > 0)
- {
- _logger.LogTrace("Quick scan successful");
- analysisStatistics.QuickScans.Increment();
- analysisStatistics.FirstPassCPUTime.AddDuration(start);
-
- return GetLongestTimeRange(lhsId, lhsRanges, rhsId, rhsRanges);
- }
-
- // ===== Method 3: Full scan =====
- // Compares all elements of the shortest fingerprint to the other fingerprint.
- var limit = Math.Min(lhsPoints.Length, rhsPoints.Length);
- (lhsRanges, rhsRanges) = ShiftEpisodes(lhsPoints, rhsPoints, -1 * limit, limit);
-
- if (lhsRanges.Count > 0)
- {
- _logger.LogTrace("Full scan successful");
- analysisStatistics.FullScans.Increment();
- analysisStatistics.FirstPassCPUTime.AddDuration(start);
-
- return GetLongestTimeRange(lhsId, lhsRanges, rhsId, rhsRanges);
- }
-
- // No method was able to find an introduction, return nothing.
-
_logger.LogTrace(
"Unable to find a shared introduction sequence between {LHS} and {RHS}",
lhsId,
rhsId);
- analysisStatistics.FirstPassCPUTime.AddDuration(start);
+ analysisStatistics.AnalysisCPUTime.AddDuration(start);
return (new Intro(lhsId), new Intro(rhsId));
}
@@ -694,184 +643,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
return BitOperations.PopCount(number);
}
- ///
- /// Reanalyze the most recently analyzed season.
- /// Looks for and fixes intro durations that were either not found or are statistical outliers.
- ///
- /// List of episodes that was just analyzed.
- private void RunSecondPass(List episodes)
- {
- // First, assert that at least half of the episodes in this season have an intro.
- var validCount = 0;
- var totalCount = episodes.Count;
-
- foreach (var episode in episodes)
- {
- if (Plugin.Instance!.Intros.TryGetValue(episode.EpisodeId, out var intro) && intro.Valid)
- {
- validCount++;
- }
- }
-
- var percentValid = (validCount * 100) / totalCount;
- _logger.LogTrace("Found intros in {Valid}/{Total} ({Percent}%) of episodes", validCount, totalCount, percentValid);
- if (percentValid < 50)
- {
- return;
- }
-
- // Create a histogram of all episode durations
- var histogram = new Dictionary();
- foreach (var episode in episodes)
- {
- var id = episode.EpisodeId;
- var duration = GetIntroDuration(id);
-
- if (duration < minimumIntroDuration)
- {
- continue;
- }
-
- // Bucket the duration into equally sized groups
- var bucket = Convert.ToInt32(Math.Floor(duration / ReanalysisBucketWidth)) * ReanalysisBucketWidth;
-
- // TryAdd returns true when the key was successfully added (i.e. for newly created buckets).
- // Newly created buckets are initialized with the provided episode ID, so nothing else needs to be done for them.
- if (histogram.TryAdd(bucket, new SeasonHistogram(id)))
- {
- continue;
- }
-
- histogram[bucket].Episodes.Add(id);
- }
-
- // Find the bucket that was seen most often, as this is likely to be the true intro length.
- var maxDuration = 0;
- var maxBucket = new SeasonHistogram(Guid.Empty);
- foreach (var entry in histogram)
- {
- if (entry.Value.Count > maxBucket.Count)
- {
- maxDuration = entry.Key;
- maxBucket = entry.Value;
- }
- }
-
- // Ensure that the most frequently seen bucket has a majority
- percentValid = (maxBucket.Count * 100) / validCount;
- _logger.LogTrace(
- "Intro duration {Duration} appeared {Frequency} times ({Percent}%)",
- maxDuration,
- maxBucket.Count,
- percentValid);
-
- if (percentValid < 50 || maxBucket.Episodes[0].Equals(Guid.Empty))
- {
- return;
- }
-
- _logger.LogTrace("Second pass is processing {Count} episodes", totalCount - maxBucket.Count);
-
- // Calculate a range of intro durations that are most likely to be correct.
- var maxEpisode = episodes.Find(x => x.EpisodeId == maxBucket.Episodes[0]);
- if (maxEpisode is null)
- {
- _logger.LogError("Second pass failed to get episode from bucket");
- return;
- }
-
- var lhsDuration = GetIntroDuration(maxEpisode.EpisodeId);
- var (lowTargetDuration, highTargetDuration) = (
- lhsDuration - ReanalysisTolerance,
- lhsDuration + ReanalysisTolerance);
-
- // TODO: add limit and make it customizable
- var count = maxBucket.Episodes.Count - 1;
- var goodFingerprints = new List();
- foreach (var id in maxBucket.Episodes)
- {
- if (!_fingerprintCache.TryGetValue(id, out var fp))
- {
- _logger.LogTrace("Second pass: max bucket episode {Id} not in cache, skipping", id);
- continue;
- }
-
- goodFingerprints.Add(fp);
- }
-
- foreach (var episode in episodes)
- {
- // Don't reanalyze episodes from the max bucket
- if (maxBucket.Episodes.Contains(episode.EpisodeId))
- {
- continue;
- }
-
- var oldDuration = GetIntroDuration(episode.EpisodeId);
-
- // If the episode's intro duration is close enough to the targeted bucket, leave it alone.
- if (Math.Abs(lhsDuration - oldDuration) <= ReanalysisTolerance)
- {
- _logger.LogTrace(
- "Not reanalyzing episode {Path} (intro is {Initial}, target is {Max})",
- episode.Path,
- Math.Round(oldDuration, 2),
- maxDuration);
-
- continue;
- }
-
- _logger.LogTrace(
- "Reanalyzing episode {Path} (intro is {Initial}, target is {Max})",
- episode.Path,
- Math.Round(oldDuration, 2),
- maxDuration);
-
- // Analyze the episode again, ignoring whatever is returned for the known good episode.
- foreach (var lhsFingerprint in goodFingerprints)
- {
- if (!_fingerprintCache.TryGetValue(episode.EpisodeId, out var fp))
- {
- _logger.LogTrace("Unable to get cached fingerprint for {Id}, skipping", episode.EpisodeId);
- continue;
- }
-
- var (_, newRhs) = FingerprintEpisodes(
- maxEpisode.EpisodeId,
- lhsFingerprint,
- episode.EpisodeId,
- fp,
- false);
-
- // Ensure that the new intro duration is within the targeted bucket and longer than what was found previously.
- var newDuration = Math.Round(newRhs.IntroEnd - newRhs.IntroStart, 2);
- if (newDuration < oldDuration || newDuration < lowTargetDuration || newDuration > highTargetDuration)
- {
- _logger.LogTrace(
- "Ignoring reanalysis for {Path} (was {Initial}, now is {New})",
- episode.Path,
- oldDuration,
- newDuration);
-
- continue;
- }
-
- _logger.LogTrace(
- "Reanalysis succeeded for {Path} (was {Initial}, now is {New})",
- episode.Path,
- oldDuration,
- newDuration);
-
- lock (_introsLock)
- {
- Plugin.Instance!.Intros[episode.EpisodeId] = newRhs;
- }
-
- break;
- }
- }
- }
-
private double GetIntroDuration(Guid id)
{
if (!Plugin.Instance!.Intros.TryGetValue(id, out var episode))
From 1e1db330d7b49541073ed7618c05c5d7a4e5ca2e Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 25 Aug 2022 01:37:57 -0500
Subject: [PATCH 02/29] Always analyze episodes
---
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 64 ++-----------------
1 file changed, 5 insertions(+), 59 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index 0d4507f..c66b5e9 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -264,35 +264,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
return episodes.Count;
}
- var unanalyzed = false;
-
- // Only log an analysis message if there are unanalyzed episodes in this season.
- foreach (var episode in episodes)
- {
- if (!Plugin.Instance!.Intros.ContainsKey(episode.EpisodeId))
- {
- unanalyzed = true;
- break;
- }
- }
-
- if (unanalyzed)
- {
- _logger.LogInformation(
- "Analyzing {Count} episodes from {Name} season {Season}",
- season.Value.Count,
- first.SeriesName,
- first.SeasonNumber);
- }
- else
- {
- _logger.LogDebug(
- "All episodes from {Name} season {Season} have already been analyzed",
- first.SeriesName,
- first.SeasonNumber);
-
- return 0;
- }
+ _logger.LogInformation(
+ "Analyzing {Count} episodes from {Name} season {Season}",
+ season.Value.Count,
+ first.SeriesName,
+ first.SeasonNumber);
// Ensure there are an even number of episodes
if (episodes.Count % 2 != 0)
@@ -301,8 +277,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
}
// Analyze each pair of episodes in the current season
- var everFoundIntro = false;
- var failures = 0;
for (var i = 0; i < episodes.Count; i += 2)
{
if (cancellationToken.IsCancellationRequested)
@@ -313,26 +287,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
var lhs = episodes[i];
var rhs = episodes[i + 1];
- if (!everFoundIntro && failures >= 20)
- {
- _logger.LogWarning(
- "Failed to find an introduction in {Series} season {Season}",
- lhs.SeriesName,
- lhs.SeasonNumber);
-
- break;
- }
-
- if (Plugin.Instance!.Intros.ContainsKey(lhs.EpisodeId) && Plugin.Instance!.Intros.ContainsKey(rhs.EpisodeId))
- {
- _logger.LogTrace(
- "Episodes {LHS} and {RHS} have both already been fingerprinted",
- lhs.EpisodeId,
- rhs.EpisodeId);
-
- continue;
- }
-
try
{
_logger.LogTrace("Analyzing {LHS} and {RHS}", lhs.Path, rhs.Path);
@@ -341,14 +295,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
seasonIntros[lhsIntro.EpisodeId] = lhsIntro;
seasonIntros[rhsIntro.EpisodeId] = rhsIntro;
analysisStatistics.TotalAnalyzedEpisodes.Add(2);
-
- if (!lhsIntro.Valid)
- {
- failures += 2;
- continue;
- }
-
- everFoundIntro = true;
}
catch (FingerprintException ex)
{
From 5867bb378f5f4078c7f86f71897d1fb193146e51 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 25 Aug 2022 22:11:39 -0500
Subject: [PATCH 03/29] Implement new search algorithm
---
.../TestAudioFingerprinting.cs | 2 +-
.../Data/Intro.cs | 7 ++
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 106 +++++++++++-------
3 files changed, 76 insertions(+), 39 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
index c6e2570..dbd4fab 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
@@ -90,7 +90,7 @@ public class TestAudioFingerprinting
var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3");
var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3");
- var (lhs, rhs) = task.FingerprintEpisodes(lhsEpisode, rhsEpisode);
+ var (lhs, rhs) = task.CompareEpisodes(lhsEpisode, rhsEpisode);
Assert.True(lhs.Valid);
Assert.Equal(0, lhs.IntroStart);
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs
index 0875b80..4cd88e5 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs
@@ -1,4 +1,5 @@
using System;
+using System.Text.Json.Serialization;
namespace ConfusedPolarBear.Plugin.IntroSkipper;
@@ -49,6 +50,12 @@ public class Intro
///
public bool Valid => IntroEnd > 0;
+ ///
+ /// Gets the duration of this intro.
+ ///
+ [JsonIgnore]
+ public double Duration => IntroEnd - IntroStart;
+
///
/// Gets or sets the introduction sequence start time.
///
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index c66b5e9..d66cae7 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -161,7 +161,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
{
// Increment totalProcessed by the number of episodes in this season that were actually analyzed
// (instead of just using the number of episodes in the current season).
- var analyzed = AnalyzeSeason(season, cancellationToken);
+ var analyzed = AnalyzeSeason(season.Value, cancellationToken);
Interlocked.Add(ref totalProcessed, analyzed);
writeEdl = analyzed > 0 || Plugin.Instance!.Configuration.RegenerateEdlFiles;
}
@@ -241,66 +241,94 @@ public class AnalyzeEpisodesTask : IScheduledTask
return previous;
}
+ // TODO: restore warning
+#pragma warning disable CA1002
+
///
/// Fingerprints all episodes in the provided season and stores the timestamps of all introductions.
///
- /// Pairing of season GUID to a list of QueuedEpisode objects.
+ /// Episodes in this season.
/// Cancellation token provided by the scheduled task.
/// Number of episodes from the provided season that were analyzed.
private int AnalyzeSeason(
- KeyValuePair> season,
+ List episodes,
CancellationToken cancellationToken)
{
var seasonIntros = new Dictionary();
- var episodes = season.Value;
- var first = episodes[0];
/* Don't analyze specials or seasons with an insufficient number of episodes.
* A season with only 1 episode can't be analyzed as it would compare the episode to itself,
* which would result in the entire episode being marked as an introduction, as the audio is identical.
*/
- if (season.Value.Count < 2 || first.SeasonNumber == 0)
+ if (episodes.Count < 2 || episodes[0].SeasonNumber == 0)
{
return episodes.Count;
}
+ var first = episodes[0];
+
_logger.LogInformation(
"Analyzing {Count} episodes from {Name} season {Season}",
- season.Value.Count,
+ episodes.Count,
first.SeriesName,
first.SeasonNumber);
- // Ensure there are an even number of episodes
- if (episodes.Count % 2 != 0)
+ // TODO: cache fingerprints and inverted indexes
+
+ // TODO: implementing bucketing
+
+ // For all episodes
+ foreach (var outer in episodes)
{
- episodes.Add(episodes[episodes.Count - 2]);
+ // Compare the outer episode to all other episodes
+ foreach (var inner in episodes)
+ {
+ // Don't compare the episode to itself
+ if (outer.EpisodeId == inner.EpisodeId)
+ {
+ continue;
+ }
+
+ // Fingerprint both episodes
+ Intro outerIntro;
+ Intro innerIntro;
+
+ try
+ {
+ (outerIntro, innerIntro) = CompareEpisodes(outer, inner);
+ }
+ catch (FingerprintException ex)
+ {
+ // TODO: remove the episode that threw the error from additional processing
+ _logger.LogWarning("Caught fingerprint error: {Ex}", ex);
+ continue;
+ }
+
+ if (!outerIntro.Valid)
+ {
+ continue;
+ }
+
+ // Save this intro if:
+ // - it is the first one we've seen for this episode
+ // - OR it is longer than the previous one
+ if (
+ !seasonIntros.TryGetValue(outer.EpisodeId, out var currentOuterIntro) ||
+ outerIntro.Duration > currentOuterIntro.Duration)
+ {
+ seasonIntros[outer.EpisodeId] = outerIntro;
+ }
+
+ if (
+ !seasonIntros.TryGetValue(inner.EpisodeId, out var currentInnerIntro) ||
+ innerIntro.Duration > currentInnerIntro.Duration)
+ {
+ seasonIntros[inner.EpisodeId] = innerIntro;
+ }
+ }
}
- // Analyze each pair of episodes in the current season
- for (var i = 0; i < episodes.Count; i += 2)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- break;
- }
-
- var lhs = episodes[i];
- var rhs = episodes[i + 1];
-
- try
- {
- _logger.LogTrace("Analyzing {LHS} and {RHS}", lhs.Path, rhs.Path);
-
- var (lhsIntro, rhsIntro) = FingerprintEpisodes(lhs, rhs);
- seasonIntros[lhsIntro.EpisodeId] = lhsIntro;
- seasonIntros[rhsIntro.EpisodeId] = rhsIntro;
- analysisStatistics.TotalAnalyzedEpisodes.Add(2);
- }
- catch (FingerprintException ex)
- {
- _logger.LogError("Caught fingerprint error: {Ex}", ex);
- }
- }
+ // TODO: analysisStatistics.TotalAnalyzedEpisodes.Add(2);
// Ensure only one thread at a time can update the shared intro dictionary.
lock (_introsLock)
@@ -319,13 +347,15 @@ public class AnalyzeEpisodesTask : IScheduledTask
return episodes.Count;
}
+#pragma warning restore CA1002
+
///
/// Analyze two episodes to find an introduction sequence shared between them.
///
/// First episode to analyze.
/// Second episode to analyze.
/// Intros for the first and second episodes.
- public (Intro Lhs, Intro Rhs) FingerprintEpisodes(QueuedEpisode lhsEpisode, QueuedEpisode rhsEpisode)
+ public (Intro Lhs, Intro Rhs) CompareEpisodes(QueuedEpisode lhsEpisode, QueuedEpisode rhsEpisode)
{
var start = DateTime.Now;
var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
@@ -339,7 +369,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
_fingerprintCache[rhsEpisode.EpisodeId] = rhsFingerprint;
}
- return FingerprintEpisodes(
+ return CompareEpisodes(
lhsEpisode.EpisodeId,
lhsFingerprint,
rhsEpisode.EpisodeId,
@@ -356,7 +386,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
/// Second episode fingerprint points.
/// If this was called as part of the first analysis pass, add the elapsed time to the statistics.
/// Intros for the first and second episodes.
- public (Intro Lhs, Intro Rhs) FingerprintEpisodes(
+ public (Intro Lhs, Intro Rhs) CompareEpisodes(
Guid lhsId,
uint[] lhsPoints,
Guid rhsId,
From e4da9342c838eee35f4f3ada7acc3e610f188f27 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 25 Aug 2022 23:01:46 -0500
Subject: [PATCH 04/29] Change fingerprint cache policy
---
.../TestAudioFingerprinting.cs | 15 ++-
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 91 +++++--------------
2 files changed, 36 insertions(+), 70 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
index dbd4fab..a67e858 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
@@ -89,8 +89,14 @@ public class TestAudioFingerprinting
var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3");
var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3");
+ var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
+ var rhsFingerprint = Chromaprint.Fingerprint(rhsEpisode);
- var (lhs, rhs) = task.CompareEpisodes(lhsEpisode, rhsEpisode);
+ var (lhs, rhs) = task.CompareEpisodes(
+ lhsEpisode.EpisodeId,
+ lhsFingerprint,
+ rhsEpisode.EpisodeId,
+ rhsFingerprint);
Assert.True(lhs.Valid);
Assert.Equal(0, lhs.IntroStart);
@@ -111,10 +117,11 @@ public class TestAudioFingerprinting
}
}
-public class FactSkipFFmpegTests : FactAttribute {
- #if SKIP_FFMPEG_TESTS
+public class FactSkipFFmpegTests : FactAttribute
+{
+#if SKIP_FFMPEG_TESTS
public FactSkipFFmpegTests() {
Skip = "SKIP_FFMPEG_TESTS defined, skipping unit tests that require FFmpeg to be installed";
}
- #endif
+#endif
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index d66cae7..941f8b0 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -36,22 +36,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
private readonly ILibraryManager? _libraryManager;
- ///
- /// Lock which guards the fingerprint cache dictionary.
- ///
- private readonly object _fingerprintCacheLock = new object();
-
///
/// Lock which guards the shared dictionary of intros.
///
private readonly object _introsLock = new object();
- ///
- /// Temporary fingerprint cache to speed up reanalysis.
- /// Fingerprints are removed from this after a season is analyzed.
- ///
- private Dictionary _fingerprintCache;
-
///
/// Statistics for the currently running analysis task.
///
@@ -81,8 +70,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
_logger = loggerFactory.CreateLogger();
_queueLogger = loggerFactory.CreateLogger();
- _fingerprintCache = new Dictionary();
-
EdlManager.Initialize(_logger);
}
@@ -182,15 +169,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
ex);
}
- // Clear this season's episodes from the temporary fingerprint cache.
- lock (_fingerprintCacheLock)
- {
- foreach (var ep in season.Value)
- {
- _fingerprintCache.Remove(ep.EpisodeId);
- }
- }
-
if (writeEdl && Plugin.Instance!.Configuration.EdlAction != EdlAction.None)
{
EdlManager.UpdateEDLFiles(season.Value.AsReadOnly());
@@ -255,6 +233,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
CancellationToken cancellationToken)
{
var seasonIntros = new Dictionary();
+ var fingerprintCache = new Dictionary();
/* Don't analyze specials or seasons with an insufficient number of episodes.
* A season with only 1 episode can't be analyzed as it would compare the episode to itself,
@@ -273,7 +252,23 @@ public class AnalyzeEpisodesTask : IScheduledTask
first.SeriesName,
first.SeasonNumber);
- // TODO: cache fingerprints and inverted indexes
+ // Cache all fingerprints
+ foreach (var episode in episodes)
+ {
+ try
+ {
+ fingerprintCache[episode.EpisodeId] = Chromaprint.Fingerprint(episode);
+ }
+ catch (FingerprintException ex)
+ {
+ _logger.LogWarning("Caught fingerprint error: {Ex}", ex);
+
+ // fallback to an empty fingerprint
+ fingerprintCache[episode.EpisodeId] = Array.Empty();
+ }
+ }
+
+ // TODO: cache inverted indexes
// TODO: implementing bucketing
@@ -293,16 +288,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
Intro outerIntro;
Intro innerIntro;
- try
- {
- (outerIntro, innerIntro) = CompareEpisodes(outer, inner);
- }
- catch (FingerprintException ex)
- {
- // TODO: remove the episode that threw the error from additional processing
- _logger.LogWarning("Caught fingerprint error: {Ex}", ex);
- continue;
- }
+ (outerIntro, innerIntro) = CompareEpisodes(
+ outer.EpisodeId,
+ fingerprintCache[outer.EpisodeId],
+ inner.EpisodeId,
+ fingerprintCache[inner.EpisodeId]);
if (!outerIntro.Valid)
{
@@ -349,34 +339,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
#pragma warning restore CA1002
- ///
- /// Analyze two episodes to find an introduction sequence shared between them.
- ///
- /// First episode to analyze.
- /// Second episode to analyze.
- /// Intros for the first and second episodes.
- public (Intro Lhs, Intro Rhs) CompareEpisodes(QueuedEpisode lhsEpisode, QueuedEpisode rhsEpisode)
- {
- var start = DateTime.Now;
- var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
- var rhsFingerprint = Chromaprint.Fingerprint(rhsEpisode);
- analysisStatistics.FingerprintCPUTime.AddDuration(start);
-
- // Cache the fingerprints for quicker recall in the second pass (if one is needed).
- lock (_fingerprintCacheLock)
- {
- _fingerprintCache[lhsEpisode.EpisodeId] = lhsFingerprint;
- _fingerprintCache[rhsEpisode.EpisodeId] = rhsFingerprint;
- }
-
- return CompareEpisodes(
- lhsEpisode.EpisodeId,
- lhsFingerprint,
- rhsEpisode.EpisodeId,
- rhsFingerprint,
- true);
- }
-
///
/// Analyze two episodes to find an introduction sequence shared between them.
///
@@ -384,17 +346,14 @@ public class AnalyzeEpisodesTask : IScheduledTask
/// First episode fingerprint points.
/// Second episode id.
/// Second episode fingerprint points.
- /// If this was called as part of the first analysis pass, add the elapsed time to the statistics.
/// Intros for the first and second episodes.
public (Intro Lhs, Intro Rhs) CompareEpisodes(
Guid lhsId,
uint[] lhsPoints,
Guid rhsId,
- uint[] rhsPoints,
- bool isFirstPass)
+ uint[] rhsPoints)
{
- // If this isn't running as part of the first analysis pass, don't count this CPU time as first pass time.
- var start = isFirstPass ? DateTime.Now : DateTime.MinValue;
+ var start = DateTime.Now;
// Creates an inverted fingerprint point index for both episodes.
// For every point which is a 100% match, search for an introduction at that point.
From f92eea20b30d0b456f9c38b1addfa53c32a1b142 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Fri, 26 Aug 2022 00:50:45 -0500
Subject: [PATCH 05/29] Cache inverted indexes
---
.../TestAudioFingerprinting.cs | 4 +++-
ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs | 12 +++++++++++-
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 12 +++++++-----
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
index a67e858..140ebd7 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
@@ -2,6 +2,7 @@
* which supports both chromaprint and the "-fp_format raw" flag.
*/
+using System;
using System.Collections.Generic;
using Xunit;
using Microsoft.Extensions.Logging;
@@ -77,7 +78,7 @@ public class TestAudioFingerprinting
{77, 5},
};
- var actual = Chromaprint.CreateInvertedIndex(fpr);
+ var actual = Chromaprint.CreateInvertedIndex(Guid.NewGuid(), fpr);
Assert.Equal(expected, actual);
}
@@ -111,6 +112,7 @@ public class TestAudioFingerprinting
{
return new QueuedEpisode()
{
+ EpisodeId = Guid.NewGuid(),
Path = "../../../" + path,
FingerprintDuration = 60
};
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs
index 68fd809..ffe3602 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs
@@ -20,6 +20,8 @@ public static class Chromaprint
private static Dictionary ChromaprintLogs { get; set; } = new();
+ private static Dictionary> InvertedIndexCache { get; set; } = new();
+
///
/// Check that the installed version of ffmpeg supports chromaprint.
///
@@ -124,10 +126,16 @@ public static class Chromaprint
///
/// Transforms a Chromaprint into an inverted index of fingerprint points to the last index it appeared at.
///
+ /// Episode ID.
/// Chromaprint fingerprint.
/// Inverted index.
- public static Dictionary CreateInvertedIndex(uint[] fingerprint)
+ public static Dictionary CreateInvertedIndex(Guid id, uint[] fingerprint)
{
+ if (InvertedIndexCache.TryGetValue(id, out var cached))
+ {
+ return cached;
+ }
+
var invIndex = new Dictionary();
for (int i = 0; i < fingerprint.Length; i++)
@@ -139,6 +147,8 @@ public static class Chromaprint
invIndex[point] = i;
}
+ InvertedIndexCache[id] = invIndex;
+
return invIndex;
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index 941f8b0..e2a0f78 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -268,8 +268,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
}
}
- // TODO: cache inverted indexes
-
// TODO: implementing bucketing
// For all episodes
@@ -357,7 +355,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
// Creates an inverted fingerprint point index for both episodes.
// For every point which is a 100% match, search for an introduction at that point.
- var (lhsRanges, rhsRanges) = SearchInvertedIndex(lhsPoints, rhsPoints);
+ var (lhsRanges, rhsRanges) = SearchInvertedIndex(lhsId, lhsPoints, rhsId, rhsPoints);
if (lhsRanges.Count > 0)
{
@@ -417,19 +415,23 @@ public class AnalyzeEpisodesTask : IScheduledTask
///
/// Search for a shared introduction sequence using inverted indexes.
///
+ /// LHS ID.
/// Left episode fingerprint points.
+ /// RHS ID.
/// Right episode fingerprint points.
/// List of shared TimeRanges between the left and right episodes.
private (List Lhs, List Rhs) SearchInvertedIndex(
+ Guid lhsId,
uint[] lhsPoints,
+ Guid rhsId,
uint[] rhsPoints)
{
var lhsRanges = new List();
var rhsRanges = new List();
// Generate inverted indexes for the left and right episodes.
- var lhsIndex = Chromaprint.CreateInvertedIndex(lhsPoints);
- var rhsIndex = Chromaprint.CreateInvertedIndex(rhsPoints);
+ var lhsIndex = Chromaprint.CreateInvertedIndex(lhsId, lhsPoints);
+ var rhsIndex = Chromaprint.CreateInvertedIndex(rhsId, rhsPoints);
var indexShifts = new HashSet();
// For all audio points in the left episode, check if the right episode has a point which matches exactly.
From 874f77f94c0cf029529ae6e26c75b0c9a9bf743a Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sat, 27 Aug 2022 01:58:46 -0500
Subject: [PATCH 06/29] Search episodes in O(n) instead of O(n^2) time
---
.../Data/SeasonHistogram.cs | 31 ------
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 101 +++++++++---------
2 files changed, 49 insertions(+), 83 deletions(-)
delete mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/Data/SeasonHistogram.cs
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/SeasonHistogram.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/SeasonHistogram.cs
deleted file mode 100644
index 4cd4a20..0000000
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/SeasonHistogram.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma warning disable CA1815 // Override equals and operator equals on value types
-
-using System;
-using System.Collections.ObjectModel;
-
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-///
-/// Histogram entry for episodes in a season.
-///
-public struct SeasonHistogram
-{
- ///
- /// Initializes a new instance of the struct.
- ///
- /// First episode seen with this duration.
- public SeasonHistogram(Guid firstEpisode)
- {
- Episodes.Add(firstEpisode);
- }
-
- ///
- /// Gets episodes with this duration.
- ///
- public Collection Episodes { get; } = new Collection();
-
- ///
- /// Gets the number of times an episode with an intro of this duration has been seen.
- ///
- public int Count => Episodes?.Count ?? 0;
-}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index e2a0f78..e97dee2 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
@@ -146,9 +147,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
try
{
+ var episodes = new Collection(season.Value);
+
// Increment totalProcessed by the number of episodes in this season that were actually analyzed
// (instead of just using the number of episodes in the current season).
- var analyzed = AnalyzeSeason(season.Value, cancellationToken);
+ var analyzed = AnalyzeSeason(episodes, cancellationToken);
Interlocked.Add(ref totalProcessed, analyzed);
writeEdl = analyzed > 0 || Plugin.Instance!.Configuration.RegenerateEdlFiles;
}
@@ -219,9 +222,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
return previous;
}
- // TODO: restore warning
-#pragma warning disable CA1002
-
///
/// Fingerprints all episodes in the provided season and stores the timestamps of all introductions.
///
@@ -229,19 +229,26 @@ public class AnalyzeEpisodesTask : IScheduledTask
/// Cancellation token provided by the scheduled task.
/// Number of episodes from the provided season that were analyzed.
private int AnalyzeSeason(
- List episodes,
+ Collection episodes,
CancellationToken cancellationToken)
{
+ // All intros for this season.
var seasonIntros = new Dictionary();
+
+ // Cache of all fingerprints for this season.
var fingerprintCache = new Dictionary();
+ // Total episodes in this season. Counted at the start of this function as episodes
+ // are popped from here during analysis.
+ var episodeCount = episodes.Count;
+
/* Don't analyze specials or seasons with an insufficient number of episodes.
* A season with only 1 episode can't be analyzed as it would compare the episode to itself,
* which would result in the entire episode being marked as an introduction, as the audio is identical.
*/
if (episodes.Count < 2 || episodes[0].SeasonNumber == 0)
{
- return episodes.Count;
+ return episodeCount;
}
var first = episodes[0];
@@ -252,7 +259,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
first.SeriesName,
first.SeasonNumber);
- // Cache all fingerprints
+ // Compute fingerprints for all episodes in the season
foreach (var episode in episodes)
{
try
@@ -263,60 +270,52 @@ public class AnalyzeEpisodesTask : IScheduledTask
{
_logger.LogWarning("Caught fingerprint error: {Ex}", ex);
- // fallback to an empty fingerprint
+ // Fallback to an empty fingerprint on any error
fingerprintCache[episode.EpisodeId] = Array.Empty();
}
}
- // TODO: implementing bucketing
+ /* Theory of operation:
+ * Episodes are analyzed in the same order that Jellyfin displays them in and are
+ * sorted into buckets based off of the intro sequence that the episode contains.
+ *
+ * Jellyfin's episode ordering is used because it is assumed that the introduction
+ * in each season of a show will likely either:
+ * - remain constant throughout the entire season
+ * - remain constant in subranges of the season (e.g. episodes 1 - 5 and 6 - 10 share intros)
+ * If the intros do not follow this pattern, the plugin should still find most
+ * of them.
+ */
- // For all episodes
- foreach (var outer in episodes)
+ // While there are still episodes in the queue
+ while (episodes.Count > 0)
{
- // Compare the outer episode to all other episodes
- foreach (var inner in episodes)
+ // Pop the first episode from the queue
+ var currentEpisode = episodes[0];
+ episodes.RemoveAt(0);
+
+ // Search through all remaining episodes.
+ foreach (var remainingEpisode in episodes)
{
- // Don't compare the episode to itself
- if (outer.EpisodeId == inner.EpisodeId)
+ // Compare the current episode to all remaining episodes in the queue.
+ var (currentIntro, remainingIntro) = CompareEpisodes(
+ currentEpisode.EpisodeId,
+ fingerprintCache[currentEpisode.EpisodeId],
+ remainingEpisode.EpisodeId,
+ fingerprintCache[remainingEpisode.EpisodeId]);
+
+ // If we found an intro, save it.
+ if (currentIntro.Valid)
{
- continue;
- }
+ seasonIntros[currentIntro.EpisodeId] = currentIntro;
+ seasonIntros[remainingIntro.EpisodeId] = remainingIntro;
- // Fingerprint both episodes
- Intro outerIntro;
- Intro innerIntro;
-
- (outerIntro, innerIntro) = CompareEpisodes(
- outer.EpisodeId,
- fingerprintCache[outer.EpisodeId],
- inner.EpisodeId,
- fingerprintCache[inner.EpisodeId]);
-
- if (!outerIntro.Valid)
- {
- continue;
- }
-
- // Save this intro if:
- // - it is the first one we've seen for this episode
- // - OR it is longer than the previous one
- if (
- !seasonIntros.TryGetValue(outer.EpisodeId, out var currentOuterIntro) ||
- outerIntro.Duration > currentOuterIntro.Duration)
- {
- seasonIntros[outer.EpisodeId] = outerIntro;
- }
-
- if (
- !seasonIntros.TryGetValue(inner.EpisodeId, out var currentInnerIntro) ||
- innerIntro.Duration > currentInnerIntro.Duration)
- {
- seasonIntros[inner.EpisodeId] = innerIntro;
+ break;
}
}
- }
- // TODO: analysisStatistics.TotalAnalyzedEpisodes.Add(2);
+ // If no intro is found at this point, the popped episode is not reinserted into the queue.
+ }
// Ensure only one thread at a time can update the shared intro dictionary.
lock (_introsLock)
@@ -332,11 +331,9 @@ public class AnalyzeEpisodesTask : IScheduledTask
Plugin.Instance!.SaveTimestamps();
}
- return episodes.Count;
+ return episodeCount;
}
-#pragma warning restore CA1002
-
///
/// Analyze two episodes to find an introduction sequence shared between them.
///
From bbed34b0e7dc7c7afc0a0bd72c77460adee3f940 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sun, 28 Aug 2022 22:25:27 -0500
Subject: [PATCH 07/29] Add inverted index shift amount
---
.../Configuration/PluginConfiguration.cs | 6 ++
.../Controllers/SkipIntroController.cs | 1 +
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 58 ++++++++++++-------
3 files changed, 43 insertions(+), 22 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
index db86ce3..741c561 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
@@ -78,4 +78,10 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets the seconds after the intro starts to hide the skip prompt at.
///
public int HidePromptAdjustment { get; set; } = 10;
+
+ ///
+ /// Gets or sets the amount of intro to play (in seconds).
+ /// TODO: rename.
+ ///
+ public int AmountOfIntroToPlay { get; set; } = 5;
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs
index d4d07b7..4fcb8c6 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs
@@ -44,6 +44,7 @@ public class SkipIntroController : ControllerBase
var config = Plugin.Instance!.Configuration;
intro.ShowSkipPromptAt = Math.Max(0, intro.IntroStart - config.ShowPromptAdjustment);
intro.HideSkipPromptAt = intro.IntroStart + config.HidePromptAdjustment;
+ intro.IntroEnd -= config.AmountOfIntroToPlay;
return intro;
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index e97dee2..f8d71be 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -26,6 +26,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
///
private const double MaximumDistance = 3.5;
+ ///
+ /// Amount to shift inverted index offsets by.
+ ///
+ private const int InvertedIndexShift = 2;
+
///
/// Seconds of audio in one fingerprint point. This value is defined by the Chromaprint library and should not be changed.
///
@@ -275,18 +280,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
}
}
- /* Theory of operation:
- * Episodes are analyzed in the same order that Jellyfin displays them in and are
- * sorted into buckets based off of the intro sequence that the episode contains.
- *
- * Jellyfin's episode ordering is used because it is assumed that the introduction
- * in each season of a show will likely either:
- * - remain constant throughout the entire season
- * - remain constant in subranges of the season (e.g. episodes 1 - 5 and 6 - 10 share intros)
- * If the intros do not follow this pattern, the plugin should still find most
- * of them.
- */
-
// While there are still episodes in the queue
while (episodes.Count > 0)
{
@@ -304,14 +297,30 @@ public class AnalyzeEpisodesTask : IScheduledTask
remainingEpisode.EpisodeId,
fingerprintCache[remainingEpisode.EpisodeId]);
- // If we found an intro, save it.
- if (currentIntro.Valid)
+ // If one of the intros isn't valid, ignore this comparison result.
+ if (!currentIntro.Valid)
+ {
+ continue;
+ }
+
+ // Only save the discovered intro if it is:
+ // - the first intro discovered for this episode
+ // - longer than the previously discovered intro
+ if (
+ !seasonIntros.TryGetValue(currentIntro.EpisodeId, out var savedCurrentIntro) ||
+ currentIntro.Duration > savedCurrentIntro.Duration)
{
seasonIntros[currentIntro.EpisodeId] = currentIntro;
- seasonIntros[remainingIntro.EpisodeId] = remainingIntro;
-
- break;
}
+
+ if (
+ !seasonIntros.TryGetValue(remainingIntro.EpisodeId, out var savedRemainingIntro) ||
+ remainingIntro.Duration > savedRemainingIntro.Duration)
+ {
+ seasonIntros[remainingIntro.EpisodeId] = remainingIntro;
+ }
+
+ break;
}
// If no intro is found at this point, the popped episode is not reinserted into the queue.
@@ -435,13 +444,18 @@ public class AnalyzeEpisodesTask : IScheduledTask
// If an exact match is found, calculate the shift that must be used to align the points.
foreach (var kvp in lhsIndex)
{
- var point = kvp.Key;
+ var originalPoint = kvp.Key;
- if (rhsIndex.ContainsKey(point))
+ for (var i = -1 * InvertedIndexShift; i <= InvertedIndexShift; i++)
{
- var lhsFirst = (int)lhsIndex[point];
- var rhsFirst = (int)rhsIndex[point];
- indexShifts.Add(rhsFirst - lhsFirst);
+ var modifiedPoint = (uint)(originalPoint + i);
+
+ if (rhsIndex.ContainsKey(modifiedPoint))
+ {
+ var lhsFirst = (int)lhsIndex[originalPoint];
+ var rhsFirst = (int)rhsIndex[modifiedPoint];
+ indexShifts.Add(rhsFirst - lhsFirst);
+ }
}
}
From fb6cd5c1d7a11295a2bb822f0f02d6d9331154ce Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sun, 28 Aug 2022 22:35:43 -0500
Subject: [PATCH 08/29] Rename {Chromaprint,FFmpegWrapper}.cs
---
.../TestAudioFingerprinting.cs | 10 +++++-----
.../Controllers/TroubleshootingController.cs | 2 +-
.../Controllers/VisualizationController.cs | 2 +-
ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs | 2 +-
.../{Chromaprint.cs => FFmpegWrapper.cs} | 4 ++--
ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs | 2 +-
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 6 +++---
7 files changed, 14 insertions(+), 14 deletions(-)
rename ConfusedPolarBear.Plugin.IntroSkipper/{Chromaprint.cs => FFmpegWrapper.cs} (99%)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
index 140ebd7..a940067 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
@@ -14,7 +14,7 @@ public class TestAudioFingerprinting
[FactSkipFFmpegTests]
public void TestInstallationCheck()
{
- Assert.True(Chromaprint.CheckFFmpegVersion());
+ Assert.True(FFmpegWrapper.CheckFFmpegVersion());
}
[Theory]
@@ -58,7 +58,7 @@ public class TestAudioFingerprinting
3472417825, 3395841056, 3458735136, 3341420624, 1076496560, 1076501168, 1076501136, 1076497024
};
- var actual = Chromaprint.Fingerprint(queueEpisode("audio/big_buck_bunny_intro.mp3"));
+ var actual = FFmpegWrapper.Fingerprint(queueEpisode("audio/big_buck_bunny_intro.mp3"));
Assert.Equal(expected, actual);
}
@@ -78,7 +78,7 @@ public class TestAudioFingerprinting
{77, 5},
};
- var actual = Chromaprint.CreateInvertedIndex(Guid.NewGuid(), fpr);
+ var actual = FFmpegWrapper.CreateInvertedIndex(Guid.NewGuid(), fpr);
Assert.Equal(expected, actual);
}
@@ -90,8 +90,8 @@ public class TestAudioFingerprinting
var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3");
var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3");
- var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
- var rhsFingerprint = Chromaprint.Fingerprint(rhsEpisode);
+ var lhsFingerprint = FFmpegWrapper.Fingerprint(lhsEpisode);
+ var rhsFingerprint = FFmpegWrapper.Fingerprint(rhsEpisode);
var (lhs, rhs) = task.CompareEpisodes(
lhsEpisode.EpisodeId,
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/TroubleshootingController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/TroubleshootingController.cs
index 3554feb..8923daf 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/TroubleshootingController.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/TroubleshootingController.cs
@@ -58,7 +58,7 @@ public class TroubleshootingController : ControllerBase
bundle.Append(" seasons");
bundle.Append('\n');
- bundle.Append(Chromaprint.GetChromaprintLogs());
+ bundle.Append(FFmpegWrapper.GetChromaprintLogs());
return bundle.ToString();
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
index 6d4d042..5cf7bc1 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
@@ -113,7 +113,7 @@ public class VisualizationController : ControllerBase
{
if (needle.EpisodeId == id)
{
- return Chromaprint.Fingerprint(needle);
+ return FFmpegWrapper.Fingerprint(needle);
}
}
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs
index 5113ef4..6a5dff2 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs
@@ -46,7 +46,7 @@ public class Entrypoint : IServerEntryPoint
/// Task.
public Task RunAsync()
{
- Chromaprint.Logger = _logger;
+ FFmpegWrapper.Logger = _logger;
#if DEBUG
LogVersion();
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs
similarity index 99%
rename from ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs
rename to ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs
index ffe3602..bda56ef 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Chromaprint.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs
@@ -9,9 +9,9 @@ using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper;
///
-/// Wrapper for libchromaprint.
+/// Wrapper for libchromaprint and the silencedetect filter.
///
-public static class Chromaprint
+public static class FFmpegWrapper
{
///
/// Gets or sets the logger.
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
index 6f07285..185cc60 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
@@ -40,7 +40,7 @@ public class QueueManager
public void EnqueueAllEpisodes()
{
// Assert that ffmpeg with chromaprint is installed
- if (!Chromaprint.CheckFFmpegVersion())
+ if (!FFmpegWrapper.CheckFFmpegVersion())
{
throw new FingerprintException(
"ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed. If Jellyfin is running natively, install jellyfin-ffmpeg5. If Jellyfin is running in a container, upgrade it to the latest version of 10.8.0.");
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index f8d71be..42acf8e 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -269,7 +269,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
{
try
{
- fingerprintCache[episode.EpisodeId] = Chromaprint.Fingerprint(episode);
+ fingerprintCache[episode.EpisodeId] = FFmpegWrapper.Fingerprint(episode);
}
catch (FingerprintException ex)
{
@@ -436,8 +436,8 @@ public class AnalyzeEpisodesTask : IScheduledTask
var rhsRanges = new List();
// Generate inverted indexes for the left and right episodes.
- var lhsIndex = Chromaprint.CreateInvertedIndex(lhsId, lhsPoints);
- var rhsIndex = Chromaprint.CreateInvertedIndex(rhsId, rhsPoints);
+ var lhsIndex = FFmpegWrapper.CreateInvertedIndex(lhsId, lhsPoints);
+ var rhsIndex = FFmpegWrapper.CreateInvertedIndex(rhsId, rhsPoints);
var indexShifts = new HashSet();
// For all audio points in the left episode, check if the right episode has a point which matches exactly.
From cbd00b2675aa7667c7575e390febf7b2048d7ef7 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Mon, 29 Aug 2022 23:56:13 -0500
Subject: [PATCH 09/29] Implement silence detection
---
.../TestAudioFingerprinting.cs | 23 +++
.../TestContiguous.cs | 18 +++
.../AutoSkip.cs | 4 +-
.../Configuration/PluginConfiguration.cs | 13 +-
.../Data/TimeRange.cs | 12 ++
.../FFmpegWrapper.cs | 136 +++++++++++++++---
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 91 +++++++++++-
7 files changed, 274 insertions(+), 23 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
index a940067..c0da162 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
@@ -108,6 +108,29 @@ public class TestAudioFingerprinting
Assert.Equal(22.912, rhs.IntroEnd);
}
+ ///
+ /// Test that the silencedetect wrapper is working.
+ ///
+ [FactSkipFFmpegTests]
+ public void TestSilenceDetection()
+ {
+ var clip = queueEpisode("audio/big_buck_bunny_clip.mp3");
+
+ var expected = new TimeRange[]
+ {
+ new TimeRange(44.6310, 44.8072),
+ new TimeRange(53.5905, 53.8070),
+ new TimeRange(53.8458, 54.2024),
+ new TimeRange(54.2611, 54.5935),
+ new TimeRange(54.7098, 54.9293),
+ new TimeRange(54.9294, 55.2590),
+ };
+
+ var actual = FFmpegWrapper.DetectSilence(clip, 60);
+
+ Assert.Equal(expected, actual);
+ }
+
private QueuedEpisode queueEpisode(string path)
{
return new QueuedEpisode()
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestContiguous.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestContiguous.cs
index 286aa0c..50121f6 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestContiguous.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestContiguous.cs
@@ -71,4 +71,22 @@ public class TestTimeRanges
Assert.Equal(expected, actual);
}
+
+ ///
+ /// Tests that TimeRange intersections are detected correctly.
+ /// Tests each time range against a range of 5 to 10 seconds.
+ ///
+ [Theory]
+ [InlineData(1, 4, false)] // too early
+ [InlineData(4, 6, true)] // intersects on the left
+ [InlineData(7, 8, true)] // in the middle
+ [InlineData(9, 12, true)] // intersects on the right
+ [InlineData(13, 15, false)] // too late
+ public void TestTimeRangeIntersection(int start, int end, bool expected)
+ {
+ var large = new TimeRange(5, 10);
+ var testRange = new TimeRange(start, end);
+
+ Assert.Equal(expected, large.Intersects(testRange));
+ }
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/AutoSkip.cs b/ConfusedPolarBear.Plugin.IntroSkipper/AutoSkip.cs
index 3846bb9..1cac686 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/AutoSkip.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/AutoSkip.cs
@@ -169,6 +169,8 @@ public class AutoSkip : IServerEntryPoint
// Send the seek command
_logger.LogDebug("Sending seek command to {Session}", deviceId);
+ var introEnd = (long)intro.IntroEnd - Plugin.Instance!.Configuration.AmountOfIntroToPlay;
+
_sessionManager.SendPlaystateCommand(
session.Id,
session.Id,
@@ -176,7 +178,7 @@ public class AutoSkip : IServerEntryPoint
{
Command = PlaystateCommand.Seek,
ControllingUserId = session.UserId.ToString("N"),
- SeekPositionTicks = (long)intro.IntroEnd * TimeSpan.TicksPerSecond,
+ SeekPositionTicks = introEnd * TimeSpan.TicksPerSecond,
},
CancellationToken.None);
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
index 741c561..d98554c 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
@@ -62,6 +62,17 @@ public class PluginConfiguration : BasePluginConfiguration
///
public int MinimumIntroDuration { get; set; } = 15;
+ ///
+ /// Gets or sets the maximum amount of noise (in dB) that is considered silent.
+ /// Lowering this number will increase the filter's sensitivity to noise.
+ ///
+ public int SilenceDetectionMaximumNoise { get; set; } = -50;
+
+ ///
+ /// Gets or sets the minimum duration of audio (in seconds) that is considered silent.
+ ///
+ public double SilenceDetectionMinimumDuration { get; set; } = 0.50;
+
// ===== Playback settings =====
///
@@ -83,5 +94,5 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets the amount of intro to play (in seconds).
/// TODO: rename.
///
- public int AmountOfIntroToPlay { get; set; } = 5;
+ public int AmountOfIntroToPlay { get; set; } = 2;
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/TimeRange.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/TimeRange.cs
index 7e7b51b..451df2f 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/TimeRange.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/TimeRange.cs
@@ -69,6 +69,18 @@ public class TimeRange : IComparable
return tr.Duration.CompareTo(Duration);
}
+
+ ///
+ /// Tests if this TimeRange object intersects the provided TimeRange.
+ ///
+ /// Second TimeRange object to test.
+ /// true if tr intersects the current TimeRange, false otherwise.
+ public bool Intersects(TimeRange tr)
+ {
+ return
+ (Start < tr.Start && tr.Start < End) ||
+ (Start < tr.End && tr.End < End);
+ }
}
#pragma warning restore CA1036
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs
index bda56ef..7852b97 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper;
@@ -13,6 +14,16 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
///
public static class FFmpegWrapper
{
+ // FFmpeg logs lines similar to the following:
+ // [silencedetect @ 0x000000000000] silence_start: 12.34
+ // [silencedetect @ 0x000000000000] silence_end: 56.123 | silence_duration: 43.783
+
+ ///
+ /// Used with FFmpeg's silencedetect filter to extract the start and end times of silence.
+ ///
+ private static readonly Regex SilenceDetectionExpression = new(
+ "silence_(?start|end): (?
+
+
+ Silence detection options
+
+
+
+
+
+ Noise tolerance in negative decibels.
+
+
+
+
+
+
+
+ Minimum silence duration in seconds before adjusting introduction end time.
+
+
+
@@ -201,12 +237,6 @@
-
-
- Erasing introduction timestamps is only necessary after upgrading the plugin if specifically
- requested to do so in the plugin's changelog. After the timestamps are erased, run the
- Analyze episodes scheduled task to re-analyze all media on the server.
-
@@ -337,14 +367,20 @@
// all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
var configurationFields = [
+ // analysis
"MaxParallelism",
"SelectedLibraries",
"AnalysisPercent",
"AnalysisLengthLimit",
"MinimumIntroDuration",
"EdlAction",
+ // playback
"ShowPromptAdjustment",
- "HidePromptAdjustment"
+ "HidePromptAdjustment",
+ "SecondsOfIntroToPlay",
+ // internals
+ "SilenceDetectionMaximumNoise",
+ "SilenceDetectionMinimumDuration",
]
// visualizer elements
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index 5edc7a9..fcd3e89 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -15,22 +15,6 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
///
public class AnalyzeEpisodesTask : IScheduledTask
{
- ///
- /// Maximum number of bits (out of 32 total) that can be different between segments before they are considered dissimilar.
- /// 6 bits means the audio must be at least 81% similar (1 - 6 / 32).
- ///
- private const double MaximumDifferences = 6;
-
- ///
- /// Maximum time (in seconds) permitted between timestamps before they are considered non-contiguous.
- ///
- private const double MaximumDistance = 3.5;
-
- ///
- /// Amount to shift inverted index offsets by.
- ///
- private const int InvertedIndexShift = 2;
-
///
/// Seconds of audio in one fingerprint point. This value is defined by the Chromaprint library and should not be changed.
///
@@ -57,6 +41,14 @@ public class AnalyzeEpisodesTask : IScheduledTask
///
private static int minimumIntroDuration = 15;
+ private static int maximumDifferences = 6;
+
+ private static int invertedIndexShift = 2;
+
+ private static double maximumTimeSkip = 3.5;
+
+ private static double silenceDetectionMinimumDuration = 0.33;
+
///
/// Initializes a new instance of the class.
///
@@ -124,6 +116,13 @@ public class AnalyzeEpisodesTask : IScheduledTask
"No episodes to analyze. If you are limiting the list of libraries to analyze, check that all library names have been spelled correctly.");
}
+ // Load analysis settings from configuration
+ var config = Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration();
+ maximumDifferences = config.MaximumFingerprintPointDifferences;
+ invertedIndexShift = config.InvertedIndexShift;
+ maximumTimeSkip = config.MaximumTimeSkip;
+ silenceDetectionMinimumDuration = config.SilenceDetectionMinimumDuration;
+
// Log EDL settings
EdlManager.LogConfiguration();
@@ -448,7 +447,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
{
var originalPoint = kvp.Key;
- for (var i = -1 * InvertedIndexShift; i <= InvertedIndexShift; i++)
+ for (var i = -1 * invertedIndexShift; i <= invertedIndexShift; i++)
{
var modifiedPoint = (uint)(originalPoint + i);
@@ -542,7 +541,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
var diff = lhs[lhsPosition] ^ rhs[rhsPosition];
// If the difference between the samples is small, flag both times as similar.
- if (CountBits(diff) > MaximumDifferences)
+ if (CountBits(diff) > maximumDifferences)
{
continue;
}
@@ -559,25 +558,25 @@ public class AnalyzeEpisodesTask : IScheduledTask
rhsTimes.Add(double.MaxValue);
// Now that both fingerprints have been compared at this shift, see if there's a contiguous time range.
- var lContiguous = TimeRangeHelpers.FindContiguous(lhsTimes.ToArray(), MaximumDistance);
+ var lContiguous = TimeRangeHelpers.FindContiguous(lhsTimes.ToArray(), maximumTimeSkip);
if (lContiguous is null || lContiguous.Duration < minimumIntroDuration)
{
return (new TimeRange(), new TimeRange());
}
// Since LHS had a contiguous time range, RHS must have one also.
- var rContiguous = TimeRangeHelpers.FindContiguous(rhsTimes.ToArray(), MaximumDistance)!;
+ var rContiguous = TimeRangeHelpers.FindContiguous(rhsTimes.ToArray(), maximumTimeSkip)!;
// Tweak the end timestamps just a bit to ensure as little content as possible is skipped over.
if (lContiguous.Duration >= 90)
{
- lContiguous.End -= 2 * MaximumDistance;
- rContiguous.End -= 2 * MaximumDistance;
+ lContiguous.End -= 2 * maximumTimeSkip;
+ rContiguous.End -= 2 * maximumTimeSkip;
}
else if (lContiguous.Duration >= 30)
{
- lContiguous.End -= MaximumDistance;
- rContiguous.End -= MaximumDistance;
+ lContiguous.End -= maximumTimeSkip;
+ rContiguous.End -= maximumTimeSkip;
}
return (lContiguous, rContiguous);
@@ -612,9 +611,8 @@ public class AnalyzeEpisodesTask : IScheduledTask
continue;
}
- // Since we only want to adjust the end timestamp of the intro, create a new TimeRange
- // that covers the last few seconds.
- var originalIntroEnd = new TimeRange(originalIntro.IntroEnd - 10, originalIntro.IntroEnd);
+ // Only adjust the end timestamp of the intro
+ var originalIntroEnd = new TimeRange(originalIntro.IntroEnd - 15, originalIntro.IntroEnd);
_logger.LogTrace(
"{Name} original intro: {Start} - {End}",
@@ -636,8 +634,10 @@ public class AnalyzeEpisodesTask : IScheduledTask
// Ignore any silence that:
// * doesn't intersect the ending of the intro, or
- // * is less than half a second long
- if (!originalIntroEnd.Intersects(currentRange) || currentRange.Duration < 0.5)
+ // * is shorter than the user defined minimum duration
+ if (
+ !originalIntroEnd.Intersects(currentRange) ||
+ currentRange.Duration < silenceDetectionMinimumDuration)
{
continue;
}
From 7612e6cdaf0cdec71b2286a37e58b13872c0a5ba Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Fri, 2 Sep 2022 23:42:32 -0500
Subject: [PATCH 19/29] Check cancellation token
---
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index fcd3e89..f3aef75 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -126,8 +126,7 @@ public class AnalyzeEpisodesTask : IScheduledTask
// Log EDL settings
EdlManager.LogConfiguration();
- // Include the previously processed episodes in the percentage reported to the UI.
- var totalProcessed = CountProcessedEpisodes();
+ var totalProcessed = 0;
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism
@@ -151,6 +150,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
try
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
var episodes = new ReadOnlyCollection(season.Value);
// Increment totalProcessed by the number of episodes in this season that were actually analyzed
@@ -268,6 +272,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
try
{
fingerprintCache[episode.EpisodeId] = FFmpegWrapper.Fingerprint(episode);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return episodes.Count;
+ }
}
catch (FingerprintException ex)
{
@@ -324,6 +333,11 @@ public class AnalyzeEpisodesTask : IScheduledTask
// If no intro is found at this point, the popped episode is not reinserted into the queue.
}
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return episodes.Count;
+ }
+
// Adjust all introduction end times so that they end at silence.
seasonIntros = AdjustIntroEndTimes(episodes, seasonIntros);
From c937e28d458b085f56a950e180c3d34d523e7ed1 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sat, 3 Sep 2022 00:39:35 -0500
Subject: [PATCH 20/29] Add maximum introduction duration
---
.../Configuration/PluginConfiguration.cs | 5 +++++
.../Configuration/configPage.html | 12 ++++++++++++
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 8 ++++++--
3 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
index 46d3652..837370a 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
@@ -62,6 +62,11 @@ public class PluginConfiguration : BasePluginConfiguration
///
public int MinimumIntroDuration { get; set; } = 15;
+ ///
+ /// Gets or sets the maximum length of similar audio that will be considered an introduction.
+ ///
+ public int MaximumIntroDuration { get; set; } = 120;
+
// ===== Playback settings =====
///
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
index 99981cf..4f51047 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
@@ -139,6 +139,17 @@
+
+
+
+
+ Similar sounding audio which is longer than this duration will not be considered an
+ introduction.
+
+
+
The amount of each episode's audio that will be analyzed is determined using both
the percentage of audio and maximum runtime of audio to analyze. The minimum of
@@ -373,6 +384,7 @@
"AnalysisPercent",
"AnalysisLengthLimit",
"MinimumIntroDuration",
+ "MaximumIntroDuration",
"EdlAction",
// playback
"ShowPromptAdjustment",
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index f3aef75..328e7b8 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -304,8 +304,12 @@ public class AnalyzeEpisodesTask : IScheduledTask
remainingEpisode.EpisodeId,
fingerprintCache[remainingEpisode.EpisodeId]);
- // If one of the intros isn't valid, ignore this comparison result.
- if (!currentIntro.Valid)
+ // Ignore this comparison result if:
+ // - one of the intros isn't valid, or
+ // - the introduction exceeds the configured limit
+ if (
+ !remainingIntro.Valid ||
+ remainingIntro.Duration > Plugin.Instance!.Configuration.MaximumIntroDuration)
{
continue;
}
From 9b837cbbd071ef21b18ec22670fc51384335ccdd Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sat, 3 Sep 2022 01:28:35 -0500
Subject: [PATCH 21/29] Update changelog and readme
---
CHANGELOG.md | 8 +++++++-
README.md | 4 ++--
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba258e9..4cc462e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,18 @@
# Changelog
-## v0.1.7.0
+## v0.1.7.0 (no eta)
* New features
+ * Rewrote fingerprint comparison algorithm to be faster (~30x speedup) and detect more introductions
* Add support for the `silencedetect` filter in ffmpeg
* Add support bundle
+ * Add maximum introduction duration
+ * Support playing a few seconds from the end of the introduction to verify that no episode content was skipped over
+ * Amount played is customizable and defaults to 2 seconds
+ * Support modifying introduction detection algorithm settings
* Fixes
* Fix scheduled task interval (#79)
+ * Prevent show names from becoming duplicated in the show name dropdown under the advanced section
## v0.1.6.0 (2022-08-04)
* New features
diff --git a/README.md b/README.md
index 4fe29ac..9dc5b78 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,9 @@ Plugin versions v0.1.0 and older require `fpcalc` to be installed.
Show introductions will only be detected if they are:
* Located within the first 25% of an episode, or the first 10 minutes, whichever is smaller
-* At least 15 seconds long
+* Between 15 seconds and 2 minutes long
-Both of these requirements can be customized as needed.
+All of these requirements can be customized as needed.
## Step 1: Optional: use the modified web interface
While this plugin is fully compatible with an unmodified version of Jellyfin 10.8.0, using a modified web interface allows you to click a button to skip intros. If you skip this step and do not use the modified web interface, you will have to enable the "Automatically skip intros" option in the plugin settings.
From 85f21ae51c9182cbf78c41138ce2d2d585cf9da2 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sat, 3 Sep 2022 01:42:31 -0500
Subject: [PATCH 22/29] Check silence start time before adjusting intro
---
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index 328e7b8..c4f39c8 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -652,10 +652,12 @@ public class AnalyzeEpisodesTask : IScheduledTask
// Ignore any silence that:
// * doesn't intersect the ending of the intro, or
- // * is shorter than the user defined minimum duration
+ // * is shorter than the user defined minimum duration, or
+ // * starts before the introduction does
if (
!originalIntroEnd.Intersects(currentRange) ||
- currentRange.Duration < silenceDetectionMinimumDuration)
+ currentRange.Duration < silenceDetectionMinimumDuration ||
+ currentRange.Start < originalIntro.IntroStart)
{
continue;
}
From b1cf0aa2fc30a5d9165e6f5406fab41294f6269a Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sun, 4 Sep 2022 03:16:33 -0500
Subject: [PATCH 23/29] Avoid changing intro dictionary in HTTP controller
---
.../Controllers/SkipIntroController.cs | 3 ++-
ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs | 11 +++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs
index 5a47f15..d6c97f6 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/SkipIntroController.cs
@@ -54,7 +54,8 @@ public class SkipIntroController : ControllerBase
/// Intro object if the provided item has an intro, null otherwise.
private Intro? GetIntro(Guid id)
{
- return Plugin.Instance!.Intros.TryGetValue(id, out var intro) ? intro : null;
+ // Returns a copy to avoid mutating the original Intro object stored in the dictionary.
+ return Plugin.Instance!.Intros.TryGetValue(id, out var intro) ? new Intro(intro) : null;
}
///
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs
index 4cd88e5..731778c 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/Intro.cs
@@ -32,6 +32,17 @@ public class Intro
IntroEnd = 0;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// intro.
+ public Intro(Intro intro)
+ {
+ EpisodeId = intro.EpisodeId;
+ IntroStart = intro.IntroStart;
+ IntroEnd = intro.IntroEnd;
+ }
+
///
/// Initializes a new instance of the class.
///
From 88806550d4b877c39a614316d10597ec3161d620 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sun, 4 Sep 2022 18:47:52 -0500
Subject: [PATCH 24/29] Remove unused function
---
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index c4f39c8..9a5d983 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -690,16 +690,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
return BitOperations.PopCount(number);
}
- private double GetIntroDuration(Guid id)
- {
- if (!Plugin.Instance!.Intros.TryGetValue(id, out var episode))
- {
- return 0;
- }
-
- return episode.Valid ? Math.Round(episode.IntroEnd - episode.IntroStart, 2) : 0;
- }
-
///
/// Get task triggers.
///
From cafedd8954aee8dea200ec429b5fc6b6b9deef5f Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Wed, 7 Sep 2022 22:07:49 -0500
Subject: [PATCH 25/29] Build new version of plugin in CI
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5257d0e..a1302f2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,7 +2,7 @@ name: 'Build Plugin'
on:
push:
- branches: [ "master" ]
+ branches: [ "master", "algorithm_rewrite" ]
pull_request:
branches: [ "master" ]
From bb38c90fdead8d1a9bfe254cb84b4a73ad8cfb71 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 1 Sep 2022 21:46:50 -0500
Subject: [PATCH 26/29] Automatically setup containers under test
(cherry picked from commit fa9eba300afd1b85eca88b67930c1ab80ac2ec80)
---
.../e2e_tests/config_sample.jsonc | 3 +-
.../e2e_tests/verifier/report_generator.go | 23 ++++-
.../e2e_tests/wrapper/library.json | 93 +++++++++++++++++++
.../e2e_tests/wrapper/main.go | 68 +++++++++++---
.../e2e_tests/wrapper/setup.go | 79 ++++++++++++++++
.../e2e_tests/wrapper/structs.go | 1 -
6 files changed, 246 insertions(+), 21 deletions(-)
create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/library.json
create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/setup.go
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/config_sample.jsonc b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/config_sample.jsonc
index c2eedc8..a800145 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/config_sample.jsonc
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/config_sample.jsonc
@@ -1,6 +1,6 @@
{
"common": {
- "library": "/full/path/to/test/library/on/host",
+ "library": "/full/path/to/test/library/on/host/TV",
"episode": "Episode title to search for"
},
"servers": [
@@ -9,7 +9,6 @@
"image": "ghcr.io/confusedpolarbear/jellyfin-intro-skipper:latest",
"username": "admin",
"password": "hunter2",
- "base": "config/official",
"browsers": [
"chrome",
"firefox"
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report_generator.go b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report_generator.go
index d6f13b4..4814022 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report_generator.go
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report_generator.go
@@ -99,6 +99,8 @@ func generateReport(hostAddress, apiKey, reportDestination string, keepTimestamp
}
func runAnalysisAndWait(hostAddress, apiKey string, pollInterval time.Duration) {
+ var taskId string = ""
+
type taskInfo struct {
State string
CurrentProgressPercentage int
@@ -108,9 +110,24 @@ func runAnalysisAndWait(hostAddress, apiKey string, pollInterval time.Duration)
SendRequest("POST", hostAddress+"/Intros/EraseTimestamps", apiKey)
fmt.Println()
+ // The task ID changed with v0.1.7.
+ // Old task ID: 8863329048cc357f7dfebf080f2fe204
+ // New task ID: 6adda26c5261c40e8fa4a7e7df568be2
fmt.Println("[+] Starting analysis task")
- SendRequest("POST", hostAddress+"/ScheduledTasks/Running/6adda26c5261c40e8fa4a7e7df568be2", apiKey)
- fmt.Println()
+ for _, id := range []string{"8863329048cc357f7dfebf080f2fe204", "6adda26c5261c40e8fa4a7e7df568be2"} {
+ body := SendRequest("POST", hostAddress+"/ScheduledTasks/Running/"+id, apiKey)
+ fmt.Println()
+
+ // If the scheduled task was found, store the task ID for later
+ if !strings.Contains(string(body), "Not Found") {
+ taskId = id
+ break
+ }
+ }
+
+ if taskId == "" {
+ panic("unable to find scheduled task")
+ }
fmt.Println("[+] Waiting for analysis task to complete")
fmt.Print("[+] Episodes analyzed: 0%")
@@ -141,7 +158,7 @@ func runAnalysisAndWait(hostAddress, apiKey string, pollInterval time.Duration)
lastQuery = time.Now()
- raw := SendRequest("GET", hostAddress+"/ScheduledTasks/6adda26c5261c40e8fa4a7e7df568be2?hideUrl=1", apiKey)
+ raw := SendRequest("GET", hostAddress+"/ScheduledTasks/"+taskId+"?hideUrl=1", apiKey)
if err := json.Unmarshal(raw, &info); err != nil {
fmt.Printf("[!] Unable to unmarshal response into taskInfo struct: %s\n", err)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/library.json b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/library.json
new file mode 100644
index 0000000..5c35999
--- /dev/null
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/library.json
@@ -0,0 +1,93 @@
+{
+ "LibraryOptions": {
+ "EnableArchiveMediaFiles": false,
+ "EnablePhotos": false,
+ "EnableRealtimeMonitor": false,
+ "ExtractChapterImagesDuringLibraryScan": false,
+ "EnableChapterImageExtraction": false,
+ "EnableInternetProviders": false,
+ "SaveLocalMetadata": false,
+ "EnableAutomaticSeriesGrouping": false,
+ "PreferredMetadataLanguage": "",
+ "MetadataCountryCode": "",
+ "SeasonZeroDisplayName": "Specials",
+ "AutomaticRefreshIntervalDays": 0,
+ "EnableEmbeddedTitles": false,
+ "EnableEmbeddedEpisodeInfos": false,
+ "AllowEmbeddedSubtitles": "AllowAll",
+ "SkipSubtitlesIfEmbeddedSubtitlesPresent": false,
+ "SkipSubtitlesIfAudioTrackMatches": false,
+ "SaveSubtitlesWithMedia": true,
+ "RequirePerfectSubtitleMatch": true,
+ "AutomaticallyAddToCollection": false,
+ "MetadataSavers": [],
+ "TypeOptions": [
+ {
+ "Type": "Series",
+ "MetadataFetchers": [
+ "TheMovieDb",
+ "The Open Movie Database"
+ ],
+ "MetadataFetcherOrder": [
+ "TheMovieDb",
+ "The Open Movie Database"
+ ],
+ "ImageFetchers": [
+ "TheMovieDb"
+ ],
+ "ImageFetcherOrder": [
+ "TheMovieDb"
+ ]
+ },
+ {
+ "Type": "Season",
+ "MetadataFetchers": [
+ "TheMovieDb"
+ ],
+ "MetadataFetcherOrder": [
+ "TheMovieDb"
+ ],
+ "ImageFetchers": [
+ "TheMovieDb"
+ ],
+ "ImageFetcherOrder": [
+ "TheMovieDb"
+ ]
+ },
+ {
+ "Type": "Episode",
+ "MetadataFetchers": [
+ "TheMovieDb",
+ "The Open Movie Database"
+ ],
+ "MetadataFetcherOrder": [
+ "TheMovieDb",
+ "The Open Movie Database"
+ ],
+ "ImageFetchers": [
+ "TheMovieDb",
+ "The Open Movie Database",
+ "Embedded Image Extractor",
+ "Screen Grabber"
+ ],
+ "ImageFetcherOrder": [
+ "TheMovieDb",
+ "The Open Movie Database",
+ "Embedded Image Extractor",
+ "Screen Grabber"
+ ]
+ }
+ ],
+ "LocalMetadataReaderOrder": [
+ "Nfo"
+ ],
+ "SubtitleDownloadLanguages": [],
+ "DisabledSubtitleFetchers": [],
+ "SubtitleFetcherOrder": [],
+ "PathInfos": [
+ {
+ "Path": "/media/TV"
+ }
+ ]
+ }
+}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/main.go b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/main.go
index b22e339..fe4826f 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/main.go
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/main.go
@@ -2,6 +2,8 @@ package main
import (
"bytes"
+ "crypto/rand"
+ "encoding/hex"
"encoding/json"
"flag"
"fmt"
@@ -19,10 +21,21 @@ var containerAddress string
// Path to compiled plugin DLL to install in local containers.
var pluginPath string
+// Randomly generated password used to setup container with.
+var containerPassword string
+
func flags() {
flag.StringVar(&pluginPath, "dll", "", "Path to plugin DLL to install in container images.")
flag.StringVar(&containerAddress, "caddr", "", "IP address to use when connecting to local containers.")
flag.Parse()
+
+ // Randomize the container's password
+ rawPassword := make([]byte, 32)
+ if _, err := rand.Read(rawPassword); err != nil {
+ panic(err)
+ }
+
+ containerPassword = hex.EncodeToString(rawPassword)
}
func main() {
@@ -72,11 +85,6 @@ func main() {
panic(err)
}
- // Copy the contents of the base configuration directory to a temp folder for the container
- src, dst := server.Base+"/.", configurationDirectory
- fmt.Printf(" [+] Copying %s to %s\n", src, dst)
- RunProgram("cp", []string{"-ar", src, dst}, 10*time.Second)
-
// Create a folder to install the plugin into
pluginDirectory := path.Join(configurationDirectory, "plugins", "intro-skipper")
if lsioImage {
@@ -91,15 +99,19 @@ func main() {
// If this is an LSIO container, adjust the permissions on the plugin directory
if lsioImage {
- if err := os.Chown(pluginDirectory, 911, 911); err != nil {
- fmt.Printf(" [!] Failed to change plugin directory UID/GID: %s\n", err)
- goto cleanup
- }
+ RunProgram(
+ "chown",
+ []string{
+ "911:911",
+ "-R",
+ path.Join(configurationDirectory, "data", "plugins")},
+ 2*time.Second)
}
// Install the plugin
fmt.Printf(" [+] Copying plugin %s to %s\n", pluginPath, pluginDirectory)
RunProgram("cp", []string{pluginPath, pluginDirectory}, 2*time.Second)
+ fmt.Println()
/* Start the container with the following settings:
* Name: jf-e2e
@@ -116,6 +128,18 @@ func main() {
// Wait for the container to fully start
waitForServerStartup(server.Address)
+ fmt.Println()
+
+ fmt.Println(" [+] Setting up container")
+
+ // Set up the container
+ SetupServer(server.Address, containerPassword)
+
+ // Restart the container and wait for it to come back up
+ RunProgram("docker", []string{"restart", "jf-e2e"}, 10*time.Second)
+ time.Sleep(time.Second)
+ waitForServerStartup(server.Address)
+ fmt.Println()
} else {
fmt.Println("[+] Remote instance, assuming plugin is already installed")
}
@@ -123,6 +147,21 @@ func main() {
// Get an API key
apiKey = login(server)
+ // Rescan the library if this is a server that we just setup
+ if server.Docker {
+ fmt.Println(" [+] Rescanning library")
+
+ sendRequest(
+ server.Address+"/ScheduledTasks/Running/7738148ffcd07979c7ceb148e06b3aed?api_key="+apiKey,
+ "POST",
+ "")
+
+ // TODO: poll for task completion
+ time.Sleep(10 * time.Second)
+
+ fmt.Println()
+ }
+
// Analyze episodes and save report
fmt.Println(" [+] Analyzing episodes")
fmt.Print("\033[37;1m") // change the color of the verifier's text
@@ -268,8 +307,9 @@ func loadConfiguration() Configuration {
}
// Print debugging info
- fmt.Printf("Library: %s\n", config.Common.Library)
- fmt.Printf("Episode: \"%s\"\n", config.Common.Episode)
+ fmt.Printf("Library: %s\n", config.Common.Library)
+ fmt.Printf("Episode: \"%s\"\n", config.Common.Episode)
+ fmt.Printf("Password: %s\n", containerPassword)
fmt.Println()
// Check the validity of all entries
@@ -282,14 +322,12 @@ func loadConfiguration() Configuration {
panic("The -caddr argument is required.")
}
- if server.Base == "" {
- panic("Original configuration directory is required")
- }
-
if pluginPath == "" {
panic("The -dll argument is required.")
}
+ server.Username = "admin"
+ server.Password = containerPassword
server.Address = fmt.Sprintf("http://%s:8097", containerAddress)
server.Docker = true
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/setup.go b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/setup.go
new file mode 100644
index 0000000..b428c6d
--- /dev/null
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/setup.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "bytes"
+ _ "embed"
+ "fmt"
+ "net/http"
+)
+
+//go:embed library.json
+var librarySetupPayload string
+
+func SetupServer(server, password string) {
+ makeUrl := func(u string) string {
+ return fmt.Sprintf("%s/%s", server, u)
+ }
+
+ // Set the server language to English
+ sendRequest(
+ makeUrl("Startup/Configuration"),
+ "POST",
+ `{"UICulture":"en-US","MetadataCountryCode":"US","PreferredMetadataLanguage":"en"}`)
+
+ // Get the first user
+ sendRequest(makeUrl("Startup/User"), "GET", "")
+
+ // Create the first user
+ sendRequest(
+ makeUrl("Startup/User"),
+ "POST",
+ fmt.Sprintf(`{"Name":"admin","Password":"%s"}`, password))
+
+ // Create a TV library from the media at /media/TV.
+ sendRequest(
+ makeUrl("Library/VirtualFolders?collectionType=tvshows&refreshLibrary=false&name=Shows"),
+ "POST",
+ librarySetupPayload)
+
+ // Setup remote access
+ sendRequest(
+ makeUrl("Startup/RemoteAccess"),
+ "POST",
+ `{"EnableRemoteAccess":true,"EnableAutomaticPortMapping":false}`)
+
+ // Mark the wizard as complete
+ sendRequest(
+ makeUrl("Startup/Complete"),
+ "POST",
+ ``)
+}
+
+func sendRequest(url string, method string, body string) {
+ // Create the request
+ req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(body)))
+ if err != nil {
+ panic(err)
+ }
+
+ // Set required headers
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set(
+ "X-Emby-Authorization",
+ `MediaBrowser Client="JF E2E Tests", Version="0.0.1", DeviceId="E2E", Device="E2E"`)
+
+ // Send it
+ fmt.Printf(" [+] %s %s", method, url)
+ res, err := http.DefaultClient.Do(req)
+
+ if err != nil {
+ fmt.Println()
+ panic(err)
+ }
+
+ fmt.Printf(" %d\n", res.StatusCode)
+
+ if res.StatusCode != http.StatusNoContent && res.StatusCode != http.StatusOK {
+ panic("invalid status code received during setup")
+ }
+}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/structs.go b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/structs.go
index 206c1f3..9d472a5 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/structs.go
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/wrapper/structs.go
@@ -17,7 +17,6 @@ type Server struct {
Image string `json:"image"`
Username string `json:"username"`
Password string `json:"password"`
- Base string `json:"base"`
Browsers []string `json:"browsers"`
Tests []string `json:"tests"`
From d8bea7b54ef43eafc8cef7c48329d36e506a0248 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sat, 10 Sep 2022 02:24:44 -0500
Subject: [PATCH 27/29] Ignore virtual items and log more info
---
ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
index 185cc60..3395243 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
@@ -136,6 +136,7 @@ public class QueueManager
},
IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode },
Recursive = true,
+ IsVirtualItem = false
};
_logger.LogDebug("Getting items");
@@ -174,7 +175,11 @@ public class QueueManager
if (string.IsNullOrEmpty(episode.Path))
{
- _logger.LogWarning("Not queuing episode {Id} as no path was provided by Jellyfin", episode.Id);
+ _logger.LogWarning(
+ "Not queuing episode \"{Name}\" from series \"{Series}\" ({Id}) as no path was provided by Jellyfin",
+ episode.Name,
+ episode.SeriesName,
+ episode.Id);
return;
}
From 8953b4087fbdba393daca1c2f5d76ebbb0e8ed9d Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sat, 17 Sep 2022 00:35:50 -0500
Subject: [PATCH 28/29] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cc462e..507dabc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,10 +9,12 @@
* Support playing a few seconds from the end of the introduction to verify that no episode content was skipped over
* Amount played is customizable and defaults to 2 seconds
* Support modifying introduction detection algorithm settings
+ * Add option to not skip the introduction in the first episode of a season
* Fixes
* Fix scheduled task interval (#79)
* Prevent show names from becoming duplicated in the show name dropdown under the advanced section
+ * Prevent virtual episodes from being inserted into the analysis queue
## v0.1.6.0 (2022-08-04)
* New features
From 991283387c152a449c94e4e1e2dc09dda3313517 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Fri, 23 Sep 2022 22:13:07 -0500
Subject: [PATCH 29/29] Remove analysis statistics
---
.../TestStatistics.cs | 22 ---
.../Configuration/configPage.html | 61 ---------
.../Controllers/VisualizationController.cs | 11 --
.../Data/AnalysisStatistics.cs | 128 ------------------
.../Plugin.cs | 5 -
.../ScheduledTasks/AnalyzeEpisodesTask.cs | 18 ---
6 files changed, 245 deletions(-)
delete mode 100644 ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs
delete mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs
deleted file mode 100644
index 66b2e34..0000000
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text;
-using Xunit;
-
-namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
-
-public class TestStatistics
-{
- [Fact]
- public void TestTSISerialization()
- {
- var expected = "\"TotalAnalyzedEpisodes\":42,";
-
- var stats = new AnalysisStatistics();
- stats.TotalAnalyzedEpisodes.Add(42);
-
- var actual = Encoding.UTF8.GetString(JsonSerializer.SerializeToUtf8Bytes(stats));
-
- Assert.Contains(expected, actual, StringComparison.OrdinalIgnoreCase);
- }
-}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
index 4f51047..9c3cc5e 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
@@ -258,12 +258,6 @@
-
- Analysis Statistics (experimental)
-
-
-
-
Advanced
@@ -372,7 +366,6 @@
// settings elements
var visualizer = document.querySelector("details#visualizer");
- var statistics = document.querySelector("details#statistics");
var support = document.querySelector("details#support");
var btnEraseTimestamps = document.querySelector("button#btnEraseTimestamps");
@@ -470,59 +463,6 @@
Dashboard.alert("Press Ctrl+C to copy support bundle");
}
- async function statisticsToggled() {
- if (!statistics.open) {
- return;
- }
-
- // Blank any old statistics
- const text = document.querySelector("pre#statisticsText");
- text.textContent = "All CPU times are displayed as seconds.\n\n";
-
- Dashboard.showLoadingMsg();
-
- // Load the statistics from the server
- let stats = await getJson("Intros/Statistics");
-
- // Select which fields to print and label them with more friendly descriptions
- let fields = "TotalTaskTime,TotalCPUTime,FingerprintCPUTime,FirstPassCPUTime,SecondPassCPUTime," +
- "IndexSearches,QuickScans,FullScans,TotalQueuedEpisodes";
-
- let friendlyNames = {
- TotalTaskTime: "Total task time",
- TotalCPUTime: "Total CPU time",
- FingerprintCPUTime: "Fingerprint CPU time",
- FirstPassCPUTime: "First pass CPU time",
- SecondPassCPUTime: "Second pass CPU time",
- IndexSearches: "Index searches",
- QuickScans: "Quick scans",
- FullScans: "Full scans",
- TotalQueuedEpisodes: "Episodes queued",
- };
-
- // Print all friendly names and data points
- for (var f of fields.split(",")) {
- let name = friendlyNames[f].padEnd(25);
- let value = stats[f];
-
- // If this statistic is a measure of CPU time, divide by 1,000 to turn milliseconds into seconds.
- if (name.includes("time")) {
- value = Math.round(value / 1000);
- }
-
- text.textContent += name + value + "\n";
- }
-
- // Calculate the percentage of time (to two decimal places) spent waiting for fingerprints
- const percentWait = Math.round((stats.FingerprintCPUTime * 10_000) / stats.TotalCPUTime) / 100;
-
- // Breakdown CPU time by analysis component
- text.textContent += "\nCPU time breakdown:\n";
- text.textContent += "Fingerprint generation " + percentWait + "%\n";
-
- Dashboard.hideLoadingMsg();
- }
-
// show changed, populate seasons
async function showChanged() {
clearSelect(selectSeason);
@@ -752,7 +692,6 @@
});
visualizer.addEventListener("toggle", visualizerToggled);
- statistics.addEventListener("toggle", statisticsToggled);
support.addEventListener("toggle", supportToggled);
txtOffset.addEventListener("change", renderTroubleshooter);
selectShow.addEventListener("change", showChanged);
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
index 5cf7bc1..d4cec0d 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
@@ -166,17 +166,6 @@ public class VisualizationController : ControllerBase
return NoContent();
}
- ///
- /// Returns the statistics for the most recent analysis.
- ///
- /// Analysis statistics.
- /// AnalysisStatistics.
- [HttpGet("Statistics")]
- public ActionResult GetAnalysisStatistics()
- {
- return Plugin.Instance!.AnalysisStatistics;
- }
-
private string GetSeasonName(QueuedEpisode episode)
{
return "Season " + episode.SeasonNumber.ToString(CultureInfo.InvariantCulture);
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
deleted file mode 100644
index 522dbdf..0000000
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-namespace ConfusedPolarBear.Plugin.IntroSkipper;
-
-using System;
-using System.Threading;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-///
-/// Detailed statistics about the last analysis operation performed. All times are represented as milliseconds.
-///
-public class AnalysisStatistics
-{
- ///
- /// Gets the number of episodes that have been analyzed so far.
- ///
- public ThreadSafeInteger TotalAnalyzedEpisodes { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets or sets the number of episodes that need to be analyzed.
- ///
- public int TotalQueuedEpisodes { get; set; }
-
- ///
- /// Gets the number of times an index search successfully located a pair of introductions.
- ///
- public ThreadSafeInteger IndexSearches { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets the total CPU time spent waiting for audio fingerprints to be generated.
- ///
- public ThreadSafeInteger FingerprintCPUTime { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets the total CPU time spent analyzing fingerprints.
- ///
- public ThreadSafeInteger AnalysisCPUTime { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets the total task runtime across all threads.
- ///
- public ThreadSafeInteger TotalCPUTime { get; } = new ThreadSafeInteger();
-
- ///
- /// Gets the total task runtime as measured by a clock.
- ///
- public ThreadSafeInteger TotalTaskTime { get; } = new ThreadSafeInteger();
-}
-
-///
-/// Convenience wrapper around a thread safe integer.
-///
-[JsonConverter(typeof(ThreadSafeIntegerJsonConverter))]
-public class ThreadSafeInteger
-{
- private int value = 0;
-
- ///
- /// Gets the current value stored by this integer.
- ///
- public int Value
- {
- get
- {
- return value;
- }
- }
-
- ///
- /// Increment the value of this integer by 1.
- ///
- public void Increment()
- {
- Add(1);
- }
-
- ///
- /// Adds the total milliseconds elapsed since a start time.
- ///
- /// Start time.
- public void AddDuration(DateTime start)
- {
- if (start == DateTime.MinValue)
- {
- return;
- }
-
- var elapsed = DateTime.Now.Subtract(start);
- Add((int)elapsed.TotalMilliseconds);
- }
-
- ///
- /// Adds the provided amount to this integer.
- ///
- /// Amount to add.
- public void Add(int amount)
- {
- Interlocked.Add(ref value, amount);
- }
-}
-
-///
-/// Serialize thread safe integers to a regular integer (instead of an object with a Value property).
-///
-public class ThreadSafeIntegerJsonConverter : JsonConverter
-{
- ///
- /// Deserialization of TSIs is not supported and will always throw a NotSupportedException.
- ///
- /// Reader.
- /// Type.
- /// Options.
- /// Never returns.
- public override ThreadSafeInteger? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- throw new NotSupportedException();
- }
-
- ///
- /// Serialize the provided TSI.
- ///
- /// Writer.
- /// TSI.
- /// Options.
- public override void Write(Utf8JsonWriter writer, ThreadSafeInteger value, JsonSerializerOptions options)
- {
- writer.WriteNumberValue(value.Value);
- }
-}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
index b53aec6..eb96689 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
@@ -80,11 +80,6 @@ public class Plugin : BasePlugin, IHasWebPages
///
public int TotalQueued { get; set; }
- ///
- /// Gets or sets the statistics from the most recent analysis run.
- ///
- public AnalysisStatistics AnalysisStatistics { get; set; } = new AnalysisStatistics();
-
///
/// Gets the directory to cache fingerprints in.
///
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
index 9a5d983..e8460b0 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/AnalyzeEpisodesTask.cs
@@ -31,11 +31,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
///
private readonly object _introsLock = new object();
- ///
- /// Statistics for the currently running analysis task.
- ///
- private AnalysisStatistics analysisStatistics = new AnalysisStatistics();
-
///
/// Minimum duration of similar audio that will be considered an introduction.
///
@@ -133,8 +128,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
};
var taskStart = DateTime.Now;
- analysisStatistics = new AnalysisStatistics();
- analysisStatistics.TotalQueuedEpisodes = Plugin.Instance!.TotalQueued;
minimumIntroDuration = Plugin.Instance!.Configuration.MinimumIntroDuration;
@@ -186,15 +179,8 @@ public class AnalyzeEpisodesTask : IScheduledTask
}
progress.Report((totalProcessed * 100) / Plugin.Instance!.TotalQueued);
-
- analysisStatistics.TotalCPUTime.AddDuration(workerStart);
- Plugin.Instance!.AnalysisStatistics = analysisStatistics;
});
- // Update analysis statistics
- analysisStatistics.TotalTaskTime.AddDuration(taskStart);
- Plugin.Instance!.AnalysisStatistics = analysisStatistics;
-
// Turn the regenerate EDL flag off after the scan completes.
if (Plugin.Instance!.Configuration.RegenerateEdlFiles)
{
@@ -385,8 +371,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
if (lhsRanges.Count > 0)
{
_logger.LogTrace("Index search successful");
- analysisStatistics.IndexSearches.Increment();
- analysisStatistics.AnalysisCPUTime.AddDuration(start);
return GetLongestTimeRange(lhsId, lhsRanges, rhsId, rhsRanges);
}
@@ -396,8 +380,6 @@ public class AnalyzeEpisodesTask : IScheduledTask
lhsId,
rhsId);
- analysisStatistics.AnalysisCPUTime.AddDuration(start);
-
return (new Intro(lhsId), new Intro(rhsId));
}