diff --git a/mobile/src/lib/pornxpResolver.ts b/mobile/src/lib/pornxpResolver.ts new file mode 100644 index 0000000..8633672 --- /dev/null +++ b/mobile/src/lib/pornxpResolver.ts @@ -0,0 +1,74 @@ +/** + * Mobile-side pornxp.ph resolver. + * + * pornxp.ph serwuje fluidPlayer z bezpośrednimi `` 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 = /]*\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 `` 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 { + 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(); + 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; +} diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index f15c97a..747cde7 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -21,6 +21,7 @@ import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useClient } from '../ClientContext'; import { isGetFileUrl, resolveGetFilePage } from '../lib/getfileResolver'; +import { resolvePornxpPage } from '../lib/pornxpResolver'; import type { RootStackParamList } from '../navigation'; import { theme } from '../theme'; import type { PlaybackSource, SceneOut, StreamLink } from '../types'; @@ -518,6 +519,27 @@ function PlaybackButton({ 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), // część hoster embed (embed_url, np. StreamWish/doodporn HTML page). // - direct → MX Player (intent video/*)