analyze when item is added to the server (#96)

This commit is contained in:
rlauu 2024-03-29 16:58:16 +01:00 committed by Kilian von Pflugk
parent 78c05a7e29
commit ded3c3d43a
6 changed files with 263 additions and 3 deletions

View File

@ -27,6 +27,11 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public string SelectedLibraries { get; set; } = string.Empty; public string SelectedLibraries { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether to analyze automatically, when new Items are added.
/// </summary>
public bool AutomaticAnalysis { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to analyze season 0. /// Gets or sets a value indicating whether to analyze season 0.
/// </summary> /// </summary>

View File

@ -27,6 +27,17 @@
<fieldset class="verticalSection-extrabottompadding"> <fieldset class="verticalSection-extrabottompadding">
<legend>Analysis</legend> <legend>Analysis</legend>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AutomaticAnalysis" type="checkbox" is="emby-checkbox" />
<span>Automatic analysis</span>
</label>
<div class="fieldDescription">
If checked, newly added items will be automatically analyzed.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label"> <label class="emby-checkbox-label">
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" /> <input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
@ -586,6 +597,7 @@
] ]
var booleanConfigurationFields = [ var booleanConfigurationFields = [
"AutomaticAnalysis",
"AnalyzeSeasonZero", "AnalyzeSeasonZero",
"RegenerateEdlFiles", "RegenerateEdlFiles",
"UseChromaprint", "UseChromaprint",

View File

@ -1,8 +1,12 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper;
@ -14,9 +18,11 @@ public class Entrypoint : IServerEntryPoint
{ {
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager; private readonly IUserViewManager _userViewManager;
private readonly ITaskManager _taskManager;
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 Timer _queueTimer;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Entrypoint"/> class. /// Initializes a new instance of the <see cref="Entrypoint"/> class.
@ -24,20 +30,29 @@ public class Entrypoint : IServerEntryPoint
/// <param name="userManager">User manager.</param> /// <param name="userManager">User manager.</param>
/// <param name="userViewManager">User view manager.</param> /// <param name="userViewManager">User view manager.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="taskManager">Task manager.</param>
/// <param name="logger">Logger.</param> /// <param name="logger">Logger.</param>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
public Entrypoint( public Entrypoint(
IUserManager userManager, IUserManager userManager,
IUserViewManager userViewManager, IUserViewManager userViewManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
ITaskManager taskManager,
ILogger<Entrypoint> logger, ILogger<Entrypoint> logger,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_userManager = userManager; _userManager = userManager;
_userViewManager = userViewManager; _userViewManager = userViewManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_taskManager = taskManager;
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_queueTimer = new Timer(
OnTimerCallback,
null,
Timeout.InfiniteTimeSpan,
Timeout.InfiniteTimeSpan);
} }
/// <summary> /// <summary>
@ -46,10 +61,14 @@ public class Entrypoint : IServerEntryPoint
/// <returns>Task.</returns> /// <returns>Task.</returns>
public Task RunAsync() public Task RunAsync()
{ {
FFmpegWrapper.Logger = _logger; if (Plugin.Instance!.Configuration.AutomaticAnalysis)
{
_libraryManager.ItemAdded += OnItemAdded;
_libraryManager.ItemUpdated += OnItemModified;
_taskManager.TaskCompleted += OnLibraryRefresh;
}
// TODO: when a new item is added to the server, immediately analyze the season it belongs to FFmpegWrapper.Logger = _logger;
// instead of waiting for the next task interval. The task start should be debounced by a few seconds.
try try
{ {
@ -66,6 +85,137 @@ public class Entrypoint : IServerEntryPoint
return Task.CompletedTask; return Task.CompletedTask;
} }
// Disclose source for inspiration
// Implementation based on the principles of jellyfin-plugin-media-analyzer:
// https://github.com/endrl/jellyfin-plugin-media-analyzer
/// <summary>
/// Library item was added.
/// </summary>
/// <param name="sender">The sending entity.</param>
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private void OnItemAdded(object? sender, ItemChangeEventArgs itemChangeEventArgs)
{
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Episode)
{
return;
}
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
StartTimer();
}
/// <summary>
/// Library item was modified.
/// </summary>
/// <param name="sender">The sending entity.</param>
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private void OnItemModified(object? sender, ItemChangeEventArgs itemChangeEventArgs)
{
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Episode)
{
return;
}
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
StartTimer();
}
/// <summary>
/// TaskManager task ended.
/// </summary>
/// <param name="sender">The sending entity.</param>
/// <param name="eventArgs">The <see cref="TaskCompletionEventArgs"/>.</param>
private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs)
{
var result = eventArgs.Result;
if (result.Key != "RefreshLibrary")
{
return;
}
if (result.Status != TaskCompletionStatus.Completed)
{
return;
}
StartTimer();
}
/// <summary>
/// Start timer to debounce analyzing.
/// </summary>
private void StartTimer()
{
if (Plugin.Instance!.AnalyzerTaskIsRunning)
{
return; // Don't do anything if a Analyzer is running
}
else
{
_logger.LogInformation("Media Library changed, analyzis will start soon!");
_queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
}
}
/// <summary>
/// Wait for timer callback to be completed.
/// </summary>
private void OnTimerCallback(object? state)
{
try
{
PerformAnalysis();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in PerformAnalysis");
}
}
/// <summary>
/// Wait for timer to be completed.
/// </summary>
private void PerformAnalysis()
{
_logger.LogInformation("Timer elapsed - start analyzing");
Plugin.Instance!.AnalyzerTaskIsRunning = true;
var progress = new Progress<double>();
var cancellationToken = new CancellationToken(false);
// intro
var introductionAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Introduction,
_loggerFactory.CreateLogger<Entrypoint>(),
_loggerFactory,
_libraryManager);
introductionAnalyzer.AnalyzeItems(progress, cancellationToken);
// outro
var creditsAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Credits,
_loggerFactory.CreateLogger<Entrypoint>(),
_loggerFactory,
_libraryManager);
creditsAnalyzer.AnalyzeItems(progress, cancellationToken);
Plugin.Instance!.AnalyzerTaskIsRunning = false;
}
/// <summary> /// <summary>
/// Dispose. /// Dispose.
/// </summary> /// </summary>
@ -83,6 +233,12 @@ public class Entrypoint : IServerEntryPoint
{ {
if (!dispose) if (!dispose)
{ {
_libraryManager.ItemAdded -= OnItemAdded;
_libraryManager.ItemUpdated -= OnItemModified;
_taskManager.TaskCompleted -= OnLibraryRefresh;
_queueTimer.Dispose();
return; return;
} }
} }

View File

@ -144,6 +144,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// </summary> /// </summary>
public event EventHandler? AutoSkipChanged; public event EventHandler? AutoSkipChanged;
/// <summary>
/// Gets or sets a value indicating whether analysis is running.
/// </summary>
public bool AnalyzerTaskIsRunning { get; set; } = false;
/// <summary> /// <summary>
/// Gets the results of fingerprinting all episodes. /// Gets the results of fingerprinting all episodes.
/// </summary> /// </summary>

View File

@ -14,6 +14,8 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// </summary> /// </summary>
public class DetectCreditsTask : IScheduledTask public class DetectCreditsTask : IScheduledTask
{ {
private readonly ILogger<DetectCreditsTask> _logger;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -23,10 +25,13 @@ public class DetectCreditsTask : IScheduledTask
/// </summary> /// </summary>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
public DetectCreditsTask( public DetectCreditsTask(
ILogger<DetectCreditsTask> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
_logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
@ -64,6 +69,40 @@ public class DetectCreditsTask : IScheduledTask
throw new InvalidOperationException("Library manager was null"); throw new InvalidOperationException("Library manager was null");
} }
// Wait for running analyzer
if (Plugin.Instance!.AnalyzerTaskIsRunning)
{
_logger.LogInformation("Other running Analyzer Task detected. Wait...");
using (var timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan))
{
try
{
void DisposeTimerOnCancellation() => timer.Dispose();
cancellationToken.Register(DisposeTimerOnCancellation);
while (Plugin.Instance!.AnalyzerTaskIsRunning && !cancellationToken.IsCancellationRequested)
{
timer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan); // Adjust delay
}
}
catch (OperationCanceledException)
{
return Task.CompletedTask;
}
}
if (!cancellationToken.IsCancellationRequested) // Check cancellation again before logging
{
_logger.LogInformation("No other Task active. Run Analyzer Task");
}
else
{
_logger.LogInformation("Task was canceled");
return Task.CompletedTask;
}
}
Plugin.Instance!.AnalyzerTaskIsRunning = true;
var baseAnalyzer = new BaseItemAnalyzerTask( var baseAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Credits, AnalysisMode.Credits,
_loggerFactory.CreateLogger<DetectCreditsTask>(), _loggerFactory.CreateLogger<DetectCreditsTask>(),
@ -72,6 +111,8 @@ public class DetectCreditsTask : IScheduledTask
baseAnalyzer.AnalyzeItems(progress, cancellationToken); baseAnalyzer.AnalyzeItems(progress, cancellationToken);
Plugin.Instance!.AnalyzerTaskIsRunning = false;
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -13,6 +13,8 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// </summary> /// </summary>
public class DetectIntroductionsTask : IScheduledTask public class DetectIntroductionsTask : IScheduledTask
{ {
private readonly ILogger<DetectIntroductionsTask> _logger;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -22,10 +24,13 @@ public class DetectIntroductionsTask : IScheduledTask
/// </summary> /// </summary>
/// <param name="loggerFactory">Logger factory.</param> /// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param> /// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
public DetectIntroductionsTask( public DetectIntroductionsTask(
ILogger<DetectIntroductionsTask> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
_logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
@ -63,6 +68,40 @@ public class DetectIntroductionsTask : IScheduledTask
throw new InvalidOperationException("Library manager was null"); throw new InvalidOperationException("Library manager was null");
} }
// Wait for running analyzer
if (Plugin.Instance!.AnalyzerTaskIsRunning)
{
_logger.LogInformation("Other running Analyzer Task detected. Wait...");
using (var timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan))
{
try
{
void DisposeTimerOnCancellation() => timer.Dispose();
cancellationToken.Register(DisposeTimerOnCancellation);
while (Plugin.Instance!.AnalyzerTaskIsRunning && !cancellationToken.IsCancellationRequested)
{
timer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan); // Adjust delay
}
}
catch (OperationCanceledException)
{
return Task.CompletedTask;
}
}
if (!cancellationToken.IsCancellationRequested) // Check cancellation again before logging
{
_logger.LogInformation("No other Task active. Run Analyzer Task");
}
else
{
_logger.LogInformation("Task was canceled");
return Task.CompletedTask;
}
}
Plugin.Instance!.AnalyzerTaskIsRunning = true;
var baseAnalyzer = new BaseItemAnalyzerTask( var baseAnalyzer = new BaseItemAnalyzerTask(
AnalysisMode.Introduction, AnalysisMode.Introduction,
_loggerFactory.CreateLogger<DetectIntroductionsTask>(), _loggerFactory.CreateLogger<DetectIntroductionsTask>(),
@ -71,6 +110,8 @@ public class DetectIntroductionsTask : IScheduledTask
baseAnalyzer.AnalyzeItems(progress, cancellationToken); baseAnalyzer.AnalyzeItems(progress, cancellationToken);
Plugin.Instance!.AnalyzerTaskIsRunning = false;
return Task.CompletedTask; return Task.CompletedTask;
} }