diff --git a/Manager.YouTube/Manager.YouTube.csproj b/Manager.YouTube/Manager.YouTube.csproj
index 3a63532..4166dec 100644
--- a/Manager.YouTube/Manager.YouTube.csproj
+++ b/Manager.YouTube/Manager.YouTube.csproj
@@ -6,4 +6,9 @@
enable
+
+
+
+
+
diff --git a/Manager.YouTube/Models/Innertube/ClientState.cs b/Manager.YouTube/Models/Innertube/ClientState.cs
new file mode 100644
index 0000000..8195f39
--- /dev/null
+++ b/Manager.YouTube/Models/Innertube/ClientState.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace Manager.YouTube.Models.Innertube;
+
+public class ClientState
+{
+ [JsonExtensionData]
+ public Dictionary AdditionalData { get; set; } = [];
+
+ [JsonPropertyName("INNERTUBE_API_KEY")]
+ public string? InnertubeApiKey { get; set; }
+
+ [JsonPropertyName("SIGNIN_URL")]
+ public string? SigninUrl { get; set; }
+
+ [JsonPropertyName("SBOX_SETTINGS")]
+ public SBoxSettings? SBoxSettings { get; set; }
+}
\ No newline at end of file
diff --git a/Manager.YouTube/Models/Innertube/SBoxSettings.cs b/Manager.YouTube/Models/Innertube/SBoxSettings.cs
new file mode 100644
index 0000000..659643f
--- /dev/null
+++ b/Manager.YouTube/Models/Innertube/SBoxSettings.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Manager.YouTube.Models.Innertube;
+
+public class SBoxSettings
+{
+ [JsonExtensionData]
+ public Dictionary AdditionalData { get; set; } = [];
+
+ [JsonPropertyName("VISITOR_DATA")]
+ public string? VisitorData { get; set; }
+}
\ No newline at end of file
diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs
new file mode 100644
index 0000000..29dafe8
--- /dev/null
+++ b/Manager.YouTube/NetworkService.cs
@@ -0,0 +1,59 @@
+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
+{
+ public static async Task> GetClientStateAsync(YouTubeClient client)
+ {
+ var origin = "https://www.youtube.com/";
+ var httpRequest = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ 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);
+ }
+
+ 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 Result.Fail(ResultError.Fail(responseResult));
+ }
+ var responseHtml = await response.Content.ReadAsStringAsync();
+ var clientStateResult = HtmlParser.GetJsonFromScriptFunction(responseHtml, "ytcfg.set");
+ if (clientStateResult is { IsSuccess: false, Error: not null })
+ {
+ return clientStateResult.Error;
+ }
+
+ ClientState? clientState;
+ try
+ {
+ clientState = JsonSerializer.Deserialize(clientStateResult.Value);
+ }
+ catch (Exception e)
+ {
+ return ResultError.Error(e, "Error while parsing JSON!");
+ }
+
+ return clientState == null ? ResultError.Fail("Unable to parse client state!") : clientState;
+ }
+}
\ No newline at end of file
diff --git a/Manager.YouTube/Parsers/HtmlParser.cs b/Manager.YouTube/Parsers/HtmlParser.cs
new file mode 100644
index 0000000..0a69c11
--- /dev/null
+++ b/Manager.YouTube/Parsers/HtmlParser.cs
@@ -0,0 +1,39 @@
+using System.Text.RegularExpressions;
+using DotBased.Monads;
+using HtmlAgilityPack;
+
+namespace Manager.YouTube.Parsers;
+
+public static class HtmlParser
+{
+ public static Result GetJsonFromScriptFunction(string html, string functionName)
+ {
+ 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)
+ {
+ var jsonString = match.Groups[1].Value.Trim();
+ return jsonString;
+ }
+
+ return ResultError.Fail($"Unable to parse {functionName} JSON!");
+ }
+}
\ No newline at end of file
diff --git a/Manager.YouTube/Util/AuthenticationUtilities.cs b/Manager.YouTube/Util/AuthenticationUtilities.cs
new file mode 100644
index 0000000..004b185
--- /dev/null
+++ b/Manager.YouTube/Util/AuthenticationUtilities.cs
@@ -0,0 +1,37 @@
+using System.Globalization;
+using System.Net.Http.Headers;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Manager.YouTube.Util;
+
+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)
+ {
+ 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);
+ }
+
+ private static string HashString(string stringData)
+ {
+ var dataBytes = Encoding.ASCII.GetBytes(stringData);
+ var hashData = SHA1.HashData(dataBytes);
+ return hashData.Aggregate(string.Empty, (current, item) => current + item.ToString("x2"));
+ }
+
+ private static string GetTime()
+ {
+ var st = new DateTime(1970, 1, 1);
+ var t = DateTime.Now.ToUniversalTime() - st;
+ var time = (t.TotalMilliseconds + 0.5).ToString(CultureInfo.InvariantCulture);
+ return time[..10];
+ }
+}
\ No newline at end of file
diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs
index 1ee51ee..abb256c 100644
--- a/Manager.YouTube/YouTubeClient.cs
+++ b/Manager.YouTube/YouTubeClient.cs
@@ -1,6 +1,54 @@
+using System.Net;
+using DotBased.Logging;
+using Manager.YouTube.Models.Innertube;
+
namespace Manager.YouTube;
public sealed class YouTubeClient
{
-
+ public string Id { get; private set; }
+ public string AccountName { get; private set; }
+ public string? UserAgent { get; private set; }
+ public CookieContainer CookieContainer { get; }
+ 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)
+ {
+ CookieContainer = cookieContainer;
+ _logger = logger;
+ UserAgent = userAgent;
+ SetupClient();
+ }
+
+ private void SetupClient()
+ {
+ _logger?.Information("Building http client...");
+ _httpClient?.Dispose();
+
+ var clientHandler = new HttpClientHandler
+ {
+ AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
+ UseCookies = true,
+ CookieContainer = CookieContainer
+ };
+ _httpClient = new HttpClient(clientHandler);
+ }
+
+ private async Task GetStateAsync()
+ {
+ var state = await NetworkService.GetClientStateAsync(this);
+ if (!state.IsSuccess)
+ {
+ _logger?.Warning($"Error getting client state: {state.Error}");
+ return;
+ }
+
+ ClientState = state.Value;
+ _logger?.Information("Client state retrieved. With API key: {InnertubeApiKey}", ClientState.InnertubeApiKey);
+ }
}
\ No newline at end of file