refactor item queue (#183)
Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
This commit is contained in:
parent
ddecb15a51
commit
9388f2a583
@ -51,13 +51,15 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
|
|
||||||
var creditTimes = new Dictionary<Guid, Intro>();
|
var creditTimes = new Dictionary<Guid, Intro>();
|
||||||
|
|
||||||
|
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
||||||
|
|
||||||
bool isFirstEpisode = true;
|
bool isFirstEpisode = true;
|
||||||
|
|
||||||
double searchStart = minimumCreditsDuration;
|
double searchStart = minimumCreditsDuration;
|
||||||
|
|
||||||
var searchDistance = 2 * minimumCreditsDuration;
|
var searchDistance = 2 * minimumCreditsDuration;
|
||||||
|
|
||||||
foreach (var episode in analysisQueue)
|
foreach (var episode in episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)))
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -96,13 +98,13 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
isFirstEpisode = false;
|
isFirstEpisode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var intro = AnalyzeMediaFile(
|
var credit = AnalyzeMediaFile(
|
||||||
episode,
|
episode,
|
||||||
searchStart,
|
searchStart,
|
||||||
searchDistance,
|
searchDistance,
|
||||||
blackFrameMinimumPercentage);
|
blackFrameMinimumPercentage);
|
||||||
|
|
||||||
if (intro is null)
|
if (credit is null)
|
||||||
{
|
{
|
||||||
// If no credits were found, reset the first-episode search logic for the next episode in the sequence.
|
// If no credits were found, reset the first-episode search logic for the next episode in the sequence.
|
||||||
searchStart = minimumCreditsDuration;
|
searchStart = minimumCreditsDuration;
|
||||||
@ -110,17 +112,15 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchStart = episode.Duration - intro.IntroStart + (0.5 * searchDistance);
|
searchStart = episode.Duration - credit.IntroStart + (0.5 * searchDistance);
|
||||||
|
|
||||||
creditTimes[episode.EpisodeId] = intro;
|
creditTimes.Add(episode.EpisodeId, credit);
|
||||||
|
episode.State.SetAnalyzed(mode, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance!.UpdateTimestamps(creditTimes, mode);
|
Plugin.Instance!.UpdateTimestamps(creditTimes, mode);
|
||||||
|
|
||||||
return analysisQueue
|
return episodeAnalysisQueue.AsReadOnly();
|
||||||
.Where(x => !creditTimes.ContainsKey(x.EpisodeId))
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -35,6 +35,9 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
|||||||
{
|
{
|
||||||
var skippableRanges = new Dictionary<Guid, Intro>();
|
var skippableRanges = new Dictionary<Guid, Intro>();
|
||||||
|
|
||||||
|
// Episode analysis queue.
|
||||||
|
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
||||||
|
|
||||||
var expression = mode == AnalysisMode.Introduction ?
|
var expression = mode == AnalysisMode.Introduction ?
|
||||||
Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern :
|
Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern :
|
||||||
Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern;
|
Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern;
|
||||||
@ -44,7 +47,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
|||||||
return analysisQueue;
|
return analysisQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var episode in analysisQueue)
|
foreach (var episode in episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)))
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -63,14 +66,12 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
|||||||
}
|
}
|
||||||
|
|
||||||
skippableRanges.Add(episode.EpisodeId, skipRange);
|
skippableRanges.Add(episode.EpisodeId, skipRange);
|
||||||
|
episode.State.SetAnalyzed(mode, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Instance.UpdateTimestamps(skippableRanges, mode);
|
Plugin.Instance.UpdateTimestamps(skippableRanges, mode);
|
||||||
|
|
||||||
return analysisQueue
|
return episodeAnalysisQueue.AsReadOnly();
|
||||||
.Where(x => !skippableRanges.ContainsKey(x.EpisodeId))
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -92,10 +93,11 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
|||||||
|
|
||||||
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||||
|
|
||||||
var minDuration = config.MinimumIntroDuration;
|
var (minDuration, maxDuration) = mode switch
|
||||||
int maxDuration = mode == AnalysisMode.Introduction ?
|
{
|
||||||
config.MaximumIntroDuration :
|
AnalysisMode.Introduction => (config.MinimumIntroDuration, config.MaximumIntroDuration),
|
||||||
config.MaximumCreditsDuration;
|
_ => (config.MinimumCreditsDuration, config.MaximumCreditsDuration)
|
||||||
|
};
|
||||||
|
|
||||||
if (chapters.Count == 0)
|
if (chapters.Count == 0)
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||||
@ -61,16 +63,36 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
// Cache of all fingerprints for this season.
|
// Cache of all fingerprints for this season.
|
||||||
var fingerprintCache = new Dictionary<Guid, uint[]>();
|
var fingerprintCache = new Dictionary<Guid, uint[]>();
|
||||||
|
|
||||||
// Episode analysis queue.
|
// Episode analysis queue based on not analyzed episodes
|
||||||
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
||||||
|
|
||||||
// Episodes that were analyzed and do not have an introduction.
|
// Episodes that were analyzed and do not have an introduction.
|
||||||
var episodesWithoutIntros = new List<QueuedEpisode>();
|
var episodesWithoutIntros = episodeAnalysisQueue.Where(e => !e.State.IsAnalyzed(mode)).ToList();
|
||||||
|
|
||||||
this._analysisMode = mode;
|
this._analysisMode = mode;
|
||||||
|
|
||||||
|
if (episodesWithoutIntros.Count == 0 || episodeAnalysisQueue.Count <= 1)
|
||||||
|
{
|
||||||
|
return analysisQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var episodesWithFingerprint = new List<QueuedEpisode>(episodesWithoutIntros);
|
||||||
|
|
||||||
|
// Load fingerprints from cache if available.
|
||||||
|
episodesWithFingerprint.AddRange(episodeAnalysisQueue.Where(e => e.State.IsAnalyzed(mode) && File.Exists(FFmpegWrapper.GetFingerprintCachePath(e, mode))));
|
||||||
|
|
||||||
|
// Ensure at least two fingerprints are present.
|
||||||
|
if (episodesWithFingerprint.Count == 1)
|
||||||
|
{
|
||||||
|
var indexInAnalysisQueue = episodeAnalysisQueue.FindIndex(episode => episode == episodesWithoutIntros[0]);
|
||||||
|
episodesWithFingerprint.AddRange(episodeAnalysisQueue
|
||||||
|
.Where((episode, index) => Math.Abs(index - indexInAnalysisQueue) <= 1 && index != indexInAnalysisQueue));
|
||||||
|
}
|
||||||
|
|
||||||
|
seasonIntros = episodesWithFingerprint.Where(e => e.State.IsAnalyzed(mode)).ToDictionary(e => e.EpisodeId, e => Plugin.Instance!.GetIntroByMode(e.EpisodeId, mode));
|
||||||
|
|
||||||
// Compute fingerprints for all episodes in the season
|
// Compute fingerprints for all episodes in the season
|
||||||
foreach (var episode in episodeAnalysisQueue)
|
foreach (var episode in episodesWithFingerprint)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -98,14 +120,15 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// While there are still episodes in the queue
|
// While there are still episodes in the queue
|
||||||
while (episodeAnalysisQueue.Count > 0)
|
while (episodesWithoutIntros.Count > 0)
|
||||||
{
|
{
|
||||||
// Pop the first episode from the queue
|
// Pop the first episode from the queue
|
||||||
var currentEpisode = episodeAnalysisQueue[0];
|
var currentEpisode = episodesWithoutIntros[0];
|
||||||
episodeAnalysisQueue.RemoveAt(0);
|
episodesWithoutIntros.RemoveAt(0);
|
||||||
|
episodesWithFingerprint.Remove(currentEpisode);
|
||||||
|
|
||||||
// Search through all remaining episodes.
|
// Search through all remaining episodes.
|
||||||
foreach (var remainingEpisode in episodeAnalysisQueue)
|
foreach (var remainingEpisode in episodesWithFingerprint)
|
||||||
{
|
{
|
||||||
// Compare the current episode to all remaining episodes in the queue.
|
// Compare the current episode to all remaining episodes in the queue.
|
||||||
var (currentIntro, remainingIntro) = CompareEpisodes(
|
var (currentIntro, remainingIntro) = CompareEpisodes(
|
||||||
@ -167,9 +190,10 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no intro is found at this point, the popped episode is not reinserted into the queue.
|
// If no intro is found at this point, the popped episode is not reinserted into the queue.
|
||||||
if (!seasonIntros.ContainsKey(currentEpisode.EpisodeId))
|
if (seasonIntros.ContainsKey(currentEpisode.EpisodeId))
|
||||||
{
|
{
|
||||||
episodesWithoutIntros.Add(currentEpisode);
|
episodesWithFingerprint.Add(currentEpisode);
|
||||||
|
episodeAnalysisQueue.FirstOrDefault(x => x.EpisodeId == currentEpisode.EpisodeId)?.State.SetAnalyzed(mode, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +211,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
|
|
||||||
Plugin.Instance!.UpdateTimestamps(seasonIntros, this._analysisMode);
|
Plugin.Instance!.UpdateTimestamps(seasonIntros, this._analysisMode);
|
||||||
|
|
||||||
return episodesWithoutIntros.AsReadOnly();
|
return episodeAnalysisQueue.AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -28,11 +28,6 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SelectedLibraries { get; set; } = string.Empty;
|
public string SelectedLibraries { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a temporary limitation on file paths to be analyzed. Should be empty when automatic scan is idle.
|
|
||||||
/// </summary>
|
|
||||||
public IList<string> PathRestrictions { get; } = new List<string>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether to scan for intros during a scheduled task.
|
/// Gets or sets a value indicating whether to scan for intros during a scheduled task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -80,9 +80,7 @@ public class SkipIntroController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var timestamp = mode == AnalysisMode.Introduction ?
|
var timestamp = Plugin.Instance!.GetIntroByMode(id, mode);
|
||||||
Plugin.Instance!.Intros[id] :
|
|
||||||
Plugin.Instance!.Credits[id];
|
|
||||||
|
|
||||||
// Operate on a copy to avoid mutating the original Intro object stored in the dictionary.
|
// Operate on a copy to avoid mutating the original Intro object stored in the dictionary.
|
||||||
var segment = new Intro(timestamp);
|
var segment = new Intro(timestamp);
|
||||||
@ -135,6 +133,7 @@ public class SkipIntroController : ControllerBase
|
|||||||
FFmpegWrapper.DeleteCacheFiles(mode);
|
FFmpegWrapper.DeleteCacheFiles(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Plugin.Instance!.EpisodeStates.Clear();
|
||||||
Plugin.Instance!.SaveTimestamps(mode);
|
Plugin.Instance!.SaveTimestamps(mode);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
@ -143,6 +143,7 @@ public class VisualizationController : ControllerBase
|
|||||||
{
|
{
|
||||||
Plugin.Instance!.Intros.TryRemove(e.EpisodeId, out _);
|
Plugin.Instance!.Intros.TryRemove(e.EpisodeId, out _);
|
||||||
Plugin.Instance!.Credits.TryRemove(e.EpisodeId, out _);
|
Plugin.Instance!.Credits.TryRemove(e.EpisodeId, out _);
|
||||||
|
e.State.ResetStates();
|
||||||
if (eraseCache)
|
if (eraseCache)
|
||||||
{
|
{
|
||||||
FFmpegWrapper.DeleteEpisodeCache(e.EpisodeId);
|
FFmpegWrapper.DeleteEpisodeCache(e.EpisodeId);
|
||||||
|
50
ConfusedPolarBear.Plugin.IntroSkipper/Data/EpisodeState.cs
Normal file
50
ConfusedPolarBear.Plugin.IntroSkipper/Data/EpisodeState.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the state of an episode regarding analysis and blacklist status.
|
||||||
|
/// </summary>
|
||||||
|
public class EpisodeState
|
||||||
|
{
|
||||||
|
private readonly bool[] _analyzedStates = new bool[2];
|
||||||
|
|
||||||
|
private readonly bool[] _blacklistedStates = new bool[2];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the specified analysis mode has been analyzed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">The analysis mode to check.</param>
|
||||||
|
/// <returns>True if the mode has been analyzed, false otherwise.</returns>
|
||||||
|
public bool IsAnalyzed(AnalysisMode mode) => _analyzedStates[(int)mode];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the analyzed state for the specified analysis mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">The analysis mode to set.</param>
|
||||||
|
/// <param name="value">The analyzed state to set.</param>
|
||||||
|
public void SetAnalyzed(AnalysisMode mode, bool value) => _analyzedStates[(int)mode] = value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the specified analysis mode has been blacklisted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">The analysis mode to check.</param>
|
||||||
|
/// <returns>True if the mode has been blacklisted, false otherwise.</returns>
|
||||||
|
public bool IsBlacklisted(AnalysisMode mode) => _blacklistedStates[(int)mode];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the blacklisted state for the specified analysis mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">The analysis mode to set.</param>
|
||||||
|
/// <param name="value">The blacklisted state to set.</param>
|
||||||
|
public void SetBlacklisted(AnalysisMode mode, bool value) => _blacklistedStates[(int)mode] = value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the analyzed states.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetStates()
|
||||||
|
{
|
||||||
|
Array.Clear(_analyzedStates);
|
||||||
|
Array.Clear(_blacklistedStates);
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,11 @@ public class QueuedEpisode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid EpisodeId { get; set; }
|
public Guid EpisodeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the state of the episode.
|
||||||
|
/// </summary>
|
||||||
|
public EpisodeState State => Plugin.Instance!.GetState(EpisodeId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the full path to episode.
|
/// Gets or sets the full path to episode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -32,6 +37,11 @@ public class QueuedEpisode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether an episode is Anime.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAnime { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the timestamp (in seconds) to stop searching for an introduction at.
|
/// Gets or sets the timestamp (in seconds) to stop searching for an introduction at.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -23,10 +23,9 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ILogger<Entrypoint> _logger;
|
private readonly ILogger<Entrypoint> _logger;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly object _pathRestrictionsLock = new();
|
|
||||||
private Timer _queueTimer;
|
private Timer _queueTimer;
|
||||||
private bool _analyzeAgain;
|
private bool _analyzeAgain;
|
||||||
private List<string> _pathRestrictions = new List<string>();
|
private HashSet<Guid> _seasonsToAnalyze = new HashSet<Guid>();
|
||||||
private static CancellationTokenSource? _cancellationTokenSource;
|
private static CancellationTokenSource? _cancellationTokenSource;
|
||||||
private static ManualResetEventSlim _autoTaskCompletEvent = new ManualResetEventSlim(false);
|
private static ManualResetEventSlim _autoTaskCompletEvent = new ManualResetEventSlim(false);
|
||||||
|
|
||||||
@ -140,7 +139,7 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't do anything if it's not a supported media type
|
// Don't do anything if it's not a supported media type
|
||||||
if (itemChangeEventArgs.Item is not Episode)
|
if (itemChangeEventArgs.Item is not Episode episode)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -150,10 +149,7 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_pathRestrictionsLock)
|
_seasonsToAnalyze.Add(episode.SeasonId);
|
||||||
{
|
|
||||||
_pathRestrictions.Add(itemChangeEventArgs.Item.ContainingFolderPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
StartTimer();
|
StartTimer();
|
||||||
}
|
}
|
||||||
@ -172,7 +168,7 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't do anything if it's not a supported media type
|
// Don't do anything if it's not a supported media type
|
||||||
if (itemChangeEventArgs.Item is not Episode)
|
if (itemChangeEventArgs.Item is not Episode episode)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,10 +178,7 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_pathRestrictionsLock)
|
_seasonsToAnalyze.Add(episode.SeasonId);
|
||||||
{
|
|
||||||
_pathRestrictions.Add(itemChangeEventArgs.Item.ContainingFolderPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
StartTimer();
|
StartTimer();
|
||||||
}
|
}
|
||||||
@ -255,7 +248,6 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
Plugin.Instance!.Configuration.PathRestrictions.Clear();
|
|
||||||
_cancellationTokenSource = null;
|
_cancellationTokenSource = null;
|
||||||
_autoTaskCompletEvent.Set();
|
_autoTaskCompletEvent.Set();
|
||||||
}
|
}
|
||||||
@ -271,15 +263,8 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
using (_cancellationTokenSource = new CancellationTokenSource())
|
using (_cancellationTokenSource = new CancellationTokenSource())
|
||||||
using (ScheduledTaskSemaphore.Acquire(-1, _cancellationTokenSource.Token))
|
using (ScheduledTaskSemaphore.Acquire(-1, _cancellationTokenSource.Token))
|
||||||
{
|
{
|
||||||
lock (_pathRestrictionsLock)
|
var seasonIds = new HashSet<Guid>(_seasonsToAnalyze);
|
||||||
{
|
_seasonsToAnalyze.Clear();
|
||||||
foreach (var path in _pathRestrictions)
|
|
||||||
{
|
|
||||||
Plugin.Instance!.Configuration.PathRestrictions.Add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
_pathRestrictions.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
_analyzeAgain = false;
|
_analyzeAgain = false;
|
||||||
var progress = new Progress<double>();
|
var progress = new Progress<double>();
|
||||||
@ -309,7 +294,7 @@ public class Entrypoint : IHostedService, IDisposable
|
|||||||
_loggerFactory,
|
_loggerFactory,
|
||||||
_libraryManager);
|
_libraryManager);
|
||||||
|
|
||||||
baseCreditAnalyzer.AnalyzeItems(progress, _cancellationTokenSource.Token);
|
baseCreditAnalyzer.AnalyzeItems(progress, _cancellationTokenSource.Token, seasonIds);
|
||||||
|
|
||||||
// New item detected, start timer again
|
// New item detected, start timer again
|
||||||
if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested)
|
if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested)
|
||||||
|
@ -646,7 +646,8 @@ public static class FFmpegWrapper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="episode">Episode.</param>
|
/// <param name="episode">Episode.</param>
|
||||||
/// <param name="mode">Analysis mode.</param>
|
/// <param name="mode">Analysis mode.</param>
|
||||||
private static string GetFingerprintCachePath(QueuedEpisode episode, AnalysisMode mode)
|
/// <returns>Path.</returns>
|
||||||
|
public static string GetFingerprintCachePath(QueuedEpisode episode, AnalysisMode mode)
|
||||||
{
|
{
|
||||||
var basePath = Path.Join(
|
var basePath = Path.Join(
|
||||||
Plugin.Instance!.FingerprintCachePath,
|
Plugin.Instance!.FingerprintCachePath,
|
||||||
|
@ -175,6 +175,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ConcurrentDictionary<Guid, List<QueuedEpisode>> QueuedMediaItems { get; } = new();
|
public ConcurrentDictionary<Guid, List<QueuedEpisode>> QueuedMediaItems { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all episode states.
|
||||||
|
/// </summary>
|
||||||
|
public ConcurrentDictionary<Guid, EpisodeState> EpisodeStates { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the total number of episodes in the queue.
|
/// Gets or sets the total number of episodes in the queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -316,6 +321,19 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
return commit;
|
return commit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Intro for this item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Item id.</param>
|
||||||
|
/// <param name="mode">Mode.</param>
|
||||||
|
/// <returns>Intro.</returns>
|
||||||
|
internal Intro GetIntroByMode(Guid id, AnalysisMode mode)
|
||||||
|
{
|
||||||
|
return mode == AnalysisMode.Introduction
|
||||||
|
? Instance!.Intros[id]
|
||||||
|
: Instance!.Credits[id];
|
||||||
|
}
|
||||||
|
|
||||||
internal BaseItem? GetItem(Guid id)
|
internal BaseItem? GetItem(Guid id)
|
||||||
{
|
{
|
||||||
return _libraryManager.GetItemById(id);
|
return _libraryManager.GetItemById(id);
|
||||||
@ -357,6 +375,13 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
return _itemRepository.GetChapters(item);
|
return _itemRepository.GetChapters(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the state for this item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Item ID.</param>
|
||||||
|
/// <returns>State of this item.</returns>
|
||||||
|
internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState());
|
||||||
|
|
||||||
internal void UpdateTimestamps(Dictionary<Guid, Intro> newTimestamps, AnalysisMode mode)
|
internal void UpdateTimestamps(Dictionary<Guid, Intro> newTimestamps, AnalysisMode mode)
|
||||||
{
|
{
|
||||||
foreach (var intro in newTimestamps)
|
foreach (var intro in newTimestamps)
|
||||||
|
@ -160,14 +160,6 @@ public class QueueManager
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Plugin.Instance!.Configuration.PathRestrictions.Count > 0)
|
|
||||||
{
|
|
||||||
if (!Plugin.Instance.Configuration.PathRestrictions.Contains(item.ContainingFolderPath))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueEpisode(episode);
|
QueueEpisode(episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,10 +168,7 @@ public class QueueManager
|
|||||||
|
|
||||||
private void QueueEpisode(Episode episode)
|
private void QueueEpisode(Episode episode)
|
||||||
{
|
{
|
||||||
if (Plugin.Instance is null)
|
var pluginInstance = Plugin.Instance ?? throw new InvalidOperationException("Plugin instance was null");
|
||||||
{
|
|
||||||
throw new InvalidOperationException("plugin instance was null");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(episode.Path))
|
if (string.IsNullOrEmpty(episode.Path))
|
||||||
{
|
{
|
||||||
@ -192,9 +181,13 @@ public class QueueManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allocate a new list for each new season
|
// Allocate a new list for each new season
|
||||||
_queuedEpisodes.TryAdd(episode.SeasonId, new List<QueuedEpisode>());
|
if (!_queuedEpisodes.TryGetValue(episode.SeasonId, out var seasonEpisodes))
|
||||||
|
{
|
||||||
|
seasonEpisodes = new List<QueuedEpisode>();
|
||||||
|
_queuedEpisodes[episode.SeasonId] = seasonEpisodes;
|
||||||
|
}
|
||||||
|
|
||||||
if (_queuedEpisodes[episode.SeasonId].Any(e => e.EpisodeId == episode.Id))
|
if (seasonEpisodes.Any(e => e.EpisodeId == episode.Id))
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"\"{Name}\" from series \"{Series}\" ({Id}) is already queued",
|
"\"{Name}\" from series \"{Series}\" ({Id}) is already queued",
|
||||||
@ -207,32 +200,26 @@ public class QueueManager
|
|||||||
// Limit analysis to the first X% of the episode and at most Y minutes.
|
// Limit analysis to the first X% of the episode and at most Y minutes.
|
||||||
// X and Y default to 25% and 10 minutes.
|
// X and Y default to 25% and 10 minutes.
|
||||||
var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds;
|
var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds;
|
||||||
var fingerprintDuration = duration;
|
var fingerprintDuration = Math.Min(
|
||||||
|
duration >= 5 * 60 ? duration * analysisPercent : duration,
|
||||||
if (fingerprintDuration >= 5 * 60)
|
60 * pluginInstance.Configuration.AnalysisLengthLimit);
|
||||||
{
|
|
||||||
fingerprintDuration *= analysisPercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
fingerprintDuration = Math.Min(
|
|
||||||
fingerprintDuration,
|
|
||||||
60 * Plugin.Instance.Configuration.AnalysisLengthLimit);
|
|
||||||
|
|
||||||
// Queue the episode for analysis
|
// Queue the episode for analysis
|
||||||
var maxCreditsDuration = Plugin.Instance.Configuration.MaximumCreditsDuration;
|
var maxCreditsDuration = pluginInstance.Configuration.MaximumCreditsDuration;
|
||||||
_queuedEpisodes[episode.SeasonId].Add(new QueuedEpisode
|
_queuedEpisodes[episode.SeasonId].Add(new QueuedEpisode
|
||||||
{
|
{
|
||||||
SeriesName = episode.SeriesName,
|
SeriesName = episode.SeriesName,
|
||||||
SeasonNumber = episode.AiredSeasonNumber ?? 0,
|
SeasonNumber = episode.AiredSeasonNumber ?? 0,
|
||||||
EpisodeId = episode.Id,
|
EpisodeId = episode.Id,
|
||||||
Name = episode.Name,
|
Name = episode.Name,
|
||||||
|
IsAnime = episode.GetInheritedTags().Contains("anime", StringComparer.OrdinalIgnoreCase),
|
||||||
Path = episode.Path,
|
Path = episode.Path,
|
||||||
Duration = Convert.ToInt32(duration),
|
Duration = Convert.ToInt32(duration),
|
||||||
IntroFingerprintEnd = Convert.ToInt32(fingerprintDuration),
|
IntroFingerprintEnd = Convert.ToInt32(fingerprintDuration),
|
||||||
CreditsFingerprintStart = Convert.ToInt32(duration - maxCreditsDuration),
|
CreditsFingerprintStart = Convert.ToInt32(duration - maxCreditsDuration),
|
||||||
});
|
});
|
||||||
|
|
||||||
Plugin.Instance.TotalQueued++;
|
pluginInstance.TotalQueued++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -246,10 +233,7 @@ public class QueueManager
|
|||||||
VerifyQueue(ReadOnlyCollection<QueuedEpisode> candidates, ReadOnlyCollection<AnalysisMode> modes)
|
VerifyQueue(ReadOnlyCollection<QueuedEpisode> candidates, ReadOnlyCollection<AnalysisMode> modes)
|
||||||
{
|
{
|
||||||
var verified = new List<QueuedEpisode>();
|
var verified = new List<QueuedEpisode>();
|
||||||
var reqModes = new List<AnalysisMode>();
|
var reqModes = new HashSet<AnalysisMode>();
|
||||||
|
|
||||||
var requiresIntroAnalysis = modes.Contains(AnalysisMode.Introduction);
|
|
||||||
var requiresCreditsAnalysis = modes.Contains(AnalysisMode.Credits);
|
|
||||||
|
|
||||||
foreach (var candidate in candidates)
|
foreach (var candidate in candidates)
|
||||||
{
|
{
|
||||||
@ -257,34 +241,44 @@ public class QueueManager
|
|||||||
{
|
{
|
||||||
var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId);
|
var path = Plugin.Instance!.GetItemPath(candidate.EpisodeId);
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
verified.Add(candidate);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (requiresIntroAnalysis && (!Plugin.Instance!.Intros.TryGetValue(candidate.EpisodeId, out var intro) || !intro.Valid))
|
verified.Add(candidate);
|
||||||
|
|
||||||
|
foreach (var mode in modes)
|
||||||
|
{
|
||||||
|
if (candidate.State.IsAnalyzed(mode) || candidate.State.IsBlacklisted(mode))
|
||||||
{
|
{
|
||||||
reqModes.Add(AnalysisMode.Introduction);
|
continue;
|
||||||
requiresIntroAnalysis = false; // No need to check again
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresCreditsAnalysis && (!Plugin.Instance!.Credits.TryGetValue(candidate.EpisodeId, out var credit) || !credit.Valid))
|
bool isAnalyzed = mode == AnalysisMode.Introduction
|
||||||
|
? Plugin.Instance!.Intros.ContainsKey(candidate.EpisodeId)
|
||||||
|
: Plugin.Instance!.Credits.ContainsKey(candidate.EpisodeId);
|
||||||
|
|
||||||
|
if (isAnalyzed)
|
||||||
{
|
{
|
||||||
reqModes.Add(AnalysisMode.Credits);
|
candidate.State.SetAnalyzed(mode, true);
|
||||||
requiresCreditsAnalysis = false; // No need to check again
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reqModes.Add(mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Skipping {Mode} analysis of {Name} ({Id}): {Exception}",
|
"Skipping analysis of {Name} ({Id}): {Exception}",
|
||||||
modes,
|
|
||||||
candidate.Name,
|
candidate.Name,
|
||||||
candidate.EpisodeId,
|
candidate.EpisodeId,
|
||||||
ex);
|
ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (verified.AsReadOnly(), reqModes.AsReadOnly());
|
return (verified.AsReadOnly(), reqModes.ToList().AsReadOnly());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@ -50,9 +51,11 @@ public class BaseItemAnalyzerTask
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="progress">Progress.</param>
|
/// <param name="progress">Progress.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <param name="seasonsToAnalyze">Season Ids to analyze.</param>
|
||||||
public void AnalyzeItems(
|
public void AnalyzeItems(
|
||||||
IProgress<double> progress,
|
IProgress<double> progress,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
HashSet<Guid>? seasonsToAnalyze = null)
|
||||||
{
|
{
|
||||||
var ffmpegValid = FFmpegWrapper.CheckFFmpegVersion();
|
var ffmpegValid = FFmpegWrapper.CheckFFmpegVersion();
|
||||||
// Assert that ffmpeg with chromaprint is installed
|
// Assert that ffmpeg with chromaprint is installed
|
||||||
@ -68,6 +71,13 @@ public class BaseItemAnalyzerTask
|
|||||||
|
|
||||||
var queue = queueManager.GetMediaItems();
|
var queue = queueManager.GetMediaItems();
|
||||||
|
|
||||||
|
// Filter the queue based on seasonsToAnalyze
|
||||||
|
if (seasonsToAnalyze != null && seasonsToAnalyze.Count > 0)
|
||||||
|
{
|
||||||
|
queue = queue.Where(kvp => seasonsToAnalyze.Contains(kvp.Key))
|
||||||
|
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value).AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
var totalQueued = 0;
|
var totalQueued = 0;
|
||||||
foreach (var kvp in queue)
|
foreach (var kvp in queue)
|
||||||
{
|
{
|
||||||
@ -194,6 +204,12 @@ public class BaseItemAnalyzerTask
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove from Blacklist
|
||||||
|
foreach (var item in items.Where(e => e.State.IsBlacklisted(mode)))
|
||||||
|
{
|
||||||
|
item.State.SetBlacklisted(mode, false);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}",
|
"[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}",
|
||||||
mode,
|
mode,
|
||||||
@ -204,14 +220,29 @@ public class BaseItemAnalyzerTask
|
|||||||
var analyzers = new Collection<IMediaFileAnalyzer>();
|
var analyzers = new Collection<IMediaFileAnalyzer>();
|
||||||
|
|
||||||
analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>()));
|
analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>()));
|
||||||
if (mode == AnalysisMode.Credits)
|
if (first.IsAnime)
|
||||||
{
|
{
|
||||||
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
if (Plugin.Instance!.Configuration.UseChromaprint)
|
||||||
}
|
{
|
||||||
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
||||||
|
}
|
||||||
|
|
||||||
if (Plugin.Instance!.Configuration.UseChromaprint)
|
if (mode == AnalysisMode.Credits)
|
||||||
|
{
|
||||||
|
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
if (mode == AnalysisMode.Credits)
|
||||||
|
{
|
||||||
|
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Plugin.Instance!.Configuration.UseChromaprint)
|
||||||
|
{
|
||||||
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use each analyzer to find skippable ranges in all media files, removing successfully
|
// Use each analyzer to find skippable ranges in all media files, removing successfully
|
||||||
@ -221,6 +252,13 @@ public class BaseItemAnalyzerTask
|
|||||||
items = analyzer.AnalyzeMediaFiles(items, mode, cancellationToken);
|
items = analyzer.AnalyzeMediaFiles(items, mode, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add items without intros/credits to blacklist.
|
||||||
|
foreach (var item in items.Where(e => !e.State.IsAnalyzed(mode)))
|
||||||
|
{
|
||||||
|
item.State.SetBlacklisted(mode, true);
|
||||||
|
totalItems -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
return totalItems;
|
return totalItems;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,6 @@ public class DetectCreditsTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Scheduled Task is starting");
|
_logger.LogInformation("Scheduled Task is starting");
|
||||||
|
|
||||||
Plugin.Instance!.Configuration.PathRestrictions.Clear();
|
|
||||||
var modes = new List<AnalysisMode> { AnalysisMode.Credits };
|
var modes = new List<AnalysisMode> { AnalysisMode.Credits };
|
||||||
|
|
||||||
var baseCreditAnalyzer = new BaseItemAnalyzerTask(
|
var baseCreditAnalyzer = new BaseItemAnalyzerTask(
|
||||||
|
@ -80,7 +80,6 @@ public class DetectIntrosCreditsTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Scheduled Task is starting");
|
_logger.LogInformation("Scheduled Task is starting");
|
||||||
|
|
||||||
Plugin.Instance!.Configuration.PathRestrictions.Clear();
|
|
||||||
var modes = new List<AnalysisMode> { AnalysisMode.Introduction, AnalysisMode.Credits };
|
var modes = new List<AnalysisMode> { AnalysisMode.Introduction, AnalysisMode.Credits };
|
||||||
|
|
||||||
var baseIntroAnalyzer = new BaseItemAnalyzerTask(
|
var baseIntroAnalyzer = new BaseItemAnalyzerTask(
|
||||||
|
@ -80,7 +80,6 @@ public class DetectIntrosTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Scheduled Task is starting");
|
_logger.LogInformation("Scheduled Task is starting");
|
||||||
|
|
||||||
Plugin.Instance!.Configuration.PathRestrictions.Clear();
|
|
||||||
var modes = new List<AnalysisMode> { AnalysisMode.Introduction };
|
var modes = new List<AnalysisMode> { AnalysisMode.Introduction };
|
||||||
|
|
||||||
var baseIntroAnalyzer = new BaseItemAnalyzerTask(
|
var baseIntroAnalyzer = new BaseItemAnalyzerTask(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user