2022-05-01 00:33:22 -05:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
using System.Diagnostics;
|
|
|
|
using System.Globalization;
|
2022-05-05 18:10:34 -05:00
|
|
|
using System.IO;
|
|
|
|
using System.Text;
|
2022-05-01 00:33:22 -05:00
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
|
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
|
|
|
|
|
|
|
/// <summary>
|
2022-06-09 14:07:40 -05:00
|
|
|
/// Wrapper for libchromaprint.
|
2022-05-01 00:33:22 -05:00
|
|
|
/// </summary>
|
2022-06-09 17:33:39 -05:00
|
|
|
public static class Chromaprint
|
2022-05-09 22:50:41 -05:00
|
|
|
{
|
2022-05-01 00:33:22 -05:00
|
|
|
/// <summary>
|
2022-05-09 22:50:41 -05:00
|
|
|
/// Gets or sets the logger.
|
2022-05-01 00:33:22 -05:00
|
|
|
/// </summary>
|
|
|
|
public static ILogger? Logger { get; set; }
|
|
|
|
|
|
|
|
/// <summary>
|
2022-06-09 14:07:40 -05:00
|
|
|
/// Check that the installed version of ffmpeg supports chromaprint.
|
2022-05-01 00:33:22 -05:00
|
|
|
/// </summary>
|
2022-06-09 14:07:40 -05:00
|
|
|
/// <returns>true if a compatible version of ffmpeg is installed, false on any error.</returns>
|
|
|
|
public static bool CheckFFmpegVersion()
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2022-06-09 14:07:40 -05:00
|
|
|
var version = Encoding.UTF8.GetString(GetOutput("-version", 2000));
|
|
|
|
Logger?.LogDebug("ffmpeg version: {Version}", version);
|
|
|
|
return version.Contains("--enable-chromaprint", StringComparison.OrdinalIgnoreCase);
|
2022-05-01 00:33:22 -05:00
|
|
|
}
|
|
|
|
catch
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Fingerprint a queued episode.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="episode">Queued episode to fingerprint.</param>
|
2022-05-09 22:50:41 -05:00
|
|
|
/// <returns>Numerical fingerprint points.</returns>
|
2022-05-01 00:33:22 -05:00
|
|
|
public static ReadOnlyCollection<uint> Fingerprint(QueuedEpisode episode)
|
|
|
|
{
|
2022-06-09 14:07:40 -05:00
|
|
|
// Try to load this episode from cache before running ffmpeg.
|
2022-05-09 22:50:41 -05:00
|
|
|
if (LoadCachedFingerprint(episode, out ReadOnlyCollection<uint> cachedFingerprint))
|
2022-05-05 18:10:34 -05:00
|
|
|
{
|
|
|
|
Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path);
|
|
|
|
return cachedFingerprint;
|
|
|
|
}
|
|
|
|
|
2022-05-01 00:33:22 -05:00
|
|
|
Logger?.LogDebug("Fingerprinting {Duration} seconds from {File}", episode.FingerprintDuration, episode.Path);
|
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
var args = string.Format(
|
|
|
|
CultureInfo.InvariantCulture,
|
|
|
|
"-i \"{0}\" -to {1} -ac 2 -f chromaprint -fp_format raw -",
|
|
|
|
episode.Path,
|
|
|
|
episode.FingerprintDuration);
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
// Returns all fingerprint points as raw 32 bit unsigned integers (little endian).
|
|
|
|
var rawPoints = GetOutput(args);
|
|
|
|
if (rawPoints.Length == 0 || rawPoints.Length % 4 != 0)
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
2022-06-10 22:20:05 -05:00
|
|
|
throw new FingerprintException("chromaprint output for " + episode.Path + " was malformed");
|
2022-05-01 00:33:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var results = new List<uint>();
|
2022-06-09 14:07:40 -05:00
|
|
|
for (var i = 0; i < rawPoints.Length; i += 4)
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
2022-06-09 14:07:40 -05:00
|
|
|
var rawPoint = rawPoints.Slice(i, 4);
|
|
|
|
results.Add(BitConverter.ToUInt32(rawPoint));
|
2022-05-01 00:33:22 -05:00
|
|
|
}
|
|
|
|
|
2022-05-05 18:10:34 -05:00
|
|
|
// Try to cache this fingerprint.
|
2022-05-09 22:50:41 -05:00
|
|
|
CacheFingerprint(episode, results);
|
2022-05-05 18:10:34 -05:00
|
|
|
|
2022-05-01 00:33:22 -05:00
|
|
|
return results.AsReadOnly();
|
|
|
|
}
|
|
|
|
|
2022-05-05 18:10:34 -05:00
|
|
|
/// <summary>
|
2022-06-09 14:07:40 -05:00
|
|
|
/// Runs ffmpeg and returns standard output.
|
2022-05-05 18:10:34 -05:00
|
|
|
/// </summary>
|
2022-06-09 14:07:40 -05:00
|
|
|
/// <param name="args">Arguments to pass to ffmpeg.</param>
|
|
|
|
/// <param name="timeout">Timeout (in seconds) to wait for ffmpeg to exit.</param>
|
|
|
|
private static ReadOnlySpan<byte> GetOutput(string args, int timeout = 60 * 1000)
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
2022-06-09 14:07:40 -05:00
|
|
|
var ffmpegPath = Plugin.Instance?.FFmpegPath ?? "ffmpeg";
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
var info = new ProcessStartInfo(ffmpegPath, args)
|
|
|
|
{
|
|
|
|
CreateNoWindow = true,
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
RedirectStandardError = true
|
|
|
|
};
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
var ffmpeg = new Process
|
|
|
|
{
|
|
|
|
StartInfo = info
|
|
|
|
};
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
ffmpeg.Start();
|
|
|
|
ffmpeg.WaitForExit(timeout);
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
using (MemoryStream ms = new MemoryStream())
|
|
|
|
{
|
|
|
|
var buf = new byte[4096];
|
|
|
|
var bytesRead = 0;
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2022-06-09 14:07:40 -05:00
|
|
|
do
|
|
|
|
{
|
|
|
|
bytesRead = ffmpeg.StandardOutput.BaseStream.Read(buf, 0, buf.Length);
|
|
|
|
ms.Write(buf, 0, bytesRead);
|
|
|
|
}
|
|
|
|
while (bytesRead > 0);
|
|
|
|
|
|
|
|
return ms.ToArray().AsSpan();
|
|
|
|
}
|
2022-05-01 00:33:22 -05:00
|
|
|
}
|
2022-05-05 18:10:34 -05:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Tries to load an episode's fingerprint from cache. If caching is not enabled, calling this function is a no-op.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="episode">Episode to try to load from cache.</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>
|
2022-05-09 22:50:41 -05:00
|
|
|
private static bool LoadCachedFingerprint(QueuedEpisode episode, out ReadOnlyCollection<uint> fingerprint)
|
2022-05-05 18:10:34 -05:00
|
|
|
{
|
|
|
|
fingerprint = new List<uint>().AsReadOnly();
|
|
|
|
|
|
|
|
// If fingerprint caching isn't enabled, don't try to load anything.
|
2022-05-09 03:31:10 -05:00
|
|
|
if (!(Plugin.Instance?.Configuration.CacheFingerprints ?? false))
|
2022-05-05 18:10:34 -05:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-05-09 22:50:41 -05:00
|
|
|
var path = GetFingerprintCachePath(episode);
|
2022-05-05 18:10:34 -05:00
|
|
|
|
|
|
|
// If this episode isn't cached, bail out.
|
|
|
|
if (!File.Exists(path))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: make async
|
|
|
|
var raw = File.ReadAllLines(path, Encoding.UTF8);
|
|
|
|
var result = new List<uint>();
|
|
|
|
|
|
|
|
// Read each stringified uint.
|
|
|
|
result.EnsureCapacity(raw.Length);
|
|
|
|
foreach (var rawNumber in raw)
|
|
|
|
{
|
|
|
|
result.Add(Convert.ToUInt32(rawNumber, CultureInfo.InvariantCulture));
|
|
|
|
}
|
|
|
|
|
|
|
|
fingerprint = result.AsReadOnly();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Cache an episode's fingerprint to disk. If caching is not enabled, calling this function is a no-op.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="episode">Episode to store in cache.</param>
|
|
|
|
/// <param name="fingerprint">Fingerprint of the episode to store.</param>
|
2022-05-09 22:50:41 -05:00
|
|
|
private static void CacheFingerprint(QueuedEpisode episode, List<uint> fingerprint)
|
2022-05-05 18:10:34 -05:00
|
|
|
{
|
|
|
|
// Bail out if caching isn't enabled.
|
2022-05-09 03:31:10 -05:00
|
|
|
if (!(Plugin.Instance?.Configuration.CacheFingerprints ?? false))
|
2022-05-05 18:10:34 -05:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stringify each data point.
|
|
|
|
var lines = new List<string>();
|
|
|
|
foreach (var number in fingerprint)
|
|
|
|
{
|
|
|
|
lines.Add(number.ToString(CultureInfo.InvariantCulture));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache the episode.
|
2022-05-09 22:50:41 -05:00
|
|
|
File.WriteAllLinesAsync(GetFingerprintCachePath(episode), lines, Encoding.UTF8).ConfigureAwait(false);
|
2022-05-05 18:10:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Determines the path an episode should be cached at.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="episode">Episode.</param>
|
2022-05-09 22:50:41 -05:00
|
|
|
private static string GetFingerprintCachePath(QueuedEpisode episode)
|
2022-05-05 18:10:34 -05:00
|
|
|
{
|
|
|
|
return Path.Join(Plugin.Instance!.FingerprintCachePath, episode.EpisodeId.ToString("N"));
|
|
|
|
}
|
2022-05-01 00:33:22 -05:00
|
|
|
}
|