diff --git a/Manager.App/Components/Application/Dev/CipherDev.razor b/Manager.App/Components/Application/Dev/CipherDev.razor deleted file mode 100644 index 42d9609..0000000 --- a/Manager.App/Components/Application/Dev/CipherDev.razor +++ /dev/null @@ -1,13 +0,0 @@ -@using Manager.App.Models.System -@using Manager.App.Services.System - -@inject ISnackbar Snackbar -@inject ClientService ClientService - -Cipher manager - - - - Exec - \ No newline at end of file diff --git a/Manager.App/Components/Application/Dev/CipherDev.razor.cs b/Manager.App/Components/Application/Dev/CipherDev.razor.cs deleted file mode 100644 index dd7d7a9..0000000 --- a/Manager.App/Components/Application/Dev/CipherDev.razor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Manager.App.Models.System; -using Manager.YouTube.Util.Cipher; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; -using MudBlazor; - -namespace Manager.App.Components.Application.Dev; - -public partial class CipherDev : ComponentBase -{ - private YouTubeClientItem? _selectedClient; - - private async Task ExecCipher(MouseEventArgs obj) - { - if (_selectedClient == null) - { - Snackbar.Add("No client selected", Severity.Warning); - return; - } - - var ytClientResult = await ClientService.LoadClientByIdAsync(_selectedClient.Id); - if (!ytClientResult.IsSuccess) - { - Snackbar.Add(ytClientResult.Error?.Description ?? "Failed to get the client!", Severity.Error); - return; - } - - var ytClient = ytClientResult.Value; - if (ytClient.State == null) - { - Snackbar.Add("Client state is null!", Severity.Warning); - return; - } - - var decoder = await CipherManager.GetDecoderAsync(ytClient.State, ytClient); - } - - private async Task> SearchClientsAsync(string? search, CancellationToken cancellationToken) - { - var searchResults = await ClientService.GetClientsAsync(search, cancellationToken: cancellationToken); - return !searchResults.IsSuccess ? [] : searchResults.Value; - } -} \ No newline at end of file diff --git a/Manager.App/Components/Application/Dev/DevelopmentVideo.razor b/Manager.App/Components/Application/Dev/DevelopmentVideo.razor deleted file mode 100644 index 97925bc..0000000 --- a/Manager.App/Components/Application/Dev/DevelopmentVideo.razor +++ /dev/null @@ -1,17 +0,0 @@ -@using Manager.App.Models.System -@using Manager.App.Services.System - -@inject ISnackbar Snackbar -@inject ClientService ClientService -@inject NavigationManager NavigationManager - -Video data - - - - - - - Get data - \ No newline at end of file diff --git a/Manager.App/Components/Application/Dev/DevelopmentVideo.razor.cs b/Manager.App/Components/Application/Dev/DevelopmentVideo.razor.cs deleted file mode 100644 index 93bcb9b..0000000 --- a/Manager.App/Components/Application/Dev/DevelopmentVideo.razor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Manager.App.Models.System; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; -using MudBlazor; - -namespace Manager.App.Components.Application.Dev; - -public partial class DevelopmentVideo : ComponentBase -{ - private YouTubeClientItem? _selectedClient; - private string _videoId = ""; - - private async Task> SearchClientsAsync(string? search, CancellationToken cancellationToken) - { - var searchResults = await ClientService.GetClientsAsync(search, cancellationToken: cancellationToken); - return !searchResults.IsSuccess ? [] : searchResults.Value; - } - - private void NavigateToVideo(MouseEventArgs obj) - { - if (_selectedClient == null) - { - Snackbar.Add("No client selected!", Severity.Warning); - return; - } - - if (string.IsNullOrWhiteSpace(_videoId)) - { - Snackbar.Add("No video ID set!", Severity.Warning); - return; - } - - if (_videoId.Length != 11) - { - Snackbar.Add("Video ID needs to have an length of 11 chars!", Severity.Warning); - } - - NavigationManager.NavigateTo($"/video/{_videoId}?clientId={_selectedClient.Id}"); - } -} \ No newline at end of file diff --git a/Manager.App/Components/Layout/NavMenu.razor b/Manager.App/Components/Layout/NavMenu.razor index b8a7d3d..dd3ae86 100644 --- a/Manager.App/Components/Layout/NavMenu.razor +++ b/Manager.App/Components/Layout/NavMenu.razor @@ -2,10 +2,9 @@ Home - Search Accounts Channels - Playlists + Playlists Info diff --git a/Manager.App/Components/Pages/Development.razor b/Manager.App/Components/Pages/Development.razor index bc2da8f..338665e 100644 --- a/Manager.App/Components/Pages/Development.razor +++ b/Manager.App/Components/Pages/Development.razor @@ -6,10 +6,4 @@ - - - - - - \ No newline at end of file diff --git a/Manager.App/Components/Pages/Video.razor b/Manager.App/Components/Pages/Video.razor deleted file mode 100644 index 6463efd..0000000 --- a/Manager.App/Components/Pages/Video.razor +++ /dev/null @@ -1,244 +0,0 @@ -@page "/Video/{VideoId}" -@using Manager.App.Services.System - -@inject ISnackbar Snackbar -@inject ClientService ClientService -@inject CacheService Cache - - -@if (!_loading && _video != null) -{ - - - @{ - var thumbnailUrl = _video.Thumbnails.OrderByDescending(t => t.Width).FirstOrDefault()?.Url; - } - @if (!string.IsNullOrWhiteSpace(thumbnailUrl)) - { - - } - - @_video.Title - @_video.Description - - - - - - @* Info *@ - - - - Video ID: - @_video.VideoId - - - Title: - @_video.Title - - - Description: - @_video.Description - - - HashTags: - @foreach (var hashtag in _video.HashTags) - { - @hashtag - } - - - - View count: - @_video.ViewCount - - - Like count: - @_video.LikeCount - - - Channel ID: - @_video.ChannelId - - - Author: - @_video.Author - - - Playability status: - @_video.PlayabilityStatus - - - Length seconds: - @_video.LengthSeconds - - - Keywords: - @foreach (var keyword in _video.Keywords) - { - @keyword - } - - - - Publish date: - @_video.PublishDate - - - Upload date: - @_video.UploadDate - - - Category: - @_video.Category - - - - @* Boolean values *@ - - - - Is owner viewing: - @_video.IsOwnerViewing - - - Allow rating: - @_video.AllowRating - - - Is crawlable: - @_video.IsCrawlable - - - Is private: - @_video.IsPrivate - - - Is unplugged corpus: - @_video.IsUnpluggedCorpus - - - Is live: - @_video.IsLive - - - Is family save: - @_video.IsFamilySave - - - Is unlisted: - @_video.IsUnlisted - - - Has Ypc metadata: - @_video.HasYpcMetadata - - - Is shorts eligible: - @_video.IsShortsEligible - - - - - - - @if (_video.StreamingData == null) - { - No streaming data available! - } - else - { - - - Adaptive Formats - - - Id - Mime type - Bitrate - Resolution - Last modified (UNIX epoch) - Quality - FPS - - - @context.Itag - @context.MimeType - @context.Bitrate - @($"{context.Width}x{context.Height}") - @context.LastModified - @context.Quality - @context.Fps - - - - - Formats - - - Id - Mime type - Bitrate - Resolution - Last modified (UNIX epoch) - Quality - FPS - - - @context.Itag - @context.MimeType - @context.Bitrate - @($"{context.Width}x{context.Height}") - @context.LastModified - @context.Quality - @context.Fps - - - - - } - - - @if (_video.PlayerConfig == null) - { - No player config available! - } - else - { - - - - Audio loudness DB: - @_video.PlayerConfig.AudioLoudnessDb - - - Audio perceptual loudness DB: - @_video.PlayerConfig.AudioPerceptualLoudnessDb - - - Audio enable per format loudness: - @_video.PlayerConfig.AudioLoudnessDb - - - Max bitrate: - @_video.PlayerConfig.MaxBitrate - - - Max read ahead time MS: - @_video.PlayerConfig.MaxReadAheadMediaTimeMs - - - Min read ahead time MS: - @_video.PlayerConfig.MinReadAheadMediaTimeMs - - - Read ahead growth rate MS: - @_video.PlayerConfig.ReadAheadGrowthRateMs - - - - } - - - -} \ No newline at end of file diff --git a/Manager.App/Components/Pages/Video.razor.cs b/Manager.App/Components/Pages/Video.razor.cs deleted file mode 100644 index 92efa72..0000000 --- a/Manager.App/Components/Pages/Video.razor.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Manager.YouTube.Models; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Manager.App.Components.Pages; - -public partial class Video : ComponentBase -{ - [Parameter] - public required string VideoId { get; set; } - - [SupplyParameterFromQuery(Name = "clientId")] - public string ClientId { get; set; } = ""; - - private bool _loading = true; - private YouTubeVideo? _video; - - protected override async Task OnInitializedAsync() - { - if (string.IsNullOrEmpty(VideoId)) - { - Snackbar.Add("Video id is null or empty!", Severity.Error); - _loading = false; - return; - } - - var clientResult = await ClientService.LoadClientByIdAsync(ClientId); - if (!clientResult.IsSuccess) - { - Snackbar.Add(clientResult.Error?.Description ?? "Failed to load client!", Severity.Error); - _loading = false; - return; - } - - var client = clientResult.Value; - - var videoResult = await client.GetVideoByIdAsync(VideoId); - if (!videoResult.IsSuccess) - { - Snackbar.Add(videoResult.Error?.Description ?? "Failed to get video.", Severity.Error); - _loading = false; - return; - } - - _video = videoResult.Value; - _loading = false; - } -} \ No newline at end of file diff --git a/Manager.App/Services/LibraryService.cs b/Manager.App/Services/LibraryService.cs index 3f7fa02..30212b0 100644 --- a/Manager.App/Services/LibraryService.cs +++ b/Manager.App/Services/LibraryService.cs @@ -16,6 +16,7 @@ namespace Manager.App.Services; public class LibraryService : ILibraryService { private readonly ILogger _logger; + private readonly LibrarySettings _librarySettings; private readonly IDbContextFactory _dbContextFactory; private readonly DirectoryInfo _libraryDirectory; private readonly CacheService _cacheService; @@ -23,13 +24,13 @@ public class LibraryService : ILibraryService public LibraryService(ILogger logger, IOptions librarySettings, IDbContextFactory contextFactory, CacheService cacheService) { _logger = logger; - var librarySettings1 = librarySettings.Value; + _librarySettings = librarySettings.Value; _dbContextFactory = contextFactory; _cacheService = cacheService; - _libraryDirectory = Directory.CreateDirectory(librarySettings1.Path); + _libraryDirectory = Directory.CreateDirectory(_librarySettings.Path); logger.LogDebug("Library directory: {LibraryWorkingDir}", _libraryDirectory.FullName); - Directory.CreateDirectory(Path.Combine(librarySettings1.Path, LibraryConstants.Directories.SubDirMedia)); - Directory.CreateDirectory(Path.Combine(librarySettings1.Path, LibraryConstants.Directories.SubDirChannels)); + Directory.CreateDirectory(Path.Combine(_librarySettings.Path, LibraryConstants.Directories.SubDirMedia)); + Directory.CreateDirectory(Path.Combine(_librarySettings.Path, LibraryConstants.Directories.SubDirChannels)); } private async Task AddWebImagesAsync(LibraryDbContext context, List images, string foreignKey, string libSubDir, string fileType, string subDir) @@ -101,7 +102,6 @@ public class LibraryService : ILibraryService } else { - context.HttpCookies.RemoveRange(context.HttpCookies.Where(x => x.ClientId == client.Id)); context.ClientAccounts.Add(dbClient); } @@ -144,9 +144,9 @@ public class LibraryService : ILibraryService try { await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - var channel = await context.Channels.AsSplitQuery() + var channel = await context.Channels .Include(c => c.ClientAccount) - .ThenInclude(p => p!.HttpCookies) + .ThenInclude(p => p!.HttpCookies) .Include(f => f.Files) .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); diff --git a/Manager.App/Services/System/ClientService.cs b/Manager.App/Services/System/ClientService.cs index 2690b85..4b3525d 100644 --- a/Manager.App/Services/System/ClientService.cs +++ b/Manager.App/Services/System/ClientService.cs @@ -31,15 +31,16 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger> GetClientsAsync(string? search, int offset = 0, int limit = 10, CancellationToken cancellationToken = default) + public async Task> GetClientsAsync(string search, int offset = 0, int limit = 10, CancellationToken cancellationToken = default) { if (_libraryService == null) { diff --git a/Manager.YouTube/Interpreter/JavaScriptEngineManager.cs b/Manager.YouTube/Interpreter/JavaScriptEngineManager.cs deleted file mode 100644 index 3d4197b..0000000 --- a/Manager.YouTube/Interpreter/JavaScriptEngineManager.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Text; -using DotBased.Monads; - -namespace Manager.YouTube.Interpreter; - -public static class JavaScriptEngineManager -{ - private static readonly PlayerEngineCollection Engines = []; - - public static async Task> GetPlayerEngine(string playerUrl) - { - if (string.IsNullOrEmpty(playerUrl)) - { - return ResultError.Fail("player url is empty or null!"); - } - - var version = GetScriptVersion(playerUrl); - - if (Engines.TryGetValue(version, out var engine)) - { - return engine; - } - - var playerJsSourceResult = await DownloadPlayerScriptAsync(playerUrl); - if (!playerJsSourceResult.IsSuccess) - { - return playerJsSourceResult.Error ?? ResultError.Fail("Download player script failed!"); - } - - return new PlayerEngine(version, playerJsSourceResult.Value); - } - - private static string GetScriptVersion(string relativePlayerUrl) - { - var split = relativePlayerUrl.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var v = split[2]; - var lang = split[4]; - return $"{v}-{lang}"; - } - - private static async Task> DownloadPlayerScriptAsync(string relativeUrl, YouTubeClient? client = null) - { - var downloadRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/{relativeUrl}")); - var downloadResponse = await NetworkService.DownloadBytesAsync(downloadRequest, client); - if (!downloadResponse.IsSuccess) - { - return downloadResponse.Error ?? ResultError.Fail($"Failed to download script from url: {relativeUrl}"); - } - - var playerJs = Encoding.UTF8.GetString(downloadResponse.Value.Data); - if (string.IsNullOrWhiteSpace(playerJs)) - { - return ResultError.Fail("Script value is empty!"); - } - - return playerJs; - } -} \ No newline at end of file diff --git a/Manager.YouTube/Interpreter/PlayerEngine.cs b/Manager.YouTube/Interpreter/PlayerEngine.cs deleted file mode 100644 index 2d35a33..0000000 --- a/Manager.YouTube/Interpreter/PlayerEngine.cs +++ /dev/null @@ -1,42 +0,0 @@ -using DotBased.Logging; -using Jint; - -namespace Manager.YouTube.Interpreter; - -public class PlayerEngine -{ - public string Version { get; set; } - public Engine JsEngine { get; set; } - private ILogger Logger { get; set; } - - public PlayerEngine(string version, string script) - { - if (string.IsNullOrEmpty(version)) - { - throw new ArgumentNullException(nameof(version)); - } - - if (string.IsNullOrEmpty(script)) - { - throw new ArgumentNullException(nameof(script)); - } - - Logger = LogService.RegisterLogger(typeof(PlayerEngine), version); - Version = version; - JsEngine = new Engine().Execute(script).SetValue("log", new Action(obj => - { - var logStr = obj.ToString(); - if (string.IsNullOrEmpty(logStr)) - { - return; - } - Logger.Information(logStr); - })); - - } - - public void InitializePlayer() - { - JsEngine.Execute("createPlayer"); - } -} \ No newline at end of file diff --git a/Manager.YouTube/Interpreter/PlayerEngineCollection.cs b/Manager.YouTube/Interpreter/PlayerEngineCollection.cs deleted file mode 100644 index f15ab19..0000000 --- a/Manager.YouTube/Interpreter/PlayerEngineCollection.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Manager.YouTube.Interpreter; - -public class PlayerEngineCollection : KeyedCollection -{ - protected override string GetKeyForItem(PlayerEngine item) - { - return item.Version; - } -} \ No newline at end of file diff --git a/Manager.YouTube/Manager.YouTube.csproj b/Manager.YouTube/Manager.YouTube.csproj index 4bd844d..4166dec 100644 --- a/Manager.YouTube/Manager.YouTube.csproj +++ b/Manager.YouTube/Manager.YouTube.csproj @@ -9,7 +9,6 @@ - diff --git a/Manager.YouTube/Models/Innertube/ColorInfo.cs b/Manager.YouTube/Models/Innertube/ColorInfo.cs index 642fa7b..afbd20a 100644 --- a/Manager.YouTube/Models/Innertube/ColorInfo.cs +++ b/Manager.YouTube/Models/Innertube/ColorInfo.cs @@ -1,13 +1,8 @@ -using System.Text.Json.Serialization; - namespace Manager.YouTube.Models.Innertube; public class ColorInfo { - [JsonPropertyName("primaries")] public string Primaries { get; set; } = ""; - [JsonPropertyName("transferCharacteristics")] public string TransferCharacteristics { get; set; } = ""; - [JsonPropertyName("matrixCoefficients")] public string MatrixCoefficients { get; set; } = ""; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/Range.cs b/Manager.YouTube/Models/Innertube/Range.cs index 2f6c384..38d6fdc 100644 --- a/Manager.YouTube/Models/Innertube/Range.cs +++ b/Manager.YouTube/Models/Innertube/Range.cs @@ -1,11 +1,7 @@ -using System.Text.Json.Serialization; - namespace Manager.YouTube.Models.Innertube; public class Range { - [JsonPropertyName("start")] public uint Start { get; set; } - [JsonPropertyName("end")] public uint End { get; set; } } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/StreamingData.cs b/Manager.YouTube/Models/Innertube/StreamingData.cs index 4f39c6b..a1135f6 100644 --- a/Manager.YouTube/Models/Innertube/StreamingData.cs +++ b/Manager.YouTube/Models/Innertube/StreamingData.cs @@ -1,18 +1,10 @@ -using System.Text.Json.Serialization; -using Manager.YouTube.Util.Converters; - namespace Manager.YouTube.Models.Innertube; public class StreamingData { public DateTime FetchedUtc { get; set; } = DateTime.UtcNow; - [JsonPropertyName("expiresInSeconds")] public int ExpiresInSeconds { get; set; } - [JsonPropertyName("serverAbrStreamingUrl")] - [JsonConverter(typeof(JsonUrlEscapeConverter))] public string ServerAbrStreamingUrl { get; set; } = ""; - [JsonPropertyName("formats")] public List Formats { get; set; } = []; - [JsonPropertyName("adaptiveFormats")] public List AdaptiveFormats { get; set; } = []; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/StreamingFormat.cs b/Manager.YouTube/Models/Innertube/StreamingFormat.cs index 592738d..8d84adc 100644 --- a/Manager.YouTube/Models/Innertube/StreamingFormat.cs +++ b/Manager.YouTube/Models/Innertube/StreamingFormat.cs @@ -1,59 +1,31 @@ -using System.Text.Json.Serialization; - namespace Manager.YouTube.Models.Innertube; public class StreamingFormat { - [JsonPropertyName("itag")] public int Itag { get; set; } - [JsonPropertyName("url")] public string? Url { get; set; } - [JsonPropertyName("mimeType")] public string MimeType { get; set; } = ""; - [JsonPropertyName("bitrate")] public uint Bitrate { get; set; } - [JsonPropertyName("width")] public uint? Width { get; set; } - [JsonPropertyName("height")] public uint? Height { get; set; } - [JsonPropertyName("initRange")] public Range? InitRange { get; set; } - [JsonPropertyName("indexRange")] public Range? IndexRange { get; set; } - [JsonPropertyName("lastModified")] public long LastModified { get; set; } - [JsonPropertyName("contentLength")] public long ContentLength { get; set; } - [JsonPropertyName("quality")] public string Quality { get; set; } = ""; - [JsonPropertyName("xtags")] public string? Xtags { get; set; } - [JsonPropertyName("fps")] public uint Fps { get; set; } - [JsonPropertyName("qualityLabel")] public string QualityLabel { get; set; } = ""; - [JsonPropertyName("projectionType")] public string ProjectionType { get; set; } = ""; - [JsonPropertyName("averagebitrate")] public uint? AverageBitrate { get; set; } - [JsonPropertyName("highReplication")] public bool? HighReplication { get; set; } - [JsonPropertyName("colorInfo")] public ColorInfo? ColorInfo { get; set; } - [JsonPropertyName("audioQuality")] public string? AudioQuality { get; set; } = ""; - [JsonPropertyName("approxDurationMs")] public long ApproxDurationMs { get; set; } - [JsonPropertyName("audioSampleRate")] public int? AudioSampleRate { get; set; } - [JsonPropertyName("audioChannels")] public int? AudioChannels { get; set; } - [JsonPropertyName("loudnessDb")] public double? LoudnessDb { get; set; } - [JsonPropertyName("isDrc")] public bool? IsDrc { get; set; } - [JsonPropertyName("signatureCipher")] public string? SignatureCipher { get; set; } - [JsonPropertyName("qualityOrdinal")] public string QualityOrdinal { get; set; } = ""; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/WebImage.cs b/Manager.YouTube/Models/Innertube/WebImage.cs index 8cd8101..37e282b 100644 --- a/Manager.YouTube/Models/Innertube/WebImage.cs +++ b/Manager.YouTube/Models/Innertube/WebImage.cs @@ -1,13 +1,8 @@ -using System.Text.Json.Serialization; - namespace Manager.YouTube.Models.Innertube; public class WebImage { - [JsonPropertyName("width")] public int Width { get; set; } - [JsonPropertyName("height")] public int Height { get; set; } - [JsonPropertyName("url")] public string Url { get; set; } = ""; } \ No newline at end of file diff --git a/Manager.YouTube/Models/Parser/YouTubeVideoData.cs b/Manager.YouTube/Models/Parser/YouTubeVideoData.cs index 0aae6ce..190cea2 100644 --- a/Manager.YouTube/Models/Parser/YouTubeVideoData.cs +++ b/Manager.YouTube/Models/Parser/YouTubeVideoData.cs @@ -4,6 +4,6 @@ namespace Manager.YouTube.Models.Parser; public class YouTubeVideoData { - public JsonNode? YouTubePlayerData { get; set; } - public JsonNode? YouTubeInitialData { get; set; } + public JsonObject? YouTubePlayerData { get; set; } + public JsonObject? YouTubeInitialData { get; set; } } \ No newline at end of file diff --git a/Manager.YouTube/Models/Playlist/PlaylistVideo.cs b/Manager.YouTube/Models/Playlist/PlaylistVideo.cs deleted file mode 100644 index 279c67b..0000000 --- a/Manager.YouTube/Models/Playlist/PlaylistVideo.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Manager.YouTube.Models.Innertube; - -namespace Manager.YouTube.Models.Playlist; - -public class PlaylistVideo -{ - public required string VideoId { get; set; } - public List Thumbnails { get; set; } = []; - public required string Title { get; set; } - public required string Author { get; set; } - public long LengthSeconds { get; set; } - public bool IsPlayable { get; set; } -} \ No newline at end of file diff --git a/Manager.YouTube/Models/YouTubePlaylist.cs b/Manager.YouTube/Models/YouTubePlaylist.cs deleted file mode 100644 index 2d22272..0000000 --- a/Manager.YouTube/Models/YouTubePlaylist.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Manager.YouTube.Models.Playlist; - -namespace Manager.YouTube.Models; - -public class YouTubePlaylist -{ - public required string Id { get; set; } - public required string Title { get; set; } - public required string Description { get; set; } - public required string Owner { get; set; } - public required string OwnerId { get; set; } - public bool NoIndex { get; set; } - public bool Unlisted { get; set; } - public bool CanReorder { get; set; } - public bool IsEditable { get; set; } - public List Videos { get; set; } = []; - - public string? ContinuationToken { get; set; } -} \ No newline at end of file diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index c045c7c..2353946 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -8,7 +8,7 @@ public static class NetworkService public const string Origin = "https://www.youtube.com"; private static readonly HttpClient HttpClient = new(); - public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client, bool skipAuthenticationHeader = false, CancellationToken cancellationToken = default) + public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client, bool skipAuthenticationHeader = false) { request.Headers.Add("Origin", Origin); request.Headers.UserAgent.ParseAdd(client.UserAgent); @@ -19,8 +19,8 @@ public static class NetworkService try { - var response = await client.HttpClient.SendAsync(request, cancellationToken); - var contentString = await response.Content.ReadAsStringAsync(cancellationToken); + var response = await client.HttpClient.SendAsync(request); + var contentString = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { return ResultError.Fail(contentString); diff --git a/Manager.YouTube/Parsers/HtmlParser.cs b/Manager.YouTube/Parsers/HtmlParser.cs index 7200aba..cf22929 100644 --- a/Manager.YouTube/Parsers/HtmlParser.cs +++ b/Manager.YouTube/Parsers/HtmlParser.cs @@ -74,8 +74,8 @@ public static class HtmlParser { return new YouTubeVideoData { - YouTubePlayerData = parsedPlayerInitialData, - YouTubeInitialData = parsedInitialData + YouTubePlayerData = parsedPlayerInitialData?.AsObject(), + YouTubeInitialData = parsedInitialData?.AsObject() }; } catch (Exception e) diff --git a/Manager.YouTube/Parsers/Json/JsonParser.cs b/Manager.YouTube/Parsers/Json/JsonParser.cs index 8b4130e..def4fa8 100644 --- a/Manager.YouTube/Parsers/Json/JsonParser.cs +++ b/Manager.YouTube/Parsers/Json/JsonParser.cs @@ -12,58 +12,45 @@ public static class JsonParser .Select(image => new WebImage { Width = image.GetProperty("width").GetInt32(), Height = image.GetProperty("height").GetInt32(), Url = image.GetProperty("url").GetString() ?? "" }) .ToList(); - public static string ExtractTextOrHtml(JsonNode? node) + public static string ExtractTextOrHtml(JsonElement element) { - if (node is not JsonObject nodeObj) - { - return ""; - } - // Case 1: Simple text (no formatting) - if (nodeObj.TryGetPropertyValue("simpleText", out var simpleText)) - return simpleText?.GetValue() ?? string.Empty; + if (element.TryGetProperty("simpleText", out var simpleText)) + return simpleText.GetString() ?? string.Empty; // Case 2: Runs (formatted text segments) - if (nodeObj.TryGetPropertyValue("runs", out var runs) && runs != null && runs.GetValueKind() == JsonValueKind.Array) + if (element.TryGetProperty("runs", out var runs) && runs.ValueKind == JsonValueKind.Array) { var sb = new StringBuilder(); - foreach (var runNode in runs.AsArray()) + foreach (var run in runs.EnumerateArray()) { - if (runNode is not JsonObject run) - { - continue; - } - - var text = runNode["text"]?.GetValue() ?? string.Empty; + var text = run.GetProperty("text").GetString() ?? string.Empty; var formatted = System.Net.WebUtility.HtmlEncode(text); - var bold = run.TryGetPropertyValue("bold", out var boldNode) && boldNode is JsonValue bv && bv.GetValue(); - - var italic = run.TryGetPropertyValue("italic", out var italicNode) && italicNode is JsonValue iv && iv.GetValue(); - - var underline = run.TryGetPropertyValue("underline", out var underlineNode) && underlineNode is JsonValue uv && uv.GetValue(); - - var strikethrough = run.TryGetPropertyValue("strikethrough", out var strikeNode) && strikeNode is JsonValue sv && sv.GetValue(); + var bold = run.TryGetProperty("bold", out var boldProp) && boldProp.GetBoolean(); + var italic = run.TryGetProperty("italic", out var italicProp) && italicProp.GetBoolean(); + var underline = run.TryGetProperty("underline", out var underlineProp) && underlineProp.GetBoolean(); + var strikethrough = run.TryGetProperty("strikethrough", out var strikeProp) && strikeProp.GetBoolean(); if (bold) formatted = $"{formatted}"; if (italic) formatted = $"{formatted}"; if (underline) formatted = $"{formatted}"; if (strikethrough) formatted = $"{formatted}"; - if (run.TryGetPropertyValue("navigationEndpoint", out var nav) && nav is JsonObject navObj && - navObj.TryGetPropertyValue("url", out var urlProp)) + if (run.TryGetProperty("navigationEndpoint", out var nav) && + nav.TryGetProperty("url", out var urlProp)) { - var url = urlProp?.GetValue(); + var url = urlProp.GetString(); if (!string.IsNullOrEmpty(url)) formatted = $"{formatted}"; } - if (run.TryGetPropertyValue("emoji", out var emoji) && emoji is JsonObject emojiObj) + if (run.TryGetProperty("emoji", out var emoji) && emoji.ValueKind == JsonValueKind.Object) { - if (emojiObj.TryGetPropertyValue("url", out var emojiUrl)) + if (emoji.TryGetProperty("url", out var emojiUrl)) { - var src = emojiUrl?.GetValue(); + var src = emojiUrl.GetString(); if (!string.IsNullOrEmpty(src)) formatted = $"\"{text}\""; } @@ -77,15 +64,10 @@ public static class JsonParser return string.Empty; } - - public static List ExtractWebImages(JsonNode? node) + + public static List ExtractWebImages(JsonElement element) { - if (node == null) - { - return []; - } - - var thumbnailsArray = node["thumbnails"]; - return thumbnailsArray?.Deserialize>() ?? []; + var thumbnailsArray = element.GetProperty("thumbnail").GetProperty("thumbnails"); + return thumbnailsArray.Deserialize>() ?? []; } } \ No newline at end of file diff --git a/Manager.YouTube/Parsers/Json/VideoJsonParser.cs b/Manager.YouTube/Parsers/Json/VideoJsonParser.cs index 2f17fd9..eadcecb 100644 --- a/Manager.YouTube/Parsers/Json/VideoJsonParser.cs +++ b/Manager.YouTube/Parsers/Json/VideoJsonParser.cs @@ -14,7 +14,7 @@ public static class VideoJsonParser public static Result ParseVideoData(YouTubeVideoData videoData) { - if (videoData.YouTubePlayerData == null) + if (videoData.YouTubeInitialData == null || videoData.YouTubeInitialData.Count == 0) { return ResultError.Fail("No initial video data found!"); } @@ -22,7 +22,7 @@ public static class VideoJsonParser YouTubeVideo? video; try { - video = videoData.YouTubePlayerData.Deserialize(VideoParserOptions); + video = videoData.YouTubeInitialData.Deserialize(VideoParserOptions); } catch (Exception e) { diff --git a/Manager.YouTube/Util/Cipher/CipherDecoder.cs b/Manager.YouTube/Util/Cipher/CipherDecoder.cs index 9f43ae2..1dd39f3 100644 --- a/Manager.YouTube/Util/Cipher/CipherDecoder.cs +++ b/Manager.YouTube/Util/Cipher/CipherDecoder.cs @@ -8,6 +8,7 @@ namespace Manager.YouTube.Util.Cipher; public partial class CipherDecoder { public required string Version { get; init; } + public required string OriginalRelativeUrl { get; init; } public readonly IReadOnlySet Operations; private CipherDecoder(IEnumerable operations) @@ -24,6 +25,7 @@ public partial class CipherDecoder var operations = await GetCipherOperations(relativeUrl, client); var decoder = new CipherDecoder(operations) { + OriginalRelativeUrl = relativeUrl, Version = version }; return decoder; @@ -112,7 +114,7 @@ public partial class CipherDecoder } - [GeneratedRegex(@"([A-Za-z_$][A-Za-z0-9_$]*)=function\([A-Za-z_$][A-Za-z0-9_$]*\)\{\s*([A-Za-z_$][A-Za-z0-9_$]*)=\2\.split\(\x22\x22\);[\s\S]*?return\s+\2\.join\(\x22\x22\)\s*\}")] + [GeneratedRegex(@"(\w+)=function\(\w+\){(\w+)=\2\.split\(\x22{2}\);.*?return\s+\2\.join\(\x22{2}\)}")] private static partial Regex FunctionBodyRegex(); [GeneratedRegex("([\\$_\\w]+).\\w+\\(\\w+,\\d+\\);")] private static partial Regex DefinitionBodyRegex(); diff --git a/Manager.YouTube/Util/Cipher/CipherManager.cs b/Manager.YouTube/Util/Cipher/CipherManager.cs index 3a76d07..9900788 100644 --- a/Manager.YouTube/Util/Cipher/CipherManager.cs +++ b/Manager.YouTube/Util/Cipher/CipherManager.cs @@ -1,6 +1,5 @@ using DotBased.Logging; using DotBased.Monads; -using Manager.YouTube.Models.Innertube; namespace Manager.YouTube.Util.Cipher; @@ -9,9 +8,9 @@ public static class CipherManager private static readonly CipherDecoderCollection LoadedCiphers = []; private static readonly ILogger Logger = LogService.RegisterLogger(typeof(CipherManager)); - public static async Task> GetDecoderAsync(ClientState clientState, YouTubeClient? client = null) + public static async Task> GetDecoderAsync(YouTubeClient client) { - var relativePlayerJsUrl = clientState.PlayerJsUrl; + var relativePlayerJsUrl = client.State?.PlayerJsUrl; if (string.IsNullOrEmpty(relativePlayerJsUrl)) { return ResultError.Fail("Could not get player js url."); @@ -26,7 +25,7 @@ public static class CipherManager try { - var decoder = await CipherDecoder.CreateAsync(relativePlayerJsUrl, version, client); + var decoder = await CipherDecoder.CreateAsync(relativePlayerJsUrl, version); LoadedCiphers.Add(decoder); } catch (Exception e) @@ -39,7 +38,7 @@ public static class CipherManager private static string GetCipherVersion(string relativePlayerUrl) { - var split = relativePlayerUrl.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var split = relativePlayerUrl.Split('/'); var v = split[2]; var lang = split[4]; return $"{v}_{lang}"; diff --git a/Manager.YouTube/Util/Converters/JsonUrlEscapeConverter.cs b/Manager.YouTube/Util/Converters/JsonUrlEscapeConverter.cs deleted file mode 100644 index f5efd32..0000000 --- a/Manager.YouTube/Util/Converters/JsonUrlEscapeConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; - -namespace Manager.YouTube.Util.Converters; - -public partial class JsonUrlEscapeConverter : JsonConverter -{ - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var url = reader.GetString(); - if (string.IsNullOrWhiteSpace(url)) - { - return url; - } - - return UrlPatternRegex().IsMatch(url) ? Uri.UnescapeDataString(url) : url; - } - - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - writer.WriteStringValue(value); - } - - [GeneratedRegex("^(https?|ftp)://", RegexOptions.IgnoreCase | RegexOptions.Compiled, "nl-NL")] - private static partial Regex UrlPatternRegex(); -} \ No newline at end of file diff --git a/Manager.YouTube/Util/Converters/NumericJsonConverter.cs b/Manager.YouTube/Util/Converters/NumericJsonConverter.cs deleted file mode 100644 index 18e672e..0000000 --- a/Manager.YouTube/Util/Converters/NumericJsonConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Manager.YouTube.Util.Converters; - -public class NumericJsonConverter : JsonConverter where T : struct, IConvertible -{ - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - try - { - if (reader.TokenType == JsonTokenType.Number) - { - // Direct numeric value - return (T)Convert.ChangeType(reader.GetDouble(), typeof(T)); - } - - if (reader.TokenType == JsonTokenType.String) - { - var str = reader.GetString(); - if (string.IsNullOrWhiteSpace(str)) - throw new JsonException("Empty string cannot be converted to a number."); - - return (T)Convert.ChangeType(str, typeof(T)); - } - - throw new JsonException($"Unexpected token {reader.TokenType} for type {typeof(T)}."); - } - catch (Exception ex) - { - throw new JsonException($"Error converting value to {typeof(T)}.", ex); - } - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteNumberValue(Convert.ToDouble(value)); - } -} \ No newline at end of file diff --git a/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs b/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs index f6eea63..d7f73eb 100644 --- a/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs +++ b/Manager.YouTube/Util/Converters/YouTubeVideoJsonConverter.cs @@ -1,6 +1,5 @@ using System.Runtime.Serialization; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using DotBased.Logging; using Manager.YouTube.Models; @@ -12,74 +11,56 @@ namespace Manager.YouTube.Util.Converters; public class YouTubeVideoJsonConverter : JsonConverter { private readonly ILogger _logger = LogService.RegisterLogger(); - private readonly JsonSerializerOptions _serializerOptions = new() - { - Converters = { - new NumericJsonConverter(), - new NumericJsonConverter(), - new NumericJsonConverter(), - new NumericJsonConverter(), - new NumericJsonConverter() }, - PropertyNameCaseInsensitive = true - }; public override YouTubeVideo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var node = JsonNode.Parse(ref reader); - if (node == null) - { - throw new SerializationException("Failed to parse JSON reader."); - } + using var document = JsonDocument.ParseValue(ref reader); + var root = document.RootElement; - var rootObject = node.AsObject(); + var playabilityStatus = root.GetProperty("playabilityStatus"); + var streamingData = root.GetProperty("streamingData"); + var videoDetails = root.GetProperty("videoDetails"); + var playerConfigJson = root.GetProperty("playerConfig"); + var microformat = root.GetProperty("microformat").GetProperty("playerMicroformatRenderer"); - var playabilityStatus = rootObject["playabilityStatus"]; - var streamingDataJson = rootObject["streamingData"]; - var videoDetails = rootObject["videoDetails"]; - var playerConfigJson = rootObject["playerConfig"]; - var microformat = rootObject["microformat"]?["playerMicroformatRenderer"]; - - var videoId = videoDetails?["videoId"]?.GetValue() ?? microformat?["externalVideoId"]?.GetValue(); + var videoId = videoDetails.GetProperty("videoId").GetString() ?? microformat.GetProperty("externalVideoId").GetString(); if (string.IsNullOrEmpty(videoId)) { throw new SerializationException("Failed to get videoId"); } - - var thumbnails = JsonParser.ExtractWebImages(videoDetails?["thumbnail"]); - thumbnails.AddRange(JsonParser.ExtractWebImages(microformat?["thumbnail"])); - var streamingData = streamingDataJson.Deserialize(_serializerOptions); - var playerConfig = ExtractPlayerConfig(playerConfigJson); + var thumbnails = JsonParser.ExtractWebImages(videoDetails.GetProperty("thumbnail")); + thumbnails.AddRange(JsonParser.ExtractWebImages(microformat.GetProperty("thumbnail"))); var video = new YouTubeVideo { VideoId = videoId, - Title = JsonParser.ExtractTextOrHtml(microformat?["title"]), - Description = JsonParser.ExtractTextOrHtml(microformat?["description"]), - ViewCount = long.TryParse(microformat?["viewCount"]?.GetValue(), out var viewCountParsed) ? viewCountParsed : -1, - LikeCount = long.TryParse(microformat?["likeCount"]?.GetValue(), out var likeCountParsed) ? likeCountParsed : -1, - ChannelId = videoDetails?["channelId"]?.GetValue() ?? "", - Author = videoDetails?["author"]?.GetValue() ?? "", - PlayabilityStatus = playabilityStatus?["status"]?.GetValue() ?? "", - LengthSeconds = long.TryParse(videoDetails?["lengthSeconds"]?.GetValue(), out var lengthSecondsParsed) ? lengthSecondsParsed : -1, - Keywords = videoDetails?["keywords"]?.AsArray().Select(v => v?.GetValue() ?? "").ToArray() ?? [], - IsOwnerViewing = videoDetails?["isOwnerViewing"]?.GetValue() ?? false, - AllowRating = videoDetails?["allowRating"]?.GetValue() ?? false, - IsCrawlable = videoDetails?["isCrawlable"]?.GetValue() ?? false, - IsPrivate = videoDetails?["isPrivate"]?.GetValue() ?? false, - IsUnpluggedCorpus = videoDetails?["isUnpluggedCorpus"]?.GetValue() ?? false, - IsLive = videoDetails?["isLiveContent"]?.GetValue() ?? false, - IsFamilySave = microformat?["isFamilySave"]?.GetValue() ?? false, - AvailableCountries = microformat?["availableCountries"]?.AsArray().Select(v => v?.GetValue() ?? "").ToArray() ?? [], - IsUnlisted = microformat?["isUnlisted"]?.GetValue() ?? false, - HasYpcMetadata = microformat?["hasYpcMetadata"]?.GetValue() ?? false, - PublishDate = DateTime.TryParse(microformat?["publishDate"]?.GetValue(), out var parsedPublishDate) ? parsedPublishDate : DateTime.MinValue, - UploadDate = DateTime.TryParse(microformat?["uploadDate"]?.GetValue(), out var parsedUploadDate) ? parsedUploadDate : DateTime.MinValue, - IsShortsEligible = microformat?["isShortsEligible"]?.GetValue() ?? false, - Category = microformat?["category"]?.GetValue() ?? "", - StreamingData = streamingData, + Title = JsonParser.ExtractTextOrHtml(microformat.GetProperty("title")), + Description = JsonParser.ExtractTextOrHtml(microformat.GetProperty("description")), + ViewCount = videoDetails.GetProperty("viewCount").GetInt32(), + LikeCount = videoDetails.GetProperty("likeCount").GetInt32(), + ChannelId = videoDetails.GetProperty("channelId").GetString() ?? "", + Author = JsonParser.ExtractTextOrHtml(videoDetails.GetProperty("author")), + PlayabilityStatus = playabilityStatus.GetProperty("status").GetString() ?? "", + LengthSeconds = videoDetails.GetProperty("lengthSeconds").GetInt32(), + Keywords = videoDetails.GetProperty("keywords").EnumerateArray().Select(v => v.GetString()).Cast().ToArray(), + IsOwnerViewing = videoDetails.GetProperty("isOwnerViewing").GetBoolean(), + AllowRating = videoDetails.GetProperty("allowRating").GetBoolean(), + IsCrawlable = videoDetails.GetProperty("isCrawlable").GetBoolean(), + IsPrivate = videoDetails.GetProperty("isPrivate").GetBoolean(), + IsUnpluggedCorpus = videoDetails.GetProperty("isUnpluggedCorpus").GetBoolean(), + IsLive = videoDetails.GetProperty("isLiveContent").GetBoolean(), + IsFamilySave = microformat.GetProperty("isFamilySave").GetBoolean(), + AvailableCountries = microformat.GetProperty("availableCountries").EnumerateArray().Select(v => v.GetString()).Cast().ToArray(), + IsUnlisted = microformat.GetProperty("isUnlisted").GetBoolean(), + HasYpcMetadata = microformat.GetProperty("hasYpcMetadata").GetBoolean(), + PublishDate = microformat.GetProperty("publishDate").GetDateTime(), + UploadDate = microformat.GetProperty("uploadDate").GetDateTime(), + IsShortsEligible = microformat.GetProperty("isShortsEligible").GetBoolean(), + Category = microformat.GetProperty("category").GetString() ?? "", + StreamingData = streamingData.Deserialize(), Thumbnails = thumbnails, - PlayerConfig = playerConfig + PlayerConfig = ExtractPlayerConfig(playerConfigJson) }; return video; @@ -90,25 +71,23 @@ public class YouTubeVideoJsonConverter : JsonConverter throw new NotImplementedException("Converter only supports reading."); } - private PlayerConfig? ExtractPlayerConfig(JsonNode? playerConfigNode) + private PlayerConfig? ExtractPlayerConfig(JsonElement element) { - if (playerConfigNode == null) - { - return null; - } - try { - var playerConfigObj = playerConfigNode.AsObject(); var playerConfig = new PlayerConfig { - AudioLoudnessDb = playerConfigObj["audioConfig"]?["loudnessDb"]?.GetValue() ?? 0, - AudioPerceptualLoudnessDb = playerConfigObj["audioConfig"]?["perceptualLoudnessDb"]?.GetValue() ?? 0, - AudioEnablePerFormatLoudness = playerConfigObj["audioConfig"]?["enablePerFormatLoudness"]?.GetValue() ?? false, - MaxBitrate = uint.TryParse(playerConfigObj["streamSelectionConfig"]?["maxBitrate"]?.GetValue(), out var parsedMaxBitrate) ? parsedMaxBitrate : 0, - MaxReadAheadMediaTimeMs = playerConfigObj["mediaCommonConfig"]?["dynamicReadaheadConfig"]?["maxReadAheadMediaTimeMs"]?.GetValue() ?? 0, - MinReadAheadMediaTimeMs = playerConfigObj["mediaCommonConfig"]?["dynamicReadaheadConfig"]?["minReadAheadMediaTimeMs"]?.GetValue() ?? 0, - ReadAheadGrowthRateMs = playerConfigObj["mediaCommonConfig"]?["dynamicReadaheadConfig"]?["readAheadGrowthRateMs"]?.GetValue() ?? 0, + AudioLoudnessDb = element.GetProperty("audioConfig").GetProperty("loudnessDb").GetDouble(), + AudioPerceptualLoudnessDb = element.GetProperty("audioConfig").GetProperty("perceptualLoudnessDb").GetDouble(), + AudioEnablePerFormatLoudness = element.GetProperty("audioConfig").GetProperty("enablePerFormatLoudness") + .GetBoolean(), + MaxBitrate = element.GetProperty("streamSelectionConfig").GetProperty("maxBitrate").GetUInt32(), + MaxReadAheadMediaTimeMs = element.GetProperty("mediaCommonConfig").GetProperty("dynamicReadaheadConfig") + .GetProperty("maxReadAheadMediaTimeMs").GetUInt32(), + MinReadAheadMediaTimeMs = element.GetProperty("mediaCommonConfig").GetProperty("dynamicReadaheadConfig") + .GetProperty("minReadAheadMediaTimeMs").GetUInt32(), + ReadAheadGrowthRateMs = element.GetProperty("mediaCommonConfig").GetProperty("dynamicReadaheadConfig") + .GetProperty("readAheadGrowthRateMs").GetUInt32(), }; return playerConfig; } diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index dbbd1fb..b8369fc 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -9,14 +9,15 @@ using Manager.YouTube.Models; using Manager.YouTube.Models.Innertube; using Manager.YouTube.Parsers; using Manager.YouTube.Parsers.Json; +using Manager.YouTube.Util.Cipher; namespace Manager.YouTube; public sealed class YouTubeClient : IDisposable { public string Id { get; private set; } = ""; - public string UserAgent { get; private set; } - public bool IsAnonymous { get; private set; } + public string? UserAgent { get; set; } + public bool IsAnonymous { get; } public CookieContainer CookieContainer { get; } = new() { PerDomainCapacity = 50 }; public ClientState? State { get; private set; } public List DatasyncIds { get; } = []; @@ -51,7 +52,7 @@ public sealed class YouTubeClient : IDisposable /// /// The cookies to use for making requests. Empty collection or null for anonymous requests. /// The user agent to use for the requests. Only WEB client is supported. - /// The logger that the client is going to use, null will create a new logger. + /// The logger that the client is going to use, if null will create a new logger. /// public static async Task> CreateAsync(CookieCollection? cookies, string userAgent, ILogger? logger = null) { @@ -67,57 +68,114 @@ public sealed class YouTubeClient : IDisposable return client; } - public void SetUserAgent(string userAgent) + private HttpClientHandler GetHttpClientHandler() { - if (string.IsNullOrWhiteSpace(userAgent)) + var clientHandler = new HttpClientHandler { - _logger.Warning("UserAgent cannot be null or empty!"); - return; - } - UserAgent = userAgent; + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, + UseCookies = true, + CookieContainer = CookieContainer + }; + return clientHandler; } - public async Task> GetVideoByIdAsync(string videoId, CancellationToken cancellationToken = default) + internal async Task FetchClientDataAsync() { - if (string.IsNullOrWhiteSpace(videoId)) + if (State is not { LoggedIn: true }) { - return ResultError.Fail("Video id is empty!"); + var state = await GetClientStateAsync(); + if (!state.IsSuccess) + { + return state; + } } - var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/watch?v={videoId}")); - - var videoResponse = await NetworkService.MakeRequestAsync(request, this, true, cancellationToken); - if (!videoResponse.IsSuccess && !string.IsNullOrWhiteSpace(videoResponse.Value)) + if (string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) { - return videoResponse.Error ?? ResultError.Fail("Request failed!"); + var datasyncResult = await GetDatasyncIdsAsync(); + if (!datasyncResult.IsSuccess) + { + return datasyncResult; + } + + foreach (var id in datasyncResult.Value) + { + if (DatasyncIds.Contains(id)) + continue; + DatasyncIds.Add(id); + } } - var html = videoResponse.Value; - - var stateResult = GetClientStateFromHtml(html); - var state = stateResult.Value; - if (!stateResult.IsSuccess && State != null) + if (string.IsNullOrWhiteSpace(Id)) { - state = State; - } - - var htmlParseResult = HtmlParser.GetVideoDataFromHtml(html); - if (!htmlParseResult.IsSuccess) - { - return htmlParseResult.Error ?? ResultError.Fail("Failed to parse HTML video data!"); - } - - var videoParseResult = VideoJsonParser.ParseVideoData(htmlParseResult.Value); - if (!videoParseResult.IsSuccess) - { - return videoParseResult; - } - - //await DecipherSignaturesAsync(videoParseResult.Value, state); + var accountInfoResult = await GetCurrentAccountIdAsync(); + if (!accountInfoResult.IsSuccess) + { + return accountInfoResult; + } - return videoParseResult.Value; + Id = accountInfoResult.Value; + } + + return Result.Success(); } + private async Task GetClientStateAsync() + { + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(NetworkService.Origin) + }; + + var result = await NetworkService.MakeRequestAsync(httpRequest, this, true); + if (!result.IsSuccess) + { + return result.Error ?? ResultError.Fail("Request failed!"); + } + + var stateResult = SetClientStateFromHtml(result.Value); + if (!stateResult.IsSuccess) + { + return stateResult; + } + + var cookieRotationResult = await RotateCookiesPageAsync(); + return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success(); + } + + private Result SetClientStateFromHtml(string html) + { + if (string.IsNullOrWhiteSpace(html)) + { + return ResultError.Fail("HTML is empty!!"); + } + + var clientStateResult = HtmlParser.GetStateJson(html); + if (clientStateResult is { IsSuccess: false, Error: not null }) + { + return clientStateResult.Error; + } + + try + { + State = JsonSerializer.Deserialize(clientStateResult.Value.Item1); + } + catch (Exception e) + { + return ResultError.Error(e, "Error while parsing JSON!"); + } + + if (State == null) + { + return ResultError.Fail("Unable to parse client state!"); + } + + State.IsPremiumUser = clientStateResult.Value.Item2; + + return Result.Success(); + } + public async Task> GetChannelByIdAsync(string channelId) { if (State == null) @@ -198,137 +256,11 @@ public sealed class YouTubeClient : IDisposable var rotateRequest = new HttpRequestMessage(HttpMethod.Post, new Uri("https://accounts.youtube.com/RotateCookies")); return await NetworkService.MakeRequestAsync(rotateRequest, this, true); } - + public void Dispose() { HttpClient.Dispose(); } - - private async Task FetchClientDataAsync() - { - if (State is not { LoggedIn: true }) - { - var stateResult = await GetClientStateAsync(); - if (!stateResult.IsSuccess) - { - return stateResult; - } - } - - if (string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) - { - var datasyncResult = await GetDatasyncIdsAsync(); - if (!datasyncResult.IsSuccess) - { - return datasyncResult; - } - - foreach (var id in datasyncResult.Value) - { - if (DatasyncIds.Contains(id)) - continue; - DatasyncIds.Add(id); - } - } - - if (string.IsNullOrWhiteSpace(Id)) - { - var accountInfoResult = await GetCurrentAccountIdAsync(); - if (!accountInfoResult.IsSuccess) - { - return accountInfoResult; - } - - Id = accountInfoResult.Value; - } - - return Result.Success(); - } - - private HttpClientHandler GetHttpClientHandler() - { - var clientHandler = new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - UseCookies = true, - CookieContainer = CookieContainer - }; - return clientHandler; - } - - private async Task GetClientStateAsync() - { - var httpRequest = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(NetworkService.Origin) - }; - - var result = await NetworkService.MakeRequestAsync(httpRequest, this, true); - if (!result.IsSuccess) - { - return result.Error ?? ResultError.Fail("Request failed!"); - } - - var stateResult = SetClientStateFromHtml(result.Value); - if (!stateResult.IsSuccess) - { - return stateResult; - } - - if (State is { LoggedIn: false }) - { - _logger.Warning("Client is not logged in!"); - return ResultError.Fail("Client login failed!"); - } - - var cookieRotationResult = await RotateCookiesPageAsync(); - return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success(); - } - - private Result SetClientStateFromHtml(string html) - { - if (string.IsNullOrWhiteSpace(html)) - { - return ResultError.Fail("HTML is empty!!"); - } - - var clientStateResult = GetClientStateFromHtml(html); - if (clientStateResult is { IsSuccess: false, Error: not null }) - { - return clientStateResult.Error; - } - - State = clientStateResult.Value; - IsAnonymous = !State.LoggedIn; - - return Result.Success(); - } - - private Result GetClientStateFromHtml(string html) - { - var clientStateResult = HtmlParser.GetStateJson(html); - if (clientStateResult is { IsSuccess: false, Error: not null }) - { - return clientStateResult.Error; - } - - ClientState? state; - try - { - state = JsonSerializer.Deserialize(clientStateResult.Value.Item1); - if (state != null) - { - state.IsPremiumUser = clientStateResult.Value.Item2; - } - } - catch (Exception e) - { - return ResultError.Error(e, "Error while parsing JSON!"); - } - - return state == null ? ResultError.Fail("Unable to parse client state!") : state; - } private async Task> GetCurrentAccountIdAsync() { @@ -385,7 +317,47 @@ public sealed class YouTubeClient : IDisposable return ResultError.Fail("Failed to get datasyncIds! Client not logged in."); } - /*private async Task DecipherSignaturesAsync(YouTubeVideo video, ClientState state) + public async Task> GetVideoByIdAsync(string videoId, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(videoId)) + { + return ResultError.Fail("Video id is empty!"); + } + + var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/watch?v={videoId}")); + + var response = await NetworkService.MakeRequestAsync(request, this); + if (!response.IsSuccess && !string.IsNullOrWhiteSpace(response.Value)) + { + return response.Error ?? ResultError.Fail("Request failed!"); + } + + var html = response.Value; + + var stateResult = SetClientStateFromHtml(html); + if (!stateResult.IsSuccess) + { + _logger.Warning("Failed to update client state."); + } + + var htmlParseResult = HtmlParser.GetVideoDataFromHtml(html); + if (!htmlParseResult.IsSuccess) + { + return htmlParseResult.Error ?? ResultError.Fail("Failed to parse HTML video data!"); + } + + var videoParseResult = VideoJsonParser.ParseVideoData(htmlParseResult.Value); + if (!videoParseResult.IsSuccess) + { + return videoParseResult; + } + + await DecipherSignatures(videoParseResult.Value); + + return videoParseResult.Value; + } + + private async Task DecipherSignatures(YouTubeVideo video) { var streamingData = video.StreamingData; if (streamingData == null) @@ -393,40 +365,25 @@ public sealed class YouTubeClient : IDisposable _logger.Debug("No streaming data available, skipping decipher."); return; } - - if (string.IsNullOrWhiteSpace(state.PlayerJsUrl)) - { - _logger.Warning("No player js url found."); - } var formatsWithCipher = streamingData.Formats.Concat(streamingData.AdaptiveFormats).Where(x => !string.IsNullOrWhiteSpace(x.SignatureCipher)).ToList(); if (formatsWithCipher.Count == 0) { - _logger.Debug("Skipping signature decipher, no signatures found to decipher."); - } - - if (string.IsNullOrWhiteSpace(streamingData.ServerAbrStreamingUrl)) - { - _logger.Warning("No ABR streaming url available."); - } - - var abrStreamUri = new Uri(streamingData.ServerAbrStreamingUrl); - var queries = HttpUtility.ParseQueryString(abrStreamUri.Query); - var nSig = queries.Get("n"); - - if (string.IsNullOrWhiteSpace(nSig)) - { - _logger.Warning("No N signature found."); - } - - /*var jsEngineResult = await JavaScriptEngineManager.GetPlayerEngine(state.PlayerJsUrl ?? ""); - if (!jsEngineResult.IsSuccess) - { - _logger.Warning(jsEngineResult.Error?.Description ?? "Failed to get player script engine."); + _logger.Debug("Skipping decipher, no signatures found to decipher."); return; } - var engine = jsEngineResult.Value; - engine.InitializePlayer();#1# - }*/ + var decipherDecoderResult = await CipherManager.GetDecoderAsync(this); + if (!decipherDecoderResult.IsSuccess) + { + _logger.Warning(decipherDecoderResult.Error?.Description ?? "Failed to get the cipher decoder!"); + return; + } + var decoder = decipherDecoderResult.Value; + + foreach (var format in formatsWithCipher) + { + format.Url = decoder.Decipher(format.SignatureCipher); + } + } } \ No newline at end of file