/** * Mobile-side DoodStream resolver. * * Powód: VPS Hetzner IP dostaje Cloudflare Turnstile gate dla playmogo/dood * embed pages (5KB challenge response). Mobile IP user'a (T-Mobile/PLAY/wifi) * pewnie nie triggers CAPTCHA — embed page renderuje pełny HTML z player JS, * w którym jest `pass_md5` token + `splash_error` key. To wystarczy do * obliczenia direct mp4 URL. * * Algorithm (reverse-engineered z porn-app APK, com.streamdev.aiostreamer * via jadx — class `defpackage/rs0.java`): * * 1. Replace /d/ → /e/ w URL (playmogo używa obu) * 2. Fetch embed page z Chrome UA + Referer = source page * 3. Regex w HTML: * token = $.get('/pass_md5/', ← pierwszy parametr * key = $.get('/?op=splash_error', ''); ← drugi parametr * 4. Fetch https:///pass_md5/ → response body = base URL * 5. Final URL = base + key (od porn-app: append key to base) * + ?token= jak w starszym format też się próbuje * 6. ExoPlayer plays z headers Referer = embed page * * Wspierane DoodStream variants (sync z mobile/src/lib/realdebrid.ts): * playmogo, doodstream, dood.*, d0000d, dooood, do0od, do7go, ds2play * * Returns: { url, headers } gdy sukces, lub { error } gdy fail. Fail może * znaczyć CAPTCHA gate dla user IP — wtedy fallback do WebView. */ const DOOD_HOSTS = new Set([ 'playmogo.com', // doply.net 301 → playmogo.com (verified 2026-05-15 — doodcdn.io scripts + // pass_md5 endpoint w body). 205 mango movies origin='mangoporn:doply'. 'doply.net', 'doodstream.com', 'doodporn.com', 'dood.la', 'dood.li', 'dood.ws', 'dood.so', 'dood.to', 'dood.watch', 'dood.work', 'dood.yt', 'dood.re', 'ds2play.com', 'ds2video.com', 'd000d.com', 'd0o0d.com', 'do0od.com', 'do7go.com', 'dooood.com', 'd0000d.com', ]); 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'; export function isDoodStream(url: string): boolean { if (!url) return false; try { const u = new URL(url); const host = u.hostname.toLowerCase().replace(/^www\./, ''); if (DOOD_HOSTS.has(host)) return true; for (const h of DOOD_HOSTS) { if (host.endsWith('.' + h)) return true; } } catch {} return false; } export interface ResolveResult { url?: string; headers?: Record; error?: string; } function randomString(n: number): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let out = ''; for (let i = 0; i < n; i++) out += chars[Math.floor(Math.random() * chars.length)]; 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( embedUrl: string, sourceUrl?: string, ): Promise { 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 { // /d/ (download page) → /e/ (embed player page). Embed ma JS z pass_md5, // download page ma countdown + button. Porn-app rs0.java: linia 38. const url = embedUrl.replace('/d/', '/e/'); const initialHost = new URL(url).hostname; const referer = sourceUrl || `https://${initialHost}/`; // Fetch embed page. WAŻNE: d0000d.com (i kilka innych dood mirrorów) robi // server-side 30x redirect do playmogo.com gdy CF challenge solved — wtedy // pass_md5 endpoint musi iść na host PO redirekcie (playmogo), nie na // original (d0000d). Bug-report e9ba57d9 + 001a72c7 2026-05-16: scena // "przekierowuje na stronę" bo resolver kierował pass_md5 na d0000d/ // post-redirect 404. // // RN/Hermes quirk: `Response.url` często jest pustym stringiem na Android // po follow-redirects (bug w native networking stack). Manualnie chodzimy // po Location headerach żeby host PO redirekcie był deterministyczny. let html: string; let host = initialHost; let currentUrl = url; try { for (let hops = 0; hops < 5; hops++) { const r = await fetch(currentUrl, { method: 'GET', headers: { 'User-Agent': UA, 'Referer': referer }, redirect: 'manual', }); // 30x → resolve Location, kontynuuj. Inaczej traktuj jako final. if (r.status >= 300 && r.status < 400) { const loc = r.headers.get('location'); if (!loc) return { error: `redirect ${r.status} without Location` }; try { currentUrl = new URL(loc, currentUrl).toString(); } catch { return { error: `bad Location url: ${loc}` }; } continue; } if (!r.ok) return { error: `embed http ${r.status}` }; html = await r.text(); // Final host = post-redirect URL. r.url też preferujemy jeśli niepuste // (na iOS bywa wypełnione), inaczej parse currentUrl. const finalUrl = r.url || currentUrl; try { host = new URL(finalUrl).hostname; } catch {} return await _continueAfterFetch(html, host, referer); } return { error: 'too many redirects (5)' }; } catch (e: any) { return { error: `embed fetch fail: ${e?.message || e}` }; } } async function _continueAfterFetch( html: string, host: string, referer: string, ): Promise { if (html.toLowerCase().includes('video not found')) { return { error: 'video_deleted' }; } // pass_md5: '$.get('/pass_md5//', ...)'. KEY = last path segment; // identical do `?token=` w makePlay/splash_error. 2026-05-15 playmogo // przeszło z 2-arg splash_error (key jako drugi parametr) na 1-arg z token // inside URL — trzymamy KEY z path zamiast osobnego regex matchu. const passMatch = html.match(/\$\.get\(\s*['"]\/(pass_md5\/[^'"]+)['"]/); if (!passMatch) { // Brak pass_md5 = pełny captcha gate (5KB body z VPS IP), pusta strona, etc. // Tu odsyłamy do WebView fallback. Sam `turnstile` w HTML nie wystarcza — // pełna strona playera ZAWIERA opcjonalny turnstile container. if (html.length < 2000 || /challenge-platform/i.test(html)) { return { error: 'captcha_gate (mobile IP also blocked?)' }; } return { error: 'no_pass_md5_in_html' }; } // Extract KEY = last segment of pass_md5 path: "pass_md5//" const passPath = passMatch[1]; const keyFromPath = passPath.split('/').pop(); if (keyFromPath) { return await fetchFinalUrl(host, passPath, keyFromPath, referer); } // Legacy fallback: 2-arg splash_error (stary format, jeszcze może być na dood mirrorach) const splashMatch = html.match(/\$\.get\(\s*['"]\/\?op=splash_error[^'"]*['"]\s*,\s*['"]([^'"]+)['"]/); if (splashMatch) { return await fetchFinalUrl(host, passPath, splashMatch[1], referer); } const tokenMatch = html.match(/\?token=([^&"']+)/); if (!tokenMatch) return { error: 'no_splash_error_or_token' }; return await fetchFinalUrl(host, passPath, tokenMatch[1], referer); } async function fetchFinalUrl( host: string, passMd5Path: string, key: string, referer: string, ): Promise { const passUrl = `https://${host}/${passMd5Path}`; let baseUrl: string; try { const r = await fetch(passUrl, { method: 'GET', headers: { 'User-Agent': UA, 'Referer': referer }, }); if (!r.ok) return { error: `pass_md5 http ${r.status}` }; baseUrl = (await r.text()).trim(); 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?token=...` → ExoPlayer „no extractors" → WebView. if (!/^https?:\/\//i.test(baseUrl)) return { error: 'pass_md5_reload' }; } catch (e: any) { return { error: `pass_md5 fetch fail: ${e?.message || e}` }; } // Final URL construction. Porn-app format (z rs0.java): base + randomChars + ?token=&expiry= // (animdl używa randomChars per dood spec; key idzie jako token query param). const randomSuffix = randomString(10); const expiry = Date.now(); const finalUrl = `${baseUrl}${randomSuffix}?token=${key}&expiry=${expiry}`; return { url: finalUrl, headers: { 'User-Agent': UA, 'Referer': referer, }, }; }