/** * Mobile-side eporner.com resolver. * * eporner XHR zwraca gvideo.eporner.com mp4 URL-e podpisane krótkoterminowym tokenem * BOUND DO IP requestera (URL ma `__` 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 { 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; }