2022-11-24 00:43:23 -06:00
using System ;
using System.Collections.Generic ;
2022-11-25 00:37:30 -06:00
using System.Globalization ;
2022-11-26 02:28:40 -06:00
using System.Linq ;
2022-11-24 00:43:23 -06:00
using System.Text.RegularExpressions ;
using System.Threading ;
2024-10-19 23:50:41 +02:00
using IntroSkipper.Configuration ;
using IntroSkipper.Data ;
2022-11-24 00:43:23 -06:00
using MediaBrowser.Model.Entities ;
2024-03-03 21:46:52 -05:00
using Microsoft.Extensions.Logging ;
2022-11-24 00:43:23 -06:00
2024-10-19 23:50:41 +02:00
namespace IntroSkipper.Analyzers ;
2024-04-20 12:58:29 +02:00
2022-11-24 00:43:23 -06:00
/// <summary>
/// Chapter name analyzer.
/// </summary>
2024-09-21 18:06:11 +02:00
/// <remarks>
/// Initializes a new instance of the <see cref="ChapterAnalyzer"/> class.
/// </remarks>
/// <param name="logger">Logger.</param>
public class ChapterAnalyzer ( ILogger < ChapterAnalyzer > logger ) : IMediaFileAnalyzer
2022-11-24 00:43:23 -06:00
{
2024-09-21 18:06:11 +02:00
private ILogger < ChapterAnalyzer > _logger = logger ;
2022-11-24 00:43:23 -06:00
/// <inheritdoc />
2024-10-05 19:30:30 +02:00
public IReadOnlyList < QueuedEpisode > AnalyzeMediaFiles (
IReadOnlyList < QueuedEpisode > analysisQueue ,
2024-10-16 16:05:59 +02:00
AnalysisMode mode ,
2022-11-24 00:43:23 -06:00
CancellationToken cancellationToken )
{
2024-09-12 08:37:47 +00:00
var skippableRanges = new Dictionary < Guid , Segment > ( ) ;
2022-11-25 00:37:30 -06:00
2024-06-15 13:16:47 +02:00
// Episode analysis queue.
var episodeAnalysisQueue = new List < QueuedEpisode > ( analysisQueue ) ;
2024-10-16 16:05:59 +02:00
var expression = mode = = AnalysisMode . Introduction ?
2022-11-24 00:43:23 -06:00
Plugin . Instance ! . Configuration . ChapterAnalyzerIntroductionPattern :
Plugin . Instance ! . Configuration . ChapterAnalyzerEndCreditsPattern ;
2022-11-29 20:31:41 -06:00
if ( string . IsNullOrWhiteSpace ( expression ) )
{
return analysisQueue ;
}
2024-06-15 13:16:47 +02:00
foreach ( var episode in episodeAnalysisQueue . Where ( e = > ! e . State . IsAnalyzed ( mode ) ) )
2022-11-24 00:43:23 -06:00
{
if ( cancellationToken . IsCancellationRequested )
{
break ;
}
var skipRange = FindMatchingChapter (
2022-11-25 00:37:30 -06:00
episode ,
2024-10-09 18:33:41 +02:00
Plugin . Instance . GetChapters ( episode . EpisodeId ) ,
2022-11-24 00:43:23 -06:00
expression ,
mode ) ;
2024-10-20 13:35:33 +02:00
if ( skipRange is null | | ! skipRange . Valid )
2022-11-24 00:43:23 -06:00
{
continue ;
}
skippableRanges . Add ( episode . EpisodeId , skipRange ) ;
2024-06-15 13:16:47 +02:00
episode . State . SetAnalyzed ( mode , true ) ;
2022-11-24 00:43:23 -06:00
}
2024-04-20 12:21:07 +02:00
Plugin . Instance . UpdateTimestamps ( skippableRanges , mode ) ;
2022-11-24 00:43:23 -06:00
2024-09-21 18:06:11 +02:00
return episodeAnalysisQueue ;
2022-11-24 00:43:23 -06:00
}
/// <summary>
/// Searches a list of chapter names for one that matches the provided regular expression.
/// Only public to allow for unit testing.
/// </summary>
2022-11-25 00:37:30 -06:00
/// <param name="episode">Episode.</param>
2022-11-24 00:43:23 -06:00
/// <param name="chapters">Media item chapters.</param>
/// <param name="expression">Regular expression pattern.</param>
/// <param name="mode">Analysis mode.</param>
/// <returns>Intro object containing skippable time range, or null if no chapter matched.</returns>
2024-09-12 08:37:47 +00:00
public Segment ? FindMatchingChapter (
2022-11-25 00:37:30 -06:00
QueuedEpisode episode ,
2024-10-09 18:33:41 +02:00
IReadOnlyList < ChapterInfo > chapters ,
2022-11-24 00:43:23 -06:00
string expression ,
2024-10-16 16:05:59 +02:00
AnalysisMode mode )
2022-11-24 00:43:23 -06:00
{
2024-06-28 17:06:41 +02:00
var count = chapters . Count ;
if ( count = = 0 )
2024-03-06 10:15:03 -05:00
{
return null ;
}
2024-06-26 17:31:18 +02:00
var config = Plugin . Instance ? . Configuration ? ? new PluginConfiguration ( ) ;
2024-10-18 14:15:09 +02:00
var creditDuration = episode . IsMovie ? config . MaximumMovieCreditsDuration : config . MaximumCreditsDuration ;
2024-10-16 16:05:59 +02:00
var reversed = mode ! = AnalysisMode . Introduction ;
2024-06-28 17:06:41 +02:00
var ( minDuration , maxDuration ) = reversed
2024-10-18 14:15:09 +02:00
? ( config . MinimumCreditsDuration , creditDuration )
2024-06-28 17:06:41 +02:00
: ( config . MinimumIntroDuration , config . MaximumIntroDuration ) ;
2024-06-26 17:31:18 +02:00
// Check all chapters
for ( int i = reversed ? count - 1 : 0 ; reversed ? i > = 0 : i < count ; i + = reversed ? - 1 : 1 )
2022-11-24 00:43:23 -06:00
{
2024-06-26 17:31:18 +02:00
var chapter = chapters [ i ] ;
var next = chapters . ElementAtOrDefault ( i + 1 ) ? ?
new ChapterInfo { StartPositionTicks = TimeSpan . FromSeconds ( episode . Duration ) . Ticks } ; // Since the ending credits chapter may be the last chapter in the file, append a virtual chapter.
2022-11-24 00:43:23 -06:00
2024-06-26 17:31:18 +02:00
if ( string . IsNullOrWhiteSpace ( chapter . Name ) )
2022-11-25 00:37:30 -06:00
{
2024-06-26 17:31:18 +02:00
continue ;
}
2022-11-24 00:43:23 -06:00
2024-06-26 17:31:18 +02:00
var currentRange = new TimeRange (
TimeSpan . FromTicks ( chapter . StartPositionTicks ) . TotalSeconds ,
TimeSpan . FromTicks ( next . StartPositionTicks ) . TotalSeconds ) ;
2022-11-25 00:37:30 -06:00
2024-06-26 17:31:18 +02:00
var baseMessage = string . Format (
2024-03-06 10:15:03 -05:00
CultureInfo . InvariantCulture ,
"{0}: Chapter \"{1}\" ({2} - {3})" ,
episode . Path ,
2024-06-26 17:31:18 +02:00
chapter . Name ,
2024-03-06 10:15:03 -05:00
currentRange . Start ,
currentRange . End ) ;
2022-11-24 00:43:23 -06:00
2024-06-26 17:31:18 +02:00
if ( currentRange . Duration < minDuration | | currentRange . Duration > maxDuration )
2024-03-02 20:30:30 -05:00
{
2024-06-26 17:31:18 +02:00
_logger . LogTrace ( "{Base}: ignoring (invalid duration)" , baseMessage ) ;
continue ;
}
2024-03-06 10:15:03 -05:00
2024-06-26 17:31:18 +02:00
// Regex.IsMatch() is used here in order to allow the runtime to cache the compiled regex
// between function invocations.
var match = Regex . IsMatch (
chapter . Name ,
expression ,
RegexOptions . None ,
TimeSpan . FromSeconds ( 1 ) ) ;
2024-03-06 10:15:03 -05:00
2024-06-26 17:31:18 +02:00
if ( ! match )
{
_logger . LogTrace ( "{Base}: ignoring (does not match regular expression)" , baseMessage ) ;
continue ;
}
2024-03-06 10:15:03 -05:00
2024-06-26 17:31:18 +02:00
// Check if the next (or previous for Credits) chapter also matches
var adjacentChapter = reversed ? chapters . ElementAtOrDefault ( i - 1 ) : next ;
if ( adjacentChapter ! = null & & ! string . IsNullOrWhiteSpace ( adjacentChapter . Name ) )
{
// Check for possibility of overlapping keywords
var overlap = Regex . IsMatch (
adjacentChapter . Name ,
2024-03-06 08:56:19 -05:00
expression ,
RegexOptions . None ,
TimeSpan . FromSeconds ( 1 ) ) ;
2024-06-26 17:31:18 +02:00
if ( overlap )
2024-03-06 08:56:19 -05:00
{
2024-06-26 17:31:18 +02:00
_logger . LogTrace ( "{Base}: ignoring (adjacent chapter also matches)" , baseMessage ) ;
2024-03-06 08:56:19 -05:00
continue ;
}
2024-03-06 10:15:03 -05:00
}
2024-06-26 17:31:18 +02:00
_logger . LogTrace ( "{Base}: okay" , baseMessage ) ;
2024-09-12 08:37:47 +00:00
return new Segment ( episode . EpisodeId , currentRange ) ;
2022-11-24 00:43:23 -06:00
}
2024-06-26 17:31:18 +02:00
return null ;
2022-11-24 00:43:23 -06:00
}
}