perf(mobile): rule34video rozwiazywany od razu na telefonie
Some checks are pending
Backend tests / test (push) Waiting to run
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:
parent
97bca096e6
commit
b19d041f09
4 changed files with 83 additions and 5 deletions
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue