diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs index 72279ac..fbf6076 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs @@ -1,14 +1,20 @@ +/* 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; +// TODO: rename + public class TestFPCalc { [Fact] public void TestInstallationCheck() { - Assert.True(FPCalc.CheckFPCalcInstalled()); + Assert.True(FPCalc.CheckFFmpegVersion()); } [Theory] @@ -43,7 +49,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, diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs index 6fb7e49..9b3c9cf 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs @@ -15,7 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration } /// - /// 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. /// public bool CacheFingerprints { get; set; } = true; diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html index 7e00e3b..ca14011 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html @@ -33,8 +33,8 @@
If checked, will store the fingerprints for all subsequently scanned files to disk. - Caching fingerprints avoids having to re-run fpcalc on each file, at the expense of disk - usage. + Caching fingerprints avoids having to re-run chromaprint on each file, at the expense of + disk usage.
@@ -66,7 +66,8 @@

Erasing introduction timestamps is only necessary after upgrading the plugin if specifically - requested to do so in the plugin's changelog. After the timestamps are erased, run the Analyze episodes scheduled task to re-analyze all media on the server. + requested to do so in the plugin's changelog. After the timestamps are erased, run the + Analyze episodes scheduled task to re-analyze all media on the server.

diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj index 5a7b391..16990a2 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj @@ -3,8 +3,8 @@ net6.0 ConfusedPolarBear.Plugin.IntroSkipper - 0.1.0.0 - 0.1.0.0 + 0.1.5.0 + 0.1.5.0 true true enable diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs index 952cc46..c3df4f1 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs @@ -51,10 +51,10 @@ public class Entrypoint : IServerEntryPoint { FPCalc.Logger = _logger; - // Assert that fpcalc is installed - if (!FPCalc.CheckFPCalcInstalled()) + // Assert that ffmpeg with chromaprint is installed + if (!FPCalc.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; } diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs b/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs index af80747..7deba7c 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs @@ -7,10 +7,12 @@ using System.IO; using System.Text; using Microsoft.Extensions.Logging; +// TODO: rename + namespace ConfusedPolarBear.Plugin.IntroSkipper; /// -/// Wrapper for the fpcalc utility. +/// Wrapper for libchromaprint. /// public static class FPCalc { @@ -20,16 +22,16 @@ public static class FPCalc public static ILogger? Logger { get; set; } /// - /// Check that the fpcalc utility is installed. + /// Check that the installed version of ffmpeg supports chromaprint. /// - /// true if fpcalc is installed, false on any error. - public static bool CheckFPCalcInstalled() + /// true if a compatible version of ffmpeg is installed, false on any error. + 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 +46,7 @@ public static class FPCalc /// Numerical fingerprint points. public static ReadOnlyCollection 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 cachedFingerprint)) { Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path); @@ -53,32 +55,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 was malformed"); + throw new FingerprintException("chromaprint output was malformed"); } - // Remove the "FINGERPRINT=" prefix and split into an array of numbers. - var fingerprint = lines[1].Substring(12).Split(","); - var results = new List(); - 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 +82,43 @@ public static class FPCalc } /// - /// Runs fpcalc and returns standard output. + /// Runs ffmpeg and returns standard output. /// - /// Arguments to pass to fpcalc. - /// Timeout (in seconds) to wait for fpcalc to exit. - private static string GetOutput(string args, int timeout = 60 * 1000) + /// 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 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(); + } } /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs index 6dde104..e069363 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs @@ -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, IHasWebPages /// /// Instance of the interface. /// Instance of the interface. - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + /// Server configuration manager. + public Plugin( + IApplicationPaths applicationPaths, + IXmlSerializer xmlSerializer, + IServerConfigurationManager serverConfiguration) : base(applicationPaths, xmlSerializer) { _xmlSerializer = xmlSerializer; @@ -37,6 +42,9 @@ public class Plugin : BasePlugin, IHasWebPages _introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml"); + // Get the path to FFmpeg. + FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay; + Intros = new Dictionary(); AnalysisQueue = new Dictionary>(); Instance = this; @@ -64,6 +72,11 @@ public class Plugin : BasePlugin, IHasWebPages /// public string FingerprintCachePath { get; private set; } + /// + /// Gets the full path to FFmpeg. + /// + public string FFmpegPath { get; private set; } + /// public override string Name => "Intro Skipper"; diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs index 11bc351..c674822 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs @@ -104,6 +104,12 @@ public class FingerprinterTask : IScheduledTask var queue = Plugin.Instance!.AnalysisQueue; var totalProcessed = 0; + if (queue.Count == 0) + { + throw new FingerprintException( + "No episodes to analyze: either no show libraries are defined or ffmpeg is not properly installed"); + } + // TODO: make configurable var options = new ParallelOptions(); options.MaxDegreeOfParallelism = 2; diff --git a/README.md b/README.md index b8b7a0e..6dc62cc 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ -# Intro Skipper (ALPHA) +# Intro Skipper (beta)
Plugin Banner
-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 diff --git a/docker/Dockerfile.ffmpeg5 b/docker/Dockerfile.ffmpeg5 new file mode 100644 index 0000000..36358c1 --- /dev/null +++ b/docker/Dockerfile.ffmpeg5 @@ -0,0 +1,10 @@ +FROM jellyfin/jellyfin:10.8.0-beta3 + +RUN curl -Lo jellyfin-ffmpeg5.deb \ + https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v5.0.1-5/jellyfin-ffmpeg5_5.0.1-5-bullseye_amd64.deb + +RUN apt update +RUN apt install -y libxcb-randr0 +RUN dpkg -i jellyfin-ffmpeg5.deb + +COPY dist/ /jellyfin/jellyfin-web/ diff --git a/docker/README.md b/docker/README.md index eaa5124..11f158e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -4,3 +4,8 @@ Build instructions for the `ghcr.io/confusedpolarbear/jellyfin-intro-skipper` co 2. Run `npm run build:production` 3. Copy the `dist` folder into this folder 4. Run `docker build .` + +## `Dockerfile.ffmpeg5` testing instructions + +1. Follow steps 1 - 3 above (only needed if you don't use the automatic skip feature) +2. Run `docker build . -t jellyfin-ffmpeg5 -f Dockerfile.ffmpeg5` diff --git a/docs/native.md b/docs/native.md index ae09f4b..07307fd 100644 --- a/docs/native.md +++ b/docs/native.md @@ -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