fix(mobile): re-resolve IP-bound tubes on playback error (sxyprn/eporner/fpoxxx)
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) <noreply@anthropic.com>
This commit is contained in:
parent
585e5d59f5
commit
813bf741b9
3 changed files with 64 additions and 2 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<NativeStackNavigationProp<RootStackParamList, 'Player'>>();
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue