Update MediaSegments directly (#350)

Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
Co-authored-by: Kilian von Pflugk <github@jumoog.io>
This commit is contained in:
rlauuzo 2024-10-19 22:49:47 +02:00 committed by GitHub
parent 8f7c63172f
commit 1a731e3acc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 285 additions and 272 deletions

View File

@ -68,6 +68,20 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary>
public bool WithChromaprint { get; set; } = true;
// ===== Media Segment handling =====
/// <summary>
/// Gets or sets a value indicating whether to update Media Segments.
/// </summary>
public bool UpdateMediaSegments { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether to regenerate all EDL files during the next scan.
/// By default, EDL files are only written for a season if the season had at least one newly analyzed episode.
/// If this is set, all EDL files will be regenerated and overwrite any existing EDL file.
/// </summary>
public bool RegenerateMediaSegments { get; set; } = true;
// ===== EDL handling =====
/// <summary>

View File

@ -153,6 +153,29 @@
</div>
</details>
<details id="mediasegment">
<summary>Jellyfin Mediasegment Generation</summary>
<br />
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="UpdateMediaSegments" type="checkbox" is="emby-checkbox" />
<span>Update Media Segments for Newly Added Files During Scan</span>
</label>
<div class="fieldDescription">Enable this option to update media segments for newly added files during a scan. <strong>Warning:</strong> This should be disabled if you're using media segment providers other than Intro Skipper.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="RegenerateMediaSegments" type="checkbox" is="emby-checkbox" />
<span>Regenerate All Media Segments on Next Scan</span>
</label>
<div class="fieldDescription">When enabled, this option will <strong>overwrite all existing media segments</strong> for your episodes with currently detected introduction and credit timestamps during the next scan.</div>
</div>
</details>
<details id="edl">
<summary>EDL File Generation</summary>
@ -671,7 +694,7 @@
"AutoSkipCreditsNotificationText",
];
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"];
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "RegenerateMediaSegments", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"];
// visualizer elements
var ignorelistSection = document.querySelector("div#ignorelistSection");

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using MediaBrowser.Controller;
using MediaBrowser.Model;
using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Manager
{
/// <summary>
/// Initializes a new instance of the <see cref="MediaSegmentUpdateManager" /> class.
/// </summary>
/// <param name="mediaSegmentManager">MediaSegmentManager.</param>
/// <param name="logger">logger.</param>
/// <param name="segmentProvider">segmentProvider.</param>
public class MediaSegmentUpdateManager(IMediaSegmentManager mediaSegmentManager, ILogger<MediaSegmentUpdateManager> logger, IMediaSegmentProvider segmentProvider)
{
private readonly IMediaSegmentManager _mediaSegmentManager = mediaSegmentManager;
private readonly ILogger<MediaSegmentUpdateManager> _logger = logger;
private readonly IMediaSegmentProvider _segmentProvider = segmentProvider;
private readonly string _name = Plugin.Instance!.Name;
/// <summary>
/// Updates all media items in a List.
/// </summary>
/// <param name="episodes">Queued media items.</param>
/// <param name="cancellationToken">CancellationToken.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task UpdateMediaSegmentsAsync(IReadOnlyList<QueuedEpisode> episodes, CancellationToken cancellationToken)
{
foreach (var episode in episodes)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var existingSegments = await _mediaSegmentManager.GetSegmentsAsync(episode.EpisodeId, null).ConfigureAwait(false);
var deleteTasks = existingSegments.Select(s => _mediaSegmentManager.DeleteSegmentAsync(s.Id));
await Task.WhenAll(deleteTasks).ConfigureAwait(false);
var newSegments = await _segmentProvider.GetMediaSegments(new MediaSegmentGenerationRequest { ItemId = episode.EpisodeId }, cancellationToken).ConfigureAwait(false);
if (newSegments.Count == 0)
{
_logger.LogDebug("No segments found for episode {EpisodeId}", episode.EpisodeId);
continue;
}
var createTasks = newSegments.Select(s => _mediaSegmentManager.CreateSegmentAsync(s, _name));
await Task.WhenAll(createTasks).ConfigureAwait(false);
_logger.LogDebug("Updated {SegmentCount} segments for episode {EpisodeId}", newSegments.Count, episode.EpisodeId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing episode {EpisodeId}", episode.EpisodeId);
}
}
}
}
}

View File

@ -1,3 +1,4 @@
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.Providers;
using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller;
@ -18,6 +19,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
serviceCollection.AddHostedService<AutoSkipCredits>();
serviceCollection.AddHostedService<Entrypoint>();
serviceCollection.AddSingleton<IMediaSegmentProvider, SegmentProvider>();
serviceCollection.AddSingleton<MediaSegmentUpdateManager>();
}
}
}

View File

@ -15,15 +15,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers
/// </summary>
public class SegmentProvider : IMediaSegmentProvider
{
private readonly long _remainingTicks;
/// <summary>
/// Initializes a new instance of the <see cref="SegmentProvider"/> class.
/// </summary>
public SegmentProvider()
{
_remainingTicks = TimeSpan.FromSeconds(Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2).Ticks;
}
private static long RemainingTicks => TimeSpan.FromSeconds(Plugin.Instance?.Configuration.RemainingSecondsOfIntro ?? 2).Ticks;
/// <inheritdoc/>
public string Name => Plugin.Instance!.Name;
@ -38,7 +30,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers
segments.Add(new MediaSegmentDto
{
StartTicks = TimeSpan.FromSeconds(introValue.Start).Ticks,
EndTicks = TimeSpan.FromSeconds(introValue.End).Ticks - _remainingTicks,
EndTicks = TimeSpan.FromSeconds(introValue.End).Ticks - RemainingTicks,
ItemId = request.ItemId,
Type = MediaSegmentType.Intro
});
@ -61,7 +53,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Providers
}
else
{
outroSegment.EndTicks = creditEndTicks - _remainingTicks;
outroSegment.EndTicks = creditEndTicks - RemainingTicks;
}
segments.Add(outroSegment);

View File

@ -18,12 +18,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
public class BaseItemAnalyzerTask
{
private readonly IReadOnlyCollection<AnalysisMode> _analysisModes;
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager;
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemAnalyzerTask"/> class.
@ -32,16 +30,19 @@ public class BaseItemAnalyzerTask
/// <param name="logger">Task logger.</param>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="mediaSegmentUpdateManager">MediaSegmentUpdateManager.</param>
public BaseItemAnalyzerTask(
IReadOnlyCollection<AnalysisMode> modes,
ILogger logger,
ILoggerFactory loggerFactory,
ILibraryManager libraryManager)
ILibraryManager libraryManager,
MediaSegmentUpdateManager mediaSegmentUpdateManager)
{
_analysisModes = modes;
_logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager;
_mediaSegmentUpdateManager = mediaSegmentUpdateManager;
if (Plugin.Instance!.Configuration.EdlAction != EdlAction.None)
{
@ -55,7 +56,8 @@ public class BaseItemAnalyzerTask
/// <param name="progress">Progress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="seasonsToAnalyze">Season Ids to analyze.</param>
public void AnalyzeItems(
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task AnalyzeItems(
IProgress<double> progress,
CancellationToken cancellationToken,
IReadOnlyCollection<Guid>? seasonsToAnalyze = null)
@ -95,12 +97,13 @@ public class BaseItemAnalyzerTask
var totalProcessed = 0;
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Plugin.Instance.Configuration.MaxParallelism
MaxDegreeOfParallelism = Plugin.Instance.Configuration.MaxParallelism,
CancellationToken = cancellationToken
};
Parallel.ForEach(queue, options, season =>
await Parallel.ForEachAsync(queue, options, async (season, ct) =>
{
var writeEdl = false;
var updateManagers = false;
// Since the first run of the task can run for multiple hours, ensure that none
// of the current media items were deleted from Jellyfin since the task was started.
@ -132,17 +135,17 @@ public class BaseItemAnalyzerTask
try
{
if (cancellationToken.IsCancellationRequested)
if (ct.IsCancellationRequested)
{
return;
}
foreach (AnalysisMode mode in requiredModes)
{
var analyzed = AnalyzeItems(episodes, mode, cancellationToken);
var analyzed = AnalyzeItems(episodes, mode, ct);
Interlocked.Add(ref totalProcessed, analyzed);
writeEdl = analyzed > 0 || Plugin.Instance.Configuration.RegenerateEdlFiles;
updateManagers = analyzed > 0 || updateManagers;
progress.Report(totalProcessed * 100 / totalQueued);
}
@ -156,15 +159,21 @@ public class BaseItemAnalyzerTask
ex);
}
if (writeEdl && Plugin.Instance.Configuration.EdlAction != EdlAction.None)
if (Plugin.Instance.Configuration.RegenerateMediaSegments || (updateManagers && Plugin.Instance.Configuration.UpdateMediaSegments))
{
await _mediaSegmentUpdateManager.UpdateMediaSegmentsAsync(episodes, ct).ConfigureAwait(false);
}
if (Plugin.Instance.Configuration.RegenerateEdlFiles || (updateManagers && Plugin.Instance.Configuration.EdlAction != EdlAction.None))
{
EdlManager.UpdateEDLFiles(episodes);
}
});
}).ConfigureAwait(false);
if (Plugin.Instance.Configuration.RegenerateEdlFiles)
if (Plugin.Instance.Configuration.RegenerateMediaSegments || Plugin.Instance.Configuration.RegenerateEdlFiles)
{
_logger.LogInformation("Turning EDL file regeneration flag off");
_logger.LogInformation("Turning Mediasegment/EDL file regeneration flag off");
Plugin.Instance.Configuration.RegenerateMediaSegments = false;
Plugin.Instance.Configuration.RegenerateEdlFiles = false;
Plugin.Instance.SaveConfiguration();
}
@ -182,7 +191,7 @@ public class BaseItemAnalyzerTask
AnalysisMode mode,
CancellationToken cancellationToken)
{
var totalItems = items.Count;
var totalItems = items.Count(e => !e.State.IsAnalyzed(mode));
// Only analyze specials (season 0) if the user has opted in.
var first = items[0];

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
@ -14,29 +15,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// Analyze all television episodes for credits.
/// TODO: analyze all media files.
/// </summary>
public class DetectCreditsTask : IScheduledTask
{
private readonly ILogger<DetectCreditsTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="DetectCreditsTask"/> class.
/// </summary>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
public DetectCreditsTask(
/// <remarks>
/// Initializes a new instance of the <see cref="DetectCreditsTask"/> class.
/// </remarks>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
/// <param name="mediaSegmentUpdateManager">MediaSegment Update Manager.</param>
public class DetectCreditsTask(
ILogger<DetectCreditsTask> logger,
ILoggerFactory loggerFactory,
ILibraryManager libraryManager)
{
_logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager;
}
ILibraryManager libraryManager,
MediaSegmentUpdateManager mediaSegmentUpdateManager) : IScheduledTask
{
private readonly ILogger<DetectCreditsTask> _logger = logger;
private readonly ILoggerFactory _loggerFactory = loggerFactory;
private readonly ILibraryManager _libraryManager = libraryManager;
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager;
/// <summary>
/// Gets the task name.
@ -64,7 +62,7 @@ public class DetectCreditsTask : IScheduledTask
/// <param name="progress">Task progress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task.</returns>
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
if (_libraryManager is null)
{
@ -75,10 +73,10 @@ public class DetectCreditsTask : IScheduledTask
if (Entrypoint.AutomaticTaskState == TaskState.Running || Entrypoint.AutomaticTaskState == TaskState.Cancelling)
{
_logger.LogInformation("Automatic Task is {0} and will be canceled.", Entrypoint.AutomaticTaskState);
Entrypoint.CancelAutomaticTask(cancellationToken);
await Entrypoint.CancelAutomaticTaskAsync(cancellationToken).ConfigureAwait(false);
}
using (ScheduledTaskSemaphore.Acquire(cancellationToken))
using (await ScheduledTaskSemaphore.AcquireAsync(cancellationToken).ConfigureAwait(false))
{
_logger.LogInformation("Scheduled Task is starting");
@ -88,11 +86,10 @@ public class DetectCreditsTask : IScheduledTask
modes,
_loggerFactory.CreateLogger<DetectCreditsTask>(),
_loggerFactory,
_libraryManager);
_libraryManager,
_mediaSegmentUpdateManager);
baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken);
return Task.CompletedTask;
await baseCreditAnalyzer.AnalyzeItems(progress, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
@ -13,29 +14,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// <summary>
/// Analyze all television episodes for introduction sequences.
/// </summary>
public class DetectIntrosCreditsTask : IScheduledTask
{
private readonly ILogger<DetectIntrosCreditsTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="DetectIntrosCreditsTask"/> class.
/// </summary>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
public DetectIntrosCreditsTask(
/// <remarks>
/// Initializes a new instance of the <see cref="DetectIntrosCreditsTask"/> class.
/// </remarks>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
/// <param name="mediaSegmentUpdateManager">MediaSegment Update Manager.</param>
public class DetectIntrosCreditsTask(
ILogger<DetectIntrosCreditsTask> logger,
ILoggerFactory loggerFactory,
ILibraryManager libraryManager)
{
_logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager;
}
ILibraryManager libraryManager,
MediaSegmentUpdateManager mediaSegmentUpdateManager) : IScheduledTask
{
private readonly ILogger<DetectIntrosCreditsTask> _logger = logger;
private readonly ILoggerFactory _loggerFactory = loggerFactory;
private readonly ILibraryManager _libraryManager = libraryManager;
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager;
/// <summary>
/// Gets the task name.
@ -63,7 +61,7 @@ public class DetectIntrosCreditsTask : IScheduledTask
/// <param name="progress">Task progress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task.</returns>
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
if (_libraryManager is null)
{
@ -74,10 +72,10 @@ public class DetectIntrosCreditsTask : IScheduledTask
if (Entrypoint.AutomaticTaskState == TaskState.Running || Entrypoint.AutomaticTaskState == TaskState.Cancelling)
{
_logger.LogInformation("Automatic Task is {0} and will be canceled.", Entrypoint.AutomaticTaskState);
Entrypoint.CancelAutomaticTask(cancellationToken);
await Entrypoint.CancelAutomaticTaskAsync(cancellationToken).ConfigureAwait(false);
}
using (ScheduledTaskSemaphore.Acquire(cancellationToken))
using (await ScheduledTaskSemaphore.AcquireAsync(cancellationToken).ConfigureAwait(false))
{
_logger.LogInformation("Scheduled Task is starting");
@ -87,11 +85,10 @@ public class DetectIntrosCreditsTask : IScheduledTask
modes,
_loggerFactory.CreateLogger<DetectIntrosCreditsTask>(),
_loggerFactory,
_libraryManager);
_libraryManager,
_mediaSegmentUpdateManager);
baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken);
return Task.CompletedTask;
await baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
@ -13,29 +14,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
/// <summary>
/// Analyze all television episodes for introduction sequences.
/// </summary>
public class DetectIntrosTask : IScheduledTask
{
private readonly ILogger<DetectIntrosTask> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="DetectIntrosTask"/> class.
/// </summary>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
public DetectIntrosTask(
/// <remarks>
/// Initializes a new instance of the <see cref="DetectIntrosTask"/> class.
/// </remarks>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
/// <param name="mediaSegmentUpdateManager">MediaSegment Update Manager.</param>
public class DetectIntrosTask(
ILogger<DetectIntrosTask> logger,
ILoggerFactory loggerFactory,
ILibraryManager libraryManager)
{
_logger = logger;
_loggerFactory = loggerFactory;
_libraryManager = libraryManager;
}
ILibraryManager libraryManager,
MediaSegmentUpdateManager mediaSegmentUpdateManager) : IScheduledTask
{
private readonly ILogger<DetectIntrosTask> _logger = logger;
private readonly ILoggerFactory _loggerFactory = loggerFactory;
private readonly ILibraryManager _libraryManager = libraryManager;
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager;
/// <summary>
/// Gets the task name.
@ -63,7 +61,7 @@ public class DetectIntrosTask : IScheduledTask
/// <param name="progress">Task progress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task.</returns>
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
if (_libraryManager is null)
{
@ -74,10 +72,10 @@ public class DetectIntrosTask : IScheduledTask
if (Entrypoint.AutomaticTaskState == TaskState.Running || Entrypoint.AutomaticTaskState == TaskState.Cancelling)
{
_logger.LogInformation("Automatic Task is {0} and will be canceled.", Entrypoint.AutomaticTaskState);
Entrypoint.CancelAutomaticTask(cancellationToken);
await Entrypoint.CancelAutomaticTaskAsync(cancellationToken).ConfigureAwait(false);
}
using (ScheduledTaskSemaphore.Acquire(cancellationToken))
using (await ScheduledTaskSemaphore.AcquireAsync(cancellationToken).ConfigureAwait(false))
{
_logger.LogInformation("Scheduled Task is starting");
@ -87,11 +85,10 @@ public class DetectIntrosTask : IScheduledTask
modes,
_loggerFactory.CreateLogger<DetectIntrosTask>(),
_loggerFactory,
_libraryManager);
_libraryManager,
_mediaSegmentUpdateManager);
baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken);
return Task.CompletedTask;
await baseIntroAnalyzer.AnalyzeItems(progress, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
@ -11,9 +12,9 @@ internal sealed class ScheduledTaskSemaphore : IDisposable
{
}
public static IDisposable Acquire(CancellationToken cancellationToken)
public static async Task<IDisposable> AcquireAsync(CancellationToken cancellationToken)
{
_semaphore.Wait(cancellationToken);
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
return new ScheduledTaskSemaphore();
}

View File

@ -6,6 +6,7 @@ using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using ConfusedPolarBear.Plugin.IntroSkipper.Manager;
using ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@ -25,9 +26,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
private readonly ILibraryManager _libraryManager;
private readonly ILogger<Entrypoint> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager;
private readonly HashSet<Guid> _seasonsToAnalyze = [];
private readonly Timer _queueTimer;
private static readonly ManualResetEventSlim _autoTaskCompletEvent = new(false);
private static readonly SemaphoreSlim _analysisSemaphore = new(1, 1);
private PluginConfiguration _config;
private bool _analyzeAgain;
private static CancellationTokenSource? _cancellationTokenSource;
@ -39,16 +41,19 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
/// <param name="taskManager">Task manager.</param>
/// <param name="logger">Logger.</param>
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="mediaSegmentUpdateManager">MediaSegment Update Manager.</param>
public Entrypoint(
ILibraryManager libraryManager,
ITaskManager taskManager,
ILogger<Entrypoint> logger,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
MediaSegmentUpdateManager mediaSegmentUpdateManager)
{
_libraryManager = libraryManager;
_taskManager = taskManager;
_logger = logger;
_loggerFactory = loggerFactory;
_mediaSegmentUpdateManager = mediaSegmentUpdateManager;
_config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
_queueTimer = new Timer(
@ -61,42 +66,26 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
/// <summary>
/// Gets State of the automatic task.
/// </summary>
public static TaskState AutomaticTaskState
public static TaskState AutomaticTaskState => _cancellationTokenSource switch
{
get
{
if (_cancellationTokenSource is not null)
{
return _cancellationTokenSource.IsCancellationRequested
? TaskState.Cancelling
: TaskState.Running;
}
return TaskState.Idle;
}
}
null => TaskState.Idle,
{ IsCancellationRequested: true } => TaskState.Cancelling,
_ => TaskState.Running
};
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
_libraryManager.ItemAdded += OnItemAdded;
_libraryManager.ItemUpdated += OnItemModified;
_libraryManager.ItemAdded += OnItemChanged;
_libraryManager.ItemUpdated += OnItemChanged;
_taskManager.TaskCompleted += OnLibraryRefresh;
Plugin.Instance!.ConfigurationChanged += OnSettingsChanged;
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<QueueManager>(), _libraryManager);
queueManager?.GetMediaItems();
}
catch (Exception ex)
{
_logger.LogError("Unable to run startup enqueue: {Exception}", ex);
}
new QueueManager(_loggerFactory.CreateLogger<QueueManager>(), _libraryManager).GetMediaItems();
return Task.CompletedTask;
}
@ -104,75 +93,33 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
_libraryManager.ItemAdded -= OnItemAdded;
_libraryManager.ItemUpdated -= OnItemModified;
_libraryManager.ItemAdded -= OnItemChanged;
_libraryManager.ItemUpdated -= OnItemChanged;
_taskManager.TaskCompleted -= OnLibraryRefresh;
Plugin.Instance!.ConfigurationChanged -= OnSettingsChanged;
// Stop the timer
_queueTimer.Change(Timeout.Infinite, 0);
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)
private void OnItemChanged(object? sender, ItemChangeEventArgs itemChangeEventArgs)
{
// Don't do anything if auto detection is disabled
if (!_config.AutoDetectIntros && !_config.AutoDetectCredits)
if ((_config.AutoDetectIntros || _config.AutoDetectCredits) &&
itemChangeEventArgs.Item is { LocationType: not LocationType.Virtual } item)
{
return;
}
Guid? id = item is Episode episode ? episode.SeasonId : (item is Movie movie ? movie.Id : null);
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Episode episode)
if (id.HasValue)
{
return;
}
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
_seasonsToAnalyze.Add(episode.SeasonId);
_seasonsToAnalyze.Add(id.Value);
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 (!_config.AutoDetectIntros && !_config.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;
}
_seasonsToAnalyze.Add(episode.SeasonId);
StartTimer();
}
/// <summary>
@ -182,32 +129,13 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
/// <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 (!_config.AutoDetectIntros && !_config.AutoDetectCredits)
if ((_config.AutoDetectIntros || _config.AutoDetectCredits) &&
eventArgs.Result is { Key: "RefreshLibrary", Status: TaskCompletionStatus.Completed } &&
AutomaticTaskState != TaskState.Running)
{
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();
}
}
private void OnSettingsChanged(object? sender, BasePluginConfiguration e) => _config = (PluginConfiguration)e;
@ -223,90 +151,80 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
else if (AutomaticTaskState == TaskState.Idle)
{
_logger.LogDebug("Media Library changed, analyzis will start soon!");
_queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
_queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan);
}
}
/// <summary>
/// Wait for timer callback to be completed.
/// </summary>
private void OnTimerCallback(object? state)
private void OnTimerCallback(object? state) =>
_ = RunAnalysisAsync();
private async Task RunAnalysisAsync()
{
try
{
PerformAnalysis();
await PerformAnalysisAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in PerformAnalysis");
_logger.LogError(ex, "Error in RunAnalysisAsync");
}
// Clean up
_cancellationTokenSource = null;
_autoTaskCompletEvent.Set();
}
/// <summary>
/// Wait for timer to be completed.
/// </summary>
private void PerformAnalysis()
private async Task PerformAnalysisAsync()
{
await _analysisSemaphore.WaitAsync().ConfigureAwait(false);
try
{
_logger.LogInformation("Initiate automatic analysis task.");
_autoTaskCompletEvent.Reset();
using (_cancellationTokenSource = new CancellationTokenSource())
using (ScheduledTaskSemaphore.Acquire(_cancellationTokenSource.Token))
using (await ScheduledTaskSemaphore.AcquireAsync(_cancellationTokenSource.Token).ConfigureAwait(false))
{
_logger.LogInformation("Initiating automatic analysis task");
var seasonIds = new HashSet<Guid>(_seasonsToAnalyze);
_seasonsToAnalyze.Clear();
_analyzeAgain = false;
var progress = new Progress<double>();
var modes = new List<AnalysisMode>();
var tasklogger = _loggerFactory.CreateLogger("DefaultLogger");
if (_config.AutoDetectIntros)
{
modes.Add(AnalysisMode.Introduction);
tasklogger = _loggerFactory.CreateLogger<DetectIntrosTask>();
}
if (_config.AutoDetectCredits)
{
modes.Add(AnalysisMode.Credits);
tasklogger = modes.Count == 2
? _loggerFactory.CreateLogger<DetectIntrosCreditsTask>()
: _loggerFactory.CreateLogger<DetectCreditsTask>();
}
var baseCreditAnalyzer = new BaseItemAnalyzerTask(
modes,
tasklogger,
_loggerFactory,
_libraryManager);
var analyzer = new BaseItemAnalyzerTask(modes, _loggerFactory.CreateLogger<Entrypoint>(), _loggerFactory, _libraryManager, _mediaSegmentUpdateManager);
await analyzer.AnalyzeItems(new Progress<double>(), _cancellationTokenSource.Token, seasonIds).ConfigureAwait(false);
baseCreditAnalyzer.AnalyzeItems(progress, _cancellationTokenSource.Token, seasonIds);
// New item detected, start timer again
if (_analyzeAgain && !_cancellationTokenSource.IsCancellationRequested)
{
_logger.LogInformation("Analyzing ended, but we need to analyze again!");
StartTimer();
_queueTimer.Change(TimeSpan.FromSeconds(60), Timeout.InfiniteTimeSpan);
}
}
}
finally
{
_analysisSemaphore.Release();
}
}
/// <summary>
/// Method to cancel the automatic task.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
public static void CancelAutomaticTask(CancellationToken cancellationToken)
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task CancelAutomaticTaskAsync(CancellationToken cancellationToken)
{
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
if (_cancellationTokenSource is { IsCancellationRequested: false })
{
try
{
_cancellationTokenSource.Cancel();
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
catch (ObjectDisposedException)
{
@ -314,7 +232,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
}
}
_autoTaskCompletEvent.Wait(TimeSpan.FromSeconds(60), cancellationToken); // Wait for the signal
await _analysisSemaphore.WaitAsync(TimeSpan.FromSeconds(60), cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
@ -322,7 +240,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Services
{
_queueTimer.Dispose();
_cancellationTokenSource?.Dispose();
_autoTaskCompletEvent.Dispose();
_analysisSemaphore.Dispose();
}
}
}