Skip to content

Commit

Permalink
merge ytexplode v6.3.2
Browse files Browse the repository at this point in the history
  • Loading branch information
tmm360 committed Aug 28, 2023
2 parents f0ad44e + 754c1da commit 397b00c
Show file tree
Hide file tree
Showing 26 changed files with 422 additions and 1,015 deletions.
18 changes: 18 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ https://github.com/Etherna/YoutubeExplode/releases

# Changelog

## v6.3.2 (18-Aug-2023)

- Fixed an issue where calling `StreamClient.GetManifestAsync(...)` failed on videos from the "YouTube Movies & TV" system channel.
- Fixed an issue where calling `ChannelClient.GetAsync(...)` failed on the "YouTube Movies & TV" system channel.

## v6.3.1 (23-Jul-2023)

- Fixed an issue where calling `StreamClient.GetManifestAsync(...)` failed on some videos with an error saying `The format of value is invalid`.

## v6.3 (21-Jul-2023)

- Added support for providing cookies directly to `YoutubeClient` with the help of two new constructor overloads: `new YoutubeClient(IReadOnlyList<Cookie> initialCookies)` and `new YoutubeClient(HttpClient http, IReadOnlyList<Cookie> initialCookies)`. You will still need to obtain the cookies yourself (see the [readme](https://github.com/Tyrrrz/YoutubeExplode/blob/6.3/Readme.md#authentication) for some guidance), but YoutubeExplode will take care of generating the required headers and sending them with every request.

## v6.2.17 (12-Jul-2023)

- Changed the implementation of the `HttpMessageHandler` used by default in `YoutubeClient` from `HttpClientHandler` to `SocketsHttpHandler` on platforms that support it.
- Fixed an issue where certain system playlist IDs were considered invalid, such as `LL`.

## v6.2.16 (28-Jun-2023)

- Fixed an issue where `ClosedCaptionClient.WriteToAsync(...)` and `ClosedCaptionClient.DownloadAsync(...)` produced invalid SRT timestamps for caption tracks that exceeded 24 hours in length.
Expand Down
886 changes: 24 additions & 862 deletions LICENSE

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,23 @@ await foreach (var batch in youtube.Search.GetResultBatchesAsync("blender tutori
}
```

### Authentication

You can access private videos and playlists by providing cookies that correspond to a pre-authenticated YouTube account.
To do that, create an instance of `YoutubeClient` using a constructor that accepts `IReadOnlyList<Cookie>`:

```csharp
using YoutubeExplode;

// Perform authentication and extract cookies
var cookies = ...;

// Cookie collection must be of type IReadOnlyList<System.Net.Cookie>
var youtube = new YoutubeClient(cookies);
```

In order to actually perform the authentication, you can use an embedded browser such as [WebView](https://nuget.org/packages/Microsoft.Web.WebView2) to navigate the user to the [YouTube login page](https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com), let them log in, and then extract the cookies from the browser.

## Etymology

The "Explode" in **YoutubeExplode** comes from the name of a PHP function that splits up strings, [`explode(...)`](https://php.net/manual/en/function.explode.php). When I was starting the development of this library, most of the reference source code I read was written in PHP, hence the inspiration for the name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace YoutubeExplode.Converter.Tests.Utils.Extensions;

internal static class HttpExtensions
{
public static async Task DownloadAsync(this HttpClient httpClient, string url, string filePath)
public static async Task DownloadAsync(this HttpClient http, string url, string filePath)
{
using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
using var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

await using var source = await response.Content.ReadAsStreamAsync();
Expand Down
7 changes: 6 additions & 1 deletion YoutubeDownloader.Converter.Tests/Utils/FFmpeg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ private static async ValueTask DownloadAsync()

// Add the execute permission on Unix
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
File.SetUnixFileMode(FilePath, UnixFileMode.UserExecute);
{
File.SetUnixFileMode(
FilePath,
File.GetUnixFileMode(FilePath) | UnixFileMode.UserExecute
);
}
}

public static async ValueTask InitializeAsync()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</PackageReference>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
<PackageReference Include="PolyShim" Version="1.5.0" PrivateAssets="all" />
<PackageReference Include="PolyShim" Version="1.7.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
18 changes: 18 additions & 0 deletions YoutubeDownloader.Tests/ChannelSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ public async Task I_can_get_the_metadata_of_a_channel_by_handle()
channel.Thumbnails.Should().NotBeEmpty();
}

[Theory]
[InlineData(ChannelIds.Normal)]
[InlineData(ChannelIds.Movies)]
public async Task I_can_get_the_metadata_of_any_available_channel(string channelId)
{
// Arrange
var youtube = new YoutubeClient();

// Act
var channel = await youtube.Channels.GetAsync(channelId);

// Assert
channel.Id.Value.Should().Be(channelId);
channel.Url.Should().NotBeNullOrWhiteSpace();
channel.Title.Should().NotBeNullOrWhiteSpace();
channel.Thumbnails.Should().NotBeEmpty();
}

[Fact]
public async Task I_can_get_videos_uploaded_by_a_channel()
{
Expand Down
4 changes: 3 additions & 1 deletion YoutubeDownloader.Tests/PlaylistIdSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace YoutubeExplode.Tests;
public class PlaylistIdSpecs
{
[Theory]
[InlineData("WL")]
[InlineData("LL")]
[InlineData("RDMM")]
[InlineData("PL601B2E69B03FAB9D")]
[InlineData("PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e")]
[InlineData("PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk")]
Expand Down Expand Up @@ -48,7 +51,6 @@ public void I_can_parse_a_playlist_ID_from_a_URL_string(string playlistUrl, stri
[InlineData("PLm_3vnTS-pvmZFuF L1Pyhqf8kTTYVKjW")]
[InlineData("PLm_3vnTS-pvmZFuF3L=Pyhqf8kTTYVKjW")]
[InlineData("youtube.com/playlist?lisp=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H")]
[InlineData("youtube.com/playlist?list=asd")]
[InlineData("youtube.com/")]
public void I_cannot_parse_a_playlist_ID_from_an_invalid_string(string playlistIdOrUrl)
{
Expand Down
6 changes: 3 additions & 3 deletions YoutubeDownloader.Tests/StreamSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public async Task I_can_get_a_specific_stream_from_a_video(string videoId)

foreach (var streamInfo in manifest.Streams)
{
await using var stream = await youtube.Videos.Streams.GetAsync(streamInfo);
using var stream = await youtube.Videos.Streams.GetAsync(streamInfo);
var bytesRead = await stream.ReadAsync(buffer.Memory);

// Assert
Expand Down Expand Up @@ -219,14 +219,14 @@ public async Task I_can_download_the_highest_quality_stream_from_a_video()
public async Task I_can_seek_to_a_specific_position_on_a_stream_from_a_video()
{
// Arrange
await using var buffer = new MemoryStream();
using var buffer = new MemoryStream();
var youtube = new YoutubeClient();

// Act
var manifest = await youtube.Videos.Streams.GetManifestAsync(VideoIds.Normal);
var streamInfo = manifest.GetAudioStreams().OrderBy(s => s.Size).First();

await using var stream = await youtube.Videos.Streams.GetAsync(streamInfo);
using var stream = await youtube.Videos.Streams.GetAsync(streamInfo);
stream.Seek(1000, SeekOrigin.Begin);
await stream.CopyToAsync(buffer);

Expand Down
1 change: 1 addition & 0 deletions YoutubeDownloader.Tests/TestData/ChannelIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
internal static class ChannelIds
{
public const string Normal = "UCX6OQ3DkcsbYNE6H8uQQuVA";
public const string Movies = "UCuVPpxrm2VAgpH3Ktln4HXg";
}
12 changes: 7 additions & 5 deletions YoutubeDownloader.Tests/YoutubeDownloader.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFrameworks>net7.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(TargetFrameworks);net48</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
<PackageReference Include="PolyShim" Version="1.7.0" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
22 changes: 20 additions & 2 deletions YoutubeDownloader/Channels/ChannelClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,26 @@ private Channel Get(ChannelPage channelPage)
/// </summary>
public async ValueTask<Channel> GetAsync(
ChannelId channelId,
CancellationToken cancellationToken = default) =>
Get(await _controller.GetChannelPageAsync(channelId, cancellationToken));
CancellationToken cancellationToken = default)
{
// Special case for the "Movies & TV" channel, which has a custom page
if (channelId == "UCuVPpxrm2VAgpH3Ktln4HXg")
{
return new Channel(
"UCuVPpxrm2VAgpH3Ktln4HXg",
"Movies & TV",
new[]
{
new Thumbnail(
"https://www.gstatic.com/youtube/img/tvfilm/clapperboard_profile.png",
new Resolution(1024, 1024)
)
}
);
}

return Get(await _controller.GetChannelPageAsync(channelId, cancellationToken));
}

/// <summary>
/// Gets the metadata associated with the channel of the specified user.
Expand Down
5 changes: 2 additions & 3 deletions YoutubeDownloader/Playlists/PlaylistId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ public readonly partial struct PlaylistId
public partial struct PlaylistId
{
private static bool IsValid(string playlistId) =>
// "Watch later" and "My mix" playlists are special
playlistId is "WL" or "RDMM" ||
playlistId.Length >= 13 &&
// 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 '-');

private static string? TryNormalize(string? playlistIdOrUrl)
Expand Down
31 changes: 31 additions & 0 deletions YoutubeDownloader/Utils/ClientDelegatingHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace YoutubeExplode.Utils;

// Used to extend an externally provided HttpClient with additional behavior
internal abstract class ClientDelegatingHandler : HttpMessageHandler
{
private readonly HttpClient _http;
private readonly bool _disposeClient;

protected ClientDelegatingHandler(HttpClient http, bool disposeClient = false)
{
_http = http;
_disposeClient = disposeClient;
}

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);

protected override void Dispose(bool disposing)
{
if (disposing && _disposeClient)
_http.Dispose();

base.Dispose(disposing);
}
}
21 changes: 21 additions & 0 deletions YoutubeDownloader/Utils/Extensions/BinaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Globalization;
using System.Text;

namespace YoutubeExplode.Utils.Extensions;

internal static class BinaryExtensions
{
public static string ToHex(this byte[] data, bool isUpperCase = true)
{
var buffer = new StringBuilder(2 * data.Length);

foreach (var b in data)
{
buffer.Append(
b.ToString(isUpperCase ? "X2" : "x2", CultureInfo.InvariantCulture)
);
}

return buffer.ToString();
}
}
8 changes: 8 additions & 0 deletions YoutubeDownloader/Utils/Extensions/UriExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace YoutubeExplode.Utils.Extensions;

internal static class UriExtensions
{
public static string GetDomain(this Uri uri) => uri.Scheme + Uri.SchemeDelimiter + uri.Host;
}
12 changes: 12 additions & 0 deletions YoutubeDownloader/Utils/Hash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Security.Cryptography;

namespace YoutubeExplode.Utils;

internal static class Hash
{
public static byte[] Compute(HashAlgorithm algorithm, byte[] data)
{
using (algorithm)
return algorithm.ComputeHash(data);
}
}
15 changes: 1 addition & 14 deletions YoutubeDownloader/Utils/Http.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
using System;
using System.Net;
using System.Net.Http;

namespace YoutubeExplode.Utils;

internal static class Http
{
private static readonly Lazy<HttpClient> HttpClientLazy = new(() =>
{
var handler = new HttpClientHandler
{
// https://github.com/Tyrrrz/YoutubeExplode/issues/530
UseCookies = false
};

if (handler.SupportsAutomaticDecompression)
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;

return new HttpClient(handler, true);
});
private static readonly Lazy<HttpClient> HttpClientLazy = new(() => new HttpClient());

public static HttpClient Client => HttpClientLazy.Value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private async IAsyncEnumerable<ClosedCaption> GetClosedCaptionsAsync(
if (string.IsNullOrEmpty(text))
continue;

// Auto-generated captions may be missing offset or duration.
// 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)
Expand Down
5 changes: 5 additions & 0 deletions YoutubeDownloader/Videos/Streams/IStreamInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public interface IStreamInfo
/// <summary>
/// Stream URL.
/// </summary>
/// <remarks>
/// 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 <see cref="StreamClient.GetAsync" />
/// or <see cref="StreamClient.DownloadAsync"/> instead, as they do all the heavy lifting for you.
/// </remarks>
string Url { get; }

/// <summary>
Expand Down
Loading

0 comments on commit 397b00c

Please sign in to comment.