diff --git a/mobile/src/components/BugReportFAB.tsx b/mobile/src/components/BugReportFAB.tsx index dd3874f..a09e437 100644 --- a/mobile/src/components/BugReportFAB.tsx +++ b/mobile/src/components/BugReportFAB.tsx @@ -35,6 +35,7 @@ import { captureScreen } from 'react-native-view-shot'; import { GoonClient } from '../api'; import { theme } from '../theme'; import { formatLastPlayback } from '../lib/lastPlayback'; +import { getSxyprnDiag } from '../lib/sxyprnResolver'; interface Props { client: GoonClient | null; @@ -200,6 +201,10 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) { // się rozjeżdża", bo mówi KTÓRE źródło i gdzie, niezależnie od ekranu zgłoszenia. const play = formatLastPlayback(); if (play) extraContext.push(`play=${play}`); + // Diagnostyka sxyprn "ładuje reklamy" (niereprodukowalny z naszych IP → region-gate?). + // Powód ostatniego phone-resolve sxyprn (http status / challenge / brak vnfo / ok_N). + const sxy = getSxyprnDiag(); + if (sxy) extraContext.push(`sxyprn=${sxy}`); const sceneId = rawSceneId && /^[0-9a-f-]{36}$/.test(rawSceneId) ? rawSceneId : null; const finalMessage = extraContext.length > 0 diff --git a/mobile/src/lib/sxyprnResolver.ts b/mobile/src/lib/sxyprnResolver.ts index 2f0404a..d8b9200 100644 --- a/mobile/src/lib/sxyprnResolver.ts +++ b/mobile/src/lib/sxyprnResolver.ts @@ -24,6 +24,22 @@ export function isSxyprnUrl(url: string | null | undefined): boolean { return !!url && /(?:^|\/\/|\.)sxyprn\.com\//i.test(url); } +// Diagnostyka: bug-report "sxyprn ładuje reklamy zamiast wideo" jest NIEreprodukowalny +// z naszych IP (strona zwraca data-vnfo → resolver działa), więc podejrzenie na +// region/IP-gate po stronie usera. Zapamiętujemy powód OSTATNIEGO resolve i doklejamy go +// do auto-contextu zgłoszenia (BugReportFAB) — przy kolejnym raporcie zobaczymy CO sxyprn +// realnie zwrócił temu userowi (http status / challenge / brak vnfo / ok_N), bez pytania. +let _lastDiag: { at: number; reason: string } | null = null; +function _diag(reason: string): void { + _lastDiag = { at: Date.now(), reason }; +} +/** Powód ostatniego resolve sxyprn + wiek w sekundach (null gdy nic jeszcze). */ +export function getSxyprnDiag(): string | null { + if (!_lastDiag) return null; + const ageS = Math.round((Date.now() - _lastDiag.at) / 1000); + return `${_lastDiag.reason} ${ageS}s ago`; +} + /** Suma cyfr w stringu (ssut51 z main2.js). */ function ssut51(s: string): number { let sum = 0; @@ -66,18 +82,38 @@ export async function resolveSxyprnPage(pageUrl: string): Promise let html: string; try { const r = await fetch(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } }); - if (!r.ok) return []; + if (!r.ok) { + _diag(`http_${r.status}`); + return []; + } html = await r.text(); } catch { + _diag('fetch_error'); + return []; + } + // Challenge / ad-gate: gdy user dostaje CF/turnstile albo interstitial zamiast strony + // z playerem, resolve pada i coś w fallbacku pokazuje reklamę. To najbardziej + // podejrzana ścieżka dla "ładuje reklamy" (region/IP-specific). + if (/cf-challenge|challenge-platform|turnstile|Just a moment|Attention Required|Checking your browser/i.test(html)) { + _diag(`challenge_${html.length}b`); + return []; + } + if (html.includes('Post Not Found')) { + _diag('post_not_found'); // usunięty post return []; } - if (html.includes('Post Not Found')) return []; // usunięty post const m = _VNFO_RE.exec(html); - if (!m) return []; + if (!m) { + // Brak data-vnfo: albo zmiana struktury, albo user dostał inną stronę (ad-gate). + // Długość HTML rozróżnia "pełna strona bez vnfo" od "krótki interstitial". + _diag(`no_vnfo_${html.length}b`); + return []; + } let vnfo: Record; try { vnfo = JSON.parse(m[1]); } catch { + _diag('vnfo_parse_err'); return []; } const links: StreamLink[] = []; @@ -100,5 +136,6 @@ export async function resolveSxyprnPage(pageUrl: string): Promise type: 'mp4', }); } + _diag(links.length > 0 ? `ok_${links.length}` : 'vnfo_empty'); return links; }