Reformat code to comply with StyleCop analyzers
This commit is contained in:
parent
928f467871
commit
547a2c705b
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
|
@ -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[]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user