mobile: page-side get_file resolve for hdporngg/fullmovies (native, no proxy/flicker)

Device logs (not assumptions) pinned the real cause of the hdporngg/fullmovies
flicker: the backend returns a get_file URL, but get_file is bound to the IP that
loaded the *page*. The backend (VPS) loads the page, so the get_file is VPS-bound;
the phone fetching that get_file gets HTTP 410 -> ExoPlayer errors -> falls back to
the proxy via nav.replace (the "flicker"), and ends up streaming through the proxy.
(My earlier "stateless/portable" test was from the VPS — same IP as the page load —
so it wrongly showed 206.)

Fix: when the direct_url is a get_file, the phone re-fetches the *page* itself
(resolveGetFilePage on source.page_url) so the get_file is bound to the phone IP,
picks the requested quality skipping 4K (dead on fpvcdn), follows to the CDN, and
hands ExoPlayer a working URL. On failure it keeps the original (proxy fallback).

Verified on device: [getfile] page-resolve -> get_file 206 -> ExoPlayer PLAYING,
position advancing, no error/proxy/flicker, real video frame rendered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-06 00:16:19 +02:00
parent e5b6e8968c
commit a5ec6ca991
2 changed files with 62 additions and 4 deletions

View file

@ -20,6 +20,59 @@ export function isGetFileUrl(url: string): boolean {
return /\/get_file\//.test(url) && !/\/proxy\//.test(url); return /\/get_file\//.test(url) && !/\/proxy\//.test(url);
} }
const _SOURCE_RE = /<source\s+src=['"]([^'"]+\/get_file\/[^'"]+\.mp4[^'"]*)['"][^>]*?(?:title|label)=['"]?([^'">]*)/gi;
const _SKIP_Q = /2160|1440|4k/i;
/**
* Pełny resolve dla hdporn.gg/fullmovies.xxx OD STRONY (nie od get_file URL).
*
* KLUCZOWE (z logów urządzenia 2026-06-06): get_file jest zbindowany do IP tego kto
* załadował STRONĘ. Backend (VPS) ładuje stronę get_file dla IP VPS telefon fetchuje
* 410. Dlatego telefon musi sam załadować stronę (phone IP) dostać phone-bound
* get_file follow 302 fpvcdn z IP telefonu ExoPlayer gra. Pomija 4K (martwe na fpvcdn).
*
* `quality` = preferowana etykieta z backendu (np. "720p"); fallback = pierwszy nie-4K.
* Zwraca finalny CDN URL lub null (caller wtedy gra oryginał = proxy fallback).
*/
export async function resolveGetFilePage(
pageUrl: string,
quality?: string | null,
referer?: string,
): Promise<string | null> {
let html: string;
try {
const r = await fetch(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } });
if (!r.ok) return null;
html = await r.text();
} catch {
return null;
}
const wantQ = (quality || '').replace(/\s+/g, '').toLowerCase();
let pick: string | null = null;
let m: RegExpExecArray | null;
_SOURCE_RE.lastIndex = 0;
while ((m = _SOURCE_RE.exec(html)) !== null) {
const q = (m[2] || '').trim();
if (_SKIP_Q.test(q)) continue;
let u = m[1];
if (u.startsWith('//')) u = 'https:' + u;
if (wantQ && q && q.replace(/\s+/g, '').toLowerCase() === wantQ) {
pick = u;
break; // dokładne dopasowanie jakości
}
if (!pick) pick = u; // pierwszy nie-4K jako fallback
}
if (!pick) return null;
const ref = referer || (() => {
try {
return new URL(pageUrl).origin + '/';
} catch {
return undefined;
}
})();
return resolveGetFile(pick, ref);
}
/** /**
* Follow get_file 302 finalny CDN URL (z IP telefonu). Zwraca finalny URL gdy * Follow get_file 302 finalny CDN URL (z IP telefonu). Zwraca finalny URL gdy
* dojdzie do nie-get_file Location, oryginalny URL gdy get_file serwuje direct (2xx), * dojdzie do nie-get_file Location, oryginalny URL gdy get_file serwuje direct (2xx),
@ -42,8 +95,8 @@ export async function resolveGetFile(url: string, referer?: string): Promise<str
} catch { } catch {
return null; return null;
} }
const loc = r.headers.get('location');
if (r.status >= 300 && r.status < 400) { if (r.status >= 300 && r.status < 400) {
const loc = r.headers.get('location');
if (!loc) return null; if (!loc) return null;
try { try {
cur = new URL(loc, cur).toString(); cur = new URL(loc, cur).toString();
@ -56,7 +109,9 @@ export async function resolveGetFile(url: string, referer?: string): Promise<str
if (r.status >= 200 && r.status < 300) { if (r.status >= 200 && r.status < 300) {
return cur; // get_file serwuje direct, bez redirectu — grywalne return cur; // get_file serwuje direct, bez redirectu — grywalne
} }
return null; // 4xx/5xx // status 0 / opaqueredirect (RN okhttp czasem dla redirect:'manual') albo 4xx/5xx —
// nie da się odczytać celu, caller zagra oryginał (proxy fallback).
return null;
} }
return cur; return cur;
} }

View file

@ -20,7 +20,7 @@ import {
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useClient } from '../ClientContext'; import { useClient } from '../ClientContext';
import { isGetFileUrl, resolveGetFile } from '../lib/getfileResolver'; import { isGetFileUrl, resolveGetFilePage } from '../lib/getfileResolver';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { theme } from '../theme'; import { theme } from '../theme';
import type { PlaybackSource, SceneOut, StreamLink } from '../types'; import type { PlaybackSource, SceneOut, StreamLink } from '../types';
@ -483,9 +483,12 @@ function PlaybackButton({
// fallback proxy → „mignięcie". Resolvujemy redirect TU (na telefonie → fpvcdn z IP // fallback proxy → „mignięcie". Resolvujemy redirect TU (na telefonie → fpvcdn z IP
// telefonu) i podajemy ExoPlayerowi finalny URL — bez błędu/migotania/proxy. Fail = // telefonu) i podajemy ExoPlayerowi finalny URL — bez błędu/migotania/proxy. Fail =
// gramy oryginał (obecne zachowanie z fallbackiem). ~100-300ms (get_file 302 szybki). // gramy oryginał (obecne zachowanie z fallbackiem). ~100-300ms (get_file 302 szybki).
// hdporn.gg/fullmovies.xxx: backendowy get_file jest zbindowany do IP VPS (page-loader)
// → telefon dostaje 410 → fallback proxy (mignięcie). Re-fetchujemy STRONĘ na telefonie
// (phone-bound get_file) → fpvcdn z IP telefonu → ExoPlayer gra direct (bez proxy/migotania).
if (isDirect && isGetFileUrl(initialUrl)) { if (isDirect && isGetFileUrl(initialUrl)) {
const ref = link.headers?.Referer || (refererHost ? `https://${refererHost}/` : undefined); const ref = link.headers?.Referer || (refererHost ? `https://${refererHost}/` : undefined);
const resolved = await resolveGetFile(initialUrl, ref); const resolved = await resolveGetFilePage(source.page_url, link.quality, ref);
if (resolved) initialUrl = resolved; if (resolved) initialUrl = resolved;
} }