Reformat code to comply with StyleCop analyzers

This commit is contained in:
ConfusedPolarBear 2022-05-09 22:50:41 -05:00
parent 928f467871
commit 547a2c705b
10 changed files with 149 additions and 106 deletions

View File

@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Configuration; namespace ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
@ -19,17 +15,17 @@ public class PluginConfiguration : BasePluginConfiguration
} }
/// <summary> /// <summary>
/// If the output of fpcalc should be cached to the filesystem. /// Gets or sets a value indicating whether the output of fpcalc should be cached to the filesystem.
/// </summary> /// </summary>
public bool CacheFingerprints { get; set; } public bool CacheFingerprints { get; set; }
/// <summary> /// <summary>
/// Seconds before the intro starts to show the skip prompt at. /// Gets or sets the seconds before the intro starts to show the skip prompt at.
/// </summary> /// </summary>
public int ShowPromptAdjustment { get; set; } = 5; public int ShowPromptAdjustment { get; set; } = 5;
/// <summary> /// <summary>
/// Seconds after the intro starts to hide the skip prompt at. /// Gets or sets the seconds after the intro starts to hide the skip prompt at.
/// </summary> /// </summary>
public int HidePromptAdjustment { get; set; } = 10; public int HidePromptAdjustment { get; set; } = 10;
} }

View File

@ -14,7 +14,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers;
public class SkipIntroController : ControllerBase public class SkipIntroController : ControllerBase
{ {
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="SkipIntroController"/> class.
/// </summary> /// </summary>
public SkipIntroController() public SkipIntroController()
{ {
@ -26,6 +26,7 @@ public class SkipIntroController : ControllerBase
/// <param name="id">ID of the episode. Required.</param> /// <param name="id">ID of the episode. Required.</param>
/// <response code="200">Episode contains an intro.</response> /// <response code="200">Episode contains an intro.</response>
/// <response code="404">Failed to find an intro in the provided episode.</response> /// <response code="404">Failed to find an intro in the provided episode.</response>
/// <returns>Detected intro.</returns>
[HttpGet("Episode/{id}/IntroTimestamps")] [HttpGet("Episode/{id}/IntroTimestamps")]
public ActionResult<Intro> GetIntroTimestamps([FromRoute] Guid id) public ActionResult<Intro> GetIntroTimestamps([FromRoute] Guid id)
{ {

View File

@ -5,25 +5,29 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary> /// <summary>
/// Exception raised when an error is encountered analyzing audio. /// Exception raised when an error is encountered analyzing audio.
/// </summary> /// </summary>
public class FingerprintException: Exception { public class FingerprintException : Exception
{
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="FingerprintException"/> class.
/// </summary> /// </summary>
public FingerprintException() public FingerprintException()
{ {
} }
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="FingerprintException"/> class.
/// </summary> /// </summary>
public FingerprintException(string message): base(message) /// <param name="message">Exception message.</param>
public FingerprintException(string message) : base(message)
{ {
} }
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="FingerprintException"/> class.
/// </summary> /// </summary>
public FingerprintException(string message, Exception inner): base(message, inner) /// <param name="message">Exception message.</param>
/// <param name="inner">Inner exception.</param>
public FingerprintException(string message, Exception inner) : base(message, inner)
{ {
} }
} }

View File

@ -6,34 +6,36 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// Result of fingerprinting and analyzing two episodes in a season. /// Result of fingerprinting and analyzing two episodes in a season.
/// All times are measured in seconds relative to the beginning of the media file. /// All times are measured in seconds relative to the beginning of the media file.
/// </summary> /// </summary>
public class Intro { public class Intro
{
/// <summary> /// <summary>
/// Episode ID. /// Gets or sets the Episode ID.
/// </summary> /// </summary>
public Guid EpisodeId { get; set; } public Guid EpisodeId { get; set; }
/// <summary> /// <summary>
/// If this introduction is valid or not. Invalid results should not be returned through the API. /// Gets or sets a value indicating whether this introduction is valid or not.
/// Invalid results must not be returned through the API.
/// </summary> /// </summary>
public bool Valid { get; set; } public bool Valid { get; set; }
/// <summary> /// <summary>
/// Introduction sequence start time. /// Gets or sets the introduction sequence start time.
/// </summary> /// </summary>
public double IntroStart { get; set; } public double IntroStart { get; set; }
/// <summary> /// <summary>
/// Introduction sequence end time. /// Gets or sets the introduction sequence end time.
/// </summary> /// </summary>
public double IntroEnd { get; set; } public double IntroEnd { get; set; }
/// <summary> /// <summary>
/// Recommended time to display the skip intro prompt. /// Gets or sets the recommended time to display the skip intro prompt.
/// </summary> /// </summary>
public double ShowSkipPromptAt { get; set; } public double ShowSkipPromptAt { get; set; }
/// <summary> /// <summary>
/// Recommended time to hide the skip intro prompt. /// Gets or sets the recommended time to hide the skip intro prompt.
/// </summary> /// </summary>
public double HideSkipPromptAt { get; set; } public double HideSkipPromptAt { get; set; }
} }

View File

@ -5,29 +5,30 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary> /// <summary>
/// Episode queued for analysis. /// Episode queued for analysis.
/// </summary> /// </summary>
public class QueuedEpisode { public class QueuedEpisode
{
/// <summary> /// <summary>
/// Series name. /// Gets or sets the series name.
/// </summary> /// </summary>
public string SeriesName { get; set; } = ""; public string SeriesName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Season number. /// Gets or sets the season number.
/// </summary> /// </summary>
public int SeasonNumber { get; set; } public int SeasonNumber { get; set; }
/// <summary> /// <summary>
/// Episode id. /// Gets or sets the episode id.
/// </summary> /// </summary>
public Guid EpisodeId { get; set; } public Guid EpisodeId { get; set; }
/// <summary> /// <summary>
/// Full path to episode. /// Gets or sets the full path to episode.
/// </summary> /// </summary>
public string Path { get; set; } = ""; public string Path { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Seconds of media file to fingerprint. /// Gets or sets the seconds of media file to fingerprint.
/// </summary> /// </summary>
public int FingerprintDuration { get; set; } public int FingerprintDuration { get; set; }
} }

View File

@ -9,22 +9,22 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
public class TimeRange : IComparable public class TimeRange : IComparable
{ {
/// <summary> /// <summary>
/// Time range start (in seconds). /// Gets or sets the time range start (in seconds).
/// </summary> /// </summary>
public double Start { get; set; } public double Start { get; set; }
/// <summary> /// <summary>
/// Time range end (in seconds). /// Gets or sets the time range end (in seconds).
/// </summary> /// </summary>
public double End { get; set; } public double End { get; set; }
/// <summary> /// <summary>
/// Duration of this time range (in seconds). /// Gets the duration of this time range (in seconds).
/// </summary> /// </summary>
public double Duration => End - Start; public double Duration => End - Start;
/// <summary> /// <summary>
/// Default constructor. /// Initializes a new instance of the <see cref="TimeRange"/> class.
/// </summary> /// </summary>
public TimeRange() public TimeRange()
{ {
@ -33,8 +33,10 @@ public class TimeRange : IComparable
} }
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="TimeRange"/> class.
/// </summary> /// </summary>
/// <param name="start">Time range start.</param>
/// <param name="end">Time range end.</param>
public TimeRange(double start, double end) public TimeRange(double start, double end)
{ {
Start = start; Start = start;
@ -42,8 +44,9 @@ public class TimeRange : IComparable
} }
/// <summary> /// <summary>
/// Copy constructor. /// Initializes a new instance of the <see cref="TimeRange"/> class.
/// </summary> /// </summary>
/// <param name="original">Original TimeRange.</param>
public TimeRange(TimeRange original) public TimeRange(TimeRange original)
{ {
Start = original.Start; Start = original.Start;
@ -54,6 +57,7 @@ public class TimeRange : IComparable
/// Compares this TimeRange to another TimeRange. /// Compares this TimeRange to another TimeRange.
/// </summary> /// </summary>
/// <param name="obj">Other object to compare against.</param> /// <param name="obj">Other object to compare against.</param>
/// <returns>A signed integer that indicates whether this instance precedes, follows, or appears in the same position in the sort order as the obj parameter.</returns>
public int CompareTo(object? obj) public int CompareTo(object? obj)
{ {
if (obj is not TimeRange tr) if (obj is not TimeRange tr)
@ -128,6 +132,7 @@ public static class TimeRangeHelpers
/// </summary> /// </summary>
/// <param name="times">Sorted timestamps to search.</param> /// <param name="times">Sorted timestamps to search.</param>
/// <param name="maximumDistance">Maximum distance permitted between contiguous timestamps.</param> /// <param name="maximumDistance">Maximum distance permitted between contiguous timestamps.</param>
/// <returns>The longest contiguous time range (if one was found), or null (if none was found).</returns>
public static TimeRange? FindContiguous(double[] times, double maximumDistance) public static TimeRange? FindContiguous(double[] times, double maximumDistance)
{ {
if (times.Length == 0) if (times.Length == 0)

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -9,7 +9,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library; using MediaBrowser.Model.Library;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Jellyfin.Data.Enums;
namespace ConfusedPolarBear.Plugin.IntroSkipper; namespace ConfusedPolarBear.Plugin.IntroSkipper;
@ -26,7 +25,7 @@ public class Entrypoint : IServerEntryPoint
private readonly object _queueLock = new object(); private readonly object _queueLock = new object();
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="Entrypoint"/> class.
/// </summary> /// </summary>
/// <param name="userManager">User manager.</param> /// <param name="userManager">User manager.</param>
/// <param name="userViewManager">User view manager.</param> /// <param name="userViewManager">User view manager.</param>
@ -47,12 +46,14 @@ public class Entrypoint : IServerEntryPoint
/// <summary> /// <summary>
/// Registers event handler. /// Registers event handler.
/// </summary> /// </summary>
/// <returns>Task.</returns>
public Task RunAsync() public Task RunAsync()
{ {
FPCalc.Logger = _logger; FPCalc.Logger = _logger;
// Assert that fpcalc is installed // Assert that fpcalc is installed
if (!FPCalc.CheckFPCalcInstalled()) { if (!FPCalc.CheckFPCalcInstalled())
{
_logger.LogError("fpcalc is not installed on this system - episodes will not be analyzed"); _logger.LogError("fpcalc is not installed on this system - episodes will not be analyzed");
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -61,8 +62,10 @@ public class Entrypoint : IServerEntryPoint
_libraryManager.ItemAdded += ItemAdded; _libraryManager.ItemAdded += ItemAdded;
// For all TV show libraries, enqueue all contained items. // For all TV show libraries, enqueue all contained items.
foreach (var folder in _libraryManager.GetVirtualFolders()) { foreach (var folder in _libraryManager.GetVirtualFolders())
if (folder.CollectionType != CollectionTypeOptions.TvShows) { {
if (folder.CollectionType != CollectionTypeOptions.TvShows)
{
continue; continue;
} }
@ -77,25 +80,30 @@ public class Entrypoint : IServerEntryPoint
return Task.CompletedTask; return Task.CompletedTask;
} }
private void QueueLibraryContents(string rawId) { private void QueueLibraryContents(string rawId)
{
// FIXME: do smarterer // FIXME: do smarterer
var query = new UserViewQuery() { var query = new UserViewQuery()
{
UserId = GetAdministrator(), UserId = GetAdministrator(),
}; };
// Get all items from this library. Since intros may change within a season, sort the items before adding them. // Get all items from this library. Since intros may change within a season, sort the items before adding them.
var folder = _userViewManager.GetUserViews(query)[0]; var folder = _userViewManager.GetUserViews(query)[0];
var items = folder.GetItems(new InternalItemsQuery() { var items = folder.GetItems(new InternalItemsQuery()
{
ParentId = Guid.Parse(rawId), ParentId = Guid.Parse(rawId),
OrderBy = new [] { ("SortName", SortOrder.Ascending) }, OrderBy = new[] { ("SortName", SortOrder.Ascending) },
IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode }, IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode },
Recursive = true, Recursive = true,
}); });
// Queue all episodes on the server for fingerprinting. // Queue all episodes on the server for fingerprinting.
foreach (var item in items.Items) { foreach (var item in items.Items)
if (item is not Episode episode) { {
if (item is not Episode episode)
{
_logger.LogError("Item {Name} is not an episode", item.Name); _logger.LogError("Item {Name} is not an episode", item.Name);
continue; continue;
} }
@ -111,7 +119,8 @@ public class Entrypoint : IServerEntryPoint
/// <param name="e">ItemChangeEventArgs.</param> /// <param name="e">ItemChangeEventArgs.</param>
private void ItemAdded(object? sender, ItemChangeEventArgs e) private void ItemAdded(object? sender, ItemChangeEventArgs e)
{ {
if (e.Item is not Episode episode) { if (e.Item is not Episode episode)
{
return; return;
} }
@ -120,28 +129,34 @@ public class Entrypoint : IServerEntryPoint
QueueEpisode(episode); QueueEpisode(episode);
} }
private void QueueEpisode(Episode episode) { private void QueueEpisode(Episode episode)
if (Plugin.Instance is null) { {
if (Plugin.Instance is null)
{
throw new InvalidOperationException("plugin instance was null"); throw new InvalidOperationException("plugin instance was null");
} }
lock (_queueLock) { lock (_queueLock)
{
var queue = Plugin.Instance.AnalysisQueue; var queue = Plugin.Instance.AnalysisQueue;
// Allocate a new list for each new season // Allocate a new list for each new season
if (!queue.ContainsKey(episode.SeasonId)) { if (!queue.ContainsKey(episode.SeasonId))
{
Plugin.Instance.AnalysisQueue[episode.SeasonId] = new List<QueuedEpisode>(); Plugin.Instance.AnalysisQueue[episode.SeasonId] = new List<QueuedEpisode>();
} }
// Only fingerprint up to 25% of the episode and at most 10 minutes. // Only fingerprint up to 25% of the episode and at most 10 minutes.
var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds; var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds;
if (duration >= 5*60) { if (duration >= 5 * 60)
{
duration /= 4; duration /= 4;
} }
duration = Math.Min(duration, 10 * 60); duration = Math.Min(duration, 10 * 60);
Plugin.Instance.AnalysisQueue[episode.SeasonId].Add(new QueuedEpisode() { Plugin.Instance.AnalysisQueue[episode.SeasonId].Add(new QueuedEpisode()
{
SeriesName = episode.SeriesName, SeriesName = episode.SeriesName,
SeasonNumber = episode.AiredSeasonNumber ?? 0, SeasonNumber = episode.AiredSeasonNumber ?? 0,
EpisodeId = episode.Id, EpisodeId = episode.Id,
@ -156,9 +171,12 @@ public class Entrypoint : IServerEntryPoint
/// <summary> /// <summary>
/// FIXME: don't do this. /// FIXME: don't do this.
/// </summary> /// </summary>
private Guid GetAdministrator() { private Guid GetAdministrator()
foreach (var user in _userManager.Users) { {
if (!user.HasPermission(Jellyfin.Data.Enums.PermissionKind.IsAdministrator)) { foreach (var user in _userManager.Users)
{
if (!user.HasPermission(Jellyfin.Data.Enums.PermissionKind.IsAdministrator))
{
continue; continue;
} }

View File

@ -12,20 +12,22 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary> /// <summary>
/// Wrapper for the fpcalc utility. /// Wrapper for the fpcalc utility.
/// </summary> /// </summary>
public static class FPCalc { public static class FPCalc
{
/// <summary> /// <summary>
/// Logger. /// Gets or sets the logger.
/// </summary> /// </summary>
public static ILogger? Logger { get; set; } public static ILogger? Logger { get; set; }
/// <summary> /// <summary>
/// Check that the fpcalc utility is installed. /// Check that the fpcalc utility is installed.
/// </summary> /// </summary>
/// <returns>true if fpcalc is installed, false on any error.</returns>
public static bool CheckFPCalcInstalled() public static bool CheckFPCalcInstalled()
{ {
try try
{ {
var version = getOutput("-version", 2000); var version = GetOutput("-version", 2000);
Logger?.LogDebug("fpcalc version: {Version}", version); Logger?.LogDebug("fpcalc version: {Version}", version);
return version.StartsWith("fpcalc version", StringComparison.OrdinalIgnoreCase); return version.StartsWith("fpcalc version", StringComparison.OrdinalIgnoreCase);
} }
@ -39,10 +41,11 @@ public static class FPCalc {
/// Fingerprint a queued episode. /// Fingerprint a queued episode.
/// </summary> /// </summary>
/// <param name="episode">Queued episode to fingerprint.</param> /// <param name="episode">Queued episode to fingerprint.</param>
/// <returns>Numerical fingerprint points.</returns>
public static ReadOnlyCollection<uint> Fingerprint(QueuedEpisode episode) public static ReadOnlyCollection<uint> Fingerprint(QueuedEpisode episode)
{ {
// Try to load this episode from cache before running fpcalc. // Try to load this episode from cache before running fpcalc.
if (loadCachedFingerprint(episode, out ReadOnlyCollection<uint> cachedFingerprint)) if (LoadCachedFingerprint(episode, out ReadOnlyCollection<uint> cachedFingerprint))
{ {
Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path); Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path);
return cachedFingerprint; return cachedFingerprint;
@ -60,7 +63,7 @@ public static class FPCalc {
* FINGERPRINT=123456789,987654321,123456789,987654321,123456789,987654321 * FINGERPRINT=123456789,987654321,123456789,987654321,123456789,987654321
*/ */
var raw = getOutput(args); var raw = GetOutput(args);
var lines = raw.Split("\n"); var lines = raw.Split("\n");
if (lines.Length < 2) if (lines.Length < 2)
@ -79,7 +82,7 @@ public static class FPCalc {
} }
// Try to cache this fingerprint. // Try to cache this fingerprint.
cacheFingerprint(episode, results); CacheFingerprint(episode, results);
return results.AsReadOnly(); return results.AsReadOnly();
} }
@ -89,7 +92,7 @@ public static class FPCalc {
/// </summary> /// </summary>
/// <param name="args">Arguments to pass to fpcalc.</param> /// <param name="args">Arguments to pass to fpcalc.</param>
/// <param name="timeout">Timeout (in seconds) to wait for fpcalc to exit.</param> /// <param name="timeout">Timeout (in seconds) to wait for fpcalc to exit.</param>
private static string getOutput(string args, int timeout = 60 * 1000) private static string GetOutput(string args, int timeout = 60 * 1000)
{ {
var info = new ProcessStartInfo("fpcalc", args); var info = new ProcessStartInfo("fpcalc", args);
info.CreateNoWindow = true; info.CreateNoWindow = true;
@ -110,7 +113,7 @@ public static class FPCalc {
/// <param name="episode">Episode to try to load from cache.</param> /// <param name="episode">Episode to try to load from cache.</param>
/// <param name="fingerprint">ReadOnlyCollection to store the fingerprint in.</param> /// <param name="fingerprint">ReadOnlyCollection to store the fingerprint in.</param>
/// <returns>true if the episode was successfully loaded from cache, false on any other error.</returns> /// <returns>true if the episode was successfully loaded from cache, false on any other error.</returns>
private static bool loadCachedFingerprint(QueuedEpisode episode, out ReadOnlyCollection<uint> fingerprint) private static bool LoadCachedFingerprint(QueuedEpisode episode, out ReadOnlyCollection<uint> fingerprint)
{ {
fingerprint = new List<uint>().AsReadOnly(); fingerprint = new List<uint>().AsReadOnly();
@ -120,7 +123,7 @@ public static class FPCalc {
return false; return false;
} }
var path = getFingerprintCachePath(episode); var path = GetFingerprintCachePath(episode);
// If this episode isn't cached, bail out. // If this episode isn't cached, bail out.
if (!File.Exists(path)) if (!File.Exists(path))
@ -148,7 +151,7 @@ public static class FPCalc {
/// </summary> /// </summary>
/// <param name="episode">Episode to store in cache.</param> /// <param name="episode">Episode to store in cache.</param>
/// <param name="fingerprint">Fingerprint of the episode to store.</param> /// <param name="fingerprint">Fingerprint of the episode to store.</param>
private static void cacheFingerprint(QueuedEpisode episode, List<uint> fingerprint) private static void CacheFingerprint(QueuedEpisode episode, List<uint> fingerprint)
{ {
// Bail out if caching isn't enabled. // Bail out if caching isn't enabled.
if (!(Plugin.Instance?.Configuration.CacheFingerprints ?? false)) if (!(Plugin.Instance?.Configuration.CacheFingerprints ?? false))
@ -164,14 +167,14 @@ public static class FPCalc {
} }
// Cache the episode. // Cache the episode.
File.WriteAllLinesAsync(getFingerprintCachePath(episode), lines, Encoding.UTF8).ConfigureAwait(false); File.WriteAllLinesAsync(GetFingerprintCachePath(episode), lines, Encoding.UTF8).ConfigureAwait(false);
} }
/// <summary> /// <summary>
/// Determines the path an episode should be cached at. /// Determines the path an episode should be cached at.
/// </summary> /// </summary>
/// <param name="episode">Episode.</param> /// <param name="episode">Episode.</param>
private static string getFingerprintCachePath(QueuedEpisode episode) private static string GetFingerprintCachePath(QueuedEpisode episode)
{ {
return Path.Join(Plugin.Instance!.FingerprintCachePath, episode.EpisodeId.ToString("N")); return Path.Join(Plugin.Instance!.FingerprintCachePath, episode.EpisodeId.ToString("N"));
} }

View File

@ -92,22 +92,22 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
} }
/// <summary> /// <summary>
/// Results of fingerprinting all episodes. /// Gets the results of fingerprinting all episodes.
/// </summary> /// </summary>
public Dictionary<Guid, Intro> Intros { get; } public Dictionary<Guid, Intro> Intros { get; }
/// <summary> /// <summary>
/// Map of season ids to episodes that have been queued for fingerprinting. /// Gets the mapping of season ids to episodes that have been queued for fingerprinting.
/// </summary> /// </summary>
public Dictionary<Guid, List<QueuedEpisode>> AnalysisQueue { get; } public Dictionary<Guid, List<QueuedEpisode>> AnalysisQueue { get; }
/// <summary> /// <summary>
/// Total number of episodes in the queue. /// Gets or sets the total number of episodes in the queue.
/// </summary> /// </summary>
public int TotalQueued { get; set; } public int TotalQueued { get; set; }
/// <summary> /// <summary>
/// Directory to cache fingerprints in. /// Gets the directory to cache fingerprints in.
/// </summary> /// </summary>
public string FingerprintCachePath { get; private set; } public string FingerprintCachePath { get; private set; }

View File

@ -11,7 +11,8 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary> /// <summary>
/// Fingerprint all queued episodes at the set time. /// Fingerprint all queued episodes at the set time.
/// </summary> /// </summary>
public class FingerprinterTask : IScheduledTask { public class FingerprinterTask : IScheduledTask
{
private readonly ILogger<FingerprinterTask> _logger; private readonly ILogger<FingerprinterTask> _logger;
/// <summary> /// <summary>
@ -35,35 +36,36 @@ public class FingerprinterTask : IScheduledTask {
private const double SAMPLES_TO_SECONDS = 0.128; private const double SAMPLES_TO_SECONDS = 0.128;
/// <summary> /// <summary>
/// Gets or sets the last detected intro sequence. Only populated when a unit test is running. /// Gets the last detected intro sequence. Only populated when a unit test is running.
/// </summary> /// </summary>
public static Intro LastIntro { get; private set; } = new Intro(); public static Intro LastIntro { get; private set; } = new Intro();
/// <summary> /// <summary>
/// Constructor. /// Initializes a new instance of the <see cref="FingerprinterTask"/> class.
/// </summary> /// </summary>
/// <param name="logger">Logger.</param>
public FingerprinterTask(ILogger<FingerprinterTask> logger) public FingerprinterTask(ILogger<FingerprinterTask> logger)
{ {
_logger = logger; _logger = logger;
} }
/// <summary> /// <summary>
/// Task name. /// Gets the task name.
/// </summary> /// </summary>
public string Name => "Analyze episodes"; public string Name => "Analyze episodes";
/// <summary> /// <summary>
/// Task category. /// Gets the task category.
/// </summary> /// </summary>
public string Category => "Intro Skipper"; public string Category => "Intro Skipper";
/// <summary> /// <summary>
/// Task description. /// Gets the task description.
/// </summary> /// </summary>
public string Description => "Analyzes the audio of all television episodes to find introduction sequences."; public string Description => "Analyzes the audio of all television episodes to find introduction sequences.";
/// <summary> /// <summary>
/// Key. /// Gets the task key.
/// </summary> /// </summary>
public string Key => "CPBIntroSkipperRunFingerprinter"; public string Key => "CPBIntroSkipperRunFingerprinter";
@ -72,12 +74,14 @@ public class FingerprinterTask : IScheduledTask {
/// </summary> /// </summary>
/// <param name="progress">Progress.</param> /// <param name="progress">Progress.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task.</returns>
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{ {
var queue = Plugin.Instance!.AnalysisQueue; var queue = Plugin.Instance!.AnalysisQueue;
var totalProcessed = 0; var totalProcessed = 0;
foreach (var season in queue) { foreach (var season in queue)
{
var first = season.Value[0]; var first = season.Value[0];
// Don't analyze seasons with <= 1 episode or specials // Don't analyze seasons with <= 1 episode or specials
@ -94,7 +98,8 @@ public class FingerprinterTask : IScheduledTask {
// Ensure there are an even number of episodes // Ensure there are an even number of episodes
var episodes = season.Value; var episodes = season.Value;
if (episodes.Count % 2 != 0) { if (episodes.Count % 2 != 0)
{
episodes.Add(episodes[episodes.Count - 2]); episodes.Add(episodes[episodes.Count - 2]);
} }
@ -109,7 +114,7 @@ public class FingerprinterTask : IScheduledTask {
} }
var lhs = episodes[i]; var lhs = episodes[i];
var rhs = episodes[i+1]; var rhs = episodes[i + 1];
// TODO: make configurable // TODO: make configurable
if (!everFoundIntro && failures >= 6) if (!everFoundIntro && failures >= 6)
@ -190,7 +195,7 @@ public class FingerprinterTask : IScheduledTask {
var limit = Math.Min(lhs.Count, rhs.Count); var limit = Math.Min(lhs.Count, rhs.Count);
// First, test if an intro can be found within the first 5 seconds of the episodes (±5/0.128 = ±40 samples). // First, test if an intro can be found within the first 5 seconds of the episodes (±5/0.128 = ±40 samples).
var (lhsContiguous, rhsContiguous) = shiftEpisodes(lhs, rhs, -40, 40); var (lhsContiguous, rhsContiguous) = ShiftEpisodes(lhs, rhs, -40, 40);
lhsRanges.AddRange(lhsContiguous); lhsRanges.AddRange(lhsContiguous);
rhsRanges.AddRange(rhsContiguous); rhsRanges.AddRange(rhsContiguous);
@ -199,7 +204,7 @@ public class FingerprinterTask : IScheduledTask {
{ {
_logger.LogDebug("using full scan"); _logger.LogDebug("using full scan");
(lhsContiguous, rhsContiguous) = shiftEpisodes(lhs, rhs, -1 * limit, limit); (lhsContiguous, rhsContiguous) = ShiftEpisodes(lhs, rhs, -1 * limit, limit);
lhsRanges.AddRange(lhsContiguous); lhsRanges.AddRange(lhsContiguous);
rhsRanges.AddRange(rhsContiguous); rhsRanges.AddRange(rhsContiguous);
} }
@ -219,8 +224,8 @@ public class FingerprinterTask : IScheduledTask {
// TODO: is this the optimal way to indicate that an intro couldn't be found? // TODO: is this the optimal way to indicate that an intro couldn't be found?
// the goal here is to not waste time every task run reprocessing episodes that we know will fail. // the goal here is to not waste time every task run reprocessing episodes that we know will fail.
storeIntro(lhsEpisode.EpisodeId, 0, 0); StoreIntro(lhsEpisode.EpisodeId, 0, 0);
storeIntro(rhsEpisode.EpisodeId, 0, 0); StoreIntro(rhsEpisode.EpisodeId, 0, 0);
return false; return false;
} }
@ -243,8 +248,8 @@ public class FingerprinterTask : IScheduledTask {
rhsIntro.Start = 0; rhsIntro.Start = 0;
} }
storeIntro(lhsEpisode.EpisodeId, lhsIntro.Start, lhsIntro.End); StoreIntro(lhsEpisode.EpisodeId, lhsIntro.Start, lhsIntro.End);
storeIntro(rhsEpisode.EpisodeId, rhsIntro.Start, rhsIntro.End); StoreIntro(rhsEpisode.EpisodeId, rhsIntro.Start, rhsIntro.End);
return true; return true;
} }
@ -256,18 +261,18 @@ public class FingerprinterTask : IScheduledTask {
/// <param name="rhs">Second episode fingerprint.</param> /// <param name="rhs">Second episode fingerprint.</param>
/// <param name="lower">Lower end of the shift range.</param> /// <param name="lower">Lower end of the shift range.</param>
/// <param name="upper">Upper end of the shift range.</param> /// <param name="upper">Upper end of the shift range.</param>
private static (List<TimeRange>, List<TimeRange>) shiftEpisodes( private static (List<TimeRange> Lhs, List<TimeRange> Rhs) ShiftEpisodes(
ReadOnlyCollection<uint> lhs, ReadOnlyCollection<uint> lhs,
ReadOnlyCollection<uint> rhs, ReadOnlyCollection<uint> rhs,
int lower, int lower,
int upper int upper)
) { {
var lhsRanges = new List<TimeRange>(); var lhsRanges = new List<TimeRange>();
var rhsRanges = new List<TimeRange>(); var rhsRanges = new List<TimeRange>();
for (int amount = lower; amount <= upper; amount++) for (int amount = lower; amount <= upper; amount++)
{ {
var (lRange, rRange) = findContiguous(lhs, rhs, amount); var (lRange, rRange) = FindContiguous(lhs, rhs, amount);
if (lRange.End == 0 && rRange.End == 0) if (lRange.End == 0 && rRange.End == 0)
{ {
@ -287,18 +292,21 @@ public class FingerprinterTask : IScheduledTask {
/// <param name="lhs">First fingerprint to compare.</param> /// <param name="lhs">First fingerprint to compare.</param>
/// <param name="rhs">Second fingerprint to compare.</param> /// <param name="rhs">Second fingerprint to compare.</param>
/// <param name="shiftAmount">Amount to shift one fingerprint by.</param> /// <param name="shiftAmount">Amount to shift one fingerprint by.</param>
private static (TimeRange, TimeRange) findContiguous( private static (TimeRange Lhs, TimeRange Rhs) FindContiguous(
ReadOnlyCollection<uint> lhs, ReadOnlyCollection<uint> lhs,
ReadOnlyCollection<uint> rhs, ReadOnlyCollection<uint> rhs,
int shiftAmount int shiftAmount)
) { {
var leftOffset = 0; var leftOffset = 0;
var rightOffset = 0; var rightOffset = 0;
// Calculate the offsets for the left and right hand sides. // Calculate the offsets for the left and right hand sides.
if (shiftAmount < 0) { if (shiftAmount < 0)
{
leftOffset -= shiftAmount; leftOffset -= shiftAmount;
} else { }
else
{
rightOffset += shiftAmount; rightOffset += shiftAmount;
} }
@ -308,14 +316,15 @@ public class FingerprinterTask : IScheduledTask {
var upperLimit = Math.Min(lhs.Count, rhs.Count) - Math.Abs(shiftAmount); var upperLimit = Math.Min(lhs.Count, rhs.Count) - Math.Abs(shiftAmount);
// XOR all elements in LHS and RHS, using the shift amount from above. // XOR all elements in LHS and RHS, using the shift amount from above.
for (var i = 0; i < upperLimit; i++) { for (var i = 0; i < upperLimit; i++)
{
// XOR both samples at the current position. // XOR both samples at the current position.
var lhsPosition = i + leftOffset; var lhsPosition = i + leftOffset;
var rhsPosition = i + rightOffset; var rhsPosition = i + rightOffset;
var diff = lhs[lhsPosition] ^ rhs[rhsPosition]; var diff = lhs[lhsPosition] ^ rhs[rhsPosition];
// If the difference between the samples is small, flag both times as similar. // If the difference between the samples is small, flag both times as similar.
if (countBits(diff) > MAXIMUM_DIFFERENCES) if (CountBits(diff) > MAXIMUM_DIFFERENCES)
{ {
continue; continue;
} }
@ -328,8 +337,8 @@ public class FingerprinterTask : IScheduledTask {
} }
// Ensure the last timestamp is checked // Ensure the last timestamp is checked
lhsTimes.Add(Double.MaxValue); lhsTimes.Add(double.MaxValue);
rhsTimes.Add(Double.MaxValue); rhsTimes.Add(double.MaxValue);
// Now that both fingerprints have been compared at this shift, see if there's a contiguous time range. // Now that both fingerprints have been compared at this shift, see if there's a contiguous time range.
var lContiguous = TimeRangeHelpers.FindContiguous(lhsTimes.ToArray(), MAXIMUM_DISTANCE); var lContiguous = TimeRangeHelpers.FindContiguous(lhsTimes.ToArray(), MAXIMUM_DISTANCE);
@ -356,7 +365,7 @@ public class FingerprinterTask : IScheduledTask {
return (lContiguous, rContiguous); return (lContiguous, rContiguous);
} }
private static void storeIntro(Guid episode, double introStart, double introEnd) private static void StoreIntro(Guid episode, double introStart, double introEnd)
{ {
var intro = new Intro() var intro = new Intro()
{ {
@ -375,12 +384,15 @@ public class FingerprinterTask : IScheduledTask {
Plugin.Instance.Intros[episode] = intro; Plugin.Instance.Intros[episode] = intro;
} }
private static int countBits(uint number) { private static int CountBits(uint number)
{
var count = 0; var count = 0;
for (var i = 0; i < 32; i++) { for (var i = 0; i < 32; i++)
{
var low = (number >> i) & 1; var low = (number >> i) & 1;
if (low == 1) { if (low == 1)
{
count++; count++;
} }
} }
@ -391,6 +403,7 @@ public class FingerprinterTask : IScheduledTask {
/// <summary> /// <summary>
/// Get task triggers. /// Get task triggers.
/// </summary> /// </summary>
/// <returns>Task triggers.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{ {
return new[] return new[]