From 813bf741b9ccc2831741a8992370a0cbeae365c3 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Thu, 25 Jun 2026 11:11:21 +0200 Subject: [PATCH] fix(mobile): re-resolve IP-bound tubes on playback error (sxyprn/eporner/fpoxxx) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sxyprn's video token is bound to the IP that fetched the post page; on mobile the phone resolver works ~74% but ~26% fail when the egress IP shifts (CGNAT / network switch) or the token goes stale → native player hung on a dead URL (18 reports, 26% error rate in telemetry). Now on an initial-load error for these phone-resolved tubes, the player re-fetches the page fresh (new token bound to the current IP) and swaps the source before falling through to the proxy/WebView chain. Zero VPS bandwidth. Gated by resolvePageUrl so other tubes are completely unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/src/changelog.ts | 7 +++ mobile/src/screens/PlayerScreen.tsx | 55 +++++++++++++++++++++++- mobile/src/screens/SceneDetailScreen.tsx | 4 ++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 86c8914..8785124 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-06-25', + date: 'June 2026', + items: [ + 'sxyprn and a few other sites: if a video fails to start, the app now grabs a fresh link and retries automatically instead of just hanging.', + ], + }, { id: '2026-06-22b', date: 'June 2026', diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index 31b6e9a..a95922b 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -23,6 +23,7 @@ import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import { useClient } from '../ClientContext'; import type { RootStackParamList } from '../navigation'; import { theme } from '../theme'; +import type { StreamLink } from '../types'; interface RouteParams { url: string; @@ -145,7 +146,7 @@ export function PlayerScreen() { function NativeVideoPlayer({ params }: { params: RouteParams }) { const client = useClient(); const nav = useNavigation>(); - const { url, sceneId, origin: playOrigin, entityKind, durationSec, refererHost, title, fallbackEmbedUrl, headers: paramHeaders, fallbackProxyUrl } = params; + const { url, sceneId, origin: playOrigin, resolvePageUrl, entityKind, durationSec, refererHost, title, fallbackEmbedUrl, headers: paramHeaders, fallbackProxyUrl } = params; const { markBroken, canMark, busy: markBusy } = useMarkSourceBroken(params); // 'movie' → /movies/{id}/progress, 'scene' (default) → /scenes/{id}/progress. const upsertProgress = React.useCallback( @@ -204,6 +205,14 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { // Każdy step ma osobną ref żeby nie loopować. const didFallbackProxyRef = React.useRef(false); const didFallbackWebViewRef = React.useRef(false); + // Re-resolve dla IP-bound tubów (sxyprn/eporner/fpoxxx): token jest bound do IP + // które pobrało stronę; jeśli IP się zmieniło (CGNAT/przełączenie sieci) albo token + // wygasł, native player pada na initial-load. Zamiast retry martwego URL-a pobieramy + // stronę ŚWIEŻO (bieżący IP) i podmieniamy źródło. Flaga `reResolveDone` gate'uje + // łańcuch fallback (proxy/WebView) póki re-resolve nie skończy — i jest no-op dla + // tubów BEZ resolvePageUrl (czyli zero wpływu na resztę). + const didReResolveRef = React.useRef(false); + const [reResolveDone, setReResolveDone] = React.useState(false); // Seek/decode recovery (bug f6c86847: doply/playmogo „invalid NAL length” przy // przewijaniu). Stream jest poprawny — faststart MP4, CDN wspiera Range 206 // (zweryfikowane 2026-06-01 cross-IP) — więc to wewnętrzny błąd seeka ExoPlayera, @@ -217,6 +226,45 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { React.useEffect(() => { if (status === 'readyToPlay') loadedOnceRef.current = true; }, [status]); + + // Re-resolve IP-bound tubów (sxyprn/eporner/fpoxxx) na initial-load error: pobierz + // stronę ŚWIEŻO z urządzenia (token bound do bieżącego IP) i podmień źródło. Tylko + // 1× / mount. No-op gdy brak resolvePageUrl. Po zakończeniu (sukces lub nie) + // ustawia reResolveDone → odblokowuje łańcuch fallback gdy nie pomogło. + React.useEffect(() => { + if (status !== 'error' || loadedOnceRef.current) return; + if (didReResolveRef.current || !resolvePageUrl || !playOrigin) return; + if (isGoneError(playerError?.message)) return; // skasowany post → niech łańcuch oznaczy dead + didReResolveRef.current = true; + let cancelled = false; + (async () => { + try { + let links: StreamLink[] = []; + if (playOrigin === 'tube:sxyprncom') { + links = await (await import('../lib/sxyprnResolver')).resolveSxyprnPage(resolvePageUrl); + } else if (playOrigin === 'tube:epornercom') { + links = await (await import('../lib/epornerResolver')).resolveEpornerPage(resolvePageUrl); + } else if (playOrigin === 'tube:fpoxxx') { + links = await (await import('../lib/fpoxxxResolver')).resolveFpoxxxPage(resolvePageUrl); + } + const fresh = links?.[0]; + const freshUrl = fresh?.direct_url || fresh?.stream_url; + if (!cancelled && freshUrl && freshUrl !== url) { + player.replace(fresh?.headers ? { uri: freshUrl, headers: fresh.headers } : freshUrl); + player.play(); + return; // sukces → status zmieni się z 'error', łańcuch fallback nie ruszy + } + } catch { + // ignore → łańcuch fallback przejmie + } finally { + if (!cancelled) setReResolveDone(true); + } + })(); + return () => { + cancelled = true; + }; + }, [status, resolvePageUrl, playOrigin, playerError, player, url]); + React.useEffect(() => { if (status !== 'error') return; // Step 0: post-load decode/seek error → recover in-place (przed proxy/WebView, @@ -243,6 +291,9 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { } return; } + // Gate: dla IP-bound tubów (resolvePageUrl) poczekaj aż re-resolve się zakończy + // zanim ruszysz proxy/WebView. No-op gdy brak resolvePageUrl (reszta tubów). + if (resolvePageUrl && !reResolveDone) return; // Step 1 → 2: direct fail (403/410/etc), spróbuj proxy URL. if (fallbackProxyUrl && !didFallbackProxyRef.current && url !== fallbackProxyUrl) { didFallbackProxyRef.current = true; @@ -277,7 +328,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { mode: 'webview', }); } - }, [status, fallbackProxyUrl, fallbackEmbedUrl, url, nav, sceneId, durationSec, refererHost, title, player, source, playerError]); + }, [status, fallbackProxyUrl, fallbackEmbedUrl, url, nav, sceneId, durationSec, refererHost, title, player, source, playerError, resolvePageUrl, reResolveDone, playOrigin]); // Telemetria odtwarzania (ranking źródeł). Tylko native-player path (WebView mode // ma osobny komponent, nie umiemy tam wykryć sukcesu → pomijamy, fair). Jeden ping diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index 6b4771f..60b2b17 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -548,10 +548,14 @@ function PlaybackButton({ if (resolved) initialUrl = resolved; } + // 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']; nav.navigate('Player', { url: initialUrl, sceneId, origin: source.origin, + resolvePageUrl: PHONE_RESOLVE_ORIGINS.includes(source.origin) ? source.page_url : undefined, playbackId: source.id, durationSec: sceneDurationSec, refererHost,