From 92adaccf1bb94a4631cd16d72ac9293cf57229c2 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 14 Jul 2026 22:20:43 +0300 Subject: [PATCH] fix(hqfap): resolve on the device (site blocks the server IP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mobile/src/changelog.ts | 7 ++++ mobile/src/lib/hqfapResolver.ts | 52 ++++++++++++++++++++++++ mobile/src/screens/PlayerScreen.tsx | 2 + mobile/src/screens/SceneDetailScreen.tsx | 7 +++- 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 mobile/src/lib/hqfapResolver.ts diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 504bb16..67874e4 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-07-14', + date: 'July 2026', + items: [ + 'hqfap: videos play reliably again. The site started blocking our server, so the app now resolves hqfap links directly on your device, fixing the cases that only showed ads or failed to start.', + ], + }, { id: '2026-07-11', date: 'July 2026', diff --git a/mobile/src/lib/hqfapResolver.ts b/mobile/src/lib/hqfapResolver.ts new file mode 100644 index 0000000..05dc938 --- /dev/null +++ b/mobile/src/lib/hqfapResolver.ts @@ -0,0 +1,52 @@ +/** + * 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', + }, + ]; +} diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index 70c82d6..0162340 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -261,6 +261,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { links = await (await import('../lib/epornerResolver')).resolveEpornerPage(resolvePageUrl); } else if (resolvePageUrl && playOrigin === 'tube:fpoxxx') { links = await (await import('../lib/fpoxxxResolver')).resolveFpoxxxPage(resolvePageUrl); + } else if (resolvePageUrl && playOrigin === 'tube:hqfapcom') { + links = await (await import('../lib/hqfapResolver')).resolveHqfapPage(resolvePageUrl); } else if (canBackendReresolve && sceneId && params.playbackId) { // Świeży resolve przez backend (VPS re-runuje extractor KVS → nowy nod + token). const res = await client.resolvePlayback(sceneId, params.playbackId); diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index 3d632e0..6d36580 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -25,6 +25,7 @@ import { resolvePornxpPage } from '../lib/pornxpResolver'; import { resolveSxyprnPage } from '../lib/sxyprnResolver'; import { resolveEpornerPage } from '../lib/epornerResolver'; import { resolveFpoxxxPage } from '../lib/fpoxxxResolver'; +import { resolveHqfapPage } from '../lib/hqfapResolver'; import type { RootStackParamList } from '../navigation'; import { theme } from '../theme'; import type { PlaybackSource, SceneOut, StreamLink } from '../types'; @@ -570,7 +571,7 @@ function PlaybackButton({ // IP-bound tuby resolwowane phone-side: przekaż page_url, by Player mógł // re-resolve'ować świeży token gdy native padnie na initial-load (zmiana IP / TTL). - const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx']; + const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx', 'tube:hqfapcom']; nav.navigate('Player', { url: initialUrl, sceneId, @@ -613,7 +614,9 @@ function PlaybackButton({ ? resolveFpoxxxPage : source.origin === 'tube:pornxpph' ? resolvePornxpPage - : null; + : source.origin === 'tube:hqfapcom' + ? resolveHqfapPage + : null; if (phoneResolver) { setResolving(true); try {