These CDNs bind their signed video URL to the IP that fetched the page, so a server-side resolve hands the phone a URL bound to the server IP -- the device then gets a placeholder/403 and falls back through the proxy, streaming the whole video through the server. Resolve on the device instead (token binds to the phone IP) so playback goes direct with zero proxy bandwidth. Ports of the existing backend extractors: - sxyprnResolver.ts: data-vnfo + boo/ssut51 transform - epornerResolver.ts: vid+hash -> /xhr/video mp4 sources - voeResolver.ts: mirror redirect + 7-step payload decoder Wired into SceneDetailScreen.onPress (sxyprn/eporner) and MovieDetailScreen.playVoe (voe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
/**
|
|
* Mobile-side sxyprn.com resolver.
|
|
*
|
|
* Token wideo sxyprn jest bound do IP które POBRAŁO stronę /post/<id>.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 `<ss>-sxyprn.com-<es>` 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<StreamLink[]> {
|
|
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<string, unknown>;
|
|
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;
|
|
}
|