Tłumaczenie wszystkich user-facing stringów PL→EN (bug-report 2026-05-31 "dalej wszystko po polsku"). Alerty, przyciski, placeholdery, labelki w 12 ekranach/komponentach: BugReportFAB, AppLock(Screen/Settings/PinEntry), applock biometric prompts, doodstream error msgs, MovieDetail, PlaybackQuality, Player, SceneDetail, ScenesFilter, SiteScenes. Komentarze w kodzie zostają PL. Zmiany były WIP drugiego okna (uncommitted); wjechały do bundla 0.2.1 przy buildzie (były w working tree) — apka zainstalowana już ma EN. Ten commit utrwala je w gicie żeby nie zginęły. Czysto stringi, zero zmian logiki. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
206 lines
7.7 KiB
TypeScript
206 lines
7.7 KiB
TypeScript
/**
|
|
* 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/<TOKEN>', ← pierwszy parametr
|
|
* key = $.get('/?op=splash_error', '<KEY>'); ← drugi parametr
|
|
* 4. Fetch https://<host>/pass_md5/<TOKEN> → response body = base URL
|
|
* 5. Final URL = base + key (od porn-app: append key to base)
|
|
* + ?token=<KEY> 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<string, string>;
|
|
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;
|
|
}
|
|
|
|
export async function resolveDoodStream(
|
|
embedUrl: string,
|
|
sourceUrl?: string,
|
|
): Promise<ResolveResult> {
|
|
if (!isDoodStream(embedUrl)) return { error: 'not_doodstream' };
|
|
|
|
// /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<ResolveResult> {
|
|
|
|
if (html.toLowerCase().includes('video not found')) {
|
|
return { error: 'video_deleted' };
|
|
}
|
|
|
|
// pass_md5: '$.get('/pass_md5/<HASH>/<KEY>', ...)'. KEY = last path segment;
|
|
// identical do `?token=<KEY>` 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/<HASH>/<KEY>"
|
|
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<ResolveResult> {
|
|
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' };
|
|
} catch (e: any) {
|
|
return { error: `pass_md5 fetch fail: ${e?.message || e}` };
|
|
}
|
|
|
|
// Final URL construction. Porn-app format (z rs0.java): base + randomChars + ?token=<key>&expiry=<timestamp>
|
|
// (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,
|
|
},
|
|
};
|
|
}
|