From ca99f2bc46cbeceee145ee80d923e179d0116459 Mon Sep 17 00:00:00 2001 From: Mikhail Kaufmann Date: Sat, 26 Oct 2024 11:43:05 +0300 Subject: [PATCH 1/3] Update YoutubeExplode to the latest version (6.4.3+) --- .../YoutubeExplode/Bridge/DashManifest.cs | 15 ++- .../YoutubeExplode/Bridge/IPlaylistData.cs | 2 + .../Bridge/PlaylistBrowseResponse.cs | 40 +++++- .../Bridge/PlaylistNextResponse.cs | 22 +++- .../YoutubeExplode/Bridge/SearchResponse.cs | 7 ++ .../YoutubeExplode/Bridge/VideoWatchPage.cs | 67 +++++----- .../YoutubeExplode/Channels/ChannelClient.cs | 2 +- .../YoutubeExplode/Common/Thumbnail.cs | 2 +- .../YoutubeExplode/Playlists/Playlist.cs | 9 ++ .../Playlists/PlaylistClient.cs | 4 +- .../Playlists/PlaylistController.cs | 81 ++++++------ .../YoutubeExplode/Search/SearchController.cs | 52 ++++---- .../YoutubeExplode/Search/SearchFilter.cs | 2 +- .../Utils/Extensions/HttpExtensions.cs | 2 +- .../Utils/Extensions/StringExtensions.cs | 2 +- .../Utils/Extensions/XElementExtensions.cs | 6 +- YoutubeExplode/YoutubeExplode/Utils/Json.cs | 33 +++++ .../ClosedCaptions/ClosedCaptionClient.cs | 8 +- .../ClosedCaptions/ClosedCaptionManifest.cs | 7 +- .../Videos/Streams/MediaStream.cs | 2 +- .../Videos/Streams/VideoQuality.cs | 4 +- .../YoutubeExplode/Videos/VideoController.cs | 118 ++++++++++-------- .../YoutubeExplode/YoutubeClient.cs | 40 +++--- .../YoutubeExplode/YoutubeExplode.csproj | 10 +- .../YoutubeExplode/YoutubeHttpHandler.cs | 26 ++-- 25 files changed, 338 insertions(+), 225 deletions(-) diff --git a/YoutubeExplode/YoutubeExplode/Bridge/DashManifest.cs b/YoutubeExplode/YoutubeExplode/Bridge/DashManifest.cs index ee63b23cd..6f24426f6 100644 --- a/YoutubeExplode/YoutubeExplode/Bridge/DashManifest.cs +++ b/YoutubeExplode/YoutubeExplode/Bridge/DashManifest.cs @@ -16,13 +16,11 @@ internal partial class DashManifest(XElement content) content .Descendants("Representation") .Where(x => x.Attribute("id")?.Value.All(char.IsDigit) == true) - .Where( - x => - x.Descendants("Initialization") - .FirstOrDefault() - ?.Attribute("sourceURL") - ?.Value - .Contains("sq/") != true + .Where(x => + x.Descendants("Initialization") + .FirstOrDefault() + ?.Attribute("sourceURL") + ?.Value.Contains("sq/") != true ) // Skip streams without codecs .Where(x => !string.IsNullOrWhiteSpace(x.Attribute("codecs")?.Value)) @@ -58,7 +56,8 @@ public class StreamData(XElement content) : IStreamData [Lazy] public string? Container => - Url?.Pipe(s => Regex.Match(s, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value) + Url + ?.Pipe(s => Regex.Match(s, @"mime[/=]\w*%2F([\w\d]*)").Groups[1].Value) .Pipe(WebUtility.UrlDecode); [Lazy] diff --git a/YoutubeExplode/YoutubeExplode/Bridge/IPlaylistData.cs b/YoutubeExplode/YoutubeExplode/Bridge/IPlaylistData.cs index e5d370ce3..46e30ae42 100644 --- a/YoutubeExplode/YoutubeExplode/Bridge/IPlaylistData.cs +++ b/YoutubeExplode/YoutubeExplode/Bridge/IPlaylistData.cs @@ -12,5 +12,7 @@ internal interface IPlaylistData string? Description { get; } + int? Count { get; } + IReadOnlyList Thumbnails { get; } } diff --git a/YoutubeExplode/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs b/YoutubeExplode/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs index 074c10e0a..c9894a6a7 100644 --- a/YoutubeExplode/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs +++ b/YoutubeExplode/YoutubeExplode/Bridge/PlaylistBrowseResponse.cs @@ -46,7 +46,14 @@ internal partial class PlaylistBrowseResponse(JsonElement content) : IPlaylistDa ?.EnumerateArrayOrNull() ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString(); + .ConcatToString() + ?? SidebarPrimary + ?.GetPropertyOrNull("titleForm") + ?.GetPropertyOrNull("inlineFormRenderer") + ?.GetPropertyOrNull("formField") + ?.GetPropertyOrNull("textInputFormFieldRenderer") + ?.GetPropertyOrNull("value") + ?.GetStringOrNull(); [Lazy] private JsonElement? AuthorDetails => @@ -86,7 +93,36 @@ internal partial class PlaylistBrowseResponse(JsonElement content) : IPlaylistDa ?.EnumerateArrayOrNull() ?.Select(j => j.GetPropertyOrNull("text")?.GetStringOrNull()) .WhereNotNull() - .ConcatToString(); + .ConcatToString() + ?? SidebarPrimary + ?.GetPropertyOrNull("descriptionForm") + ?.GetPropertyOrNull("inlineFormRenderer") + ?.GetPropertyOrNull("formField") + ?.GetPropertyOrNull("textInputFormFieldRenderer") + ?.GetPropertyOrNull("value") + ?.GetStringOrNull(); + + [Lazy] + public int? Count => + SidebarPrimary + ?.GetPropertyOrNull("stats") + ?.EnumerateArrayOrNull() + ?.FirstOrNull() + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.FirstOrNull() + ?.GetPropertyOrNull("text") + ?.GetStringOrNull() + ?.ParseIntOrNull() + ?? SidebarPrimary + ?.GetPropertyOrNull("stats") + ?.EnumerateArrayOrNull() + ?.FirstOrNull() + ?.GetPropertyOrNull("simpleText") + ?.GetStringOrNull() + ?.Split(' ') + ?.FirstOrDefault() + ?.ParseIntOrNull(); [Lazy] public IReadOnlyList Thumbnails => diff --git a/YoutubeExplode/YoutubeExplode/Bridge/PlaylistNextResponse.cs b/YoutubeExplode/YoutubeExplode/Bridge/PlaylistNextResponse.cs index 0334f6248..2e65aff89 100644 --- a/YoutubeExplode/YoutubeExplode/Bridge/PlaylistNextResponse.cs +++ b/YoutubeExplode/YoutubeExplode/Bridge/PlaylistNextResponse.cs @@ -36,8 +36,26 @@ internal partial class PlaylistNextResponse(JsonElement content) : IPlaylistData public string? Description => null; [Lazy] - public IReadOnlyList Thumbnails => - Videos.FirstOrDefault()?.Thumbnails ?? Array.Empty(); + public int? Count => + ContentRoot + ?.GetPropertyOrNull("totalVideosText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.FirstOrNull() + ?.GetPropertyOrNull("text") + ?.GetStringOrNull() + ?.ParseIntOrNull() + ?? ContentRoot + ?.GetPropertyOrNull("videoCountText") + ?.GetPropertyOrNull("runs") + ?.EnumerateArrayOrNull() + ?.ElementAtOrNull(2) + ?.GetPropertyOrNull("text") + ?.GetStringOrNull() + ?.ParseIntOrNull(); + + [Lazy] + public IReadOnlyList Thumbnails => Videos.FirstOrDefault()?.Thumbnails ?? []; [Lazy] public IReadOnlyList Videos => diff --git a/YoutubeExplode/YoutubeExplode/Bridge/SearchResponse.cs b/YoutubeExplode/YoutubeExplode/Bridge/SearchResponse.cs index fb8a47ae8..0aa323261 100644 --- a/YoutubeExplode/YoutubeExplode/Bridge/SearchResponse.cs +++ b/YoutubeExplode/YoutubeExplode/Bridge/SearchResponse.cs @@ -85,6 +85,13 @@ internal class VideoData(JsonElement content) [Lazy] public string? ChannelId => AuthorDetails + ?.GetPropertyOrNull("navigationEndpoint") + ?.GetPropertyOrNull("browseEndpoint") + ?.GetPropertyOrNull("browseId") + ?.GetStringOrNull() + ?? content + .GetPropertyOrNull("channelThumbnailSupportedRenderers") + ?.GetPropertyOrNull("channelThumbnailWithLinkRenderer") ?.GetPropertyOrNull("navigationEndpoint") ?.GetPropertyOrNull("browseEndpoint") ?.GetPropertyOrNull("browseId") diff --git a/YoutubeExplode/YoutubeExplode/Bridge/VideoWatchPage.cs b/YoutubeExplode/YoutubeExplode/Bridge/VideoWatchPage.cs index b53e9193c..c634a9bec 100644 --- a/YoutubeExplode/YoutubeExplode/Bridge/VideoWatchPage.cs +++ b/YoutubeExplode/YoutubeExplode/Bridge/VideoWatchPage.cs @@ -31,33 +31,31 @@ internal partial class VideoWatchPage(IHtmlDocument content) [Lazy] public long? LikeCount => content - .Source.Text.Pipe( - s => - Regex - .Match( - s, - """ - "\s*:\s*"([\d,\.]+) likes" - """ - ) - .Groups[1] - .Value + .Source.Text.Pipe(s => + Regex + .Match( + s, + """ + "\s*:\s*"([\d,\.]+) likes" + """ + ) + .Groups[1] + .Value ) .NullIfWhiteSpace() ?.StripNonDigit() .ParseLongOrNull() ?? content - .Source.Text.Pipe( - s => - Regex - .Match( - s, - """ - along with ([\d,\.]+) other people" - """ - ) - .Groups[1] - .Value + .Source.Text.Pipe(s => + Regex + .Match( + s, + """ + along with ([\d,\.]+) other people" + """ + ) + .Groups[1] + .Value ) .NullIfWhiteSpace() ?.StripNonDigit() @@ -66,17 +64,16 @@ along with ([\d,\.]+) other people" [Lazy] public long? DislikeCount => content - .Source.Text.Pipe( - s => - Regex - .Match( - s, - """ - "\s*:\s*"([\d,\.]+) dislikes" - """ - ) - .Groups[1] - .Value + .Source.Text.Pipe(s => + Regex + .Match( + s, + """ + "\s*:\s*"([\d,\.]+) dislikes" + """ + ) + .Groups[1] + .Value ) .NullIfWhiteSpace() ?.StripNonDigit() @@ -98,8 +95,8 @@ along with ([\d,\.]+) other people" content .GetElementsByTagName("script") .Select(e => e.Text()) - .Select( - s => Regex.Match(s, @"var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})").Groups[1].Value + .Select(s => + Regex.Match(s, @"var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})").Groups[1].Value ) .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) ?.NullIfWhiteSpace() diff --git a/YoutubeExplode/YoutubeExplode/Channels/ChannelClient.cs b/YoutubeExplode/YoutubeExplode/Channels/ChannelClient.cs index c5b757e1c..e12ccf5fd 100644 --- a/YoutubeExplode/YoutubeExplode/Channels/ChannelClient.cs +++ b/YoutubeExplode/YoutubeExplode/Channels/ChannelClient.cs @@ -65,7 +65,7 @@ public async ValueTask GetAsync( new Thumbnail( "https://www.gstatic.com/youtube/img/tvfilm/clapperboard_profile.png", new Resolution(1024, 1024) - ) + ), ] ); } diff --git a/YoutubeExplode/YoutubeExplode/Common/Thumbnail.cs b/YoutubeExplode/YoutubeExplode/Common/Thumbnail.cs index 4c58498f4..abb625a41 100644 --- a/YoutubeExplode/YoutubeExplode/Common/Thumbnail.cs +++ b/YoutubeExplode/YoutubeExplode/Common/Thumbnail.cs @@ -41,7 +41,7 @@ internal static IReadOnlyList GetDefaultSet(VideoId videoId) => new Thumbnail( $"https://img.youtube.com/vi/{videoId}/hqdefault.jpg", new Resolution(480, 360) - ) + ), ]; } diff --git a/YoutubeExplode/YoutubeExplode/Playlists/Playlist.cs b/YoutubeExplode/YoutubeExplode/Playlists/Playlist.cs index 37a770fd2..61e213227 100644 --- a/YoutubeExplode/YoutubeExplode/Playlists/Playlist.cs +++ b/YoutubeExplode/YoutubeExplode/Playlists/Playlist.cs @@ -12,6 +12,7 @@ public class Playlist( string title, Author? author, string description, + int? count, IReadOnlyList thumbnails ) : IPlaylist { @@ -32,6 +33,14 @@ IReadOnlyList thumbnails /// public string Description { get; } = description; + /// + /// Total count of videos included in the playlist. + /// + /// + /// May be null in case of infinite playlists (e.g. auto-generated mixes). + /// + public int? Count { get; } = count; + /// public IReadOnlyList Thumbnails { get; } = thumbnails; diff --git a/YoutubeExplode/YoutubeExplode/Playlists/PlaylistClient.cs b/YoutubeExplode/YoutubeExplode/Playlists/PlaylistClient.cs index ba18ed9df..cc480fe34 100644 --- a/YoutubeExplode/YoutubeExplode/Playlists/PlaylistClient.cs +++ b/YoutubeExplode/YoutubeExplode/Playlists/PlaylistClient.cs @@ -42,6 +42,8 @@ channelId is not null && channelTitle is not null // System playlists have no description var description = response.Description ?? ""; + var count = response.Count; + var thumbnails = response .Thumbnails.Select(t => { @@ -63,7 +65,7 @@ channelId is not null && channelTitle is not null }) .ToArray(); - return new Playlist(playlistId, title, author, description, thumbnails); + return new Playlist(playlistId, title, author, description, count, thumbnails); } /// diff --git a/YoutubeExplode/YoutubeExplode/Playlists/PlaylistController.cs b/YoutubeExplode/YoutubeExplode/Playlists/PlaylistController.cs index 479db4004..9d43bf98c 100644 --- a/YoutubeExplode/YoutubeExplode/Playlists/PlaylistController.cs +++ b/YoutubeExplode/YoutubeExplode/Playlists/PlaylistController.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using YoutubeExplode.Bridge; using YoutubeExplode.Exceptions; +using YoutubeExplode.Utils; using YoutubeExplode.Videos; namespace YoutubeExplode.Playlists; @@ -18,26 +19,25 @@ public async ValueTask GetPlaylistBrowseResponseAsync( using var request = new HttpRequestMessage( HttpMethod.Post, "https://www.youtube.com/youtubei/v1/browse" - ) - { - Content = new StringContent( - // lang=json - $$""" - { - "browseId": "VL{{playlistId}}", - "context": { - "client": { - "clientName": "WEB", - "clientVersion": "2.20210408.08.00", - "hl": "en", - "gl": "US", - "utcOffsetMinutes": 0 - } - } + ); + + request.Content = new StringContent( + // lang=json + $$""" + { + "browseId": {{Json.Serialize("VL" + playlistId)}}, + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20210408.08.00", + "hl": "en", + "gl": "US", + "utcOffsetMinutes": 0 } - """ - ) - }; + } + } + """ + ); using var response = await http.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); @@ -66,29 +66,28 @@ public async ValueTask GetPlaylistNextResponseAsync( using var request = new HttpRequestMessage( HttpMethod.Post, "https://www.youtube.com/youtubei/v1/next" - ) - { - Content = new StringContent( - // lang=json - $$""" - { - "playlistId": "{{playlistId}}", - "videoId": "{{videoId}}", - "playlistIndex": {{index}}, - "context": { - "client": { - "clientName": "WEB", - "clientVersion": "2.20210408.08.00", - "hl": "en", - "gl": "US", - "utcOffsetMinutes": 0, - "visitorData": "{{visitorData}}" - } - } + ); + + request.Content = new StringContent( + // lang=json + $$""" + { + "playlistId": {{Json.Serialize(playlistId)}}, + "videoId": {{Json.Serialize(videoId)}}, + "playlistIndex": {{Json.Serialize(index)}}, + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20210408.08.00", + "hl": "en", + "gl": "US", + "utcOffsetMinutes": 0, + "visitorData": {{Json.Serialize(visitorData)}} } - """ - ) - }; + } + } + """ + ); using var response = await http.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); diff --git a/YoutubeExplode/YoutubeExplode/Search/SearchController.cs b/YoutubeExplode/YoutubeExplode/Search/SearchController.cs index 2a3e5adcd..8da1a23b4 100644 --- a/YoutubeExplode/YoutubeExplode/Search/SearchController.cs +++ b/YoutubeExplode/YoutubeExplode/Search/SearchController.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using YoutubeExplode.Bridge; +using YoutubeExplode.Utils; namespace YoutubeExplode.Search; @@ -17,34 +18,33 @@ public async ValueTask GetSearchResponseAsync( using var request = new HttpRequestMessage( HttpMethod.Post, "https://www.youtube.com/youtubei/v1/search" - ) - { - Content = new StringContent( - // lang=json - $$""" + ); + + request.Content = new StringContent( + // lang=json + $$""" + { + "query": {{Json.Serialize(searchQuery)}}, + "params": {{Json.Serialize(searchFilter switch { - "query": "{{searchQuery}}", - "params": "{{searchFilter switch - { - SearchFilter.Video => "EgIQAQ%3D%3D", - SearchFilter.Playlist => "EgIQAw%3D%3D", - SearchFilter.Channel => "EgIQAg%3D%3D", - _ => null - }}}", - "continuation": "{{continuationToken}}", - "context": { - "client": { - "clientName": "WEB", - "clientVersion": "2.20210408.08.00", - "hl": "en", - "gl": "US", - "utcOffsetMinutes": 0 - } - } + SearchFilter.Video => "EgIQAQ%3D%3D", + SearchFilter.Playlist => "EgIQAw%3D%3D", + SearchFilter.Channel => "EgIQAg%3D%3D", + _ => null + })}}, + "continuation": {{Json.Serialize(continuationToken)}}, + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20210408.08.00", + "hl": "en", + "gl": "US", + "utcOffsetMinutes": 0 + } } - """ - ) - }; + } + """ + ); using var response = await http.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); diff --git a/YoutubeExplode/YoutubeExplode/Search/SearchFilter.cs b/YoutubeExplode/YoutubeExplode/Search/SearchFilter.cs index 9c25daec6..ace91fe98 100644 --- a/YoutubeExplode/YoutubeExplode/Search/SearchFilter.cs +++ b/YoutubeExplode/YoutubeExplode/Search/SearchFilter.cs @@ -23,5 +23,5 @@ public enum SearchFilter /// /// Only search for channels. /// - Channel + Channel, } diff --git a/YoutubeExplode/YoutubeExplode/Utils/Extensions/HttpExtensions.cs b/YoutubeExplode/YoutubeExplode/Utils/Extensions/HttpExtensions.cs index 3146a053c..49de90d71 100644 --- a/YoutubeExplode/YoutubeExplode/Utils/Extensions/HttpExtensions.cs +++ b/YoutubeExplode/YoutubeExplode/Utils/Extensions/HttpExtensions.cs @@ -30,7 +30,7 @@ public static HttpRequestMessage Clone(this HttpRequestMessage request) // Don't dispose the original request's content Content = request.Content is not null ? new NonDisposableHttpContent(request.Content) - : null + : null, }; foreach (var (key, value) in request.Headers) diff --git a/YoutubeExplode/YoutubeExplode/Utils/Extensions/StringExtensions.cs b/YoutubeExplode/YoutubeExplode/Utils/Extensions/StringExtensions.cs index b067efc17..188ee1fa0 100644 --- a/YoutubeExplode/YoutubeExplode/Utils/Extensions/StringExtensions.cs +++ b/YoutubeExplode/YoutubeExplode/Utils/Extensions/StringExtensions.cs @@ -58,7 +58,7 @@ public static string SwapChars(this string str, int firstCharIndex, int secondCh new StringBuilder(str) { [firstCharIndex] = str[secondCharIndex], - [secondCharIndex] = str[firstCharIndex] + [secondCharIndex] = str[firstCharIndex], }.ToString(); public static int? ParseIntOrNull(this string str) => diff --git a/YoutubeExplode/YoutubeExplode/Utils/Extensions/XElementExtensions.cs b/YoutubeExplode/YoutubeExplode/Utils/Extensions/XElementExtensions.cs index 629b19fee..5f0394ad8 100644 --- a/YoutubeExplode/YoutubeExplode/Utils/Extensions/XElementExtensions.cs +++ b/YoutubeExplode/YoutubeExplode/Utils/Extensions/XElementExtensions.cs @@ -19,10 +19,8 @@ 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)) ); diff --git a/YoutubeExplode/YoutubeExplode/Utils/Json.cs b/YoutubeExplode/YoutubeExplode/Utils/Json.cs index 62bb0b0de..c2e652291 100644 --- a/YoutubeExplode/YoutubeExplode/Utils/Json.cs +++ b/YoutubeExplode/YoutubeExplode/Utils/Json.cs @@ -1,3 +1,5 @@ +using System.Globalization; +using System.Linq; using System.Text; using System.Text.Json; using YoutubeExplode.Utils.Extensions; @@ -56,4 +58,35 @@ public static JsonElement Parse(string source) return null; } } + + public static string Encode(string value) + { + var buffer = new StringBuilder(value.Length); + + foreach (var c in value) + { + if (c == '\n') + buffer.Append("\\n"); + else if (c == '\r') + buffer.Append("\\r"); + else if (c == '\t') + buffer.Append("\\t"); + else if (c == '\\') + buffer.Append("\\\\"); + else if (c == '"') + buffer.Append("\\\""); + else + buffer.Append(c); + } + + return buffer.ToString(); + } + + // AOT-compatible serialization + public static string Serialize(string? value) => + value is not null ? '"' + Encode(value) + '"' : "null"; + + // AOT-compatible serialization + public static string Serialize(int? value) => + value is not null ? value.Value.ToString(CultureInfo.InvariantCulture) : "null"; } diff --git a/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionClient.cs b/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionClient.cs index 85834e9d6..3ba8c203e 100644 --- a/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionClient.cs +++ b/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Text; @@ -24,12 +25,7 @@ private async IAsyncEnumerable GetClosedCaptionTrackInfo [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, cancellationToken); foreach (var trackData in playerResponse.ClosedCaptionTracks) { diff --git a/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionManifest.cs b/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionManifest.cs index 1e2ed6600..a9500345c 100644 --- a/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionManifest.cs +++ b/YoutubeExplode/YoutubeExplode/Videos/ClosedCaptions/ClosedCaptionManifest.cs @@ -19,10 +19,9 @@ public class ClosedCaptionManifest(IReadOnlyList tracks) /// 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) ); /// diff --git a/YoutubeExplode/YoutubeExplode/Videos/Streams/MediaStream.cs b/YoutubeExplode/YoutubeExplode/Videos/Streams/MediaStream.cs index 0f729aa2c..e0834c6ed 100644 --- a/YoutubeExplode/YoutubeExplode/Videos/Streams/MediaStream.cs +++ b/YoutubeExplode/YoutubeExplode/Videos/Streams/MediaStream.cs @@ -131,7 +131,7 @@ public override long Seek(long offset, SeekOrigin origin) => SeekOrigin.Begin => offset, SeekOrigin.Current => Position + offset, SeekOrigin.End => Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) + _ => throw new ArgumentOutOfRangeException(nameof(origin)), }; [ExcludeFromCodeCoverage] diff --git a/YoutubeExplode/YoutubeExplode/Videos/Streams/VideoQuality.cs b/YoutubeExplode/YoutubeExplode/Videos/Streams/VideoQuality.cs index f52b875d7..33b38f0de 100644 --- a/YoutubeExplode/YoutubeExplode/Videos/Streams/VideoQuality.cs +++ b/YoutubeExplode/YoutubeExplode/Videos/Streams/VideoQuality.cs @@ -51,7 +51,7 @@ internal Resolution GetDefaultVideoResolution() => 2880 => new Resolution(5120, 2880), 3072 => new Resolution(4096, 3072), 4320 => new Resolution(7680, 4320), - _ => new Resolution(16 * MaxHeight / 9, MaxHeight) + _ => new Resolution(16 * MaxHeight / 9, MaxHeight), }; /// @@ -188,7 +188,7 @@ internal static VideoQuality FromItag(int itag, int framerate) 396 => 360, 395 => 240, 394 => 144, - _ => throw new ArgumentException($"Unrecognized itag '{itag}'.", nameof(itag)) + _ => throw new ArgumentException($"Unrecognized itag '{itag}'.", nameof(itag)), }; return new VideoQuality(maxHeight, framerate); diff --git a/YoutubeExplode/YoutubeExplode/Videos/VideoController.cs b/YoutubeExplode/YoutubeExplode/Videos/VideoController.cs index d328c4dda..2f2c3de28 100644 --- a/YoutubeExplode/YoutubeExplode/Videos/VideoController.cs +++ b/YoutubeExplode/YoutubeExplode/Videos/VideoController.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using YoutubeExplode.Bridge; using YoutubeExplode.Exceptions; +using YoutubeExplode.Utils; namespace YoutubeExplode.Videos; @@ -30,7 +31,7 @@ await Http.GetStringAsync( continue; throw new YoutubeExplodeException( - "Video watch page is broken. " + "Please try again in a few minutes." + "Video watch page is broken. Please try again in a few minutes." ); } @@ -46,42 +47,50 @@ public async ValueTask GetPlayerResponseAsync( CancellationToken cancellationToken = default ) { - // The most optimal client to impersonate is the Android client, because - // it doesn't require signature deciphering (for both normal and n-parameter signatures). - // However, the regular Android client has a limitation, preventing it from downloading - // multiple streams from the same manifest (or the same stream multiple times). - // As a workaround, we're using ANDROID_TESTSUITE which appears to offer the same - // functionality, but doesn't impose the aforementioned limitation. + // The most optimal client to impersonate is any mobile client, because they + // don't require signature deciphering (for both normal and n-parameter signatures). + // However, we can't use the ANDROID client because it has a limitation, preventing it + // from downloading multiple streams from the same manifest (or the same stream multiple times). + // https://github.com/Tyrrrz/YoutubeExplode/issues/705 + // Previously, we were using ANDROID_TESTSUITE as a workaround, which appeared to offer the same + // functionality, but without the aforementioned limitation. However, YouTube discontinued this + // client, so now we have to use IOS instead. + // https://github.com/Tyrrrz/YoutubeExplode/issues/817 using var request = new HttpRequestMessage( HttpMethod.Post, "https://www.youtube.com/youtubei/v1/player" - ) - { - Content = new StringContent( - // lang=json - $$""" - { - "videoId": "{{videoId}}", - "context": { - "client": { - "clientName": "ANDROID_TESTSUITE", - "clientVersion": "1.9", - "androidSdkVersion": 30, - "hl": "en", - "gl": "US", - "utcOffsetMinutes": 0 - } - } + ); + + request.Content = new StringContent( + // lang=json + $$""" + { + "videoId": {{Json.Serialize(videoId)}}, + "contentCheckOk": true, + "context": { + "client": { + "clientName": "IOS", + "clientVersion": "19.29.1", + "deviceMake": "Apple", + "deviceModel": "iPhone16,2", + "hl": "en", + "osName": "iPhone", + "osVersion": "17.5.1.21F90", + "timeZone": "UTC", + "userAgent": "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)", + "gl": "US", + "utcOffsetMinutes": 0 } - """ - ) - }; + } + } + """ + ); // User agent appears to be sometimes required when impersonating Android // https://github.com/iv-org/invidious/issues/3230#issuecomment-1226887639 request.Headers.Add( "User-Agent", - "com.google.android.youtube/17.36.4 (Linux; U; Android 12; GB) gzip" + "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X)" ); using var response = await Http.SendAsync(request, cancellationToken); @@ -109,34 +118,33 @@ public async ValueTask GetPlayerResponseAsync( using var request = new HttpRequestMessage( HttpMethod.Post, "https://www.youtube.com/youtubei/v1/player" - ) - { - Content = new StringContent( - // lang=json - $$""" - { - "videoId": "{{videoId}}", - "context": { - "client": { - "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER", - "clientVersion": "2.0", - "hl": "en", - "gl": "US", - "utcOffsetMinutes": 0 - }, - "thirdParty": { - "embedUrl": "https://www.youtube.com" - } - }, - "playbackContext": { - "contentPlaybackContext": { - "signatureTimestamp": "{{signatureTimestamp ?? "19369"}}" - } - } + ); + + request.Content = new StringContent( + // lang=json + $$""" + { + "videoId": {{Json.Serialize(videoId)}}, + "context": { + "client": { + "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER", + "clientVersion": "2.0", + "hl": "en", + "gl": "US", + "utcOffsetMinutes": 0 + }, + "thirdParty": { + "embedUrl": "https://www.youtube.com" } - """ - ) - }; + }, + "playbackContext": { + "contentPlaybackContext": { + "signatureTimestamp": {{Json.Serialize(signatureTimestamp)}} + } + } + } + """ + ); using var response = await Http.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); diff --git a/YoutubeExplode/YoutubeExplode/YoutubeClient.cs b/YoutubeExplode/YoutubeExplode/YoutubeClient.cs index db03e1b09..e0f0c423a 100644 --- a/YoutubeExplode/YoutubeExplode/YoutubeClient.cs +++ b/YoutubeExplode/YoutubeExplode/YoutubeClient.cs @@ -15,26 +15,6 @@ namespace YoutubeExplode; /// public class YoutubeClient { - /// - /// Operations related to YouTube videos. - /// - public VideoClient Videos { get; } - - /// - /// Operations related to YouTube playlists. - /// - public PlaylistClient Playlists { get; } - - /// - /// Operations related to YouTube channels. - /// - public ChannelClient Channels { get; } - - /// - /// Operations related to YouTube search. - /// - public SearchClient Search { get; } - /// /// Initializes an instance of . /// @@ -65,4 +45,24 @@ public YoutubeClient(IReadOnlyList initialCookies) /// public YoutubeClient() : this(Http.Client) { } + + /// + /// Operations related to YouTube videos. + /// + public VideoClient Videos { get; } + + /// + /// Operations related to YouTube playlists. + /// + public PlaylistClient Playlists { get; } + + /// + /// Operations related to YouTube channels. + /// + public ChannelClient Channels { get; } + + /// + /// Operations related to YouTube search. + /// + public SearchClient Search { get; } } diff --git a/YoutubeExplode/YoutubeExplode/YoutubeExplode.csproj b/YoutubeExplode/YoutubeExplode/YoutubeExplode.csproj index a0db86a56..85c9491a5 100644 --- a/YoutubeExplode/YoutubeExplode/YoutubeExplode.csproj +++ b/YoutubeExplode/YoutubeExplode/YoutubeExplode.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1;netcoreapp3.1;net461;net5.0 + netstandard2.0;netstandard2.1;netcoreapp3.1;net461;net8.0 true @@ -15,14 +15,14 @@ - - + + - + - + diff --git a/YoutubeExplode/YoutubeExplode/YoutubeHttpHandler.cs b/YoutubeExplode/YoutubeExplode/YoutubeHttpHandler.cs index 05d2bfabf..160e65b02 100644 --- a/YoutubeExplode/YoutubeExplode/YoutubeHttpHandler.cs +++ b/YoutubeExplode/YoutubeExplode/YoutubeHttpHandler.cs @@ -31,12 +31,9 @@ public YoutubeHttpHandler( // Consent to the use of cookies on YouTube. // This is required to access some personalized content, such as mix playlists. _cookieContainer.Add( - new Cookie( - "SOCS", - "CAISNQgDEitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjMwODI5LjA3X3AxGgJlbiACGgYIgLC_pwY" - ) + new Cookie("SOCS", "CAISEwgDEgk2NzM5OTg2ODUaAmVuIAEaBgiA6p23Bg") { - Domain = "youtube.com" + Domain = "youtube.com", } ); } @@ -47,8 +44,8 @@ public YoutubeHttpHandler( var sessionId = cookies - .FirstOrDefault( - c => string.Equals(c.Name, "__Secure-3PAPISID", StringComparison.Ordinal) + .FirstOrDefault(c => + string.Equals(c.Name, "__Secure-3PAPISID", StringComparison.Ordinal) ) ?.Value ?? cookies @@ -151,7 +148,20 @@ private HttpResponseMessage HandleResponse(HttpResponseMessage response) if (response.Headers.TryGetValues("Set-Cookie", out var cookieHeaderValues)) { foreach (var cookieHeaderValue in cookieHeaderValues) - _cookieContainer.SetCookies(response.RequestMessage.RequestUri, cookieHeaderValue); + { + try + { + _cookieContainer.SetCookies( + response.RequestMessage.RequestUri, + cookieHeaderValue + ); + } + catch (CookieException) + { + // YouTube may send cookies for other domains, ignore them + // https://github.com/Tyrrrz/YoutubeExplode/issues/762 + } + } } return response; From e1286de1e2a028545a71b5e35f0bbf9d31b9f545 Mon Sep 17 00:00:00 2001 From: Mikhail Kaufmann <68746803+Spectralon@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:42:03 +0300 Subject: [PATCH 2/3] Update README.md --- README.md | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d9d297ae9..82ec8f053 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,29 @@ -# Денацифицированный YoutubeDownloader (RU) -**YoutubeDownloaderRU** основан на данном репозитории: https://github.com/Tyrrrz/YoutubeDownloader +# YoutubeDownloader (RU)

Icon

-# Как скачать -Последняя версия: v2.38 -
Дата: 13.01.2024 -
Ссылка в **Releases** : [Нажмите, чтобы скачать](https://github.com/reawoken/YoutubeDownloaderRU/releases/download/v2.38/YoutubeDownloaderRU.v2.38.zip) +Based on: https://github.com/Tyrrrz/YoutubeDownloader -# Почему я сделал YoutubeDownloaderRU -Чтобы каждый мог им пользоваться, независимо от идеологии и местопложения. Автор данного приложения закоренелый нацист и русособ, который распространяет недопустимую пропаганду в приложении, вообще не имеющем отношения к политике, войнам и прочему. +**YoutubeDownloader** is an application that lets you download videos from YouTube. +You can copy-paste URL of any video, playlist or channel and download it directly to a format of your choice. +It also supports searching by keywords, which is helpful if you want to quickly look up and download videos. +**YoutubeDownloader (RU)** features complete country restrictions bypass. -# Что было изменено -Основано на версии оригинального репозитория 12.01.2024 -- Полностью удалена проверка национальной принадлежности по локали системы, которая запрещала запуск приложения в России и Белоруссии. Приложение может запустить **кто угодно и где угодно**, будь то житель России, Украины, Белоруссии и любой другой страны. -- Больше не опирается на внешние библиотеки автора через NuGet, теперь они часть проекта в репозитории. Также удалена зависимость от омерзительной библиотеки автора с омерзительным названием **deorcify**, которая не делает ничего, кроме дискриминации по национальному признаку. -- Вычищена подчистую вся политическая повестка (кроме скромной сноски в настройках), удалена вся агрессивная пропаганда и русофобия. -- Интерфейс полностью переведён на русский -- Убраны все упоминания авторства безумного пропагандиста, который изначально делал это приложение. -- Отключены обновления (возможно временно, в будущем смогу добавить свою систему обновлений, не опираясь на изначальный модуль если буду вообще обновлять этот репозиторий) +## Terms of use[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/why-so-political.md) -# Как собрать -Просто скачать и открыть **YoutubeDownloader.sln** в Visual Studio 2022 последней версии с установленным .NET 8.0 и собрать. -
Собранный проект появится в директории **YoutubeDownloaderRU\bin**. \ No newline at end of file +> **Note**: +> **YoutubeDownloader** comes bundled with [FFmpeg](https://ffmpeg.org) which is used for processing videos. +> You can also download a version of **YoutubeDownloader** that doesn't include FFmpeg (`YoutubeDownloader.Bare.*` builds) if you prefer to use your own installation. + +## Features + +- Cross-platform graphical user interface +- Download videos by URL +- Download videos from playlists or channels +- Download videos by search query +- Selectable video quality and format +- Automatically embed subtitles +- Automatically inject media tags +- Log in with a YouTube account to access private content From b9bc593156dd545438c7f8efa4e72905d51f8f5a Mon Sep 17 00:00:00 2001 From: Mikhail Kaufmann <68746803+Spectralon@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:42:03 +0300 Subject: [PATCH 3/3] Update README.md --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d9d297ae9..af6312cd1 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,27 @@ -# Денацифицированный YoutubeDownloader (RU) -**YoutubeDownloaderRU** основан на данном репозитории: https://github.com/Tyrrrz/YoutubeDownloader +# YoutubeDownloader (RU)

Icon

-# Как скачать -Последняя версия: v2.38 -
Дата: 13.01.2024 -
Ссылка в **Releases** : [Нажмите, чтобы скачать](https://github.com/reawoken/YoutubeDownloaderRU/releases/download/v2.38/YoutubeDownloaderRU.v2.38.zip) +Based on: https://github.com/Tyrrrz/YoutubeDownloader -# Почему я сделал YoutubeDownloaderRU -Чтобы каждый мог им пользоваться, независимо от идеологии и местопложения. Автор данного приложения закоренелый нацист и русособ, который распространяет недопустимую пропаганду в приложении, вообще не имеющем отношения к политике, войнам и прочему. +**YoutubeDownloader** is an application that lets you download videos from YouTube. +You can copy-paste URL of any video, playlist or channel and download it directly to a format of your choice. +It also supports searching by keywords, which is helpful if you want to quickly look up and download videos. -# Что было изменено -Основано на версии оригинального репозитория 12.01.2024 -- Полностью удалена проверка национальной принадлежности по локали системы, которая запрещала запуск приложения в России и Белоруссии. Приложение может запустить **кто угодно и где угодно**, будь то житель России, Украины, Белоруссии и любой другой страны. -- Больше не опирается на внешние библиотеки автора через NuGet, теперь они часть проекта в репозитории. Также удалена зависимость от омерзительной библиотеки автора с омерзительным названием **deorcify**, которая не делает ничего, кроме дискриминации по национальному признаку. -- Вычищена подчистую вся политическая повестка (кроме скромной сноски в настройках), удалена вся агрессивная пропаганда и русофобия. -- Интерфейс полностью переведён на русский -- Убраны все упоминания авторства безумного пропагандиста, который изначально делал это приложение. -- Отключены обновления (возможно временно, в будущем смогу добавить свою систему обновлений, не опираясь на изначальный модуль если буду вообще обновлять этот репозиторий) +**YoutubeDownloader (RU)** features complete country restrictions bypass. -# Как собрать -Просто скачать и открыть **YoutubeDownloader.sln** в Visual Studio 2022 последней версии с установленным .NET 8.0 и собрать. -
Собранный проект появится в директории **YoutubeDownloaderRU\bin**. \ No newline at end of file +> **Note**: +> **YoutubeDownloader** comes bundled with [FFmpeg](https://ffmpeg.org) which is used for processing videos. + +## Features + +- Cross-platform graphical user interface +- Download videos by URL +- Download videos from playlists or channels +- Download videos by search query +- Selectable video quality and format +- Automatically embed subtitles +- Automatically inject media tags +- Log in with a YouTube account to access private content