goon/mobile/src/lib/hqfapResolver.ts
goon-foss 92adaccf1b
Some checks are pending
Backend tests / test (push) Waiting to run
fix(hqfap): resolve on the device (site blocks the server IP)
hqfap.com's Cloudflare blocks the VPS (Hetzner) IP — the server times out fetching
the page (0 bytes), so backend resolve returns None (503) or a stale/IP-bound URL that
fails on the phone (35 player_errors / 3 days, the top error source). But the page loads
fine from residential IPs (200, 0.7s) and its okcdn/vstor/cdnde mp4 is portable cross-IP
(srcIp not enforced, verified 206 from residential).

Fix: phone-side resolve, same pattern as sxyprn/eporner/fpoxxx. New hqfapResolver.ts
(fetch page on device -> JSON-LD contentUrl -> direct mp4, mirror of hqfap.py) wired into
the proactive phone-resolve chain in SceneDetail and the on-error re-resolve in
PlayerScreen; hqfapcom added to PHONE_RESOLVE_ORIGINS. Backend hqfap.extract stays as a
last-resort fallback.

Note: sxyprn/eporner were checked and are NOT broken (95-96% play success; their "gone"
events are real deletions), so no change there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:20:43 +03:00

52 lines
2.1 KiB
TypeScript

/**
* 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<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 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',
},
];
}