diff --git a/.gitignore b/.gitignore index d73045d5..26d5010f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ *.user *.userosscache *.sln.docstates -.idea/ - # Build results [Dd]ebug/ @@ -22,4 +20,4 @@ bld/ *.opencover.xml #Visual studio cache/options directory -.vs/ \ No newline at end of file +.vs/ diff --git a/.idea/.idea.YoutubeDownloader/.idea/.gitignore b/.idea/.idea.YoutubeDownloader/.idea/.gitignore new file mode 100644 index 00000000..60f7d5ea --- /dev/null +++ b/.idea/.idea.YoutubeDownloader/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.YoutubeDownloader.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.YoutubeDownloader/.idea/.name b/.idea/.idea.YoutubeDownloader/.idea/.name new file mode 100644 index 00000000..d8eeee7d --- /dev/null +++ b/.idea/.idea.YoutubeDownloader/.idea/.name @@ -0,0 +1 @@ +YoutubeDownloader \ No newline at end of file diff --git a/.idea/.idea.YoutubeDownloader/.idea/encodings.xml b/.idea/.idea.YoutubeDownloader/.idea/encodings.xml new file mode 100644 index 00000000..df87cf95 --- /dev/null +++ b/.idea/.idea.YoutubeDownloader/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.YoutubeDownloader/.idea/indexLayout.xml b/.idea/.idea.YoutubeDownloader/.idea/indexLayout.xml new file mode 100644 index 00000000..7b08163c --- /dev/null +++ b/.idea/.idea.YoutubeDownloader/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.YoutubeDownloader/.idea/vcs.xml b/.idea/.idea.YoutubeDownloader/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/.idea.YoutubeDownloader/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Changelog.md b/Changelog.md index 8fc9fb32..a5499de0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,26 @@ https://github.com/Etherna/YoutubeExplode/releases # Changelog +## v6.3.7 (09-Nov-2023) + +- [Converter] Fixed an issue where subtitles embedded via `VideoClient.DownloadAsync(...)` had their languages specified using ISO 639-1 codes instead of ISO 639-2 codes. + +## v6.3.6 (17-Oct-2023) + +- Fixed an issue where calling `VideoClient.GetAsync(...)` on certain videos failed with an exception due to recent YouTube changes. + +## v6.3.5 (28-Sep-2023) + +- Added support for parsing live video URLs (i.e. `youtube.com/live/...`). (Thanks [@eimigueloliveir](https://github.com/eimigueloliveir)) + +## v6.3.4 (06-Sep-2023) + +- Fixed an issue where calling any method on `SearchClient` resulted in an exception on mobile devices. (Thanks [@jerry08](https://github.com/jerry08)) + +## v6.3.3 (31-Aug-2023) + +- Fixed an issue where calling `ChannelClient.GetAsync(...)` and `PlaylistClient.GetAsync(...)` failed on some channels and playlists due to recent YouTube changes. (Thanks [@tmm360](https://github.com/tmm360)) + ## v6.3.2 (18-Aug-2023) - Fixed an issue where calling `StreamClient.GetManifestAsync(...)` failed on videos from the "YouTube Movies & TV" system channel. diff --git a/Directory.Build.props b/Directory.Build.props index 78fb6d97..3ee8007d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,14 +1,16 @@ - Etherna Sagl - Copyright (C) Oleksii Holub, Copyright (C) 2023 Etherna Sagl + Etherna SA + Copyright (C) Oleksii Holub, Copyright (C) 2023 Etherna SA latest enable - nullable + true false true false + + $(NoWarn);CS1591 diff --git a/LICENSE b/LICENSE index e5bb5517..6003f645 100644 --- a/LICENSE +++ b/LICENSE @@ -4,7 +4,7 @@ YoutubeExplode Copyright (C) 2016-2023 Oleksii Holub Etherna YoutubeDownloader fork -Copyright (C) 2023-present Etherna Sagl +Copyright (C) 2023-present Etherna SA Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Readme.md b/Readme.md index 223f7436..96c04a03 100644 --- a/Readme.md +++ b/Readme.md @@ -122,6 +122,10 @@ var stream = await youtube.Videos.Streams.GetAsync(streamInfo); await youtube.Videos.Streams.DownloadAsync(streamInfo, $"video.{streamInfo.Container}"); ``` +> **Warning**: +> While the `Url` property in the stream metadata can be used to access the underlying content, you need a series of carefully crafted HTTP requests in order to do so. +> It's highly recommended to use `Videos.Streams.GetAsync(...)` or `Videos.Streams.DownloadAsync(...)` instead, as they will all the heavy lifting for you. + #### Downloading closed captions Closed captions can be downloaded in a similar way to media streams. diff --git a/YoutubeDownloader.Converter.Tests/GeneralSpecs.cs b/YoutubeDownloader.Converter.Tests/GeneralSpecs.cs index ad0cdedf..d67d3cc1 100644 --- a/YoutubeDownloader.Converter.Tests/GeneralSpecs.cs +++ b/YoutubeDownloader.Converter.Tests/GeneralSpecs.cs @@ -16,8 +16,7 @@ public class GeneralSpecs : IAsyncLifetime { private readonly ITestOutputHelper _testOutput; - public GeneralSpecs(ITestOutputHelper testOutput) => - _testOutput = testOutput; + public GeneralSpecs(ITestOutputHelper testOutput) => _testOutput = testOutput; public async Task InitializeAsync() => await FFmpeg.InitializeAsync(); @@ -114,16 +113,23 @@ public async Task I_can_download_a_video_as_a_single_mp4_file_with_multiple_stre .Take(3) .ToArray(); - await youtube.Videos.DownloadAsync( - videoStreamInfos.Concat(audioStreamInfos).ToArray(), - new ConversionRequestBuilder(filePath).Build() - ); + await youtube + .Videos + .DownloadAsync( + videoStreamInfos.Concat(audioStreamInfos).ToArray(), + new ConversionRequestBuilder(filePath).Build() + ); // Assert MediaFormat.IsMp4File(filePath).Should().BeTrue(); foreach (var streamInfo in videoStreamInfos) - FileEx.ContainsBytes(filePath, Encoding.ASCII.GetBytes(streamInfo.VideoQuality.Label)).Should().BeTrue(); + { + FileEx + .ContainsBytes(filePath, Encoding.ASCII.GetBytes(streamInfo.VideoQuality.Label)) + .Should() + .BeTrue(); + } } [Fact] @@ -153,16 +159,23 @@ public async Task I_can_download_a_video_as_a_single_webm_file_with_multiple_str .Take(3) .ToArray(); - await youtube.Videos.DownloadAsync( - videoStreamInfos.Concat(audioStreamInfos).ToArray(), - new ConversionRequestBuilder(filePath).Build() - ); + await youtube + .Videos + .DownloadAsync( + videoStreamInfos.Concat(audioStreamInfos).ToArray(), + new ConversionRequestBuilder(filePath).Build() + ); // Assert MediaFormat.IsWebMFile(filePath).Should().BeTrue(); foreach (var streamInfo in videoStreamInfos) - FileEx.ContainsBytes(filePath, Encoding.ASCII.GetBytes(streamInfo.VideoQuality.Label)).Should().BeTrue(); + { + FileEx + .ContainsBytes(filePath, Encoding.ASCII.GetBytes(streamInfo.VideoQuality.Label)) + .Should() + .BeTrue(); + } } [Fact] @@ -175,11 +188,16 @@ public async Task I_can_download_a_video_using_custom_conversion_settings() var filePath = Path.Combine(dir.Path, "video.mp3"); // Act - await youtube.Videos.DownloadAsync("9bZkp7q19f0", filePath, o => o - .SetFFmpegPath(FFmpeg.FilePath) - .SetContainer("mp4") - .SetPreset(ConversionPreset.UltraFast) - ); + await youtube + .Videos + .DownloadAsync( + "9bZkp7q19f0", + filePath, + o => + o.SetFFmpegPath(FFmpeg.FilePath) + .SetContainer("mp4") + .SetPreset(ConversionPreset.UltraFast) + ); // Assert MediaFormat.IsMp4File(filePath).Should().BeTrue(); @@ -208,4 +226,4 @@ public async Task I_can_download_a_video_while_tracking_progress() foreach (var value in progressValues) _testOutput.WriteLine($"Progress: {value:P2}"); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/SubtitleSpecs.cs b/YoutubeDownloader.Converter.Tests/SubtitleSpecs.cs index c37ba323..845dd628 100644 --- a/YoutubeDownloader.Converter.Tests/SubtitleSpecs.cs +++ b/YoutubeDownloader.Converter.Tests/SubtitleSpecs.cs @@ -36,17 +36,20 @@ public async Task I_can_download_a_video_as_a_single_mp4_file_with_subtitles() var trackInfos = trackManifest.Tracks; // Act - await youtube.Videos.DownloadAsync( - streamInfos, - trackInfos, - new ConversionRequestBuilder(filePath).Build() - ); + await youtube + .Videos + .DownloadAsync(streamInfos, trackInfos, new ConversionRequestBuilder(filePath).Build()); // Assert MediaFormat.IsMp4File(filePath).Should().BeTrue(); foreach (var trackInfo in trackInfos) - FileEx.ContainsBytes(filePath, Encoding.ASCII.GetBytes(trackInfo.Language.Name)).Should().BeTrue(); + { + FileEx + .ContainsBytes(filePath, Encoding.ASCII.GetBytes(trackInfo.Language.Name)) + .Should() + .BeTrue(); + } } [Fact] @@ -70,16 +73,19 @@ public async Task I_can_download_a_video_as_a_single_webm_file_with_subtitles() var trackInfos = trackManifest.Tracks; // Act - await youtube.Videos.DownloadAsync( - streamInfos, - trackInfos, - new ConversionRequestBuilder(filePath).Build() - ); + await youtube + .Videos + .DownloadAsync(streamInfos, trackInfos, new ConversionRequestBuilder(filePath).Build()); // Assert MediaFormat.IsWebMFile(filePath).Should().BeTrue(); foreach (var trackInfo in trackInfos) - FileEx.ContainsBytes(filePath, Encoding.ASCII.GetBytes(trackInfo.Language.Name)).Should().BeTrue(); + { + FileEx + .ContainsBytes(filePath, Encoding.ASCII.GetBytes(trackInfo.Language.Name)) + .Should() + .BeTrue(); + } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/Utils/Extensions/HttpExtensions.cs b/YoutubeDownloader.Converter.Tests/Utils/Extensions/HttpExtensions.cs index b3a8e6b9..235b190c 100644 --- a/YoutubeDownloader.Converter.Tests/Utils/Extensions/HttpExtensions.cs +++ b/YoutubeDownloader.Converter.Tests/Utils/Extensions/HttpExtensions.cs @@ -16,4 +16,4 @@ public static async Task DownloadAsync(this HttpClient http, string url, string await source.CopyToAsync(destination); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/Utils/FFmpeg.cs b/YoutubeDownloader.Converter.Tests/Utils/FFmpeg.cs index c132d518..628bad0d 100644 --- a/YoutubeDownloader.Converter.Tests/Utils/FFmpeg.cs +++ b/YoutubeDownloader.Converter.Tests/Utils/FFmpeg.cs @@ -16,24 +16,24 @@ public static class FFmpeg { private static readonly SemaphoreSlim Lock = new(1, 1); - public static Version Version { get; } = new(4, 4, 1); + public static Version Version { get; } = new(6, 0); private static string FileName { get; } = - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "ffmpeg.exe" - : "ffmpeg"; + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "ffmpeg.exe" : "ffmpeg"; - public static string FilePath { get; } = Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Directory.GetCurrentDirectory(), - FileName - ); + public static string FilePath { get; } = + Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? Directory.GetCurrentDirectory(), + FileName + ); private static string GetDownloadUrl() { static string GetPlatformMoniker() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "win"; + return "windows"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux"; @@ -47,16 +47,13 @@ static string GetPlatformMoniker() static string GetArchitectureMoniker() { if (RuntimeInformation.ProcessArchitecture == Architecture.X64) - return "64"; + return "x64"; if (RuntimeInformation.ProcessArchitecture == Architecture.X86) - return "32"; + return "x86"; if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - return "arm-64"; - - if (RuntimeInformation.ProcessArchitecture == Architecture.Arm) - return "arm"; + return "arm64"; throw new NotSupportedException("Unsupported architecture."); } @@ -64,7 +61,7 @@ static string GetArchitectureMoniker() var plat = GetPlatformMoniker(); var arch = GetArchitectureMoniker(); - return $"https://github.com/vot/ffbinaries-prebuilt/releases/download/v{Version}/ffmpeg-{Version}-{plat}-{arch}.zip"; + return $"https://github.com/Tyrrrz/FFmpegBin/releases/download/{Version}/ffmpeg-{plat}-{arch}.zip"; } private static byte[] GetDownloadHash() @@ -72,27 +69,30 @@ private static byte[] GetDownloadHash() static string GetHashString() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Only x64 build is available - return "d1124593b7453fc54dd90ca3819dc82c22ffa957937f33dd650082f1a495b10e"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { if (RuntimeInformation.ProcessArchitecture == Architecture.X64) - return "4348301b0d5e18174925e2022da1823aebbdb07282bbe9adb64b2485e1ef2df7"; + return "29289b1008a8fadbb012e7dc0e325fea9eebbe87ac2019a4fa7df7fc15af02d0"; if (RuntimeInformation.ProcessArchitecture == Architecture.X86) - return "a292731806fe3733b9e2281edba881d1035e4018599577174a54e275c0afc931"; + return "edc8c9bda8a10e138386cd9b6953127906bde0f89d2b872cf8e9046d3c559b28"; if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - return "7d57e730cc34208743cc1a97134541656ecd2c3adcdfad450dedb61d465857da"; + return "dfd42f47c47559ccb594965f897530bb9daa62d4ce6883c3f4082b7d037832d1"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + return "0b7808c8f93a3235efc2448c33086e8ce10295999bd93a40b060fbe7f2e92338"; } if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - // Only x64 build is available - return "e08c670fcbdc2e627aa4c0d0c5ee1ef20e82378af2f14e4e7ae421a148bd49af"; + if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + return "7898153f5785a739b1314ef3fb9c511be26bc7879d972c301a170e6ab8027652"; + + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return "a26adea0b56376df8c46118c15ae478ba02e9ac57097f569a32100760cea1cd2"; } throw new NotSupportedException("Unsupported architecture."); @@ -110,10 +110,10 @@ static string GetHashString() private static async ValueTask DownloadAsync() { using var archiveFile = TempFile.Create(); - using var httpClient = new HttpClient(); + using var http = new HttpClient(); // Download the archive - await httpClient.DownloadAsync(GetDownloadUrl(), archiveFile.Path); + await http.DownloadAsync(GetDownloadUrl(), archiveFile.Path); // Verify the hash await using (var archiveStream = File.OpenRead(archiveFile.Path)) @@ -129,8 +129,10 @@ private static async ValueTask DownloadAsync() using (var zip = ZipFile.OpenRead(archiveFile.Path)) { var entry = - zip.GetEntry(FileName) ?? - throw new FileNotFoundException("Downloaded archive doesn't contain the FFmpeg executable."); + zip.GetEntry(FileName) + ?? throw new FileNotFoundException( + "Downloaded archive doesn't contain the FFmpeg executable." + ); entry.ExtractToFile(FilePath, true); } @@ -159,4 +161,4 @@ public static async ValueTask InitializeAsync() Lock.Release(); } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/Utils/FileEx.cs b/YoutubeDownloader.Converter.Tests/Utils/FileEx.cs index 9f09a63b..0467b6a5 100644 --- a/YoutubeDownloader.Converter.Tests/Utils/FileEx.cs +++ b/YoutubeDownloader.Converter.Tests/Utils/FileEx.cs @@ -28,4 +28,4 @@ public static bool ContainsBytes(string filePath, byte[] data) return false; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/Utils/MediaFormat.cs b/YoutubeDownloader.Converter.Tests/Utils/MediaFormat.cs index e7486575..74f640b1 100644 --- a/YoutubeDownloader.Converter.Tests/Utils/MediaFormat.cs +++ b/YoutubeDownloader.Converter.Tests/Utils/MediaFormat.cs @@ -92,4 +92,4 @@ public static bool IsOggFile(string filePath) return true; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/Utils/TempDir.cs b/YoutubeDownloader.Converter.Tests/Utils/TempDir.cs index 368e48ba..724555d8 100644 --- a/YoutubeDownloader.Converter.Tests/Utils/TempDir.cs +++ b/YoutubeDownloader.Converter.Tests/Utils/TempDir.cs @@ -9,8 +9,7 @@ internal partial class TempDir : IDisposable { public string Path { get; } - public TempDir(string path) => - Path = path; + public TempDir(string path) => Path = path; public void Dispose() { @@ -18,9 +17,7 @@ public void Dispose() { Directory.Delete(Path, true); } - catch (DirectoryNotFoundException) - { - } + catch (DirectoryNotFoundException) { } } } @@ -29,7 +26,8 @@ internal partial class TempDir public static TempDir Create() { var dirPath = PathEx.Combine( - PathEx.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Directory.GetCurrentDirectory(), + PathEx.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? Directory.GetCurrentDirectory(), "Temp", Guid.NewGuid().ToString() ); @@ -38,4 +36,4 @@ public static TempDir Create() return new TempDir(dirPath); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/Utils/TempFile.cs b/YoutubeDownloader.Converter.Tests/Utils/TempFile.cs index f50a152b..c125501d 100644 --- a/YoutubeDownloader.Converter.Tests/Utils/TempFile.cs +++ b/YoutubeDownloader.Converter.Tests/Utils/TempFile.cs @@ -9,8 +9,7 @@ internal partial class TempFile : IDisposable { public string Path { get; } - public TempFile(string path) => - Path = path; + public TempFile(string path) => Path = path; public void Dispose() { @@ -18,9 +17,7 @@ public void Dispose() { File.Delete(Path); } - catch (FileNotFoundException) - { - } + catch (FileNotFoundException) { } } } @@ -29,17 +26,15 @@ internal partial class TempFile public static TempFile Create() { var dirPath = PathEx.Combine( - PathEx.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Directory.GetCurrentDirectory(), + PathEx.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? Directory.GetCurrentDirectory(), "Temp" ); Directory.CreateDirectory(dirPath); - var filePath = PathEx.Combine( - dirPath, - Guid.NewGuid() + ".tmp" - ); + var filePath = PathEx.Combine(dirPath, Guid.NewGuid() + ".tmp"); return new TempFile(filePath); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter.Tests/YoutubeDownloader.Converter.Tests.csproj b/YoutubeDownloader.Converter.Tests/YoutubeDownloader.Converter.Tests.csproj index 48c3e715..a08c49fb 100644 --- a/YoutubeDownloader.Converter.Tests/YoutubeDownloader.Converter.Tests.csproj +++ b/YoutubeDownloader.Converter.Tests/YoutubeDownloader.Converter.Tests.csproj @@ -10,12 +10,13 @@ - - + + + - - - + + + diff --git a/YoutubeDownloader.Converter/ConversionExtensions.cs b/YoutubeDownloader.Converter/ConversionExtensions.cs index 5d363a8c..ae74178d 100644 --- a/YoutubeDownloader.Converter/ConversionExtensions.cs +++ b/YoutubeDownloader.Converter/ConversionExtensions.cs @@ -27,11 +27,14 @@ private static async IAsyncEnumerable GetOptimalStreamInfosAsync( this VideoClient videoClient, VideoId videoId, Container container, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { var streamManifest = await videoClient.Streams.GetManifestAsync(videoId, cancellationToken); - if (streamManifest.GetAudioOnlyStreams().Any() && streamManifest.GetVideoOnlyStreams().Any()) + if ( + streamManifest.GetAudioOnlyStreams().Any() && streamManifest.GetVideoOnlyStreams().Any() + ) { // Include audio stream // Priority: transcoding -> bitrate @@ -73,7 +76,8 @@ public static async ValueTask DownloadAsync( IReadOnlyList closedCaptionTrackInfos, ConversionRequest request, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var ffmpeg = new FFmpeg(request.FFmpegCliFilePath); var converter = new Converter(videoClient, ffmpeg, request.Preset); @@ -96,7 +100,8 @@ public static async ValueTask DownloadAsync( IReadOnlyList streamInfos, ConversionRequest request, IProgress? progress = null, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default + ) => await videoClient.DownloadAsync( streamInfos, Array.Empty(), @@ -114,9 +119,14 @@ public static async ValueTask DownloadAsync( VideoId videoId, ConversionRequest request, IProgress? progress = null, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default + ) => await videoClient.DownloadAsync( - await videoClient.GetOptimalStreamInfosAsync(videoId, request.Container, cancellationToken), + await videoClient.GetOptimalStreamInfosAsync( + videoId, + request.Container, + cancellationToken + ), request, progress, cancellationToken @@ -135,7 +145,8 @@ public static async ValueTask DownloadAsync( string outputFilePath, Action configure, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var requestBuilder = new ConversionRequestBuilder(outputFilePath); configure(requestBuilder); @@ -157,6 +168,13 @@ public static async ValueTask DownloadAsync( VideoId videoId, string outputFilePath, IProgress? progress = null, - CancellationToken cancellationToken = default) => - await videoClient.DownloadAsync(videoId, outputFilePath, _ => { }, progress, cancellationToken); -} \ No newline at end of file + CancellationToken cancellationToken = default + ) => + await videoClient.DownloadAsync( + videoId, + outputFilePath, + _ => { }, + progress, + cancellationToken + ); +} diff --git a/YoutubeDownloader.Converter/ConversionFormat.cs b/YoutubeDownloader.Converter/ConversionFormat.cs index 75ad571b..c911bfa3 100644 --- a/YoutubeDownloader.Converter/ConversionFormat.cs +++ b/YoutubeDownloader.Converter/ConversionFormat.cs @@ -27,4 +27,4 @@ public readonly struct ConversionFormat /// public override string ToString() => Name; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/ConversionPreset.cs b/YoutubeDownloader.Converter/ConversionPreset.cs index 3d77780c..b59aae97 100644 --- a/YoutubeDownloader.Converter/ConversionPreset.cs +++ b/YoutubeDownloader.Converter/ConversionPreset.cs @@ -35,4 +35,4 @@ public enum ConversionPreset /// Fastest conversion speed and biggest output file size. /// UltraFast = 3 -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/ConversionRequest.cs b/YoutubeDownloader.Converter/ConversionRequest.cs index e4f8aa66..876c6b4e 100644 --- a/YoutubeDownloader.Converter/ConversionRequest.cs +++ b/YoutubeDownloader.Converter/ConversionRequest.cs @@ -42,7 +42,8 @@ public ConversionRequest( string ffmpegCliFilePath, string outputFilePath, Container container, - ConversionPreset preset) + ConversionPreset preset + ) { FFmpegCliFilePath = ffmpegCliFilePath; OutputFilePath = outputFilePath; @@ -58,8 +59,7 @@ public ConversionRequest( string ffmpegCliFilePath, string outputFilePath, ConversionFormat format, - ConversionPreset preset) - : this(ffmpegCliFilePath, outputFilePath, new Container(format.Name), preset) - { - } -} \ No newline at end of file + ConversionPreset preset + ) + : this(ffmpegCliFilePath, outputFilePath, new Container(format.Name), preset) { } +} diff --git a/YoutubeDownloader.Converter/ConversionRequestBuilder.cs b/YoutubeDownloader.Converter/ConversionRequestBuilder.cs index 0fce0f4a..f6a7c7ed 100644 --- a/YoutubeDownloader.Converter/ConversionRequestBuilder.cs +++ b/YoutubeDownloader.Converter/ConversionRequestBuilder.cs @@ -20,13 +20,10 @@ public class ConversionRequestBuilder /// /// Initializes an instance of . /// - public ConversionRequestBuilder(string outputFilePath) => - _outputFilePath = outputFilePath; + public ConversionRequestBuilder(string outputFilePath) => _outputFilePath = outputFilePath; - private Container GetDefaultContainer() => new( - Path.GetExtension(_outputFilePath).TrimStart('.').NullIfWhiteSpace() ?? - "mp4" - ); + private Container GetDefaultContainer() => + new(Path.GetExtension(_outputFilePath).TrimStart('.').NullIfWhiteSpace() ?? "mp4"); /// /// Sets the path to the FFmpeg CLI. @@ -63,8 +60,7 @@ public ConversionRequestBuilder SetFormat(ConversionFormat format) => /// Sets the conversion format. /// [Obsolete("Use SetContainer instead."), ExcludeFromCodeCoverage] - public ConversionRequestBuilder SetFormat(string format) => - SetContainer(format); + public ConversionRequestBuilder SetFormat(string format) => SetContainer(format); /// /// Sets the conversion preset. @@ -78,10 +74,11 @@ public ConversionRequestBuilder SetPreset(ConversionPreset preset) /// /// Builds the resulting request. /// - public ConversionRequest Build() => new( - _ffmpegCliFilePath ?? FFmpeg.GetFilePath(), - _outputFilePath, - _container ?? GetDefaultContainer(), - _preset - ); -} \ No newline at end of file + public ConversionRequest Build() => + new( + _ffmpegCliFilePath ?? FFmpeg.GetFilePath(), + _outputFilePath, + _container ?? GetDefaultContainer(), + _preset + ); +} diff --git a/YoutubeDownloader.Converter/Converter.cs b/YoutubeDownloader.Converter/Converter.cs index 40d23b7d..8b6feed2 100644 --- a/YoutubeDownloader.Converter/Converter.cs +++ b/YoutubeDownloader.Converter/Converter.cs @@ -32,7 +32,8 @@ private async ValueTask ProcessAsync( IReadOnlyList streamInputs, IReadOnlyList subtitleInputs, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var arguments = new ArgumentsBuilder(); @@ -50,9 +51,7 @@ private async ValueTask ProcessAsync( arguments.Add("-map").Add(i); // Output format and encoding preset - arguments - .Add("-f").Add(container.Name) - .Add("-preset").Add(_preset); + arguments.Add("-f").Add(container.Name).Add("-preset").Add(_preset); // Avoid transcoding inputs that have the same container as the output { @@ -66,9 +65,7 @@ private async ValueTask ProcessAsync( { if (audioStreamInfo.Container == container) { - arguments - .Add($"-c:a:{lastAudioStreamIndex}") - .Add("copy"); + arguments.Add($"-c:a:{lastAudioStreamIndex}").Add("copy"); } lastAudioStreamIndex++; @@ -78,9 +75,7 @@ private async ValueTask ProcessAsync( { if (videoStreamInfo.Container == container) { - arguments - .Add($"-c:v:{lastVideoStreamIndex}") - .Add("copy"); + arguments.Add($"-c:v:{lastVideoStreamIndex}").Add("copy"); } lastVideoStreamIndex++; @@ -135,7 +130,9 @@ private async ValueTask ProcessAsync( { arguments .Add($"-metadata:s:v:{lastVideoStreamIndex++}") - .Add($"title={videoStreamInfo.VideoQuality.Label} | {videoStreamInfo.Bitrate}"); + .Add( + $"title={videoStreamInfo.VideoQuality.Label} | {videoStreamInfo.Bitrate}" + ); } } } @@ -145,7 +142,7 @@ private async ValueTask ProcessAsync( { arguments .Add($"-metadata:s:s:{i}") - .Add($"language={subtitleInput.Info.Language.Code}") + .Add($"language={subtitleInput.Info.Language.GetThreeLetterCode()}") .Add($"-metadata:s:s:{i}") .Add($"title={subtitleInput.Info.Language.Name}"); } @@ -153,13 +150,15 @@ private async ValueTask ProcessAsync( // Enable progress reporting arguments // Info log level is required to extract total stream duration - .Add("-loglevel").Add("info") + .Add("-loglevel") + .Add("info") .Add("-stats"); // Misc settings arguments .Add("-hide_banner") - .Add("-threads").Add(Environment.ProcessorCount) + .Add("-threads") + .Add(Environment.ProcessorCount) .Add("-nostdin") .Add("-y"); @@ -174,10 +173,13 @@ private async ValueTask PopulateStreamInputsAsync( IReadOnlyList streamInfos, ICollection streamInputs, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var progressMuxer = progress?.Pipe(p => new ProgressMuxer(p)); - var progresses = streamInfos.Select(s => progressMuxer?.CreateInput(s.Size.MegaBytes)).ToArray(); + var progresses = streamInfos + .Select(s => progressMuxer?.CreateInput(s.Size.MegaBytes)) + .ToArray(); var lastIndex = 0; @@ -190,12 +192,9 @@ private async ValueTask PopulateStreamInputsAsync( streamInputs.Add(streamInput); - await _videoClient.Streams.DownloadAsync( - streamInfo, - streamInput.FilePath, - streamProgress, - cancellationToken - ); + await _videoClient + .Streams + .DownloadAsync(streamInfo, streamInput.FilePath, streamProgress, cancellationToken); } progress?.Report(1); @@ -206,10 +205,13 @@ private async ValueTask PopulateSubtitleInputsAsync( IReadOnlyList closedCaptionTrackInfos, ICollection subtitleInputs, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var progressMuxer = progress?.Pipe(p => new ProgressMuxer(p)); - var progresses = closedCaptionTrackInfos.Select(_ => progressMuxer?.CreateInput()).ToArray(); + var progresses = closedCaptionTrackInfos + .Select(_ => progressMuxer?.CreateInput()) + .ToArray(); var lastIndex = 0; @@ -222,12 +224,9 @@ private async ValueTask PopulateSubtitleInputsAsync( subtitleInputs.Add(subtitleInput); - await _videoClient.ClosedCaptions.DownloadAsync( - trackInfo, - subtitleInput.FilePath, - trackProgress, - cancellationToken - ); + await _videoClient + .ClosedCaptions + .DownloadAsync(trackInfo, subtitleInput.FilePath, trackProgress, cancellationToken); } progress?.Report(1); @@ -239,7 +238,8 @@ public async ValueTask ProcessAsync( IReadOnlyList streamInfos, IReadOnlyList closedCaptionTrackInfos, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { if (!streamInfos.Any()) throw new InvalidOperationException("No streams provided."); @@ -249,9 +249,10 @@ public async ValueTask ProcessAsync( var streamDownloadProgress = progressMuxer?.CreateInput(); var subtitleDownloadProgress = progressMuxer?.CreateInput(0.01); var conversionProgress = progressMuxer?.CreateInput( - 0.05 + - // Increase weight for each stream that needs to be transcoded - 5 * streamInfos.Count(s => s.Container != container) + 0.05 + + + // Increase weight for each stream that needs to be transcoded + 5 * streamInfos.Count(s => s.Container != container) ); // Populate inputs @@ -347,4 +348,4 @@ public void Dispose() } } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/FFmpeg.cs b/YoutubeDownloader.Converter/FFmpeg.cs index dcc8919c..50179d9a 100644 --- a/YoutubeDownloader.Converter/FFmpeg.cs +++ b/YoutubeDownloader.Converter/FFmpeg.cs @@ -23,7 +23,8 @@ internal partial class FFmpeg public async ValueTask ExecuteAsync( string arguments, IProgress? progress, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var stdErrBuffer = new StringBuilder(); @@ -62,17 +63,19 @@ internal partial class FFmpeg public static string GetFilePath() => // Try to find FFmpeg in the probe directory Directory - .EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory ?? Directory.GetCurrentDirectory()) - .FirstOrDefault(f => - string.Equals( - Path.GetFileNameWithoutExtension(f), - "ffmpeg", - StringComparison.OrdinalIgnoreCase - ) - ) ?? - + .EnumerateFiles( + AppDomain.CurrentDomain.BaseDirectory ?? Directory.GetCurrentDirectory() + ) + .FirstOrDefault( + f => + string.Equals( + Path.GetFileNameWithoutExtension(f), + "ffmpeg", + StringComparison.OrdinalIgnoreCase + ) + ) // Otherwise fallback to just "ffmpeg" and hope it's on the PATH - "ffmpeg"; + ?? "ffmpeg"; private static PipeTarget CreateProgressRouter(IProgress progress) { @@ -88,14 +91,23 @@ private static PipeTarget CreateProgressRouter(IProgress progress) var totalDurationMatch = Regex.Match(line, @"Duration:\s(\d+):(\d+):(\d+\.\d+)"); if (totalDurationMatch.Success) { - var hours = int.Parse(totalDurationMatch.Groups[1].Value, CultureInfo.InvariantCulture); - var minutes = int.Parse(totalDurationMatch.Groups[2].Value, CultureInfo.InvariantCulture); - var seconds = double.Parse(totalDurationMatch.Groups[3].Value, CultureInfo.InvariantCulture); + var hours = int.Parse( + totalDurationMatch.Groups[1].Value, + CultureInfo.InvariantCulture + ); + var minutes = int.Parse( + totalDurationMatch.Groups[2].Value, + CultureInfo.InvariantCulture + ); + var seconds = double.Parse( + totalDurationMatch.Groups[3].Value, + CultureInfo.InvariantCulture + ); totalDuration = - TimeSpan.FromHours(hours) + - TimeSpan.FromMinutes(minutes) + - TimeSpan.FromSeconds(seconds); + TimeSpan.FromHours(hours) + + TimeSpan.FromMinutes(minutes) + + TimeSpan.FromSeconds(seconds); } } @@ -106,20 +118,30 @@ private static PipeTarget CreateProgressRouter(IProgress progress) var processedDurationMatch = Regex.Match(line, @"time=(\d+):(\d+):(\d+\.\d+)"); if (processedDurationMatch.Success) { - var hours = int.Parse(processedDurationMatch.Groups[1].Value, CultureInfo.InvariantCulture); - var minutes = int.Parse(processedDurationMatch.Groups[2].Value, CultureInfo.InvariantCulture); - var seconds = double.Parse(processedDurationMatch.Groups[3].Value, CultureInfo.InvariantCulture); + var hours = int.Parse( + processedDurationMatch.Groups[1].Value, + CultureInfo.InvariantCulture + ); + var minutes = int.Parse( + processedDurationMatch.Groups[2].Value, + CultureInfo.InvariantCulture + ); + var seconds = double.Parse( + processedDurationMatch.Groups[3].Value, + CultureInfo.InvariantCulture + ); var processedDuration = - TimeSpan.FromHours(hours) + - TimeSpan.FromMinutes(minutes) + - TimeSpan.FromSeconds(seconds); - - progress.Report(( - processedDuration.TotalMilliseconds / - totalDuration.Value.TotalMilliseconds - ).Clamp(0, 1)); + TimeSpan.FromHours(hours) + + TimeSpan.FromMinutes(minutes) + + TimeSpan.FromSeconds(seconds); + + progress.Report( + ( + processedDuration.TotalMilliseconds / totalDuration.Value.TotalMilliseconds + ).Clamp(0, 1) + ); } }); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/Utils/Extensions/AsyncCollectionExtensions.cs b/YoutubeDownloader.Converter/Utils/Extensions/AsyncCollectionExtensions.cs index 21d1adad..da45dc80 100644 --- a/YoutubeDownloader.Converter/Utils/Extensions/AsyncCollectionExtensions.cs +++ b/YoutubeDownloader.Converter/Utils/Extensions/AsyncCollectionExtensions.cs @@ -18,4 +18,4 @@ public static async ValueTask> ToListAsync(this IAsyncEnumerable s public static ValueTaskAwaiter> GetAwaiter(this IAsyncEnumerable source) => source.ToListAsync().GetAwaiter(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/Utils/Extensions/CollectionExtensions.cs b/YoutubeDownloader.Converter/Utils/Extensions/CollectionExtensions.cs index a82aab72..de6f0d02 100644 --- a/YoutubeDownloader.Converter/Utils/Extensions/CollectionExtensions.cs +++ b/YoutubeDownloader.Converter/Utils/Extensions/CollectionExtensions.cs @@ -10,4 +10,4 @@ internal static class CollectionExtensions foreach (var o in source) yield return (o, i++); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/Utils/Extensions/GenericExtensions.cs b/YoutubeDownloader.Converter/Utils/Extensions/GenericExtensions.cs index d9e517ed..e1dd28cd 100644 --- a/YoutubeDownloader.Converter/Utils/Extensions/GenericExtensions.cs +++ b/YoutubeDownloader.Converter/Utils/Extensions/GenericExtensions.cs @@ -4,12 +4,14 @@ namespace YoutubeExplode.Converter.Utils.Extensions; internal static class GenericExtensions { - public static TOut Pipe(this TIn input, Func transform) => transform(input); + public static TOut Pipe(this TIn input, Func transform) => + transform(input); - public static T Clamp(this T value, T min, T max) where T : IComparable => + public static T Clamp(this T value, T min, T max) + where T : IComparable => value.CompareTo(min) <= 0 ? min : value.CompareTo(max) >= 0 ? max : value; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/Utils/Extensions/LanguageExtensions.cs b/YoutubeDownloader.Converter/Utils/Extensions/LanguageExtensions.cs new file mode 100644 index 00000000..36aa4489 --- /dev/null +++ b/YoutubeDownloader.Converter/Utils/Extensions/LanguageExtensions.cs @@ -0,0 +1,203 @@ +using System; +using YoutubeExplode.Videos.ClosedCaptions; + +namespace YoutubeExplode.Converter.Utils.Extensions; + +internal static class LanguageExtensions +{ + public static string GetTwoLetterCode(this Language language) + { + var dashIndex = language.Code.IndexOf('-'); + return dashIndex >= 0 ? language.Code[..dashIndex] : language.Code; + } + + public static string GetThreeLetterCode(this Language language) => + language.GetTwoLetterCode().ToLowerInvariant() switch + { + "aa" => "aar", + "ab" => "abk", + "ae" => "ave", + "af" => "afr", + "ak" => "aka", + "am" => "amh", + "an" => "arg", + "ar" => "ara", + "as" => "asm", + "av" => "ava", + "ay" => "aym", + "az" => "aze", + "ba" => "bak", + "be" => "bel", + "bg" => "bul", + "bh" => "bih", + "bi" => "bis", + "bm" => "bam", + "bn" => "ben", + "bo" => "tib", + "br" => "bre", + "bs" => "bos", + "ca" => "cat", + "ce" => "che", + "ch" => "cha", + "co" => "cos", + "cr" => "cre", + "cs" => "cze", + "cu" => "chu", + "cv" => "chv", + "cy" => "wel", + "da" => "dan", + "de" => "ger", + "dv" => "div", + "dz" => "dzo", + "ee" => "ewe", + "el" => "gre", + "en" => "eng", + "eo" => "epo", + "es" => "spa", + "et" => "est", + "eu" => "baq", + "fa" => "per", + "ff" => "ful", + "fi" => "fin", + "fj" => "fij", + "fo" => "fao", + "fr" => "fre", + "fy" => "fry", + "ga" => "gle", + "gd" => "gla", + "gl" => "glg", + "gn" => "grn", + "gu" => "guj", + "gv" => "glv", + "ha" => "hau", + "he" => "heb", + "hi" => "hin", + "ho" => "hmo", + "hr" => "hrv", + "ht" => "hat", + "hu" => "hun", + "hy" => "arm", + "hz" => "her", + "ia" => "ina", + "id" => "ind", + "ie" => "ile", + "ig" => "ibo", + "ii" => "iii", + "ik" => "ipk", + "io" => "ido", + "is" => "ice", + "it" => "ita", + "iu" => "iku", + "ja" => "jpn", + "jv" => "jav", + "ka" => "geo", + "kg" => "kon", + "ki" => "kik", + "kj" => "kua", + "kk" => "kaz", + "kl" => "kal", + "km" => "khm", + "kn" => "kan", + "ko" => "kor", + "kr" => "kau", + "ks" => "kas", + "ku" => "kur", + "kv" => "kom", + "kw" => "cor", + "ky" => "kir", + "la" => "lat", + "lb" => "ltz", + "lg" => "lug", + "li" => "lim", + "ln" => "lin", + "lo" => "lao", + "lt" => "lit", + "lu" => "lub", + "lv" => "lav", + "mg" => "mlg", + "mh" => "mah", + "mi" => "mao", + "mk" => "mac", + "ml" => "mal", + "mn" => "mon", + "mr" => "mar", + "ms" => "may", + "mt" => "mlt", + "my" => "bur", + "na" => "nau", + "nb" => "nob", + "nd" => "nde", + "ne" => "nep", + "ng" => "ndo", + "nl" => "dut", + "nn" => "nno", + "no" => "nor", + "nr" => "nbl", + "nv" => "nav", + "ny" => "nya", + "oc" => "oci", + "oj" => "oji", + "om" => "orm", + "or" => "ori", + "os" => "oss", + "pa" => "pan", + "pi" => "pli", + "pl" => "pol", + "ps" => "pus", + "pt" => "por", + "qu" => "que", + "rm" => "roh", + "rn" => "run", + "ro" => "rum", + "ru" => "rus", + "rw" => "kin", + "sa" => "san", + "sc" => "srd", + "sd" => "snd", + "se" => "sme", + "sg" => "sag", + "si" => "sin", + "sk" => "slo", + "sl" => "slv", + "sm" => "smo", + "sn" => "sna", + "so" => "som", + "sq" => "alb", + "sr" => "srp", + "ss" => "ssw", + "st" => "sot", + "su" => "sun", + "sv" => "swe", + "sw" => "swa", + "ta" => "tam", + "te" => "tel", + "tg" => "tgk", + "th" => "tha", + "ti" => "tir", + "tk" => "tuk", + "tl" => "tgl", + "tn" => "tsn", + "to" => "ton", + "tr" => "tur", + "ts" => "tso", + "tt" => "tat", + "tw" => "twi", + "ty" => "tah", + "ug" => "uig", + "uk" => "ukr", + "ur" => "urd", + "uz" => "uzb", + "ve" => "ven", + "vi" => "vie", + "vo" => "vol", + "wa" => "wln", + "wo" => "wol", + "xh" => "xho", + "yi" => "yid", + "yo" => "yor", + "za" => "zha", + "zh" => "chi", + "zu" => "zul", + var code => throw new InvalidOperationException($"Unrecognized language code '{code}'.") + }; +} diff --git a/YoutubeDownloader.Converter/Utils/Extensions/StringExtensions.cs b/YoutubeDownloader.Converter/Utils/Extensions/StringExtensions.cs index d70ee19c..3be3a493 100644 --- a/YoutubeDownloader.Converter/Utils/Extensions/StringExtensions.cs +++ b/YoutubeDownloader.Converter/Utils/Extensions/StringExtensions.cs @@ -3,7 +3,5 @@ internal static class StringExtensions { public static string? NullIfWhiteSpace(this string s) => - !string.IsNullOrWhiteSpace(s) - ? s - : null; -} \ No newline at end of file + !string.IsNullOrWhiteSpace(s) ? s : null; +} diff --git a/YoutubeDownloader.Converter/Utils/ProgressMuxer.cs b/YoutubeDownloader.Converter/Utils/ProgressMuxer.cs index bb344abc..0c03a90c 100644 --- a/YoutubeDownloader.Converter/Utils/ProgressMuxer.cs +++ b/YoutubeDownloader.Converter/Utils/ProgressMuxer.cs @@ -11,8 +11,7 @@ internal class ProgressMuxer private readonly Dictionary _splitWeights = new(); private readonly Dictionary _splitValues = new(); - public ProgressMuxer(IProgress target) => - _target = target; + public ProgressMuxer(IProgress target) => _target = target; public IProgress CreateInput(double weight = 1) { @@ -43,4 +42,4 @@ public IProgress CreateInput(double weight = 1) }); } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Converter/YoutubeDownloader.Converter.csproj b/YoutubeDownloader.Converter/YoutubeDownloader.Converter.csproj index 3f7d57c3..aa069b4e 100644 --- a/YoutubeDownloader.Converter/YoutubeDownloader.Converter.csproj +++ b/YoutubeDownloader.Converter/YoutubeDownloader.Converter.csproj @@ -16,13 +16,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/YoutubeDownloader.Demo.Cli/Program.cs b/YoutubeDownloader.Demo.Cli/Program.cs index 516d0368..f8ec527c 100644 --- a/YoutubeDownloader.Demo.Cli/Program.cs +++ b/YoutubeDownloader.Demo.Cli/Program.cs @@ -46,4 +46,4 @@ public static async Task Main() Console.WriteLine("Done"); Console.WriteLine($"Video saved to '{fileName}'"); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Demo.Cli/Utils/ConsoleProgress.cs b/YoutubeDownloader.Demo.Cli/Utils/ConsoleProgress.cs index 412d6faa..3e002854 100644 --- a/YoutubeDownloader.Demo.Cli/Utils/ConsoleProgress.cs +++ b/YoutubeDownloader.Demo.Cli/Utils/ConsoleProgress.cs @@ -19,9 +19,7 @@ public ConsoleProgress(TextWriter writer) } public ConsoleProgress() - : this(Console.Out) - { - } + : this(Console.Out) { } private void EraseLast() { @@ -43,4 +41,4 @@ private void Write(string text) public void Report(double progress) => Write($"{progress:P1}"); public void Dispose() => EraseLast(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Demo.Cli/YoutubeDownloader.Demo.Cli.csproj b/YoutubeDownloader.Demo.Cli/YoutubeDownloader.Demo.Cli.csproj index d695ce88..9c28ab56 100644 --- a/YoutubeDownloader.Demo.Cli/YoutubeDownloader.Demo.Cli.csproj +++ b/YoutubeDownloader.Demo.Cli/YoutubeDownloader.Demo.Cli.csproj @@ -5,6 +5,10 @@ net7.0 + + + + diff --git a/YoutubeDownloader.Demo.Gui/App.xaml.cs b/YoutubeDownloader.Demo.Gui/App.xaml.cs index d9ba7565..d7ef57c4 100644 --- a/YoutubeDownloader.Demo.Gui/App.xaml.cs +++ b/YoutubeDownloader.Demo.Gui/App.xaml.cs @@ -1,5 +1,3 @@ namespace YoutubeExplode.Demo.Gui; -public partial class App -{ -} \ No newline at end of file +public partial class App { } diff --git a/YoutubeDownloader.Demo.Gui/Converters/ArrayToStringConverter.cs b/YoutubeDownloader.Demo.Gui/Converters/ArrayToStringConverter.cs index 8118fe1d..5bb6bbbe 100644 --- a/YoutubeDownloader.Demo.Gui/Converters/ArrayToStringConverter.cs +++ b/YoutubeDownloader.Demo.Gui/Converters/ArrayToStringConverter.cs @@ -13,12 +13,13 @@ public class ArrayToStringConverter : IValueConverter public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) => value is IEnumerable enumerableValue - ? string.Join( - parameter as string ?? ", ", - enumerableValue.Cast() - ) + ? string.Join(parameter as string ?? ", ", enumerableValue.Cast()) : default; - public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) => - throw new NotSupportedException(); -} \ No newline at end of file + public object ConvertBack( + object? value, + Type targetType, + object parameter, + CultureInfo culture + ) => throw new NotSupportedException(); +} diff --git a/YoutubeDownloader.Demo.Gui/Converters/BoolToStringConverter.cs b/YoutubeDownloader.Demo.Gui/Converters/BoolToStringConverter.cs index 0d67aa0b..c0155a47 100644 --- a/YoutubeDownloader.Demo.Gui/Converters/BoolToStringConverter.cs +++ b/YoutubeDownloader.Demo.Gui/Converters/BoolToStringConverter.cs @@ -11,9 +11,15 @@ public class BoolToStringConverter : IValueConverter public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) => value is bool boolValue - ? boolValue ? "yes" : "no" + ? boolValue + ? "yes" + : "no" : default; - public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) => - throw new NotSupportedException(); -} \ No newline at end of file + public object ConvertBack( + object? value, + Type targetType, + object parameter, + CultureInfo culture + ) => throw new NotSupportedException(); +} diff --git a/YoutubeDownloader.Demo.Gui/Converters/BoolToVisibilityConverter.cs b/YoutubeDownloader.Demo.Gui/Converters/BoolToVisibilityConverter.cs index 2b8811f9..cc90a303 100644 --- a/YoutubeDownloader.Demo.Gui/Converters/BoolToVisibilityConverter.cs +++ b/YoutubeDownloader.Demo.Gui/Converters/BoolToVisibilityConverter.cs @@ -16,11 +16,13 @@ public object Convert(object? value, Type targetType, object parameter, CultureI ? visibilityParameter : Visibility.Hidden; - return value is true - ? Visibility.Visible - : falseVisibility; + return value is true ? Visibility.Visible : falseVisibility; } - public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) => - throw new NotSupportedException(); -} \ No newline at end of file + public object ConvertBack( + object? value, + Type targetType, + object parameter, + CultureInfo culture + ) => throw new NotSupportedException(); +} diff --git a/YoutubeDownloader.Demo.Gui/ViewModels/Framework/AsyncRelayCommand.cs b/YoutubeDownloader.Demo.Gui/ViewModels/Framework/AsyncRelayCommand.cs new file mode 100644 index 00000000..1760914e --- /dev/null +++ b/YoutubeDownloader.Demo.Gui/ViewModels/Framework/AsyncRelayCommand.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace YoutubeExplode.Demo.Gui.ViewModels.Framework; + +public class AsyncRelayCommand : RelayCommand +{ + public AsyncRelayCommand(Func executeAsync, Func canExecute) + : base(async x => await executeAsync(x), canExecute) { } + + public AsyncRelayCommand(Func executeAsync) + : this(executeAsync, _ => true) { } +} + +public class AsyncRelayCommand : RelayCommand +{ + public AsyncRelayCommand(Func executeAsync, Func canExecute) + : base(async () => await executeAsync(), canExecute) { } + + public AsyncRelayCommand(Func executeAsync) + : this(executeAsync, () => true) { } +} diff --git a/YoutubeDownloader.Demo.Gui/ViewModels/Framework/RelayCommand.cs b/YoutubeDownloader.Demo.Gui/ViewModels/Framework/RelayCommand.cs index 738d223f..e30dcc87 100644 --- a/YoutubeDownloader.Demo.Gui/ViewModels/Framework/RelayCommand.cs +++ b/YoutubeDownloader.Demo.Gui/ViewModels/Framework/RelayCommand.cs @@ -17,18 +17,15 @@ public RelayCommand(Action execute, Func canExecute) } public RelayCommand(Action execute) - : this(execute, _ => true) - { - } + : this(execute, _ => true) { } public bool CanExecute(object? parameter) => - _canExecute(parameter is not null ? (T) parameter : default!); + _canExecute(parameter is not null ? (T)parameter : default!); public void Execute(object? parameter) => - _execute(parameter is not null ? (T) parameter : default!); + _execute(parameter is not null ? (T)parameter : default!); - public void RaiseCanExecuteChanged() => - CanExecuteChanged?.Invoke(this, EventArgs.Empty); + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } public class RelayCommand : ICommand @@ -45,14 +42,11 @@ public RelayCommand(Action execute, Func canExecute) } public RelayCommand(Action execute) - : this(execute, () => true) - { - } + : this(execute, () => true) { } public bool CanExecute(object? parameter) => _canExecute(); public void Execute(object? parameter) => _execute(); - public void RaiseCanExecuteChanged() => - CanExecuteChanged?.Invoke(this, EventArgs.Empty); -} \ No newline at end of file + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/YoutubeDownloader.Demo.Gui/ViewModels/Framework/ViewModelBase.cs b/YoutubeDownloader.Demo.Gui/ViewModels/Framework/ViewModelBase.cs index f0ba4d41..118aabcb 100644 --- a/YoutubeDownloader.Demo.Gui/ViewModels/Framework/ViewModelBase.cs +++ b/YoutubeDownloader.Demo.Gui/ViewModels/Framework/ViewModelBase.cs @@ -19,4 +19,4 @@ protected void Set(ref T field, T value, [CallerMemberName] string? propertyN field = value; RaisePropertyChanged(propertyName); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Demo.Gui/ViewModels/MainViewModel.cs b/YoutubeDownloader.Demo.Gui/ViewModels/MainViewModel.cs index 6470997c..5c005bb5 100644 --- a/YoutubeDownloader.Demo.Gui/ViewModels/MainViewModel.cs +++ b/YoutubeDownloader.Demo.Gui/ViewModels/MainViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.Win32; using YoutubeExplode.Channels; using YoutubeExplode.Common; @@ -142,14 +143,14 @@ private set } public bool IsDataAvailable => - Video is not null && - VideoThumbnail is not null && - Channel is not null && - ChannelThumbnail is not null && - MuxedStreamInfos is not null && - AudioOnlyStreamInfos is not null && - VideoOnlyStreamInfos is not null && - ClosedCaptionTrackInfos is not null; + Video is not null + && VideoThumbnail is not null + && Channel is not null + && ChannelThumbnail is not null + && MuxedStreamInfos is not null + && AudioOnlyStreamInfos is not null + && VideoOnlyStreamInfos is not null + && ClosedCaptionTrackInfos is not null; public RelayCommand PullDataCommand { get; } @@ -159,18 +160,18 @@ VideoOnlyStreamInfos is not null && public MainViewModel() { - PullDataCommand = new RelayCommand( - PullData, + PullDataCommand = new AsyncRelayCommand( + PullDataAsync, () => !IsBusy && !string.IsNullOrWhiteSpace(Query) ); - DownloadStreamCommand = new RelayCommand( - DownloadStream, + DownloadStreamCommand = new AsyncRelayCommand( + DownloadStreamAsync, _ => !IsBusy ); - DownloadClosedCaptionTrackCommand = new RelayCommand( - DownloadClosedCaptionTrack, + DownloadClosedCaptionTrackCommand = new AsyncRelayCommand( + DownloadClosedCaptionTrackAsync, _ => !IsBusy ); } @@ -196,7 +197,7 @@ private static string SanitizeFileName(string fileName) return dialog.ShowDialog() == true ? dialog.FileName : null; } - private async void PullData() + private async Task PullDataAsync() { if (IsBusy || string.IsNullOrWhiteSpace(Query)) return; @@ -245,10 +246,7 @@ private async void PullData() var trackManifest = await _youtube.Videos.ClosedCaptions.GetManifestAsync(videoIdOrUrl); - ClosedCaptionTrackInfos = trackManifest - .Tracks - .OrderBy(t => t.Language.Name) - .ToArray(); + ClosedCaptionTrackInfos = trackManifest.Tracks.OrderBy(t => t.Language.Name).ToArray(); } finally { @@ -258,7 +256,7 @@ private async void PullData() } } - private async void DownloadStream(IStreamInfo streamInfo) + private async Task DownloadStreamAsync(IStreamInfo streamInfo) { if (IsBusy || Video is null) return; @@ -295,7 +293,7 @@ private async void DownloadStream(IStreamInfo streamInfo) } } - private async void DownloadClosedCaptionTrack(ClosedCaptionTrackInfo trackInfo) + private async Task DownloadClosedCaptionTrackAsync(ClosedCaptionTrackInfo trackInfo) { if (IsBusy || Video is null) return; @@ -318,7 +316,10 @@ private async void DownloadClosedCaptionTrack(ClosedCaptionTrackInfo trackInfo) var progressHandler = new Progress(p => Progress = p); // Download to file - await _youtube.Videos.ClosedCaptions.DownloadAsync(trackInfo, filePath, progressHandler); + await _youtube + .Videos + .ClosedCaptions + .DownloadAsync(trackInfo, filePath, progressHandler); } finally { @@ -327,4 +328,4 @@ private async void DownloadClosedCaptionTrack(ClosedCaptionTrackInfo trackInfo) Progress = 0; } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Demo.Gui/Views/MainWindow.xaml.cs b/YoutubeDownloader.Demo.Gui/Views/MainWindow.xaml.cs index 235a4a5e..2ff5836c 100644 --- a/YoutubeDownloader.Demo.Gui/Views/MainWindow.xaml.cs +++ b/YoutubeDownloader.Demo.Gui/Views/MainWindow.xaml.cs @@ -6,4 +6,4 @@ public MainWindow() { InitializeComponent(); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Demo.Gui/YoutubeDownloader.Demo.Gui.csproj b/YoutubeDownloader.Demo.Gui/YoutubeDownloader.Demo.Gui.csproj index f282bd5a..ab7166fc 100644 --- a/YoutubeDownloader.Demo.Gui/YoutubeDownloader.Demo.Gui.csproj +++ b/YoutubeDownloader.Demo.Gui/YoutubeDownloader.Demo.Gui.csproj @@ -7,6 +7,7 @@ + diff --git a/YoutubeDownloader.Tests/ChannelHandleSpecs.cs b/YoutubeDownloader.Tests/ChannelHandleSpecs.cs index d759536e..fda0fdd6 100644 --- a/YoutubeDownloader.Tests/ChannelHandleSpecs.cs +++ b/YoutubeDownloader.Tests/ChannelHandleSpecs.cs @@ -22,7 +22,10 @@ public void I_can_parse_a_channel_handle_from_a_handle_string(string channelHand [Theory] [InlineData("youtube.com/@BeauMiles", "BeauMiles")] [InlineData("youtube.com/@a-z.0_9", "a-z.0_9")] - public void I_can_parse_a_channel_handle_from_a_URL_string(string channelUrl, string expectedChannelHandle) + public void I_can_parse_a_channel_handle_from_a_URL_string( + string channelUrl, + string expectedChannelHandle + ) { // Act var parsed = ChannelHandle.Parse(channelUrl); @@ -45,4 +48,4 @@ public void I_cannot_parse_a_channel_handle_from_an_invalid_string(string channe // Act & assert Assert.Throws(() => ChannelHandle.Parse(channelHandleOrUrl)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/ChannelIdSpecs.cs b/YoutubeDownloader.Tests/ChannelIdSpecs.cs index 01f49156..900be5e5 100644 --- a/YoutubeDownloader.Tests/ChannelIdSpecs.cs +++ b/YoutubeDownloader.Tests/ChannelIdSpecs.cs @@ -23,7 +23,10 @@ public void I_can_parse_a_channel_ID_from_an_ID_string(string channelId) [InlineData("youtube.com/channel/UC3xnGqlcL3y-GXz5N3wiTJQ", "UC3xnGqlcL3y-GXz5N3wiTJQ")] [InlineData("youtube.com/channel/UCkQO3QsgTpNTsOw6ujimT5Q", "UCkQO3QsgTpNTsOw6ujimT5Q")] [InlineData("youtube.com/channel/UCQtjJDOYluum87LA4sI6xcg", "UCQtjJDOYluum87LA4sI6xcg")] - public void I_can_parse_a_channel_ID_from_a_URL_string(string channelUrl, string expectedChannelId) + public void I_can_parse_a_channel_ID_from_a_URL_string( + string channelUrl, + string expectedChannelId + ) { // Act var parsed = ChannelId.Parse(channelUrl); @@ -44,4 +47,4 @@ public void I_cannot_parse_a_channel_ID_from_an_invalid_string(string channelIdO // Act & assert Assert.Throws(() => ChannelId.Parse(channelIdOrUrl)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/ChannelSlugSpecs.cs b/YoutubeDownloader.Tests/ChannelSlugSpecs.cs index 67350448..cd3b3292 100644 --- a/YoutubeDownloader.Tests/ChannelSlugSpecs.cs +++ b/YoutubeDownloader.Tests/ChannelSlugSpecs.cs @@ -22,8 +22,14 @@ public void I_can_parse_a_channel_slug_from_a_slug_string(string channelSlug) [Theory] [InlineData("youtube.com/c/Tyrrrz", "Tyrrrz")] [InlineData("youtube.com/c/BlenderFoundation", "BlenderFoundation")] - [InlineData("youtube.com/c/%D0%9C%D0%B5%D0%BB%D0%B0%D0%BD%D1%96%D1%8F%D0%9F%D0%BE%D0%B4%D0%BE%D0%BB%D1%8F%D0%BA", "МеланіяПодоляк")] - public void I_can_parse_a_channel_slug_from_a_URL_string(string channelUrl, string expectedChannelSlug) + [InlineData( + "youtube.com/c/%D0%9C%D0%B5%D0%BB%D0%B0%D0%BD%D1%96%D1%8F%D0%9F%D0%BE%D0%B4%D0%BE%D0%BB%D1%8F%D0%BA", + "МеланіяПодоляк" + )] + public void I_can_parse_a_channel_slug_from_a_URL_string( + string channelUrl, + string expectedChannelSlug + ) { // Act var parsed = ChannelSlug.Parse(channelUrl); @@ -43,4 +49,4 @@ public void I_cannot_parse_a_channel_slug_from_an_invalid_string(string channelS // Act & assert Assert.Throws(() => ChannelSlug.Parse(channelSlugOrUrl)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/ChannelSpecs.cs b/YoutubeDownloader.Tests/ChannelSpecs.cs index b8fe6b24..3e02c4e0 100644 --- a/YoutubeDownloader.Tests/ChannelSpecs.cs +++ b/YoutubeDownloader.Tests/ChannelSpecs.cs @@ -104,4 +104,4 @@ public async Task I_can_get_videos_uploaded_by_a_channel() videos.Should().HaveCountGreaterOrEqualTo(730); videos.Select(v => v.Author.ChannelId).Should().OnlyContain(i => i == ChannelIds.Normal); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/ClosedCaptionSpecs.cs b/YoutubeDownloader.Tests/ClosedCaptionSpecs.cs index 38419857..5472dfd0 100644 --- a/YoutubeDownloader.Tests/ClosedCaptionSpecs.cs +++ b/YoutubeDownloader.Tests/ClosedCaptionSpecs.cs @@ -17,28 +17,43 @@ public async Task I_can_get_the_list_of_available_closed_caption_tracks_on_a_vid var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync(VideoIds.WithClosedCaptions); + var manifest = await youtube + .Videos + .ClosedCaptions + .GetManifestAsync(VideoIds.WithClosedCaptions); // Assert manifest.Tracks.Should().HaveCountGreaterOrEqualTo(3); - manifest.Tracks.Should().Contain(t => - t.Language.Code == "en" && - t.Language.Name == "English (auto-generated)" && - t.IsAutoGenerated - ); - - manifest.Tracks.Should().Contain(t => - t.Language.Code == "en-US" && - t.Language.Name == "English (United States) - Captions" && - !t.IsAutoGenerated - ); - - manifest.Tracks.Should().Contain(t => - t.Language.Code == "es-419" && - t.Language.Name == "Spanish (Latin America)" && - !t.IsAutoGenerated - ); + manifest + .Tracks + .Should() + .Contain( + t => + t.Language.Code == "en" + && t.Language.Name == "English (auto-generated)" + && t.IsAutoGenerated + ); + + manifest + .Tracks + .Should() + .Contain( + t => + t.Language.Code == "en-US" + && t.Language.Name == "English (United States) - Captions" + && !t.IsAutoGenerated + ); + + manifest + .Tracks + .Should() + .Contain( + t => + t.Language.Code == "es-419" + && t.Language.Name == "Spanish (Latin America)" + && !t.IsAutoGenerated + ); } [Fact] @@ -48,7 +63,10 @@ public async Task I_can_get_a_specific_closed_caption_track_from_a_video() var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync(VideoIds.WithClosedCaptions); + var manifest = await youtube + .Videos + .ClosedCaptions + .GetManifestAsync(VideoIds.WithClosedCaptions); var trackInfo = manifest.GetByLanguage("en-US"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); @@ -64,7 +82,10 @@ public async Task I_can_get_a_specific_closed_caption_track_from_a_video_that_ha var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync(VideoIds.WithBrokenClosedCaptions); + var manifest = await youtube + .Videos + .ClosedCaptions + .GetManifestAsync(VideoIds.WithBrokenClosedCaptions); var trackInfo = manifest.GetByLanguage("en"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); @@ -80,7 +101,10 @@ public async Task I_can_get_an_individual_closed_caption_from_a_video() var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync(VideoIds.WithClosedCaptions); + var manifest = await youtube + .Videos + .ClosedCaptions + .GetManifestAsync(VideoIds.WithClosedCaptions); var trackInfo = manifest.GetByLanguage("en-US"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); @@ -98,7 +122,10 @@ public async Task I_can_get_an_individual_closed_caption_part_from_a_video() var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync(VideoIds.WithClosedCaptions); + var manifest = await youtube + .Videos + .ClosedCaptions + .GetManifestAsync(VideoIds.WithClosedCaptions); var trackInfo = manifest.GetByLanguage("en"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); @@ -119,7 +146,10 @@ public async Task I_can_download_a_specific_closed_caption_track_from_a_video() var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync(VideoIds.WithClosedCaptions); + var manifest = await youtube + .Videos + .ClosedCaptions + .GetManifestAsync(VideoIds.WithClosedCaptions); var trackInfo = manifest.GetByLanguage("en-US"); await youtube.Videos.ClosedCaptions.DownloadAsync(trackInfo, file.Path); @@ -129,4 +159,4 @@ public async Task I_can_download_a_specific_closed_caption_track_from_a_video() fileInfo.Exists.Should().BeTrue(); fileInfo.Length.Should().BeGreaterThan(0); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/PlaylistIdSpecs.cs b/YoutubeDownloader.Tests/PlaylistIdSpecs.cs index 4cc28c30..2e0ad2ba 100644 --- a/YoutubeDownloader.Tests/PlaylistIdSpecs.cs +++ b/YoutubeDownloader.Tests/PlaylistIdSpecs.cs @@ -32,12 +32,30 @@ public void I_can_parse_a_playlist_ID_from_an_ID_string(string playlistId) } [Theory] - [InlineData("youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H", "PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H")] - [InlineData("youtube.com/watch?v=b8m9zhNAgKs&list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr", "PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr")] - [InlineData("youtu.be/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr", "PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr")] - [InlineData("youtube.com/embed/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr", "PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr")] - [InlineData("youtube.com/watch?v=x2ZRoWQ0grU&list=RDEMNJhLy4rECJ_fG8NL-joqsg", "RDEMNJhLy4rECJ_fG8NL-joqsg")] - public void I_can_parse_a_playlist_ID_from_a_URL_string(string playlistUrl, string expectedPlaylistId) + [InlineData( + "youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H", + "PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H" + )] + [InlineData( + "youtube.com/watch?v=b8m9zhNAgKs&list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr", + "PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr" + )] + [InlineData( + "youtu.be/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr", + "PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr" + )] + [InlineData( + "youtube.com/embed/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr", + "PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr" + )] + [InlineData( + "youtube.com/watch?v=x2ZRoWQ0grU&list=RDEMNJhLy4rECJ_fG8NL-joqsg", + "RDEMNJhLy4rECJ_fG8NL-joqsg" + )] + public void I_can_parse_a_playlist_ID_from_a_URL_string( + string playlistUrl, + string expectedPlaylistId + ) { // Act var parsed = PlaylistId.Parse(playlistUrl); @@ -57,4 +75,4 @@ public void I_cannot_parse_a_playlist_ID_from_an_invalid_string(string playlistI // Act & assert Assert.Throws(() => PlaylistId.Parse(playlistIdOrUrl)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/PlaylistSpecs.cs b/YoutubeDownloader.Tests/PlaylistSpecs.cs index 3e87da06..ddf09afe 100644 --- a/YoutubeDownloader.Tests/PlaylistSpecs.cs +++ b/YoutubeDownloader.Tests/PlaylistSpecs.cs @@ -13,8 +13,7 @@ public class PlaylistSpecs { private readonly ITestOutputHelper _testOutput; - public PlaylistSpecs(ITestOutputHelper testOutput) => - _testOutput = testOutput; + public PlaylistSpecs(ITestOutputHelper testOutput) => _testOutput = testOutput; [Fact] public async Task I_can_get_the_metadata_of_a_playlist() @@ -33,7 +32,10 @@ public async Task I_can_get_the_metadata_of_a_playlist() playlist.Author?.ChannelId.Value.Should().Be("UCJ5UyIAa5nEGksjcdp43Ixw"); playlist.Author?.ChannelUrl.Should().NotBeNullOrWhiteSpace(); playlist.Author?.ChannelTitle.Should().Be("Google Analytics"); - playlist.Description.Should().Contain("Digital Analytics Fundamentals course on Analytics Academy"); + playlist + .Description + .Should() + .Contain("Digital Analytics Fundamentals course on Analytics Academy"); playlist.Thumbnails.Should().NotBeEmpty(); } @@ -44,8 +46,8 @@ public async Task I_cannot_get_the_metadata_of_a_private_playlist() var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Playlists.GetAsync(PlaylistIds.Private) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Playlists.GetAsync(PlaylistIds.Private) ); _testOutput.WriteLine(ex.Message); @@ -58,8 +60,8 @@ public async Task I_cannot_get_the_metadata_of_a_non_existing_playlist() var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Playlists.GetAsync(PlaylistIds.NonExisting) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Playlists.GetAsync(PlaylistIds.NonExisting) ); _testOutput.WriteLine(ex.Message); @@ -99,30 +101,35 @@ public async Task I_can_get_videos_included_in_a_playlist() // Assert videos.Should().HaveCountGreaterOrEqualTo(21); - videos.Select(v => v.Id.Value).Should().Contain(new[] - { - "uPZSSdkGQhM", - "fi0w57kr_jY", - "xLJt5A-NeQI", - "EpDA3XaELqs", - "eyltEFyZ678", - "TW3gx4t4944", - "w9H_P2wAwSE", - "OyixJ7A9phg", - "dzwRzUEc_tA", - "vEpq3nYeZBc", - "4gYioQkIqKk", - "xyh8iG5mRIs", - "ORrYEEH_KPc", - "ii0T5JUO2BY", - "hgycbw6Beuc", - "Dz-zgq6OqTI", - "I1b4GT-GuEs", - "dN3gkBBffhs", - "8Kg-8ZjgLAQ", - "E9zfpKsw6f8", - "eBCw9sC5D40" - }); + videos + .Select(v => v.Id.Value) + .Should() + .Contain( + new[] + { + "uPZSSdkGQhM", + "fi0w57kr_jY", + "xLJt5A-NeQI", + "EpDA3XaELqs", + "eyltEFyZ678", + "TW3gx4t4944", + "w9H_P2wAwSE", + "OyixJ7A9phg", + "dzwRzUEc_tA", + "vEpq3nYeZBc", + "4gYioQkIqKk", + "xyh8iG5mRIs", + "ORrYEEH_KPc", + "ii0T5JUO2BY", + "hgycbw6Beuc", + "Dz-zgq6OqTI", + "I1b4GT-GuEs", + "dN3gkBBffhs", + "8Kg-8ZjgLAQ", + "E9zfpKsw6f8", + "eBCw9sC5D40" + } + ); } [Fact] @@ -136,18 +143,23 @@ public async Task I_can_get_videos_included_in_a_large_playlist() // Assert videos.Should().HaveCountGreaterOrEqualTo(1900); - videos.Select(v => v.Id.Value).Should().Contain(new[] - { - "RBumgq5yVrA", - "kN0iD0pI3o0", - "YqB8Dm65X18", - "jlvY1o6XKwA", - "-0kcet4aPpQ", - "RnGJ3KJri1g", - "x-IR7PtA7RA", - "N-8E9mHxDy0", - "5ly88Ju1N6A" - }); + videos + .Select(v => v.Id.Value) + .Should() + .Contain( + new[] + { + "RBumgq5yVrA", + "kN0iD0pI3o0", + "YqB8Dm65X18", + "jlvY1o6XKwA", + "-0kcet4aPpQ", + "RnGJ3KJri1g", + "x-IR7PtA7RA", + "N-8E9mHxDy0", + "5ly88Ju1N6A" + } + ); } [Theory] @@ -182,4 +194,4 @@ public async Task I_can_get_a_subset_of_videos_included_in_a_playlist() // Assert videos.Should().HaveCount(10); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/SearchSpecs.cs b/YoutubeDownloader.Tests/SearchSpecs.cs index cbf58890..61f61361 100644 --- a/YoutubeDownloader.Tests/SearchSpecs.cs +++ b/YoutubeDownloader.Tests/SearchSpecs.cs @@ -58,4 +58,4 @@ public async Task I_can_get_channel_results_from_a_search_query() // Assert channels.Should().NotBeEmpty(); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/StreamSpecs.cs b/YoutubeDownloader.Tests/StreamSpecs.cs index 309ef283..d9052962 100644 --- a/YoutubeDownloader.Tests/StreamSpecs.cs +++ b/YoutubeDownloader.Tests/StreamSpecs.cs @@ -16,8 +16,7 @@ public class StreamSpecs { private readonly ITestOutputHelper _testOutput; - public StreamSpecs(ITestOutputHelper testOutput) => - _testOutput = testOutput; + public StreamSpecs(ITestOutputHelper testOutput) => _testOutput = testOutput; [Fact] public async Task I_can_get_the_list_of_available_streams_on_a_video() @@ -26,7 +25,10 @@ public async Task I_can_get_the_list_of_available_streams_on_a_video() var youtube = new YoutubeClient(); // Act - var manifest = await youtube.Videos.Streams.GetManifestAsync(VideoIds.WithHighQualityStreams); + var manifest = await youtube + .Videos + .Streams + .GetManifestAsync(VideoIds.WithHighQualityStreams); // Assert manifest.Streams.Should().NotBeEmpty(); @@ -34,27 +36,35 @@ public async Task I_can_get_the_list_of_available_streams_on_a_video() manifest.GetAudioStreams().Should().NotBeEmpty(); manifest.GetVideoStreams().Should().NotBeEmpty(); - manifest.GetVideoStreams().Should().Contain(s => - s.VideoQuality.MaxHeight == 2160 && - s.VideoQuality.Framerate == 60 && - s.VideoQuality.IsHighDefinition - ); - - manifest.GetVideoStreams().Should().Contain(s => - s.VideoQuality.MaxHeight == 1080 && - s.VideoQuality.Framerate == 60 && - s.VideoQuality.IsHighDefinition - ); - - manifest.GetVideoStreams().Should().Contain(s => - s.VideoQuality.MaxHeight == 720 && - !s.VideoQuality.IsHighDefinition - ); - - manifest.GetVideoStreams().Should().Contain(s => - s.VideoQuality.MaxHeight == 144 && - !s.VideoQuality.IsHighDefinition - ); + manifest + .GetVideoStreams() + .Should() + .Contain( + s => + s.VideoQuality.MaxHeight == 2160 + && s.VideoQuality.Framerate == 60 + && s.VideoQuality.IsHighDefinition + ); + + manifest + .GetVideoStreams() + .Should() + .Contain( + s => + s.VideoQuality.MaxHeight == 1080 + && s.VideoQuality.Framerate == 60 + && s.VideoQuality.IsHighDefinition + ); + + manifest + .GetVideoStreams() + .Should() + .Contain(s => s.VideoQuality.MaxHeight == 720 && !s.VideoQuality.IsHighDefinition); + + manifest + .GetVideoStreams() + .Should() + .Contain(s => s.VideoQuality.MaxHeight == 144 && !s.VideoQuality.IsHighDefinition); } [Theory] @@ -87,8 +97,8 @@ public async Task I_cannot_get_the_list_of_available_streams_on_a_paid_video() var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.Streams.GetManifestAsync(VideoIds.RequiresPurchase) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Videos.Streams.GetManifestAsync(VideoIds.RequiresPurchase) ); ex.PreviewVideoId.Value.Should().NotBeNullOrWhiteSpace(); @@ -103,8 +113,8 @@ public async Task I_cannot_get_the_list_of_available_streams_on_a_private_video( var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.Streams.GetManifestAsync(VideoIds.Private) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Videos.Streams.GetManifestAsync(VideoIds.Private) ); _testOutput.WriteLine(ex.Message); @@ -117,8 +127,8 @@ public async Task I_cannot_get_the_list_of_available_streams_on_a_non_existing_v var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.Streams.GetManifestAsync(VideoIds.Deleted) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Videos.Streams.GetManifestAsync(VideoIds.Deleted) ); _testOutput.WriteLine(ex.Message); @@ -254,8 +264,9 @@ public async Task I_cannot_get_the_HTTP_live_stream_URL_from_an_unplayable_video var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.Streams.GetHttpLiveStreamUrlAsync(VideoIds.RequiresPurchase) + var ex = await Assert.ThrowsAsync( + async () => + await youtube.Videos.Streams.GetHttpLiveStreamUrlAsync(VideoIds.RequiresPurchase) ); _testOutput.WriteLine(ex.Message); @@ -268,10 +279,10 @@ public async Task I_cannot_get_the_HTTP_live_stream_URL_from_a_non_live_video() var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.Streams.GetHttpLiveStreamUrlAsync(VideoIds.Normal) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Videos.Streams.GetHttpLiveStreamUrlAsync(VideoIds.Normal) ); _testOutput.WriteLine(ex.Message); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/TestData/ChannelHandles.cs b/YoutubeDownloader.Tests/TestData/ChannelHandles.cs index 2cffe997..621f8a66 100644 --- a/YoutubeDownloader.Tests/TestData/ChannelHandles.cs +++ b/YoutubeDownloader.Tests/TestData/ChannelHandles.cs @@ -3,4 +3,4 @@ internal static class ChannelHandles { public const string Normal = "MrBeast"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/TestData/ChannelIds.cs b/YoutubeDownloader.Tests/TestData/ChannelIds.cs index 7ca6b977..0167e420 100644 --- a/YoutubeDownloader.Tests/TestData/ChannelIds.cs +++ b/YoutubeDownloader.Tests/TestData/ChannelIds.cs @@ -4,4 +4,4 @@ internal static class ChannelIds { public const string Normal = "UCX6OQ3DkcsbYNE6H8uQQuVA"; public const string Movies = "UCuVPpxrm2VAgpH3Ktln4HXg"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/TestData/ChannelSlugs.cs b/YoutubeDownloader.Tests/TestData/ChannelSlugs.cs index a046092b..6deb4c0a 100644 --- a/YoutubeDownloader.Tests/TestData/ChannelSlugs.cs +++ b/YoutubeDownloader.Tests/TestData/ChannelSlugs.cs @@ -3,4 +3,4 @@ internal static class ChannelSlugs { public const string Normal = "МеланіяПодоляк"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/TestData/PlaylistIds.cs b/YoutubeDownloader.Tests/TestData/PlaylistIds.cs index 943e03e3..f6ed9f35 100644 --- a/YoutubeDownloader.Tests/TestData/PlaylistIds.cs +++ b/YoutubeDownloader.Tests/TestData/PlaylistIds.cs @@ -12,4 +12,4 @@ internal static class PlaylistIds public const string UserUploads = "UUTMt7iMWa7jy0fNXIktwyLA"; public const string Weird = "PL601B2E69B03FAB9D"; public const string ContainsLongVideos = "PLkk2FsMngwGi9FNkWIoNZlfqglcldj_Zs"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/TestData/UserNames.cs b/YoutubeDownloader.Tests/TestData/UserNames.cs index ed303b07..1ac4a940 100644 --- a/YoutubeDownloader.Tests/TestData/UserNames.cs +++ b/YoutubeDownloader.Tests/TestData/UserNames.cs @@ -3,4 +3,4 @@ internal static class UserNames { public const string Normal = "mrbeast6000"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/TestData/VideoIds.cs b/YoutubeDownloader.Tests/TestData/VideoIds.cs index 85295bbd..bbf30715 100644 --- a/YoutubeDownloader.Tests/TestData/VideoIds.cs +++ b/YoutubeDownloader.Tests/TestData/VideoIds.cs @@ -20,4 +20,4 @@ internal static class VideoIds public const string WithHighDynamicRangeStreams = "vX2vsvdq8nw"; public const string WithClosedCaptions = "YltHGKX80Y8"; public const string WithBrokenClosedCaptions = "1VKIIw05JnE"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/UserNameSpecs.cs b/YoutubeDownloader.Tests/UserNameSpecs.cs index 0397e16c..d90dbe89 100644 --- a/YoutubeDownloader.Tests/UserNameSpecs.cs +++ b/YoutubeDownloader.Tests/UserNameSpecs.cs @@ -45,4 +45,4 @@ public void I_cannot_parse_a_user_name_from_an_invalid_string(string userName) // Act & assert Assert.Throws(() => UserName.Parse(userName)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/Utils/TempFile.cs b/YoutubeDownloader.Tests/Utils/TempFile.cs index 46ba25c3..b8dff358 100644 --- a/YoutubeDownloader.Tests/Utils/TempFile.cs +++ b/YoutubeDownloader.Tests/Utils/TempFile.cs @@ -9,8 +9,7 @@ internal partial class TempFile : IDisposable { public string Path { get; } - public TempFile(string path) => - Path = path; + public TempFile(string path) => Path = path; public void Dispose() { @@ -18,9 +17,7 @@ public void Dispose() { File.Delete(Path); } - catch (FileNotFoundException) - { - } + catch (FileNotFoundException) { } } } @@ -29,17 +26,15 @@ internal partial class TempFile public static TempFile Create() { var dirPath = PathEx.Combine( - PathEx.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Directory.GetCurrentDirectory(), + PathEx.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? Directory.GetCurrentDirectory(), "Temp" ); Directory.CreateDirectory(dirPath); - var filePath = PathEx.Combine( - dirPath, - Guid.NewGuid() + ".tmp" - ); + var filePath = PathEx.Combine(dirPath, Guid.NewGuid() + ".tmp"); return new TempFile(filePath); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/VideoIdSpecs.cs b/YoutubeDownloader.Tests/VideoIdSpecs.cs index a7bae819..3bf067df 100644 --- a/YoutubeDownloader.Tests/VideoIdSpecs.cs +++ b/YoutubeDownloader.Tests/VideoIdSpecs.cs @@ -25,6 +25,7 @@ public void I_can_parse_a_video_ID_from_an_ID_string(string videoId) [InlineData("youtu.be/yIVRs6YSbOM", "yIVRs6YSbOM")] [InlineData("youtube.com/embed/yIVRs6YSbOM", "yIVRs6YSbOM")] [InlineData("youtube.com/shorts/sKL1vjP0tIo", "sKL1vjP0tIo")] + [InlineData("youtube.com/live/jfKfPfyJRdk", "jfKfPfyJRdk")] public void I_can_parse_a_video_ID_from_a_URL_string(string videoUrl, string expectedVideoId) { // Act @@ -41,9 +42,10 @@ public void I_can_parse_a_video_ID_from_a_URL_string(string videoUrl, string exp [InlineData("youtube.com/xxx?v=pI2I2zqzeKg")] [InlineData("youtu.be/watch?v=xxx")] [InlineData("youtube.com/embed/")] + [InlineData("youtube.com/live/")] public void I_cannot_parse_a_video_ID_from_an_invalid_string(string videoId) { // Act & assert Assert.Throws(() => VideoId.Parse(videoId)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/VideoSpecs.cs b/YoutubeDownloader.Tests/VideoSpecs.cs index 6e099f44..a2630c8e 100644 --- a/YoutubeDownloader.Tests/VideoSpecs.cs +++ b/YoutubeDownloader.Tests/VideoSpecs.cs @@ -13,8 +13,7 @@ public class VideoSpecs { private readonly ITestOutputHelper _testOutput; - public VideoSpecs(ITestOutputHelper testOutput) => - _testOutput = testOutput; + public VideoSpecs(ITestOutputHelper testOutput) => _testOutput = testOutput; [Fact] public async Task I_can_get_the_metadata_of_a_video() @@ -36,11 +35,25 @@ public async Task I_can_get_the_metadata_of_a_video() video.Description.Should().Contain("More about PSY@"); video.Duration.Should().BeCloseTo(TimeSpan.FromSeconds(252), TimeSpan.FromSeconds(1)); video.Thumbnails.Should().NotBeEmpty(); - video.Keywords.Should().BeEquivalentTo( - "PSY", "싸이", "강남스타일", "뮤직비디오", - "Music Video", "Gangnam Style", "KOREAN SINGER", "KPOP", "KOERAN WAVE", - "PSY 6甲", "6th Studio Album", "싸이6집", "육갑" - ); + video + .Keywords + .Should() + .BeEquivalentTo( + "PSY", + "싸이", + "강남스타일", + "뮤직비디오", + "Music Video", + "Gangnam Style", + "KOREAN SINGER", + "KPOP", + "KOERAN WAVE", + "PSY 6甲", + "6th Studio Album", + "싸이6집", + "육갑", + "Psy Gangnam Style" + ); video.Engagement.ViewCount.Should().BeGreaterOrEqualTo(4_650_000_000); video.Engagement.LikeCount.Should().BeGreaterOrEqualTo(24_000_000); video.Engagement.DislikeCount.Should().BeGreaterOrEqualTo(0); @@ -54,8 +67,8 @@ public async Task I_cannot_get_the_metadata_of_a_private_video() var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.GetAsync(VideoIds.Private) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Videos.GetAsync(VideoIds.Private) ); _testOutput.WriteLine(ex.Message); @@ -68,8 +81,8 @@ public async Task I_cannot_get_the_metadata_of_a_non_existing_video() var youtube = new YoutubeClient(); // Act & assert - var ex = await Assert.ThrowsAsync(async () => - await youtube.Videos.GetAsync(VideoIds.Deleted) + var ex = await Assert.ThrowsAsync( + async () => await youtube.Videos.GetAsync(VideoIds.Deleted) ); _testOutput.WriteLine(ex.Message); @@ -117,4 +130,4 @@ public async Task I_can_get_the_highest_resolution_thumbnail_from_a_video() // Assert thumbnail.Url.Should().NotBeNullOrWhiteSpace(); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader.Tests/YoutubeDownloader.Tests.csproj b/YoutubeDownloader.Tests/YoutubeDownloader.Tests.csproj index 3bc84f63..f9af5323 100644 --- a/YoutubeDownloader.Tests/YoutubeDownloader.Tests.csproj +++ b/YoutubeDownloader.Tests/YoutubeDownloader.Tests.csproj @@ -11,12 +11,13 @@ - - - - - - + + + + + + + diff --git a/YoutubeDownloader.sln b/YoutubeDownloader.sln index 28175f6b..0af6ff1c 100644 --- a/YoutubeDownloader.sln +++ b/YoutubeDownloader.sln @@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution GitVersion.yml = GitVersion.yml LICENSE = LICENSE Readme.md = Readme.md + .gitignore = .gitignore EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YoutubeDownloader", "YoutubeDownloader\YoutubeDownloader.csproj", "{A7C8A144-D9EC-4DDA-8CE3-D48BA3AD88CD}" diff --git a/YoutubeDownloader/Bridge/ChannelPage.cs b/YoutubeDownloader/Bridge/ChannelPage.cs index cf89acb5..c46ea4f2 100644 --- a/YoutubeDownloader/Bridge/ChannelPage.cs +++ b/YoutubeDownloader/Bridge/ChannelPage.cs @@ -1,5 +1,6 @@ using System; using AngleSharp.Html.Dom; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -9,27 +10,20 @@ internal partial class ChannelPage { private readonly IHtmlDocument _content; - public string? Url => Memo.Cache(this, () => - _content - .QuerySelector("meta[property=\"og:url\"]")? - .GetAttribute("content") - ); + [Lazy] + public string? Url => + _content.QuerySelector("meta[property=\"og:url\"]")?.GetAttribute("content"); - public string? Id => Memo.Cache(this, () => - Url?.SubstringAfter("channel/", StringComparison.OrdinalIgnoreCase) - ); + [Lazy] + public string? Id => Url?.SubstringAfter("channel/", StringComparison.OrdinalIgnoreCase); - public string? Title => Memo.Cache(this, () => - _content - .QuerySelector("meta[property=\"og:title\"]")? - .GetAttribute("content") - ); + [Lazy] + public string? Title => + _content.QuerySelector("meta[property=\"og:title\"]")?.GetAttribute("content"); - public string? LogoUrl => Memo.Cache(this, () => - _content - .QuerySelector("meta[property=\"og:image\"]")? - .GetAttribute("content") - ); + [Lazy] + public string? LogoUrl => + _content.QuerySelector("meta[property=\"og:image\"]")?.GetAttribute("content"); public ChannelPage(IHtmlDocument content) => _content = content; } @@ -45,4 +39,4 @@ internal partial class ChannelPage return new ChannelPage(content); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/Cipher/CipherManifest.cs b/YoutubeDownloader/Bridge/Cipher/CipherManifest.cs index 359ff007..93ead367 100644 --- a/YoutubeDownloader/Bridge/Cipher/CipherManifest.cs +++ b/YoutubeDownloader/Bridge/Cipher/CipherManifest.cs @@ -17,4 +17,4 @@ public CipherManifest(string signatureTimestamp, IReadOnlyList public string Decipher(string input) => Operations.Aggregate(input, (acc, op) => op.Decipher(acc)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/Cipher/ICipherOperation.cs b/YoutubeDownloader/Bridge/Cipher/ICipherOperation.cs index 0dfe7de7..9449faf3 100644 --- a/YoutubeDownloader/Bridge/Cipher/ICipherOperation.cs +++ b/YoutubeDownloader/Bridge/Cipher/ICipherOperation.cs @@ -3,4 +3,4 @@ namespace YoutubeExplode.Bridge.Cipher; internal interface ICipherOperation { string Decipher(string input); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/Cipher/ReverseCipherOperation.cs b/YoutubeDownloader/Bridge/Cipher/ReverseCipherOperation.cs index 7dd8cc66..18c2bbf3 100644 --- a/YoutubeDownloader/Bridge/Cipher/ReverseCipherOperation.cs +++ b/YoutubeDownloader/Bridge/Cipher/ReverseCipherOperation.cs @@ -9,4 +9,4 @@ internal class ReverseCipherOperation : ICipherOperation [ExcludeFromCodeCoverage] public override string ToString() => "Reverse"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/Cipher/SpliceCipherOperation.cs b/YoutubeDownloader/Bridge/Cipher/SpliceCipherOperation.cs index 82e432b4..8bf829cb 100644 --- a/YoutubeDownloader/Bridge/Cipher/SpliceCipherOperation.cs +++ b/YoutubeDownloader/Bridge/Cipher/SpliceCipherOperation.cs @@ -12,4 +12,4 @@ internal class SpliceCipherOperation : ICipherOperation [ExcludeFromCodeCoverage] public override string ToString() => $"Splice ({_index})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/Cipher/SwapCipherOperation.cs b/YoutubeDownloader/Bridge/Cipher/SwapCipherOperation.cs index 5b69bb14..63dcfba7 100644 --- a/YoutubeDownloader/Bridge/Cipher/SwapCipherOperation.cs +++ b/YoutubeDownloader/Bridge/Cipher/SwapCipherOperation.cs @@ -13,4 +13,4 @@ internal class SwapCipherOperation : ICipherOperation [ExcludeFromCodeCoverage] public override string ToString() => $"Swap ({_index})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/ClosedCaptionTrackResponse.cs b/YoutubeDownloader/Bridge/ClosedCaptionTrackResponse.cs index cb50da78..9b6cd1d7 100644 --- a/YoutubeDownloader/Bridge/ClosedCaptionTrackResponse.cs +++ b/YoutubeDownloader/Bridge/ClosedCaptionTrackResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -11,12 +12,9 @@ internal partial class ClosedCaptionTrackResponse { private readonly XElement _content; - public IReadOnlyList Captions => Memo.Cache(this, () => - _content - .Descendants("p") - .Select(x => new CaptionData(x)) - .ToArray() - ); + [Lazy] + public IReadOnlyList Captions => + _content.Descendants("p").Select(x => new CaptionData(x)).ToArray(); public ClosedCaptionTrackResponse(XElement content) => _content = content; } @@ -27,24 +25,20 @@ public class CaptionData { private readonly XElement _content; - public string? Text => Memo.Cache(this, () => - (string?)_content - ); + [Lazy] + public string? Text => (string?)_content; - public TimeSpan? Offset => Memo.Cache(this, () => - ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds) - ); + [Lazy] + public TimeSpan? Offset => + ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds); - public TimeSpan? Duration => Memo.Cache(this, () => - ((double?)_content.Attribute("d"))?.Pipe(TimeSpan.FromMilliseconds) - ); + [Lazy] + public TimeSpan? Duration => + ((double?)_content.Attribute("d"))?.Pipe(TimeSpan.FromMilliseconds); - public IReadOnlyList Parts => Memo.Cache(this, () => - _content - .Elements("s") - .Select(x => new PartData(x)) - .ToArray() - ); + [Lazy] + public IReadOnlyList Parts => + _content.Elements("s").Select(x => new PartData(x)).ToArray(); public CaptionData(XElement content) => _content = content; } @@ -56,15 +50,14 @@ public class PartData { private readonly XElement _content; - public string? Text => Memo.Cache(this, () => - (string?)_content - ); + [Lazy] + public string? Text => (string?)_content; - public TimeSpan? Offset => Memo.Cache(this, () => - ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds) ?? - ((double?)_content.Attribute("ac"))?.Pipe(TimeSpan.FromMilliseconds) ?? - TimeSpan.Zero - ); + [Lazy] + public TimeSpan? Offset => + ((double?)_content.Attribute("t"))?.Pipe(TimeSpan.FromMilliseconds) + ?? ((double?)_content.Attribute("ac"))?.Pipe(TimeSpan.FromMilliseconds) + ?? TimeSpan.Zero; public PartData(XElement content) => _content = content; } @@ -73,4 +66,4 @@ public class PartData internal partial class ClosedCaptionTrackResponse { public static ClosedCaptionTrackResponse Parse(string raw) => new(Xml.Parse(raw)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/DashManifest.cs b/YoutubeDownloader/Bridge/DashManifest.cs index 0f85c401..28be83ad 100644 --- a/YoutubeDownloader/Bridge/DashManifest.cs +++ b/YoutubeDownloader/Bridge/DashManifest.cs @@ -3,6 +3,7 @@ using System.Net; using System.Text.RegularExpressions; using System.Xml.Linq; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -12,30 +13,27 @@ internal partial class DashManifest { private readonly XElement _content; - public IReadOnlyList Streams => Memo.Cache(this, () => + [Lazy] + public IReadOnlyList Streams => _content .Descendants("Representation") // Skip non-media representations (like "rawcc") // https://github.com/Tyrrrz/YoutubeExplode/issues/546 - .Where(x => x - .Attribute("id")? - .Value - .All(char.IsDigit) == true - ) + .Where(x => x.Attribute("id")?.Value.All(char.IsDigit) == true) // Skip segmented streams // https://github.com/Tyrrrz/YoutubeExplode/issues/159 - .Where(x => x - .Descendants("Initialization") - .FirstOrDefault()? - .Attribute("sourceURL")? - .Value - .Contains("sq/") != true + .Where( + x => + x.Descendants("Initialization") + .FirstOrDefault() + ?.Attribute("sourceURL") + ?.Value + .Contains("sq/") != true ) // Skip streams without codecs .Where(x => !string.IsNullOrWhiteSpace(x.Attribute("codecs")?.Value)) .Select(x => new StreamData(x)) - .ToArray() - ); + .ToArray(); public DashManifest(XElement content) => _content = content; } @@ -46,13 +44,11 @@ public class StreamData : IStreamData { private readonly XElement _content; - public int? Itag => Memo.Cache(this, () => - (int?)_content.Attribute("id") - ); + [Lazy] + public int? Itag => (int?)_content.Attribute("id"); - public string? Url => Memo.Cache(this, () => - (string?)_content.Element("BaseURL") - ); + [Lazy] + public string? Url => (string?)_content.Element("BaseURL"); // DASH streams don't have signatures public string? Signature => null; @@ -60,54 +56,40 @@ public class StreamData : IStreamData // DASH streams don't have signatures public string? SignatureParameter => null; - public long? ContentLength => Memo.Cache(this, () => - (long?)_content.Attribute("contentLength") ?? - - Url? - .Pipe(s => Regex.Match(s, @"[/\?]clen[/=](\d+)").Groups[1].Value) - .NullIfWhiteSpace()? - .ParseLongOrNull() - ); - - public long? Bitrate => Memo.Cache(this, () => - (long?)_content.Attribute("bandwidth") - ); - - public string? Container => Memo.Cache(this, () => - Url? - .Pipe(s => Regex.Match(s, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value) - .Pipe(WebUtility.UrlDecode) - ); - - private bool IsAudioOnly => Memo.Cache(this, () => - _content.Element("AudioChannelConfiguration") is not null - ); - - public string? AudioCodec => Memo.Cache(this, () => - IsAudioOnly - ? (string?)_content.Attribute("codecs") - : null - ); - - public string? VideoCodec => Memo.Cache(this, () => - IsAudioOnly - ? null - : (string?)_content.Attribute("codecs") - ); + [Lazy] + public long? ContentLength => + (long?)_content.Attribute("contentLength") + ?? Url?.Pipe(s => Regex.Match(s, @"[/\?]clen[/=](\d+)").Groups[1].Value) + .NullIfWhiteSpace() + ?.ParseLongOrNull(); + + [Lazy] + public long? Bitrate => (long?)_content.Attribute("bandwidth"); + + [Lazy] + public string? Container => + Url?.Pipe(s => Regex.Match(s, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value) + .Pipe(WebUtility.UrlDecode); + + [Lazy] + private bool IsAudioOnly => _content.Element("AudioChannelConfiguration") is not null; + + [Lazy] + public string? AudioCodec => IsAudioOnly ? (string?)_content.Attribute("codecs") : null; + + [Lazy] + public string? VideoCodec => IsAudioOnly ? null : (string?)_content.Attribute("codecs"); public string? VideoQualityLabel => null; - public int? VideoWidth => Memo.Cache(this, () => - (int?)_content.Attribute("width") - ); + [Lazy] + public int? VideoWidth => (int?)_content.Attribute("width"); - public int? VideoHeight => Memo.Cache(this, () => - (int?)_content.Attribute("height") - ); + [Lazy] + public int? VideoHeight => (int?)_content.Attribute("height"); - public int? VideoFramerate => Memo.Cache(this, () => - (int?)_content.Attribute("frameRate") - ); + [Lazy] + public int? VideoFramerate => (int?)_content.Attribute("frameRate"); public StreamData(XElement content) => _content = content; } @@ -116,4 +98,4 @@ public class StreamData : IStreamData internal partial class DashManifest { public static DashManifest Parse(string raw) => new(Xml.Parse(raw)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/IPlaylistData.cs b/YoutubeDownloader/Bridge/IPlaylistData.cs index 4a30cd98..e5d370ce 100644 --- a/YoutubeDownloader/Bridge/IPlaylistData.cs +++ b/YoutubeDownloader/Bridge/IPlaylistData.cs @@ -13,4 +13,4 @@ internal interface IPlaylistData string? Description { get; } IReadOnlyList Thumbnails { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/IStreamData.cs b/YoutubeDownloader/Bridge/IStreamData.cs index daf86a6c..f2385bab 100644 --- a/YoutubeDownloader/Bridge/IStreamData.cs +++ b/YoutubeDownloader/Bridge/IStreamData.cs @@ -27,4 +27,4 @@ internal interface IStreamData int? VideoHeight { get; } int? VideoFramerate { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/PlayerResponse.cs b/YoutubeDownloader/Bridge/PlayerResponse.cs index dd010327..02ff3aad 100644 --- a/YoutubeDownloader/Bridge/PlayerResponse.cs +++ b/YoutubeDownloader/Bridge/PlayerResponse.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -13,124 +14,97 @@ internal partial class PlayerResponse { private readonly JsonElement _content; - private JsonElement? Playability => Memo.Cache(this, () => - _content.GetPropertyOrNull("playabilityStatus") - ); - - private string? PlayabilityStatus => Memo.Cache(this, () => - Playability? - .GetPropertyOrNull("status")? - .GetStringOrNull() - ); - - public string? PlayabilityError => Memo.Cache(this, () => - Playability? - .GetPropertyOrNull("reason")? - .GetStringOrNull() - ); - - public bool IsAvailable => Memo.Cache(this, () => - !string.Equals(PlayabilityStatus, "error", StringComparison.OrdinalIgnoreCase) && - Details is not null - ); - - public bool IsPlayable => Memo.Cache(this, () => - string.Equals(PlayabilityStatus, "ok", StringComparison.OrdinalIgnoreCase) - ); - - private JsonElement? Details => Memo.Cache(this, () => - _content.GetPropertyOrNull("videoDetails") - ); - - public string? Title => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("title")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("channelId")? - .GetStringOrNull() - ); - - public string? Author => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("author")? - .GetStringOrNull() - ); - - public DateTimeOffset? UploadDate => Memo.Cache(this, () => + [Lazy] + private JsonElement? Playability => _content.GetPropertyOrNull("playabilityStatus"); + + [Lazy] + private string? PlayabilityStatus => + Playability?.GetPropertyOrNull("status")?.GetStringOrNull(); + + [Lazy] + public string? PlayabilityError => Playability?.GetPropertyOrNull("reason")?.GetStringOrNull(); + + [Lazy] + public bool IsAvailable => + !string.Equals(PlayabilityStatus, "error", StringComparison.OrdinalIgnoreCase) + && Details is not null; + + [Lazy] + public bool IsPlayable => + string.Equals(PlayabilityStatus, "ok", StringComparison.OrdinalIgnoreCase); + + [Lazy] + private JsonElement? Details => _content.GetPropertyOrNull("videoDetails"); + + [Lazy] + public string? Title => Details?.GetPropertyOrNull("title")?.GetStringOrNull(); + + [Lazy] + public string? ChannelId => Details?.GetPropertyOrNull("channelId")?.GetStringOrNull(); + + [Lazy] + public string? Author => Details?.GetPropertyOrNull("author")?.GetStringOrNull(); + + [Lazy] + public DateTimeOffset? UploadDate => _content - .GetPropertyOrNull("microformat")? - .GetPropertyOrNull("playerMicroformatRenderer")? - .GetPropertyOrNull("uploadDate")? - .GetDateTimeOffset() - ); - - public TimeSpan? Duration => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("lengthSeconds")? - .GetStringOrNull()? - .ParseDoubleOrNull()? - .Pipe(TimeSpan.FromSeconds) - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); - - public IReadOnlyList Keywords => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("keywords")? - .EnumerateArrayOrNull()? - .Select(j => j.GetStringOrNull()) + .GetPropertyOrNull("microformat") + ?.GetPropertyOrNull("playerMicroformatRenderer") + ?.GetPropertyOrNull("uploadDate") + ?.GetDateTimeOffset(); + + [Lazy] + public TimeSpan? Duration => + Details + ?.GetPropertyOrNull("lengthSeconds") + ?.GetStringOrNull() + ?.ParseDoubleOrNull() + ?.Pipe(TimeSpan.FromSeconds); + + [Lazy] + public IReadOnlyList Thumbnails => + Details + ?.GetPropertyOrNull("thumbnail") + ?.GetPropertyOrNull("thumbnails") + ?.EnumerateArrayOrNull() + ?.Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); + + public IReadOnlyList Keywords => + Details + ?.GetPropertyOrNull("keywords") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetStringOrNull()) .WhereNotNull() - .ToArray() ?? - - Array.Empty() - ); - - public string? Description => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("shortDescription")? - .GetStringOrNull() - ); - - public long? ViewCount => Memo.Cache(this, () => - Details? - .GetPropertyOrNull("viewCount")? - .GetStringOrNull()? - .ParseLongOrNull() - ); - - public string? PreviewVideoId => Memo.Cache(this, () => - Playability? - .GetPropertyOrNull("errorScreen")? - .GetPropertyOrNull("playerLegacyDesktopYpcTrailerRenderer")? - .GetPropertyOrNull("trailerVideoId")? - .GetStringOrNull() ?? - - Playability? - .GetPropertyOrNull("errorScreen")? - .GetPropertyOrNull("ypcTrailerRenderer")? - .GetPropertyOrNull("playerVars")? - .GetStringOrNull()? - .Pipe(UrlEx.GetQueryParameters) - .GetValueOrDefault("video_id") ?? - - Playability? - .GetPropertyOrNull("errorScreen")? - .GetPropertyOrNull("ypcTrailerRenderer")? - .GetPropertyOrNull("playerResponse")? - .GetStringOrNull()? + .ToArray() ?? Array.Empty(); + + [Lazy] + public string? Description => Details?.GetPropertyOrNull("shortDescription")?.GetStringOrNull(); + + [Lazy] + public long? ViewCount => + Details?.GetPropertyOrNull("viewCount")?.GetStringOrNull()?.ParseLongOrNull(); + + [Lazy] + public string? PreviewVideoId => + Playability + ?.GetPropertyOrNull("errorScreen") + ?.GetPropertyOrNull("playerLegacyDesktopYpcTrailerRenderer") + ?.GetPropertyOrNull("trailerVideoId") + ?.GetStringOrNull() + ?? Playability + ?.GetPropertyOrNull("errorScreen") + ?.GetPropertyOrNull("ypcTrailerRenderer") + ?.GetPropertyOrNull("playerVars") + ?.GetStringOrNull() + ?.Pipe(UrlEx.GetQueryParameters) + .GetValueOrDefault("video_id") + ?? Playability + ?.GetPropertyOrNull("errorScreen") + ?.GetPropertyOrNull("ypcTrailerRenderer") + ?.GetPropertyOrNull("playerResponse") + ?.GetStringOrNull() + ? // YouTube uses weird base64-like encoding here that I don't know how to deal with. // It's supposed to have JSON inside, but if extracted as is, it contains garbage. // Luckily, some of the text gets decoded correctly, which is enough for us to @@ -140,59 +114,55 @@ Details is not null .Pipe(Convert.FromBase64String) .Pipe(Encoding.UTF8.GetString) .Pipe(s => Regex.Match(s, @"video_id=(.{11})").Groups[1].Value) - .NullIfWhiteSpace() - ); - - private JsonElement? StreamingData => Memo.Cache(this, () => - _content.GetPropertyOrNull("streamingData") - ); - - public string? DashManifestUrl => Memo.Cache(this, () => - StreamingData? - .GetPropertyOrNull("dashManifestUrl")? - .GetStringOrNull() - ); - - public string? HlsManifestUrl => Memo.Cache(this, () => - StreamingData? - .GetPropertyOrNull("hlsManifestUrl")? - .GetStringOrNull() - ); - - public IReadOnlyList Streams => Memo.Cache(this, () => + .NullIfWhiteSpace(); + + [Lazy] + private JsonElement? StreamingData => _content.GetPropertyOrNull("streamingData"); + + [Lazy] + public string? DashManifestUrl => + StreamingData?.GetPropertyOrNull("dashManifestUrl")?.GetStringOrNull(); + + [Lazy] + public string? HlsManifestUrl => + StreamingData?.GetPropertyOrNull("hlsManifestUrl")?.GetStringOrNull(); + + [Lazy] + public IReadOnlyList Streams { - var result = new List(); + get + { + var result = new List(); - var muxedStreams = StreamingData? - .GetPropertyOrNull("formats")? - .EnumerateArrayOrNull()? - .Select(j => new StreamData(j)); + var muxedStreams = StreamingData + ?.GetPropertyOrNull("formats") + ?.EnumerateArrayOrNull() + ?.Select(j => new StreamData(j)); - if (muxedStreams is not null) - result.AddRange(muxedStreams); + if (muxedStreams is not null) + result.AddRange(muxedStreams); - var adaptiveStreams = StreamingData? - .GetPropertyOrNull("adaptiveFormats")? - .EnumerateArrayOrNull()? - .Select(j => new StreamData(j)); + var adaptiveStreams = StreamingData + ?.GetPropertyOrNull("adaptiveFormats") + ?.EnumerateArrayOrNull() + ?.Select(j => new StreamData(j)); - if (adaptiveStreams is not null) - result.AddRange(adaptiveStreams); + if (adaptiveStreams is not null) + result.AddRange(adaptiveStreams); - return result; - }); + return result; + } + } - public IReadOnlyList ClosedCaptionTracks => Memo.Cache(this, () => + [Lazy] + public IReadOnlyList ClosedCaptionTracks => _content - .GetPropertyOrNull("captions")? - .GetPropertyOrNull("playerCaptionsTracklistRenderer")? - .GetPropertyOrNull("captionTracks")? - .EnumerateArrayOrNull()? - .Select(j => new ClosedCaptionTrackData(j)) - .ToArray() ?? - - Array.Empty() - ); + .GetPropertyOrNull("captions") + ?.GetPropertyOrNull("playerCaptionsTracklistRenderer") + ?.GetPropertyOrNull("captionTracks") + ?.EnumerateArrayOrNull() + ?.Select(j => new ClosedCaptionTrackData(j)) + .ToArray() ?? Array.Empty(); public PlayerResponse(JsonElement content) => _content = content; } @@ -203,39 +173,30 @@ public class ClosedCaptionTrackData { private readonly JsonElement _content; - public string? Url => Memo.Cache(this, () => - _content - .GetPropertyOrNull("baseUrl")? - .GetStringOrNull() - ); - - public string? LanguageCode => Memo.Cache(this, () => - _content - .GetPropertyOrNull("languageCode")? - .GetStringOrNull() - ); - - public string? LanguageName => Memo.Cache(this, () => - _content - .GetPropertyOrNull("name")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - _content - .GetPropertyOrNull("name")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + [Lazy] + public string? Url => _content.GetPropertyOrNull("baseUrl")?.GetStringOrNull(); + + [Lazy] + public string? LanguageCode => + _content.GetPropertyOrNull("languageCode")?.GetStringOrNull(); + + [Lazy] + public string? LanguageName => + _content.GetPropertyOrNull("name")?.GetPropertyOrNull("simpleText")?.GetStringOrNull() + ?? _content + .GetPropertyOrNull("name") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - public bool IsAutoGenerated => Memo.Cache(this, () => + [Lazy] + public bool IsAutoGenerated => _content - .GetPropertyOrNull("vssId")? - .GetStringOrNull()? - .StartsWith("a.", StringComparison.OrdinalIgnoreCase) ?? false - ); + .GetPropertyOrNull("vssId") + ?.GetStringOrNull() + ?.StartsWith("a.", StringComparison.OrdinalIgnoreCase) ?? false; public ClosedCaptionTrackData(JsonElement content) => _content = content; } @@ -247,122 +208,82 @@ public class StreamData : IStreamData { private readonly JsonElement _content; - public int? Itag => Memo.Cache(this, () => - _content - .GetPropertyOrNull("itag")? - .GetInt32OrNull() - ); + [Lazy] + public int? Itag => _content.GetPropertyOrNull("itag")?.GetInt32OrNull(); - private IReadOnlyDictionary? CipherData => Memo.Cache(this, () => - _content - .GetPropertyOrNull("cipher")? - .GetStringOrNull()? - .Pipe(UrlEx.GetQueryParameters) ?? + [Lazy] + private IReadOnlyDictionary? CipherData => + _content.GetPropertyOrNull("cipher")?.GetStringOrNull()?.Pipe(UrlEx.GetQueryParameters) + ?? _content + .GetPropertyOrNull("signatureCipher") + ?.GetStringOrNull() + ?.Pipe(UrlEx.GetQueryParameters); - _content - .GetPropertyOrNull("signatureCipher")? - .GetStringOrNull()? - .Pipe(UrlEx.GetQueryParameters) - ); + [Lazy] + public string? Url => + _content.GetPropertyOrNull("url")?.GetStringOrNull() + ?? CipherData?.GetValueOrDefault("url"); - public string? Url => Memo.Cache(this, () => - _content - .GetPropertyOrNull("url")? - .GetStringOrNull() ?? + [Lazy] + public string? Signature => CipherData?.GetValueOrDefault("s"); - CipherData?.GetValueOrDefault("url") - ); + [Lazy] + public string? SignatureParameter => CipherData?.GetValueOrDefault("sp"); - public string? Signature => Memo.Cache(this, () => - CipherData?.GetValueOrDefault("s") - ); + [Lazy] + public long? ContentLength => + _content.GetPropertyOrNull("contentLength")?.GetStringOrNull()?.ParseLongOrNull() + ?? Url?.Pipe(s => UrlEx.TryGetQueryParameterValue(s, "clen")) + ?.NullIfWhiteSpace() + ?.ParseLongOrNull(); - public string? SignatureParameter => Memo.Cache(this, () => - CipherData?.GetValueOrDefault("sp") - ); + [Lazy] + public long? Bitrate => _content.GetPropertyOrNull("bitrate")?.GetInt64OrNull(); - public long? ContentLength => Memo.Cache(this, () => - _content - .GetPropertyOrNull("contentLength")? - .GetStringOrNull()? - .ParseLongOrNull() ?? + [Lazy] + private string? MimeType => _content.GetPropertyOrNull("mimeType")?.GetStringOrNull(); - Url? - .Pipe(s => UrlEx.TryGetQueryParameterValue(s, "clen"))? - .NullIfWhiteSpace()? - .ParseLongOrNull() - ); + [Lazy] + public string? Container => MimeType?.SubstringUntil(";").SubstringAfter("/"); - public long? Bitrate => Memo.Cache(this, () => - _content - .GetPropertyOrNull("bitrate")? - .GetInt64OrNull() - ); + [Lazy] + private bool IsAudioOnly => + MimeType?.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ?? false; - private string? MimeType => Memo.Cache(this, () => - _content - .GetPropertyOrNull("mimeType")? - .GetStringOrNull() - ); - - public string? Container => Memo.Cache(this, () => - MimeType? - .SubstringUntil(";") - .SubstringAfter("/") - ); - - private bool IsAudioOnly => Memo.Cache(this, () => - MimeType?.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ?? false - ); - - public string? Codecs => Memo.Cache(this, () => - MimeType? - .SubstringAfter("codecs=\"") - .SubstringUntil("\"") - ); - - public string? AudioCodec => Memo.Cache(this, () => - IsAudioOnly - ? Codecs - : Codecs?.SubstringAfter(", ").NullIfWhiteSpace() - ); - - public string? VideoCodec => Memo.Cache(this, () => + [Lazy] + public string? Codecs => MimeType?.SubstringAfter("codecs=\"").SubstringUntil("\""); + + [Lazy] + public string? AudioCodec => + IsAudioOnly ? Codecs : Codecs?.SubstringAfter(", ").NullIfWhiteSpace(); + + [Lazy] + public string? VideoCodec { - var codec = IsAudioOnly - ? null - : Codecs?.SubstringUntil(", ").NullIfWhiteSpace(); + get + { + var codec = IsAudioOnly ? null : Codecs?.SubstringUntil(", ").NullIfWhiteSpace(); - // "unknown" value indicates av01 codec - if (string.Equals(codec, "unknown", StringComparison.OrdinalIgnoreCase)) - return "av01.0.05M.08"; + // "unknown" value indicates av01 codec + if (string.Equals(codec, "unknown", StringComparison.OrdinalIgnoreCase)) + return "av01.0.05M.08"; - return codec; - }); + return codec; + } + } - public string? VideoQualityLabel => Memo.Cache(this, () => - _content - .GetPropertyOrNull("qualityLabel")? - .GetStringOrNull() - ); + [Lazy] + public string? VideoQualityLabel => + _content.GetPropertyOrNull("qualityLabel")?.GetStringOrNull(); - public int? VideoWidth => Memo.Cache(this, () => - _content - .GetPropertyOrNull("width")? - .GetInt32OrNull() - ); + [Lazy] + public int? VideoWidth => _content.GetPropertyOrNull("width")?.GetInt32OrNull(); - public int? VideoHeight => Memo.Cache(this, () => - _content - .GetPropertyOrNull("height")? - .GetInt32OrNull() - ); + [Lazy] + public int? VideoHeight => _content.GetPropertyOrNull("height")?.GetInt32OrNull(); - public int? VideoFramerate => Memo.Cache(this, () => - _content - .GetPropertyOrNull("fps")? - .GetInt32OrNull() - ); + [Lazy] + public int? VideoFramerate => _content.GetPropertyOrNull("fps")?.GetInt32OrNull(); public StreamData(JsonElement content) => _content = content; } @@ -371,4 +292,4 @@ public class StreamData : IStreamData internal partial class PlayerResponse { public static PlayerResponse Parse(string raw) => new(Json.Parse(raw)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/PlayerSource.cs b/YoutubeDownloader/Bridge/PlayerSource.cs index 6f905f41..b188a143 100644 --- a/YoutubeDownloader/Bridge/PlayerSource.cs +++ b/YoutubeDownloader/Bridge/PlayerSource.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using Lazy; using YoutubeExplode.Bridge.Cipher; -using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Bridge; @@ -11,96 +11,134 @@ internal partial class PlayerSource { private readonly string _content; - public CipherManifest? CipherManifest => Memo.Cache(this, () => + [Lazy] + public CipherManifest? CipherManifest { - // Extract the signature timestamp - var signatureTimestamp = Regex.Match(_content, @"(?:signatureTimestamp|sts):(\d{5})") - .Groups[1] - .Value - .NullIfWhiteSpace(); - - if (string.IsNullOrWhiteSpace(signatureTimestamp)) - return null; - - // Find where the player calls the cipher functions - var cipherCallsite = Regex.Match( - _content, - """ - [$_\w]+=function\([$_\w]+\){([$_\w]+)=\1\.split\(['"]{2}\);.*?return \1\.join\(['"]{2}\)} - """, - RegexOptions.Singleline - ).Groups[0].Value.NullIfWhiteSpace(); - - if (string.IsNullOrWhiteSpace(cipherCallsite)) - return null; - - // Find the object that defines the cipher functions - var cipherContainerName = Regex.Match(cipherCallsite, @"([$_\w]+)\.[$_\w]+\([$_\w]+,\d+\);") - .Groups[1] - .Value; - - if (string.IsNullOrWhiteSpace(cipherContainerName)) - return null; - - // Find the definition of the cipher functions - var cipherDefinition = Regex.Match( - _content, - $$""" - var {{Regex.Escape(cipherContainerName)}}={.*?}; - """, - RegexOptions.Singleline - ).Groups[0].Value.NullIfWhiteSpace(); - - if (string.IsNullOrWhiteSpace(cipherDefinition)) - return null; - - // Identify the swap cipher function - var swapFuncName = Regex.Match( - cipherDefinition, - @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?%[^}]*?}", - RegexOptions.Singleline - ).Groups[1].Value.NullIfWhiteSpace(); - - // Identify the splice cipher function - var spliceFuncName = Regex.Match( - cipherDefinition, - @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?splice[^}]*?}", - RegexOptions.Singleline - ).Groups[1].Value.NullIfWhiteSpace(); - - // Identify the reverse cipher function - var reverseFuncName = Regex.Match( - cipherDefinition, - @"([$_\w]+):function\([$_\w]+\){+[^}]*?reverse[^}]*?}", - RegexOptions.Singleline - ).Groups[1].Value.NullIfWhiteSpace(); - - var operations = new List(); - - foreach (var statement in cipherCallsite.Split(';')) + get { - var calledFuncName = Regex.Match(statement, @"[$_\w]+\.([$_\w]+)\([$_\w]+,\d+\)").Groups[1].Value; - if (string.IsNullOrWhiteSpace(calledFuncName)) - continue; - - if (string.Equals(calledFuncName, swapFuncName, StringComparison.Ordinal)) - { - var index = Regex.Match(statement, @"\([$_\w]+,(\d+)\)").Groups[1].Value.ParseInt(); - operations.Add(new SwapCipherOperation(index)); - } - else if (string.Equals(calledFuncName, spliceFuncName, StringComparison.Ordinal)) - { - var index = Regex.Match(statement, @"\([$_\w]+,(\d+)\)").Groups[1].Value.ParseInt(); - operations.Add(new SpliceCipherOperation(index)); - } - else if (string.Equals(calledFuncName, reverseFuncName, StringComparison.Ordinal)) + // Extract the signature timestamp + var signatureTimestamp = Regex + .Match(_content, @"(?:signatureTimestamp|sts):(\d{5})") + .Groups[1] + .Value + .NullIfWhiteSpace(); + + if (string.IsNullOrWhiteSpace(signatureTimestamp)) + return null; + + // Find where the player calls the cipher functions + var cipherCallsite = Regex + .Match( + _content, + """ + [$_\w]+=function\([$_\w]+\){([$_\w]+)=\1\.split\(['"]{2}\);.*?return \1\.join\(['"]{2}\)} + """, + RegexOptions.Singleline + ) + .Groups[0] + .Value + .NullIfWhiteSpace(); + + if (string.IsNullOrWhiteSpace(cipherCallsite)) + return null; + + // Find the object that defines the cipher functions + var cipherContainerName = Regex + .Match(cipherCallsite, @"([$_\w]+)\.[$_\w]+\([$_\w]+,\d+\);") + .Groups[1] + .Value; + + if (string.IsNullOrWhiteSpace(cipherContainerName)) + return null; + + // Find the definition of the cipher functions + var cipherDefinition = Regex + .Match( + _content, + // lang=js + $$""" + var {{Regex.Escape(cipherContainerName)}}={.*?}; + """, + RegexOptions.Singleline + ) + .Groups[0] + .Value + .NullIfWhiteSpace(); + + if (string.IsNullOrWhiteSpace(cipherDefinition)) + return null; + + // Identify the swap cipher function + var swapFuncName = Regex + .Match( + cipherDefinition, + @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?%[^}]*?}", + RegexOptions.Singleline + ) + .Groups[1] + .Value + .NullIfWhiteSpace(); + + // Identify the splice cipher function + var spliceFuncName = Regex + .Match( + cipherDefinition, + @"([$_\w]+):function\([$_\w]+,[$_\w]+\){+[^}]*?splice[^}]*?}", + RegexOptions.Singleline + ) + .Groups[1] + .Value + .NullIfWhiteSpace(); + + // Identify the reverse cipher function + var reverseFuncName = Regex + .Match( + cipherDefinition, + @"([$_\w]+):function\([$_\w]+\){+[^}]*?reverse[^}]*?}", + RegexOptions.Singleline + ) + .Groups[1] + .Value + .NullIfWhiteSpace(); + + var operations = new List(); + + foreach (var statement in cipherCallsite.Split(';')) { - operations.Add(new ReverseCipherOperation()); + var calledFuncName = Regex + .Match(statement, @"[$_\w]+\.([$_\w]+)\([$_\w]+,\d+\)") + .Groups[1] + .Value; + if (string.IsNullOrWhiteSpace(calledFuncName)) + continue; + + if (string.Equals(calledFuncName, swapFuncName, StringComparison.Ordinal)) + { + var index = Regex + .Match(statement, @"\([$_\w]+,(\d+)\)") + .Groups[1] + .Value + .ParseInt(); + operations.Add(new SwapCipherOperation(index)); + } + else if (string.Equals(calledFuncName, spliceFuncName, StringComparison.Ordinal)) + { + var index = Regex + .Match(statement, @"\([$_\w]+,(\d+)\)") + .Groups[1] + .Value + .ParseInt(); + operations.Add(new SpliceCipherOperation(index)); + } + else if (string.Equals(calledFuncName, reverseFuncName, StringComparison.Ordinal)) + { + operations.Add(new ReverseCipherOperation()); + } } - } - return new CipherManifest(signatureTimestamp, operations); - }); + return new CipherManifest(signatureTimestamp, operations); + } + } public PlayerSource(string content) => _content = content; } @@ -108,4 +146,4 @@ internal partial class PlayerSource internal partial class PlayerSource { public static PlayerSource Parse(string raw) => new(raw); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/PlaylistBrowseResponse.cs b/YoutubeDownloader/Bridge/PlaylistBrowseResponse.cs index 72c5cb67..7ab43b9e 100644 --- a/YoutubeDownloader/Bridge/PlaylistBrowseResponse.cs +++ b/YoutubeDownloader/Bridge/PlaylistBrowseResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -11,111 +12,103 @@ internal partial class PlaylistBrowseResponse : IPlaylistData { private readonly JsonElement _content; - private JsonElement? Sidebar => Memo.Cache(this, () => + [Lazy] + private JsonElement? Sidebar => _content - .GetPropertyOrNull("sidebar")? - .GetPropertyOrNull("playlistSidebarRenderer")? - .GetPropertyOrNull("items") - ); - - private JsonElement? SidebarPrimary => Memo.Cache(this, () => - Sidebar? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0)? - .GetPropertyOrNull("playlistSidebarPrimaryInfoRenderer") - ); - - private JsonElement? SidebarSecondary => Memo.Cache(this, () => - Sidebar? - .EnumerateArrayOrNull()? - .ElementAtOrNull(1)? - .GetPropertyOrNull("playlistSidebarSecondaryInfoRenderer") - ); - - public bool IsAvailable => Memo.Cache(this, () => - Sidebar is not null - ); - - public string? Title => Memo.Cache(this, () => - SidebarPrimary? - .GetPropertyOrNull("title")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - SidebarPrimary? - .GetPropertyOrNull("title")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .GetPropertyOrNull("sidebar") + ?.GetPropertyOrNull("playlistSidebarRenderer") + ?.GetPropertyOrNull("items"); + + [Lazy] + private JsonElement? SidebarPrimary => + Sidebar + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(0) + ?.GetPropertyOrNull("playlistSidebarPrimaryInfoRenderer"); + + [Lazy] + private JsonElement? SidebarSecondary => + Sidebar + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(1) + ?.GetPropertyOrNull("playlistSidebarSecondaryInfoRenderer"); + + [Lazy] + public bool IsAvailable => Sidebar is not null; + + [Lazy] + public string? Title => + SidebarPrimary + ?.GetPropertyOrNull("title") + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull() + ?? SidebarPrimary + ?.GetPropertyOrNull("title") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); - - private JsonElement? AuthorDetails => Memo.Cache(this, () => - SidebarSecondary? - .GetPropertyOrNull("videoOwner")? - .GetPropertyOrNull("videoOwnerRenderer") - ); - - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("title")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - AuthorDetails? - .GetPropertyOrNull("title")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .ConcatToString(); + + [Lazy] + private JsonElement? AuthorDetails => + SidebarSecondary?.GetPropertyOrNull("videoOwner")?.GetPropertyOrNull("videoOwnerRenderer"); + + [Lazy] + public string? Author => + AuthorDetails + ?.GetPropertyOrNull("title") + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull() + ?? AuthorDetails + ?.GetPropertyOrNull("title") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); - - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); - - public string? Description => Memo.Cache(this, () => - SidebarPrimary? - .GetPropertyOrNull("description")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - SidebarPrimary? - .GetPropertyOrNull("description")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .ConcatToString(); + + [Lazy] + public string? ChannelId => + AuthorDetails + ?.GetPropertyOrNull("navigationEndpoint") + ?.GetPropertyOrNull("browseEndpoint") + ?.GetPropertyOrNull("browseId") + ?.GetStringOrNull(); + + [Lazy] + public string? Description => + SidebarPrimary + ?.GetPropertyOrNull("description") + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull() + ?? SidebarPrimary + ?.GetPropertyOrNull("description") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - SidebarPrimary? - .GetPropertyOrNull("thumbnailRenderer")? - .GetPropertyOrNull("playlistVideoThumbnailRenderer")? - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - SidebarPrimary? - .GetPropertyOrNull("thumbnailRenderer")? - .GetPropertyOrNull("playlistCustomThumbnailRenderer")? - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ConcatToString(); + + [Lazy] + public IReadOnlyList Thumbnails => + SidebarPrimary + ?.GetPropertyOrNull("thumbnailRenderer") + ?.GetPropertyOrNull("playlistVideoThumbnailRenderer") + ?.GetPropertyOrNull("thumbnail") + ?.GetPropertyOrNull("thumbnails") + ?.EnumerateArrayOrNull() + ?.Select(j => new ThumbnailData(j)) + .ToArray() + ?? SidebarPrimary + ?.GetPropertyOrNull("thumbnailRenderer") + ?.GetPropertyOrNull("playlistCustomThumbnailRenderer") + ?.GetPropertyOrNull("thumbnail") + ?.GetPropertyOrNull("thumbnails") + ?.EnumerateArrayOrNull() + ?.Select(j => new ThumbnailData(j)) + .ToArray() + ?? Array.Empty(); public PlaylistBrowseResponse(JsonElement content) => _content = content; } @@ -123,4 +116,4 @@ Sidebar is not null internal partial class PlaylistBrowseResponse { public static PlaylistBrowseResponse Parse(string raw) => new(Json.Parse(raw)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/PlaylistNextResponse.cs b/YoutubeDownloader/Bridge/PlaylistNextResponse.cs index b4e36d6c..3adc5be9 100644 --- a/YoutubeDownloader/Bridge/PlaylistNextResponse.cs +++ b/YoutubeDownloader/Bridge/PlaylistNextResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -11,61 +12,51 @@ internal partial class PlaylistNextResponse : IPlaylistData { private readonly JsonElement _content; - private JsonElement? ContentRoot => Memo.Cache(this, () => + [Lazy] + private JsonElement? ContentRoot => _content - .GetPropertyOrNull("contents")? - .GetPropertyOrNull("twoColumnWatchNextResults")? - .GetPropertyOrNull("playlist")? - .GetPropertyOrNull("playlist") - ); + .GetPropertyOrNull("contents") + ?.GetPropertyOrNull("twoColumnWatchNextResults") + ?.GetPropertyOrNull("playlist") + ?.GetPropertyOrNull("playlist"); - public bool IsAvailable => Memo.Cache(this, () => - ContentRoot is not null - ); + [Lazy] + public bool IsAvailable => ContentRoot is not null; - public string? Title => Memo.Cache(this, () => - ContentRoot? - .GetPropertyOrNull("title")? - .GetStringOrNull() - ); + [Lazy] + public string? Title => ContentRoot?.GetPropertyOrNull("title")?.GetStringOrNull(); - public string? Author => Memo.Cache(this, () => - ContentRoot? - .GetPropertyOrNull("ownerName")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() - ); + [Lazy] + public string? Author => + ContentRoot + ?.GetPropertyOrNull("ownerName") + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull(); public string? ChannelId => null; public string? Description => null; - public IReadOnlyList Thumbnails => Memo.Cache(this, () => - Videos - .FirstOrDefault()? - .Thumbnails ?? + [Lazy] + public IReadOnlyList Thumbnails => + Videos.FirstOrDefault()?.Thumbnails ?? Array.Empty(); - Array.Empty() - ); - - public IReadOnlyList Videos => Memo.Cache(this, () => - ContentRoot? - .GetPropertyOrNull("contents")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("playlistPanelVideoRenderer")) + [Lazy] + public IReadOnlyList Videos => + ContentRoot + ?.GetPropertyOrNull("contents") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("playlistPanelVideoRenderer")) .WhereNotNull() .Select(j => new PlaylistVideoData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ToArray() ?? Array.Empty(); - public string? VisitorData => Memo.Cache(this, () => + [Lazy] + public string? VisitorData => _content - .GetPropertyOrNull("responseContext")? - .GetPropertyOrNull("visitorData")? - .GetStringOrNull() - ); + .GetPropertyOrNull("responseContext") + ?.GetPropertyOrNull("visitorData") + ?.GetStringOrNull(); public PlaylistNextResponse(JsonElement content) => _content = content; } @@ -73,4 +64,4 @@ ContentRoot is not null internal partial class PlaylistNextResponse { public static PlaylistNextResponse Parse(string raw) => new(Json.Parse(raw)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/PlaylistVideoData.cs b/YoutubeDownloader/Bridge/PlaylistVideoData.cs index e5a1d0cf..8655e0ad 100644 --- a/YoutubeDownloader/Bridge/PlaylistVideoData.cs +++ b/YoutubeDownloader/Bridge/PlaylistVideoData.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using YoutubeExplode.Utils; +using Lazy; using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Bridge; @@ -11,96 +11,81 @@ internal class PlaylistVideoData { private readonly JsonElement _content; - public int? Index => Memo.Cache(this, () => + [Lazy] + public int? Index => _content - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("watchEndpoint")? - .GetPropertyOrNull("index")? - .GetInt32OrNull() - ); - - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("videoId")? - .GetStringOrNull() - ); - - public string? Title => Memo.Cache(this, () => - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .GetPropertyOrNull("navigationEndpoint") + ?.GetPropertyOrNull("watchEndpoint") + ?.GetPropertyOrNull("index") + ?.GetInt32OrNull(); + + [Lazy] + public string? Id => _content.GetPropertyOrNull("videoId")?.GetStringOrNull(); + + [Lazy] + public string? Title => + _content.GetPropertyOrNull("title")?.GetPropertyOrNull("simpleText")?.GetStringOrNull() + ?? _content + .GetPropertyOrNull("title") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - private JsonElement? AuthorDetails => Memo.Cache(this, () => + [Lazy] + private JsonElement? AuthorDetails => _content - .GetPropertyOrNull("longBylineText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0) ?? - + .GetPropertyOrNull("longBylineText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(0) + ?? _content + .GetPropertyOrNull("shortBylineText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(0); + + [Lazy] + public string? Author => AuthorDetails?.GetPropertyOrNull("text")?.GetStringOrNull(); + + [Lazy] + public string? ChannelId => + AuthorDetails + ?.GetPropertyOrNull("navigationEndpoint") + ?.GetPropertyOrNull("browseEndpoint") + ?.GetPropertyOrNull("browseId") + ?.GetStringOrNull(); + + [Lazy] + public TimeSpan? Duration => _content - .GetPropertyOrNull("shortBylineText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0) - ); - - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("text")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); - - public TimeSpan? Duration => Memo.Cache(this, () => - _content - .GetPropertyOrNull("lengthSeconds")? - .GetStringOrNull()? - .ParseDoubleOrNull()? - .Pipe(TimeSpan.FromSeconds) ?? - - _content - .GetPropertyOrNull("lengthText")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull()? - .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) ?? - - _content - .GetPropertyOrNull("lengthText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .GetPropertyOrNull("lengthSeconds") + ?.GetStringOrNull() + ?.ParseDoubleOrNull() + ?.Pipe(TimeSpan.FromSeconds) + ?? _content + .GetPropertyOrNull("lengthText") + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull() + ?.ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) + ?? _content + .GetPropertyOrNull("lengthText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() .ConcatToString() - .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) - ); + .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }); - public IReadOnlyList Thumbnails => Memo.Cache(this, () => + [Lazy] + public IReadOnlyList Thumbnails => _content - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .GetPropertyOrNull("thumbnail") + ?.GetPropertyOrNull("thumbnails") + ?.EnumerateArrayOrNull() + ?.Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public PlaylistVideoData(JsonElement content) => _content = content; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/SearchResponse.cs b/YoutubeDownloader/Bridge/SearchResponse.cs index ff5702d8..2582a9bd 100644 --- a/YoutubeDownloader/Bridge/SearchResponse.cs +++ b/YoutubeDownloader/Bridge/SearchResponse.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -14,45 +15,39 @@ internal partial class SearchResponse // Search response is incredibly inconsistent (with at least 5 variations), // so we employ descendant searching, which is inefficient but resilient. - private JsonElement? ContentRoot => Memo.Cache(this, () => - _content.GetPropertyOrNull("contents") ?? - _content.GetPropertyOrNull("onResponseReceivedCommands") - ); + [Lazy] + private JsonElement? ContentRoot => + _content.GetPropertyOrNull("contents") + ?? _content.GetPropertyOrNull("onResponseReceivedCommands"); - public IReadOnlyList Videos => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("videoRenderer") + [Lazy] + public IReadOnlyList Videos => + ContentRoot + ?.EnumerateDescendantProperties("videoRenderer") .Select(j => new VideoData(j)) - .ToArray() ?? + .ToArray() ?? Array.Empty(); - Array.Empty() - ); - - public IReadOnlyList Playlists => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("playlistRenderer") + [Lazy] + public IReadOnlyList Playlists => + ContentRoot + ?.EnumerateDescendantProperties("playlistRenderer") .Select(j => new PlaylistData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ToArray() ?? Array.Empty(); - public IReadOnlyList Channels => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("channelRenderer") + [Lazy] + public IReadOnlyList Channels => + ContentRoot + ?.EnumerateDescendantProperties("channelRenderer") .Select(j => new ChannelData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ToArray() ?? Array.Empty(); - public string? ContinuationToken => Memo.Cache(this, () => - ContentRoot? - .EnumerateDescendantProperties("continuationCommand") - .FirstOrNull()? - .GetPropertyOrNull("token")? - .GetStringOrNull() - ); + [Lazy] + public string? ContinuationToken => + ContentRoot + ?.EnumerateDescendantProperties("continuationCommand") + .FirstOrNull() + ?.GetPropertyOrNull("token") + ?.GetStringOrNull(); public SearchResponse(JsonElement content) => _content = content; } @@ -63,82 +58,68 @@ internal class VideoData { private readonly JsonElement _content; - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("videoId")? - .GetStringOrNull() - ); - - public string? Title => Memo.Cache(this, () => - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + [Lazy] + public string? Id => _content.GetPropertyOrNull("videoId")?.GetStringOrNull(); + + [Lazy] + public string? Title => + _content.GetPropertyOrNull("title")?.GetPropertyOrNull("simpleText")?.GetStringOrNull() + ?? _content + .GetPropertyOrNull("title") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); - - private JsonElement? AuthorDetails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("longBylineText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0) ?? - - _content - .GetPropertyOrNull("shortBylineText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0) - ); - - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("text")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); - - public TimeSpan? Duration => Memo.Cache(this, () => - _content - .GetPropertyOrNull("lengthText")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull()? - .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) ?? - - _content - .GetPropertyOrNull("lengthText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .ConcatToString(); + + [Lazy] + private JsonElement? AuthorDetails => + _content + .GetPropertyOrNull("longBylineText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(0) + ?? _content + .GetPropertyOrNull("shortBylineText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(0); + + [Lazy] + public string? Author => AuthorDetails?.GetPropertyOrNull("text")?.GetStringOrNull(); + + [Lazy] + public string? ChannelId => + AuthorDetails + ?.GetPropertyOrNull("navigationEndpoint") + ?.GetPropertyOrNull("browseEndpoint") + ?.GetPropertyOrNull("browseId") + ?.GetStringOrNull(); + + [Lazy] + public TimeSpan? Duration => + _content + .GetPropertyOrNull("lengthText") + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull() + ?.ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) + ?? _content + .GetPropertyOrNull("lengthText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() .ConcatToString() - .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }) - ); + .ParseTimeSpanOrNull(new[] { @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss" }); - public IReadOnlyList Thumbnails => Memo.Cache(this, () => + [Lazy] + public IReadOnlyList Thumbnails => _content - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .GetPropertyOrNull("thumbnail") + ?.GetPropertyOrNull("thumbnails") + ?.EnumerateArrayOrNull() + ?.Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public VideoData(JsonElement content) => _content = content; } @@ -150,59 +131,47 @@ public class PlaylistData { private readonly JsonElement _content; - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("playlistId")? - .GetStringOrNull() - ); + [Lazy] + public string? Id => _content.GetPropertyOrNull("playlistId")?.GetStringOrNull(); + + [Lazy] + public string? Title => + _content.GetPropertyOrNull("title")?.GetPropertyOrNull("simpleText")?.GetStringOrNull() + ?? _content + .GetPropertyOrNull("title") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + .WhereNotNull() + .ConcatToString(); - public string? Title => Memo.Cache(this, () => + [Lazy] + private JsonElement? AuthorDetails => _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? + .GetPropertyOrNull("longBylineText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(0); - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) - .WhereNotNull() - .ConcatToString() - ); + [Lazy] + public string? Author => AuthorDetails?.GetPropertyOrNull("text")?.GetStringOrNull(); - private JsonElement? AuthorDetails => Memo.Cache(this, () => - _content - .GetPropertyOrNull("longBylineText")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .ElementAtOrNull(0) - ); - - public string? Author => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("text")? - .GetStringOrNull() - ); - - public string? ChannelId => Memo.Cache(this, () => - AuthorDetails? - .GetPropertyOrNull("navigationEndpoint")? - .GetPropertyOrNull("browseEndpoint")? - .GetPropertyOrNull("browseId")? - .GetStringOrNull() - ); - - public IReadOnlyList Thumbnails => Memo.Cache(this, () => + [Lazy] + public string? ChannelId => + AuthorDetails + ?.GetPropertyOrNull("navigationEndpoint") + ?.GetPropertyOrNull("browseEndpoint") + ?.GetPropertyOrNull("browseId") + ?.GetStringOrNull(); + + [Lazy] + public IReadOnlyList Thumbnails => _content - .GetPropertyOrNull("thumbnails")? - .EnumerateDescendantProperties("thumbnails") + .GetPropertyOrNull("thumbnails") + ?.EnumerateDescendantProperties("thumbnails") .SelectMany(j => j.EnumerateArrayOrEmpty()) .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .ToArray() ?? Array.Empty(); public PlaylistData(JsonElement content) => _content = content; } @@ -214,37 +183,28 @@ public class ChannelData { private readonly JsonElement _content; - public string? Id => Memo.Cache(this, () => - _content - .GetPropertyOrNull("channelId")? - .GetStringOrNull() - ); - - public string? Title => Memo.Cache(this, () => - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("simpleText")? - .GetStringOrNull() ?? - - _content - .GetPropertyOrNull("title")? - .GetPropertyOrNull("runs")? - .EnumerateArrayOrNull()? - .Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) + [Lazy] + public string? Id => _content.GetPropertyOrNull("channelId")?.GetStringOrNull(); + + [Lazy] + public string? Title => + _content.GetPropertyOrNull("title")?.GetPropertyOrNull("simpleText")?.GetStringOrNull() + ?? _content + .GetPropertyOrNull("title") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString() - ); + .ConcatToString(); - public IReadOnlyList Thumbnails => Memo.Cache(this, () => + [Lazy] + public IReadOnlyList Thumbnails => _content - .GetPropertyOrNull("thumbnail")? - .GetPropertyOrNull("thumbnails")? - .EnumerateArrayOrNull()? - .Select(j => new ThumbnailData(j)) - .ToArray() ?? - - Array.Empty() - ); + .GetPropertyOrNull("thumbnail") + ?.GetPropertyOrNull("thumbnails") + ?.EnumerateArrayOrNull() + ?.Select(j => new ThumbnailData(j)) + .ToArray() ?? Array.Empty(); public ChannelData(JsonElement content) => _content = content; } @@ -253,4 +213,4 @@ public class ChannelData internal partial class SearchResponse { public static SearchResponse Parse(string raw) => new(Json.Parse(raw)); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Bridge/ThumbnailData.cs b/YoutubeDownloader/Bridge/ThumbnailData.cs index aceda254..1fa6478b 100644 --- a/YoutubeDownloader/Bridge/ThumbnailData.cs +++ b/YoutubeDownloader/Bridge/ThumbnailData.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using YoutubeExplode.Utils; +using Lazy; using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Bridge; @@ -10,15 +10,12 @@ internal class ThumbnailData public ThumbnailData(JsonElement content) => _content = content; - public string? Url => Memo.Cache(this, () => - _content.GetPropertyOrNull("url")?.GetStringOrNull() - ); + [Lazy] + public string? Url => _content.GetPropertyOrNull("url")?.GetStringOrNull(); - public int? Width => Memo.Cache(this, () => - _content.GetPropertyOrNull("width")?.GetInt32OrNull() - ); + [Lazy] + public int? Width => _content.GetPropertyOrNull("width")?.GetInt32OrNull(); - public int? Height => Memo.Cache(this, () => - _content.GetPropertyOrNull("height")?.GetInt32OrNull() - ); -} \ No newline at end of file + [Lazy] + public int? Height => _content.GetPropertyOrNull("height")?.GetInt32OrNull(); +} diff --git a/YoutubeDownloader/Bridge/VideoWatchPage.cs b/YoutubeDownloader/Bridge/VideoWatchPage.cs index 89dc2b9d..36ddded4 100644 --- a/YoutubeDownloader/Bridge/VideoWatchPage.cs +++ b/YoutubeDownloader/Bridge/VideoWatchPage.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using AngleSharp.Dom; using AngleSharp.Html.Dom; +using Lazy; using YoutubeExplode.Utils; using YoutubeExplode.Utils.Extensions; @@ -13,77 +14,94 @@ internal partial class VideoWatchPage { private readonly IHtmlDocument _content; - public bool IsAvailable => Memo.Cache(this, () => - _content.QuerySelector("meta[property=\"og:url\"]") is not null - ); + [Lazy] + public bool IsAvailable => _content.QuerySelector("meta[property=\"og:url\"]") is not null; - public DateTimeOffset? UploadDate => Memo.Cache(this, () => + [Lazy] + public DateTimeOffset? UploadDate => _content - .QuerySelector("meta[itemprop=\"datePublished\"]")? - .GetAttribute("content")? - .NullIfWhiteSpace()? - .ParseDateTimeOffsetOrNull(new[] { @"yyyy-MM-dd" }) - ); + .QuerySelector("meta[itemprop=\"uploadDate\"]") + ?.GetAttribute("content") + ?.NullIfWhiteSpace() + ?.ParseDateTimeOffsetOrNull() + ?? _content + .QuerySelector("meta[itemprop=\"datePublished\"]") + ?.GetAttribute("content") + ?.NullIfWhiteSpace() + ?.ParseDateTimeOffsetOrNull(); - public long? LikeCount => Memo.Cache(this, () => + [Lazy] + public long? LikeCount => _content .Source .Text - .Pipe(s => Regex.Match( - s, - """ - "label"\s*:\s*"([\d,\.]+) likes" - """ - ).Groups[1].Value) - .NullIfWhiteSpace()? - .StripNonDigit() - .ParseLongOrNull() - ); + .Pipe( + s => + Regex + .Match( + s, + """ + "label"\s*:\s*"([\d,\.]+) likes" + """ + ) + .Groups[1] + .Value + ) + .NullIfWhiteSpace() + ?.StripNonDigit() + .ParseLongOrNull(); - public long? DislikeCount => Memo.Cache(this, () => + [Lazy] + public long? DislikeCount => _content .Source .Text - .Pipe(s => Regex.Match( - s, - """ - "label"\s*:\s*"([\d,\.]+) dislikes" - """ - ).Groups[1].Value) - .NullIfWhiteSpace()? - .StripNonDigit() - .ParseLongOrNull() - ); + .Pipe( + s => + Regex + .Match( + s, + """ + "label"\s*:\s*"([\d,\.]+) dislikes" + """ + ) + .Groups[1] + .Value + ) + .NullIfWhiteSpace() + ?.StripNonDigit() + .ParseLongOrNull(); - private JsonElement? PlayerConfig => Memo.Cache(this, () => + [Lazy] + private JsonElement? PlayerConfig => _content .GetElementsByTagName("script") .Select(e => e.Text()) .Select(s => Regex.Match(s, @"ytplayer\.config\s*=\s*(\{.*\})").Groups[1].Value) - .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))? - .NullIfWhiteSpace()? - .Pipe(Json.Extract) - .Pipe(Json.TryParse) - ); + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) + ?.NullIfWhiteSpace() + ?.Pipe(Json.Extract) + .Pipe(Json.TryParse); - public PlayerResponse? PlayerResponse => Memo.Cache(this, () => + [Lazy] + public PlayerResponse? PlayerResponse => _content .GetElementsByTagName("script") .Select(e => e.Text()) - .Select(s => Regex.Match(s, @"var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})").Groups[1].Value) - .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))? - .NullIfWhiteSpace()? - .Pipe(Json.Extract) - .Pipe(Json.TryParse)? - .Pipe(j => new PlayerResponse(j)) ?? - - PlayerConfig? - .GetPropertyOrNull("args")? - .GetPropertyOrNull("player_response")? - .GetStringOrNull()? - .Pipe(Json.TryParse)? - .Pipe(j => new PlayerResponse(j)) - ); + .Select( + s => Regex.Match(s, @"var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})").Groups[1].Value + ) + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) + ?.NullIfWhiteSpace() + ?.Pipe(Json.Extract) + .Pipe(Json.TryParse) + ?.Pipe(j => new PlayerResponse(j)) + ?? PlayerConfig + ?.GetPropertyOrNull("args") + ?.GetPropertyOrNull("player_response") + ?.GetStringOrNull() + ?.Pipe(Json.TryParse) + ?.Pipe(j => new PlayerResponse(j)); public VideoWatchPage(IHtmlDocument content) => _content = content; } @@ -99,4 +117,4 @@ internal partial class VideoWatchPage return new VideoWatchPage(content); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/Channel.cs b/YoutubeDownloader/Channels/Channel.cs index aa6417e7..3bf472d0 100644 --- a/YoutubeDownloader/Channels/Channel.cs +++ b/YoutubeDownloader/Channels/Channel.cs @@ -34,4 +34,4 @@ public Channel(ChannelId id, string title, IReadOnlyList thumbnails) /// [ExcludeFromCodeCoverage] public override string ToString() => $"Channel ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/ChannelClient.cs b/YoutubeDownloader/Channels/ChannelClient.cs index 81a69963..0b643dae 100644 --- a/YoutubeDownloader/Channels/ChannelClient.cs +++ b/YoutubeDownloader/Channels/ChannelClient.cs @@ -32,30 +32,27 @@ public ChannelClient(HttpClient http) private Channel Get(ChannelPage channelPage) { var channelId = - channelPage.Id ?? - throw new YoutubeExplodeException("Could not extract channel ID."); + channelPage.Id ?? throw new YoutubeExplodeException("Could not extract channel ID."); var title = - channelPage.Title ?? - throw new YoutubeExplodeException("Could not extract channel title."); + channelPage.Title + ?? throw new YoutubeExplodeException("Could not extract channel title."); var logoUrl = - channelPage.LogoUrl ?? - throw new YoutubeExplodeException("Could not extract channel logo URL."); + channelPage.LogoUrl + ?? throw new YoutubeExplodeException("Could not extract channel logo URL."); - var logoSize = Regex - .Matches(logoUrl, @"\bs(\d+)\b") - .ToArray() - .LastOrDefault()? - .Groups[1] - .Value - .NullIfWhiteSpace()? - .ParseIntOrNull() ?? 100; + var logoSize = + Regex + .Matches(logoUrl, @"\bs(\d+)\b") + .ToArray() + .LastOrDefault() + ?.Groups[1] + .Value + .NullIfWhiteSpace() + ?.ParseIntOrNull() ?? 100; - var thumbnails = new[] - { - new Thumbnail(logoUrl, new Resolution(logoSize, logoSize)) - }; + var thumbnails = new[] { new Thumbnail(logoUrl, new Resolution(logoSize, logoSize)) }; return new Channel(channelId, title, thumbnails); } @@ -65,7 +62,8 @@ private Channel Get(ChannelPage channelPage) /// public async ValueTask GetAsync( ChannelId channelId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { // Special case for the "Movies & TV" channel, which has a custom page if (channelId == "UCuVPpxrm2VAgpH3Ktln4HXg") @@ -91,24 +89,24 @@ public async ValueTask GetAsync( /// public async ValueTask GetByUserAsync( UserName userName, - CancellationToken cancellationToken = default) => - Get(await _controller.GetChannelPageAsync(userName, cancellationToken)); + CancellationToken cancellationToken = default + ) => Get(await _controller.GetChannelPageAsync(userName, cancellationToken)); /// /// Gets the metadata associated with the channel identified by the specified slug or legacy custom URL. /// public async ValueTask GetBySlugAsync( ChannelSlug channelSlug, - CancellationToken cancellationToken = default) => - Get(await _controller.GetChannelPageAsync(channelSlug, cancellationToken)); + CancellationToken cancellationToken = default + ) => Get(await _controller.GetChannelPageAsync(channelSlug, cancellationToken)); /// /// Gets the metadata associated with the channel identified by the specified handle or custom URL. /// public async ValueTask GetByHandleAsync( ChannelHandle channelHandle, - CancellationToken cancellationToken = default) => - Get(await _controller.GetChannelPageAsync(channelHandle, cancellationToken)); + CancellationToken cancellationToken = default + ) => Get(await _controller.GetChannelPageAsync(channelHandle, cancellationToken)); /// /// Enumerates videos uploaded by the specified channel. @@ -116,10 +114,11 @@ public async ValueTask GetByHandleAsync( // TODO: should return sequence instead (breaking change) public IAsyncEnumerable GetUploadsAsync( ChannelId channelId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { // Replace 'UC' in the channel ID with 'UU' var playlistId = "UU" + channelId.Value[2..]; return new PlaylistClient(_http).GetVideosAsync(playlistId, cancellationToken); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/ChannelController.cs b/YoutubeDownloader/Channels/ChannelController.cs index cf72df13..596bc6de 100644 --- a/YoutubeDownloader/Channels/ChannelController.cs +++ b/YoutubeDownloader/Channels/ChannelController.cs @@ -14,12 +14,16 @@ internal class ChannelController private async ValueTask GetChannelPageAsync( string channelRoute, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - for (var retriesRemaining = 5;; retriesRemaining--) + for (var retriesRemaining = 5; ; retriesRemaining--) { var channelPage = ChannelPage.TryParse( - await _http.GetStringAsync("https://www.youtube.com/" + channelRoute, cancellationToken) + await _http.GetStringAsync( + "https://www.youtube.com/" + channelRoute, + cancellationToken + ) ); if (channelPage is null) @@ -28,8 +32,7 @@ await _http.GetStringAsync("https://www.youtube.com/" + channelRoute, cancellati continue; throw new YoutubeExplodeException( - "Channel page is broken. " + - "Please try again in a few minutes." + "Channel page is broken. " + "Please try again in a few minutes." ); } @@ -39,21 +42,21 @@ await _http.GetStringAsync("https://www.youtube.com/" + channelRoute, cancellati public async ValueTask GetChannelPageAsync( ChannelId channelId, - CancellationToken cancellationToken = default) => - await GetChannelPageAsync("channel/" + channelId, cancellationToken); + CancellationToken cancellationToken = default + ) => await GetChannelPageAsync("channel/" + channelId, cancellationToken); public async ValueTask GetChannelPageAsync( UserName userName, - CancellationToken cancellationToken = default) => - await GetChannelPageAsync("user/" + userName, cancellationToken); + CancellationToken cancellationToken = default + ) => await GetChannelPageAsync("user/" + userName, cancellationToken); public async ValueTask GetChannelPageAsync( ChannelSlug channelSlug, - CancellationToken cancellationToken = default) => - await GetChannelPageAsync("c/" + channelSlug, cancellationToken); + CancellationToken cancellationToken = default + ) => await GetChannelPageAsync("c/" + channelSlug, cancellationToken); public async ValueTask GetChannelPageAsync( ChannelHandle channelHandle, - CancellationToken cancellationToken = default) => - await GetChannelPageAsync("@" + channelHandle, cancellationToken); -} \ No newline at end of file + CancellationToken cancellationToken = default + ) => await GetChannelPageAsync("@" + channelHandle, cancellationToken); +} diff --git a/YoutubeDownloader/Channels/ChannelHandle.cs b/YoutubeDownloader/Channels/ChannelHandle.cs index 7d54010f..44a7d80b 100644 --- a/YoutubeDownloader/Channels/ChannelHandle.cs +++ b/YoutubeDownloader/Channels/ChannelHandle.cs @@ -63,16 +63,19 @@ private static bool IsValid(string channelHandle) => /// Parses the specified string as a YouTube channel handle or custom URL. /// public static ChannelHandle Parse(string channelHandleOrUrl) => - TryParse(channelHandleOrUrl) ?? - throw new ArgumentException($"Invalid YouTube channel handle or custom URL '{channelHandleOrUrl}'."); + TryParse(channelHandleOrUrl) + ?? throw new ArgumentException( + $"Invalid YouTube channel handle or custom URL '{channelHandleOrUrl}'." + ); /// /// Converts string to channel handle. /// - public static implicit operator ChannelHandle(string channelHandleOrUrl) => Parse(channelHandleOrUrl); + public static implicit operator ChannelHandle(string channelHandleOrUrl) => + Parse(channelHandleOrUrl); /// /// Converts channel handle to string. /// public static implicit operator string(ChannelHandle channelHandle) => channelHandle.ToString(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/ChannelId.cs b/YoutubeDownloader/Channels/ChannelId.cs index e97232fd..1047ec1e 100644 --- a/YoutubeDownloader/Channels/ChannelId.cs +++ b/YoutubeDownloader/Channels/ChannelId.cs @@ -25,9 +25,9 @@ public readonly partial struct ChannelId public partial struct ChannelId { private static bool IsValid(string channelId) => - channelId.StartsWith("UC", StringComparison.Ordinal) && - channelId.Length == 24 && - channelId.All(c => char.IsLetterOrDigit(c) || c is '_' or '-'); + channelId.StartsWith("UC", StringComparison.Ordinal) + && channelId.Length == 24 + && channelId.All(c => char.IsLetterOrDigit(c) || c is '_' or '-'); private static string? TryNormalize(string? channelIdOrUrl) { @@ -65,8 +65,8 @@ private static bool IsValid(string channelId) => /// Parses the specified string as a YouTube channel ID or URL. /// public static ChannelId Parse(string channelIdOrUrl) => - TryParse(channelIdOrUrl) ?? - throw new ArgumentException($"Invalid YouTube channel ID or URL '{channelIdOrUrl}'."); + TryParse(channelIdOrUrl) + ?? throw new ArgumentException($"Invalid YouTube channel ID or URL '{channelIdOrUrl}'."); /// /// Converts string to ID. @@ -99,4 +99,4 @@ public partial struct ChannelId : IEquatable /// Equality check. /// public static bool operator !=(ChannelId left, ChannelId right) => !(left == right); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/ChannelSlug.cs b/YoutubeDownloader/Channels/ChannelSlug.cs index 279bba22..00996d46 100644 --- a/YoutubeDownloader/Channels/ChannelSlug.cs +++ b/YoutubeDownloader/Channels/ChannelSlug.cs @@ -24,8 +24,7 @@ public readonly partial struct ChannelSlug public readonly partial struct ChannelSlug { - private static bool IsValid(string channelSlug) => - channelSlug.All(char.IsLetterOrDigit); + private static bool IsValid(string channelSlug) => channelSlug.All(char.IsLetterOrDigit); private static string? TryNormalize(string? channelSlugOrUrl) { @@ -63,8 +62,10 @@ private static bool IsValid(string channelSlug) => /// Parses the specified string as a YouTube channel slug or legacy custom url. /// public static ChannelSlug Parse(string channelSlugOrUrl) => - TryParse(channelSlugOrUrl) ?? - throw new ArgumentException($"Invalid YouTube channel slug or legacy custom URL '{channelSlugOrUrl}'."); + TryParse(channelSlugOrUrl) + ?? throw new ArgumentException( + $"Invalid YouTube channel slug or legacy custom URL '{channelSlugOrUrl}'." + ); /// /// Converts string to channel slug. @@ -75,4 +76,4 @@ public static ChannelSlug Parse(string channelSlugOrUrl) => /// Converts channel slug to string. /// public static implicit operator string(ChannelSlug channelSlug) => channelSlug.ToString(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/IChannel.cs b/YoutubeDownloader/Channels/IChannel.cs index 75411728..ba1e1e0b 100644 --- a/YoutubeDownloader/Channels/IChannel.cs +++ b/YoutubeDownloader/Channels/IChannel.cs @@ -27,4 +27,4 @@ public interface IChannel /// Channel thumbnails. /// IReadOnlyList Thumbnails { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Channels/UserName.cs b/YoutubeDownloader/Channels/UserName.cs index 7c9af0b4..1d12407f 100644 --- a/YoutubeDownloader/Channels/UserName.cs +++ b/YoutubeDownloader/Channels/UserName.cs @@ -25,8 +25,7 @@ public readonly partial struct UserName public partial struct UserName { private static bool IsValid(string userName) => - userName.Length <= 20 && - userName.All(char.IsLetterOrDigit); + userName.Length <= 20 && userName.All(char.IsLetterOrDigit); private static string? TryNormalize(string? userNameOrUrl) { @@ -64,8 +63,10 @@ private static bool IsValid(string userName) => /// Parses the specified string as a YouTube user name or profile URL. /// public static UserName Parse(string userNameOrUrl) => - TryParse(userNameOrUrl) ?? - throw new ArgumentException($"Invalid YouTube user name or profile URL '{userNameOrUrl}'."); + TryParse(userNameOrUrl) + ?? throw new ArgumentException( + $"Invalid YouTube user name or profile URL '{userNameOrUrl}'." + ); /// /// Converts string to user name. @@ -98,4 +99,4 @@ public partial struct UserName : IEquatable /// Equality check. /// public static bool operator !=(UserName left, UserName right) => !(left == right); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Common/Author.cs b/YoutubeDownloader/Common/Author.cs index 7fdc995b..ecfbbc9c 100644 --- a/YoutubeDownloader/Common/Author.cs +++ b/YoutubeDownloader/Common/Author.cs @@ -40,4 +40,4 @@ public Author(ChannelId channelId, string channelTitle) /// [ExcludeFromCodeCoverage] public override string ToString() => ChannelTitle; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Common/Batch.cs b/YoutubeDownloader/Common/Batch.cs index 87780f37..f6187e3e 100644 --- a/YoutubeDownloader/Common/Batch.cs +++ b/YoutubeDownloader/Common/Batch.cs @@ -6,7 +6,8 @@ namespace YoutubeExplode.Common; /// /// Generic collection of items returned by a single request. /// -public class Batch where T : IBatchItem +public class Batch + where T : IBatchItem { /// /// Items included in the batch. @@ -21,11 +22,12 @@ public class Batch where T : IBatchItem internal static class Batch { - public static Batch Create(IReadOnlyList items) where T : IBatchItem => new(items); + public static Batch Create(IReadOnlyList items) + where T : IBatchItem => new(items); } internal static class BatchExtensions { public static IAsyncEnumerable FlattenAsync(this IAsyncEnumerable> source) where T : IBatchItem => source.SelectManyAsync(b => b.Items); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Common/IBatchItem.cs b/YoutubeDownloader/Common/IBatchItem.cs index c59bdcbe..8a58d9c4 100644 --- a/YoutubeDownloader/Common/IBatchItem.cs +++ b/YoutubeDownloader/Common/IBatchItem.cs @@ -9,9 +9,7 @@ namespace YoutubeExplode.Common; /// Represents an item that can be included in . /// This interface is used as a marker to enable extension methods. /// -public interface IBatchItem -{ -} +public interface IBatchItem { } /// /// Extensions for . @@ -28,17 +26,19 @@ public static class BatchItemExtensions /// /// Enumerates all items in the sequence and buffers them in memory. /// - public static async ValueTask> CollectAsync( - this IAsyncEnumerable source) where T : IBatchItem => await source.ToListAsync(); + public static async ValueTask> CollectAsync(this IAsyncEnumerable source) + where T : IBatchItem => await source.ToListAsync(); /// /// Enumerates a subset of items in the sequence and buffers them in memory. /// public static async ValueTask> CollectAsync( this IAsyncEnumerable source, - int count) where T : IBatchItem => await source.TakeAsync(count).ToListAsync(); + int count + ) + where T : IBatchItem => await source.TakeAsync(count).ToListAsync(); /// - public static ValueTaskAwaiter> GetAwaiter( - this IAsyncEnumerable source) where T : IBatchItem => source.CollectAsync().GetAwaiter(); -} \ No newline at end of file + public static ValueTaskAwaiter> GetAwaiter(this IAsyncEnumerable source) + where T : IBatchItem => source.CollectAsync().GetAwaiter(); +} diff --git a/YoutubeDownloader/Common/Resolution.cs b/YoutubeDownloader/Common/Resolution.cs index 6705e27b..f9429206 100644 --- a/YoutubeDownloader/Common/Resolution.cs +++ b/YoutubeDownloader/Common/Resolution.cs @@ -57,4 +57,4 @@ public partial struct Resolution : IEquatable /// Equality check. /// public static bool operator !=(Resolution left, Resolution right) => !(left == right); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Common/Thumbnail.cs b/YoutubeDownloader/Common/Thumbnail.cs index 6ee0c063..691ad8bf 100644 --- a/YoutubeDownloader/Common/Thumbnail.cs +++ b/YoutubeDownloader/Common/Thumbnail.cs @@ -37,12 +37,22 @@ public Thumbnail(string url, Resolution resolution) public partial class Thumbnail { - internal static IReadOnlyList GetDefaultSet(VideoId videoId) => new[] - { - new Thumbnail($"https://img.youtube.com/vi/{videoId}/default.jpg", new Resolution(120, 90)), - new Thumbnail($"https://img.youtube.com/vi/{videoId}/mqdefault.jpg", new Resolution(320, 180)), - new Thumbnail($"https://img.youtube.com/vi/{videoId}/hqdefault.jpg", new Resolution(480, 360)) - }; + internal static IReadOnlyList GetDefaultSet(VideoId videoId) => + new[] + { + new Thumbnail( + $"https://img.youtube.com/vi/{videoId}/default.jpg", + new Resolution(120, 90) + ), + new Thumbnail( + $"https://img.youtube.com/vi/{videoId}/mqdefault.jpg", + new Resolution(320, 180) + ), + new Thumbnail( + $"https://img.youtube.com/vi/{videoId}/hqdefault.jpg", + new Resolution(480, 360) + ) + }; } /// @@ -61,6 +71,6 @@ public static class ThumbnailExtensions /// Gets the thumbnail with the highest resolution (by area). /// public static Thumbnail GetWithHighestResolution(this IEnumerable thumbnails) => - thumbnails.TryGetWithHighestResolution() ?? - throw new InvalidOperationException("Input thumbnail collection is empty."); -} \ No newline at end of file + thumbnails.TryGetWithHighestResolution() + ?? throw new InvalidOperationException("Input thumbnail collection is empty."); +} diff --git a/YoutubeDownloader/Exceptions/PlaylistUnavailableException.cs b/YoutubeDownloader/Exceptions/PlaylistUnavailableException.cs index 46cbc23c..696ad1b1 100644 --- a/YoutubeDownloader/Exceptions/PlaylistUnavailableException.cs +++ b/YoutubeDownloader/Exceptions/PlaylistUnavailableException.cs @@ -8,7 +8,6 @@ public class PlaylistUnavailableException : YoutubeExplodeException /// /// Initializes an instance of . /// - public PlaylistUnavailableException(string message) : base(message) - { - } -} \ No newline at end of file + public PlaylistUnavailableException(string message) + : base(message) { } +} diff --git a/YoutubeDownloader/Exceptions/RequestLimitExceededException.cs b/YoutubeDownloader/Exceptions/RequestLimitExceededException.cs index 7fa3b65b..a52cf053 100644 --- a/YoutubeDownloader/Exceptions/RequestLimitExceededException.cs +++ b/YoutubeDownloader/Exceptions/RequestLimitExceededException.cs @@ -8,7 +8,6 @@ public class RequestLimitExceededException : YoutubeExplodeException /// /// Initializes an instance of . /// - public RequestLimitExceededException(string message) : base(message) - { - } -} \ No newline at end of file + public RequestLimitExceededException(string message) + : base(message) { } +} diff --git a/YoutubeDownloader/Exceptions/VideoRequiresPurchaseException.cs b/YoutubeDownloader/Exceptions/VideoRequiresPurchaseException.cs index d5ea0736..6325c2c8 100644 --- a/YoutubeDownloader/Exceptions/VideoRequiresPurchaseException.cs +++ b/YoutubeDownloader/Exceptions/VideoRequiresPurchaseException.cs @@ -15,6 +15,6 @@ public class VideoRequiresPurchaseException : VideoUnplayableException /// /// Initializes an instance of /// - public VideoRequiresPurchaseException(string message, VideoId previewVideoId) : base(message) => - PreviewVideoId = previewVideoId; -} \ No newline at end of file + public VideoRequiresPurchaseException(string message, VideoId previewVideoId) + : base(message) => PreviewVideoId = previewVideoId; +} diff --git a/YoutubeDownloader/Exceptions/VideoUnavailableException.cs b/YoutubeDownloader/Exceptions/VideoUnavailableException.cs index cbd8183a..af7f0e1a 100644 --- a/YoutubeDownloader/Exceptions/VideoUnavailableException.cs +++ b/YoutubeDownloader/Exceptions/VideoUnavailableException.cs @@ -8,7 +8,6 @@ public class VideoUnavailableException : VideoUnplayableException /// /// Initializes an instance of . /// - public VideoUnavailableException(string message) : base(message) - { - } -} \ No newline at end of file + public VideoUnavailableException(string message) + : base(message) { } +} diff --git a/YoutubeDownloader/Exceptions/VideoUnplayableException.cs b/YoutubeDownloader/Exceptions/VideoUnplayableException.cs index b9ee1f51..c7c65f7f 100644 --- a/YoutubeDownloader/Exceptions/VideoUnplayableException.cs +++ b/YoutubeDownloader/Exceptions/VideoUnplayableException.cs @@ -8,7 +8,6 @@ public class VideoUnplayableException : YoutubeExplodeException /// /// Initializes an instance of . /// - public VideoUnplayableException(string message) : base(message) - { - } -} \ No newline at end of file + public VideoUnplayableException(string message) + : base(message) { } +} diff --git a/YoutubeDownloader/Exceptions/YoutubeExplodeException.cs b/YoutubeDownloader/Exceptions/YoutubeExplodeException.cs index 60c399cf..e8385d66 100644 --- a/YoutubeDownloader/Exceptions/YoutubeExplodeException.cs +++ b/YoutubeDownloader/Exceptions/YoutubeExplodeException.cs @@ -11,7 +11,6 @@ public class YoutubeExplodeException : Exception /// Initializes an instance of . /// /// - public YoutubeExplodeException(string message) : base(message) - { - } -} \ No newline at end of file + public YoutubeExplodeException(string message) + : base(message) { } +} diff --git a/YoutubeDownloader/FodyWeavers.xml b/YoutubeDownloader/FodyWeavers.xml new file mode 100644 index 00000000..6ef70586 --- /dev/null +++ b/YoutubeDownloader/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/YoutubeDownloader/FodyWeavers.xsd b/YoutubeDownloader/FodyWeavers.xsd new file mode 100644 index 00000000..fe819e8e --- /dev/null +++ b/YoutubeDownloader/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/YoutubeDownloader/IYoutubeClient.cs b/YoutubeDownloader/IYoutubeClient.cs index 543ee7f6..11553a73 100644 --- a/YoutubeDownloader/IYoutubeClient.cs +++ b/YoutubeDownloader/IYoutubeClient.cs @@ -30,4 +30,4 @@ public interface IYoutubeClient /// public VideoClient Videos { get; } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Playlists/IPlaylist.cs b/YoutubeDownloader/Playlists/IPlaylist.cs index c617a802..ead927fe 100644 --- a/YoutubeDownloader/Playlists/IPlaylist.cs +++ b/YoutubeDownloader/Playlists/IPlaylist.cs @@ -35,4 +35,4 @@ public interface IPlaylist /// Playlist thumbnails. /// IReadOnlyList Thumbnails { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Playlists/Playlist.cs b/YoutubeDownloader/Playlists/Playlist.cs index fef05f89..141c9dec 100644 --- a/YoutubeDownloader/Playlists/Playlist.cs +++ b/YoutubeDownloader/Playlists/Playlist.cs @@ -37,7 +37,8 @@ public Playlist( string title, Author? author, string description, - IReadOnlyList thumbnails) + IReadOnlyList thumbnails + ) { Id = id; Title = title; @@ -49,4 +50,4 @@ public Playlist( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Playlist ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Playlists/PlaylistClient.cs b/YoutubeDownloader/Playlists/PlaylistClient.cs index 95d206e8..4ef40271 100644 --- a/YoutubeDownloader/Playlists/PlaylistClient.cs +++ b/YoutubeDownloader/Playlists/PlaylistClient.cs @@ -27,50 +27,48 @@ public class PlaylistClient /// public async ValueTask GetAsync( PlaylistId playlistId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var response = await _controller.GetPlaylistResponseAsync(playlistId, cancellationToken); var title = - response.Title ?? - throw new YoutubeExplodeException("Could not extract playlist title."); + response.Title + ?? throw new YoutubeExplodeException("Could not extract playlist title."); // System playlists have no author var channelId = response.ChannelId; var channelTitle = response.Author; - var author = channelId is not null && channelTitle is not null - ? new Author(channelId, channelTitle) - : null; + var author = + channelId is not null && channelTitle is not null + ? new Author(channelId, channelTitle) + : null; // System playlists have no description var description = response.Description ?? ""; - var thumbnails = response.Thumbnails.Select(t => - { - var thumbnailUrl = - t.Url ?? - throw new YoutubeExplodeException("Could not extract thumbnail URL."); - - var thumbnailWidth = - t.Width ?? - throw new YoutubeExplodeException("Could not extract thumbnail width."); - - var thumbnailHeight = - t.Height ?? - throw new YoutubeExplodeException("Could not extract thumbnail height."); - - var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); - - return new Thumbnail(thumbnailUrl, thumbnailResolution); - }).ToArray(); - - return new Playlist( - playlistId, - title, - author, - description, - thumbnails - ); + var thumbnails = response + .Thumbnails + .Select(t => + { + var thumbnailUrl = + t.Url ?? throw new YoutubeExplodeException("Could not extract thumbnail URL."); + + var thumbnailWidth = + t.Width + ?? throw new YoutubeExplodeException("Could not extract thumbnail width."); + + var thumbnailHeight = + t.Height + ?? throw new YoutubeExplodeException("Could not extract thumbnail height."); + + var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); + + return new Thumbnail(thumbnailUrl, thumbnailResolution); + }) + .ToArray(); + + return new Playlist(playlistId, title, author, description, thumbnails); } /// @@ -78,7 +76,8 @@ public async ValueTask GetAsync( /// public async IAsyncEnumerable> GetVideoBatchesAsync( PlaylistId playlistId, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { var encounteredIds = new HashSet(); var lastVideoId = default(VideoId?); @@ -100,51 +99,61 @@ public async IAsyncEnumerable> GetVideoBatchesAsync( foreach (var videoData in response.Videos) { var videoId = - videoData.Id ?? - throw new YoutubeExplodeException("Could not extract video ID."); + videoData.Id + ?? throw new YoutubeExplodeException("Could not extract video ID."); lastVideoId = videoId; lastVideoIndex = - videoData.Index ?? - throw new YoutubeExplodeException("Could not extract video index."); + videoData.Index + ?? throw new YoutubeExplodeException("Could not extract video index."); // Don't yield the same video twice if (!encounteredIds.Add(videoId)) continue; var videoTitle = - videoData.Title ?? + videoData.Title // Videos without title are legal // https://github.com/Tyrrrz/YoutubeExplode/issues/700 - ""; + ?? ""; var videoChannelTitle = - videoData.Author ?? - throw new YoutubeExplodeException("Could not extract video author."); + videoData.Author + ?? throw new YoutubeExplodeException("Could not extract video author."); var videoChannelId = - videoData.ChannelId ?? - throw new YoutubeExplodeException("Could not extract video channel ID."); - - var videoThumbnails = videoData.Thumbnails.Select(t => - { - var thumbnailUrl = - t.Url ?? - throw new YoutubeExplodeException("Could not extract thumbnail URL."); - - var thumbnailWidth = - t.Width ?? - throw new YoutubeExplodeException("Could not extract thumbnail width."); - - var thumbnailHeight = - t.Height ?? - throw new YoutubeExplodeException("Could not extract thumbnail height."); - - var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); - - return new Thumbnail(thumbnailUrl, thumbnailResolution); - }).Concat(Thumbnail.GetDefaultSet(videoId)).ToArray(); + videoData.ChannelId + ?? throw new YoutubeExplodeException("Could not extract video channel ID."); + + var videoThumbnails = videoData + .Thumbnails + .Select(t => + { + var thumbnailUrl = + t.Url + ?? throw new YoutubeExplodeException( + "Could not extract thumbnail URL." + ); + + var thumbnailWidth = + t.Width + ?? throw new YoutubeExplodeException( + "Could not extract thumbnail width." + ); + + var thumbnailHeight = + t.Height + ?? throw new YoutubeExplodeException( + "Could not extract thumbnail height." + ); + + var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); + + return new Thumbnail(thumbnailUrl, thumbnailResolution); + }) + .Concat(Thumbnail.GetDefaultSet(videoId)) + .ToArray(); var video = new PlaylistVideo( playlistId, @@ -173,6 +182,6 @@ public async IAsyncEnumerable> GetVideoBatchesAsync( /// public IAsyncEnumerable GetVideosAsync( PlaylistId playlistId, - CancellationToken cancellationToken = default) => - GetVideoBatchesAsync(playlistId, cancellationToken).FlattenAsync(); -} \ No newline at end of file + CancellationToken cancellationToken = default + ) => GetVideoBatchesAsync(playlistId, cancellationToken).FlattenAsync(); +} diff --git a/YoutubeDownloader/Playlists/PlaylistController.cs b/YoutubeDownloader/Playlists/PlaylistController.cs index 63e7d442..dd4618ab 100644 --- a/YoutubeDownloader/Playlists/PlaylistController.cs +++ b/YoutubeDownloader/Playlists/PlaylistController.cs @@ -16,9 +16,13 @@ internal class PlaylistController // Works only with user-made playlists public async ValueTask GetPlaylistBrowseResponseAsync( PlaylistId playlistId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - using var request = new HttpRequestMessage(HttpMethod.Post, "https://www.youtube.com/youtubei/v1/browse") + using var request = new HttpRequestMessage( + HttpMethod.Post, + "https://www.youtube.com/youtubei/v1/browse" + ) { Content = new StringContent( // lang=json @@ -58,11 +62,15 @@ public async ValueTask GetPlaylistNextResponseAsync( VideoId? videoId = null, int index = 0, string? visitorData = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - for (var retriesRemaining = 5;; retriesRemaining--) + for (var retriesRemaining = 5; ; retriesRemaining--) { - using var request = new HttpRequestMessage(HttpMethod.Post, "https://www.youtube.com/youtubei/v1/next") + using var request = new HttpRequestMessage( + HttpMethod.Post, + "https://www.youtube.com/youtubei/v1/next" + ) { Content = new StringContent( // lang=json @@ -100,7 +108,9 @@ await response.Content.ReadAsStringAsync(cancellationToken) if (index > 0 && !string.IsNullOrWhiteSpace(visitorData) && retriesRemaining > 0) continue; - throw new PlaylistUnavailableException($"Playlist '{playlistId}' is not available."); + throw new PlaylistUnavailableException( + $"Playlist '{playlistId}' is not available." + ); } return playlistResponse; @@ -109,7 +119,8 @@ await response.Content.ReadAsStringAsync(cancellationToken) public async ValueTask GetPlaylistResponseAsync( PlaylistId playlistId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { try { @@ -120,4 +131,4 @@ public async ValueTask GetPlaylistResponseAsync( return await GetPlaylistNextResponseAsync(playlistId, null, 0, null, cancellationToken); } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Playlists/PlaylistId.cs b/YoutubeDownloader/Playlists/PlaylistId.cs index 36583f20..3d765d75 100644 --- a/YoutubeDownloader/Playlists/PlaylistId.cs +++ b/YoutubeDownloader/Playlists/PlaylistId.cs @@ -26,8 +26,8 @@ public partial struct PlaylistId { private static bool IsValid(string playlistId) => // Playlist IDs vary greatly in length, but they are at least 2 characters long - playlistId.Length >= 2 && - playlistId.All(c => char.IsLetterOrDigit(c) || c is '_' or '-'); + playlistId.Length >= 2 + && playlistId.All(c => char.IsLetterOrDigit(c) || c is '_' or '-'); private static string? TryNormalize(string? playlistIdOrUrl) { @@ -98,8 +98,8 @@ private static bool IsValid(string playlistId) => /// Parses the specified string as a YouTube playlist ID or URL. /// public static PlaylistId Parse(string playlistIdOrUrl) => - TryParse(playlistIdOrUrl) ?? - throw new ArgumentException($"Invalid YouTube playlist ID or URL '{playlistIdOrUrl}'."); + TryParse(playlistIdOrUrl) + ?? throw new ArgumentException($"Invalid YouTube playlist ID or URL '{playlistIdOrUrl}'."); /// /// Converts string to ID. @@ -132,4 +132,4 @@ public partial struct PlaylistId : IEquatable /// Equality check. /// public static bool operator !=(PlaylistId left, PlaylistId right) => !(left == right); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Playlists/PlaylistVideo.cs b/YoutubeDownloader/Playlists/PlaylistVideo.cs index b4c7727a..8b50a68e 100644 --- a/YoutubeDownloader/Playlists/PlaylistVideo.cs +++ b/YoutubeDownloader/Playlists/PlaylistVideo.cs @@ -43,7 +43,8 @@ public PlaylistVideo( string title, Author author, TimeSpan? duration, - IReadOnlyList thumbnails) + IReadOnlyList thumbnails + ) { PlaylistId = playlistId; Id = id; @@ -63,12 +64,11 @@ public PlaylistVideo( string title, Author author, TimeSpan? duration, - IReadOnlyList thumbnails) - : this(default, id, title, author, duration, thumbnails) - { - } + IReadOnlyList thumbnails + ) + : this(default, id, title, author, duration, thumbnails) { } /// [ExcludeFromCodeCoverage] public override string ToString() => $"Video ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/ChannelSearchResult.cs b/YoutubeDownloader/Search/ChannelSearchResult.cs index 3f02e378..8bdaf94b 100644 --- a/YoutubeDownloader/Search/ChannelSearchResult.cs +++ b/YoutubeDownloader/Search/ChannelSearchResult.cs @@ -35,4 +35,4 @@ public ChannelSearchResult(ChannelId id, string title, IReadOnlyList /// [ExcludeFromCodeCoverage] public override string ToString() => $"Channel ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/ISearchResult.cs b/YoutubeDownloader/Search/ISearchResult.cs index ed5f111a..06c30482 100644 --- a/YoutubeDownloader/Search/ISearchResult.cs +++ b/YoutubeDownloader/Search/ISearchResult.cs @@ -27,4 +27,4 @@ public interface ISearchResult : IBatchItem /// Result title. /// string Title { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/PlaylistSearchResult.cs b/YoutubeDownloader/Search/PlaylistSearchResult.cs index ec51bb0a..49127f0e 100644 --- a/YoutubeDownloader/Search/PlaylistSearchResult.cs +++ b/YoutubeDownloader/Search/PlaylistSearchResult.cs @@ -32,7 +32,8 @@ public PlaylistSearchResult( PlaylistId id, string title, Author? author, - IReadOnlyList thumbnails) + IReadOnlyList thumbnails + ) { Id = id; Title = title; @@ -43,4 +44,4 @@ public PlaylistSearchResult( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Playlist ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/SearchClient.cs b/YoutubeDownloader/Search/SearchClient.cs index f2051044..b896259b 100644 --- a/YoutubeDownloader/Search/SearchClient.cs +++ b/YoutubeDownloader/Search/SearchClient.cs @@ -29,7 +29,8 @@ public class SearchClient public async IAsyncEnumerable> GetResultBatchesAsync( string searchQuery, SearchFilter searchFilter, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { var encounteredIds = new HashSet(StringComparer.Ordinal); var continuationToken = default(string?); @@ -55,43 +56,53 @@ public async IAsyncEnumerable> GetResultBatchesAsync( } var videoId = - videoData.Id ?? - throw new YoutubeExplodeException("Could not extract video ID."); + videoData.Id + ?? throw new YoutubeExplodeException("Could not extract video ID."); // Don't yield the same result twice if (!encounteredIds.Add(videoId)) continue; var videoTitle = - videoData.Title ?? - throw new YoutubeExplodeException("Could not extract video title."); + videoData.Title + ?? throw new YoutubeExplodeException("Could not extract video title."); var videoChannelTitle = - videoData.Author ?? - throw new YoutubeExplodeException("Could not extract video author."); + videoData.Author + ?? throw new YoutubeExplodeException("Could not extract video author."); var videoChannelId = - videoData.ChannelId ?? - throw new YoutubeExplodeException("Could not extract video channel ID."); - - var videoThumbnails = videoData.Thumbnails.Select(t => - { - var thumbnailUrl = - t.Url ?? - throw new YoutubeExplodeException("Could not extract video thumbnail URL."); - - var thumbnailWidth = - t.Width ?? - throw new YoutubeExplodeException("Could not extract video thumbnail width."); - - var thumbnailHeight = - t.Height ?? - throw new YoutubeExplodeException("Could not extract video thumbnail height."); - - var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); - - return new Thumbnail(thumbnailUrl, thumbnailResolution); - }).Concat(Thumbnail.GetDefaultSet(videoId)).ToArray(); + videoData.ChannelId + ?? throw new YoutubeExplodeException("Could not extract video channel ID."); + + var videoThumbnails = videoData + .Thumbnails + .Select(t => + { + var thumbnailUrl = + t.Url + ?? throw new YoutubeExplodeException( + "Could not extract video thumbnail URL." + ); + + var thumbnailWidth = + t.Width + ?? throw new YoutubeExplodeException( + "Could not extract video thumbnail width." + ); + + var thumbnailHeight = + t.Height + ?? throw new YoutubeExplodeException( + "Could not extract video thumbnail height." + ); + + var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); + + return new Thumbnail(thumbnailUrl, thumbnailResolution); + }) + .Concat(Thumbnail.GetDefaultSet(videoId)) + .ToArray(); var video = new VideoSearchResult( videoId, @@ -114,42 +125,51 @@ public async IAsyncEnumerable> GetResultBatchesAsync( } var playlistId = - playlistData.Id ?? - throw new YoutubeExplodeException("Could not extract playlist ID."); + playlistData.Id + ?? throw new YoutubeExplodeException("Could not extract playlist ID."); // Don't yield the same result twice if (!encounteredIds.Add(playlistId)) continue; var playlistTitle = - playlistData.Title ?? - throw new YoutubeExplodeException("Could not extract playlist title."); + playlistData.Title + ?? throw new YoutubeExplodeException("Could not extract playlist title."); // System playlists have no author var playlistAuthor = - !string.IsNullOrWhiteSpace(playlistData.ChannelId) && - !string.IsNullOrWhiteSpace(playlistData.Author) + !string.IsNullOrWhiteSpace(playlistData.ChannelId) + && !string.IsNullOrWhiteSpace(playlistData.Author) ? new Author(playlistData.ChannelId, playlistData.Author) : null; - var playlistThumbnails = playlistData.Thumbnails.Select(t => - { - var thumbnailUrl = - t.Url ?? - throw new YoutubeExplodeException("Could not extract playlist thumbnail URL."); - - var thumbnailWidth = - t.Width ?? - throw new YoutubeExplodeException("Could not extract playlist thumbnail width."); - - var thumbnailHeight = - t.Height ?? - throw new YoutubeExplodeException("Could not extract playlist thumbnail height."); - - var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); - - return new Thumbnail(thumbnailUrl, thumbnailResolution); - }).ToArray(); + var playlistThumbnails = playlistData + .Thumbnails + .Select(t => + { + var thumbnailUrl = + t.Url + ?? throw new YoutubeExplodeException( + "Could not extract playlist thumbnail URL." + ); + + var thumbnailWidth = + t.Width + ?? throw new YoutubeExplodeException( + "Could not extract playlist thumbnail width." + ); + + var thumbnailHeight = + t.Height + ?? throw new YoutubeExplodeException( + "Could not extract playlist thumbnail height." + ); + + var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); + + return new Thumbnail(thumbnailUrl, thumbnailResolution); + }) + .ToArray(); var playlist = new PlaylistSearchResult( playlistId, @@ -171,37 +191,42 @@ public async IAsyncEnumerable> GetResultBatchesAsync( } var channelId = - channelData.Id ?? - throw new YoutubeExplodeException("Could not extract channel ID."); + channelData.Id + ?? throw new YoutubeExplodeException("Could not extract channel ID."); var channelTitle = - channelData.Title ?? - throw new YoutubeExplodeException("Could not extract channel title."); - - var channelThumbnails = channelData.Thumbnails.Select(t => - { - var thumbnailUrl = - t.Url ?? - throw new YoutubeExplodeException("Could not extract channel thumbnail URL."); - - var thumbnailWidth = - t.Width ?? - throw new YoutubeExplodeException("Could not extract channel thumbnail width."); - - var thumbnailHeight = - t.Height ?? - throw new YoutubeExplodeException("Could not extract channel thumbnail height."); - - var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); - - return new Thumbnail(thumbnailUrl, thumbnailResolution); - }).ToArray(); - - var channel = new ChannelSearchResult( - channelId, - channelTitle, - channelThumbnails - ); + channelData.Title + ?? throw new YoutubeExplodeException("Could not extract channel title."); + + var channelThumbnails = channelData + .Thumbnails + .Select(t => + { + var thumbnailUrl = + t.Url + ?? throw new YoutubeExplodeException( + "Could not extract channel thumbnail URL." + ); + + var thumbnailWidth = + t.Width + ?? throw new YoutubeExplodeException( + "Could not extract channel thumbnail width." + ); + + var thumbnailHeight = + t.Height + ?? throw new YoutubeExplodeException( + "Could not extract channel thumbnail height." + ); + + var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); + + return new Thumbnail(thumbnailUrl, thumbnailResolution); + }) + .ToArray(); + + var channel = new ChannelSearchResult(channelId, channelTitle, channelThumbnails); results.Add(channel); } @@ -217,23 +242,24 @@ public async IAsyncEnumerable> GetResultBatchesAsync( /// public IAsyncEnumerable> GetResultBatchesAsync( string searchQuery, - CancellationToken cancellationToken = default) => - GetResultBatchesAsync(searchQuery, SearchFilter.None, cancellationToken); + CancellationToken cancellationToken = default + ) => GetResultBatchesAsync(searchQuery, SearchFilter.None, cancellationToken); /// /// Enumerates search results returned by the specified query. /// public IAsyncEnumerable GetResultsAsync( string searchQuery, - CancellationToken cancellationToken = default) => - GetResultBatchesAsync(searchQuery, cancellationToken).FlattenAsync(); + CancellationToken cancellationToken = default + ) => GetResultBatchesAsync(searchQuery, cancellationToken).FlattenAsync(); /// /// Enumerates video search results returned by the specified query. /// public IAsyncEnumerable GetVideosAsync( string searchQuery, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default + ) => GetResultBatchesAsync(searchQuery, SearchFilter.Video, cancellationToken) .FlattenAsync() .OfTypeAsync(); @@ -243,7 +269,8 @@ public IAsyncEnumerable GetVideosAsync( /// public IAsyncEnumerable GetPlaylistsAsync( string searchQuery, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default + ) => GetResultBatchesAsync(searchQuery, SearchFilter.Playlist, cancellationToken) .FlattenAsync() .OfTypeAsync(); @@ -253,8 +280,9 @@ public IAsyncEnumerable GetPlaylistsAsync( /// public IAsyncEnumerable GetChannelsAsync( string searchQuery, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default + ) => GetResultBatchesAsync(searchQuery, SearchFilter.Channel, cancellationToken) .FlattenAsync() .OfTypeAsync(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/SearchController.cs b/YoutubeDownloader/Search/SearchController.cs index 7fed981d..81b13a28 100644 --- a/YoutubeDownloader/Search/SearchController.cs +++ b/YoutubeDownloader/Search/SearchController.cs @@ -15,9 +15,13 @@ public async ValueTask GetSearchResponseAsync( string searchQuery, SearchFilter searchFilter, string? continuationToken, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - using var request = new HttpRequestMessage(HttpMethod.Post, "https://www.youtube.com/youtubei/v1/search") + using var request = new HttpRequestMessage( + HttpMethod.Post, + "https://www.youtube.com/youtubei/v1/search" + ) { Content = new StringContent( // lang=json @@ -49,8 +53,6 @@ public async ValueTask GetSearchResponseAsync( using var response = await _http.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - return SearchResponse.Parse( - await response.Content.ReadAsStringAsync(cancellationToken) - ); + return SearchResponse.Parse(await response.Content.ReadAsStringAsync(cancellationToken)); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/SearchFilter.cs b/YoutubeDownloader/Search/SearchFilter.cs index e9e84986..9c25daec 100644 --- a/YoutubeDownloader/Search/SearchFilter.cs +++ b/YoutubeDownloader/Search/SearchFilter.cs @@ -24,4 +24,4 @@ public enum SearchFilter /// Only search for channels. /// Channel -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Search/VideoSearchResult.cs b/YoutubeDownloader/Search/VideoSearchResult.cs index fe3885cd..dd847578 100644 --- a/YoutubeDownloader/Search/VideoSearchResult.cs +++ b/YoutubeDownloader/Search/VideoSearchResult.cs @@ -37,7 +37,8 @@ public VideoSearchResult( string title, Author author, TimeSpan? duration, - IReadOnlyList thumbnails) + IReadOnlyList thumbnails + ) { Id = id; Title = title; @@ -49,4 +50,4 @@ public VideoSearchResult( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Video ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/ClientDelegatingHandler.cs b/YoutubeDownloader/Utils/ClientDelegatingHandler.cs index 83f8a856..02155d1d 100644 --- a/YoutubeDownloader/Utils/ClientDelegatingHandler.cs +++ b/YoutubeDownloader/Utils/ClientDelegatingHandler.cs @@ -1,10 +1,12 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using YoutubeExplode.Utils.Extensions; namespace YoutubeExplode.Utils; -// Used to extend an externally provided HttpClient with additional behavior +// Like DelegatingHandler, but wraps an HttpClient instead of an HttpMessageHandler. +// Used to extend an externally provided HttpClient with additional behavior. internal abstract class ClientDelegatingHandler : HttpMessageHandler { private readonly HttpClient _http; @@ -18,8 +20,19 @@ protected ClientDelegatingHandler(HttpClient http, bool disposeClient = false) protected override async Task SendAsync( HttpRequestMessage request, - CancellationToken cancellationToken) => - await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + CancellationToken cancellationToken + ) + { + // Clone the request to reset its completion status, which is required + // in order to pass the request from one HttpClient to another. + using var clonedRequest = request.Clone(); + + return await _http.SendAsync( + clonedRequest, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ); + } protected override void Dispose(bool disposing) { @@ -28,4 +41,4 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/AsyncCollectionExtensions.cs b/YoutubeDownloader/Utils/Extensions/AsyncCollectionExtensions.cs index 5811f7b4..b0765444 100644 --- a/YoutubeDownloader/Utils/Extensions/AsyncCollectionExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/AsyncCollectionExtensions.cs @@ -23,7 +23,8 @@ public static async IAsyncEnumerable TakeAsync(this IAsyncEnumerable so public static async IAsyncEnumerable SelectManyAsync( this IAsyncEnumerable source, - Func> transform) + Func> transform + ) { await foreach (var i in source) { @@ -53,4 +54,4 @@ public static async ValueTask> ToListAsync(this IAsyncEnumerable s public static ValueTaskAwaiter> GetAwaiter(this IAsyncEnumerable source) => source.ToListAsync().GetAwaiter(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/BinaryExtensions.cs b/YoutubeDownloader/Utils/Extensions/BinaryExtensions.cs index 52b5e80b..cf807201 100644 --- a/YoutubeDownloader/Utils/Extensions/BinaryExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/BinaryExtensions.cs @@ -11,11 +11,9 @@ public static string ToHex(this byte[] data, bool isUpperCase = true) foreach (var b in data) { - buffer.Append( - b.ToString(isUpperCase ? "X2" : "x2", CultureInfo.InvariantCulture) - ); + buffer.Append(b.ToString(isUpperCase ? "X2" : "x2", CultureInfo.InvariantCulture)); } return buffer.ToString(); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/CollectionExtensions.cs b/YoutubeDownloader/Utils/Extensions/CollectionExtensions.cs index 260ba86b..4bd44148 100644 --- a/YoutubeDownloader/Utils/Extensions/CollectionExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/CollectionExtensions.cs @@ -12,7 +12,8 @@ internal static class CollectionExtensions yield return (o, i++); } - public static IEnumerable WhereNotNull(this IEnumerable source) where T : class + public static IEnumerable WhereNotNull(this IEnumerable source) + where T : class { foreach (var i in source) { @@ -21,7 +22,8 @@ public static IEnumerable WhereNotNull(this IEnumerable source) where } } - public static IEnumerable WhereNotNull(this IEnumerable source) where T : struct + public static IEnumerable WhereNotNull(this IEnumerable source) + where T : struct { foreach (var i in source) { @@ -30,19 +32,19 @@ public static IEnumerable WhereNotNull(this IEnumerable source) where } } - public static T? ElementAtOrNull(this IEnumerable source, int index) where T : struct + public static T? ElementAtOrNull(this IEnumerable source, int index) + where T : struct { var sourceAsList = source as IReadOnlyList ?? source.ToArray(); - return index < sourceAsList.Count - ? sourceAsList[index] - : null; + return index < sourceAsList.Count ? sourceAsList[index] : null; } - public static T? FirstOrNull(this IEnumerable source) where T : struct + public static T? FirstOrNull(this IEnumerable source) + where T : struct { foreach (var i in source) return i; return null; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/GenericExtensions.cs b/YoutubeDownloader/Utils/Extensions/GenericExtensions.cs index b7e034c3..dde7bed4 100644 --- a/YoutubeDownloader/Utils/Extensions/GenericExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/GenericExtensions.cs @@ -4,5 +4,6 @@ namespace YoutubeExplode.Utils.Extensions; internal static class GenericExtensions { - public static TOut Pipe(this TIn input, Func transform) => transform(input); -} \ No newline at end of file + public static TOut Pipe(this TIn input, Func transform) => + transform(input); +} diff --git a/YoutubeDownloader/Utils/Extensions/HttpExtensions.cs b/YoutubeDownloader/Utils/Extensions/HttpExtensions.cs index fcd7d895..2bc31677 100644 --- a/YoutubeDownloader/Utils/Extensions/HttpExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/HttpExtensions.cs @@ -1,4 +1,6 @@ -using System.Net.Http; +using System.IO; +using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -6,25 +8,55 @@ namespace YoutubeExplode.Utils.Extensions; internal static class HttpExtensions { - public static HttpRequestMessage Clone(this HttpRequestMessage request) + private class NonDisposableHttpContent : HttpContent { - var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri); + private readonly HttpContent _content; + + public NonDisposableHttpContent(HttpContent content) => _content = content; - clonedRequest.Content = request.Content; - clonedRequest.Version = request.Version; + protected override async Task SerializeToStreamAsync( + Stream stream, + TransportContext? context + ) => await _content.CopyToAsync(stream); + + protected override bool TryComputeLength(out long length) + { + length = default; + return false; + } + } + + public static HttpRequestMessage Clone(this HttpRequestMessage request) + { + var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri) + { + Version = request.Version, + // Don't dispose the original request's content + Content = request.Content is not null + ? new NonDisposableHttpContent(request.Content) + : null + }; foreach (var (key, value) in request.Headers) clonedRequest.Headers.TryAddWithoutValidation(key, value); + if (request.Content is not null && clonedRequest.Content is not null) + { + foreach (var (key, value) in request.Content.Headers) + clonedRequest.Content.Headers.TryAddWithoutValidation(key, value); + } + return clonedRequest; } public static async ValueTask HeadAsync( this HttpClient http, string requestUri, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { using var request = new HttpRequestMessage(HttpMethod.Head, requestUri); + return await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, @@ -36,7 +68,8 @@ public static async ValueTask HeadAsync( this HttpClient http, string requestUri, bool ensureSuccess = true, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { using var response = await http.HeadAsync(requestUri, cancellationToken); @@ -45,4 +78,4 @@ public static async ValueTask HeadAsync( return response.Content.Headers.ContentLength; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/JsonExtensions.cs b/YoutubeDownloader/Utils/Extensions/JsonExtensions.cs index ebb7b050..70628a7b 100644 --- a/YoutubeDownloader/Utils/Extensions/JsonExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/JsonExtensions.cs @@ -13,9 +13,11 @@ internal static class JsonExtensions return null; } - if (element.TryGetProperty(propertyName, out var result) && - result.ValueKind != JsonValueKind.Null && - result.ValueKind != JsonValueKind.Undefined) + if ( + element.TryGetProperty(propertyName, out var result) + && result.ValueKind != JsonValueKind.Null + && result.ValueKind != JsonValueKind.Undefined + ) { return result; } @@ -24,9 +26,7 @@ internal static class JsonExtensions } public static string? GetStringOrNull(this JsonElement element) => - element.ValueKind == JsonValueKind.String - ? element.GetString() - : null; + element.ValueKind == JsonValueKind.String ? element.GetString() : null; public static int? GetInt32OrNull(this JsonElement element) => element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out var result) @@ -39,24 +39,21 @@ internal static class JsonExtensions : null; public static JsonElement.ArrayEnumerator? EnumerateArrayOrNull(this JsonElement element) => - element.ValueKind == JsonValueKind.Array - ? element.EnumerateArray() - : null; + element.ValueKind == JsonValueKind.Array ? element.EnumerateArray() : null; public static JsonElement.ArrayEnumerator EnumerateArrayOrEmpty(this JsonElement element) => element.EnumerateArrayOrNull() ?? default; public static JsonElement.ObjectEnumerator? EnumerateObjectOrNull(this JsonElement element) => - element.ValueKind == JsonValueKind.Object - ? element.EnumerateObject() - : null; + element.ValueKind == JsonValueKind.Object ? element.EnumerateObject() : null; public static JsonElement.ObjectEnumerator EnumerateObjectOrEmpty(this JsonElement element) => element.EnumerateObjectOrNull() ?? default; public static IEnumerable EnumerateDescendantProperties( this JsonElement element, - string propertyName) + string propertyName + ) { // Check if this property exists on the current object var property = element.GetPropertyOrNull(propertyName); @@ -79,4 +76,4 @@ public static IEnumerable EnumerateDescendantProperties( foreach (var deepDescendant in deepObjectDescendants) yield return deepDescendant; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/StreamExtensions.cs b/YoutubeDownloader/Utils/Extensions/StreamExtensions.cs index 6cf02161..a4311388 100644 --- a/YoutubeDownloader/Utils/Extensions/StreamExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/StreamExtensions.cs @@ -12,7 +12,8 @@ public static async ValueTask CopyToAsync( this Stream source, Stream destination, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { using var buffer = MemoryPool.Shared.Rent(81920); @@ -29,4 +30,4 @@ public static async ValueTask CopyToAsync( progress?.Report(1.0 * totalBytesRead / source.Length); } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/StringExtensions.cs b/YoutubeDownloader/Utils/Extensions/StringExtensions.cs index 5efc42bc..f23e6041 100644 --- a/YoutubeDownloader/Utils/Extensions/StringExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/StringExtensions.cs @@ -9,26 +9,24 @@ namespace YoutubeExplode.Utils.Extensions; internal static class StringExtensions { public static string? NullIfWhiteSpace(this string str) => - !string.IsNullOrWhiteSpace(str) - ? str - : null; + !string.IsNullOrWhiteSpace(str) ? str : null; public static string SubstringUntil( this string str, string sub, - StringComparison comparison = StringComparison.Ordinal) + StringComparison comparison = StringComparison.Ordinal + ) { var index = str.IndexOf(sub, comparison); - return index < 0 - ? str - : str[..index]; + return index < 0 ? str : str[..index]; } public static string SubstringAfter( this string str, string sub, - StringComparison comparison = StringComparison.Ordinal) + StringComparison comparison = StringComparison.Ordinal + ) { var index = str.IndexOf(sub, comparison); @@ -70,8 +68,8 @@ public static string SwapChars(this string str, int firstCharIndex, int secondCh : null; public static int ParseInt(this string str) => - ParseIntOrNull(str) ?? - throw new FormatException($"Cannot parse integer number from string '{str}'."); + ParseIntOrNull(str) + ?? throw new FormatException($"Cannot parse integer number from string '{str}'."); public static long? ParseLongOrNull(this string str) => long.TryParse(str, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var result) @@ -79,8 +77,12 @@ public static int ParseInt(this string str) => : null; public static double? ParseDoubleOrNull(this string str) => - double.TryParse(str, NumberStyles.Float | NumberStyles.AllowThousands, NumberFormatInfo.InvariantInfo, - out var result) + double.TryParse( + str, + NumberStyles.Float | NumberStyles.AllowThousands, + NumberFormatInfo.InvariantInfo, + out var result + ) ? result : null; @@ -89,15 +91,15 @@ public static int ParseInt(this string str) => ? result : null; - public static DateTimeOffset? ParseDateTimeOffsetOrNull(this string str, string[] formats) => - DateTimeOffset.TryParseExact( + public static DateTimeOffset? ParseDateTimeOffsetOrNull(this string str) => + DateTimeOffset.TryParse( str, - formats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, - out var result) + out var result + ) ? result : null; public static string ConcatToString(this IEnumerable source) => string.Concat(source); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/UriExtensions.cs b/YoutubeDownloader/Utils/Extensions/UriExtensions.cs index bb7434e3..a62ff117 100644 --- a/YoutubeDownloader/Utils/Extensions/UriExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/UriExtensions.cs @@ -5,4 +5,4 @@ namespace YoutubeExplode.Utils.Extensions; internal static class UriExtensions { public static string GetDomain(this Uri uri) => uri.Scheme + Uri.SchemeDelimiter + uri.Host; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Extensions/XElementExtensions.cs b/YoutubeDownloader/Utils/Extensions/XElementExtensions.cs index 1ade1865..629b19fe 100644 --- a/YoutubeDownloader/Utils/Extensions/XElementExtensions.cs +++ b/YoutubeDownloader/Utils/Extensions/XElementExtensions.cs @@ -19,11 +19,15 @@ public static XElement StripNamespaces(this XElement element) descendantElement .Attributes() .Where(a => !a.IsNamespaceDeclaration) - .Where(a => a.Name.Namespace != XNamespace.Xml && a.Name.Namespace != XNamespace.Xmlns) + .Where( + a => + a.Name.Namespace != XNamespace.Xml + && a.Name.Namespace != XNamespace.Xmlns + ) .Select(a => new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value)) ); } return result; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Hash.cs b/YoutubeDownloader/Utils/Hash.cs index 543a59df..593bdff7 100644 --- a/YoutubeDownloader/Utils/Hash.cs +++ b/YoutubeDownloader/Utils/Hash.cs @@ -9,4 +9,4 @@ public static byte[] Compute(HashAlgorithm algorithm, byte[] data) using (algorithm) return algorithm.ComputeHash(data); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Html.cs b/YoutubeDownloader/Utils/Html.cs index 18d70d59..b060a24e 100644 --- a/YoutubeDownloader/Utils/Html.cs +++ b/YoutubeDownloader/Utils/Html.cs @@ -8,4 +8,4 @@ internal static class Html private static readonly HtmlParser HtmlParser = new(); public static IHtmlDocument Parse(string source) => HtmlParser.ParseDocument(source); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Http.cs b/YoutubeDownloader/Utils/Http.cs index b102994f..30947972 100644 --- a/YoutubeDownloader/Utils/Http.cs +++ b/YoutubeDownloader/Utils/Http.cs @@ -8,4 +8,4 @@ internal static class Http private static readonly Lazy HttpClientLazy = new(() => new HttpClient()); public static HttpClient Client => HttpClientLazy.Value; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Json.cs b/YoutubeDownloader/Utils/Json.cs index 76c6b71c..62bb0b0d 100644 --- a/YoutubeDownloader/Utils/Json.cs +++ b/YoutubeDownloader/Utils/Json.cs @@ -56,4 +56,4 @@ public static JsonElement Parse(string source) return null; } } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Memo.cs b/YoutubeDownloader/Utils/Memo.cs deleted file mode 100644 index 9c55328c..00000000 --- a/YoutubeDownloader/Utils/Memo.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace YoutubeExplode.Utils; - -// Helper utility used to cache the result of a function -internal static class Memo -{ - private static class For - { - private static readonly ConditionalWeakTable> CacheManifest = new(); - - public static Dictionary GetCache(object owner) => - CacheManifest.GetOrCreateValue(owner); - } - - public static T Cache(object owner, Func getValue) - { - var cache = For.GetCache(owner); - var key = getValue.Method.GetHashCode(); - - if (cache.TryGetValue(key, out var cachedValue)) - return cachedValue; - - var value = getValue(); - cache[key] = value; - - return value; - } -} \ No newline at end of file diff --git a/YoutubeDownloader/Utils/UrlEx.cs b/YoutubeDownloader/Utils/UrlEx.cs index 5898d613..7ce499f7 100644 --- a/YoutubeDownloader/Utils/UrlEx.cs +++ b/YoutubeDownloader/Utils/UrlEx.cs @@ -11,9 +11,7 @@ internal static class UrlEx { private static IEnumerable> EnumerateQueryParameters(string url) { - var query = url.Contains('?') - ? url.SubstringAfter("?") - : url; + var query = url.Contains('?') ? url.SubstringAfter("?") : url; foreach (var parameter in query.Split('&')) { @@ -60,11 +58,7 @@ public static string RemoveQueryParameter(string url, string key) if (string.Equals(parameter.Key, key, StringComparison.Ordinal)) continue; - queryBuilder.Append( - queryBuilder.Length > 0 - ? '&' - : '?' - ); + queryBuilder.Append(queryBuilder.Length > 0 ? '&' : '?'); queryBuilder.Append(WebUtility.UrlEncode(parameter.Key)); queryBuilder.Append('='); @@ -81,11 +75,10 @@ public static string SetQueryParameter(string url, string key, string value) var urlWithoutParameter = RemoveQueryParameter(url, key); var hasOtherParameters = urlWithoutParameter.Contains('?'); - return - urlWithoutParameter + - (hasOtherParameters ? '&' : '?') + - WebUtility.UrlEncode(key) + - '=' + - WebUtility.UrlEncode(value); + return urlWithoutParameter + + (hasOtherParameters ? '&' : '?') + + WebUtility.UrlEncode(key) + + '=' + + WebUtility.UrlEncode(value); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Utils/Xml.cs b/YoutubeDownloader/Utils/Xml.cs index 69eddc37..1e8f770a 100644 --- a/YoutubeDownloader/Utils/Xml.cs +++ b/YoutubeDownloader/Utils/Xml.cs @@ -7,4 +7,4 @@ internal static class Xml { public static XElement Parse(string source) => XElement.Parse(source, LoadOptions.PreserveWhitespace).StripNamespaces(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaption.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaption.cs index 511911b4..1b3f8a3e 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaption.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaption.cs @@ -40,7 +40,8 @@ public ClosedCaption( string text, TimeSpan offset, TimeSpan duration, - IReadOnlyList parts) + IReadOnlyList parts + ) { Text = text; Offset = offset; @@ -59,10 +60,10 @@ public ClosedCaption( /// Gets the caption part displayed at the specified point in time, relative to the caption's own offset. /// public ClosedCaptionPart GetPartByTime(TimeSpan time) => - TryGetPartByTime(time) ?? - throw new InvalidOperationException($"No closed caption part found at {time}."); + TryGetPartByTime(time) + ?? throw new InvalidOperationException($"No closed caption part found at {time}."); /// [ExcludeFromCodeCoverage] public override string ToString() => Text; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionClient.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionClient.cs index b6817f3e..157b8a5a 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionClient.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionClient.cs @@ -26,24 +26,28 @@ public class ClosedCaptionClient private async IAsyncEnumerable GetClosedCaptionTrackInfosAsync( VideoId videoId, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { // Use the TVHTML5 client instead of ANDROID_TESTSUITE because the latter doesn't provide closed captions - var playerResponse = await _controller.GetPlayerResponseAsync(videoId, null, cancellationToken); + var playerResponse = await _controller.GetPlayerResponseAsync( + videoId, + null, + cancellationToken + ); foreach (var trackData in playerResponse.ClosedCaptionTracks) { var url = - trackData.Url ?? - throw new YoutubeExplodeException("Could not extract track URL."); + trackData.Url ?? throw new YoutubeExplodeException("Could not extract track URL."); var languageCode = - trackData.LanguageCode ?? - throw new YoutubeExplodeException("Could not extract track language code."); + trackData.LanguageCode + ?? throw new YoutubeExplodeException("Could not extract track language code."); var languageName = - trackData.LanguageName ?? - throw new YoutubeExplodeException("Could not extract track language name."); + trackData.LanguageName + ?? throw new YoutubeExplodeException("Could not extract track language name."); yield return new ClosedCaptionTrackInfo( url, @@ -58,14 +62,18 @@ private async IAsyncEnumerable GetClosedCaptionTrackInfo /// public async ValueTask GetManifestAsync( VideoId videoId, - CancellationToken cancellationToken = default) => - new(await GetClosedCaptionTrackInfosAsync(videoId, cancellationToken)); + CancellationToken cancellationToken = default + ) => new(await GetClosedCaptionTrackInfosAsync(videoId, cancellationToken)); private async IAsyncEnumerable GetClosedCaptionsAsync( ClosedCaptionTrackInfo trackInfo, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { - var response = await _controller.GetClosedCaptionTrackResponseAsync(trackInfo.Url, cancellationToken); + var response = await _controller.GetClosedCaptionTrackResponseAsync( + trackInfo.Url, + cancellationToken + ); foreach (var captionData in response.Captions) { @@ -78,8 +86,7 @@ private async IAsyncEnumerable GetClosedCaptionsAsync( // Auto-generated captions may be missing offset or duration // https://github.com/Tyrrrz/YoutubeExplode/discussions/619 - if (captionData.Offset is not { } offset || - captionData.Duration is not { } duration) + if (captionData.Offset is not { } offset || captionData.Duration is not { } duration) { continue; } @@ -95,20 +102,15 @@ private async IAsyncEnumerable GetClosedCaptionsAsync( continue; var partOffset = - partData.Offset ?? - throw new YoutubeExplodeException("Could not extract caption part offset."); + partData.Offset + ?? throw new YoutubeExplodeException("Could not extract caption part offset."); var part = new ClosedCaptionPart(partText, partOffset); parts.Add(part); } - yield return new ClosedCaption( - text, - offset, - duration, - parts - ); + yield return new ClosedCaption(text, offset, duration, parts); } } @@ -117,8 +119,8 @@ private async IAsyncEnumerable GetClosedCaptionsAsync( /// public async ValueTask GetAsync( ClosedCaptionTrackInfo trackInfo, - CancellationToken cancellationToken = default) => - new(await GetClosedCaptionsAsync(trackInfo, cancellationToken)); + CancellationToken cancellationToken = default + ) => new(await GetClosedCaptionsAsync(trackInfo, cancellationToken)); /// /// Writes the closed caption track identified by the specified metadata to the specified writer. @@ -130,13 +132,17 @@ public async ValueTask WriteToAsync( ClosedCaptionTrackInfo trackInfo, TextWriter writer, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { static string FormatTimestamp(TimeSpan value) => - Math.Floor(value.TotalHours).ToString("00", CultureInfo.InvariantCulture) + ':' + - value.Minutes.ToString("00", CultureInfo.InvariantCulture) + ':' + - value.Seconds.ToString("00", CultureInfo.InvariantCulture) + ',' + - value.Milliseconds.ToString("000", CultureInfo.InvariantCulture); + Math.Floor(value.TotalHours).ToString("00", CultureInfo.InvariantCulture) + + ':' + + value.Minutes.ToString("00", CultureInfo.InvariantCulture) + + ':' + + value.Seconds.ToString("00", CultureInfo.InvariantCulture) + + ',' + + value.Milliseconds.ToString("000", CultureInfo.InvariantCulture); // Would be better to use GetClosedCaptionsAsync(...) instead for streaming, // but we need the total number of captions to report progress. @@ -175,9 +181,10 @@ public async ValueTask DownloadAsync( ClosedCaptionTrackInfo trackInfo, string filePath, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { using var writer = File.CreateText(filePath); await WriteToAsync(trackInfo, writer, progress, cancellationToken); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionController.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionController.cs index 9faeff9c..efd3ff8b 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionController.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionController.cs @@ -9,21 +9,20 @@ namespace YoutubeExplode.Videos.ClosedCaptions; internal class ClosedCaptionController : VideoController { - public ClosedCaptionController(HttpClient http) : base(http) - { - } + public ClosedCaptionController(HttpClient http) + : base(http) { } public async ValueTask GetClosedCaptionTrackResponseAsync( string url, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { // Enforce known format - var urlWithFormat = url - .Pipe(s => UrlEx.SetQueryParameter(s, "format", "3")) + var urlWithFormat = url.Pipe(s => UrlEx.SetQueryParameter(s, "format", "3")) .Pipe(s => UrlEx.SetQueryParameter(s, "fmt", "3")); return ClosedCaptionTrackResponse.Parse( await Http.GetStringAsync(urlWithFormat, cancellationToken) ); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionManifest.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionManifest.cs index ddb51274..cd9f3521 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionManifest.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionManifest.cs @@ -17,23 +17,25 @@ public class ClosedCaptionManifest /// /// Initializes an instance of . /// - public ClosedCaptionManifest(IReadOnlyList tracks) => - Tracks = tracks; + public ClosedCaptionManifest(IReadOnlyList tracks) => Tracks = tracks; /// /// Gets the closed caption track in the specified language (identified by ISO-639-1 code or display name). /// Returns null if not found. /// public ClosedCaptionTrackInfo? TryGetByLanguage(string language) => - Tracks.FirstOrDefault(t => - string.Equals(t.Language.Code, language, StringComparison.OrdinalIgnoreCase) || - string.Equals(t.Language.Name, language, StringComparison.OrdinalIgnoreCase) + Tracks.FirstOrDefault( + t => + string.Equals(t.Language.Code, language, StringComparison.OrdinalIgnoreCase) + || string.Equals(t.Language.Name, language, StringComparison.OrdinalIgnoreCase) ); /// /// Gets the closed caption track in the specified language (identified by ISO-639-1 code or display name). /// public ClosedCaptionTrackInfo GetByLanguage(string language) => - TryGetByLanguage(language) ?? - throw new InvalidOperationException($"No closed caption track available for language '{language}'."); -} \ No newline at end of file + TryGetByLanguage(language) + ?? throw new InvalidOperationException( + $"No closed caption track available for language '{language}'." + ); +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionPart.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionPart.cs index c75a15ef..31091e98 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionPart.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionPart.cs @@ -30,4 +30,4 @@ public ClosedCaptionPart(string text, TimeSpan offset) /// [ExcludeFromCodeCoverage] public override string ToString() => Text; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrack.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrack.cs index 9c450542..28319502 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrack.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrack.cs @@ -33,6 +33,6 @@ public ClosedCaptionTrack(IReadOnlyList captions) /// Gets the caption displayed at the specified point in time. /// public ClosedCaption GetByTime(TimeSpan time) => - TryGetByTime(time) ?? - throw new InvalidOperationException($"No closed caption found at {time}."); -} \ No newline at end of file + TryGetByTime(time) + ?? throw new InvalidOperationException($"No closed caption found at {time}."); +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrackInfo.cs b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrackInfo.cs index 29975cd3..7802365a 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrackInfo.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/ClosedCaptionTrackInfo.cs @@ -35,4 +35,4 @@ public ClosedCaptionTrackInfo(string url, Language language, bool isAutoGenerate /// [ExcludeFromCodeCoverage] public override string ToString() => $"CC Track ({Language})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/ClosedCaptions/Language.cs b/YoutubeDownloader/Videos/ClosedCaptions/Language.cs index 51b246bd..0907295b 100644 --- a/YoutubeDownloader/Videos/ClosedCaptions/Language.cs +++ b/YoutubeDownloader/Videos/ClosedCaptions/Language.cs @@ -9,7 +9,7 @@ namespace YoutubeExplode.Videos.ClosedCaptions; public readonly partial struct Language { /// - /// ISO 639-1 code of the language. + /// Two-letter (ISO 639-1) language code, possibly with a regional identifier (e.g. 'en' or 'en-US'). /// public string Code { get; } @@ -52,4 +52,4 @@ public partial struct Language : IEquatable /// Equality check. /// public static bool operator !=(Language left, Language right) => !(left == right); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Engagement.cs b/YoutubeDownloader/Videos/Engagement.cs index f67f3851..75b89446 100644 --- a/YoutubeDownloader/Videos/Engagement.cs +++ b/YoutubeDownloader/Videos/Engagement.cs @@ -31,9 +31,8 @@ public class Engagement /// /// YouTube no longer shows dislikes, so this value is always 5. /// - public double AverageRating => LikeCount + DislikeCount != 0 - ? 1 + 4.0 * LikeCount / (LikeCount + DislikeCount) - : 0; // avoid division by 0 + public double AverageRating => + LikeCount + DislikeCount != 0 ? 1 + 4.0 * LikeCount / (LikeCount + DislikeCount) : 0; // avoid division by 0 /// /// Initializes an instance of . @@ -48,4 +47,4 @@ public Engagement(long viewCount, long likeCount, long dislikeCount) /// [ExcludeFromCodeCoverage] public override string ToString() => $"Rating: {AverageRating:N1}"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/IVideo.cs b/YoutubeDownloader/Videos/IVideo.cs index d3a25643..77146b89 100644 --- a/YoutubeDownloader/Videos/IVideo.cs +++ b/YoutubeDownloader/Videos/IVideo.cs @@ -41,4 +41,4 @@ public interface IVideo /// Video thumbnails. /// IReadOnlyList Thumbnails { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/AudioOnlyStreamInfo.cs b/YoutubeDownloader/Videos/Streams/AudioOnlyStreamInfo.cs index a86046f6..1346d2a1 100644 --- a/YoutubeDownloader/Videos/Streams/AudioOnlyStreamInfo.cs +++ b/YoutubeDownloader/Videos/Streams/AudioOnlyStreamInfo.cs @@ -30,7 +30,8 @@ public AudioOnlyStreamInfo( Container container, FileSize size, Bitrate bitrate, - string audioCodec) + string audioCodec + ) { Url = url; Container = container; @@ -42,4 +43,4 @@ public AudioOnlyStreamInfo( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Audio-only ({Container})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/Bitrate.cs b/YoutubeDownloader/Videos/Streams/Bitrate.cs index f187e4c2..50f6a6f7 100644 --- a/YoutubeDownloader/Videos/Streams/Bitrate.cs +++ b/YoutubeDownloader/Videos/Streams/Bitrate.cs @@ -61,7 +61,8 @@ private double GetLargestWholeNumberValue() } /// - public override string ToString() => $"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}"; + public override string ToString() => + $"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}"; } public partial struct Bitrate : IComparable, IEquatable @@ -97,4 +98,4 @@ public partial struct Bitrate : IComparable, IEquatable /// Comparison. /// public static bool operator <(Bitrate left, Bitrate right) => left.CompareTo(right) < 0; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/Container.cs b/YoutubeDownloader/Videos/Streams/Container.cs index f5cc63f7..52a8e02a 100644 --- a/YoutubeDownloader/Videos/Streams/Container.cs +++ b/YoutubeDownloader/Videos/Streams/Container.cs @@ -22,13 +22,13 @@ public readonly partial struct Container /// If the container IS NOT audio-only, it MAY contain video streams, but is not required to. /// public bool IsAudioOnly => - string.Equals(Name, "mp3", StringComparison.OrdinalIgnoreCase) || - string.Equals(Name, "m4a", StringComparison.OrdinalIgnoreCase) || - string.Equals(Name, "wav", StringComparison.OrdinalIgnoreCase) || - string.Equals(Name, "wma", StringComparison.OrdinalIgnoreCase) || - string.Equals(Name, "ogg", StringComparison.OrdinalIgnoreCase) || - string.Equals(Name, "aac", StringComparison.OrdinalIgnoreCase) || - string.Equals(Name, "opus", StringComparison.OrdinalIgnoreCase); + string.Equals(Name, "mp3", StringComparison.OrdinalIgnoreCase) + || string.Equals(Name, "m4a", StringComparison.OrdinalIgnoreCase) + || string.Equals(Name, "wav", StringComparison.OrdinalIgnoreCase) + || string.Equals(Name, "wma", StringComparison.OrdinalIgnoreCase) + || string.Equals(Name, "ogg", StringComparison.OrdinalIgnoreCase) + || string.Equals(Name, "aac", StringComparison.OrdinalIgnoreCase) + || string.Equals(Name, "opus", StringComparison.OrdinalIgnoreCase); /// /// Initializes an instance of . @@ -44,6 +44,9 @@ public partial struct Container /// /// MPEG-2 Audio Layer III (mp3). /// + /// + /// YouTube does not natively provide streams in this container. + /// public static Container Mp3 { get; } = new("mp3"); /// @@ -65,7 +68,8 @@ public partial struct Container public partial struct Container : IEquatable { /// - public bool Equals(Container other) => StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name); + public bool Equals(Container other) => + StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name); /// public override bool Equals(object? obj) => obj is Container other && Equals(other); @@ -82,4 +86,4 @@ public partial struct Container : IEquatable /// Equality check. /// public static bool operator !=(Container left, Container right) => !(left == right); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/FileSize.cs b/YoutubeDownloader/Videos/Streams/FileSize.cs index a163e018..d0086284 100644 --- a/YoutubeDownloader/Videos/Streams/FileSize.cs +++ b/YoutubeDownloader/Videos/Streams/FileSize.cs @@ -62,7 +62,8 @@ private double GetLargestWholeNumberValue() } /// - public override string ToString() => $"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}"; + public override string ToString() => + $"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}"; } public partial struct FileSize : IComparable, IEquatable @@ -98,4 +99,4 @@ public partial struct FileSize : IComparable, IEquatable /// Comparison. /// public static bool operator <(FileSize left, FileSize right) => left.CompareTo(right) < 0; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/IAudioStreamInfo.cs b/YoutubeDownloader/Videos/Streams/IAudioStreamInfo.cs index 6097c349..9de2853a 100644 --- a/YoutubeDownloader/Videos/Streams/IAudioStreamInfo.cs +++ b/YoutubeDownloader/Videos/Streams/IAudioStreamInfo.cs @@ -9,4 +9,4 @@ public interface IAudioStreamInfo : IStreamInfo /// Audio codec. /// string AudioCodec { get; } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/IStreamInfo.cs b/YoutubeDownloader/Videos/Streams/IStreamInfo.cs index 669c9812..7984c1b0 100644 --- a/YoutubeDownloader/Videos/Streams/IStreamInfo.cs +++ b/YoutubeDownloader/Videos/Streams/IStreamInfo.cs @@ -14,9 +14,11 @@ public interface IStreamInfo /// Stream URL. /// /// - /// While this URL can be used to access the underlying stream, you need a series of carefully crafted - /// HTTP requests to properly resolve it. It's recommended to use - /// or instead, as they do all the heavy lifting for you. + /// While this URL can be used to access the underlying stream, you need a series + /// of carefully crafted HTTP requests in order to do so. + /// It's highly recommended to use + /// or + /// instead, as they will all the heavy lifting for you. /// string Url { get; } @@ -41,23 +43,25 @@ public interface IStreamInfo /// public static class StreamInfoExtensions { - internal static bool IsThrottled(this IStreamInfo streamInfo) => !string.Equals( - UrlEx.TryGetQueryParameterValue(streamInfo.Url, "ratebypass"), - "yes", - StringComparison.OrdinalIgnoreCase - ); + internal static bool IsThrottled(this IStreamInfo streamInfo) => + !string.Equals( + UrlEx.TryGetQueryParameterValue(streamInfo.Url, "ratebypass"), + "yes", + StringComparison.OrdinalIgnoreCase + ); /// /// Gets the stream with the highest bitrate. /// Returns null if the sequence is empty. /// - public static IStreamInfo? TryGetWithHighestBitrate(this IEnumerable streamInfos) => - streamInfos.MaxBy(s => s.Bitrate); + public static IStreamInfo? TryGetWithHighestBitrate( + this IEnumerable streamInfos + ) => streamInfos.MaxBy(s => s.Bitrate); /// /// Gets the stream with the highest bitrate. /// public static IStreamInfo GetWithHighestBitrate(this IEnumerable streamInfos) => - streamInfos.TryGetWithHighestBitrate() ?? - throw new InvalidOperationException("Input stream collection is empty."); -} \ No newline at end of file + streamInfos.TryGetWithHighestBitrate() + ?? throw new InvalidOperationException("Input stream collection is empty."); +} diff --git a/YoutubeDownloader/Videos/Streams/IVideoStreamInfo.cs b/YoutubeDownloader/Videos/Streams/IVideoStreamInfo.cs index 03eb8357..c47e69d8 100644 --- a/YoutubeDownloader/Videos/Streams/IVideoStreamInfo.cs +++ b/YoutubeDownloader/Videos/Streams/IVideoStreamInfo.cs @@ -35,13 +35,16 @@ public static class VideoStreamInfoExtensions /// Gets the video stream with the highest video quality (including framerate). /// Returns null if the sequence is empty. /// - public static IVideoStreamInfo? TryGetWithHighestVideoQuality(this IEnumerable streamInfos) => - streamInfos.MaxBy(s => s.VideoQuality); + public static IVideoStreamInfo? TryGetWithHighestVideoQuality( + this IEnumerable streamInfos + ) => streamInfos.MaxBy(s => s.VideoQuality); /// /// Gets the video stream with the highest video quality (including framerate). /// - public static IVideoStreamInfo GetWithHighestVideoQuality(this IEnumerable streamInfos) => - streamInfos.TryGetWithHighestVideoQuality() ?? - throw new InvalidOperationException("Input stream collection is empty."); -} \ No newline at end of file + public static IVideoStreamInfo GetWithHighestVideoQuality( + this IEnumerable streamInfos + ) => + streamInfos.TryGetWithHighestVideoQuality() + ?? throw new InvalidOperationException("Input stream collection is empty."); +} diff --git a/YoutubeDownloader/Videos/Streams/MediaStream.cs b/YoutubeDownloader/Videos/Streams/MediaStream.cs index e86f87db..33867b86 100644 --- a/YoutubeDownloader/Videos/Streams/MediaStream.cs +++ b/YoutubeDownloader/Videos/Streams/MediaStream.cs @@ -42,9 +42,7 @@ public MediaStream(HttpClient http, IStreamInfo streamInfo) // we want to download the stream as fast as possible. // To solve this, we divide the logical stream up into multiple segments and download // them all separately. - _segmentLength = streamInfo.IsThrottled() - ? 9_898_989 - : streamInfo.Size.Bytes; + _segmentLength = streamInfo.IsThrottled() ? 9_898_989 : streamInfo.Size.Bytes; } private void ResetSegment() @@ -53,7 +51,9 @@ private void ResetSegment() _segmentStream = null; } - private async ValueTask ResolveSegmentAsync(CancellationToken cancellationToken = default) + private async ValueTask ResolveSegmentAsync( + CancellationToken cancellationToken = default + ) { if (_segmentStream is not null) return _segmentStream; @@ -74,9 +74,10 @@ private async ValueTask ReadSegmentAsync( byte[] buffer, int offset, int count, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - for (var retriesRemaining = 5;; retriesRemaining--) + for (var retriesRemaining = 5; ; retriesRemaining--) { try { @@ -95,7 +96,8 @@ public override async Task ReadAsync( byte[] buffer, int offset, int count, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { while (true) { @@ -130,21 +132,20 @@ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); [ExcludeFromCodeCoverage] - public override void SetLength(long value) => - throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); [ExcludeFromCodeCoverage] - public override long Seek(long offset, SeekOrigin origin) => Position = origin switch - { - SeekOrigin.Begin => offset, - SeekOrigin.Current => Position + offset, - SeekOrigin.End => Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) - }; + public override long Seek(long offset, SeekOrigin origin) => + Position = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => Position + offset, + SeekOrigin.End => Length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; [ExcludeFromCodeCoverage] - public override void Flush() => - throw new NotSupportedException(); + public override void Flush() => throw new NotSupportedException(); protected override void Dispose(bool disposing) { @@ -153,4 +154,4 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/MuxedStreamInfo.cs b/YoutubeDownloader/Videos/Streams/MuxedStreamInfo.cs index f4768f58..a07358c0 100644 --- a/YoutubeDownloader/Videos/Streams/MuxedStreamInfo.cs +++ b/YoutubeDownloader/Videos/Streams/MuxedStreamInfo.cs @@ -43,7 +43,8 @@ public MuxedStreamInfo( string audioCodec, string videoCodec, VideoQuality videoQuality, - Resolution resolution) + Resolution resolution + ) { Url = url; Container = container; @@ -58,4 +59,4 @@ public MuxedStreamInfo( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Muxed ({VideoQuality} | {Container})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/StreamClient.cs b/YoutubeDownloader/Videos/Streams/StreamClient.cs index 49089824..e2e19f5b 100644 --- a/YoutubeDownloader/Videos/Streams/StreamClient.cs +++ b/YoutubeDownloader/Videos/Streams/StreamClient.cs @@ -36,7 +36,9 @@ public StreamClient(HttpClient http) _controller = new StreamController(http); } - private async ValueTask ResolveCipherManifestAsync(CancellationToken cancellationToken) + private async ValueTask ResolveCipherManifestAsync( + CancellationToken cancellationToken + ) { if (_cipherManifest is not null) return _cipherManifest; @@ -44,23 +46,24 @@ private async ValueTask ResolveCipherManifestAsync(CancellationT var playerSource = await _controller.GetPlayerSourceAsync(cancellationToken); return _cipherManifest = - playerSource.CipherManifest ?? - throw new YoutubeExplodeException("Could not get cipher manifest."); + playerSource.CipherManifest + ?? throw new YoutubeExplodeException("Could not get cipher manifest."); } private async IAsyncEnumerable GetStreamInfosAsync( IEnumerable streamDatas, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { foreach (var streamData in streamDatas) { var itag = - streamData.Itag ?? - throw new YoutubeExplodeException("Could not extract stream itag."); + streamData.Itag + ?? throw new YoutubeExplodeException("Could not extract stream itag."); var url = - streamData.Url ?? - throw new YoutubeExplodeException("Could not extract stream URL."); + streamData.Url + ?? throw new YoutubeExplodeException("Could not extract stream URL."); // Handle cipher-protected streams if (!string.IsNullOrWhiteSpace(streamData.Signature)) @@ -75,21 +78,21 @@ private async IAsyncEnumerable GetStreamInfosAsync( } var contentLength = - streamData.ContentLength ?? - await _http.TryGetContentLengthAsync(url, false, cancellationToken) ?? - 0; + streamData.ContentLength + ?? await _http.TryGetContentLengthAsync(url, false, cancellationToken) + ?? 0; // Stream cannot be accessed if (contentLength <= 0) continue; var container = - streamData.Container?.Pipe(s => new Container(s)) ?? - throw new YoutubeExplodeException("Could not extract stream container."); + streamData.Container?.Pipe(s => new Container(s)) + ?? throw new YoutubeExplodeException("Could not extract stream container."); var bitrate = - streamData.Bitrate?.Pipe(s => new Bitrate(s)) ?? - throw new YoutubeExplodeException("Could not extract stream bitrate."); + streamData.Bitrate?.Pipe(s => new Bitrate(s)) + ?? throw new YoutubeExplodeException("Could not extract stream bitrate."); // Muxed or video-only stream if (!string.IsNullOrWhiteSpace(streamData.VideoCodec)) @@ -101,8 +104,7 @@ await _http.TryGetContentLengthAsync(url, false, cancellationToken) ?? : VideoQuality.FromItag(itag, framerate); var videoResolution = - streamData.VideoWidth is not null && - streamData.VideoHeight is not null + streamData.VideoWidth is not null && streamData.VideoHeight is not null ? new Resolution(streamData.VideoWidth.Value, streamData.VideoHeight.Value) : videoQuality.GetDefaultVideoResolution(); @@ -160,7 +162,8 @@ streamData.VideoHeight is not null private async IAsyncEnumerable GetStreamInfosAsync( VideoId videoId, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) { var playerResponse = await _controller.GetPlayerResponseAsync(videoId, cancellationToken); @@ -189,13 +192,15 @@ private async IAsyncEnumerable GetStreamInfosAsync( if (!playerResponse.IsPlayable) { throw new VideoUnplayableException( - $"Video '{videoId}' is unplayable. " + - $"Reason: '{playerResponse.PlayabilityError}'." + $"Video '{videoId}' is unplayable. " + + $"Reason: '{playerResponse.PlayabilityError}'." ); } // Extract streams from the player response - await foreach (var streamInfo in GetStreamInfosAsync(playerResponse.Streams, cancellationToken)) + await foreach ( + var streamInfo in GetStreamInfosAsync(playerResponse.Streams, cancellationToken) + ) yield return streamInfo; // Extract streams from the DASH manifest @@ -212,13 +217,13 @@ private async IAsyncEnumerable GetStreamInfosAsync( } // Some DASH manifest URLs return 404 for whatever reason // https://github.com/Tyrrrz/YoutubeExplode/issues/728 - catch (HttpRequestException) - { - } + catch (HttpRequestException) { } if (dashManifest is not null) { - await foreach (var streamInfo in GetStreamInfosAsync(dashManifest.Streams, cancellationToken)) + await foreach ( + var streamInfo in GetStreamInfosAsync(dashManifest.Streams, cancellationToken) + ) yield return streamInfo; } } @@ -229,9 +234,10 @@ private async IAsyncEnumerable GetStreamInfosAsync( /// public async ValueTask GetManifestAsync( VideoId videoId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - for (var retriesRemaining = 5;; retriesRemaining--) + for (var retriesRemaining = 5; ; retriesRemaining--) { var streamInfos = await GetStreamInfosAsync(videoId, cancellationToken); @@ -260,22 +266,23 @@ public async ValueTask GetManifestAsync( /// public async ValueTask GetHttpLiveStreamUrlAsync( VideoId videoId, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var playerResponse = await _controller.GetPlayerResponseAsync(videoId, cancellationToken); if (!playerResponse.IsPlayable) { throw new VideoUnplayableException( - $"Video '{videoId}' is unplayable. " + - $"Reason: '{playerResponse.PlayabilityError}'." + $"Video '{videoId}' is unplayable. " + + $"Reason: '{playerResponse.PlayabilityError}'." ); } if (string.IsNullOrWhiteSpace(playerResponse.HlsManifestUrl)) { throw new YoutubeExplodeException( - "Could not extract HTTP Live Stream manifest URL. " + - $"Video '{videoId}' is likely not a live stream." + "Could not extract HTTP Live Stream manifest URL. " + + $"Video '{videoId}' is likely not a live stream." ); } @@ -287,7 +294,8 @@ public async ValueTask GetHttpLiveStreamUrlAsync( /// public async ValueTask GetAsync( IStreamInfo streamInfo, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { var stream = new MediaStream(_http, streamInfo); await stream.InitializeAsync(cancellationToken); @@ -302,7 +310,8 @@ public async ValueTask CopyToAsync( IStreamInfo streamInfo, Stream destination, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { using var input = await GetAsync(streamInfo, cancellationToken); await input.CopyToAsync(destination, progress, cancellationToken); @@ -315,9 +324,10 @@ public async ValueTask DownloadAsync( IStreamInfo streamInfo, string filePath, IProgress? progress = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { using var destination = File.Create(filePath); await CopyToAsync(streamInfo, destination, progress, cancellationToken); } -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/StreamController.cs b/YoutubeDownloader/Videos/Streams/StreamController.cs index ee9e3f9f..454f28d7 100644 --- a/YoutubeDownloader/Videos/Streams/StreamController.cs +++ b/YoutubeDownloader/Videos/Streams/StreamController.cs @@ -9,14 +9,17 @@ namespace YoutubeExplode.Videos.Streams; internal class StreamController : VideoController { - public StreamController(HttpClient http) : base(http) - { - } + public StreamController(HttpClient http) + : base(http) { } public async ValueTask GetPlayerSourceAsync( - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - var iframe = await Http.GetStringAsync("https://www.youtube.com/iframe_api", cancellationToken); + var iframe = await Http.GetStringAsync( + "https://www.youtube.com/iframe_api", + cancellationToken + ); var version = Regex.Match(iframe, @"player\\?/([0-9a-fA-F]{8})\\?/").Groups[1].Value; if (string.IsNullOrWhiteSpace(version)) @@ -32,8 +35,6 @@ await Http.GetStringAsync( public async ValueTask GetDashManifestAsync( string url, - CancellationToken cancellationToken = default) => - DashManifest.Parse( - await Http.GetStringAsync(url, cancellationToken) - ); -} \ No newline at end of file + CancellationToken cancellationToken = default + ) => DashManifest.Parse(await Http.GetStringAsync(url, cancellationToken)); +} diff --git a/YoutubeDownloader/Videos/Streams/StreamManifest.cs b/YoutubeDownloader/Videos/Streams/StreamManifest.cs index de184fda..7cb12d6e 100644 --- a/YoutubeDownloader/Videos/Streams/StreamManifest.cs +++ b/YoutubeDownloader/Videos/Streams/StreamManifest.cs @@ -24,20 +24,17 @@ public StreamManifest(IReadOnlyList streams) /// /// Gets streams that contain audio (i.e. muxed and audio-only streams). /// - public IEnumerable GetAudioStreams() => - Streams.OfType(); + public IEnumerable GetAudioStreams() => Streams.OfType(); /// /// Gets streams that contain video (i.e. muxed and video-only streams). /// - public IEnumerable GetVideoStreams() => - Streams.OfType(); + public IEnumerable GetVideoStreams() => Streams.OfType(); /// /// Gets muxed streams (i.e. streams containing both audio and video). /// - public IEnumerable GetMuxedStreams() => - Streams.OfType(); + public IEnumerable GetMuxedStreams() => Streams.OfType(); /// /// Gets audio-only streams. @@ -50,4 +47,4 @@ public IEnumerable GetAudioOnlyStreams() => /// public IEnumerable GetVideoOnlyStreams() => GetVideoStreams().OfType(); -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/VideoOnlyStreamInfo.cs b/YoutubeDownloader/Videos/Streams/VideoOnlyStreamInfo.cs index b2e66819..487d793b 100644 --- a/YoutubeDownloader/Videos/Streams/VideoOnlyStreamInfo.cs +++ b/YoutubeDownloader/Videos/Streams/VideoOnlyStreamInfo.cs @@ -39,7 +39,8 @@ public VideoOnlyStreamInfo( Bitrate bitrate, string videoCodec, VideoQuality videoQuality, - Resolution videoResolution) + Resolution videoResolution + ) { Url = url; Container = container; @@ -53,4 +54,4 @@ public VideoOnlyStreamInfo( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Video-only ({VideoQuality} | {Container})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/Streams/VideoQuality.cs b/YoutubeDownloader/Videos/Streams/VideoQuality.cs index f3a78417..fcf3bbd4 100644 --- a/YoutubeDownloader/Videos/Streams/VideoQuality.cs +++ b/YoutubeDownloader/Videos/Streams/VideoQuality.cs @@ -45,25 +45,24 @@ public VideoQuality(string label, int maxHeight, int framerate) /// Initializes an instance of . /// public VideoQuality(int maxHeight, int framerate) - : this(FormatLabel(maxHeight, framerate), maxHeight, framerate) - { - } + : this(FormatLabel(maxHeight, framerate), maxHeight, framerate) { } - internal Resolution GetDefaultVideoResolution() => MaxHeight switch - { - 144 => new Resolution(256, 144), - 240 => new Resolution(426, 240), - 360 => new Resolution(640, 360), - 480 => new Resolution(854, 480), - 720 => new Resolution(1280, 720), - 1080 => new Resolution(1920, 1080), - 1440 => new Resolution(2560, 1440), - 2160 => new Resolution(3840, 2160), - 2880 => new Resolution(5120, 2880), - 3072 => new Resolution(4096, 3072), - 4320 => new Resolution(7680, 4320), - _ => new Resolution(16 * MaxHeight / 9, MaxHeight) - }; + internal Resolution GetDefaultVideoResolution() => + MaxHeight switch + { + 144 => new Resolution(256, 144), + 240 => new Resolution(426, 240), + 360 => new Resolution(640, 360), + 480 => new Resolution(854, 480), + 720 => new Resolution(1280, 720), + 1080 => new Resolution(1920, 1080), + 1440 => new Resolution(2560, 1440), + 2160 => new Resolution(3840, 2160), + 2880 => new Resolution(5120, 2880), + 3072 => new Resolution(4096, 3072), + 4320 => new Resolution(7680, 4320), + _ => new Resolution(16 * MaxHeight / 9, MaxHeight) + }; /// public override string ToString() => Label; @@ -78,7 +77,7 @@ private static string FormatLabel(int maxHeight, int framerate) return $"{maxHeight}p"; // YouTube rounds framerate to the next nearest decimal - var framerateRounded = (int) Math.Ceiling(framerate / 10.0) * 10; + var framerateRounded = (int)Math.Ceiling(framerate / 10.0) * 10; return $"{maxHeight}p{framerateRounded}"; } @@ -96,11 +95,7 @@ internal static VideoQuality FromLabel(string label, int framerateFallback) var maxHeight = match.Groups[1].Value.ParseInt(); var framerate = match.Groups[2].Value.NullIfWhiteSpace()?.ParseIntOrNull(); - return new VideoQuality( - label, - maxHeight, - framerate ?? framerateFallback - ); + return new VideoQuality(label, maxHeight, framerate ?? framerateFallback); } internal static VideoQuality FromItag(int itag, int framerate) @@ -145,29 +140,31 @@ internal static VideoQuality FromItag(int itag, int framerate) 136 => 720, 137 => 1080, 138 => 4320, + 142 => 240, + 143 => 360, + 144 => 480, + 145 => 720, + 146 => 1080, 160 => 144, + 161 => 144, + 167 => 360, + 168 => 480, + 169 => 720, + 170 => 1080, 212 => 480, 213 => 480, 214 => 720, 215 => 720, 216 => 1080, 217 => 1080, - 264 => 1440, - 266 => 2160, - 298 => 720, - 299 => 1080, - 399 => 1080, - 398 => 720, - 397 => 480, - 396 => 360, - 395 => 240, - 394 => 144, - 167 => 360, - 168 => 480, - 169 => 720, - 170 => 1080, 218 => 480, 219 => 480, + 222 => 480, + 223 => 480, + 224 => 720, + 225 => 720, + 226 => 1080, + 227 => 1080, 242 => 240, 243 => 360, 244 => 480, @@ -175,9 +172,13 @@ internal static VideoQuality FromItag(int itag, int framerate) 246 => 480, 247 => 720, 248 => 1080, + 264 => 1440, + 266 => 2160, 271 => 1440, 272 => 2160, 278 => 144, + 298 => 720, + 299 => 1080, 302 => 720, 303 => 1080, 308 => 1440, @@ -191,6 +192,12 @@ internal static VideoQuality FromItag(int itag, int framerate) 335 => 1080, 336 => 1440, 337 => 2160, + 399 => 1080, + 398 => 720, + 397 => 480, + 396 => 360, + 395 => 240, + 394 => 144, _ => throw new ArgumentException($"Unrecognized itag '{itag}'.", nameof(itag)) }; @@ -217,19 +224,16 @@ public int CompareTo(VideoQuality other) /// public bool Equals(VideoQuality other) => - StringComparer.OrdinalIgnoreCase.Equals(Label, other.Label) && - MaxHeight == other.MaxHeight && - Framerate == other.Framerate; + StringComparer.OrdinalIgnoreCase.Equals(Label, other.Label) + && MaxHeight == other.MaxHeight + && Framerate == other.Framerate; /// public override bool Equals(object? obj) => obj is VideoQuality other && Equals(other); /// - public override int GetHashCode() => HashCode.Combine( - StringComparer.OrdinalIgnoreCase.GetHashCode(Label), - MaxHeight, - Framerate - ); + public override int GetHashCode() => + HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(Label), MaxHeight, Framerate); /// /// Equality check. @@ -244,10 +248,12 @@ public override int GetHashCode() => HashCode.Combine( /// /// Comparison. /// - public static bool operator >(VideoQuality left, VideoQuality right) => left.CompareTo(right) > 0; + public static bool operator >(VideoQuality left, VideoQuality right) => + left.CompareTo(right) > 0; /// /// Comparison. /// - public static bool operator <(VideoQuality left, VideoQuality right) => left.CompareTo(right) < 0; -} \ No newline at end of file + public static bool operator <(VideoQuality left, VideoQuality right) => + left.CompareTo(right) < 0; +} diff --git a/YoutubeDownloader/Videos/Video.cs b/YoutubeDownloader/Videos/Video.cs index 3ba12457..b2661f31 100644 --- a/YoutubeDownloader/Videos/Video.cs +++ b/YoutubeDownloader/Videos/Video.cs @@ -60,7 +60,8 @@ public Video( TimeSpan? duration, IReadOnlyList thumbnails, IReadOnlyList keywords, - Engagement engagement) + Engagement engagement + ) { Id = id; Title = title; @@ -76,4 +77,4 @@ public Video( /// [ExcludeFromCodeCoverage] public override string ToString() => $"Video ({Title})"; -} \ No newline at end of file +} diff --git a/YoutubeDownloader/Videos/VideoClient.cs b/YoutubeDownloader/Videos/VideoClient.cs index 0c846987..69a73a0b 100644 --- a/YoutubeDownloader/Videos/VideoClient.cs +++ b/YoutubeDownloader/Videos/VideoClient.cs @@ -42,51 +42,55 @@ public VideoClient(HttpClient http) /// public async ValueTask