From 858162d6343d666f9c260b32c810db1ca2ac25b5 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:27:35 +0200 Subject: [PATCH 1/7] Filter out streams with mismatched content length Closes #759 --- YoutubeExplode/Videos/Streams/MediaStream.cs | 25 +++++++++++-------- YoutubeExplode/Videos/Streams/StreamClient.cs | 22 +++++++++++++++- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/YoutubeExplode/Videos/Streams/MediaStream.cs b/YoutubeExplode/Videos/Streams/MediaStream.cs index 7ca3fe04..03ff042f 100644 --- a/YoutubeExplode/Videos/Streams/MediaStream.cs +++ b/YoutubeExplode/Videos/Streams/MediaStream.cs @@ -9,8 +9,14 @@ namespace YoutubeExplode.Videos.Streams; // Works around YouTube's rate throttling, provides seeking support, and some resiliency -internal class MediaStream(HttpClient http, IStreamInfo streamInfo) : Stream +internal partial class MediaStream(HttpClient http, IStreamInfo streamInfo) : Stream { + // For most streams, YouTube limits transfer speed to match the video playback rate. + // This helps them avoid unnecessary bandwidth, but for us it's a hindrance because + // 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. + private readonly long _segmentLength = streamInfo.IsThrottled() ? 9_898_989 : streamInfo.Size.Bytes; @@ -31,12 +37,6 @@ internal class MediaStream(HttpClient http, IStreamInfo streamInfo) : Stream public override long Position { get; set; } - // For most streams, YouTube limits transfer speed to match the video playback rate. - // This helps them avoid unnecessary bandwidth, but for us it's a hindrance because - // 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. - private void ResetSegment() { _segmentStream?.Dispose(); @@ -50,10 +50,7 @@ private async ValueTask ResolveSegmentAsync( if (_segmentStream is not null) return _segmentStream; - var from = Position; - var to = Position + _segmentLength - 1; - var url = UrlEx.SetQueryParameter(streamInfo.Url, "range", $"{from}-{to}"); - + var url = GetSegmentUrl(streamInfo.Url, Position, Position + _segmentLength - 1); var stream = await http.GetStreamAsync(url, cancellationToken); return _segmentStream = stream; @@ -147,3 +144,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } + +internal partial class MediaStream +{ + public static string GetSegmentUrl(string streamUrl, long from, long to) => + UrlEx.SetQueryParameter(streamUrl, "range", $"{from}-{to}"); +} diff --git a/YoutubeExplode/Videos/Streams/StreamClient.cs b/YoutubeExplode/Videos/Streams/StreamClient.cs index f0fc04f2..54894b6e 100644 --- a/YoutubeExplode/Videos/Streams/StreamClient.cs +++ b/YoutubeExplode/Videos/Streams/StreamClient.cs @@ -82,10 +82,26 @@ private async IAsyncEnumerable GetStreamInfosAsync( ?? await _http.TryGetContentLengthAsync(url, false, cancellationToken) ?? 0; - // Stream cannot be accessed + // Stream is empty or cannot be accessed if (contentLength <= 0) continue; + // Some streams have mismatched content length, so we need to make sure the value we + // obtained is correct, otherwise we may get a 404 error while trying to read the stream. + // https://github.com/Tyrrrz/YoutubeExplode/issues/759 + using ( + var response = await _http.GetAsync( + // Try to access the last byte of the stream + MediaStream.GetSegmentUrl(url, contentLength - 2, contentLength - 1), + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ) + ) + { + if (!response.IsSuccessStatusCode) + continue; + } + var container = streamData.Container?.Pipe(s => new Container(s)) ?? throw new YoutubeExplodeException("Failed to extract the stream container."); @@ -201,7 +217,9 @@ private async IAsyncEnumerable GetStreamInfosAsync( await foreach ( var streamInfo in GetStreamInfosAsync(playerResponse.Streams, cancellationToken) ) + { yield return streamInfo; + } // Extract streams from the DASH manifest if (!string.IsNullOrWhiteSpace(playerResponse.DashManifestUrl)) @@ -224,7 +242,9 @@ var streamInfo in GetStreamInfosAsync(playerResponse.Streams, cancellationToken) await foreach ( var streamInfo in GetStreamInfosAsync(dashManifest.Streams, cancellationToken) ) + { yield return streamInfo; + } } } } From 4b95090ec5b93b2d4c89dca08f919b25fbe24f3f Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:32:00 +0200 Subject: [PATCH 2/7] Add tests --- YoutubeExplode.Tests/ClosedCaptionSpecs.cs | 10 +++++----- YoutubeExplode.Tests/StreamSpecs.cs | 3 +++ YoutubeExplode.Tests/TestData/VideoIds.cs | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/YoutubeExplode.Tests/ClosedCaptionSpecs.cs b/YoutubeExplode.Tests/ClosedCaptionSpecs.cs index 3b86dead..ca1613a5 100644 --- a/YoutubeExplode.Tests/ClosedCaptionSpecs.cs +++ b/YoutubeExplode.Tests/ClosedCaptionSpecs.cs @@ -62,8 +62,8 @@ public async Task I_can_get_a_specific_closed_caption_track_from_a_video() var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync( VideoIds.WithClosedCaptions ); - var trackInfo = manifest.GetByLanguage("en-US"); + var trackInfo = manifest.GetByLanguage("en-US"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); // Assert @@ -80,8 +80,8 @@ public async Task I_can_get_a_specific_closed_caption_track_from_a_video_that_ha var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync( VideoIds.WithBrokenClosedCaptions ); - var trackInfo = manifest.GetByLanguage("en"); + var trackInfo = manifest.GetByLanguage("en"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); // Assert @@ -98,8 +98,8 @@ public async Task I_can_get_an_individual_closed_caption_from_a_video() var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync( VideoIds.WithClosedCaptions ); - var trackInfo = manifest.GetByLanguage("en-US"); + var trackInfo = manifest.GetByLanguage("en-US"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); var caption = track.GetByTime(TimeSpan.FromSeconds(641)); @@ -118,8 +118,8 @@ public async Task I_can_get_an_individual_closed_caption_part_from_a_video() var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync( VideoIds.WithClosedCaptions ); - var trackInfo = manifest.GetByLanguage("en"); + var trackInfo = manifest.GetByLanguage("en"); var track = await youtube.Videos.ClosedCaptions.GetAsync(trackInfo); var captionPart = track @@ -141,8 +141,8 @@ public async Task I_can_download_a_specific_closed_caption_track_from_a_video() var manifest = await youtube.Videos.ClosedCaptions.GetManifestAsync( VideoIds.WithClosedCaptions ); - var trackInfo = manifest.GetByLanguage("en-US"); + var trackInfo = manifest.GetByLanguage("en-US"); await youtube.Videos.ClosedCaptions.DownloadAsync(trackInfo, file.Path); // Assert diff --git a/YoutubeExplode.Tests/StreamSpecs.cs b/YoutubeExplode.Tests/StreamSpecs.cs index d57759e3..0fbdeae2 100644 --- a/YoutubeExplode.Tests/StreamSpecs.cs +++ b/YoutubeExplode.Tests/StreamSpecs.cs @@ -71,6 +71,7 @@ public async Task I_can_get_the_list_of_available_streams_of_a_video() [InlineData(VideoIds.AgeRestrictedSexual)] [InlineData(VideoIds.AgeRestrictedEmbedRestricted)] [InlineData(VideoIds.LiveStreamRecording)] + [InlineData(VideoIds.WithBrokenStreams)] [InlineData(VideoIds.WithOmnidirectionalStreams)] [InlineData(VideoIds.WithHighDynamicRangeStreams)] public async Task I_can_get_the_list_of_available_streams_of_any_playable_video(string videoId) @@ -134,6 +135,7 @@ public async Task I_can_try_to_get_the_list_of_available_streams_of_a_video_and_ [InlineData(VideoIds.AgeRestrictedViolent)] [InlineData(VideoIds.AgeRestrictedSexual)] [InlineData(VideoIds.LiveStreamRecording)] + [InlineData(VideoIds.WithBrokenStreams)] [InlineData(VideoIds.WithOmnidirectionalStreams)] public async Task I_can_get_a_specific_stream_of_a_video(string videoId) { @@ -163,6 +165,7 @@ public async Task I_can_get_a_specific_stream_of_a_video(string videoId) [InlineData(VideoIds.AgeRestrictedSexual)] [InlineData(VideoIds.AgeRestrictedEmbedRestricted)] [InlineData(VideoIds.LiveStreamRecording)] + [InlineData(VideoIds.WithBrokenStreams)] [InlineData(VideoIds.WithOmnidirectionalStreams)] public async Task I_can_download_a_specific_stream_of_a_video(string videoId) { diff --git a/YoutubeExplode.Tests/TestData/VideoIds.cs b/YoutubeExplode.Tests/TestData/VideoIds.cs index bbf30715..9beef14f 100644 --- a/YoutubeExplode.Tests/TestData/VideoIds.cs +++ b/YoutubeExplode.Tests/TestData/VideoIds.cs @@ -15,6 +15,7 @@ internal static class VideoIds public const string LiveStream = "jfKfPfyJRdk"; public const string LiveStreamRecording = "rsAAeyAr-9Y"; public const string WithBrokenTitle = "4ZJWv6t-PfY"; + public const string WithBrokenStreams = "JQgKhZZyBYg"; public const string WithHighQualityStreams = "V5Fsj_sCKdg"; public const string WithOmnidirectionalStreams = "-xNN-bJQ4vI"; public const string WithHighDynamicRangeStreams = "vX2vsvdq8nw"; From 89feae9819db71c0e7372118df8fddd9497faada Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:02:59 +0200 Subject: [PATCH 3/7] Retry with a different client if no streams are returned --- YoutubeExplode/Videos/Streams/StreamClient.cs | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/YoutubeExplode/Videos/Streams/StreamClient.cs b/YoutubeExplode/Videos/Streams/StreamClient.cs index 54894b6e..ab7ee38a 100644 --- a/YoutubeExplode/Videos/Streams/StreamClient.cs +++ b/YoutubeExplode/Videos/Streams/StreamClient.cs @@ -176,14 +176,13 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null } } - private async IAsyncEnumerable GetStreamInfosAsync( + private async ValueTask> GetStreamInfosAsync( VideoId videoId, - [EnumeratorCancellation] CancellationToken cancellationToken = default + PlayerResponse playerResponse, + CancellationToken cancellationToken = default ) { - var playerResponse = await _controller.GetPlayerResponseAsync(videoId, cancellationToken); - - // If the video is pay-to-play, error out + // Video is pay-to-play if (!string.IsNullOrWhiteSpace(playerResponse.PreviewVideoId)) { throw new VideoRequiresPurchaseException( @@ -192,60 +191,85 @@ private async IAsyncEnumerable GetStreamInfosAsync( ); } - // If the video is unplayable, try one more time by fetching the player response - // with signature deciphering. This is (only) required for age-restricted videos. - if (!playerResponse.IsPlayable) - { - var cipherManifest = await ResolveCipherManifestAsync(cancellationToken); - playerResponse = await _controller.GetPlayerResponseAsync( - videoId, - cipherManifest.SignatureTimestamp, - cancellationToken - ); - } - - // If the video is still unplayable, error out + // Video is unplayable if (!playerResponse.IsPlayable) { throw new VideoUnplayableException( - $"Video '{videoId}' is unplayable. " - + $"Reason: '{playerResponse.PlayabilityError}'." + $"Video '{videoId}' is unplayable. Reason: '{playerResponse.PlayabilityError}'." ); } + var streamInfos = new List(); + // Extract streams from the player response await foreach ( var streamInfo in GetStreamInfosAsync(playerResponse.Streams, cancellationToken) ) { - yield return streamInfo; + streamInfos.Add(streamInfo); } // Extract streams from the DASH manifest if (!string.IsNullOrWhiteSpace(playerResponse.DashManifestUrl)) { - var dashManifest = default(DashManifest?); - try { - dashManifest = await _controller.GetDashManifestAsync( + var dashManifest = await _controller.GetDashManifestAsync( playerResponse.DashManifestUrl, cancellationToken ); - } - // Some DASH manifest URLs return 404 for whatever reason - // https://github.com/Tyrrrz/YoutubeExplode/issues/728 - catch (HttpRequestException) { } - if (dashManifest is not null) - { await foreach ( var streamInfo in GetStreamInfosAsync(dashManifest.Streams, cancellationToken) ) { - yield return streamInfo; + streamInfos.Add(streamInfo); } } + // Some DASH manifest URLs return 404 for whatever reason + // https://github.com/Tyrrrz/YoutubeExplode/issues/728 + catch (HttpRequestException) { } + } + + // Error if no streams were found + if (!streamInfos.Any()) + { + throw new VideoUnplayableException( + $"Video '{videoId}' does not contain any playable streams." + ); + } + + return streamInfos; + } + + private async ValueTask> GetStreamInfosAsync( + VideoId videoId, + CancellationToken cancellationToken = default + ) + { + // Try to get player response from a cipher-less client + try + { + var playerResponse = await _controller.GetPlayerResponseAsync( + videoId, + cancellationToken + ); + + return await GetStreamInfosAsync(videoId, playerResponse, cancellationToken); + } + catch (VideoUnplayableException) { } + + // Try to get player response from a client with cipher + { + var cipherManifest = await ResolveCipherManifestAsync(cancellationToken); + + var playerResponse = await _controller.GetPlayerResponseAsync( + videoId, + cipherManifest.SignatureTimestamp, + cancellationToken + ); + + return await GetStreamInfosAsync(videoId, playerResponse, cancellationToken); } } @@ -261,13 +285,6 @@ public async ValueTask GetManifestAsync( { var streamInfos = await GetStreamInfosAsync(videoId, cancellationToken); - if (!streamInfos.Any()) - { - throw new VideoUnplayableException( - $"Video '{videoId}' does not contain any playable streams." - ); - } - // YouTube sometimes returns stream URLs that produce 403 Forbidden errors when accessed. // This happens for both protected and non-protected streams, so the cause is unclear. // As a workaround, we can access one of the stream URLs and retry if it fails. From 4b8b1dc0c7fd4cace2b8f60e57656ba9a3c76f7c Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:13:53 +0200 Subject: [PATCH 4/7] Test --- YoutubeExplode/Videos/Streams/StreamClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/YoutubeExplode/Videos/Streams/StreamClient.cs b/YoutubeExplode/Videos/Streams/StreamClient.cs index ab7ee38a..9ecbf977 100644 --- a/YoutubeExplode/Videos/Streams/StreamClient.cs +++ b/YoutubeExplode/Videos/Streams/StreamClient.cs @@ -86,6 +86,7 @@ private async IAsyncEnumerable GetStreamInfosAsync( if (contentLength <= 0) continue; + /* // Some streams have mismatched content length, so we need to make sure the value we // obtained is correct, otherwise we may get a 404 error while trying to read the stream. // https://github.com/Tyrrrz/YoutubeExplode/issues/759 @@ -101,6 +102,7 @@ private async IAsyncEnumerable GetStreamInfosAsync( if (!response.IsSuccessStatusCode) continue; } + */ var container = streamData.Container?.Pipe(s => new Container(s)) From f935cbd92a3c98b22e2c771e1edc818100b5582a Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:29:36 +0200 Subject: [PATCH 5/7] Only filter out 404s --- YoutubeExplode/Videos/Streams/StreamClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/YoutubeExplode/Videos/Streams/StreamClient.cs b/YoutubeExplode/Videos/Streams/StreamClient.cs index 9ecbf977..aa3e65fc 100644 --- a/YoutubeExplode/Videos/Streams/StreamClient.cs +++ b/YoutubeExplode/Videos/Streams/StreamClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading; @@ -86,9 +87,7 @@ private async IAsyncEnumerable GetStreamInfosAsync( if (contentLength <= 0) continue; - /* - // Some streams have mismatched content length, so we need to make sure the value we - // obtained is correct, otherwise we may get a 404 error while trying to read the stream. + // Streams may have mismatched content length, so ensure that the obtained value is correct // https://github.com/Tyrrrz/YoutubeExplode/issues/759 using ( var response = await _http.GetAsync( @@ -99,10 +98,11 @@ private async IAsyncEnumerable GetStreamInfosAsync( ) ) { - if (!response.IsSuccessStatusCode) + // Only check for the 404 status here, as other errors (e.g. 401/403) may indicate + // at issues with the extraction process, not with the stream itself. + if (response.StatusCode == HttpStatusCode.NotFound) continue; } - */ var container = streamData.Container?.Pipe(s => new Container(s)) From 6aabc9e8e22dccc3aae6fd08f1dcbc3166cc5713 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:41:37 +0200 Subject: [PATCH 6/7] Refactor --- YoutubeExplode/Videos/Streams/MediaStream.cs | 3 +- YoutubeExplode/Videos/Streams/StreamClient.cs | 93 +++++++++++-------- YoutubeExplode/YoutubeHttpHandler.cs | 4 +- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/YoutubeExplode/Videos/Streams/MediaStream.cs b/YoutubeExplode/Videos/Streams/MediaStream.cs index 03ff042f..0f729aa2 100644 --- a/YoutubeExplode/Videos/Streams/MediaStream.cs +++ b/YoutubeExplode/Videos/Streams/MediaStream.cs @@ -74,7 +74,8 @@ private async ValueTask ReadSegmentAsync( return await stream.ReadAsync(buffer, offset, count, cancellationToken); } // Retry on connectivity issues - catch (IOException) when (retriesRemaining > 0) + catch (Exception ex) + when (ex is HttpRequestException or IOException && retriesRemaining > 0) { ResetSegment(); } diff --git a/YoutubeExplode/Videos/Streams/StreamClient.cs b/YoutubeExplode/Videos/Streams/StreamClient.cs index aa3e65fc..c3e42be7 100644 --- a/YoutubeExplode/Videos/Streams/StreamClient.cs +++ b/YoutubeExplode/Videos/Streams/StreamClient.cs @@ -51,6 +51,48 @@ CancellationToken cancellationToken ?? throw new YoutubeExplodeException("Failed to extract the cipher manifest."); } + private async ValueTask TryGetContentLengthAsync( + IStreamData streamData, + string url, + CancellationToken cancellationToken = default + ) + { + var contentLength = streamData.ContentLength; + + // If content length is not available in the metadata, get it by + // sending a HEAD request and parsing the Content-Length header. + if (contentLength is null) + { + using var response = await _http.HeadAsync(url, cancellationToken); + + // 404 error indicates that the stream is not available + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + + response.EnsureSuccessStatusCode(); + } + + if (contentLength is not null) + { + // Streams may have mismatched content length, so ensure that the obtained value is correct + // https://github.com/Tyrrrz/YoutubeExplode/issues/759 + using var response = await _http.GetAsync( + // Try to access the last byte of the stream + MediaStream.GetSegmentUrl(url, contentLength.Value - 2, contentLength.Value - 1), + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ); + + // 404 error indicates that the stream has mismatched content length + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + + response.EnsureSuccessStatusCode(); + } + + return contentLength; + } + private async IAsyncEnumerable GetStreamInfosAsync( IEnumerable streamDatas, [EnumeratorCancellation] CancellationToken cancellationToken = default @@ -78,32 +120,10 @@ private async IAsyncEnumerable GetStreamInfosAsync( ); } - var contentLength = - streamData.ContentLength - ?? await _http.TryGetContentLengthAsync(url, false, cancellationToken) - ?? 0; - - // Stream is empty or cannot be accessed - if (contentLength <= 0) + var contentLength = await TryGetContentLengthAsync(streamData, url, cancellationToken); + if (contentLength is null) continue; - // Streams may have mismatched content length, so ensure that the obtained value is correct - // https://github.com/Tyrrrz/YoutubeExplode/issues/759 - using ( - var response = await _http.GetAsync( - // Try to access the last byte of the stream - MediaStream.GetSegmentUrl(url, contentLength - 2, contentLength - 1), - HttpCompletionOption.ResponseHeadersRead, - cancellationToken - ) - ) - { - // Only check for the 404 status here, as other errors (e.g. 401/403) may indicate - // at issues with the extraction process, not with the stream itself. - if (response.StatusCode == HttpStatusCode.NotFound) - continue; - } - var container = streamData.Container?.Pipe(s => new Container(s)) ?? throw new YoutubeExplodeException("Failed to extract the stream container."); @@ -132,7 +152,7 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null var streamInfo = new MuxedStreamInfo( url, container, - new FileSize(contentLength), + new FileSize(contentLength.Value), bitrate, streamData.AudioCodec, streamData.VideoCodec, @@ -148,7 +168,7 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null var streamInfo = new VideoOnlyStreamInfo( url, container, - new FileSize(contentLength), + new FileSize(contentLength.Value), bitrate, streamData.VideoCodec, videoQuality, @@ -164,7 +184,7 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null var streamInfo = new AudioOnlyStreamInfo( url, container, - new FileSize(contentLength), + new FileSize(contentLength.Value), bitrate, streamData.AudioCodec ); @@ -285,18 +305,13 @@ public async ValueTask GetManifestAsync( { for (var retriesRemaining = 5; ; retriesRemaining--) { - var streamInfos = await GetStreamInfosAsync(videoId, cancellationToken); - - // YouTube sometimes returns stream URLs that produce 403 Forbidden errors when accessed. - // This happens for both protected and non-protected streams, so the cause is unclear. - // As a workaround, we can access one of the stream URLs and retry if it fails. - using var response = await _http.HeadAsync(streamInfos.First().Url, cancellationToken); - if ((int)response.StatusCode == 403 && retriesRemaining > 0) - continue; - - response.EnsureSuccessStatusCode(); - - return new StreamManifest(streamInfos); + try + { + return new StreamManifest(await GetStreamInfosAsync(videoId, cancellationToken)); + } + // Retry on connectivity issues + catch (Exception ex) + when (ex is HttpRequestException or IOException && retriesRemaining > 0) { } } } diff --git a/YoutubeExplode/YoutubeHttpHandler.cs b/YoutubeExplode/YoutubeHttpHandler.cs index a5f8c246..9e470ae6 100644 --- a/YoutubeExplode/YoutubeHttpHandler.cs +++ b/YoutubeExplode/YoutubeHttpHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -186,7 +187,8 @@ await base.SendAsync( return response; } // Retry on connectivity issues - catch (HttpRequestException) when (retriesRemaining > 0) { } + catch (Exception ex) + when (ex is HttpRequestException or IOException && retriesRemaining > 0) { } } } } From 174bad0b5e5e70eaa51495648d10b852ea0d0547 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:03:10 +0200 Subject: [PATCH 7/7] jl --- YoutubeExplode.Tests/StreamSpecs.cs | 3 --- YoutubeExplode/Utils/Extensions/HttpExtensions.cs | 15 --------------- YoutubeExplode/Videos/Streams/StreamClient.cs | 8 +++----- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/YoutubeExplode.Tests/StreamSpecs.cs b/YoutubeExplode.Tests/StreamSpecs.cs index 0fbdeae2..36c5a555 100644 --- a/YoutubeExplode.Tests/StreamSpecs.cs +++ b/YoutubeExplode.Tests/StreamSpecs.cs @@ -27,9 +27,6 @@ public async Task I_can_get_the_list_of_available_streams_of_a_video() // Assert manifest.Streams.Should().NotBeEmpty(); - manifest.GetMuxedStreams().Should().NotBeEmpty(); - manifest.GetAudioStreams().Should().NotBeEmpty(); - manifest.GetVideoStreams().Should().NotBeEmpty(); manifest .GetVideoStreams() diff --git a/YoutubeExplode/Utils/Extensions/HttpExtensions.cs b/YoutubeExplode/Utils/Extensions/HttpExtensions.cs index 16a9a77b..3146a053 100644 --- a/YoutubeExplode/Utils/Extensions/HttpExtensions.cs +++ b/YoutubeExplode/Utils/Extensions/HttpExtensions.cs @@ -59,19 +59,4 @@ public static async ValueTask HeadAsync( cancellationToken ); } - - public static async ValueTask TryGetContentLengthAsync( - this HttpClient http, - string requestUri, - bool ensureSuccess = true, - CancellationToken cancellationToken = default - ) - { - using var response = await http.HeadAsync(requestUri, cancellationToken); - - if (ensureSuccess) - response.EnsureSuccessStatusCode(); - - return response.Content.Headers.ContentLength; - } } diff --git a/YoutubeExplode/Videos/Streams/StreamClient.cs b/YoutubeExplode/Videos/Streams/StreamClient.cs index c3e42be7..09d89f83 100644 --- a/YoutubeExplode/Videos/Streams/StreamClient.cs +++ b/YoutubeExplode/Videos/Streams/StreamClient.cs @@ -83,7 +83,7 @@ CancellationToken cancellationToken cancellationToken ); - // 404 error indicates that the stream has mismatched content length + // 404 error indicates that the stream has mismatched content length or is not available if (response.StatusCode == HttpStatusCode.NotFound) return null; @@ -327,16 +327,14 @@ public async ValueTask GetHttpLiveStreamUrlAsync( 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( - "Failed to extract the HTTP Live Stream manifest URL. " - + $"Video '{videoId}' is likely not a live stream." + $"Failed to extract the HTTP Live Stream manifest URL. Video '{videoId}' is likely not a live stream." ); }