diff --git a/listenarr.infrastructure/Ffmpeg/Installation/FfprobePlatformDefaults.cs b/listenarr.infrastructure/Ffmpeg/Installation/FfprobePlatformDefaults.cs index 2eb4b55b7..9fd776926 100644 --- a/listenarr.infrastructure/Ffmpeg/Installation/FfprobePlatformDefaults.cs +++ b/listenarr.infrastructure/Ffmpeg/Installation/FfprobePlatformDefaults.cs @@ -25,20 +25,49 @@ internal static class FfprobePlatformDefaults { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - if (RuntimeInformation.OSArchitecture == Architecture.Arm64) - { - return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz"; - } - - return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"; + return GetDownloadUrl(OSPlatform.Linux, RuntimeInformation.OSArchitecture); } if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return "https://evermeet.cx/ffmpeg/ffmpeg-6.0.zip"; + return GetDownloadUrl(OSPlatform.OSX, RuntimeInformation.OSArchitecture); } if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return GetDownloadUrl(OSPlatform.Windows, RuntimeInformation.OSArchitecture); + } + + return null; + } + + /// + /// Resolves the archive URL for an explicit platform, so the mapping for every platform can + /// be asserted from a test host running on any one of them. + /// + /// + /// Two constraints apply to anything returned here. The archive has to contain ffprobe: + /// the Linux and Windows builds ship a bundle carrying both binaries, but evermeet + /// publishes one binary per archive, so macOS needs the ffprobe archive and not the ffmpeg + /// one. The URL also has to end in a suffix + /// recognises, which rules out endpoints that redirect to the current release without + /// naming a file. + /// + internal static string? GetDownloadUrl(OSPlatform platform, Architecture architecture) + { + if (platform == OSPlatform.Linux) + { + return architecture == Architecture.Arm64 + ? "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz" + : "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"; + } + + if (platform == OSPlatform.OSX) + { + return "https://evermeet.cx/ffmpeg/ffprobe-6.0.zip"; + } + + if (platform == OSPlatform.Windows) { return "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"; } diff --git a/tests/Features/Infrastructure/Ffmpeg/Installation/FfprobePlatformDefaultsTests.cs b/tests/Features/Infrastructure/Ffmpeg/Installation/FfprobePlatformDefaultsTests.cs new file mode 100644 index 000000000..e7d13db46 --- /dev/null +++ b/tests/Features/Infrastructure/Ffmpeg/Installation/FfprobePlatformDefaultsTests.cs @@ -0,0 +1,113 @@ +/* + * Listenarr - Audiobook Management System + * Copyright (C) 2024-2026 Listenarr Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +using System.Runtime.InteropServices; + +// Aliased because `Listenarr.Tests.Features.Architecture` is an enclosing namespace here, and a +// namespace member wins over the type of the same name. +using Arch = System.Runtime.InteropServices.Architecture; + +namespace Listenarr.Tests.Features.Infrastructure.Ffmpeg.Installation +{ + [Trait("Name", "FfprobePlatformDefaultsTests")] + [Trait("Category", "FfmpegService")] + public class FfprobePlatformDefaultsTests + { + // Suffixes FfprobeArchiveExtractor dispatches on. A URL outside this set downloads fine and + // then extracts nothing, which is the failure mode that hid the macOS bug: the install log + // shows a successful download and no binary ever appears. + private static readonly string[] ExtractableSuffixes = [".zip", ".tar.xz", ".tar.gz", ".tgz"]; + + public static TheoryData AllPlatforms() => new() + { + { OSPlatform.Linux, Arch.X64 }, + { OSPlatform.Linux, Arch.Arm64 }, + { OSPlatform.OSX, Arch.X64 }, + { OSPlatform.Windows, Arch.X64 }, + }; + + [Theory] + [MemberData(nameof(AllPlatforms))] + [Trait("Method", "GetDownloadUrl")] + public void GetDownloadUrl_ReturnsAnArchiveTheExtractorCanOpen( + OSPlatform platform, + Arch architecture) + { + var url = FfprobePlatformDefaults.GetDownloadUrl(platform, architecture); + + Assert.NotNull(url); + Assert.Contains( + ExtractableSuffixes, + suffix => url!.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + [Trait("Method", "GetDownloadUrl")] + public void GetDownloadUrl_ForMacOs_PointsAtTheFfprobeArchive() + { + // evermeet ships a separate archive per binary, so the ffmpeg archive contains no + // ffprobe at all and the install can never succeed on macOS. + var url = FfprobePlatformDefaults.GetDownloadUrl(OSPlatform.OSX, Arch.X64); + + Assert.NotNull(url); + var fileName = url!.Split('/')[^1]; + Assert.Contains("ffprobe", fileName, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [MemberData(nameof(AllPlatforms))] + [Trait("Method", "GetDownloadUrl")] + public void GetDownloadUrl_IsServedOverHttps(OSPlatform platform, Arch architecture) + { + var url = FfprobePlatformDefaults.GetDownloadUrl(platform, architecture); + + Assert.NotNull(url); + Assert.StartsWith("https://", url, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + [Trait("Method", "GetDownloadUrl")] + public void GetDownloadUrl_ForLinux_DistinguishesArm64FromX64() + { + var x64 = FfprobePlatformDefaults.GetDownloadUrl(OSPlatform.Linux, Arch.X64); + var arm64 = FfprobePlatformDefaults.GetDownloadUrl(OSPlatform.Linux, Arch.Arm64); + + Assert.NotEqual(x64, arm64); + Assert.Contains("arm64", arm64!, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + [Trait("Method", "GetDownloadUrl")] + public void GetDownloadUrl_ForAnUnsupportedPlatform_ReturnsNull() + { + Assert.Null(FfprobePlatformDefaults.GetDownloadUrl(OSPlatform.FreeBSD, Arch.X64)); + } + + [Fact] + [Trait("Method", "GetDownloadUrl")] + public void GetDownloadUrl_WithoutArguments_MatchesTheHostPlatform() + { + var expected = FfprobePlatformDefaults.GetDownloadUrl( + OperatingSystem.IsWindows() ? OSPlatform.Windows + : OperatingSystem.IsMacOS() ? OSPlatform.OSX + : OSPlatform.Linux, + RuntimeInformation.OSArchitecture); + + Assert.Equal(expected, FfprobePlatformDefaults.GetDownloadUrl()); + } + } +}