refactor item queue (#183)

Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
This commit is contained in:
rlauuzo 2024-06-15 13:16:47 +02:00 committed by GitHub
parent ddecb15a51
commit 9388f2a583
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 231 additions and 110 deletions

View File

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

View File

@ -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)
{ {

View File

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

View File

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

View File

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

View File

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

View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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