150 lines
5.4 KiB
C#
150 lines
5.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Replica el resolvedor ofuscado que utiliza la app Android para reconstruir la URL real del stream.
|
|
/// </summary>
|
|
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<string> 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<string> 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<Entry> 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<Entry>();
|
|
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);
|
|
}
|