perf(mobile): rule34video rozwiazywany od razu na telefonie
Some checks are pending
Backend tests / test (push) Waiting to run

Wczesniej resolve na serwerze, a potem telefon i tak pobieral cala strone
jeszcze raz (token zwiazany z IP) i robil przeskok przez get_file. Teraz
jedno pobranie strony na telefonie i wszystkie jakosci z flashvars,
przekierowanie get_file robi ExoPlayer. Limity czasu na pobrania.
Emulator: wybor jakosci -> odtwarzacz 3,1 s -> 0,1 s, -> dzwiek 7,2 s -> 2,9 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-10-15 12:51:14 +02:00
parent 97bca096e6
commit b19d041f09
4 changed files with 83 additions and 5 deletions

View file

@ -35,6 +35,13 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: ChangelogEntry[] = [ export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-10-15c',
date: 'October 2026',
items: [
'rule34video starts playing more than twice as fast: the app reads the video straight from the page instead of loading it twice.',
],
},
{ {
id: '2026-10-15b', id: '2026-10-15b',
date: 'October 2026', date: 'October 2026',

View file

@ -12,9 +12,22 @@
* bez proxy. Ten sam wzorzec co doodstream.ts (`redirect: 'manual'` + Location header, * bez proxy. Ten sam wzorzec co doodstream.ts (`redirect: 'manual'` + Location header,
* potwierdzone że działa na Android RN). * potwierdzone że działa na Android RN).
*/ */
import type { StreamLink } from '../types';
const UA = 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'; 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
/** fetch z limitem czasu: bez niego wiszaca strona trzymala ekran "resolving" bez konca. */
async function fetchWithTimeout(url: string, init: RequestInit, ms: number): Promise<Response> {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
return await fetch(url, { ...init, signal: ctrl.signal });
} finally {
clearTimeout(t);
}
}
/** get_file URL który wymaga in-session resolve (nie nasz proxy URL). */ /** get_file URL który wymaga in-session resolve (nie nasz proxy URL). */
export function isGetFileUrl(url: string): boolean { export function isGetFileUrl(url: string): boolean {
return /\/get_file\//.test(url) && !/\/proxy\//.test(url); return /\/get_file\//.test(url) && !/\/proxy\//.test(url);
@ -49,7 +62,7 @@ export async function resolveGetFilePage(
): Promise<string | null> { ): Promise<string | null> {
let html: string; let html: string;
try { try {
const r = await fetch(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } }); const r = await fetchWithTimeout(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } }, 10000);
if (!r.ok) return null; if (!r.ok) return null;
html = await r.text(); html = await r.text();
} catch { } catch {
@ -118,7 +131,7 @@ export async function resolveGetFile(url: string, referer?: string): Promise<str
for (let hop = 0; hop < 4; hop++) { for (let hop = 0; hop < 4; hop++) {
let r: Response; let r: Response;
try { try {
r = await fetch(cur, { r = await fetchWithTimeout(cur, {
method: 'GET', method: 'GET',
redirect: 'manual', redirect: 'manual',
headers: { headers: {
@ -126,7 +139,7 @@ export async function resolveGetFile(url: string, referer?: string): Promise<str
Range: 'bytes=0-1', Range: 'bytes=0-1',
...(referer ? { Referer: referer } : {}), ...(referer ? { Referer: referer } : {}),
}, },
}); }, 6000);
} catch { } catch {
return null; return null;
} }
@ -150,3 +163,54 @@ export async function resolveGetFile(url: string, referer?: string): Promise<str
} }
return cur; return cur;
} }
/**
* rule34video od razu na telefonie (zgloszenie 008e261d, 2026-10-12: "dziala, ale strasznie
* wolno"). Wczesniej: resolve na serwerze, potem telefon i tak pobieral cala strone jeszcze
* raz (242 KB, token v-acctoken jest zwiazany z IP pobierajacego) i robil przeskok przez
* get_file, w sumie ok. 3,5 s po odpowiedzi serwera. Tu jedno pobranie strony na telefonie
* i wszystkie jakosci z flashvars. Przekierowanie get_file -> boomio-cdn robi sam ExoPlayer
* (302, potem 206 bez Referera, sprawdzone z IP domowego).
*/
export async function resolveRule34Page(pageUrl: string): Promise<StreamLink[]> {
let html: string;
try {
const r = await fetchWithTimeout(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } }, 10000);
if (!r.ok) return [];
html = await r.text();
} catch {
return [];
}
const labels = new Map<string, string>();
let lm: RegExpExecArray | null;
_FLASHVAR_LABEL_RE.lastIndex = 0;
while ((lm = _FLASHVAR_LABEL_RE.exec(html)) !== null) labels.set(lm[1], lm[2].trim());
let origin = '';
try {
origin = new URL(pageUrl).origin;
} catch {
// zostaje pusty
}
const rank = (q: string) => ({ '1080p': 0, '720p': 1, '480p': 2, '360p': 3 }[q.toLowerCase()] ?? 99);
const out: StreamLink[] = [];
const seen = new Set<string>();
let fm: RegExpExecArray | null;
_FLASHVAR_URL_RE.lastIndex = 0;
while ((fm = _FLASHVAR_URL_RE.exec(html)) !== null) {
const q = labels.get(fm[1]) || '';
if (_SKIP_Q.test(q)) continue;
let u = fm[2];
if (u.startsWith('//')) u = 'https:' + u;
if (seen.has(u)) continue;
seen.add(u);
out.push({
stream_url: u,
direct_url: u,
headers: { 'User-Agent': UA, ...(origin ? { Referer: origin + '/' } : {}) },
quality: q || null,
type: 'mp4',
} as StreamLink);
}
out.sort((a, b) => rank(a.quality || '') - rank(b.quality || ''));
return out;
}

View file

@ -525,6 +525,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
links = await (await import('../lib/fpoxxxResolver')).resolveFpoxxxPage(resolvePageUrl); links = await (await import('../lib/fpoxxxResolver')).resolveFpoxxxPage(resolvePageUrl);
} else if (resolvePageUrl && playOrigin === 'tube:porndoecom') { } else if (resolvePageUrl && playOrigin === 'tube:porndoecom') {
links = await (await import('../lib/porndoeResolver')).resolvePorndoePage(resolvePageUrl); links = await (await import('../lib/porndoeResolver')).resolvePorndoePage(resolvePageUrl);
} else if (resolvePageUrl && playOrigin === 'tube:rule34video') {
links = await (await import('../lib/getfileResolver')).resolveRule34Page(resolvePageUrl);
} else if (canBackendReresolve && sceneId && params.playbackId) { } else if (canBackendReresolve && sceneId && params.playbackId) {
// Świeży resolve przez backend (VPS re-runuje extractor KVS → nowy nod + token). // Świeży resolve przez backend (VPS re-runuje extractor KVS → nowy nod + token).
const res = await client.resolvePlayback(sceneId, params.playbackId); const res = await client.resolvePlayback(sceneId, params.playbackId);

View file

@ -26,6 +26,7 @@ import { resolveSxyprnPage } from '../lib/sxyprnResolver';
import { resolveEpornerPage } from '../lib/epornerResolver'; import { resolveEpornerPage } from '../lib/epornerResolver';
import { resolveFpoxxxPage } from '../lib/fpoxxxResolver'; import { resolveFpoxxxPage } from '../lib/fpoxxxResolver';
import { resolvePorndoePage } from '../lib/porndoeResolver'; import { resolvePorndoePage } from '../lib/porndoeResolver';
import { resolveRule34Page } from '../lib/getfileResolver';
import { prefersPageMode } from '../lib/pageMode'; import { prefersPageMode } from '../lib/pageMode';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { fonts, theme } from '../theme'; import { fonts, theme } from '../theme';
@ -640,7 +641,9 @@ function PlaybackButton({
// hdporn.gg/fullmovies.xxx: backendowy get_file jest zbindowany do IP VPS (page-loader) // 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 // → 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). // (phone-bound get_file) → fpvcdn z IP telefonu → ExoPlayer gra direct (bez proxy/migotania).
if (isDirect && isGetFileUrl(initialUrl)) { // Link z resolvera na telefonie jest juz zwiazany z IP telefonu, drugie pobranie strony
// tylko opoznialo start (rule34video, 008e261d).
if (isDirect && via !== 'phone' && isGetFileUrl(initialUrl)) {
const ref = link.headers?.Referer || (refererHost ? `https://${refererHost}/` : undefined); const ref = link.headers?.Referer || (refererHost ? `https://${refererHost}/` : undefined);
const resolved = await resolveGetFilePage(source.page_url, link.quality, ref); const resolved = await resolveGetFilePage(source.page_url, link.quality, ref);
if (resolved) initialUrl = resolved; if (resolved) initialUrl = resolved;
@ -648,7 +651,7 @@ function PlaybackButton({
// IP-bound tuby resolwowane phone-side: przekaż page_url, by Player mógł // 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). // 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', 'tube:porndoecom']; const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx', 'tube:porndoecom', 'tube:rule34video'];
nav.navigate('Player', { nav.navigate('Player', {
url: initialUrl, url: initialUrl,
sceneId, sceneId,
@ -729,6 +732,8 @@ function PlaybackButton({
? resolvePornxpPage ? resolvePornxpPage
: source.origin === 'tube:porndoecom' : source.origin === 'tube:porndoecom'
? resolvePorndoePage ? resolvePorndoePage
: source.origin === 'tube:rule34video'
? resolveRule34Page
: // hqfap zdjęty 2026-07-28: strona wdrożyła Cloudflare z JS-challenge na : // hqfap zdjęty 2026-07-28: strona wdrożyła Cloudflare z JS-challenge na
// /watch/ (403 dla każdego zwykłego fetcha, także z pełnym kompletem // /watch/ (403 dla każdego zwykłego fetcha, także z pełnym kompletem
// nagłówków przeglądarki), więc phone-resolve tylko padał i zrzucał usera // nagłówków przeglądarki), więc phone-resolve tylko padał i zrzucał usera