diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 660b6ab..91a0bc5 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -35,6 +35,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-10-02c', + date: 'October 2026', + items: [ + 'Porndoe: many plays ended with "Response code: 403". The video link is tied to the address that asked for it, and the page and the player could reach the site through different addresses. The app now asks for the link itself, the same way the player connects, and skips the web page.', + ], + }, { id: '2026-10-02b', date: 'October 2026', diff --git a/mobile/src/lib/porndoeResolver.ts b/mobile/src/lib/porndoeResolver.ts new file mode 100644 index 0000000..3fdd67a --- /dev/null +++ b/mobile/src/lib/porndoeResolver.ts @@ -0,0 +1,60 @@ +/** + * Mobile-side porndoe.com resolver. + * + * Linki do pliku (mp4 `?ip=&token=` i HLS z tokenem w sciezce) sa podpisane pod + * adres, z ktorego przyszlo zapytanie `/service/index.json`. Sprawdzone 2026-10-02: token + * pobrany przez IPv4 gra tylko przez IPv4, przez IPv6 tylko przez IPv6, z VPS 403. + * + * Wczesniej porndoe szedl przez WebView: token pobieral Chromium, a plik ExoPlayer + * (OkHttp). Na sieciach z IPv4 i IPv6 albo za CGNAT z pula adresow te dwa stosy potrafia + * wyjsc innymi adresami i plik dostawal 403 (164 bledy na 278 udanych w tydzien). + * Tu token pobiera zwykly `fetch` (tez OkHttp), wiec idzie ta sama droga co odtwarzacz. + * + * Jedno zapytanie wystarcza: serwis rozpoznaje film po Refererze, ciasteczka niepotrzebne. + */ +import type { StreamLink } from '../types'; + +const UA = + 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36'; + +type PorndoeSource = { + name?: string; + height?: string | number; + link?: string; + type?: string; +}; + +export async function resolvePorndoePage(pageUrl: string): Promise { + let origin: string; + try { + origin = new URL(pageUrl).origin; + } catch { + return []; + } + let data: { payload?: { player?: { sources?: { mp4?: unknown; hls?: unknown } } } } | null; + try { + const r = await fetch(`${origin}/service/index.json?device=mobile`, { + headers: { 'User-Agent': UA, Referer: pageUrl, Accept: 'application/json' }, + }); + if (!r.ok) return []; + data = await r.json(); + } catch { + return []; + } + const sources = data?.payload?.player?.sources; + // mp4 przed HLS: jeden plik z przewijaniem. 1080p ma `type: 'link'` (reklama konta + // premium zamiast adresu), dlatego bierzemy tylko `type: 'video'`. + const pick = (list: unknown, kind: 'mp4' | 'm3u8'): StreamLink[] => + (Array.isArray(list) ? (list as PorndoeSource[]) : []) + .filter((s) => s.type === 'video' && /^https?:\/\//.test(s.link || '')) + .sort((a, b) => Number(b.height || 0) - Number(a.height || 0)) + .map((s) => ({ + stream_url: s.link!, + direct_url: s.link!, + headers: { Referer: `${origin}/`, 'User-Agent': UA }, + quality: s.name || (s.height ? `${s.height}p` : null), + type: kind, + })); + const mp4 = pick(sources?.mp4, 'mp4'); + return mp4.length > 0 ? mp4 : pick(sources?.hls, 'm3u8'); +} diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index 848de04..ebf6dba 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -505,6 +505,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { links = await (await import('../lib/epornerResolver')).resolveEpornerPage(resolvePageUrl); } else if (resolvePageUrl && playOrigin === 'tube:fpoxxx') { links = await (await import('../lib/fpoxxxResolver')).resolveFpoxxxPage(resolvePageUrl); + } else if (resolvePageUrl && playOrigin === 'tube:porndoecom') { + links = await (await import('../lib/porndoeResolver')).resolvePorndoePage(resolvePageUrl); } else if (canBackendReresolve && sceneId && params.playbackId) { // Świeży resolve przez backend (VPS re-runuje extractor KVS → nowy nod + token). const res = await client.resolvePlayback(sceneId, params.playbackId); diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index d44624a..ca08471 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -25,6 +25,7 @@ import { resolvePornxpPage } from '../lib/pornxpResolver'; import { resolveSxyprnPage } from '../lib/sxyprnResolver'; import { resolveEpornerPage } from '../lib/epornerResolver'; import { resolveFpoxxxPage } from '../lib/fpoxxxResolver'; +import { resolvePorndoePage } from '../lib/porndoeResolver'; import type { RootStackParamList } from '../navigation'; import { fonts, theme } from '../theme'; import { HeaderIconButton } from '../ui'; @@ -643,7 +644,7 @@ function PlaybackButton({ // 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). - const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx']; + const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx', 'tube:porndoecom']; nav.navigate('Player', { url: initialUrl, sceneId, @@ -696,6 +697,8 @@ function PlaybackButton({ ? resolveFpoxxxPage : source.origin === 'tube:pornxpph' ? resolvePornxpPage + : source.origin === 'tube:porndoecom' + ? resolvePorndoePage : // 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 // nagłówków przeglądarki), więc phone-resolve tylko padał i zrzucał usera