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>
88 lines
3 KiB
TypeScript
88 lines
3 KiB
TypeScript
/**
|
|
* Mobile-side eporner.com resolver.
|
|
*
|
|
* eporner XHR zwraca gvideo.eporner.com mp4 URL-e podpisane krótkoterminowym tokenem
|
|
* BOUND DO IP requestera (URL ma `<ts>_<ip>_<n>` w ścieżce — audit 2026-06-11 pokazał
|
|
* `..._46.62.219.154_...` z VPS). Backend resolvuje z VPS → telefon dostaje URL bound
|
|
* do IP VPS → direct 403 → fallback proxy → wideo przez Hetzner.
|
|
*
|
|
* Fix: telefon sam pobiera stronę + XHR (phone IP) → URL bound do telefonu → gra direct,
|
|
* zero VPS. Port z app/extractors/tubes/eporner.py (vid+hash → /xhr/video → mp4 dict).
|
|
*/
|
|
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 _VID_RE = /EP\.video\.player\.vid\s*=\s*'([^']+)'/;
|
|
const _HASH_RE = /EP\.video\.player\.hash\s*=\s*'([0-9a-fA-F]{32})'/;
|
|
|
|
/** True dla page_url eporner (origin tube:epornercom). */
|
|
export function isEpornerUrl(url: string | null | undefined): boolean {
|
|
return !!url && /(?:^|\/\/|\.)eporner\.com\//i.test(url);
|
|
}
|
|
|
|
/** 32-hex hash → base36 w 4 chunkach po 8 hex (port _hash_to_b36). */
|
|
function hashToB36(hexHash: string): string {
|
|
let out = '';
|
|
for (const i of [0, 8, 16, 24]) {
|
|
const n = parseInt(hexHash.slice(i, i + 8), 16);
|
|
out += n.toString(36);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Pobiera eporner stronę + XHR NA TELEFONIE → mp4 StreamLink-i (phone-IP-bound, grają
|
|
* direct). Zwraca [] gdy brak (caller spada na backend). Mirror eporner.py.
|
|
*/
|
|
export async function resolveEpornerPage(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 [];
|
|
}
|
|
const mVid = _VID_RE.exec(html);
|
|
const mHash = _HASH_RE.exec(html);
|
|
if (!mVid || !mHash) return [];
|
|
const vid = mVid[1];
|
|
const hashB36 = hashToB36(mHash[1]);
|
|
const xhrUrl =
|
|
`https://www.eporner.com/xhr/video/${vid}?hash=${hashB36}` +
|
|
'&domain=www.eporner.com&pixelRatio=1&playerWidth=0&playerHeight=0' +
|
|
'&fallback=false&embed=false&supportedFormats=mp4';
|
|
let data: any;
|
|
try {
|
|
const r = await fetch(xhrUrl, {
|
|
headers: {
|
|
'User-Agent': UA,
|
|
Referer: pageUrl,
|
|
Accept: '*/*',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
});
|
|
if (!r.ok) return [];
|
|
data = await r.json();
|
|
} catch {
|
|
return [];
|
|
}
|
|
if (data && data.available === false) return [];
|
|
const mp4 = (data && data.sources && data.sources.mp4) || {};
|
|
const links: StreamLink[] = [];
|
|
for (const [label, info] of Object.entries(mp4)) {
|
|
const src = info && typeof info === 'object' ? (info as any).src : null;
|
|
if (!src) continue;
|
|
links.push({
|
|
stream_url: src,
|
|
direct_url: src,
|
|
headers: { Referer: pageUrl, 'User-Agent': UA },
|
|
quality: label,
|
|
type: 'mp4',
|
|
});
|
|
}
|
|
links.sort((a, b) => (parseInt(b.quality || '0', 10) || 0) - (parseInt(a.quality || '0', 10) || 0));
|
|
return links;
|
|
}
|