diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index 2353946..c045c7c 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -8,7 +8,7 @@ public static class NetworkService public const string Origin = "https://www.youtube.com"; private static readonly HttpClient HttpClient = new(); - public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client, bool skipAuthenticationHeader = false) + public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client, bool skipAuthenticationHeader = false, CancellationToken cancellationToken = default) { request.Headers.Add("Origin", Origin); request.Headers.UserAgent.ParseAdd(client.UserAgent); @@ -19,8 +19,8 @@ public static class NetworkService try { - var response = await client.HttpClient.SendAsync(request); - var contentString = await response.Content.ReadAsStringAsync(); + var response = await client.HttpClient.SendAsync(request, cancellationToken); + var contentString = await response.Content.ReadAsStringAsync(cancellationToken); if (!response.IsSuccessStatusCode) { return ResultError.Fail(contentString); diff --git a/Manager.YouTube/Util/Cipher/CipherManager.cs b/Manager.YouTube/Util/Cipher/CipherManager.cs index 9900788..e4d2a99 100644 --- a/Manager.YouTube/Util/Cipher/CipherManager.cs +++ b/Manager.YouTube/Util/Cipher/CipherManager.cs @@ -1,5 +1,6 @@ using DotBased.Logging; using DotBased.Monads; +using Manager.YouTube.Models.Innertube; namespace Manager.YouTube.Util.Cipher; @@ -8,9 +9,9 @@ public static class CipherManager private static readonly CipherDecoderCollection LoadedCiphers = []; private static readonly ILogger Logger = LogService.RegisterLogger(typeof(CipherManager)); - public static async Task> GetDecoderAsync(YouTubeClient client) + public static async Task> GetDecoderAsync(ClientState clientState) { - var relativePlayerJsUrl = client.State?.PlayerJsUrl; + var relativePlayerJsUrl = clientState.PlayerJsUrl; if (string.IsNullOrEmpty(relativePlayerJsUrl)) { return ResultError.Fail("Could not get player js url."); diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index b8369fc..c37039e 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -16,8 +16,8 @@ namespace Manager.YouTube; public sealed class YouTubeClient : IDisposable { public string Id { get; private set; } = ""; - public string? UserAgent { get; set; } - public bool IsAnonymous { get; } + public string UserAgent { get; private set; } + public bool IsAnonymous { get; private set; } public CookieContainer CookieContainer { get; } = new() { PerDomainCapacity = 50 }; public ClientState? State { get; private set; } public List DatasyncIds { get; } = []; @@ -52,7 +52,7 @@ public sealed class YouTubeClient : IDisposable /// /// The cookies to use for making requests. Empty collection or null for anonymous requests. /// The user agent to use for the requests. Only WEB client is supported. - /// The logger that the client is going to use, if null will create a new logger. + /// The logger that the client is going to use, null will create a new logger. /// public static async Task> CreateAsync(CookieCollection? cookies, string userAgent, ILogger? logger = null) { @@ -68,114 +68,58 @@ public sealed class YouTubeClient : IDisposable return client; } - private HttpClientHandler GetHttpClientHandler() + public void SetUserAgent(string userAgent) { - var clientHandler = new HttpClientHandler + if (string.IsNullOrWhiteSpace(userAgent)) { - AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - UseCookies = true, - CookieContainer = CookieContainer - }; - return clientHandler; + _logger.Warning("UserAgent cannot be null or empty!"); + return; + } + UserAgent = userAgent; } - internal async Task FetchClientDataAsync() + public async Task> GetVideoByIdAsync(string videoId, CancellationToken cancellationToken = default) { - if (State is not { LoggedIn: true }) + if (string.IsNullOrWhiteSpace(videoId)) { - var state = await GetClientStateAsync(); - if (!state.IsSuccess) - { - return state; - } + return ResultError.Fail("Video id is empty!"); } - if (string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) + var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/watch?v={videoId}")); + + var response = await NetworkService.MakeRequestAsync(request, this, true, cancellationToken); + if (!response.IsSuccess && !string.IsNullOrWhiteSpace(response.Value)) { - var datasyncResult = await GetDatasyncIdsAsync(); - if (!datasyncResult.IsSuccess) - { - return datasyncResult; - } - - foreach (var id in datasyncResult.Value) - { - if (DatasyncIds.Contains(id)) - continue; - DatasyncIds.Add(id); - } + return response.Error ?? ResultError.Fail("Request failed!"); } - if (string.IsNullOrWhiteSpace(Id)) + var html = response.Value; + + var stateResult = GetClientStateFromHtml(html); + var state = stateResult.Value; + if (!stateResult.IsSuccess && State != null) { - var accountInfoResult = await GetCurrentAccountIdAsync(); - if (!accountInfoResult.IsSuccess) - { - return accountInfoResult; - } - - Id = accountInfoResult.Value; + state = State; } + + + var htmlParseResult = HtmlParser.GetVideoDataFromHtml(html); + if (!htmlParseResult.IsSuccess) + { + return htmlParseResult.Error ?? ResultError.Fail("Failed to parse HTML video data!"); + } + + var videoParseResult = VideoJsonParser.ParseVideoData(htmlParseResult.Value); + if (!videoParseResult.IsSuccess) + { + return videoParseResult; + } + + await DecipherSignatures(videoParseResult.Value, state); - return Result.Success(); + return videoParseResult.Value; } - private async Task GetClientStateAsync() - { - var httpRequest = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(NetworkService.Origin) - }; - - var result = await NetworkService.MakeRequestAsync(httpRequest, this, true); - if (!result.IsSuccess) - { - return result.Error ?? ResultError.Fail("Request failed!"); - } - - var stateResult = SetClientStateFromHtml(result.Value); - if (!stateResult.IsSuccess) - { - return stateResult; - } - - var cookieRotationResult = await RotateCookiesPageAsync(); - return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success(); - } - - private Result SetClientStateFromHtml(string html) - { - if (string.IsNullOrWhiteSpace(html)) - { - return ResultError.Fail("HTML is empty!!"); - } - - var clientStateResult = HtmlParser.GetStateJson(html); - if (clientStateResult is { IsSuccess: false, Error: not null }) - { - return clientStateResult.Error; - } - - try - { - State = JsonSerializer.Deserialize(clientStateResult.Value.Item1); - } - catch (Exception e) - { - return ResultError.Error(e, "Error while parsing JSON!"); - } - - if (State == null) - { - return ResultError.Fail("Unable to parse client state!"); - } - - State.IsPremiumUser = clientStateResult.Value.Item2; - - return Result.Success(); - } - public async Task> GetChannelByIdAsync(string channelId) { if (State == null) @@ -256,11 +200,131 @@ public sealed class YouTubeClient : IDisposable var rotateRequest = new HttpRequestMessage(HttpMethod.Post, new Uri("https://accounts.youtube.com/RotateCookies")); return await NetworkService.MakeRequestAsync(rotateRequest, this, true); } - + public void Dispose() { HttpClient.Dispose(); } + + private async Task FetchClientDataAsync() + { + if (State is not { LoggedIn: true }) + { + var stateResult = await GetClientStateAsync(); + if (!stateResult.IsSuccess) + { + return stateResult; + } + } + + if (string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) + { + var datasyncResult = await GetDatasyncIdsAsync(); + if (!datasyncResult.IsSuccess) + { + return datasyncResult; + } + + foreach (var id in datasyncResult.Value) + { + if (DatasyncIds.Contains(id)) + continue; + DatasyncIds.Add(id); + } + } + + if (string.IsNullOrWhiteSpace(Id)) + { + var accountInfoResult = await GetCurrentAccountIdAsync(); + if (!accountInfoResult.IsSuccess) + { + return accountInfoResult; + } + + Id = accountInfoResult.Value; + } + + return Result.Success(); + } + + private HttpClientHandler GetHttpClientHandler() + { + var clientHandler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, + UseCookies = true, + CookieContainer = CookieContainer + }; + return clientHandler; + } + + private async Task GetClientStateAsync() + { + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(NetworkService.Origin) + }; + + var result = await NetworkService.MakeRequestAsync(httpRequest, this, true); + if (!result.IsSuccess) + { + return result.Error ?? ResultError.Fail("Request failed!"); + } + + var stateResult = SetClientStateFromHtml(result.Value); + if (!stateResult.IsSuccess) + { + return stateResult; + } + + var cookieRotationResult = await RotateCookiesPageAsync(); + return !cookieRotationResult.IsSuccess ? cookieRotationResult : Result.Success(); + } + + private Result SetClientStateFromHtml(string html) + { + if (string.IsNullOrWhiteSpace(html)) + { + return ResultError.Fail("HTML is empty!!"); + } + + var clientStateResult = GetClientStateFromHtml(html); + if (clientStateResult is { IsSuccess: false, Error: not null }) + { + return clientStateResult.Error; + } + + State = clientStateResult.Value; + IsAnonymous = !State.LoggedIn; + + return Result.Success(); + } + + private Result GetClientStateFromHtml(string html) + { + var clientStateResult = HtmlParser.GetStateJson(html); + if (clientStateResult is { IsSuccess: false, Error: not null }) + { + return clientStateResult.Error; + } + + ClientState? state; + try + { + state = JsonSerializer.Deserialize(clientStateResult.Value.Item1); + if (state != null) + { + state.IsPremiumUser = clientStateResult.Value.Item2; + } + } + catch (Exception e) + { + return ResultError.Error(e, "Error while parsing JSON!"); + } + + return state == null ? ResultError.Fail("Unable to parse client state!") : state; + } private async Task> GetCurrentAccountIdAsync() { @@ -317,47 +381,7 @@ public sealed class YouTubeClient : IDisposable return ResultError.Fail("Failed to get datasyncIds! Client not logged in."); } - public async Task> GetVideoByIdAsync(string videoId, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(videoId)) - { - return ResultError.Fail("Video id is empty!"); - } - - var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/watch?v={videoId}")); - - var response = await NetworkService.MakeRequestAsync(request, this); - if (!response.IsSuccess && !string.IsNullOrWhiteSpace(response.Value)) - { - return response.Error ?? ResultError.Fail("Request failed!"); - } - - var html = response.Value; - - var stateResult = SetClientStateFromHtml(html); - if (!stateResult.IsSuccess) - { - _logger.Warning("Failed to update client state."); - } - - var htmlParseResult = HtmlParser.GetVideoDataFromHtml(html); - if (!htmlParseResult.IsSuccess) - { - return htmlParseResult.Error ?? ResultError.Fail("Failed to parse HTML video data!"); - } - - var videoParseResult = VideoJsonParser.ParseVideoData(htmlParseResult.Value); - if (!videoParseResult.IsSuccess) - { - return videoParseResult; - } - - await DecipherSignatures(videoParseResult.Value); - - return videoParseResult.Value; - } - - private async Task DecipherSignatures(YouTubeVideo video) + private async Task DecipherSignatures(YouTubeVideo video, ClientState state) { var streamingData = video.StreamingData; if (streamingData == null) @@ -373,7 +397,7 @@ public sealed class YouTubeClient : IDisposable return; } - var decipherDecoderResult = await CipherManager.GetDecoderAsync(this); + var decipherDecoderResult = await CipherManager.GetDecoderAsync(state); if (!decipherDecoderResult.IsSuccess) { _logger.Warning(decipherDecoderResult.Error?.Description ?? "Failed to get the cipher decoder!");