/** * Mobile-side hqfap.com resolver. * * hqfap.com Cloudflare blokuje IP VPS (Hetzner: timeout, 0 bajtów) ale strona odpowiada * 200 z residential IP (audit 2026-07-14: local 200/0.7s, VPS + Bright Data proxy oba * timeout). Backend resolve więc pada (None → 503) albo oddaje URL zbindowany do kontekstu * VPS → player_error. Strumień (okcdn.ru / vstor.top / cdnde, direct mp4 z JSON-LD * contentUrl) jest PORTABLE cross-IP (srcIp NIEegzekwowany, zweryfikowane 206 z residential). * * Fix: telefon sam pobiera stronę (residential IP) → JSON-LD contentUrl → gra direct, zero * VPS. Mirror app/extractors/tubes/hqfap.py. Zwraca [] gdy brak (caller spada na backend). */ 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 _CONTENT_URL_RE = /"contentUrl"\s*:\s*"([^"]+)"/; const _QUALITY_RE = /_(\d{3,4})p\.mp4/i; /** True dla page_url hqfap (origin tube:hqfapcom). */ export function isHqfapUrl(url: string | null | undefined): boolean { return !!url && /(?:^|\/\/|\.)hqfap\.com\//i.test(url); } export async function resolveHqfapPage(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 m = _CONTENT_URL_RE.exec(html); let contentUrl = m ? m[1].trim() : ''; contentUrl = contentUrl.replace(/\\\//g, '/'); // JSON-LD bywa z escaped slash if (!contentUrl.startsWith('http')) return []; // Stub guard: `/upload/videos/video_down.mp4` = stały "server down" placeholder // (powód usunięcia strony 2026-06-25). Lepiej żadne źródło niż stub. if (contentUrl.includes('/upload/videos/video_down.mp4')) return []; const qm = _QUALITY_RE.exec(contentUrl); return [ { stream_url: contentUrl, direct_url: contentUrl, headers: { Referer: 'https://hqfap.com/', 'User-Agent': UA }, quality: qm ? `${qm[1]}p` : 'auto', type: 'mp4', }, ]; }