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
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