From 7be54963050f0950d7628dc9a4210e6a13e6f299 Mon Sep 17 00:00:00 2001 From: Kerry Cao Date: Mon, 27 Jan 2025 22:22:47 -0500 Subject: [PATCH] major refactoring --- .../Enums/FFMpegBinaries.cs | 9 + .../Enums/FFMpegVersions.cs | 29 ++ .../Enums/SupportedPlatforms.cs | 13 + .../Exceptions/FFMpegDownloaderException.cs | 18 ++ .../Extensions/EnumExtensions.cs | 21 ++ .../FFMpegDownloader.cs | 260 ++---------------- .../Models/BinaryInfo.cs | 82 ++++++ .../Models/DownloadInfo.cs | 15 + .../Models/VersionInfo.cs | 15 + .../Services/FFbinariesService.cs | 75 +++++ FFMpegCore.Test/DownloaderTests.cs | 7 +- 11 files changed, 300 insertions(+), 244 deletions(-) create mode 100644 FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs create mode 100644 FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs create mode 100644 FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs create mode 100644 FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs create mode 100644 FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs create mode 100644 FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs create mode 100644 FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs create mode 100644 FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs create mode 100644 FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs diff --git a/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs b/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs new file mode 100644 index 0000000..d92116a --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs @@ -0,0 +1,9 @@ +namespace FFMpegCore.Extensions.Downloader.Enums; + +[Flags] +public enum FFMpegBinaries : ushort +{ + FFMpeg, + FFProbe, + FFPlay +} diff --git a/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs b/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs new file mode 100644 index 0000000..7a9f476 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; + +namespace FFMpegCore.Extensions.Downloader.Enums; + +public enum FFMpegVersions : ushort +{ + [Description("https://ffbinaries.com/api/v1/version/latest")] + Latest, + [Description("https://ffbinaries.com/api/v1/version/6.1")] + V6_1, + [Description("https://ffbinaries.com/api/v1/version/5.1")] + V5_1, + [Description("https://ffbinaries.com/api/v1/version/4.4.1")] + V4_4_1, + [Description("https://ffbinaries.com/api/v1/version/4.2.1")] + V4_2_1, + [Description("https://ffbinaries.com/api/v1/version/4.2")] + V4_2, + [Description("https://ffbinaries.com/api/v1/version/4.1")] + V4_1, + [Description("https://ffbinaries.com/api/v1/version/4.0")] + V4_0, + [Description("https://ffbinaries.com/api/v1/version/3.4")] + V3_4, + [Description("https://ffbinaries.com/api/v1/version/3.3")] + V3_3, + [Description("https://ffbinaries.com/api/v1/version/3.2")] + V3_2 +} diff --git a/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs b/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs new file mode 100644 index 0000000..0378f3e --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs @@ -0,0 +1,13 @@ +namespace FFMpegCore.Extensions.Downloader.Enums; + +public enum SupportedPlatforms : ushort +{ + Windows64, + Windows32, + Linux64, + Linux32, + LinuxArmhf, + LinuxArmel, + LinuxArm64, + Osx64 +} diff --git a/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs b/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs new file mode 100644 index 0000000..c7d2ead --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs @@ -0,0 +1,18 @@ +namespace FFMpegCore.Extensions.Downloader.Exceptions; + +/// +/// Custom exception for FFMpegDownloader +/// +public class FFMpegDownloaderException : Exception +{ + public string Detail { get; set; } = ""; + + public FFMpegDownloaderException(string message) : base(message) + { + } + + public FFMpegDownloaderException(string message, string detail) : base(message) + { + Detail = detail; + } +} diff --git a/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs b/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..e2f5616 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace FFMpegCore.Extensions.Downloader.Extensions; + +internal static class EnumExtensions +{ + public static string GetDescription(this Enum enumValue) + { + var field = enumValue.GetType().GetField(enumValue.ToString()); + if (field == null) + return enumValue.ToString(); + + var attributes = field.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute) + { + return attribute.Description; + } + + return enumValue.ToString(); + } +} diff --git a/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs b/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs index 139921c..adc9c74 100644 --- a/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs +++ b/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs @@ -1,51 +1,11 @@ -using System.IO.Compression; -using System.Net; -using System.Runtime.InteropServices; -using System.Text.Json; -using System.Text.Json.Serialization; +using FFMpegCore.Extensions.Downloader.Enums; +using FFMpegCore.Extensions.Downloader.Exceptions; +using FFMpegCore.Extensions.Downloader.Services; namespace FFMpegCore.Extensions.Downloader; -/// -/// Downloads the latest FFMpeg suite binaries from ffbinaries.com. -/// public class FFMpegDownloader { - [Flags] - public enum FFMpegBinaries : ushort - { - FFMpeg, - FFProbe, - FFPlay - } - - public enum FFMpegVersions : ushort - { - Latest, - V6_1, - V5_1, - V4_4_1, - V4_2_1, - V4_2, - V4_1, - V4_0, - V3_4, - V3_3, - V3_2 - } - - public enum PlatformOverride : short - { - Windows64, - Windows32, - Linux64, - Linux32, - LinuxArmhf, - LinuxArmel, - LinuxArm64, - Osx64 - } - /// /// Download the latest FFMpeg suite binaries for current platform /// @@ -56,221 +16,39 @@ public class FFMpegDownloader public static async Task> DownloadFFMpegSuite( FFMpegVersions version = FFMpegVersions.Latest, FFMpegBinaries binaries = FFMpegBinaries.FFMpeg | FFMpegBinaries.FFProbe, - PlatformOverride? platformOverride = null) + SupportedPlatforms? platformOverride = null) { - var versionInfo = await GetVersionInfo(version); + // get all available versions + var versionInfo = await FFbinariesService.GetVersionInfo(version); + + // get the download info for the current platform var downloadInfo = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo(platformOverride) ?? throw new FFMpegDownloaderException("Failed to get compatible download info"); - + var successList = new List(); - - // if ffmpeg is selected + + // download ffmpeg if selected if (binaries.HasFlag(FFMpegBinaries.FFMpeg) && downloadInfo.FFMpeg is not null) { - var zipStream = DownloadFileAsSteam(new Uri(downloadInfo.FFMpeg)); - successList.AddRange(ExtractZipAndSave(zipStream)); + var zipStream = FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFMpeg)); + successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream)); } - // if ffprobe is selected + // download ffprobe if selected if (binaries.HasFlag(FFMpegBinaries.FFProbe) && downloadInfo.FFProbe is not null) { - var zipStream = DownloadFileAsSteam(new Uri(downloadInfo.FFProbe)); - successList.AddRange(ExtractZipAndSave(zipStream)); + var zipStream = FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFProbe)); + successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream)); } - // if ffplay is selected + // download ffplay if selected if (binaries.HasFlag(FFMpegBinaries.FFPlay) && downloadInfo.FFPlay is not null) { - var zipStream = DownloadFileAsSteam(new Uri(downloadInfo.FFPlay)); - successList.AddRange(ExtractZipAndSave(zipStream)); + var zipStream = FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFPlay)); + successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream)); } return successList; } - - /// - /// Download file from uri - /// - /// uri of the file - /// - private static MemoryStream DownloadFileAsSteam(Uri address) - { - var client = new WebClient(); - var fileStream = new MemoryStream(client.DownloadData(address)); - fileStream.Position = 0; - - return fileStream; - } - - /// - /// Extracts the binaries from the zip stream and saves them to the current binary folder - /// - /// steam of the zip file - /// - private static IEnumerable ExtractZipAndSave(Stream zipStream) - { - using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); - List files = new(); - foreach (var entry in archive.Entries) - { - if (entry.Name is "ffmpeg" or "ffmpeg.exe" or "ffprobe.exe" or "ffprobe" or "ffplay.exe" or "ffplay") - { - entry.ExtractToFile(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name), true); - files.Add(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name)); - } - } - - return files; - } - - #region FFbinaries api - - private class DownloadInfo - { - [JsonPropertyName("ffmpeg")] public string? FFMpeg { get; set; } - - [JsonPropertyName("ffprobe")] public string? FFProbe { get; set; } - - [JsonPropertyName("ffplay")] public string? FFPlay { get; set; } - } - - private class BinaryInfo - { - [JsonPropertyName("windows-64")] public DownloadInfo? Windows64 { get; set; } - - [JsonPropertyName("windows-32")] public DownloadInfo? Windows32 { get; set; } - - [JsonPropertyName("linux-32")] public DownloadInfo? Linux32 { get; set; } - - [JsonPropertyName("linux-64")] public DownloadInfo? Linux64 { get; set; } - - [JsonPropertyName("linux-armhf")] public DownloadInfo? LinuxArmhf { get; set; } - - [JsonPropertyName("linux-armel")] public DownloadInfo? LinuxArmel { get; set; } - - [JsonPropertyName("linux-arm64")] public DownloadInfo? LinuxArm64 { get; set; } - - [JsonPropertyName("osx-64")] public DownloadInfo? Osx64 { get; set; } - - /// - /// Automatically get the compatible download info for current os and architecture - /// - /// - /// - /// - /// - public DownloadInfo? GetCompatibleDownloadInfo(PlatformOverride? platformOverride = null) - { - if (platformOverride is not null) - { - return platformOverride switch - { - PlatformOverride.Windows64 => Windows64, - PlatformOverride.Windows32 => Windows32, - PlatformOverride.Linux64 => Linux64, - PlatformOverride.Linux32 => Linux32, - PlatformOverride.LinuxArmhf => LinuxArmhf, - PlatformOverride.LinuxArmel => LinuxArmel, - PlatformOverride.LinuxArm64 => LinuxArm64, - PlatformOverride.Osx64 => Osx64, - _ => throw new ArgumentOutOfRangeException(nameof(platformOverride), platformOverride, null) - }; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return RuntimeInformation.OSArchitecture == Architecture.X64 ? Windows64 : Windows32; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return RuntimeInformation.OSArchitecture switch - { - Architecture.X86 => Linux32, - Architecture.X64 => Linux64, - Architecture.Arm => LinuxArmhf, - Architecture.Arm64 => LinuxArm64, - _ => LinuxArmel - }; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return Osx64; - } - - throw new PlatformNotSupportedException("Unsupported OS or Architecture"); - } - } - - private class VersionInfo - { - [JsonPropertyName("version")] public string? Version { get; set; } - - [JsonPropertyName("permalink")] public string? Permalink { get; set; } - - [JsonPropertyName("bin")] public BinaryInfo? BinaryInfo { get; set; } - } - - private static readonly Dictionary _FFBinariesAPIs = new() - { - { FFMpegVersions.Latest, "https://ffbinaries.com/api/v1/version/latest" }, - { FFMpegVersions.V6_1, "https://ffbinaries.com/api/v1/version/6.1" }, - { FFMpegVersions.V5_1, "https://ffbinaries.com/api/v1/version/5.1" }, - { FFMpegVersions.V4_4_1, "https://ffbinaries.com/api/v1/version/4.4.1" }, - { FFMpegVersions.V4_2_1, "https://ffbinaries.com/api/v1/version/4.2.1" }, - { FFMpegVersions.V4_2, "https://ffbinaries.com/api/v1/version/4.2" }, - { FFMpegVersions.V4_1, "https://ffbinaries.com/api/v1/version/4.1" }, - { FFMpegVersions.V4_0, "https://ffbinaries.com/api/v1/version/4.0" }, - { FFMpegVersions.V3_4, "https://ffbinaries.com/api/v1/version/3.4" }, - { FFMpegVersions.V3_3, "https://ffbinaries.com/api/v1/version/3.3" }, - { FFMpegVersions.V3_2, "https://ffbinaries.com/api/v1/version/3.2" } - }; - - /// - /// Get version info from ffbinaries.com - /// - /// use to explicitly state the version of ffmpeg you want - /// - /// - private static async Task GetVersionInfo(FFMpegVersions version) - { - if (!_FFBinariesAPIs.TryGetValue(version, out var versionUri)) - { - throw new FFMpegDownloaderException($"Invalid version selected: {version}", "contact dev"); - } - - HttpClient client = new(); - var response = await client.GetAsync(versionUri); - - if (!response.IsSuccessStatusCode) - { - throw new FFMpegDownloaderException($"Failed to get version info from {versionUri}", "network error"); - } - - var jsonString = await response.Content.ReadAsStringAsync(); - var versionInfo = JsonSerializer.Deserialize(jsonString); - - return versionInfo ?? - throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString); - } - - #endregion } -/// -/// Custom exception for FFMpegDownloader -/// -public class FFMpegDownloaderException : Exception -{ - public FFMpegDownloaderException(string message) : base(message) - { - } - - public FFMpegDownloaderException(string message, string detail) : base(message) - { - Detail = detail; - } - - public string Detail { get; set; } = ""; -} diff --git a/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs b/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs new file mode 100644 index 0000000..e855b2d --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs @@ -0,0 +1,82 @@ +using System.Runtime.InteropServices; +using System.Text.Json.Serialization; +using FFMpegCore.Extensions.Downloader.Enums; + +namespace FFMpegCore.Extensions.Downloader.Models; + +internal record BinaryInfo +{ + [JsonPropertyName("windows-64")] + public DownloadInfo? Windows64 { get; set; } + + [JsonPropertyName("windows-32")] + public DownloadInfo? Windows32 { get; set; } + + [JsonPropertyName("linux-32")] + public DownloadInfo? Linux32 { get; set; } + + [JsonPropertyName("linux-64")] + public DownloadInfo? Linux64 { get; set; } + + [JsonPropertyName("linux-armhf")] + public DownloadInfo? LinuxArmhf { get; set; } + + [JsonPropertyName("linux-armel")] + public DownloadInfo? LinuxArmel { get; set; } + + [JsonPropertyName("linux-arm64")] + public DownloadInfo? LinuxArm64 { get; set; } + + [JsonPropertyName("osx-64")] + public DownloadInfo? Osx64 { get; set; } + + /// + /// Automatically get the compatible download info for current os and architecture + /// + /// + /// + /// + /// + public DownloadInfo? GetCompatibleDownloadInfo(SupportedPlatforms? platformOverride = null) + { + if (platformOverride is not null) + { + return platformOverride switch + { + SupportedPlatforms.Windows64 => Windows64, + SupportedPlatforms.Windows32 => Windows32, + SupportedPlatforms.Linux64 => Linux64, + SupportedPlatforms.Linux32 => Linux32, + SupportedPlatforms.LinuxArmhf => LinuxArmhf, + SupportedPlatforms.LinuxArmel => LinuxArmel, + SupportedPlatforms.LinuxArm64 => LinuxArm64, + SupportedPlatforms.Osx64 => Osx64, + _ => throw new ArgumentOutOfRangeException(nameof(platformOverride), platformOverride, null) + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return RuntimeInformation.OSArchitecture == Architecture.X64 ? Windows64 : Windows32; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.OSArchitecture switch + { + Architecture.X86 => Linux32, + Architecture.X64 => Linux64, + Architecture.Arm => LinuxArmhf, + Architecture.Arm64 => LinuxArm64, + _ => LinuxArmel + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Osx64; + } + + throw new PlatformNotSupportedException("Unsupported OS or Architecture"); + } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs b/FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs new file mode 100644 index 0000000..2e3e692 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace FFMpegCore.Extensions.Downloader.Models; + +internal record DownloadInfo +{ + [JsonPropertyName("ffmpeg")] + public string? FFMpeg { get; set; } + + [JsonPropertyName("ffprobe")] + public string? FFProbe { get; set; } + + [JsonPropertyName("ffplay")] + public string? FFPlay { get; set; } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs b/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs new file mode 100644 index 0000000..490f431 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace FFMpegCore.Extensions.Downloader.Models; + +internal record VersionInfo +{ + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("permalink")] + public string? Permalink { get; set; } + + [JsonPropertyName("bin")] + public BinaryInfo? BinaryInfo { get; set; } +} diff --git a/FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs b/FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs new file mode 100644 index 0000000..155d925 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs @@ -0,0 +1,75 @@ +using System.IO.Compression; +using System.Net; +using System.Text.Json; +using FFMpegCore.Extensions.Downloader.Enums; +using FFMpegCore.Extensions.Downloader.Exceptions; +using FFMpegCore.Extensions.Downloader.Extensions; +using FFMpegCore.Extensions.Downloader.Models; + +namespace FFMpegCore.Extensions.Downloader.Services; + +/// +/// Service to interact with ffbinaries.com API +/// +internal class FFbinariesService +{ + /// + /// Get version info from ffbinaries.com + /// + /// use to explicitly state the version of ffmpeg you want + /// + /// + internal static async Task GetVersionInfo(FFMpegVersions version) + { + var versionUri = version.GetDescription(); + + HttpClient client = new(); + var response = await client.GetAsync(versionUri); + + if (!response.IsSuccessStatusCode) + { + throw new FFMpegDownloaderException($"Failed to get version info from {versionUri}", "network error"); + } + + var jsonString = await response.Content.ReadAsStringAsync(); + var versionInfo = JsonSerializer.Deserialize(jsonString); + + return versionInfo ?? + throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString); + } + + /// + /// Download file from uri + /// + /// uri of the file + /// + internal static MemoryStream DownloadFileAsSteam(Uri address) + { + var client = new WebClient(); + var fileStream = new MemoryStream(client.DownloadData(address)); + fileStream.Position = 0; + + return fileStream; + } + + /// + /// Extracts the binaries from the zip stream and saves them to the current binary folder + /// + /// steam of the zip file + /// + internal static IEnumerable ExtractZipAndSave(Stream zipStream) + { + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); + List files = new(); + foreach (var entry in archive.Entries) + { + if (entry.Name is "ffmpeg" or "ffmpeg.exe" or "ffprobe.exe" or "ffprobe" or "ffplay.exe" or "ffplay") + { + entry.ExtractToFile(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name), true); + files.Add(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name)); + } + } + + return files; + } +} diff --git a/FFMpegCore.Test/DownloaderTests.cs b/FFMpegCore.Test/DownloaderTests.cs index 5a89a59..1a86b4c 100644 --- a/FFMpegCore.Test/DownloaderTests.cs +++ b/FFMpegCore.Test/DownloaderTests.cs @@ -1,4 +1,5 @@ using FFMpegCore.Extensions.Downloader; +using FFMpegCore.Extensions.Downloader.Enums; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test; @@ -9,14 +10,14 @@ public class DownloaderTests [TestMethod] public void GetAllLatestSuiteTest() { - var binaries = FFMpegDownloader.DownloadFFMpegSuite(binaries: FFMpegDownloader.FFMpegBinaries.FFMpeg).Result; - Assert.IsTrue(binaries.Count == 1); // many platforms have only ffmpeg and ffprobe + var binaries = FFMpegDownloader.DownloadFFMpegSuite().Result; + Assert.IsTrue(binaries.Count == 2); // many platforms have only ffmpeg and ffprobe } [TestMethod] public void GetSpecificVersionTest() { - var binaries = FFMpegDownloader.DownloadFFMpegSuite(FFMpegDownloader.FFMpegVersions.V4_0, binaries: FFMpegDownloader.FFMpegBinaries.FFMpeg).Result; + var binaries = FFMpegDownloader.DownloadFFMpegSuite(FFMpegVersions.V4_0, binaries: FFMpegBinaries.FFMpeg).Result; Assert.IsTrue(binaries.Count == 1); } }