2024-10-25 14:31:50 -04:00
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
// SPDX-License-Identifier: GPL-3.0-only.
2024-10-25 14:15:12 -04:00
2023-02-02 01:20:11 -06:00
using System ;
2024-04-20 21:12:04 +02:00
using System.Collections.Generic ;
2024-06-15 13:16:47 +02:00
using System.Linq ;
2023-02-02 01:20:11 -06:00
using System.Threading ;
using System.Threading.Tasks ;
2024-10-19 23:50:41 +02:00
using IntroSkipper.Analyzers ;
2024-11-21 15:42:55 +01:00
using IntroSkipper.Configuration ;
2024-10-19 23:50:41 +02:00
using IntroSkipper.Data ;
2024-11-21 15:42:55 +01:00
using IntroSkipper.Db ;
2024-10-19 23:50:41 +02:00
using IntroSkipper.Manager ;
2023-02-02 01:20:11 -06:00
using MediaBrowser.Controller.Library ;
using Microsoft.Extensions.Logging ;
2024-10-19 23:50:41 +02:00
namespace IntroSkipper.ScheduledTasks ;
2024-04-20 12:58:29 +02:00
2023-02-02 01:20:11 -06:00
/// <summary>
/// Common code shared by all media item analyzer tasks.
/// </summary>
2024-11-21 15:42:55 +01:00
/// <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 )
2023-02-02 01:20:11 -06:00
{
2024-11-21 15:42:55 +01:00
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 ( ) ;
2023-02-02 01:20:11 -06:00
/// <summary>
/// Analyze all media items on the server.
/// </summary>
2024-11-21 15:42:55 +01:00
/// <param name="progress">Progress reporter.</param>
2023-02-02 01:20:11 -06:00
/// <param name="cancellationToken">Cancellation token.</param>
2024-11-21 15:42:55 +01:00
/// <param name="seasonsToAnalyze">Season IDs to analyze.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task AnalyzeItemsAsync (
2023-02-02 01:20:11 -06:00
IProgress < double > progress ,
2024-06-15 13:16:47 +02:00
CancellationToken cancellationToken ,
2024-09-25 17:23:25 +02:00
IReadOnlyCollection < Guid > ? seasonsToAnalyze = null )
2023-02-02 01:20:11 -06:00
{
2024-03-04 08:58:15 -05:00
// Assert that ffmpeg with chromaprint is installed
2024-11-21 15:42:55 +01:00
if ( _config . WithChromaprint & & ! FFmpegWrapper . CheckFFmpegVersion ( ) )
2024-03-04 08:58:15 -05:00
{
throw new FingerprintException (
2024-11-21 15:42:55 +01:00
"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." ) ;
2024-03-04 08:58:15 -05:00
}
2024-11-21 15:42:55 +01:00
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 > ( )
] ;
2023-02-02 01:20:11 -06:00
var queueManager = new QueueManager (
_loggerFactory . CreateLogger < QueueManager > ( ) ,
_libraryManager ) ;
var queue = queueManager . GetMediaItems ( ) ;
2024-11-21 15:42:55 +01:00
if ( seasonsToAnalyze ? . Count > 0 )
2024-06-15 13:16:47 +02:00
{
2024-11-21 15:42:55 +01:00
queue = queue . Where ( kvp = > seasonsToAnalyze . Contains ( kvp . Key ) )
. ToDictionary ( kvp = > kvp . Key , kvp = > kvp . Value ) ;
2024-06-15 13:16:47 +02:00
}
2024-11-21 15:42:55 +01:00
int totalQueued = queue . Sum ( kvp = > kvp . Value . Count ) * modes . Count ;
2023-02-02 01:20:11 -06:00
if ( totalQueued = = 0 )
{
throw new FingerprintException (
2024-09-17 08:41:56 +02:00
"No libraries selected for analysis. Please visit the plugin settings to configure." ) ;
2023-02-02 01:20:11 -06:00
}
2024-11-21 15:42:55 +01:00
int totalProcessed = 0 ;
2024-04-20 12:58:29 +02:00
var options = new ParallelOptions
2023-02-02 01:20:11 -06:00
{
2024-11-21 15:42:55 +01:00
MaxDegreeOfParallelism = Math . Max ( 1 , _config . MaxParallelism ) ,
2024-10-19 22:49:47 +02:00
CancellationToken = cancellationToken
2023-02-02 01:20:11 -06:00
} ;
2024-10-19 22:49:47 +02:00
await Parallel . ForEachAsync ( queue , options , async ( season , ct ) = >
2023-02-02 01:20:11 -06:00
{
2024-11-21 15:42:55 +01:00
var updateMediaSegments = false ;
2023-02-02 01:20:11 -06:00
2024-11-21 15:42:55 +01:00
var ( episodes , requiredModes ) = queueManager . VerifyQueue ( season . Value , modes ) ;
2024-09-25 17:23:25 +02:00
if ( episodes . Count = = 0 )
2023-02-02 01:20:11 -06:00
{
return ;
}
try
{
2024-11-21 15:42:55 +01:00
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 ) ;
}
2023-02-02 01:20:11 -06:00
2024-11-21 15:42:55 +01:00
foreach ( var mode in requiredModes )
2024-04-20 21:12:04 +02:00
{
2024-11-21 15:42:55 +01:00
ct . ThrowIfCancellationRequested ( ) ;
int analyzed = await AnalyzeItemsAsync (
episodes ,
mode ,
ct ) . ConfigureAwait ( false ) ;
2024-04-20 21:12:04 +02:00
Interlocked . Add ( ref totalProcessed , analyzed ) ;
2024-11-21 15:42:55 +01:00
updateMediaSegments = analyzed > 0 | | updateMediaSegments ;
progress . Report ( ( double ) totalProcessed / totalQueued * 100 ) ;
2024-04-20 21:12:04 +02:00
}
2023-02-02 01:20:11 -06:00
}
2024-11-21 15:42:55 +01:00
catch ( OperationCanceledException )
2024-10-20 10:26:38 +02:00
{
2024-11-21 15:42:55 +01:00
_logger . LogInformation ( "Analysis was canceled." ) ;
2024-10-20 10:26:38 +02:00
}
2023-02-02 01:20:11 -06:00
catch ( FingerprintException ex )
{
2024-11-21 15:42:55 +01:00
_logger . LogWarning ( ex , "Fingerprint exception during analysis." ) ;
2023-02-02 01:20:11 -06:00
}
2024-10-20 10:26:38 +02:00
catch ( Exception ex )
{
2024-11-21 15:42:55 +01:00
_logger . LogError ( ex , "An unexpected error occurred during analysis." ) ;
2024-10-20 10:26:38 +02:00
throw ;
}
2023-02-02 01:20:11 -06:00
2024-11-21 15:42:55 +01:00
if ( _config . RebuildMediaSegments | | ( updateMediaSegments & & _config . UpdateMediaSegments ) )
2024-10-19 22:49:47 +02:00
{
await _mediaSegmentUpdateManager . UpdateMediaSegmentsAsync ( episodes , ct ) . ConfigureAwait ( false ) ;
}
} ) . ConfigureAwait ( false ) ;
2023-02-02 01:20:11 -06:00
2024-11-21 15:42:55 +01:00
Plugin . Instance ! . AnalyzeAgain = false ;
if ( _config . RebuildMediaSegments )
2023-02-02 01:20:11 -06:00
{
2024-11-21 15:42:55 +01:00
_logger . LogInformation ( "Regenerated media segments." ) ;
_config . RebuildMediaSegments = false ;
Plugin . Instance ! . SaveConfiguration ( ) ;
2023-02-02 01:20:11 -06:00
}
}
/// <summary>
/// Analyze a group of media items for skippable segments.
/// </summary>
/// <param name="items">Media items to analyze.</param>
2024-04-20 21:12:04 +02:00
/// <param name="mode">Analysis mode.</param>
2023-02-02 01:20:11 -06:00
/// <param name="cancellationToken">Cancellation token.</param>
2024-11-21 15:42:55 +01:00
/// <returns>Number of items successfully analyzed.</returns>
private async Task < int > AnalyzeItemsAsync (
2024-10-05 19:30:30 +02:00
IReadOnlyList < QueuedEpisode > items ,
2024-10-16 16:05:59 +02:00
AnalysisMode mode ,
2023-02-02 01:20:11 -06:00
CancellationToken cancellationToken )
{
2024-10-05 19:30:30 +02:00
var first = items [ 0 ] ;
2024-11-21 15:42:55 +01:00
if ( ! first . IsMovie & & first . SeasonNumber = = 0 & & ! _config . AnalyzeSeasonZero )
2023-02-02 01:20:11 -06:00
{
return 0 ;
}
2024-11-21 15:42:55 +01:00
// 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 ) ;
2023-02-02 01:20:11 -06:00
_logger . LogInformation (
2024-04-20 21:12:04 +02:00
"[Mode: {Mode}] Analyzing {Count} files from {Name} season {Season}" ,
mode ,
2023-02-02 01:20:11 -06:00
items . Count ,
first . SeriesName ,
first . SeasonNumber ) ;
2024-11-21 15:42:55 +01:00
// Create a list of analyzers to use for the current mode
var analyzers = new List < IMediaFileAnalyzer > ( ) ;
2024-11-02 18:17:22 +01:00
2024-11-06 10:30:00 +01:00
if ( action is AnalyzerAction . Chapter or AnalyzerAction . Default )
2024-09-10 18:08:42 +02:00
{
2024-11-02 18:17:22 +01:00
analyzers . Add ( new ChapterAnalyzer ( _loggerFactory . CreateLogger < ChapterAnalyzer > ( ) ) ) ;
}
2023-02-02 01:20:11 -06:00
2024-11-21 15:42:55 +01:00
if ( first . IsAnime & & _config . WithChromaprint & &
mode is not ( AnalysisMode . Recap or AnalysisMode . Preview ) & &
action is AnalyzerAction . Default or AnalyzerAction . Chromaprint )
2024-09-25 17:23:25 +02:00
{
analyzers . Add ( new ChromaprintAnalyzer ( _loggerFactory . CreateLogger < ChromaprintAnalyzer > ( ) ) ) ;
2024-06-15 13:16:47 +02:00
}
2024-09-25 17:23:25 +02:00
2024-11-21 15:42:55 +01:00
if ( mode is AnalysisMode . Credits & &
action is AnalyzerAction . Default or AnalyzerAction . BlackFrame )
2023-02-02 01:20:11 -06:00
{
2024-09-25 17:23:25 +02:00
analyzers . Add ( new BlackFrameAnalyzer ( _loggerFactory . CreateLogger < BlackFrameAnalyzer > ( ) ) ) ;
}
2024-06-15 13:16:47 +02:00
2024-11-21 15:42:55 +01:00
if ( ! first . IsAnime & & ! first . IsMovie & &
mode is not ( AnalysisMode . Recap or AnalysisMode . Preview ) & &
action is AnalyzerAction . Default or AnalyzerAction . Chromaprint )
2024-09-25 17:23:25 +02:00
{
analyzers . Add ( new ChromaprintAnalyzer ( _loggerFactory . CreateLogger < ChromaprintAnalyzer > ( ) ) ) ;
2023-02-02 01:20:11 -06:00
}
// Use each analyzer to find skippable ranges in all media files, removing successfully
// analyzed items from the queue.
foreach ( var analyzer in analyzers )
{
2024-10-20 10:26:38 +02:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2024-11-21 15:42:55 +01:00
items = await analyzer . AnalyzeMediaFiles ( items , mode , cancellationToken ) . ConfigureAwait ( false ) ;
2023-02-02 01:20:11 -06:00
}
2024-11-21 15:42:55 +01:00
// Set the episode IDs for the analyzed items
await Plugin . Instance ! . SetEpisodeIdsAsync ( first . SeasonId , mode , items . Select ( i = > i . EpisodeId ) ) . ConfigureAwait ( false ) ;
2024-06-15 13:16:47 +02:00
2024-11-21 15:42:55 +01:00
return items . Where ( i = > i . IsAnalyzed ) . Count ( ) ;
2023-02-02 01:20:11 -06:00
}
}