using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace StreamPlayer.Desktop.Services; /// /// Replica el resolvedor ofuscado que utiliza la app Android para reconstruir la URL real del stream. /// public sealed class StreamUrlResolver { private static readonly Regex ArrayNameRegex = new(@"var\s+playbackURL\s*=\s*""""\s*,\s*([A-Za-z0-9]+)\s*=\s*\[\]", RegexOptions.Compiled); private static readonly Regex EntryRegex = new(@"\[(\d+),""([A-Za-z0-9+/=]+)""\]", RegexOptions.Compiled); private static readonly Regex KeyFunctionsRegex = new(@"var\s+k=(\w+)\(\)\+(\w+)\(\);", RegexOptions.Compiled); private const string FunctionTemplate = @"function\s+{0}\(\)\s*\{{\s*return\s+(\d+);\s*\}}"; private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) StreamPlayerResolver/1.0"; private static readonly HttpClient HttpClient = CreateHttpClient(); public async Task ResolveAsync(string pageUrl, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(pageUrl)) { throw new ArgumentException("URL inválida", nameof(pageUrl)); } string html = await DownloadPageAsync(pageUrl, cancellationToken).ConfigureAwait(false); long keyOffset = ExtractKeyOffset(html); var entries = ExtractEntries(html); if (entries.Count == 0) { throw new InvalidOperationException("No se pudieron obtener los fragmentos del stream."); } var builder = new StringBuilder(); foreach (var entry in entries) { string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(entry.Encoded)); string numeric = new string(decoded.Where(char.IsDigit).ToArray()); if (string.IsNullOrEmpty(numeric)) { continue; } if (!long.TryParse(numeric, out long value)) { continue; } builder.Append((char)(value - keyOffset)); } string url = builder.ToString(); if (string.IsNullOrWhiteSpace(url)) { throw new InvalidOperationException("No se pudo reconstruir la URL de reproducción."); } return url.Trim(); } private static async Task DownloadPageAsync(string url, CancellationToken cancellationToken) { using var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.UserAgent.ParseAdd(UserAgent); request.Headers.Referrer = new Uri("https://streamtpmedia.com/"); using var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); } private static long ExtractKeyOffset(string html) { var match = KeyFunctionsRegex.Match(html); if (!match.Success) { throw new InvalidOperationException("No se encontró la clave para el stream."); } string first = match.Groups[1].Value; string second = match.Groups[2].Value; long firstVal = ExtractReturnValue(html, first); long secondVal = ExtractReturnValue(html, second); return firstVal + secondVal; } private static long ExtractReturnValue(string html, string functionName) { var pattern = string.Format(CultureInfo.InvariantCulture, FunctionTemplate, Regex.Escape(functionName)); var regex = new Regex(pattern); var match = regex.Match(html); if (!match.Success) { throw new InvalidOperationException($"No se encontró el valor de la función {functionName}."); } return long.Parse(match.Groups[1].Value); } private static List ExtractEntries(string html) { var matcher = ArrayNameRegex.Match(html); if (!matcher.Success) { throw new InvalidOperationException("No se detectó la variable de fragmentos."); } string arrayName = matcher.Groups[1].Value; var arrayRegex = new Regex($"{Regex.Escape(arrayName)}=\\[(.*?)\\];", RegexOptions.Singleline); var arrayMatch = arrayRegex.Match(html); if (!arrayMatch.Success) { throw new InvalidOperationException("No se encontró el arreglo de fragmentos."); } string rawEntries = arrayMatch.Groups[1].Value; var entries = new List(); foreach (Match match in EntryRegex.Matches(rawEntries)) { if (int.TryParse(match.Groups[1].Value, out int index)) { entries.Add(new Entry(index, match.Groups[2].Value)); } } return entries.OrderBy(e => e.Index).ToList(); } private static HttpClient CreateHttpClient() { var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All, AllowAutoRedirect = true }; return new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; } private sealed record Entry(int Index, string Encoded); }