using System.Collections.Frozen; using System.Text; using System.Text.RegularExpressions; using Manager.YouTube.Util.Cipher.Operations; namespace Manager.YouTube.Util.Cipher; public partial class CipherDecoder { public required string Version { get; init; } public required string OriginalRelativeUrl { get; init; } public readonly IReadOnlySet Operations; private CipherDecoder(IEnumerable operations) { Operations = operations.ToFrozenSet(); if (Operations.Count == 0) { throw new ArgumentNullException(nameof(operations), "No decipher operations given."); } } public static async Task CreateAsync(string relativeUrl, string version, YouTubeClient? client = null) { var operations = await GetCipherOperations(relativeUrl, client); var decoder = new CipherDecoder(operations) { OriginalRelativeUrl = relativeUrl, Version = version }; return decoder; } public string Decipher(string signatureCipher) { var urlBuilder = new StringBuilder(); var indexStart = signatureCipher.IndexOf("s=", StringComparison.Ordinal); var indexEnd = signatureCipher.IndexOf("&", StringComparison.Ordinal); var signature = signatureCipher.Substring(indexStart, indexEnd); indexStart = signatureCipher.IndexOf("&sp", StringComparison.Ordinal); indexEnd = signatureCipher.IndexOf("&url", StringComparison.Ordinal); var spParam = signatureCipher.Substring(indexStart, indexEnd - indexStart); indexStart = signatureCipher.IndexOf("&url", StringComparison.Ordinal); var videoUrl = signatureCipher[indexStart..]; signature = signature[(signature.IndexOf('=') + 1)..]; spParam = spParam[(spParam.IndexOf('=') + 1)..]; videoUrl = videoUrl[(videoUrl.IndexOf('=') + 1)..]; if (string.IsNullOrWhiteSpace(signature)) { throw new InvalidOperationException("Invalid signature."); } var signatureDeciphered = Operations.Aggregate(signature, (acc, op) => op.Decipher(acc)); urlBuilder.Append(videoUrl); urlBuilder.Append($"&{spParam}="); urlBuilder.Append(signatureDeciphered); return urlBuilder.ToString(); } private static async Task> GetCipherOperations(string relativeUrl, YouTubeClient? client = null) { var downloadRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"{NetworkService.Origin}/{relativeUrl}")); var downloadResponse = await NetworkService.DownloadBytesAsync(downloadRequest, client); if (!downloadResponse.IsSuccess) { return []; } var playerJs = Encoding.UTF8.GetString(downloadResponse.Value.Data); if (string.IsNullOrWhiteSpace(playerJs)) { return []; } var functionBody = FunctionBodyRegex().Match(playerJs).Groups[0].ToString(); var definitionBody = DefinitionBodyRegex().Match(functionBody).Groups[1].Value; var decipherDefinition = Regex.Match(playerJs, $@"var\s+{definitionBody}=\{{(\w+:function\(\w+(,\w+)?\)\{{(.*?)\}}),?\}};", RegexOptions.Singleline).Groups[0].ToString(); SortedSet operations = []; foreach (var statement in functionBody.Split(';')) { // Get the name of the function called in this statement var calledFuncName = StatementFunctionNameRegex().Match(statement).Groups[1].Value; if (string.IsNullOrWhiteSpace(calledFuncName)) continue; if (Regex.IsMatch(decipherDefinition, $@"{Regex.Escape(calledFuncName)}:\bfunction\b\([a],b\).(\breturn\b)?.?\w+\.")) { var index = int.Parse(OperationIndexRegex().Match(statement).Groups[1].Value); operations.Add(new CipherSlice(index)); } else if (Regex.IsMatch(decipherDefinition, $@"{Regex.Escape(calledFuncName)}:\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b")) { var index = int.Parse(OperationIndexRegex().Match(statement).Groups[1].Value); operations.Add(new CipherSwap(index)); } else if (Regex.IsMatch(decipherDefinition, $@"{Regex.Escape(calledFuncName)}:\bfunction\b\(\w+\)")) { operations.Add(new CipherReverse()); } } return operations; } [GeneratedRegex(@"(\w+)=function\(\w+\){(\w+)=\2\.split\(\x22{2}\);.*?return\s+\2\.join\(\x22{2}\)}")] private static partial Regex FunctionBodyRegex(); [GeneratedRegex("([\\$_\\w]+).\\w+\\(\\w+,\\d+\\);")] private static partial Regex DefinitionBodyRegex(); [GeneratedRegex(@"\(\w+,(\d+)\)")] private static partial Regex OperationIndexRegex(); [GeneratedRegex(@"\w+(?:.|\[)(\""?\w+(?:\"")?)\]?\(")] private static partial Regex StatementFunctionNameRegex(); }