TwistedUmbrellaX 10ec9eff83 Prevent a possible runtime mistake
Technically, the instance is already validated by intros, but if they get skipped for some reason, the variable might lose its confidence. Automated code correction does things like that a lot.
2024-04-20 09:26:05 -04:00

355 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary>
/// Server entrypoint.
/// </summary>
public class Entrypoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager;
private readonly ITaskManager _taskManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<Entrypoint> _logger;
private readonly ILoggerFactory _loggerFactory;
private Timer _queueTimer;
private bool _analyzeAgain;
private static CancellationTokenSource? _cancellationTokenSource;
private static ManualResetEventSlim _autoTaskCompletEvent = new ManualResetEventSlim(false);
private QueueManager _queueManager;
/// <summary>
/// Initializes a new instance of the <see cref="Entrypoint"/> class.
/// </summary>
/// <param name="userManager">User manager.</param>
/// <param name="userViewManager">User view manager.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="taskManager">Task manager.</param>
/// <param name="logger">Logger.</param>
/// <param name="loggerFactory">Logger factory.</param>
public Entrypoint(
IUserManager userManager,
IUserViewManager userViewManager,
ILibraryManager libraryManager,
ITaskManager taskManager,
ILogger<Entrypoint> logger,
ILoggerFactory loggerFactory)
{
_userManager = userManager;
_userViewManager = userViewManager;
_libraryManager = libraryManager;
_taskManager = taskManager;
_logger = logger;
_loggerFactory = loggerFactory;
_queueTimer = new Timer(
OnTimerCallback,
null,
Timeout.InfiniteTimeSpan,
Timeout.InfiniteTimeSpan);
_queueManager = new QueueManager(
_loggerFactory.CreateLogger<QueueManager>(),
_libraryManager);
}
/// <summary>
/// Gets State of the automatic task.
/// </summary>
public static TaskState AutomaticTaskState
{
get
{
if (_cancellationTokenSource is not null)
{
return _cancellationTokenSource.IsCancellationRequested
? TaskState.Cancelling
: TaskState.Running;
}
return TaskState.Idle;
}
}
/// <summary>
/// Registers event handler.
/// </summary>
/// <returns>Task.</returns>
public Task RunAsync()
{
_libraryManager.ItemAdded += OnItemAdded;
_libraryManager.ItemUpdated += OnItemModified;
_taskManager.TaskCompleted += OnLibraryRefresh;
FFmpegWrapper.Logger = _logger;
try
{
// Enqueue all episodes at startup to ensure any FFmpeg errors appear as early as possible
_logger.LogInformation("Running startup enqueue");
_queueManager.GetMediaItems();
}
catch (Exception ex)
{
_logger.LogError("Unable to run startup enqueue: {Exception}", ex);
}
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 auto detection is disabled
if (!Plugin.Instance!.Configuration.AutoDetectIntros && !Plugin.Instance!.Configuration.AutoDetectCredits)
{
return;
}
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Episode episode)
{
return;
}
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
if (Entrypoint.AutomaticTaskState == TaskState.Running)
{
_queueManager.QueueEpisode(episode);
}
else
{
Plugin.Instance!.Configuration.PathRestrictions.Add(itemChangeEventArgs.Item.ContainingFolderPath);
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 auto detection is disabled
if (!Plugin.Instance!.Configuration.AutoDetectIntros && !Plugin.Instance!.Configuration.AutoDetectCredits)
{
return;
}
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Episode episode)
{
return;
}
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
if (Entrypoint.AutomaticTaskState == TaskState.Running)
{
_queueManager.QueueEpisode(episode);
}
else
{
Plugin.Instance!.Configuration.PathRestrictions.Add(itemChangeEventArgs.Item.ContainingFolderPath);
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)
{
// Don't do anything if auto detection is disabled
if (!Plugin.Instance!.Configuration.AutoDetectIntros && !Plugin.Instance!.Configuration.AutoDetectCredits)
{
return;
}
var result = eventArgs.Result;
if (result.Key != "RefreshLibrary")
{
return;
}
if (result.Status != TaskCompletionStatus.Completed)
{
return;
}
// Unless user initiated, this is likely an overlap
if (AutomaticTaskState == TaskState.Running)
{
return;
}
StartTimer();
}
/// <summary>
/// Start timer to debounce analyzing.
/// </summary>
private void StartTimer()
{
if (AutomaticTaskState == TaskState.Running)
{
_analyzeAgain = true; // Items added during a scan will be included later.
}
else if (ScheduledTaskSemaphore.CurrentCount > 0)
{
_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");
_autoTaskCompletEvent.Reset();
using (_cancellationTokenSource = new CancellationTokenSource())
{
var progress = new Progress<double>();
var cancellationToken = _cancellationTokenSource.Token;
var modes = new List<AnalysisMode>();
var tasklogger = _loggerFactory.CreateLogger("DefaultLogger");
if (Plugin.Instance!.Configuration.AutoDetectIntros && Plugin.Instance!.Configuration.AutoDetectCredits)
{
modes.Add(AnalysisMode.Introduction);
modes.Add(AnalysisMode.Credits);
tasklogger = _loggerFactory.CreateLogger<DetectIntrosCreditsTask>();
}
else if (Plugin.Instance!.Configuration.AutoDetectIntros)
{
modes.Add(AnalysisMode.Introduction);
tasklogger = _loggerFactory.CreateLogger<DetectIntrosTask>();
}
else if (Plugin.Instance!.Configuration.AutoDetectCredits)
{
modes.Add(AnalysisMode.Credits);
tasklogger = _loggerFactory.CreateLogger<DetectCreditsTask>();
}
var baseCreditAnalyzer = new BaseItemAnalyzerTask(
modes.AsReadOnly(),
tasklogger,
_loggerFactory,
_libraryManager);
baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken);
}
Plugin.Instance!.Configuration.PathRestrictions.Clear();
_autoTaskCompletEvent.Set();
_cancellationTokenSource = null;
// New item detected, start timer again
if (_analyzeAgain)
{
_logger.LogInformation("Analyzing ended, but we need to analyze again!");
_analyzeAgain = false;
StartTimer();
}
}
/// <summary>
/// Method to cancel the automatic task.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
public static void CancelAutomaticTask(CancellationToken cancellationToken)
{
if (_cancellationTokenSource != null)
{
if (!_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Cancel();
}
_autoTaskCompletEvent.Wait(TimeSpan.FromSeconds(60), cancellationToken); // Wait for the signal
}
}
/// <summary>
/// Dispose.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Protected dispose.
/// </summary>
/// <param name="dispose">Dispose.</param>
protected virtual void Dispose(bool dispose)
{
if (!dispose)
{
Plugin.Instance!.Configuration.PathRestrictions.Clear();
_libraryManager.ItemAdded -= OnItemAdded;
_libraryManager.ItemUpdated -= OnItemModified;
_taskManager.TaskCompleted -= OnLibraryRefresh;
if (_cancellationTokenSource != null) // Null Check
{
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
_queueTimer.Dispose();
return;
}
}
}