namespace ConfusedPolarBear.Plugin.IntroSkipper; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Extensions.Logging; using MediaBrowser.Model.Entities; /// /// Chapter name analyzer. /// public class ChapterAnalyzer : IMediaFileAnalyzer { private ILogger _logger; /// /// Initializes a new instance of the class. /// /// Logger. public ChapterAnalyzer(ILogger logger) { _logger = logger; } /// public ReadOnlyCollection AnalyzeMediaFiles( ReadOnlyCollection analysisQueue, AnalysisMode mode, CancellationToken cancellationToken) { var skippableRanges = new Dictionary(); var expression = mode == AnalysisMode.Introduction ? Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern : Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern; foreach (var episode in analysisQueue) { if (cancellationToken.IsCancellationRequested) { break; } var skipRange = FindMatchingChapter( episode.EpisodeId, episode.Duration, new(Plugin.Instance!.GetChapters(episode.EpisodeId)), expression, mode); if (skipRange is null) { continue; } skippableRanges.Add(episode.EpisodeId, skipRange); } Plugin.Instance!.UpdateTimestamps(skippableRanges, mode); return analysisQueue; } /// /// Searches a list of chapter names for one that matches the provided regular expression. /// Only public to allow for unit testing. /// /// Item id. /// Duration of media file in seconds. /// Media item chapters. /// Regular expression pattern. /// Analysis mode. /// Intro object containing skippable time range, or null if no chapter matched. public Intro? FindMatchingChapter( Guid id, int duration, Collection chapters, string expression, AnalysisMode mode) { Intro? matchingChapter = null; var config = Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration(); var minDuration = config.MinimumIntroDuration; int maxDuration = mode == AnalysisMode.Introduction ? config.MaximumIntroDuration : config.MaximumEpisodeCreditsDuration; if (mode == AnalysisMode.Credits) { // Since the ending credits chapter may be the last chapter in the file, append a virtual // chapter at the very end of the file. chapters.Add(new ChapterInfo() { StartPositionTicks = TimeSpan.FromSeconds(duration).Ticks }); } // Check all chapters for (int i = 0; i < chapters.Count - 1; i++) { // Calculate chapter position and duration var current = chapters[i]; var next = chapters[i + 1]; var currentRange = new TimeRange( TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds, TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds); // Skip chapters with that don't have a name or are too short/long if (string.IsNullOrEmpty(current.Name) || currentRange.Duration < minDuration || currentRange.Duration > maxDuration) { continue; } // Regex.IsMatch() is used here in order to allow the runtime to cache the compiled regex // between function invocations. var match = Regex.IsMatch( current.Name, expression, RegexOptions.None, TimeSpan.FromSeconds(1)); if (!match) { continue; } matchingChapter = new Intro(id, currentRange); break; } return matchingChapter; } }