Merge branch 'ffmpeg5'
This commit is contained in:
commit
70b4f0e034
@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.5.0 (unreleased)
|
||||
* Use `ffmpeg` to generate audio fingerprints instead of `fpcalc`
|
||||
* Requires that the installed version of `ffmpeg`:
|
||||
* Was compiled with the `--enable-chromaprint` option
|
||||
* Understands the `-fp_format raw` flag
|
||||
* `jellyfin-ffmpeg 5.0.1-5` meets both of these requirements
|
||||
|
||||
## v0.1.0.0 (2022-06-09)
|
||||
* Add option to automatically skip intros
|
||||
* Cache audio fingerprints by default
|
||||
|
@ -1,14 +1,18 @@
|
||||
/* These tests require that the host system has a version of FFmpeg installed
|
||||
* which supports both chromaprint and the "-fp_format raw" flag.
|
||||
*/
|
||||
|
||||
using Xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
||||
|
||||
public class TestFPCalc
|
||||
public class TestAudioFingerprinting
|
||||
{
|
||||
[Fact]
|
||||
public void TestInstallationCheck()
|
||||
{
|
||||
Assert.True(FPCalc.CheckFPCalcInstalled());
|
||||
Assert.True(Chromaprint.CheckFFmpegVersion());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -43,7 +47,7 @@ public class TestFPCalc
|
||||
3416816715, 3404331257, 3395844345, 3395836155, 3408464089, 3374975369, 1282036360, 1290457736,
|
||||
1290400440, 1290314408, 1281925800, 1277727404, 1277792932, 1278785460, 1561962388, 1426698196,
|
||||
3607924711, 4131892839, 4140215815, 4292259591, 3218515717, 3209938229, 3171964197, 3171956013,
|
||||
4229117295, 4229312879, 4242407935, 4238016959, 4239987133, 4239990013, 3703060732, 1547188252,
|
||||
4229117295, 4229312879, 4242407935, 4240114111, 4239987133, 4239990013, 3703060732, 1547188252,
|
||||
1278748677, 1278748935, 1144662786, 1148854786, 1090388802, 1090388962, 1086260130, 1085940098,
|
||||
1102709122, 45811586, 44634002, 44596656, 44592544, 1122527648, 1109944736, 1109977504, 1111030243,
|
||||
1111017762, 1109969186, 1126721826, 1101556002, 1084844322, 1084979506, 1084914450, 1084914449,
|
||||
@ -52,7 +56,7 @@ public class TestFPCalc
|
||||
3472417825, 3395841056, 3458735136, 3341420624, 1076496560, 1076501168, 1076501136, 1076497024
|
||||
};
|
||||
|
||||
var actual = FPCalc.Fingerprint(queueEpisode("audio/big_buck_bunny_intro.mp3"));
|
||||
var actual = Chromaprint.Fingerprint(queueEpisode("audio/big_buck_bunny_intro.mp3"));
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ using Microsoft.Extensions.Logging;
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for the fpcalc utility.
|
||||
/// Wrapper for libchromaprint.
|
||||
/// </summary>
|
||||
public static class FPCalc
|
||||
public static class Chromaprint
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
@ -20,16 +20,16 @@ public static class FPCalc
|
||||
public static ILogger? Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Check that the fpcalc utility is installed.
|
||||
/// Check that the installed version of ffmpeg supports chromaprint.
|
||||
/// </summary>
|
||||
/// <returns>true if fpcalc is installed, false on any error.</returns>
|
||||
public static bool CheckFPCalcInstalled()
|
||||
/// <returns>true if a compatible version of ffmpeg is installed, false on any error.</returns>
|
||||
public static bool CheckFFmpegVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = GetOutput("-version", 2000).TrimEnd();
|
||||
Logger?.LogInformation("fpcalc -version: {Version}", version);
|
||||
return version.StartsWith("fpcalc version", StringComparison.OrdinalIgnoreCase);
|
||||
var version = Encoding.UTF8.GetString(GetOutput("-version", 2000));
|
||||
Logger?.LogDebug("ffmpeg version: {Version}", version);
|
||||
return version.Contains("--enable-chromaprint", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -44,7 +44,7 @@ public static class FPCalc
|
||||
/// <returns>Numerical fingerprint points.</returns>
|
||||
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 ffmpeg.
|
||||
if (LoadCachedFingerprint(episode, out ReadOnlyCollection<uint> cachedFingerprint))
|
||||
{
|
||||
Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path);
|
||||
@ -53,32 +53,24 @@ public static class FPCalc
|
||||
|
||||
Logger?.LogDebug("Fingerprinting {Duration} seconds from {File}", episode.FingerprintDuration, episode.Path);
|
||||
|
||||
// FIXME: revisit escaping
|
||||
var path = "\"" + episode.Path + "\"";
|
||||
var duration = episode.FingerprintDuration.ToString(CultureInfo.InvariantCulture);
|
||||
var args = " -raw -length " + duration + " " + path;
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i \"{0}\" -to {1} -ac 2 -f chromaprint -fp_format raw -",
|
||||
episode.Path,
|
||||
episode.FingerprintDuration);
|
||||
|
||||
/* Returns output similar to the following:
|
||||
* DURATION=123
|
||||
* FINGERPRINT=123456789,987654321,123456789,987654321,123456789,987654321
|
||||
*/
|
||||
|
||||
var raw = GetOutput(args);
|
||||
var lines = raw.Split("\n");
|
||||
|
||||
if (lines.Length < 2)
|
||||
// Returns all fingerprint points as raw 32 bit unsigned integers (little endian).
|
||||
var rawPoints = GetOutput(args);
|
||||
if (rawPoints.Length == 0 || rawPoints.Length % 4 != 0)
|
||||
{
|
||||
Logger?.LogTrace("fpcalc output is {Raw}", raw);
|
||||
throw new FingerprintException("fpcalc output for " + episode.Path + " was malformed");
|
||||
throw new FingerprintException("chromaprint output for " + episode.Path + " was malformed");
|
||||
}
|
||||
|
||||
// Remove the "FINGERPRINT=" prefix and split into an array of numbers.
|
||||
var fingerprint = lines[1].Substring(12).Split(",");
|
||||
|
||||
var results = new List<uint>();
|
||||
foreach (var rawNumber in fingerprint)
|
||||
for (var i = 0; i < rawPoints.Length; i += 4)
|
||||
{
|
||||
results.Add(Convert.ToUInt32(rawNumber, CultureInfo.InvariantCulture));
|
||||
var rawPoint = rawPoints.Slice(i, 4);
|
||||
results.Add(BitConverter.ToUInt32(rawPoint));
|
||||
}
|
||||
|
||||
// Try to cache this fingerprint.
|
||||
@ -88,23 +80,43 @@ public static class FPCalc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs fpcalc and returns standard output.
|
||||
/// Runs ffmpeg and returns standard output.
|
||||
/// </summary>
|
||||
/// <param name="args">Arguments to pass to fpcalc.</param>
|
||||
/// <param name="timeout">Timeout (in seconds) to wait for fpcalc to exit.</param>
|
||||
private static string GetOutput(string args, int timeout = 60 * 1000)
|
||||
/// <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)
|
||||
{
|
||||
var info = new ProcessStartInfo("fpcalc", args);
|
||||
info.CreateNoWindow = true;
|
||||
info.RedirectStandardOutput = true;
|
||||
var ffmpegPath = Plugin.Instance?.FFmpegPath ?? "ffmpeg";
|
||||
|
||||
var fpcalc = new Process();
|
||||
fpcalc.StartInfo = info;
|
||||
var info = new ProcessStartInfo(ffmpegPath, args)
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
fpcalc.Start();
|
||||
fpcalc.WaitForExit(timeout);
|
||||
var ffmpeg = new Process
|
||||
{
|
||||
StartInfo = info
|
||||
};
|
||||
|
||||
return fpcalc.StandardOutput.ReadToEnd();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
@ -15,7 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the output of fpcalc should be cached to the filesystem.
|
||||
/// Gets or sets a value indicating whether the episode's fingerprint should be cached to the filesystem.
|
||||
/// </summary>
|
||||
public bool CacheFingerprints { get; set; } = true;
|
||||
|
||||
|
@ -3,8 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
|
||||
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||
<FileVersion>0.1.0.0</FileVersion>
|
||||
<AssemblyVersion>0.1.5.0</AssemblyVersion>
|
||||
<FileVersion>0.1.5.0</FileVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
|
@ -105,7 +105,7 @@ public class VisualizationController : ControllerBase
|
||||
{
|
||||
if (needle.EpisodeId == id)
|
||||
{
|
||||
return FPCalc.Fingerprint(needle);
|
||||
return Chromaprint.Fingerprint(needle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,12 +49,12 @@ public class Entrypoint : IServerEntryPoint
|
||||
/// <returns>Task.</returns>
|
||||
public Task RunAsync()
|
||||
{
|
||||
FPCalc.Logger = _logger;
|
||||
Chromaprint.Logger = _logger;
|
||||
|
||||
// Assert that fpcalc is installed
|
||||
if (!FPCalc.CheckFPCalcInstalled())
|
||||
// Assert that ffmpeg with chromaprint is installed
|
||||
if (!Chromaprint.CheckFFmpegVersion())
|
||||
{
|
||||
_logger.LogError("fpcalc is not installed on this system - episodes will not be analyzed");
|
||||
_logger.LogError("ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System.IO;
|
||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
@ -23,7 +24,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||
/// <param name="serverConfiguration">Server configuration manager.</param>
|
||||
public Plugin(
|
||||
IApplicationPaths applicationPaths,
|
||||
IXmlSerializer xmlSerializer,
|
||||
IServerConfigurationManager serverConfiguration)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
@ -37,6 +42,9 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
|
||||
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
|
||||
|
||||
// Get the path to FFmpeg.
|
||||
FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
|
||||
|
||||
Intros = new Dictionary<Guid, Intro>();
|
||||
AnalysisQueue = new Dictionary<Guid, List<QueuedEpisode>>();
|
||||
Instance = this;
|
||||
@ -64,6 +72,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// </summary>
|
||||
public string FingerprintCachePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path to FFmpeg.
|
||||
/// </summary>
|
||||
public string FFmpegPath { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Intro Skipper";
|
||||
|
||||
|
@ -106,7 +106,7 @@ public class FingerprinterTask : IScheduledTask
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
throw new FingerprintException(
|
||||
"No episodes to analyze: either no show libraries are defined or fpcalc is not properly installed");
|
||||
"No episodes to analyze: either no show libraries are defined or ffmpeg could not be found");
|
||||
}
|
||||
|
||||
var totalProcessed = 0;
|
||||
@ -274,8 +274,8 @@ public class FingerprinterTask : IScheduledTask
|
||||
/// <returns>Intros for the first and second episodes.</returns>
|
||||
public (Intro Lhs, Intro Rhs) FingerprintEpisodes(QueuedEpisode lhsEpisode, QueuedEpisode rhsEpisode)
|
||||
{
|
||||
var lhsFingerprint = FPCalc.Fingerprint(lhsEpisode);
|
||||
var rhsFingerprint = FPCalc.Fingerprint(rhsEpisode);
|
||||
var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
|
||||
var rhsFingerprint = Chromaprint.Fingerprint(rhsEpisode);
|
||||
|
||||
// Cache the fingerprints for quicker recall in the second pass (if one is needed).
|
||||
lock (_fingerprintCacheLock)
|
||||
|
13
README.md
13
README.md
@ -1,18 +1,16 @@
|
||||
# Intro Skipper (ALPHA)
|
||||
# Intro Skipper (beta)
|
||||
|
||||
<div align="center">
|
||||
<img alt="Plugin Banner" src="https://raw.githubusercontent.com/ConfusedPolarBear/intro-skipper/master/images/logo.png" />
|
||||
</div>
|
||||
|
||||
Analyzes the audio of television episodes to detect and skip over intros. Currently in alpha.
|
||||
Analyzes the audio of television episodes to detect and skip over intros. Jellyfin must use a version of `ffmpeg` that has been compiled with `--enable-chromaprint` (`jellyfin-ffmpeg` versions 5.0.1-5 and later will work).
|
||||
|
||||
Installing this plugin (along with a modified web interface and `fpcalc`) will result in a skip intro button displaying in the video player, like this:
|
||||
If you use the custom web interface on your server, you will be able to click a skip button to skip intros, like this:
|
||||
|
||||
![Skip intro button](images/skip-button.png)
|
||||
|
||||
If you use Jellyfin clients that do not use the web interface provided by the server, the plugin can be configured to automatically skip intros.
|
||||
|
||||
This plugin **will not work** without installing `fpcalc`. The easiest way to do this is to follow the steps below.
|
||||
However, if you use Jellyfin clients that do not use the web interface provided by the server, the plugin can be configured to automatically skip intros.
|
||||
|
||||
## Introduction requirements
|
||||
|
||||
@ -21,7 +19,8 @@ Show introductions will only be detected if they are:
|
||||
* Located within the first 25% of an episode, or the first 10 minutes, whichever is smaller
|
||||
* At least 20 seconds long
|
||||
|
||||
## Step 1: Install the modified web interface + fpcalc
|
||||
## Step 1: Install the modified web interface (optional)
|
||||
This step is only necessary if you do not use the automatic skip feature.
|
||||
1. Run the `ghcr.io/confusedpolarbear/jellyfin-intro-skipper` container just as you would any other Jellyfin container
|
||||
1. If you reuse the configuration data from another container, **make sure to create a backup first**.
|
||||
2. Follow the plugin installation steps below
|
||||
|
@ -1,5 +1,3 @@
|
||||
FROM jellyfin/jellyfin:10.8.0-beta2
|
||||
|
||||
RUN apt update && apt install -y libchromaprint-tools
|
||||
FROM jellyfin/jellyfin:10.8.0
|
||||
|
||||
COPY dist/ /jellyfin/jellyfin-web/
|
||||
|
@ -2,15 +2,12 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
* Jellyfin 10.8.0 beta 2 (beta 3 may also work, untested)
|
||||
* Jellyfin 10.8.0 beta 3
|
||||
* Compiled [jellyfin-web](https://github.com/ConfusedPolarBear/jellyfin-web/tree/intros) interface with intro skip button
|
||||
* [chromaprint](https://github.com/acoustid/chromaprint) (only versions 1.4.3 and later have been verified to work)
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Install the `fpcalc` program
|
||||
1. On Debian based distributions, this is provided by the `libchromaprint-tools` package
|
||||
2. Compiled binaries can also be downloaded from the [GitHub repository](https://github.com/acoustid/chromaprint/releases/tag/v1.5.1)
|
||||
1. Install the latest version of ffmpeg from https://github.com/jellyfin/jellyfin-ffmpeg/releases
|
||||
2. Download the latest modified web interface from the releases tab and either:
|
||||
1. Serve the web interface directly from your Jellyfin server, or
|
||||
2. Serve the web interface using an external web server
|
||||
|
Loading…
x
Reference in New Issue
Block a user