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;
///
/// Server entrypoint.
///
public class Entrypoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager;
private readonly ITaskManager _taskManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger _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;
///
/// Initializes a new instance of the class.
///
/// User manager.
/// User view manager.
/// Library manager.
/// Task manager.
/// Logger.
/// Logger factory.
public Entrypoint(
IUserManager userManager,
IUserViewManager userViewManager,
ILibraryManager libraryManager,
ITaskManager taskManager,
ILogger 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(),
_libraryManager);
}
///
/// Gets State of the automatic task.
///
public static TaskState AutomaticTaskState
{
get
{
if (_cancellationTokenSource is not null)
{
return _cancellationTokenSource.IsCancellationRequested
? TaskState.Cancelling
: TaskState.Running;
}
return TaskState.Idle;
}
}
///
/// Registers event handler.
///
/// Task.
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
///
/// Library item was added.
///
/// The sending entity.
/// The .
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();
}
}
///
/// Library item was modified.
///
/// The sending entity.
/// The .
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();
}
}
///
/// TaskManager task ended.
///
/// The sending entity.
/// The .
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();
}
///
/// Start timer to debounce analyzing.
///
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);
}
}
///
/// Wait for timer callback to be completed.
///
private void OnTimerCallback(object? state)
{
try
{
PerformAnalysis();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in PerformAnalysis");
}
}
///
/// Wait for timer to be completed.
///
private void PerformAnalysis()
{
_logger.LogInformation("Timer elapsed - start analyzing");
_autoTaskCompletEvent.Reset();
using (_cancellationTokenSource = new CancellationTokenSource())
{
var progress = new Progress();
var cancellationToken = _cancellationTokenSource.Token;
var modes = new List();
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();
}
else if (Plugin.Instance!.Configuration.AutoDetectIntros)
{
modes.Add(AnalysisMode.Introduction);
tasklogger = _loggerFactory.CreateLogger();
}
else if (Plugin.Instance!.Configuration.AutoDetectCredits)
{
modes.Add(AnalysisMode.Credits);
tasklogger = _loggerFactory.CreateLogger();
}
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();
}
}
///
/// Method to cancel the automatic task.
///
/// Cancellation token.
public static void CancelAutomaticTask(CancellationToken cancellationToken)
{
if (_cancellationTokenSource != null)
{
if (!_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Cancel();
}
_autoTaskCompletEvent.Wait(TimeSpan.FromSeconds(60), cancellationToken); // Wait for the signal
}
}
///
/// Dispose.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Protected dispose.
///
/// Dispose.
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;
}
}
}