From b2c9fc2c527f3453a20d1a665a366934c54cc25e Mon Sep 17 00:00:00 2001 From: max Date: Mon, 8 Sep 2025 01:40:43 +0200 Subject: [PATCH] [CHANGE] Finished impl required data for client --- .../Components/Dialogs/AccountDialog.razor | 23 ++- Manager.App/Services/System/ClientManager.cs | 16 ++ Manager.YouTube/Models/ClientExternalData.cs | 2 +- Manager.YouTube/Models/ClientInformation.cs | 9 - .../Innertube/{ChannelFetch.cs => Channel.cs} | 3 +- .../Models/Innertube/ClientState.cs | 1 + Manager.YouTube/NetworkService.cs | 182 ++---------------- .../Parsers/Json/ChannelJsonParser.cs | 40 ++-- Manager.YouTube/YouTubeClient.cs | 145 +++++++++++++- Manager.YouTube/YouTubeService.cs | 6 - 10 files changed, 199 insertions(+), 228 deletions(-) delete mode 100644 Manager.YouTube/Models/ClientInformation.cs rename Manager.YouTube/Models/Innertube/{ChannelFetch.cs => Channel.cs} (86%) delete mode 100644 Manager.YouTube/YouTubeService.cs diff --git a/Manager.App/Components/Dialogs/AccountDialog.razor b/Manager.App/Components/Dialogs/AccountDialog.razor index 3992946..f75e676 100644 --- a/Manager.App/Components/Dialogs/AccountDialog.razor +++ b/Manager.App/Components/Dialogs/AccountDialog.razor @@ -20,11 +20,11 @@ Account name: - @Client.External.Information.AccountName + @Client.External.Channel?.ChannelName Account handle: - @Client.External.Information.AccountHandle + @Client.External.Channel?.Handle Logged in: @@ -32,17 +32,13 @@ YouTube Premium: - @Client.External.Information.IsPremiumUser + @Client.External.State?.IsPremiumUser User agent: @Client.UserAgent - - InnerTube API key: - @Client.External.State?.InnertubeApiKey - InnerTube client: @Client.External.State?.InnerTubeClient @@ -51,16 +47,23 @@ InnerTube client version: @Client.External.State?.InnerTubeClientVersion + + InnerTube API key: + @Client.External.State?.InnertubeApiKey + Language: @Client.External.State?.InnerTubeContext?.InnerTubeClient?.HLanguage - @*@if (!string.IsNullOrWhiteSpace(Client.AccountImage)) + @{ + var avatar = Client.External.Channel?.AvatarImages.FirstOrDefault(); + } + @if (avatar != null) { - - }*@ + + } diff --git a/Manager.App/Services/System/ClientManager.cs b/Manager.App/Services/System/ClientManager.cs index 759b810..91a7cc8 100644 --- a/Manager.App/Services/System/ClientManager.cs +++ b/Manager.App/Services/System/ClientManager.cs @@ -1,3 +1,4 @@ +using DotBased.Monads; using Manager.YouTube; namespace Manager.App.Services.System; @@ -17,4 +18,19 @@ public class ClientManager : BackgroundService { // Clear up } + + public async Task SaveClientAsync(YouTubeClient client) + { + return ResultError.Fail("Not implemented"); + } + + public async Task> LoadClientByIdAsync(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return ResultError.Fail("Client ID is empty!"); + } + + return ResultError.Fail("Not implemented"); + } } \ No newline at end of file diff --git a/Manager.YouTube/Models/ClientExternalData.cs b/Manager.YouTube/Models/ClientExternalData.cs index 4674e0c..36d2128 100644 --- a/Manager.YouTube/Models/ClientExternalData.cs +++ b/Manager.YouTube/Models/ClientExternalData.cs @@ -5,7 +5,7 @@ namespace Manager.YouTube.Models; public class ClientExternalData { public ClientState? State { get; set; } - public ClientInformation Information { get; set; } = new(); + public Channel? Channel { get; set; } public List DatasyncIds { get; set; } = []; public string GetDatasyncId() diff --git a/Manager.YouTube/Models/ClientInformation.cs b/Manager.YouTube/Models/ClientInformation.cs deleted file mode 100644 index 607cf9e..0000000 --- a/Manager.YouTube/Models/ClientInformation.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Manager.YouTube.Models; - -public class ClientInformation -{ - public string? AccountName { get; set; } - public string? AccountHandle { get; set; } - public string? Description { get; set; } - public bool IsPremiumUser { get; set; } -} \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/ChannelFetch.cs b/Manager.YouTube/Models/Innertube/Channel.cs similarity index 86% rename from Manager.YouTube/Models/Innertube/ChannelFetch.cs rename to Manager.YouTube/Models/Innertube/Channel.cs index 149deea..ed65f70 100644 --- a/Manager.YouTube/Models/Innertube/ChannelFetch.cs +++ b/Manager.YouTube/Models/Innertube/Channel.cs @@ -1,10 +1,11 @@ namespace Manager.YouTube.Models.Innertube; -public class ChannelFetch +public class Channel { public bool NoIndex { get; set; } public bool Unlisted { get; set; } public bool FamilySafe { get; set; } + public string? ChannelName { get; set; } public string? Handle { get; set; } public string? Description { get; set; } public List AvailableCountries { get; set; } = []; diff --git a/Manager.YouTube/Models/Innertube/ClientState.cs b/Manager.YouTube/Models/Innertube/ClientState.cs index bc131c0..3f1d912 100644 --- a/Manager.YouTube/Models/Innertube/ClientState.cs +++ b/Manager.YouTube/Models/Innertube/ClientState.cs @@ -4,6 +4,7 @@ namespace Manager.YouTube.Models.Innertube; public class ClientState : AdditionalJsonData { + public bool IsPremiumUser { get; set; } [JsonPropertyName("INNERTUBE_API_KEY")] public string? InnertubeApiKey { get; set; } diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index 2826f75..e4bcf87 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -1,200 +1,40 @@ -using System.Net.Mime; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; using DotBased.Monads; -using Manager.YouTube.Models.Innertube; -using Manager.YouTube.Parsers; -using Manager.YouTube.Parsers.Json; using Manager.YouTube.Util; namespace Manager.YouTube; public static class NetworkService { - private const string Origin = "https://www.youtube.com"; - - public static async Task> GetClientStateAsync(YouTubeClient client) - { - var httpRequest = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(Origin) - }; - httpRequest.Headers.Clear(); - httpRequest.Headers.UserAgent.ParseAdd(client.UserAgent); - - var http = client.GetHttpClient(); - if (http == null) - { - return ResultError.Fail("Unable to get http client!"); - } - - var response = await http.SendAsync(httpRequest); - if (!response.IsSuccessStatusCode) - { - var responseResult = await response.Content.ReadAsStringAsync(); - return ResultError.Fail(responseResult); - } - var responseHtml = await response.Content.ReadAsStringAsync(); - var clientStateResult = HtmlParser.GetStateJson(responseHtml); - if (clientStateResult is { IsSuccess: false, Error: not null }) - { - return clientStateResult.Error; - } + public const string Origin = "https://www.youtube.com"; - ClientState? clientState; - try - { - clientState = JsonSerializer.Deserialize(clientStateResult.Value.Item1); - } - catch (Exception e) - { - return ResultError.Error(e, "Error while parsing JSON!"); - } - - client.External.Information.IsPremiumUser = clientStateResult.Value.Item2; - - return clientState == null ? ResultError.Fail("Unable to parse client state!") : clientState; - } - - public static async Task> GetDatasyncIds(YouTubeClient client) + public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client) { - if (client.External.State is not { LoggedIn: true } || client.CookieContainer.Count == 0) + request.Headers.Add("Origin", Origin); + request.Headers.UserAgent.ParseAdd(client.UserAgent); + if (client.SapisidCookie != null) { - return ResultError.Fail("Client is not logged in, requires logged in client for this endpoint (/getDatasyncIdsEndpoint)."); + request.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.External.GetDatasyncId(), client.SapisidCookie.Value, Origin); } var httpClient = client.GetHttpClient(); if (httpClient == null) { - return ResultError.Fail("Unable to get http client!"); + return ResultError.Fail("Failed getting http client!"); } - var httpRequest = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri($"{Origin}/getDatasyncIdsEndpoint") - }; - httpRequest.Headers.UserAgent.ParseAdd(client.UserAgent); - httpRequest.Headers.Add("Origin", Origin); - - var response = await httpClient.SendAsync(httpRequest); - if (!response.IsSuccessStatusCode) - { - var responseResult = await response.Content.ReadAsStringAsync(); - return ResultError.Fail(responseResult); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var datasyncIdsJson = JsonNode.Parse(responseContent.Replace(")]}'", "")); - - var isLoggedOut = datasyncIdsJson?["responseContext"]?["mainAppWebResponseContext"]?["loggedOut"] - .Deserialize() ?? true; - if (!isLoggedOut) - { - return datasyncIdsJson?["datasyncIds"].Deserialize() ?? []; - } - - return ResultError.Fail("Failed to get datasyncIds!"); - } - - public static async Task> GetCurrentAccountIdAsync(YouTubeClient client) - { - if (client.External.State is not { LoggedIn: true }) - { - return ResultError.Fail("Client not logged in!"); - } - - var httpRequest = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri($"{Origin}/youtubei/v1/account/account_menu") - }; - httpRequest.Headers.UserAgent.ParseAdd(client.UserAgent); - httpRequest.Headers.Add("Origin", Origin); - - if (client.SapisidCookie != null) - { - httpRequest.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.External.GetDatasyncId(), client.SapisidCookie.Value, Origin); - } - - var serializedContext = JsonSerializer.SerializeToNode(client.External.State.InnerTubeContext); - var payload = new JsonObject { { "context", serializedContext } }; - httpRequest.Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json); - - var http = client.GetHttpClient(); - if (http == null) - { - return ResultError.Fail("Unable to get http client!"); - } - - string json; try { - var response = await http.SendAsync(httpRequest); + var response = await httpClient.SendAsync(request); + var contentString = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { - var responseResult = await response.Content.ReadAsStringAsync(); - return ResultError.Fail(responseResult); + return ResultError.Fail(contentString); } - - json = await response.Content.ReadAsStringAsync(); + return contentString; } catch (Exception e) { return ResultError.Error(e); } - - return JsonAccountParser.ParseAccountId(json); } - - public static async Task> GetChannelAsync(string channelId, YouTubeClient client) - { - if (client.External.State == null) - { - return ResultError.Fail("No client state!"); - } - - if (string.IsNullOrWhiteSpace(channelId)) - { - return ResultError.Fail("Channel id is empty!"); - } - - var httpClient = client.GetHttpClient(); - if (httpClient == null) - { - return ResultError.Fail("Unable to get http client!"); - } - - var serializedContext = JsonSerializer.SerializeToNode(client.External.State.InnerTubeContext); - var payload = new JsonObject { { "context", serializedContext }, { "browseId", channelId } }; - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri($"{Origin}/youtubei/v1/browse?key={client.External.State.InnertubeApiKey}"), - Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json) - }; - requestMessage.Headers.UserAgent.ParseAdd(client.UserAgent); - requestMessage.Headers.Add("Origin", Origin); - - if (client.SapisidCookie != null) - { - requestMessage.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.External.GetDatasyncId(), client.SapisidCookie.Value, Origin); - } - - var response = await httpClient.SendAsync(requestMessage); - if (!response.IsSuccessStatusCode) - { - var responseResult = await response.Content.ReadAsStringAsync(); - return ResultError.Fail(responseResult); - } - - var jsonContent = await response.Content.ReadAsStringAsync(); - var parsed = ChannelJsonParser.ParseJsonToChannelData(jsonContent); - - return ResultError.Fail("Not implemented!"); - } - - } \ No newline at end of file diff --git a/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs b/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs index 6a4172a..7c528dd 100644 --- a/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs +++ b/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs @@ -9,25 +9,26 @@ namespace Manager.YouTube.Parsers.Json; /// public static class ChannelJsonParser { - public static Result ParseJsonToChannelData(string json) + public static Result ParseJsonToChannelData(string json) { try { + var channel = new Channel(); var doc = JsonDocument.Parse(json); var rootDoc = doc.RootElement; var microformat = rootDoc.GetProperty("microformat").GetProperty("microformatDataRenderer"); - var availableCountries = microformat + channel.AvailableCountries = microformat .GetProperty("availableCountries") .EnumerateArray() .Select(e => e.GetString()) - .ToList(); - - var description = microformat.GetProperty("description").GetString(); - var noIndex = microformat.GetProperty("noindex").GetBoolean(); - var unlisted = microformat.GetProperty("unlisted").GetBoolean(); - var familySafe = microformat.GetProperty("familySafe").GetBoolean(); + .OfType().ToList(); + channel.Description = microformat.GetProperty("description").GetString(); + channel.NoIndex = microformat.GetProperty("noindex").GetBoolean(); + channel.Unlisted = microformat.GetProperty("unlisted").GetBoolean(); + channel.FamilySafe = microformat.GetProperty("familySafe").GetBoolean(); + channel.ChannelName = microformat.GetProperty("title").GetString(); var avatarThumbnails = rootDoc .GetProperty("metadata") @@ -35,15 +36,14 @@ public static class ChannelJsonParser .GetProperty("avatar") .GetProperty("thumbnails") .EnumerateArray(); - - var avatars = JsonParser.ParseImages(avatarThumbnails); + channel.AvatarImages = JsonParser.ParseImages(avatarThumbnails); var headerContent = rootDoc .GetProperty("header") .GetProperty("pageHeaderRenderer") .GetProperty("content"); - var metadataPartHandle = headerContent + channel.Handle = headerContent .GetProperty("pageHeaderViewModel") .GetProperty("metadata") .GetProperty("contentMetadataViewModel") @@ -63,21 +63,9 @@ public static class ChannelJsonParser .GetProperty("image") .GetProperty("sources") .EnumerateArray(); - - var banners = JsonParser.ParseImages(bannerImages); - - var resultFetch = new ChannelFetch - { - NoIndex = noIndex, - Unlisted = unlisted, - FamilySafe = familySafe, - Handle = metadataPartHandle, - Description = description, - AvailableCountries = availableCountries.OfType().ToList(), - AvatarImages = avatars, - BannerImages = banners - }; - return resultFetch; + channel.BannerImages = JsonParser.ParseImages(bannerImages); + + return channel; } catch (Exception e) { diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index 686d3b5..e402ea5 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -1,6 +1,13 @@ using System.Net; +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using DotBased.Monads; using Manager.YouTube.Models; +using Manager.YouTube.Models.Innertube; +using Manager.YouTube.Parsers; +using Manager.YouTube.Parsers.Json; namespace Manager.YouTube; @@ -38,7 +45,7 @@ public sealed class YouTubeClient : IDisposable { if (External.State is not { LoggedIn: true }) { - var state = await NetworkService.GetClientStateAsync(this); + var state = await GetClientStateAsync(); if (!state.IsSuccess) { return state; @@ -48,7 +55,7 @@ public sealed class YouTubeClient : IDisposable if (string.IsNullOrWhiteSpace(External.State.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) { - var datasyncResult = await NetworkService.GetDatasyncIds(this); + var datasyncResult = await GetDatasyncIds(); if (!datasyncResult.IsSuccess) { return datasyncResult; @@ -64,7 +71,7 @@ public sealed class YouTubeClient : IDisposable if (string.IsNullOrWhiteSpace(Id)) { - var accountInfoResult = await NetworkService.GetCurrentAccountIdAsync(this); + var accountInfoResult = await GetCurrentAccountIdAsync(); if (!accountInfoResult.IsSuccess) { return accountInfoResult; @@ -73,13 +80,143 @@ public sealed class YouTubeClient : IDisposable Id = accountInfoResult.Value; } - var accountInfo = await NetworkService.GetChannelAsync(Id, this); + var channelResult = await GetChannelByIdAsync(Id); + if (!channelResult.IsSuccess) + { + return channelResult.Error ?? ResultError.Fail("Failed to get channel."); + } + External.Channel = channelResult.Value; return Result.Success(); } + + public async Task> GetClientStateAsync() + { + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(NetworkService.Origin) + }; + + var result = await NetworkService.MakeRequestAsync(httpRequest, this); + if (!result.IsSuccess) + { + return result.Error ?? ResultError.Fail("Request failed!"); + } + + var clientStateResult = HtmlParser.GetStateJson(result.Value); + if (clientStateResult is { IsSuccess: false, Error: not null }) + { + return clientStateResult.Error; + } + + ClientState? clientState; + try + { + clientState = JsonSerializer.Deserialize(clientStateResult.Value.Item1); + } + catch (Exception e) + { + return ResultError.Error(e, "Error while parsing JSON!"); + } + + + if (clientState == null) + { + return ResultError.Fail("Unable to parse client state!"); + } + + clientState.IsPremiumUser = clientStateResult.Value.Item2; + + return clientState; + } + + public async Task> GetChannelByIdAsync(string channelId) + { + if (External.State == null) + { + return ResultError.Fail("No client state!"); + } + + if (string.IsNullOrWhiteSpace(channelId)) + { + return ResultError.Fail("Channel id is empty!"); + } + + var serializedContext = JsonSerializer.SerializeToNode(External.State.InnerTubeContext); + var payload = new JsonObject { { "context", serializedContext }, { "browseId", channelId } }; + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{NetworkService.Origin}/youtubei/v1/browse?key={External.State.InnertubeApiKey}"), + Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json) + }; + var responseResult = await NetworkService.MakeRequestAsync(requestMessage, this); + if (!responseResult.IsSuccess) + { + return responseResult.Error ?? ResultError.Fail("Request failed!"); + } + + return ChannelJsonParser.ParseJsonToChannelData(responseResult.Value); + } public void Dispose() { _httpClient?.Dispose(); } + + private async Task> GetCurrentAccountIdAsync() + { + if (External.State is not { LoggedIn: true }) + { + return ResultError.Fail("Client not logged in!"); + } + + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{NetworkService.Origin}/youtubei/v1/account/account_menu") + }; + var serializedContext = JsonSerializer.SerializeToNode(External.State.InnerTubeContext); + var payload = new JsonObject { { "context", serializedContext } }; + httpRequest.Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json); + var responseResult = await NetworkService.MakeRequestAsync(httpRequest, this); + + if (!responseResult.IsSuccess) + { + return responseResult.Error ?? ResultError.Fail("Request failed!"); + } + + return JsonAccountParser.ParseAccountId(responseResult.Value); + } + + private async Task> GetDatasyncIds() + { + if (External.State is not { LoggedIn: true } || CookieContainer.Count == 0) + { + return ResultError.Fail("Client is not logged in, requires logged in client for this endpoint (/getDatasyncIdsEndpoint)."); + } + + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri($"{NetworkService.Origin}/getDatasyncIdsEndpoint") + }; + var responseResult = await NetworkService.MakeRequestAsync(httpRequest, this); + if (!responseResult.IsSuccess) + { + return responseResult.Error ?? ResultError.Fail("Request failed!"); + } + + var datasyncIdsJson = JsonNode.Parse(responseResult.Value.Replace(")]}'", "")); + + var isLoggedOut = datasyncIdsJson?["responseContext"]?["mainAppWebResponseContext"]?["loggedOut"] + .Deserialize() ?? true; + if (!isLoggedOut) + { + return datasyncIdsJson?["datasyncIds"].Deserialize() ?? []; + } + + return ResultError.Fail("Failed to get datasyncIds! Client not logged in."); + } } \ No newline at end of file diff --git a/Manager.YouTube/YouTubeService.cs b/Manager.YouTube/YouTubeService.cs deleted file mode 100644 index 14c398c..0000000 --- a/Manager.YouTube/YouTubeService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Manager.YouTube; - -public class YouTubeService() -{ - -} \ No newline at end of file