6ccf002e51
* Recaps and Previews Support * Add draft UI of preview / recap edit * remove intro/credit tasks * Update configPage.html * rename task * Reorganize settings by relation * More standardized formatting * Some additional formatting * fix a typo * Update configPage.html * Allow missing recap / prview data * More risk to corrupt than benefit * Update TimeStamps.cs * Update PluginConfiguration.cs * Update configPage.html * Update PluginConfiguration.cs * Add chapter regex to settings * Move all UI into UI section * Move ending seconds with similar * Add default * fixes * Update SkipIntroController.cs * Autoskip all segments * Check if adjacent segment * Update AutoSkip.cs * Update AutoSkip.cs * Settings apply to all segment types * Update SegmentProvider * Update configPage.html Whoops * Update Plugin.cs * Update AutoSkip.cs * Let’s call it missing instead * Update BaseItemAnalyzerTask.cs * Update BaseItemAnalyzerTask.cs * Update BaseItemAnalyzerTask.cs * Move "select" all below list * Clarify button wording * Update configPage.html * Nope, long client list will hide it * Simplify wording * Update QueuedEpisode.cs * fix unit test for ffmpeg7 * Add migration * Restore DataContract * update * Update configPage.html * remove analyzed status * Update AutoSkip.cs * Update configPage.html typo * Store analyzed items in seasoninfo * Update VisualizationController.cs * update * Update IntroSkipperDbContext.cs * Add preview / recap delete * This keeps changing itself * Update SkipIntroController.cs * Rather add it to be removed --------- Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com> Co-authored-by: TwistedUmbrellaX <1173913+AbandonedCart@users.noreply.github.com> Co-authored-by: Kilian von Pflugk <github@jumoog.io>
231 lines
8.8 KiB
C#
231 lines
8.8 KiB
C#
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
|
// SPDX-License-Identifier: GPL-3.0-only.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using IntroSkipper.Analyzers;
|
|
using IntroSkipper.Configuration;
|
|
using IntroSkipper.Data;
|
|
using IntroSkipper.Db;
|
|
using IntroSkipper.Manager;
|
|
using MediaBrowser.Controller.Library;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace IntroSkipper.ScheduledTasks;
|
|
|
|
/// <summary>
|
|
/// Common code shared by all media item analyzer tasks.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Initializes a new instance of the <see cref="BaseItemAnalyzerTask"/> class.
|
|
/// </remarks>
|
|
/// <param name="logger">Task logger.</param>
|
|
/// <param name="loggerFactory">Logger factory.</param>
|
|
/// <param name="libraryManager">Library manager.</param>
|
|
/// <param name="mediaSegmentUpdateManager">MediaSegmentUpdateManager.</param>
|
|
public class BaseItemAnalyzerTask(
|
|
ILogger logger,
|
|
ILoggerFactory loggerFactory,
|
|
ILibraryManager libraryManager,
|
|
MediaSegmentUpdateManager mediaSegmentUpdateManager)
|
|
{
|
|
private readonly ILogger _logger = logger;
|
|
private readonly ILoggerFactory _loggerFactory = loggerFactory;
|
|
private readonly ILibraryManager _libraryManager = libraryManager;
|
|
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager;
|
|
private readonly PluginConfiguration _config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
|
|
|
/// <summary>
|
|
/// Analyze all media items on the server.
|
|
/// </summary>
|
|
/// <param name="progress">Progress reporter.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <param name="seasonsToAnalyze">Season IDs to analyze.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
public async Task AnalyzeItemsAsync(
|
|
IProgress<double> progress,
|
|
CancellationToken cancellationToken,
|
|
IReadOnlyCollection<Guid>? seasonsToAnalyze = null)
|
|
{
|
|
// Assert that ffmpeg with chromaprint is installed
|
|
if (_config.WithChromaprint && !FFmpegWrapper.CheckFFmpegVersion())
|
|
{
|
|
throw new FingerprintException(
|
|
"Analysis terminated! Chromaprint is not enabled in the current ffmpeg. If Jellyfin is running natively, install jellyfin-ffmpeg7. If Jellyfin is running in a container, upgrade to version 10.10.0 or newer.");
|
|
}
|
|
|
|
HashSet<AnalysisMode> modes = [
|
|
.. _config.ScanIntroduction ? [AnalysisMode.Introduction] : Array.Empty<AnalysisMode>(),
|
|
.. _config.ScanCredits ? [AnalysisMode.Credits] : Array.Empty<AnalysisMode>(),
|
|
.. _config.ScanRecap ? [AnalysisMode.Recap] : Array.Empty<AnalysisMode>(),
|
|
.. _config.ScanPreview ? [AnalysisMode.Preview] : Array.Empty<AnalysisMode>()
|
|
];
|
|
|
|
var queueManager = new QueueManager(
|
|
_loggerFactory.CreateLogger<QueueManager>(),
|
|
_libraryManager);
|
|
|
|
var queue = queueManager.GetMediaItems();
|
|
|
|
if (seasonsToAnalyze?.Count > 0)
|
|
{
|
|
queue = queue.Where(kvp => seasonsToAnalyze.Contains(kvp.Key))
|
|
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
|
}
|
|
|
|
int totalQueued = queue.Sum(kvp => kvp.Value.Count) * modes.Count;
|
|
if (totalQueued == 0)
|
|
{
|
|
throw new FingerprintException(
|
|
"No libraries selected for analysis. Please visit the plugin settings to configure.");
|
|
}
|
|
|
|
int totalProcessed = 0;
|
|
var options = new ParallelOptions
|
|
{
|
|
MaxDegreeOfParallelism = Math.Max(1, _config.MaxParallelism),
|
|
CancellationToken = cancellationToken
|
|
};
|
|
|
|
await Parallel.ForEachAsync(queue, options, async (season, ct) =>
|
|
{
|
|
var updateMediaSegments = false;
|
|
|
|
var (episodes, requiredModes) = queueManager.VerifyQueue(season.Value, modes);
|
|
if (episodes.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var firstEpisode = episodes[0];
|
|
if (modes.Count != requiredModes.Count)
|
|
{
|
|
Interlocked.Add(ref totalProcessed, episodes.Count * (modes.Count - requiredModes.Count));
|
|
progress.Report((double)totalProcessed / totalQueued * 100);
|
|
}
|
|
|
|
foreach (var mode in requiredModes)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
int analyzed = await AnalyzeItemsAsync(
|
|
episodes,
|
|
mode,
|
|
ct).ConfigureAwait(false);
|
|
Interlocked.Add(ref totalProcessed, analyzed);
|
|
|
|
updateMediaSegments = analyzed > 0 || updateMediaSegments;
|
|
progress.Report((double)totalProcessed / totalQueued * 100);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Analysis was canceled.");
|
|
}
|
|
catch (FingerprintException ex)
|
|
{
|
|
_logger.LogWarning(ex, "Fingerprint exception during analysis.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "An unexpected error occurred during analysis.");
|
|
throw;
|
|
}
|
|
|
|
if (_config.RebuildMediaSegments || (updateMediaSegments && _config.UpdateMediaSegments))
|
|
{
|
|
await _mediaSegmentUpdateManager.UpdateMediaSegmentsAsync(episodes, ct).ConfigureAwait(false);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
Plugin.Instance!.AnalyzeAgain = false;
|
|
|
|
if (_config.RebuildMediaSegments)
|
|
{
|
|
_logger.LogInformation("Regenerated media segments.");
|
|
_config.RebuildMediaSegments = false;
|
|
Plugin.Instance!.SaveConfiguration();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyze a group of media items for skippable segments.
|
|
/// </summary>
|
|
/// <param name="items">Media items to analyze.</param>
|
|
/// <param name="mode">Analysis mode.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Number of items successfully analyzed.</returns>
|
|
private async Task<int> AnalyzeItemsAsync(
|
|
IReadOnlyList<QueuedEpisode> items,
|
|
AnalysisMode mode,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var first = items[0];
|
|
if (!first.IsMovie && first.SeasonNumber == 0 && !_config.AnalyzeSeasonZero)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Reset the IsAnalyzed flag for all items
|
|
foreach (var item in items)
|
|
{
|
|
item.IsAnalyzed = false;
|
|
}
|
|
|
|
// Get the analyzer action for the current mode
|
|
var action = Plugin.Instance!.GetAnalyzerAction(first.SeasonId, mode);
|
|
|
|
_logger.LogInformation(
|
|
"[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}",
|
|
mode,
|
|
items.Count,
|
|
first.SeriesName,
|
|
first.SeasonNumber);
|
|
|
|
// Create a list of analyzers to use for the current mode
|
|
var analyzers = new List<IMediaFileAnalyzer>();
|
|
|
|
if (action is AnalyzerAction.Chapter or AnalyzerAction.Default)
|
|
{
|
|
analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>()));
|
|
}
|
|
|
|
if (first.IsAnime && _config.WithChromaprint &&
|
|
mode is not (AnalysisMode.Recap or AnalysisMode.Preview) &&
|
|
action is AnalyzerAction.Default or AnalyzerAction.Chromaprint)
|
|
{
|
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
|
}
|
|
|
|
if (mode is AnalysisMode.Credits &&
|
|
action is AnalyzerAction.Default or AnalyzerAction.BlackFrame)
|
|
{
|
|
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
|
}
|
|
|
|
if (!first.IsAnime && !first.IsMovie &&
|
|
mode is not (AnalysisMode.Recap or AnalysisMode.Preview) &&
|
|
action is AnalyzerAction.Default or AnalyzerAction.Chromaprint)
|
|
{
|
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
|
}
|
|
|
|
// Use each analyzer to find skippable ranges in all media files, removing successfully
|
|
// analyzed items from the queue.
|
|
foreach (var analyzer in analyzers)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
items = await analyzer.AnalyzeMediaFiles(items, mode, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
// Set the episode IDs for the analyzed items
|
|
await Plugin.Instance!.SetEpisodeIdsAsync(first.SeasonId, mode, items.Select(i => i.EpisodeId)).ConfigureAwait(false);
|
|
|
|
return items.Where(i => i.IsAnalyzed).Count();
|
|
}
|
|
}
|