Add Windows desktop version
This commit is contained in:
149
windows/StreamPlayer.Desktop/Services/StreamUrlResolver.cs
Normal file
149
windows/StreamPlayer.Desktop/Services/StreamUrlResolver.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user