diff --git a/Manager.App/Components/Dialogs/AccountDialog.razor.cs b/Manager.App/Components/Dialogs/AccountDialog.razor.cs index b174a35..16c6f64 100644 --- a/Manager.App/Components/Dialogs/AccountDialog.razor.cs +++ b/Manager.App/Components/Dialogs/AccountDialog.razor.cs @@ -71,14 +71,12 @@ namespace Manager.App.Components.Dialogs var name = parts[0].Trim(); var value = parts[1].Trim(); - // Escape invalid characters - var safeName = Uri.EscapeDataString(name); - var safeValue = Uri.EscapeDataString(value); - - var cookie = new Cookie(safeName, safeValue); + var cookie = new Cookie(name, value); if (!string.IsNullOrEmpty(domain)) cookie.Domain = domain; + cookie.Expires = DateTime.Now.AddDays(1); + cookie.Path = "/"; collection.Add(cookie); } @@ -105,7 +103,7 @@ namespace Manager.App.Components.Dialogs private async Task ValidateAccount() { _isLoading = true; - await Client.GetStateAsync(); + await Client.BuildClientAsync(); _isLoading = false; } diff --git a/Manager.App/Program.cs b/Manager.App/Program.cs index 7ee974e..0f530d6 100644 --- a/Manager.App/Program.cs +++ b/Manager.App/Program.cs @@ -8,6 +8,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +AppContext.SetSwitch("System.Net.Http.EnableActivityPropagation", false); /* Manager */ builder.SetupLogging(); diff --git a/Manager.App/Services/System/ClientManager.cs b/Manager.App/Services/System/ClientManager.cs index 64a00d8..65f6140 100644 --- a/Manager.App/Services/System/ClientManager.cs +++ b/Manager.App/Services/System/ClientManager.cs @@ -44,7 +44,7 @@ public class ClientManager : BackgroundService var ytClient = new YouTubeClient(); //ytClient.CookieContainer = container; ytClient.UserAgent = accountEntity.UserAgent; - await ytClient.GetStateAsync(); + await ytClient.BuildClientAsync(); return ytClient; } diff --git a/Manager.YouTube/Models/AccountMenuInfo.cs b/Manager.YouTube/Models/AccountMenuInfo.cs new file mode 100644 index 0000000..2e1306b --- /dev/null +++ b/Manager.YouTube/Models/AccountMenuInfo.cs @@ -0,0 +1,8 @@ +namespace Manager.YouTube.Models; + +public class AccountMenuInfo +{ + public string? AccountId { get; set; } + public string? AccountHandle { get; set; } + public string? ImageUrl { get; set; } +} \ No newline at end of file diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index 1566d65..b3f4606 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -1,8 +1,10 @@ +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.Util; @@ -20,12 +22,8 @@ public static class NetworkService Method = HttpMethod.Get, RequestUri = new Uri(Origin) }; - httpRequest.Headers.IfModifiedSince = new DateTimeOffset(DateTime.UtcNow); + httpRequest.Headers.Clear(); httpRequest.Headers.UserAgent.ParseAdd(client.UserAgent); - httpRequest.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - httpRequest.Headers.Connection.Add("keep-alive"); - httpRequest.Headers.Add("DNT", "1"); - httpRequest.Headers.Add("Upgrade-Insecure-Requests", "1"); var http = client.GetHttpClient(); if (http == null) @@ -59,7 +57,48 @@ public static class NetworkService return clientState == null ? ResultError.Fail("Unable to parse client state!") : clientState; } - public static async Task GetCurrentAccountInfoAsync(YouTubeClient client) + public static async Task> GetDatasyncIds(YouTubeClient client) + { + if (client.ClientState is not { LoggedIn: true } || client.CookieContainer.Count == 0) + { + return ResultError.Fail("Client is not logged in, requires logged in client for this endpoint (/getDatasyncIdsEndpoint)."); + } + + var httpClient = client.GetHttpClient(); + if (httpClient == null) + { + return ResultError.Fail("Unable to get 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> GetCurrentAccountInfoAsync(YouTubeClient client) { if (client.ClientState is not { LoggedIn: true }) { diff --git a/Manager.YouTube/Util/AuthenticationUtilities.cs b/Manager.YouTube/Util/AuthenticationUtilities.cs index 7df18a3..918fd59 100644 --- a/Manager.YouTube/Util/AuthenticationUtilities.cs +++ b/Manager.YouTube/Util/AuthenticationUtilities.cs @@ -9,11 +9,11 @@ public static class AuthenticationUtilities { private const string HeaderScheme = "SAPISIDHASH"; - // Dave Thomas @ https://stackoverflow.com/a/32065323/9948300 + // Dave Thomas & windy for updated answer @ https://stackoverflow.com/a/32065323/9948300 public static AuthenticationHeaderValue? GetSapisidHashHeader(string datasyncId, string sapisid, string origin) { var strHash = GetSapisidHash(datasyncId, sapisid, origin); - return new AuthenticationHeaderValue(HeaderScheme, strHash); + return strHash == null ? null : new AuthenticationHeaderValue(HeaderScheme, strHash); } public static string? GetSapisidHash(string datasyncId, string sapisid, string origin, string? time = null) diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index a526d37..a6e489a 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -8,9 +8,10 @@ public sealed class YouTubeClient : IDisposable public string Id { get; private set; } = ""; public string AccountName => ClientState?.UserAccountName ?? ""; public string? UserAgent { get; set; } - public CookieContainer CookieContainer { get; } = new(); + public CookieContainer CookieContainer { get; } = new() { Capacity = 100, PerDomainCapacity = 50 }; public ClientState? ClientState { get; private set; } - public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"] ?? CookieContainer.GetAllCookies()["__Secure-3PAPISID"]; + public List DatasyncIds { get; set; } = []; + public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"]; public HttpClient? GetHttpClient() => _httpClient; private HttpClient? _httpClient; @@ -31,9 +32,10 @@ public sealed class YouTubeClient : IDisposable CookieContainer = CookieContainer }; _httpClient = new HttpClient(clientHandler); + _httpClient.DefaultRequestHeaders.Clear(); } - public async Task GetStateAsync() + public async Task BuildClientAsync() { if (ClientState == null || !ClientState.LoggedIn) { @@ -44,7 +46,23 @@ public sealed class YouTubeClient : IDisposable } ClientState = state.Value; } - + + if (string.IsNullOrWhiteSpace(ClientState.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) + { + var datasyncResult = await NetworkService.GetDatasyncIds(this); + if (!datasyncResult.IsSuccess) + { + return; + } + + foreach (var id in datasyncResult.Value) + { + if (DatasyncIds.Contains(id)) + continue; + DatasyncIds.Add(id); + } + } + var accountInfo = await NetworkService.GetCurrentAccountInfoAsync(this); }