/** * 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 = /(\[[\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 { 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; }