2022-11-24 00:43:23 -06:00
|
|
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Collections.ObjectModel;
|
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;
|
|
|
|
using MediaBrowser.Model.Entities;
|
2024-03-03 21:46:52 -05:00
|
|
|
using Microsoft.Extensions.Logging;
|
2022-11-24 00:43:23 -06:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Chapter name analyzer.
|
|
|
|
/// </summary>
|
|
|
|
public class ChapterAnalyzer : IMediaFileAnalyzer
|
|
|
|
{
|
|
|
|
private ILogger<ChapterAnalyzer> _logger;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="ChapterAnalyzer"/> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="logger">Logger.</param>
|
|
|
|
public ChapterAnalyzer(ILogger<ChapterAnalyzer> logger)
|
|
|
|
{
|
|
|
|
_logger = logger;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public ReadOnlyCollection<QueuedEpisode> AnalyzeMediaFiles(
|
|
|
|
ReadOnlyCollection<QueuedEpisode> analysisQueue,
|
|
|
|
AnalysisMode mode,
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
{
|
|
|
|
var skippableRanges = new Dictionary<Guid, Intro>();
|
2022-11-25 00:37:30 -06:00
|
|
|
|
2022-11-24 00:43:23 -06:00
|
|
|
var expression = mode == AnalysisMode.Introduction ?
|
|
|
|
Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern :
|
|
|
|
Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern;
|
|
|
|
|
2022-11-29 20:31:41 -06:00
|
|
|
if (string.IsNullOrWhiteSpace(expression))
|
|
|
|
{
|
|
|
|
return analysisQueue;
|
|
|
|
}
|
|
|
|
|
2022-11-24 00:43:23 -06:00
|
|
|
foreach (var episode in analysisQueue)
|
|
|
|
{
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
var skipRange = FindMatchingChapter(
|
2022-11-25 00:37:30 -06:00
|
|
|
episode,
|
2024-04-20 12:21:07 +02:00
|
|
|
new(Plugin.Instance.GetChapters(episode.EpisodeId)),
|
2022-11-24 00:43:23 -06:00
|
|
|
expression,
|
|
|
|
mode);
|
|
|
|
|
|
|
|
if (skipRange is null)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
skippableRanges.Add(episode.EpisodeId, skipRange);
|
|
|
|
}
|
|
|
|
|
2024-04-20 12:21:07 +02:00
|
|
|
Plugin.Instance.UpdateTimestamps(skippableRanges, mode);
|
2022-11-24 00:43:23 -06:00
|
|
|
|
2022-11-26 02:28:40 -06:00
|
|
|
return analysisQueue
|
|
|
|
.Where(x => !skippableRanges.ContainsKey(x.EpisodeId))
|
|
|
|
.ToList()
|
|
|
|
.AsReadOnly();
|
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>
|
|
|
|
public Intro? FindMatchingChapter(
|
2022-11-25 00:37:30 -06:00
|
|
|
QueuedEpisode episode,
|
2022-11-24 00:43:23 -06:00
|
|
|
Collection<ChapterInfo> chapters,
|
|
|
|
string expression,
|
|
|
|
AnalysisMode mode)
|
|
|
|
{
|
|
|
|
Intro? matchingChapter = null;
|
|
|
|
|
|
|
|
var config = Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration();
|
2022-11-25 00:37:30 -06:00
|
|
|
|
2022-11-24 00:43:23 -06:00
|
|
|
var minDuration = config.MinimumIntroDuration;
|
|
|
|
int maxDuration = mode == AnalysisMode.Introduction ?
|
|
|
|
config.MaximumIntroDuration :
|
2024-04-13 14:07:12 -04:00
|
|
|
config.MaximumCreditsDuration;
|
2022-11-24 00:43:23 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
if (chapters.Count == 0)
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-11-24 00:43:23 -06:00
|
|
|
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.
|
2022-11-25 00:37:30 -06:00
|
|
|
chapters.Add(new()
|
2022-11-24 00:43:23 -06:00
|
|
|
{
|
2022-11-25 00:37:30 -06:00
|
|
|
StartPositionTicks = TimeSpan.FromSeconds(episode.Duration).Ticks
|
2022-11-24 00:43:23 -06:00
|
|
|
});
|
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
// Check all chapters in reverse order, skipping the virtual chapter
|
|
|
|
for (int i = chapters.Count - 2; i >= 0; i--)
|
2022-11-25 00:37:30 -06:00
|
|
|
{
|
2024-03-06 10:15:03 -05:00
|
|
|
var current = chapters[i];
|
|
|
|
var next = chapters[i + 1];
|
2022-11-25 00:37:30 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
if (string.IsNullOrWhiteSpace(current.Name))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
2022-11-24 00:43:23 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
var currentRange = new TimeRange(
|
|
|
|
TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds,
|
|
|
|
TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds);
|
2022-11-25 00:37:30 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
var baseMessage = string.Format(
|
|
|
|
CultureInfo.InvariantCulture,
|
|
|
|
"{0}: Chapter \"{1}\" ({2} - {3})",
|
|
|
|
episode.Path,
|
|
|
|
current.Name,
|
|
|
|
currentRange.Start,
|
|
|
|
currentRange.End);
|
2022-11-24 00:43:23 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
if (currentRange.Duration < minDuration || currentRange.Duration > maxDuration)
|
|
|
|
{
|
|
|
|
_logger.LogTrace("{Base}: ignoring (invalid duration)", baseMessage);
|
|
|
|
continue;
|
|
|
|
}
|
2022-11-24 00:43:23 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
// 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));
|
2022-11-24 00:43:23 -06:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
if (!match)
|
|
|
|
{
|
|
|
|
_logger.LogTrace("{Base}: ignoring (does not match regular expression)", baseMessage);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
matchingChapter = new(episode.EpisodeId, currentRange);
|
|
|
|
_logger.LogTrace("{Base}: okay", baseMessage);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Check all chapters
|
|
|
|
for (int i = 0; i < chapters.Count - 1; i++)
|
2024-03-02 20:30:30 -05:00
|
|
|
{
|
2024-03-06 10:15:03 -05:00
|
|
|
var current = chapters[i];
|
|
|
|
var next = chapters[i + 1];
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(current.Name))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
var currentRange = new TimeRange(
|
|
|
|
TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds,
|
|
|
|
TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds);
|
|
|
|
|
|
|
|
var baseMessage = string.Format(
|
|
|
|
CultureInfo.InvariantCulture,
|
|
|
|
"{0}: Chapter \"{1}\" ({2} - {3})",
|
|
|
|
episode.Path,
|
|
|
|
current.Name,
|
|
|
|
currentRange.Start,
|
|
|
|
currentRange.End);
|
|
|
|
|
|
|
|
if (currentRange.Duration < minDuration || currentRange.Duration > maxDuration)
|
|
|
|
{
|
|
|
|
_logger.LogTrace("{Base}: ignoring (invalid duration)", baseMessage);
|
|
|
|
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,
|
2024-03-06 08:56:19 -05:00
|
|
|
expression,
|
|
|
|
RegexOptions.None,
|
|
|
|
TimeSpan.FromSeconds(1));
|
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
if (!match)
|
2024-03-06 08:56:19 -05:00
|
|
|
{
|
2024-03-06 10:15:03 -05:00
|
|
|
_logger.LogTrace("{Base}: ignoring (does not match regular expression)", baseMessage);
|
2024-03-06 08:56:19 -05:00
|
|
|
continue;
|
|
|
|
}
|
2024-03-04 17:50:12 -05:00
|
|
|
|
2024-03-06 10:15:03 -05:00
|
|
|
if (!string.IsNullOrWhiteSpace(next.Name))
|
|
|
|
{
|
|
|
|
// Check for possibility of overlapping keywords
|
|
|
|
var overlap = Regex.IsMatch(
|
|
|
|
next.Name,
|
|
|
|
expression,
|
|
|
|
RegexOptions.None,
|
|
|
|
TimeSpan.FromSeconds(1));
|
|
|
|
|
|
|
|
if (overlap)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
matchingChapter = new(episode.EpisodeId, currentRange);
|
|
|
|
_logger.LogTrace("{Base}: okay", baseMessage);
|
|
|
|
break;
|
|
|
|
}
|
2022-11-24 00:43:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return matchingChapter;
|
|
|
|
}
|
|
|
|
}
|