fix(playback): retry DoodStream/playmogo resolve, handle "RELOAD" token response

porndish scenes resolve only to playmogo.com embeds, which are DoodStream clones
(doodcdn.io + pass_md5 + Cloudflare Turnstile). The mobile resolver already
supported playmogo, but DoodStream is flaky from a single shot: the embed is
sometimes Turnstile-gated (no pass_md5), and the pass_md5 endpoint intermittently
returns the literal string "RELOAD" (stale/consumed token) instead of a base URL.
The old code built "RELOAD<suffix>?token=..." -> ExoPlayer "no extractors" ->
WebView -> loading forever (bug 62e78c9a).

Wrap resolveDoodStream in a 3-attempt retry that re-fetches the embed (fresh
token) on retryable failures (gate / RELOAD / empty / stale token), and reject a
non-http pass_md5 body as retryable instead of building a garbage URL. Verified
cross-IP that the pass_md5 -> base -> final flow yields 206 video/mp4 when not
gated; real carrier IPs are gated far less than the test proxy. Strict
improvement: worst case is the existing WebView fallback, best case native play.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-06 21:14:26 +02:00
parent 83918e9a8d
commit 77323d23e6

View file

@ -68,12 +68,44 @@ function randomString(n: number): string {
return out; return out;
} }
// Błędy które warto powtórzyć (świeży fetch embeda = nowy token / inny CF routing).
// playmogo/dood są FLAKY: ten sam URL daje raz Turnstile-gate (brak pass_md5), raz
// pass_md5='RELOAD' (token zużyty/wygasł), raz pełny player. Zweryfikowane 2026-06-06
// cross-IP (Bright Data): 5 prób = 2× gate, 1× RELOAD, 1× zły token (200 text/html),
// 1× 206 video/mp4. Bez retry mobile trafiał na fail → WebView „loading w nieskończoność"
// (bug-report 62e78c9a porndish). Realne IP telefonu (carrier) gating'uje rzadziej niż
// Bright Data, więc 3 próby zwykle łapią sukces.
const _RETRYABLE = new Set([
'no_pass_md5_in_html',
'pass_md5_empty_response',
'pass_md5_reload',
'no_splash_error_or_token',
'captcha_gate (mobile IP also blocked?)',
]);
function _isRetryable(err?: string): boolean {
return !!err && _RETRYABLE.has(err);
}
export async function resolveDoodStream( export async function resolveDoodStream(
embedUrl: string, embedUrl: string,
sourceUrl?: string, sourceUrl?: string,
): Promise<ResolveResult> { ): Promise<ResolveResult> {
if (!isDoodStream(embedUrl)) return { error: 'not_doodstream' }; if (!isDoodStream(embedUrl)) return { error: 'not_doodstream' };
let last: ResolveResult = { error: 'no_attempt' };
for (let attempt = 0; attempt < 3; attempt++) {
last = await _resolveDoodStreamOnce(embedUrl, sourceUrl);
if (last.url) return last; // sukces
if (!_isRetryable(last.error)) return last; // permanentny błąd (deleted, http 4xx/5xx) — nie retry
// retryable (gate/RELOAD/stale token) → kolejna próba pobiera embed na nowo (nowy token)
}
return last;
}
async function _resolveDoodStreamOnce(
embedUrl: string,
sourceUrl?: string,
): Promise<ResolveResult> {
// /d/ (download page) → /e/ (embed player page). Embed ma JS z pass_md5, // /d/ (download page) → /e/ (embed player page). Embed ma JS z pass_md5,
// download page ma countdown + button. Porn-app rs0.java: linia 38. // download page ma countdown + button. Porn-app rs0.java: linia 38.
const url = embedUrl.replace('/d/', '/e/'); const url = embedUrl.replace('/d/', '/e/');
@ -186,6 +218,10 @@ async function fetchFinalUrl(
if (!r.ok) return { error: `pass_md5 http ${r.status}` }; if (!r.ok) return { error: `pass_md5 http ${r.status}` };
baseUrl = (await r.text()).trim(); baseUrl = (await r.text()).trim();
if (!baseUrl) return { error: 'pass_md5_empty_response' }; if (!baseUrl) return { error: 'pass_md5_empty_response' };
// DoodStream zwraca literalnie "RELOAD" (lub inny nie-URL) gdy token pass_md5
// jest zużyty/wygasł — trzeba przeładować embed po świeży token. Bez tej walidacji
// budowaliśmy `RELOAD<suffix>?token=...` → ExoPlayer „no extractors" → WebView.
if (!/^https?:\/\//i.test(baseUrl)) return { error: 'pass_md5_reload' };
} catch (e: any) { } catch (e: any) {
return { error: `pass_md5 fetch fail: ${e?.message || e}` }; return { error: `pass_md5 fetch fail: ${e?.message || e}` };
} }