feat(playback): native pornxp.ph via phone-side resolver (kills black screen)

pornxp.ph serves direct <source> mp4 (360/720/1080p) on st.pornxp.sh whose path
token is IP-bound to whoever fetched the PAGE (verified 2026-06-07: VPS-resolved
URL → 403 cross-IP). Backend resolve was therefore impossible, so pornxpph fell
to the WebView fallback which black-screened (bug-report fd06cd86).

Fix: resolve on-device (same pattern as getfileResolver/doodstream) — the phone
fetches the page, so tokens bind to the phone IP and play natively. New
pornxpResolver.ts extracts the <source> mp4s into multi-quality StreamLinks;
SceneDetail short-circuits tube:pornxpph to it before backend resolve, feeding
the existing quality-picker + native player.

Verified on emulator (live OTA): pornxpph scene → quality picker (1080/720/360)
→ native playback PLAYING (no WebView, no ads, no black screen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-07 14:58:40 +02:00
parent 8c0edbdf7b
commit b18f07d90e
2 changed files with 96 additions and 0 deletions

View file

@ -0,0 +1,74 @@
/**
* Mobile-side pornxp.ph resolver.
*
* pornxp.ph serwuje fluidPlayer z bezpośrednimi `<source>` mp4 (360/720/1080p) na CDN
* `st.pornxp.sh`. CDN token w ścieżce jest IP-bound do tego KTO POBRAŁ STRONĘ (potwierdzone
* 2026-06-07: VPS-resolved URL 403 cross-IP z hosta). Dlatego backend NIE może tego
* zresolwować (związałby token z IP VPS telefon 403), a WebView fallback dawał czarny
* ekran (bug-report 2026-06-07, scena fd06cd86).
*
* Fix: telefon sam pobiera stronę (phone IP) tokeny bound do IP telefonu natywne
* multi-quality playback bez WebView/reklam. Ten sam wzorzec co getfileResolver.ts /
* doodstream.ts (resolve od strony, na urządzeniu).
*/
import type { StreamLink } from '../types';
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';
const _SOURCE_RE = /<source\b[^>]*\bsrc=['"]([^'"]+\.mp4[^'"]*)['"]/gi;
/** True dla page_url pornxp.ph (origin tube:pornxpph). */
export function isPornxpUrl(url: string | null | undefined): boolean {
return !!url && /(?:^|\/\/|\.)pornxp\.ph\//i.test(url);
}
function qualityFromUrl(u: string): string {
const m = u.match(/\/(\d{3,4})\.mp4/);
return m ? `${m[1]}p` : 'auto';
}
/**
* Pobiera pornxp.ph stronę NA TELEFONIE i wyciąga `<source>` mp4 jako StreamLink-i
* (direct_url == stream_url == finalny CDN mp4; Referer w headers). Token jest
* phone-IP-bound, więc gra direct bez proxy. Sort malejąco po jakości. Zwraca [] gdy
* nic nie znaleziono (caller spada na backend/WebView).
*/
export async function resolvePornxpPage(pageUrl: string): Promise<StreamLink[]> {
let html: string;
try {
const r = await fetch(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } });
if (!r.ok) return [];
html = await r.text();
} catch {
return [];
}
const referer = (() => {
try {
return new URL(pageUrl).origin + '/';
} catch {
return 'https://pornxp.ph/';
}
})();
const seen = new Set<string>();
const links: StreamLink[] = [];
let m: RegExpExecArray | null;
_SOURCE_RE.lastIndex = 0;
while ((m = _SOURCE_RE.exec(html)) !== null) {
let u = m[1];
if (u.startsWith('//')) u = 'https:' + u;
if (seen.has(u)) continue;
seen.add(u);
links.push({
// oba pola na ten sam CDN mp4: stream_url → przejście przez quality-picker
// (filtr `!!stream_url`), direct_url → openAsVideo gra direct z Refererem.
stream_url: u,
direct_url: u,
headers: { Referer: referer, 'User-Agent': UA },
quality: qualityFromUrl(u),
type: 'mp4',
});
}
links.sort((a, b) => (parseInt(b.quality || '0', 10) || 0) - (parseInt(a.quality || '0', 10) || 0));
return links;
}

View file

@ -21,6 +21,7 @@ import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useClient } from '../ClientContext'; import { useClient } from '../ClientContext';
import { isGetFileUrl, resolveGetFilePage } from '../lib/getfileResolver'; import { isGetFileUrl, resolveGetFilePage } from '../lib/getfileResolver';
import { resolvePornxpPage } from '../lib/pornxpResolver';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { theme } from '../theme'; import { theme } from '../theme';
import type { PlaybackSource, SceneOut, StreamLink } from '../types'; import type { PlaybackSource, SceneOut, StreamLink } from '../types';
@ -518,6 +519,27 @@ function PlaybackButton({
return; return;
} }
// pornxp.ph: CDN token IP-bound (backend 403 cross-IP) → backend oddaje WebView
// fallback który czarno-ekranił (bug-report 2026-06-07, fd06cd86). Telefon sam
// pobiera stronę (phone-IP-bound mp4) → natywne multi-quality, zero WebView/reklam.
if (source.origin === 'tube:pornxpph') {
setResolving(true);
try {
const links = await resolvePornxpPage(source.page_url);
if (links.length > 0) {
markStarted();
if (links.length === 1) await openAsVideo(links[0], source.page_url);
else setQualityLinks(links);
return;
}
// pusto → spadnij na backend resolve (WebView) poniżej
} catch {
// ignore → backend fallback
} finally {
setResolving(false);
}
}
// Tube origin: backend resolve → lista linków: część direct video (stream_url), // Tube origin: backend resolve → lista linków: część direct video (stream_url),
// część hoster embed (embed_url, np. StreamWish/doodporn HTML page). // część hoster embed (embed_url, np. StreamWish/doodporn HTML page).
// - direct → MX Player (intent video/*) // - direct → MX Player (intent video/*)