Refactor BaseItemAnalyzerTask and add Edl support for credits (#123)

Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
Co-authored-by: TwistedUmbrellaX <twistedumbrella@gmail.com>
Co-authored-by: TwistedUmbrellaX <1173913+AbandonedCart@users.noreply.github.com>
This commit is contained in:
rlauuzo 2024-04-20 21:12:04 +02:00 committed by GitHub
parent 1394926a3c
commit 8e23df523b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 125 additions and 95 deletions

View File

@ -193,7 +193,7 @@
</option> </option>
<option value="Intro"> <option value="Intro">
Intro (show a skip button, *experimental*) Intro/Credit (show a skip button, *experimental*)
</option> </option>
<option value="Mute"> <option value="Mute">
@ -223,7 +223,7 @@
<div class="fieldDescription"> <div class="fieldDescription">
If checked, the plugin will <strong>overwrite all EDL files</strong> associated with If checked, the plugin will <strong>overwrite all EDL files</strong> associated with
your episodes with the currently discovered introduction timestamps and EDL action. your episodes with the currently discovered introduction/credit timestamps and EDL action.
</div> </div>
</div> </div>
</details> </details>

View File

@ -34,4 +34,9 @@ public enum EdlAction
/// Show a skip button. /// Show a skip button.
/// </summary> /// </summary>
Intro, Intro,
/// <summary>
/// Show a skip button.
/// </summary>
Credit,
} }

View File

@ -63,15 +63,12 @@ public static class EdlManager
{ {
var id = episode.EpisodeId; var id = episode.EpisodeId;
if (!Plugin.Instance.Intros.TryGetValue(id, out var intro)) bool hasIntro = Plugin.Instance!.Intros.TryGetValue(id, out var intro) && intro.Valid;
{ bool hasCredit = Plugin.Instance!.Credits.TryGetValue(id, out var credit) && credit.Valid;
_logger?.LogDebug("Episode {Id} did not have an introduction, skipping", id);
continue;
}
if (!intro.Valid) if (!hasIntro && !hasCredit)
{ {
_logger?.LogDebug("Episode {Id} did not have a valid introduction, skipping", id); _logger?.LogDebug("Episode {Id} has neither a valid intro nor credit, skipping", id);
continue; continue;
} }
@ -85,7 +82,31 @@ public static class EdlManager
continue; continue;
} }
File.WriteAllText(edlPath, intro.ToEdl(action)); var edlContent = string.Empty;
if (hasIntro)
{
edlContent += intro?.ToEdl(action);
}
if (hasCredit)
{
if (edlContent.Length > 0)
{
edlContent += Environment.NewLine;
}
if (action == EdlAction.Intro)
{
edlContent += credit?.ToEdl(EdlAction.Credit);
}
else
{
edlContent += credit?.ToEdl(action);
}
}
File.WriteAllText(edlPath, edlContent);
} }
} }

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -258,46 +260,34 @@ public class Entrypoint : IHostedService, IDisposable
var progress = new Progress<double>(); var progress = new Progress<double>();
var cancellationToken = _cancellationTokenSource.Token; var cancellationToken = _cancellationTokenSource.Token;
var modes = new List<AnalysisMode>();
var tasklogger = _loggerFactory.CreateLogger("DefaultLogger");
if (Plugin.Instance!.Configuration.AutoDetectIntros && Plugin.Instance.Configuration.AutoDetectCredits) if (Plugin.Instance!.Configuration.AutoDetectIntros && Plugin.Instance.Configuration.AutoDetectCredits)
{ {
// This is where we can optimize a single scan modes.Add(AnalysisMode.Introduction);
var baseIntroAnalyzer = new BaseItemAnalyzerTask( modes.Add(AnalysisMode.Credits);
AnalysisMode.Introduction, tasklogger = _loggerFactory.CreateLogger<DetectIntrosCreditsTask>();
_loggerFactory.CreateLogger<DetectIntrosCreditsTask>(),
_loggerFactory,
_libraryManager);
baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken);
var baseCreditAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Credits,
_loggerFactory.CreateLogger<DetectIntrosCreditsTask>(),
_loggerFactory,
_libraryManager);
baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken);
} }
else if (Plugin.Instance.Configuration.AutoDetectIntros) else if (Plugin.Instance.Configuration.AutoDetectIntros)
{ {
var baseIntroAnalyzer = new BaseItemAnalyzerTask( modes.Add(AnalysisMode.Introduction);
AnalysisMode.Introduction, tasklogger = _loggerFactory.CreateLogger<DetectIntrosTask>();
_loggerFactory.CreateLogger<DetectIntrosTask>(),
_loggerFactory,
_libraryManager);
baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken);
} }
else if (Plugin.Instance.Configuration.AutoDetectCredits) else if (Plugin.Instance.Configuration.AutoDetectCredits)
{ {
modes.Add(AnalysisMode.Credits);
tasklogger = _loggerFactory.CreateLogger<DetectCreditsTask>();
}
var baseCreditAnalyzer = new BaseItemAnalyzerTask( var baseCreditAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Credits, modes.AsReadOnly(),
_loggerFactory.CreateLogger<DetectCreditsTask>(), tasklogger,
_loggerFactory, _loggerFactory,
_libraryManager); _libraryManager);
baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken); baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken);
} }
}
Plugin.Instance.Configuration.PathRestrictions.Clear(); Plugin.Instance.Configuration.PathRestrictions.Clear();
_autoTaskCompletEvent.Set(); _autoTaskCompletEvent.Set();

View File

@ -222,45 +222,51 @@ public class QueueManager
/// This is done to ensure that we don't analyze items that were deleted between the call to GetMediaItems() and popping them from the queue. /// This is done to ensure that we don't analyze items that were deleted between the call to GetMediaItems() and popping them from the queue.
/// </summary> /// </summary>
/// <param name="candidates">Queued media items.</param> /// <param name="candidates">Queued media items.</param>
/// <param name="mode">Analysis mode.</param> /// <param name="modes">Analysis mode.</param>
/// <returns>Media items that have been verified to exist in Jellyfin and in storage.</returns> /// <returns>Media items that have been verified to exist in Jellyfin and in storage.</returns>
public (ReadOnlyCollection<QueuedEpisode> VerifiedItems, bool AnyUnanalyzed) public (ReadOnlyCollection<QueuedEpisode> VerifiedItems, ReadOnlyCollection<AnalysisMode> RequiredModes)
VerifyQueue(ReadOnlyCollection<QueuedEpisode> candidates, AnalysisMode mode) VerifyQueue(ReadOnlyCollection<QueuedEpisode> candidates, ReadOnlyCollection<AnalysisMode> modes)
{ {
var unanalyzed = false;
var verified = new List<QueuedEpisode>(); var verified = new List<QueuedEpisode>();
var reqModes = new List<AnalysisMode>();
var timestamps = mode == AnalysisMode.Introduction ? var requiresIntroAnalysis = modes.Contains(AnalysisMode.Introduction);
Plugin.Instance!.Intros : var requiresCreditsAnalysis = modes.Contains(AnalysisMode.Credits);
Plugin.Instance!.Credits;
foreach (var candidate in candidates) foreach (var candidate in candidates)
{ {
try try
{ {
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); verified.Add(candidate);
if (requiresIntroAnalysis && !Plugin.Instance!.Intros.ContainsKey(candidate.EpisodeId))
{
reqModes.Add(AnalysisMode.Introduction);
requiresIntroAnalysis = false; // No need to check again
} }
if (!timestamps.ContainsKey(candidate.EpisodeId)) if (requiresCreditsAnalysis && !Plugin.Instance!.Credits.ContainsKey(candidate.EpisodeId))
{ {
unanalyzed = true; reqModes.Add(AnalysisMode.Credits);
requiresCreditsAnalysis = false; // No need to check again
}
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug( _logger.LogDebug(
"Skipping {Mode} analysis of {Name} ({Id}): {Exception}", "Skipping {Mode} analysis of {Name} ({Id}): {Exception}",
mode, modes,
candidate.Name, candidate.Name,
candidate.EpisodeId, candidate.EpisodeId,
ex); ex);
} }
} }
return (verified.AsReadOnly(), unanalyzed); return (verified.AsReadOnly(), reqModes.AsReadOnly());
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,7 +13,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// </summary> /// </summary>
public class BaseItemAnalyzerTask public class BaseItemAnalyzerTask
{ {
private readonly AnalysisMode _analysisMode; private readonly ReadOnlyCollection<AnalysisMode> _analysisModes;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -23,22 +24,22 @@ public class BaseItemAnalyzerTask
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseItemAnalyzerTask"/> class. /// Initializes a new instance of the <see cref="BaseItemAnalyzerTask"/> class.
/// </summary> /// </summary>
/// <param name="mode">Analysis mode.</param> /// <param name="modes">Analysis mode.</param>
/// <param name="logger">Task logger.</param> /// <param name="logger">Task logger.</param>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
public BaseItemAnalyzerTask( public BaseItemAnalyzerTask(
AnalysisMode mode, ReadOnlyCollection<AnalysisMode> modes,
ILogger logger, ILogger logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
_analysisMode = mode; _analysisModes = modes;
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_libraryManager = libraryManager; _libraryManager = libraryManager;
if (mode == AnalysisMode.Introduction) if (Plugin.Instance!.Configuration.EdlAction != EdlAction.None)
{ {
EdlManager.Initialize(_logger); EdlManager.Initialize(_logger);
} }
@ -73,18 +74,21 @@ public class BaseItemAnalyzerTask
totalQueued += kvp.Value.Count; totalQueued += kvp.Value.Count;
} }
totalQueued *= _analysisModes.Count;
if (totalQueued == 0) if (totalQueued == 0)
{ {
throw new FingerprintException( throw new FingerprintException(
"No episodes to analyze. If you are limiting the list of libraries to analyze, check that all library names have been spelled correctly."); "No episodes to analyze. If you are limiting the list of libraries to analyze, check that all library names have been spelled correctly.");
} }
if (this._analysisMode == AnalysisMode.Introduction) if (Plugin.Instance!.Configuration.EdlAction != EdlAction.None)
{ {
EdlManager.LogConfiguration(); EdlManager.LogConfiguration();
} }
var totalProcessed = 0; var totalProcessed = 0;
var modeCount = _analysisModes.Count;
var options = new ParallelOptions var options = new ParallelOptions
{ {
MaxDegreeOfParallelism = Plugin.Instance.Configuration.MaxParallelism MaxDegreeOfParallelism = Plugin.Instance.Configuration.MaxParallelism
@ -96,27 +100,39 @@ public class BaseItemAnalyzerTask
// Since the first run of the task can run for multiple hours, ensure that none // Since the first run of the task can run for multiple hours, ensure that none
// of the current media items were deleted from Jellyfin since the task was started. // of the current media items were deleted from Jellyfin since the task was started.
var (episodes, unanalyzed) = queueManager.VerifyQueue( var (episodes, requiredModes) = queueManager.VerifyQueue(
season.Value.AsReadOnly(), season.Value.AsReadOnly(),
this._analysisMode); _analysisModes);
if (episodes.Count == 0) var episodeCount = episodes.Count;
if (episodeCount == 0)
{ {
return; return;
} }
var first = episodes[0]; var first = episodes[0];
var requiredModeCount = requiredModes.Count;
if (!unanalyzed) if (requiredModeCount == 0)
{ {
_logger.LogDebug( _logger.LogDebug(
"All episodes in {Name} season {Season} have already been analyzed", "All episodes in {Name} season {Season} have already been analyzed",
first.SeriesName, first.SeriesName,
first.SeasonNumber); first.SeasonNumber);
Interlocked.Add(ref totalProcessed, episodeCount * modeCount); // Update total Processed directly
progress.Report((totalProcessed * 100) / totalQueued);
return; return;
} }
if (modeCount != requiredModeCount)
{
Interlocked.Add(ref totalProcessed, episodeCount);
progress.Report((totalProcessed * 100) / totalQueued); // Partial analysis some modes have already been analyzed
}
try try
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
@ -124,10 +140,15 @@ public class BaseItemAnalyzerTask
return; return;
} }
var analyzed = AnalyzeItems(episodes, cancellationToken); foreach (AnalysisMode mode in requiredModes)
{
var analyzed = AnalyzeItems(episodes, mode, cancellationToken);
Interlocked.Add(ref totalProcessed, analyzed); Interlocked.Add(ref totalProcessed, analyzed);
writeEdl = analyzed > 0 || Plugin.Instance.Configuration.RegenerateEdlFiles; writeEdl = analyzed > 0 || Plugin.Instance.Configuration.RegenerateEdlFiles;
progress.Report((totalProcessed * 100) / totalQueued);
}
} }
catch (FingerprintException ex) catch (FingerprintException ex)
{ {
@ -138,27 +159,13 @@ public class BaseItemAnalyzerTask
ex); ex);
} }
if ( if (writeEdl && Plugin.Instance.Configuration.EdlAction != EdlAction.None)
writeEdl &&
Plugin.Instance.Configuration.EdlAction != EdlAction.None &&
_analysisMode == AnalysisMode.Introduction)
{ {
EdlManager.UpdateEDLFiles(episodes); EdlManager.UpdateEDLFiles(episodes);
} }
if (_analysisMode == AnalysisMode.Introduction)
{
progress.Report(((totalProcessed * 100) / totalQueued) / 2);
}
else
{
progress.Report((((totalProcessed * 100) / totalQueued) / 2) + 50);
}
}); });
if ( if (Plugin.Instance.Configuration.RegenerateEdlFiles)
_analysisMode == AnalysisMode.Introduction &&
Plugin.Instance.Configuration.RegenerateEdlFiles)
{ {
_logger.LogInformation("Turning EDL file regeneration flag off"); _logger.LogInformation("Turning EDL file regeneration flag off");
Plugin.Instance.Configuration.RegenerateEdlFiles = false; Plugin.Instance.Configuration.RegenerateEdlFiles = false;
@ -170,10 +177,12 @@ public class BaseItemAnalyzerTask
/// Analyze a group of media items for skippable segments. /// Analyze a group of media items for skippable segments.
/// </summary> /// </summary>
/// <param name="items">Media items to analyze.</param> /// <param name="items">Media items to analyze.</param>
/// <param name="mode">Analysis mode.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Number of items that were successfully analyzed.</returns> /// <returns>Number of items that were successfully analyzed.</returns>
private int AnalyzeItems( private int AnalyzeItems(
ReadOnlyCollection<QueuedEpisode> items, ReadOnlyCollection<QueuedEpisode> items,
AnalysisMode mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var totalItems = items.Count; var totalItems = items.Count;
@ -186,7 +195,8 @@ public class BaseItemAnalyzerTask
} }
_logger.LogInformation( _logger.LogInformation(
"Analyzing {Count} files from {Name} season {Season}", "[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}",
mode,
items.Count, items.Count,
first.SeriesName, first.SeriesName,
first.SeasonNumber); first.SeasonNumber);
@ -199,7 +209,7 @@ public class BaseItemAnalyzerTask
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>())); analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
} }
if (this._analysisMode == AnalysisMode.Credits) if (mode == AnalysisMode.Credits)
{ {
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>())); analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
} }
@ -208,7 +218,7 @@ public class BaseItemAnalyzerTask
// analyzed items from the queue. // analyzed items from the queue.
foreach (var analyzer in analyzers) foreach (var analyzer in analyzers)
{ {
items = analyzer.AnalyzeMediaFiles(items, this._analysisMode, cancellationToken); items = analyzer.AnalyzeMediaFiles(items, mode, cancellationToken);
} }
return totalItems; return totalItems;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -87,9 +88,10 @@ public class DetectCreditsTask : IScheduledTask
_logger.LogInformation("Scheduled Task is starting"); _logger.LogInformation("Scheduled Task is starting");
Plugin.Instance!.Configuration.PathRestrictions.Clear(); Plugin.Instance!.Configuration.PathRestrictions.Clear();
var modes = new List<AnalysisMode> { AnalysisMode.Credits };
var baseCreditAnalyzer = new BaseItemAnalyzerTask( var baseCreditAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Credits, modes.AsReadOnly(),
_loggerFactory.CreateLogger<DetectCreditsTask>(), _loggerFactory.CreateLogger<DetectCreditsTask>(),
_loggerFactory, _loggerFactory,
_libraryManager); _libraryManager);

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -86,23 +87,16 @@ public class DetectIntrosCreditsTask : IScheduledTask
_logger.LogInformation("Scheduled Task is starting"); _logger.LogInformation("Scheduled Task is starting");
Plugin.Instance!.Configuration.PathRestrictions.Clear(); Plugin.Instance!.Configuration.PathRestrictions.Clear();
var modes = new List<AnalysisMode> { AnalysisMode.Introduction, AnalysisMode.Credits };
var baseIntroAnalyzer = new BaseItemAnalyzerTask( var baseIntroAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Introduction, modes.AsReadOnly(),
_loggerFactory.CreateLogger<DetectIntrosCreditsTask>(), _loggerFactory.CreateLogger<DetectIntrosCreditsTask>(),
_loggerFactory, _loggerFactory,
_libraryManager); _libraryManager);
baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken); baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken);
var baseCreditAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Credits,
_loggerFactory.CreateLogger<DetectIntrosCreditsTask>(),
_loggerFactory,
_libraryManager);
baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken);
ScheduledTaskSemaphore.Release(); ScheduledTaskSemaphore.Release();
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -86,9 +87,10 @@ public class DetectIntrosTask : IScheduledTask
_logger.LogInformation("Scheduled Task is starting"); _logger.LogInformation("Scheduled Task is starting");
Plugin.Instance!.Configuration.PathRestrictions.Clear(); Plugin.Instance!.Configuration.PathRestrictions.Clear();
var modes = new List<AnalysisMode> { AnalysisMode.Introduction };
var baseIntroAnalyzer = new BaseItemAnalyzerTask( var baseIntroAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Introduction, modes.AsReadOnly(),
_loggerFactory.CreateLogger<DetectIntrosTask>(), _loggerFactory.CreateLogger<DetectIntrosTask>(),
_loggerFactory, _loggerFactory,
_libraryManager); _libraryManager);