// Copyright (C) 2024 Intro-Skipper contributors // SPDX-License-Identifier: GPL-3.0-only. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using IntroSkipper.Configuration; using IntroSkipper.Manager; using IntroSkipper.ScheduledTasks; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace IntroSkipper.Services { /// /// Server entrypoint. /// public sealed class Entrypoint : IHostedService, IDisposable { private readonly ITaskManager _taskManager; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager; private readonly HashSet _seasonsToAnalyze = []; private readonly Timer _queueTimer; private static readonly SemaphoreSlim _analysisSemaphore = new(1, 1); private PluginConfiguration _config; private bool _analyzeAgain; private static CancellationTokenSource? _cancellationTokenSource; /// /// Initializes a new instance of the class. /// /// Library manager. /// Task manager. /// Logger. /// Logger factory. /// MediaSegment Update Manager. public Entrypoint( ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger, ILoggerFactory loggerFactory, MediaSegmentUpdateManager mediaSegmentUpdateManager) { _libraryManager = libraryManager; _taskManager = taskManager; _logger = logger; _loggerFactory = loggerFactory; _mediaSegmentUpdateManager = mediaSegmentUpdateManager; _config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); _queueTimer = new Timer( OnTimerCallback, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } /// /// Gets State of the automatic task. /// public static TaskState AutomaticTaskState => _cancellationTokenSource switch { null => TaskState.Idle, { IsCancellationRequested: true } => TaskState.Cancelling, _ => TaskState.Running }; /// public Task StartAsync(CancellationToken cancellationToken) { _libraryManager.ItemAdded += OnItemChanged; _libraryManager.ItemUpdated += OnItemChanged; _taskManager.TaskCompleted += OnLibraryRefresh; Plugin.Instance!.ConfigurationChanged += OnSettingsChanged; FFmpegWrapper.Logger = _logger; // Enqueue all episodes at startup to ensure any FFmpeg errors appear as early as possible _logger.LogInformation("Running startup enqueue"); new QueueManager(_loggerFactory.CreateLogger(), _libraryManager).GetMediaItems(); return Task.CompletedTask; } /// public Task StopAsync(CancellationToken cancellationToken) { _libraryManager.ItemAdded -= OnItemChanged; _libraryManager.ItemUpdated -= OnItemChanged; _taskManager.TaskCompleted -= OnLibraryRefresh; Plugin.Instance!.ConfigurationChanged -= OnSettingsChanged; _queueTimer.Change(Timeout.Infinite, 0); return Task.CompletedTask; } /// /// Library item was added. /// /// The sending entity. /// The . private void OnItemChanged(object? sender, ItemChangeEventArgs itemChangeEventArgs) { if (_config.AutoDetectIntros && itemChangeEventArgs.Item is { LocationType: not LocationType.Virtual } item) { Guid? id = item is Episode episode ? episode.SeasonId : (item is Movie movie ? movie.Id : null); if (id.HasValue) { _seasonsToAnalyze.Add(id.Value); StartTimer(); } } } /// /// TaskManager task ended. /// /// The sending entity. /// The . private void OnLibraryRefresh(object? sender, TaskCompletionEventArgs eventArgs) { if (_config.AutoDetectIntros && eventArgs.Result is { Key: "RefreshLibrary", Status: TaskCompletionStatus.Completed } && AutomaticTaskState != TaskState.Running) { StartTimer(); } } private void OnSettingsChanged(object? sender, BasePluginConfiguration e) { _config = (PluginConfiguration)e; Plugin.Instance!.AnalyzeAgain = true; } /// /// Start timer to debounce analyzing. /// private void StartTimer() { if (AutomaticTaskState == TaskState.Running) { _analyzeAgain = true; } else if (AutomaticTaskState == TaskState.Idle) { _logger.LogDebug("Media Library changed, analyzis will start soon!"); _queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan); } } private void OnTimerCallback(object? state) => _ = RunAnalysisAsync(); private async Task RunAnalysisAsync() { try { await PerformAnalysisAsync().ConfigureAwait(false); } catch (OperationCanceledException) { _logger.LogInformation("Automatic Analysis task cancelled"); } catch (Exception ex) { _logger.LogError(ex, "Error in RunAnalysisAsync"); } _cancellationTokenSource = null; } private async Task PerformAnalysisAsync() { await _analysisSemaphore.WaitAsync().ConfigureAwait(false); try { using (_cancellationTokenSource = new CancellationTokenSource()) using (await ScheduledTaskSemaphore.AcquireAsync(_cancellationTokenSource.Token).ConfigureAwait(false)) { _logger.LogInformation("Initiating automatic analysis task"); var seasonIds = new HashSet(_seasonsToAnalyze); _seasonsToAnalyze.Clear(); _analyzeAgain = false; var analyzer = new BaseItemAnalyzerTask(_loggerFactory.CreateLogger(), _loggerFactory, _libraryManager, _mediaSegmentUpdateManager); await analyzer.AnalyzeItemsAsync(new Progress(), _cancellationTokenSource.Token, seasonIds).ConfigureAwait(false); if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested) { _logger.LogInformation("Analyzing ended, but we need to analyze again!"); _queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan); } } } finally { _analysisSemaphore.Release(); } } /// /// Method to cancel the automatic task. /// /// Cancellation token. /// A representing the asynchronous operation. public static async Task CancelAutomaticTaskAsync(CancellationToken cancellationToken) { if (_cancellationTokenSource is { IsCancellationRequested: false }) { try { await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); } catch (ObjectDisposedException) { _cancellationTokenSource = null; } } await _analysisSemaphore.WaitAsync(TimeSpan.FromSeconds(60), cancellationToken).ConfigureAwait(false); } /// public void Dispose() { _queueTimer.Dispose(); _cancellationTokenSource?.Dispose(); _analysisSemaphore.Dispose(); } } }