Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
jerry08 committed Jun 10, 2024
1 parent 79f043d commit 253d77c
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 102 deletions.
30 changes: 0 additions & 30 deletions Yosu.Core/Utils/Memo.cs

This file was deleted.

40 changes: 13 additions & 27 deletions Yosu.Youtube.Core/Downloading/VideoDownloadOption.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Yosu.Core.Utils;
using Yosu.Core.Utils.Extensions;
using YoutubeExplode.Videos.Streams;

Expand All @@ -13,11 +12,8 @@ public partial record VideoDownloadOption(
IReadOnlyList<IStreamInfo> StreamInfos
)
{
public VideoQuality? VideoQuality =>
Memo.Cache(
this,
() => StreamInfos.OfType<IVideoStreamInfo>().MaxBy(s => s.VideoQuality)?.VideoQuality
);
public VideoQuality? VideoQuality { get; } =
StreamInfos.OfType<IVideoStreamInfo>().MaxBy(s => s.VideoQuality)?.VideoQuality;
}

public partial record VideoDownloadOption
Expand All @@ -26,17 +22,19 @@ internal static IReadOnlyList<VideoDownloadOption> ResolveAll(StreamManifest man
{
IEnumerable<VideoDownloadOption> GetVideoAndAudioOptions()
{
var videoStreams = manifest.GetVideoStreams().OrderByDescending(v => v.VideoQuality);
var videoStreamInfos = manifest
.GetVideoStreams()
.OrderByDescending(v => v.VideoQuality);

foreach (var videoStreamInfo in videoStreams)
foreach (var videoStreamInfo in videoStreamInfos)
{
// Muxed stream
if (videoStreamInfo is MuxedStreamInfo)
{
yield return new VideoDownloadOption(
videoStreamInfo.Container,
false,
new[] { videoStreamInfo }
[videoStreamInfo]
);
}
// Separate audio + video stream
Expand Down Expand Up @@ -75,22 +73,14 @@ IEnumerable<VideoDownloadOption> GetAudioOnlyOptions()

if (audioStreamInfo is not null)
{
yield return new VideoDownloadOption(
Container.WebM,
true,
new[] { audioStreamInfo }
);
yield return new VideoDownloadOption(Container.WebM, true, [audioStreamInfo]);

yield return new VideoDownloadOption(
Container.Mp3,
true,
new[] { audioStreamInfo }
);
yield return new VideoDownloadOption(Container.Mp3, true, [audioStreamInfo]);

yield return new VideoDownloadOption(
new Container("ogg"),
true,
new[] { audioStreamInfo }
[audioStreamInfo]
);
}
}
Expand All @@ -106,18 +96,14 @@ IEnumerable<VideoDownloadOption> GetAudioOnlyOptions()

if (audioStreamInfo is not null)
{
yield return new VideoDownloadOption(
Container.Mp4,
true,
new[] { audioStreamInfo }
);
yield return new VideoDownloadOption(Container.Mp4, true, [audioStreamInfo]);
}
}
}

// Deduplicate download options by video quality and container
var comparer = new DelegateEqualityComparer<VideoDownloadOption>(
(x, y) => x.VideoQuality == y.VideoQuality && x.Container == y.Container,
var comparer = EqualityComparer<VideoDownloadOption>.Create(
(x, y) => x?.VideoQuality == y?.VideoQuality && x?.Container == y?.Container,
x => HashCode.Combine(x.VideoQuality, x.Container)
);

Expand Down
10 changes: 5 additions & 5 deletions Yosu.Youtube.Core/Downloading/VideoDownloadPreference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ VideoQualityPreference PreferredVideoQuality
VideoQualityPreference.Highest
=> orderedOptions.LastOrDefault(o => o.Container == PreferredContainer),

VideoQualityPreference.UpTo360p
=> orderedOptions
.Where(o => o.VideoQuality?.MaxHeight <= 360)
.LastOrDefault(o => o.Container == PreferredContainer),

VideoQualityPreference.UpTo1080p
=> orderedOptions
.Where(o => o.VideoQuality?.MaxHeight <= 1080)
Expand All @@ -43,6 +38,11 @@ VideoQualityPreference PreferredVideoQuality
.Where(o => o.VideoQuality?.MaxHeight <= 480)
.LastOrDefault(o => o.Container == PreferredContainer),

VideoQualityPreference.UpTo360p
=> orderedOptions
.Where(o => o.VideoQuality?.MaxHeight <= 360)
.LastOrDefault(o => o.Container == PreferredContainer),

VideoQualityPreference.Lowest
=> orderedOptions.LastOrDefault(o => o.Container == PreferredContainer),

Expand Down
4 changes: 2 additions & 2 deletions Yosu.Youtube.Core/Downloading/VideoQualityPreference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ public static class VideoQualityPreferenceExtensions
public static string GetDisplayName(this VideoQualityPreference preference) =>
preference switch
{
VideoQualityPreference.Lowest => "Lowest",
VideoQualityPreference.Lowest => "Lowest quality",
VideoQualityPreference.UpTo360p => "≤ 360p",
VideoQualityPreference.UpTo480p => "≤ 480p",
VideoQualityPreference.UpTo720p => "≤ 720p",
VideoQualityPreference.UpTo1080p => "≤ 1080p",
VideoQualityPreference.Highest => "Highest",
VideoQualityPreference.Highest => "Highest quality",
_ => throw new ArgumentOutOfRangeException(nameof(preference))
};
}
101 changes: 65 additions & 36 deletions Yosu.Youtube.Core/Resolving/QueryResolver.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Gress;
Expand All @@ -13,80 +14,109 @@

namespace Yosu.Youtube.Core.Resolving;

public class QueryResolver
public class QueryResolver(IReadOnlyList<Cookie>? initialCookies = null)
{
private YoutubeClient _youtube = new(Http.Client);
private readonly YoutubeClient _youtube = new(Http.Client, initialCookies ?? []);
private readonly bool _isAuthenticated = initialCookies?.Any() == true;

public async Task<QueryResult> ResolveAsync(
private async Task<QueryResult?> TryResolvePlaylistAsync(
string query,
CancellationToken cancellationToken = default
)
{
// Only consider URLs when parsing IDs.
// All other queries should be treated as search queries.
var isUrl = Uri.IsWellFormedUriString(query, UriKind.Absolute);
if (PlaylistId.TryParse(query) is not { } playlistId)
return null;

// Playlist
if (isUrl && PlaylistId.TryParse(query) is { } playlistId)
{
var playlist = await _youtube.Playlists.GetAsync(playlistId, cancellationToken);
var videos = await _youtube.Playlists.GetVideosAsync(playlistId, cancellationToken);
return new QueryResult(QueryResultKind.Playlist, $"Playlist: {playlist.Title}", videos);
}
// Skip personal system playlists if the user is not authenticated
var isPersonalSystemPlaylist =
playlistId == "WL" || playlistId == "LL" || playlistId == "LM";

// Video
if (isUrl && VideoId.TryParse(query) is { } videoId)
{
var video = await _youtube.Videos.GetAsync(videoId, cancellationToken);
return new QueryResult(QueryResultKind.Video, video.Title, new[] { video });
}
if (isPersonalSystemPlaylist && !_isAuthenticated)
return null;

// Channel
if (isUrl && ChannelId.TryParse(query) is { } channelId)
var playlist = await _youtube.Playlists.GetAsync(playlistId, cancellationToken);
var videos = await _youtube.Playlists.GetVideosAsync(playlistId, cancellationToken);

return new QueryResult(QueryResultKind.Playlist, $"Playlist: {playlist.Title}", videos);
}

private async Task<QueryResult?> TryResolveVideoAsync(
string query,
CancellationToken cancellationToken = default
)
{
if (VideoId.TryParse(query) is not { } videoId)
return null;

var video = await _youtube.Videos.GetAsync(videoId, cancellationToken);
return new QueryResult(QueryResultKind.Video, video.Title, [video]);
}

private async Task<QueryResult?> TryResolveChannelAsync(
string query,
CancellationToken cancellationToken = default
)
{
if (ChannelId.TryParse(query) is { } channelId)
{
var channel = await _youtube.Channels.GetAsync(channelId, cancellationToken);
var videos = await _youtube.Channels.GetUploadsAsync(channelId, cancellationToken);

return new QueryResult(QueryResultKind.Channel, $"Channel: {channel.Title}", videos);
}

// Channel (by handle)
if (isUrl && ChannelHandle.TryParse(query) is { } channelHandle)
if (ChannelHandle.TryParse(query) is { } channelHandle)
{
var channel = await _youtube.Channels.GetByHandleAsync(
channelHandle,
cancellationToken
);

var videos = await _youtube.Channels.GetUploadsAsync(channel.Id, cancellationToken);

return new QueryResult(QueryResultKind.Channel, $"Channel: {channel.Title}", videos);
}

// Channel (by username)
if (isUrl && UserName.TryParse(query) is { } userName)
if (UserName.TryParse(query) is { } userName)
{
var channel = await _youtube.Channels.GetByUserAsync(userName, cancellationToken);
var videos = await _youtube.Channels.GetUploadsAsync(channel.Id, cancellationToken);

return new QueryResult(QueryResultKind.Channel, $"Channel: {channel.Title}", videos);
}

// Channel (by slug)
if (isUrl && ChannelSlug.TryParse(query) is { } channelSlug)
if (ChannelSlug.TryParse(query) is { } channelSlug)
{
var channel = await _youtube.Channels.GetBySlugAsync(channelSlug, cancellationToken);
var videos = await _youtube.Channels.GetUploadsAsync(channel.Id, cancellationToken);

return new QueryResult(QueryResultKind.Channel, $"Channel: {channel.Title}", videos);
}

// Search
{
_youtube = new();
return null;
}

var videos = await _youtube
.Search.GetVideosAsync(query, cancellationToken)
.CollectAsync(20);
return new QueryResult(QueryResultKind.Search, $"Search: {query}", videos);
}
private async Task<QueryResult> ResolveSearchAsync(
string query,
CancellationToken cancellationToken = default
)
{
var videos = await _youtube
.Search.GetVideosAsync(query, cancellationToken)
.CollectAsync(20);

return new QueryResult(QueryResultKind.Search, $"Search: {query}", videos);
}

public async Task<QueryResult> ResolveAsync(
string query,
CancellationToken cancellationToken = default
) =>
await TryResolvePlaylistAsync(query, cancellationToken)
?? await TryResolveVideoAsync(query, cancellationToken)
?? await TryResolveChannelAsync(query, cancellationToken)
?? await ResolveSearchAsync(query, cancellationToken);

public async Task<QueryResult> ResolveAsync(
IReadOnlyList<string> queries,
IProgress<Percentage>? progress = null,
Expand All @@ -100,7 +130,6 @@ public async Task<QueryResult> ResolveAsync(
var videoIds = new HashSet<VideoId>();

var completed = 0;

foreach (var query in queries)
{
var result = await ResolveAsync(query, cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions Yosu.Youtube.Core/Tagging/MediaTagInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ private async Task InjectMusicMetadataAsync(
var recordings = await _musicBrainz.SearchRecordingsAsync(video.Title, cancellationToken);

var recording = recordings.FirstOrDefault(r =>
// Recording title must be part of the video title.
// Recording artist must be part of the video title or channel title.
// Recording title must be a part of the video title.
// Recording artist must be a part of the video title or channel title.
video.Title.Contains(r.Title, StringComparison.OrdinalIgnoreCase)
&& (
video.Title.Contains(r.Artist, StringComparison.OrdinalIgnoreCase)
Expand Down

0 comments on commit 253d77c

Please sign in to comment.