fix(porndoe): link do pliku pobierany na telefonie, nie przez WebView

Linki porndoe (mp4 z ip= i HLS z tokenem w sciezce) sa podpisane pod adres,
z ktorego przyszlo /service/index.json: token z IPv4 gra tylko przez IPv4,
z IPv6 tylko przez IPv6, z VPS 403. Token pobieral WebView (Chromium),
a plik ExoPlayer (OkHttp), wiec na sieciach dual-stack i za CGNAT wychodzily
innymi adresami: 164 bledy 403 na 278 udanych w tydzien.

Nowy resolver po stronie telefonu robi jedno zapytanie (film rozpoznawany po
Refererze) zwyklym fetch, ta sama droga co odtwarzacz; mp4 przed HLS, ponowne
rozwiazanie przy bledzie jak eporner/sxyprn. Pusto = dotychczasowy WebView.
Emulator: gra bez WebView.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-10-02 14:58:19 +02:00
parent 3b127bff3d
commit 6c5d409fcc
4 changed files with 73 additions and 1 deletions

View file

@ -35,6 +35,13 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: 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', id: '2026-10-02b',
date: 'October 2026', date: 'October 2026',

View file

@ -0,0 +1,60 @@
/**
* Mobile-side porndoe.com resolver.
*
* Linki do pliku (mp4 `?ip=<adres>&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<StreamLink[]> {
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');
}

View file

@ -505,6 +505,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
links = await (await import('../lib/epornerResolver')).resolveEpornerPage(resolvePageUrl); links = await (await import('../lib/epornerResolver')).resolveEpornerPage(resolvePageUrl);
} else if (resolvePageUrl && playOrigin === 'tube:fpoxxx') { } else if (resolvePageUrl && playOrigin === 'tube:fpoxxx') {
links = await (await import('../lib/fpoxxxResolver')).resolveFpoxxxPage(resolvePageUrl); 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) { } 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);

View file

@ -25,6 +25,7 @@ import { resolvePornxpPage } from '../lib/pornxpResolver';
import { resolveSxyprnPage } from '../lib/sxyprnResolver'; 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 type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { fonts, theme } from '../theme'; import { fonts, theme } from '../theme';
import { HeaderIconButton } from '../ui'; import { HeaderIconButton } from '../ui';
@ -643,7 +644,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']; const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx', 'tube:porndoecom'];
nav.navigate('Player', { nav.navigate('Player', {
url: initialUrl, url: initialUrl,
sceneId, sceneId,
@ -696,6 +697,8 @@ function PlaybackButton({
? resolveFpoxxxPage ? resolveFpoxxxPage
: source.origin === 'tube:pornxpph' : source.origin === 'tube:pornxpph'
? resolvePornxpPage ? resolvePornxpPage
: source.origin === 'tube:porndoecom'
? resolvePorndoePage
: // 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