122 lines
5.0 KiB
C#
122 lines
5.0 KiB
C#
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<ICipherOperation> Operations;
|
|
|
|
private CipherDecoder(IEnumerable<ICipherOperation> operations)
|
|
{
|
|
Operations = operations.ToFrozenSet();
|
|
if (Operations.Count == 0)
|
|
{
|
|
throw new ArgumentNullException(nameof(operations), "No decipher operations given.");
|
|
}
|
|
}
|
|
|
|
public static async Task<CipherDecoder> 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<IEnumerable<ICipherOperation>> 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<ICipherOperation> 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();
|
|
} |