[CHANGE] Add account dialog
This commit is contained in:
9
Manager.YouTube/Models/AdditionalJsonData.cs
Normal file
9
Manager.YouTube/Models/AdditionalJsonData.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Manager.YouTube.Models;
|
||||
|
||||
public class AdditionalJsonData
|
||||
{
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> AdditionalData { get; set; } = [];
|
||||
}
|
@@ -2,17 +2,35 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class ClientState
|
||||
public class ClientState : AdditionalJsonData
|
||||
{
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> AdditionalData { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("INNERTUBE_API_KEY")]
|
||||
public string? InnertubeApiKey { get; set; }
|
||||
|
||||
[JsonPropertyName("LINK_API_KEY")]
|
||||
public string? LinkApiKey { get; set; }
|
||||
|
||||
[JsonPropertyName("VOZ_API_KEY")]
|
||||
public string? VozApiKey { get; set; }
|
||||
|
||||
[JsonPropertyName("SIGNIN_URL")]
|
||||
public string? SigninUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("INNERTUBE_CLIENT_VERSION")]
|
||||
public string? InnerTubeClientVersion { get; set; }
|
||||
|
||||
[JsonPropertyName("LOGGED_IN")]
|
||||
public bool LoggedIn { get; set; }
|
||||
|
||||
[JsonPropertyName("USER_ACCOUNT_NAME")]
|
||||
public string? UserAccountName { get; set; }
|
||||
|
||||
[JsonPropertyName("SERVER_VERSION")]
|
||||
public string? ServerVersion { get; set; }
|
||||
|
||||
[JsonPropertyName("INNERTUBE_CONTEXT")]
|
||||
public InnerTubeContext? InnerTubeContext { get; set; }
|
||||
|
||||
[JsonPropertyName("SBOX_SETTINGS")]
|
||||
public SBoxSettings? SBoxSettings { get; set; }
|
||||
}
|
15
Manager.YouTube/Models/Innertube/InnerTubeClient.cs
Normal file
15
Manager.YouTube/Models/Innertube/InnerTubeClient.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class InnerTubeClient : AdditionalJsonData
|
||||
{
|
||||
[JsonPropertyName("hl")]
|
||||
public string? HLanguage { get; set; }
|
||||
[JsonPropertyName("gl")]
|
||||
public string? GLanguage { get; set; }
|
||||
[JsonPropertyName("remoteHost")]
|
||||
public string? RemoteHost { get; set; }
|
||||
[JsonPropertyName("rolloutToken")]
|
||||
public string? RolloutToken { get; set; }
|
||||
}
|
9
Manager.YouTube/Models/Innertube/InnerTubeContext.cs
Normal file
9
Manager.YouTube/Models/Innertube/InnerTubeContext.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Manager.YouTube.Models.Innertube;
|
||||
|
||||
public class InnerTubeContext : AdditionalJsonData
|
||||
{
|
||||
[JsonPropertyName("client")]
|
||||
public InnerTubeClient? InnerTubeClient { get; set; }
|
||||
}
|
@@ -2,28 +2,26 @@ using System.Text.Json;
|
||||
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/";
|
||||
|
||||
public static async Task<Result<ClientState>> GetClientStateAsync(YouTubeClient client)
|
||||
{
|
||||
var origin = "https://www.youtube.com/";
|
||||
var httpRequest = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(origin)
|
||||
RequestUri = new Uri(Origin)
|
||||
};
|
||||
httpRequest.Headers.IfModifiedSince = new DateTimeOffset(DateTime.UtcNow);
|
||||
httpRequest.Headers.UserAgent.ParseAdd(client.UserAgent);
|
||||
|
||||
if (client.SapisidCookie != null)
|
||||
{
|
||||
httpRequest.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.SapisidCookie.Value, origin);
|
||||
httpRequest.Headers.Add("Origin", origin);
|
||||
}
|
||||
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)
|
||||
@@ -38,7 +36,7 @@ public static class NetworkService
|
||||
return Result<ClientState>.Fail(ResultError.Fail(responseResult));
|
||||
}
|
||||
var responseHtml = await response.Content.ReadAsStringAsync();
|
||||
var clientStateResult = HtmlParser.GetJsonFromScriptFunction(responseHtml, "ytcfg.set");
|
||||
var clientStateResult = HtmlParser.GetStateJson(responseHtml);
|
||||
if (clientStateResult is { IsSuccess: false, Error: not null })
|
||||
{
|
||||
return clientStateResult.Error;
|
||||
@@ -47,7 +45,7 @@ public static class NetworkService
|
||||
ClientState? clientState;
|
||||
try
|
||||
{
|
||||
clientState = JsonSerializer.Deserialize<ClientState>(clientStateResult.Value);
|
||||
clientState = JsonSerializer.Deserialize<ClientState>(clientStateResult.Value.Item1);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -56,4 +54,22 @@ public static class NetworkService
|
||||
|
||||
return clientState == null ? ResultError.Fail("Unable to parse client state!") : clientState;
|
||||
}
|
||||
|
||||
public static async Task<Result> GetCurrentAccountAsync()
|
||||
{
|
||||
//URL: /youtubei/v1/account/account_menu
|
||||
// Payload
|
||||
// "context": {
|
||||
// "client": {CLIENT INFO FROM STATE}
|
||||
// }
|
||||
|
||||
/* 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");
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using DotBased.Monads;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
@@ -6,34 +5,60 @@ namespace Manager.YouTube.Parsers;
|
||||
|
||||
public static class HtmlParser
|
||||
{
|
||||
public static Result<string> GetJsonFromScriptFunction(string html, string functionName)
|
||||
public static Result<(string, string)> GetStateJson(string html)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
{
|
||||
return ResultError.Fail("html cannot be empty!");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(functionName))
|
||||
{
|
||||
return ResultError.Fail("No function names provided!");
|
||||
}
|
||||
|
||||
var htmlDocument = new HtmlDocument();
|
||||
htmlDocument.LoadHtml(html);
|
||||
|
||||
var scriptNode = htmlDocument.DocumentNode.SelectSingleNode($"//script[contains(., '{functionName}')]");
|
||||
if (string.IsNullOrWhiteSpace(scriptNode.InnerText))
|
||||
return ResultError.Fail($"Could not find {functionName} in html script nodes!");
|
||||
|
||||
var regexPattern = $@"{Regex.Escape(functionName)}\(([^)]+)\);";
|
||||
var match = Regex.Match(scriptNode.InnerText, regexPattern);
|
||||
|
||||
if (match.Success)
|
||||
const string setFunction = "ytcfg.set({";
|
||||
var scriptNode = htmlDocument.DocumentNode.SelectSingleNode($"//script[contains(., '{setFunction}')]");
|
||||
if (string.IsNullOrWhiteSpace(scriptNode.InnerText))
|
||||
return ResultError.Fail($"Could not find {setFunction} in html script nodes!");
|
||||
|
||||
var json = ExtractJson(scriptNode.InnerText, "ytcfg.set(");
|
||||
var jsonText = ExtractJson(scriptNode.InnerText, "setMessage(");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(json) || string.IsNullOrWhiteSpace(jsonText))
|
||||
{
|
||||
var jsonString = match.Groups[1].Value.Trim();
|
||||
return jsonString;
|
||||
return ResultError.Fail($"Could not find {setFunction} in html script nodes!");
|
||||
}
|
||||
|
||||
return ResultError.Fail($"Unable to parse {functionName} JSON!");
|
||||
return (json, jsonText);
|
||||
}
|
||||
|
||||
static string? ExtractJson(string input, string marker)
|
||||
{
|
||||
var start = input.IndexOf(marker, StringComparison.Ordinal);
|
||||
if (start < 0) return null;
|
||||
|
||||
start += marker.Length;
|
||||
|
||||
// Skip until first '{'
|
||||
while (start < input.Length && input[start] != '{')
|
||||
start++;
|
||||
|
||||
if (start >= input.Length) return null;
|
||||
|
||||
var depth = 0;
|
||||
var i = start;
|
||||
|
||||
for (; i < input.Length; i++)
|
||||
{
|
||||
if (input[i] == '{') depth++;
|
||||
else if (input[i] == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth != 0) continue;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return input[start..i];
|
||||
}
|
||||
}
|
@@ -1,33 +1,27 @@
|
||||
using System.Net;
|
||||
using DotBased.Logging;
|
||||
using Manager.YouTube.Models.Innertube;
|
||||
|
||||
namespace Manager.YouTube;
|
||||
|
||||
public sealed class YouTubeClient : IDisposable
|
||||
{
|
||||
public string Id { get; private set; }
|
||||
public string AccountName { get; private set; }
|
||||
public string? UserAgent { get; private set; }
|
||||
public CookieContainer CookieContainer { get; }
|
||||
public string Id { get; private set; } = "";
|
||||
public string AccountName => ClientState?.UserAccountName ?? "";
|
||||
public string? UserAgent { get; set; }
|
||||
public CookieContainer CookieContainer { get; } = new();
|
||||
public ClientState? ClientState { get; private set; }
|
||||
public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"];
|
||||
public HttpClient? GetHttpClient() => _httpClient;
|
||||
|
||||
private readonly ILogger? _logger;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
public YouTubeClient(CookieContainer cookieContainer, string userAgent, ILogger? logger = null)
|
||||
public YouTubeClient()
|
||||
{
|
||||
CookieContainer = cookieContainer;
|
||||
_logger = logger;
|
||||
UserAgent = userAgent;
|
||||
SetupClient();
|
||||
}
|
||||
|
||||
private void SetupClient()
|
||||
{
|
||||
_logger?.Information("Building http client...");
|
||||
_httpClient?.Dispose();
|
||||
|
||||
var clientHandler = new HttpClientHandler
|
||||
@@ -44,12 +38,10 @@ public sealed class YouTubeClient : IDisposable
|
||||
var state = await NetworkService.GetClientStateAsync(this);
|
||||
if (!state.IsSuccess)
|
||||
{
|
||||
_logger?.Warning("Error getting client state: {StateError}", state.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ClientState = state.Value;
|
||||
_logger?.Information("Client state retrieved. With API key: {InnertubeApiKey}", ClientState.InnertubeApiKey);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
Reference in New Issue
Block a user