mobile: resolve get_file redirect client-side (kills hdporngg flicker)

hdporn.gg/fullmovies.xxx return an unresolved get_file direct_url that 302-redirects
to fpvcdn.com with the requester IP baked in. The backend can't resolve it (would
bind fpvcdn to the VPS IP -> mobile 403), so the phone must follow the redirect. But
ExoPlayer errors on that cross-domain get_file->fpvcdn redirect (drops Referer / won't
complete it) -> the native player falls back to the proxy via nav.replace, which the
user sees as a screen-reload "flicker" before playback (and means it's actually playing
through the VPS proxy, not direct).

Fix: resolve the get_file 302 in JS on the phone (so fpvcdn binds to the phone IP)
before navigating to the player, and hand ExoPlayer the final fpvcdn URL directly —
no redirect, no error, no flicker, no proxy. Uses the same redirect:'manual' +
Location-header pattern as the doodstream resolver (works on RN Android). On resolve
failure it keeps the original get_file URL (current behaviour with proxy fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-05 23:49:40 +02:00
parent e780e1ae6f
commit e5b6e8968c
2 changed files with 76 additions and 1 deletions

View file

@ -0,0 +1,62 @@
/**
* Mobile-side get_file final CDN resolver.
*
* Tuby typu hdporn.gg / fullmovies.xxx serwują `<source>` = `.../get_file/...mp4`,
* które 302-redirectuje na fpvcdn.com z IP FETCHERA wbitym w URL (`ip=<kto-pobrał>`).
* Backend NIE może tego zresolwować (związałby fpvcdn z IP VPS telefon 403), więc
* oddaje get_file URL niezresolwowany. Ale ExoPlayer wywala się na tym cross-domain
* redirekcie (gubi Referer / nie domyka) fallback na proxy mignięcie".
*
* Fix: resolvujemy redirect TU, w JS na telefonie (fpvcdn bindje się do IP telefonu),
* i podajemy ExoPlayerowi finalny CDN URL bez redirectu, bez błędu, bez migotania,
* bez proxy. Ten sam wzorzec co doodstream.ts (`redirect: 'manual'` + Location header,
* potwierdzone że działa na Android RN).
*/
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';
/** get_file URL który wymaga in-session resolve (nie nasz proxy URL). */
export function isGetFileUrl(url: string): boolean {
return /\/get_file\//.test(url) && !/\/proxy\//.test(url);
}
/**
* 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),
* lub null przy błędzie (caller wtedy gra oryginał = obecne zachowanie z fallbackiem).
*/
export async function resolveGetFile(url: string, referer?: string): Promise<string | null> {
let cur = url;
for (let hop = 0; hop < 4; hop++) {
let r: Response;
try {
r = await fetch(cur, {
method: 'GET',
redirect: 'manual',
headers: {
'User-Agent': UA,
Range: 'bytes=0-1',
...(referer ? { Referer: referer } : {}),
},
});
} catch {
return null;
}
if (r.status >= 300 && r.status < 400) {
const loc = r.headers.get('location');
if (!loc) return null;
try {
cur = new URL(loc, cur).toString();
} catch {
return null;
}
if (!isGetFileUrl(cur)) return cur; // dotarliśmy do finalnego CDN
continue; // kolejny get_file hop (rzadkie)
}
if (r.status >= 200 && r.status < 300) {
return cur; // get_file serwuje direct, bez redirectu — grywalne
}
return null; // 4xx/5xx
}
return cur;
}

View file

@ -20,6 +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 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';
@ -474,8 +475,20 @@ function PlaybackButton({
} }
// Preferuj direct CDN URL (0 VPS bandwidth) → fallback proxy URL (jeśli direct // Preferuj direct CDN URL (0 VPS bandwidth) → fallback proxy URL (jeśli direct
// fails). Backend dostarcza oba w StreamLink. Headers tylko dla direct path. // fails). Backend dostarcza oba w StreamLink. Headers tylko dla direct path.
const initialUrl = link.direct_url || link.stream_url!; let initialUrl = link.direct_url || link.stream_url!;
const isDirect = !!link.direct_url && initialUrl === link.direct_url; const isDirect = !!link.direct_url && initialUrl === link.direct_url;
// hdporn.gg/fullmovies.xxx: direct_url to `.../get_file/...` które 302-redirectuje
// na fpvcdn z IP fetchera. ExoPlayer wywala się na tym cross-domain redirekcie →
// fallback proxy → „mignięcie". Resolvujemy redirect TU (na telefonie → fpvcdn z IP
// telefonu) i podajemy ExoPlayerowi finalny URL — bez błędu/migotania/proxy. Fail =
// gramy oryginał (obecne zachowanie z fallbackiem). ~100-300ms (get_file 302 szybki).
if (isDirect && isGetFileUrl(initialUrl)) {
const ref = link.headers?.Referer || (refererHost ? `https://${refererHost}/` : undefined);
const resolved = await resolveGetFile(initialUrl, ref);
if (resolved) initialUrl = resolved;
}
nav.navigate('Player', { nav.navigate('Player', {
url: initialUrl, url: initialUrl,
sceneId, sceneId,