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