goon/mobile/src/lib/voeResolver.ts
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

162 lines
5.6 KiB
TypeScript

/**
* Mobile-side voe.sx resolver.
*
* VOE CDN URL (cloudwindow-route.com) ma `i=<2-oktetowy prefix IP>`, token bound do /16
* IP które POBRAŁO embed (audit 2026-06-11: z VPS `i=46.62`, master.m3u8 z innego /16 →
* 403). Backend resolvuje z VPS → telefon dostaje URL bound do /16 VPS → direct 403 →
* pełny proxy → CAŁE wideo przez Hetzner (131 hitów/48h).
*
* Fix: telefon sam pobiera embed (phone IP) → token bound do jego /16 → gra direct,
* zero VPS. Port 7-stopniowego dekodera z app/extractors/hosters/voe.py (rot13 → replace
* 7 magic-sepów → strip _ → atob → shift -3 → reverse → atob → JSON.parse).
*/
import type { StreamLink } from '../types';
const UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
const _REDIRECT_RE = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
const _PAYLOAD_RE = /<script\s+type=["']application\/json["']>(\[[\s\S]+?\])<\/script>/;
const _MAGIC_SEPS = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
const _B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
/** True dla embed/page voe.sx (origin *:voe). */
export function isVoeUrl(url: string | null | undefined): boolean {
return !!url && /(?:^|\/\/|\.)voe\.sx\//i.test(url);
}
function rot13(s: string): string {
let out = '';
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c >= 0x41 && c <= 0x5a) out += String.fromCharCode(((c - 0x41 + 13) % 26) + 0x41);
else if (c >= 0x61 && c <= 0x7a) out += String.fromCharCode(((c - 0x61 + 13) % 26) + 0x61);
else out += s[i];
}
return out;
}
/** Standard base64 → bytes (self-contained, bez atob/Buffer). */
function b64ToBytes(b64: string): Uint8Array {
const lookup = new Int16Array(128).fill(-1);
for (let i = 0; i < _B64.length; i++) lookup[_B64.charCodeAt(i)] = i;
const clean = b64.replace(/=+$/, '');
const out = new Uint8Array(Math.floor((clean.length * 6) / 8));
let bits = 0;
let acc = 0;
let oi = 0;
for (let i = 0; i < clean.length; i++) {
const v = lookup[clean.charCodeAt(i) & 0x7f];
if (v < 0) continue; // pomiń whitespace/nieznane
acc = (acc << 6) | v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out[oi++] = (acc >>> bits) & 0xff;
}
}
return out.subarray(0, oi);
}
/** bytes → UTF-8 string (minimalny dekoder, bez TextDecoder). */
function utf8Decode(bytes: Uint8Array): string {
let out = '';
let i = 0;
while (i < bytes.length) {
const b = bytes[i++];
if (b < 0x80) out += String.fromCharCode(b);
else if (b >= 0xc0 && b < 0xe0) out += String.fromCharCode(((b & 0x1f) << 6) | (bytes[i++] & 0x3f));
else if (b >= 0xe0 && b < 0xf0)
out += String.fromCharCode(((b & 0x0f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f));
else {
const cp = ((b & 0x07) << 18) | ((bytes[i++] & 0x3f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f);
const c = cp - 0x10000;
out += String.fromCharCode(0xd800 + (c >> 10), 0xdc00 + (c & 0x3ff));
}
}
return out;
}
/** 7-stopniowy dekoder payloadu (port _decode_payload z voe.py). */
function decodePayload(payload: string): any | null {
try {
let s = rot13(payload);
for (const sep of _MAGIC_SEPS) s = s.split(sep).join('_');
s = s.split('_').join('');
const bytes1 = b64ToBytes(s); // 1st atob (latin-1)
let shifted = '';
for (let i = 0; i < bytes1.length; i++) shifted += String.fromCharCode(bytes1[i] - 3);
const reversed = shifted.split('').reverse().join('');
const bytes2 = b64ToBytes(reversed); // 2nd atob
return JSON.parse(utf8Decode(bytes2));
} catch {
return null;
}
}
/**
* Pobiera voe.sx embed NA TELEFONIE → m3u8 (+ mp4 fallback) StreamLink-i (phone-IP-bound,
* grają direct). Zwraca [] gdy brak/dead (caller spada na backend). Mirror voe.py.
*/
export async function resolveVoePage(embedUrl: string): Promise<StreamLink[]> {
const referer = 'https://voe.sx/';
let html: string;
try {
const r = await fetch(embedUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } });
if (!r.ok) return [];
html = await r.text();
} catch {
return [];
}
// Stage 1: JS redirect do losowego mirroru.
if (html.includes('window.location.href')) {
const m = _REDIRECT_RE.exec(html);
if (!m) return [];
try {
const r2 = await fetch(m[1], { headers: { 'User-Agent': UA, Accept: 'text/html' } });
if (!r2.ok) return [];
html = await r2.text();
} catch {
return [];
}
}
// Stage 2: extract + decode JSON payload.
const pm = _PAYLOAD_RE.exec(html);
if (!pm) return [];
let payload: string | null = null;
try {
const arr = JSON.parse(pm[1]);
payload = Array.isArray(arr) && arr.length ? arr[0] : null;
} catch {
return [];
}
if (typeof payload !== 'string') return [];
const config = decodePayload(payload);
if (!config) return [];
const links: StreamLink[] = [];
const source = (config.source || '').trim();
if (source) {
links.push({
stream_url: source,
direct_url: source,
headers: { Referer: referer, 'User-Agent': UA },
quality: 'auto',
type: 'm3u8',
});
}
let fallback = config.fallback || [];
if (fallback && !Array.isArray(fallback)) fallback = [fallback];
for (const fb of fallback) {
if (fb && typeof fb === 'object' && fb.file) {
links.push({
stream_url: fb.file,
direct_url: fb.file,
headers: { Referer: referer, 'User-Agent': UA },
quality: fb.label || 'auto',
type: 'mp4',
});
}
}
return links;
}