diff --git a/Manager.YouTube/Models/Innertube/ClientState.cs b/Manager.YouTube/Models/Innertube/ClientState.cs index f7b7bfc..ad69622 100644 --- a/Manager.YouTube/Models/Innertube/ClientState.cs +++ b/Manager.YouTube/Models/Innertube/ClientState.cs @@ -21,6 +21,9 @@ public class ClientState : AdditionalJsonData [JsonPropertyName("LOGGED_IN")] public bool LoggedIn { get; set; } + + [JsonPropertyName("WEB_PLAYER_CONTEXT_CONFIGS")] + public WebPlayerContextConfig? WebPlayerContextConfig { get; set; } [JsonPropertyName("USER_ACCOUNT_NAME")] public string? UserAccountName { get; set; } diff --git a/Manager.YouTube/Models/Innertube/WebPlayerContext.cs b/Manager.YouTube/Models/Innertube/WebPlayerContext.cs new file mode 100644 index 0000000..743140b --- /dev/null +++ b/Manager.YouTube/Models/Innertube/WebPlayerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Manager.YouTube.Models.Innertube; + +public class WebPlayerContext : AdditionalJsonData +{ + [JsonPropertyName("datasyncId")] + public string? DatasyncId { get; set; } +} \ No newline at end of file diff --git a/Manager.YouTube/Models/Innertube/WebPlayerContextConfig.cs b/Manager.YouTube/Models/Innertube/WebPlayerContextConfig.cs new file mode 100644 index 0000000..a3cf814 --- /dev/null +++ b/Manager.YouTube/Models/Innertube/WebPlayerContextConfig.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Manager.YouTube.Models.Innertube; + +public class WebPlayerContextConfig : AdditionalJsonData +{ + [JsonPropertyName("WEB_PLAYER_CONTEXT_CONFIG_ID_KEVLAR_WATCH")] + public WebPlayerContext? WebPlayerContext { get; set; } +} \ No newline at end of file diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index c327412..1566d65 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -1,13 +1,17 @@ +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.Util; namespace Manager.YouTube; public static class NetworkService { - private const string Origin = "https://www.youtube.com/"; + private const string Origin = "https://www.youtube.com"; public static async Task> GetClientStateAsync(YouTubeClient client) { @@ -33,7 +37,7 @@ public static class NetworkService if (!response.IsSuccessStatusCode) { var responseResult = await response.Content.ReadAsStringAsync(); - return Result.Fail(ResultError.Fail(responseResult)); + return ResultError.Fail(responseResult); } var responseHtml = await response.Content.ReadAsStringAsync(); var clientStateResult = HtmlParser.GetStateJson(responseHtml); @@ -55,21 +59,46 @@ public static class NetworkService return clientState == null ? ResultError.Fail("Unable to parse client state!") : clientState; } - public static async Task GetCurrentAccountAsync() + public static async Task GetCurrentAccountInfoAsync(YouTubeClient client) { - //URL: /youtubei/v1/account/account_menu - // Payload - // "context": { - // "client": {CLIENT INFO FROM STATE} - // } + if (client.ClientState 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.ClientState.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId ?? "", client.SapisidCookie.Value, Origin); + } + + var serializedContext = JsonSerializer.SerializeToNode(client.ClientState.InnerTubeContext); + var contextJson = new JsonObject { { "context", serializedContext } }; + httpRequest.Content = new StringContent(contextJson.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json); + + 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 json = await response.Content.ReadAsStringAsync(); + var jsonObject = JsonNode.Parse(json); - /* Auth header - * if (client.SapisidCookie != null) - { - httpRequest.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.SapisidCookie.Value, origin); - httpRequest.Headers.Add("Origin", origin); - } - */ return ResultError.Fail("Not implemented"); } } \ No newline at end of file diff --git a/Manager.YouTube/Util/AuthenticationUtilities.cs b/Manager.YouTube/Util/AuthenticationUtilities.cs index 004b185..a0ee689 100644 --- a/Manager.YouTube/Util/AuthenticationUtilities.cs +++ b/Manager.YouTube/Util/AuthenticationUtilities.cs @@ -10,16 +10,24 @@ public static class AuthenticationUtilities private const string HeaderScheme = "SAPISIDHASH"; // Dave Thomas @ https://stackoverflow.com/a/32065323/9948300 - public static AuthenticationHeaderValue? GetSapisidHashHeader(string sapisid, string origin) + public static AuthenticationHeaderValue? GetSapisidHashHeader(string datasyncId, string sapisid, string origin) { - if (string.IsNullOrWhiteSpace(sapisid) || string.IsNullOrWhiteSpace(origin)) - return null; - var time = GetTime(); - var sha1 = HashString($"{time} {sapisid} {origin}"); - var completeHash = $"{time}_{sha1}"; - return new AuthenticationHeaderValue(HeaderScheme, completeHash); + var strHash = GetSapisidHash(datasyncId, sapisid, origin); + return new AuthenticationHeaderValue(HeaderScheme, strHash); } - + + public static string? GetSapisidHash(string datasyncId, string sapisid, string origin) + { + if (string.IsNullOrWhiteSpace(datasyncId) || string.IsNullOrWhiteSpace(sapisid) || string.IsNullOrWhiteSpace(origin)) + return null; + datasyncId = datasyncId.Replace("||", ""); + sapisid = Uri.UnescapeDataString(sapisid); + var time = GetTime(); + var sha1 = HashString($"{datasyncId} {time} {sapisid} {origin}"); + var completeHash = $"{time}_{sha1}_u"; + return completeHash; + } + private static string HashString(string stringData) { var dataBytes = Encoding.ASCII.GetBytes(stringData); diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index 47fbae3..a526d37 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -10,7 +10,7 @@ public sealed class YouTubeClient : IDisposable public string? UserAgent { get; set; } public CookieContainer CookieContainer { get; } = new(); public ClientState? ClientState { get; private set; } - public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"]; + public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"] ?? CookieContainer.GetAllCookies()["__Secure-3PAPISID"]; public HttpClient? GetHttpClient() => _httpClient; private HttpClient? _httpClient; @@ -35,13 +35,17 @@ public sealed class YouTubeClient : IDisposable public async Task GetStateAsync() { - var state = await NetworkService.GetClientStateAsync(this); - if (!state.IsSuccess) + if (ClientState == null || !ClientState.LoggedIn) { - return; + var state = await NetworkService.GetClientStateAsync(this); + if (!state.IsSuccess) + { + return; + } + ClientState = state.Value; } - ClientState = state.Value; + var accountInfo = await NetworkService.GetCurrentAccountInfoAsync(this); } public void Dispose()