/** * Mobile-side sxyprn.com resolver. * * Token wideo sxyprn jest bound do IP które POBRAŁO stronę /post/.html (audit * 2026-06-11: ten sam signed URL → 1024B realnego wideo z IP VPS, 10B placeholder z * innego IP). Backend resolvuje z VPS → telefon dostaje URL bound do IP VPS → direct * daje 10-bajtowy placeholder → ExoPlayer fail → fallback na proxy → CAŁE wideo (setki * MB) przez Hetzner przy KAŻDYM playbacku. * * Fix: telefon sam pobiera stronę (phone IP) → `data-vnfo` token bound do telefonu → * transform boo/ssut51 (port z app/extractors/tubes/sxyprn.py) → gra direct, zero VPS. * Ten sam wzorzec co pornxpResolver/getfileResolver (resolve od strony, na urządzeniu). */ 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 _VNFO_RE = /data-vnfo='([^']+)'/; const _B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; /** True dla page_url sxyprn (origin tube:sxyprncom). */ export function isSxyprnUrl(url: string | null | undefined): boolean { return !!url && /(?:^|\/\/|\.)sxyprn\.com\//i.test(url); } /** Suma cyfr w stringu (ssut51 z main2.js). */ function ssut51(s: string): number { let sum = 0; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if (c >= 48 && c <= 57) sum += c - 48; } return sum; } /** base64-encode ASCII string (self-contained, bez atob/Buffer). */ function b64encode(str: string): string { let out = ''; for (let i = 0; i < str.length; i += 3) { const a = str.charCodeAt(i); const b = i + 1 < str.length ? str.charCodeAt(i + 1) : NaN; const c = i + 2 < str.length ? str.charCodeAt(i + 2) : NaN; out += _B64[a >> 2]; out += _B64[((a & 3) << 4) | (isNaN(b) ? 0 : b >> 4)]; out += isNaN(b) ? '=' : _B64[((b & 15) << 2) | (isNaN(c) ? 0 : c >> 6)]; out += isNaN(c) ? '=' : _B64[c & 63]; } return out; } /** base64url-safe `-sxyprn.com-` z `+`→`-`, `/`→`_`, `=`→`.` (boo z main2.js). */ function boo(ss: number, es: number): string { return b64encode(`${ss}-sxyprn.com-${es}`) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '.'); } /** * Pobiera stronę sxyprn NA TELEFONIE i transformuje `data-vnfo` URL-e na grywalne * .vid mp4 (phone-IP-bound, grają direct). Zwraca [] gdy brak/dead (caller spada na * backend/WebView). Mirror app/extractors/tubes/sxyprn.py. */ export async function resolveSxyprnPage(pageUrl: string): Promise { let html: string; try { const r = await fetch(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } }); if (!r.ok) return []; html = await r.text(); } catch { return []; } if (html.includes('Post Not Found')) return []; // usunięty post const m = _VNFO_RE.exec(html); if (!m) return []; let vnfo: Record; try { vnfo = JSON.parse(m[1]); } catch { return []; } const links: StreamLink[] = []; for (const src of Object.values(vnfo)) { if (typeof src !== 'string' || !src.startsWith('/cdn/')) continue; const tmp = src.split('/'); if (tmp.length < 8) continue; const s6 = ssut51(tmp[6]); const s7 = ssut51(tmp[7]); const ts = parseInt(tmp[5], 10); if (Number.isNaN(ts)) continue; tmp[1] = tmp[1] + '8' + '/' + boo(s6, s7); tmp[5] = String(ts - s6 - s7); const full = 'https://sxyprn.com' + tmp.join('/'); links.push({ stream_url: full, direct_url: full, headers: { Referer: 'https://sxyprn.com/', 'User-Agent': UA }, quality: 'auto', type: 'mp4', }); } return links; }