using System; using System.IO; 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; /// /// 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); } /// /// Registers event handler. /// /// Task. public Task RunAsync() { if (Plugin.Instance!.Configuration.AutoDetectIntros || Plugin.Instance!.Configuration.AutoDetectCredits) { _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"); var queueManager = new QueueManager(_loggerFactory.CreateLogger(), _libraryManager); 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 it's not a supported media type if (itemChangeEventArgs.Item is not Episode) { return; } if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual) { return; } StartTimer(); } /// /// Library item was modified. /// /// The sending entity. /// The . 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(); } /// /// TaskManager task ended. /// /// The sending entity. /// The . private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs) { var result = eventArgs.Result; if (result.Key != "RefreshLibrary") { return; } if (result.Status != TaskCompletionStatus.Completed) { return; } StartTimer(); } /// /// Start timer to debounce analyzing. /// private void StartTimer() { if (Plugin.Instance!.AnalyzerTaskIsRunning) { _analyzeAgain = true; // Items added during a scan will be included later. } else { _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"); Plugin.Instance!.AnalyzerTaskIsRunning = true; var progress = new Progress(); var cancellationToken = new CancellationToken(false); if (Plugin.Instance!.Configuration.AutoDetectIntros && Plugin.Instance!.Configuration.AutoDetectCredits) { // This is where we can optimize a single scan var baseIntroAnalyzer = new BaseItemAnalyzerTask( AnalysisMode.Introduction, _loggerFactory.CreateLogger(), _loggerFactory, _libraryManager); baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken); var baseCreditAnalyzer = new BaseItemAnalyzerTask( AnalysisMode.Credits, _loggerFactory.CreateLogger(), _loggerFactory, _libraryManager); baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken); } else if (Plugin.Instance!.Configuration.AutoDetectIntros) { var baseIntroAnalyzer = new BaseItemAnalyzerTask( AnalysisMode.Introduction, _loggerFactory.CreateLogger(), _loggerFactory, _libraryManager); baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken); } else if (Plugin.Instance!.Configuration.AutoDetectCredits) { var baseCreditAnalyzer = new BaseItemAnalyzerTask( AnalysisMode.Credits, _loggerFactory.CreateLogger(), _loggerFactory, _libraryManager); baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken); } Plugin.Instance!.AnalyzerTaskIsRunning = false; // New item detected, start timer again if (_analyzeAgain) { _logger.LogInformation("Analyzing ended, but we need to analyze again!"); _analyzeAgain = false; StartTimer(); } } /// /// Dispose. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Protected dispose. /// /// Dispose. protected virtual void Dispose(bool dispose) { if (!dispose) { _libraryManager.ItemAdded -= OnItemAdded; _libraryManager.ItemUpdated -= OnItemModified; _taskManager.TaskCompleted -= OnLibraryRefresh; _queueTimer.Dispose(); return; } } }