diff --git a/alembic/versions/20261006_0053_playback_events_via.py b/alembic/versions/20261006_0053_playback_events_via.py new file mode 100644 index 0000000..58fc193 --- /dev/null +++ b/alembic/versions/20261006_0053_playback_events_via.py @@ -0,0 +1,32 @@ +"""playback_events.via: skad przyszedl adres, ktory gral (albo padl) w odtwarzaczu. + +Po przejsciu porndoe na resolver po stronie telefonu (2026-10-02) 35 urzadzen dalej +dostawalo wylacznie 403 i nie dalo sie odroznic, czy pada nowa droga (link pobrany na +telefonie), zapas przez WebView, czy stara paczka. Apka wysyla teraz `via` +('phone', 'backend', 'hoster', 'webview', 'proxy', z koncowka '-re' po ponownym +rozwiazaniu); NULL = paczka sprzed tej zmiany. + +Kolumna bez DEFAULT, nullable: ADD COLUMN to sama zmiana metadanych. + +Revision ID: 0053_playback_events_via +Revises: 0052_clips_feed_counters +Create Date: 2026-10-06 +""" +from __future__ import annotations + +from alembic import op + +revision = "0053_playback_events_via" +down_revision: str | None = "0052_clips_feed_counters" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute("SET LOCAL lock_timeout = '5s'") + op.execute("ALTER TABLE playback_events ADD COLUMN IF NOT EXISTS via varchar(16)") + + +def downgrade() -> None: + op.execute("SET LOCAL lock_timeout = '5s'") + op.execute("ALTER TABLE playback_events DROP COLUMN IF EXISTS via") diff --git a/app/api/playback_events.py b/app/api/playback_events.py index 90b9442..3795980 100644 --- a/app/api/playback_events.py +++ b/app/api/playback_events.py @@ -32,6 +32,9 @@ class PlaybackEventIn(BaseModel): # padów których nie da się odtworzyć na emulatorze (np. watchporn player_error). error_detail: str | None = Field(default=None, max_length=512) ttff_ms: int | None = Field(default=None, ge=0, le=600_000) + # Skad przyszedl adres w odtwarzaczu (patrz PlaybackEvent.via). Opcjonalne: + # starsze paczki go nie wysylaja. + via: str | None = Field(default=None, max_length=16) @router.post("", status_code=204) @@ -49,6 +52,7 @@ def post_playback_event( error_detail=body.error_detail if body.status == "error" else None, ttff_ms=body.ttff_ms if body.status == "success" else None, device_id=device_id, + via=body.via, ) ) session.commit() diff --git a/app/models/playback_event.py b/app/models/playback_event.py index 6538cc5..779c7d2 100644 --- a/app/models/playback_event.py +++ b/app/models/playback_event.py @@ -42,6 +42,9 @@ class PlaybackEvent(Base): error_detail: Mapped[str | None] = mapped_column(String(512), nullable=True) # time-to-first-frame [ms], "szybkość" (tylko przy success; None gdy nie zmierzono). ttff_ms: Mapped[int | None] = mapped_column(Integer, nullable=True) + # Skad przyszedl adres w odtwarzaczu: 'phone', 'backend', 'hoster', 'webview', + # 'proxy' (z '-re' po ponownym rozwiazaniu). NULL = paczka sprzed 2026-10-06. + via: Mapped[str | None] = mapped_column(String(16), nullable=True) device_id: Mapped[str | None] = mapped_column(String(64), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False diff --git a/mobile/src/api.ts b/mobile/src/api.ts index 4eca492..ba9d853 100644 --- a/mobile/src/api.ts +++ b/mobile/src/api.ts @@ -333,6 +333,7 @@ export class GoonClient { error_kind?: string; error_detail?: string; ttff_ms?: number; + via?: string; }): Promise { try { await this.request('/playback-events', { diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index 49b0aa2..50a3da5 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -116,6 +116,10 @@ export type RootStackParamList = { // się automatycznie na WebView z tym URL'em, tam JS embed page sam pobiera // m3u8 we własnym kontekście (Cookies + JA3 + IP set ok). fallbackEmbedUrl?: string; + // Skad przyszedl adres (telemetria playback_events.via): 'phone' (resolver na + // telefonie w SceneDetail), 'backend', 'hoster' (resolver hostera w Playerze), + // 'webview' (wyciagniety ze strony), 'proxy' (zapasowe proxy), 'movie'. + via?: string; }; }; diff --git a/mobile/src/screens/MovieDetailScreen.tsx b/mobile/src/screens/MovieDetailScreen.tsx index fe0d966..be25c57 100644 --- a/mobile/src/screens/MovieDetailScreen.tsx +++ b/mobile/src/screens/MovieDetailScreen.tsx @@ -212,6 +212,7 @@ function WatchChip({ const pDirect = p.direct_url; const pIsDirect = !!pDirect && pDirect !== p.stream_url; navigation.navigate('Player', { + via: 'movie', url: pDirect || p.stream_url || p.embed_url || pb.page_url, origin: pb.origin, sceneId: movieId, @@ -239,6 +240,7 @@ function WatchChip({ const best = links[0]; if (best && (best.direct_url || best.stream_url)) { navigation.navigate('Player', { + via: 'movie', url: best.direct_url || best.stream_url!, origin: pb.origin, sceneId: movieId, @@ -290,6 +292,7 @@ function WatchChip({ const target = bestDirect || res.best?.stream_url || res.best?.embed_url || pb.page_url; const fallbackEmbed = res.best?.embed_url || pb.embed_url || pb.page_url; navigation.navigate('Player', { + via: 'movie', url: target, origin: pb.origin, // sceneId pozostaje nazwą param-u (legacy z kiedy Player obsługiwał tylko sceny), diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index ebf6dba..c62ca8f 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -62,6 +62,8 @@ interface RouteParams { headers?: Record; fallbackProxyUrl?: string; fallbackEmbedUrl?: string; + // Skad przyszedl adres, do telemetrii (patrz RootStackParamList.Player.via). + via?: string; } // Wspólny helper dla native + WebView ekranu błędu: oznacza playback source jako @@ -264,6 +266,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { // przyciskow siedzi z boku, stad tez lewy i prawy. const insets = useSafeAreaInsets(); const { url, sceneId, origin: playOrigin, resolvePageUrl, entityKind, durationSec, refererHost, title, fallbackEmbedUrl, headers: paramHeaders, fallbackProxyUrl } = params; + // Skad przyszedl adres, do telemetrii; ponowne rozwiazanie w tym samym ekranie dopisuje '-re'. + const viaRef = React.useRef(params.via || 'other'); const { markBroken, canMark, busy: markBusy } = useMarkSourceBroken(params); // 'movie' → /movies/{id}/progress, 'scene' (default) → /scenes/{id}/progress. const upsertProgress = React.useCallback( @@ -517,6 +521,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { if (!cancelled && freshUrl && freshUrl !== url) { player.replace(fresh?.headers ? { uri: freshUrl, headers: fresh.headers } : freshUrl); player.play(); + viaRef.current = `${viaRef.current.replace(/-re$/, '')}-re`; return; // sukces → status zmieni się z 'error', łańcuch fallback nie ruszy } // Phone-resolve zadziałał, ale dał TEN SAM URL, czyli strona parsuje się dobrze, @@ -534,6 +539,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { .find((u): u is string => !!u && u !== url); if (!cancelled && viaProxy) { player.replace(viaProxy); + viaRef.current = 'proxy-re'; player.play(); return; } @@ -605,6 +611,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { headers: undefined, fallbackProxyUrl: undefined, // już użyty fallbackEmbedUrl, // zostawiamy do step 3 + via: 'proxy', }); return; } @@ -621,6 +628,8 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { refererHost, title, mode: 'webview', + // Droga, ktora wlasnie padla; WebView dopisze do niej '>wv', gdy wyciagnie adres. + via: viaRef.current, }); } }, [status, fallbackProxyUrl, fallbackEmbedUrl, url, nav, sceneId, durationSec, refererHost, title, player, source, playerError, resolvePageUrl, reResolveApplicable, reResolveDone, playOrigin]); @@ -642,6 +651,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { status: 'success', scene_id: sceneId, ttff_ms: Date.now() - (playTelemetryMountRef.current ?? Date.now()), + via: viaRef.current, }); } }, [status, playOrigin, client, sceneId]); @@ -658,6 +668,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { scene_id: sceneId, error_kind: isGoneError(playerError?.message) ? 'gone' : 'player_error', error_detail: playerError?.message ? String(playerError.message).slice(0, 512) : undefined, + via: viaRef.current, }); }, [status, playOrigin, fallbackProxyUrl, fallbackEmbedUrl, url, playerError, client, sceneId]); @@ -1855,6 +1866,9 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) { [client, sceneId, entityKind], ); const [extractedUrl, setExtractedUrl] = React.useState(null); + // Telemetria: adres wyciagniety z tej strony. Gdy WebView byl zapasem po padnietym + // odtwarzaczu, zostaje slad poprzedniej drogi (np. 'phone>wv'). + const webViewVia = params.via ? `${params.via}>wv`.slice(0, 16) : 'webview'; const [resolveStatus, setResolveStatus] = React.useState<'idle' | 'pending' | 'failed'>('idle'); const [resolveError, setResolveError] = React.useState(null); const [skipResolve, setSkipResolve] = React.useState(false); @@ -1968,6 +1982,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) { refererHost, headers: result.headers, mode: 'video', + via: 'hoster', }); } else { // Cichy resolver padl. Zamiast slepego zaulka z przyciskiem wchodzimy OD RAZU do @@ -2098,8 +2113,9 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) { durationSec, refererHost: refererHost || (initialHostRef.current || undefined), mode: 'video', + via: webViewVia, }); - }, [extractedUrl, nav, sceneId, durationSec, refererHost, params.origin, params.title, params.playbackId, entityKind]); + }, [extractedUrl, nav, sceneId, durationSec, refererHost, params.origin, params.title, params.playbackId, entityKind, webViewVia]); React.useEffect(() => { upsertWatchProgress({ position_sec: 0, duration_sec: durationSec ?? undefined }).catch( @@ -2268,6 +2284,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) { durationSec, refererHost, mode: 'video', + via: webViewVia, }) } > diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index ca08471..8276be5 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -566,6 +566,8 @@ function PlaybackButton({ const { defaultQuality } = usePreferences(); const [resolving, setResolving] = React.useState(false); const [qualityLinks, setQualityLinks] = React.useState(null); + // Skad pochodza linki w pickerze jakosci (telemetria via): resolver na telefonie czy serwer. + const [qualityVia, setQualityVia] = React.useState<'phone' | 'backend'>('backend'); // Auto-quality: 1 link → graj; wiele → spróbuj dopasować domyślną jakość usera // (null gdy 'auto'/brak dopasowania → pokaż picker jak dotąd). markStarted zostaje @@ -616,6 +618,7 @@ function PlaybackButton({ link: StreamLink, fallbackEmbedUrl?: string, allLinks?: StreamLink[], + via: 'phone' | 'backend' = 'backend', ) => { let refererHost: string | undefined; try { @@ -657,6 +660,7 @@ function PlaybackButton({ headers: isDirect && link.headers ? link.headers : undefined, fallbackProxyUrl: isDirect ? link.stream_url || undefined : undefined, fallbackEmbedUrl, + via, // Pozostałe jakości → przełącznik w playerze (bez powrotu do listy źródeł). qualityLinks: (allLinks && allLinks.length > 1) ? allLinks @@ -711,8 +715,11 @@ function PlaybackButton({ if (links.length > 0) { markStarted(); const pick = pickAuto(links); - if (pick) await openAsVideo(pick, source.page_url); - else setQualityLinks(links); + if (pick) await openAsVideo(pick, source.page_url, undefined, 'phone'); + else { + setQualityVia('phone'); + setQualityLinks(links); + } return; } // pusto → spadnij na backend resolve poniżej (HosterDead → mark dead dla @@ -792,9 +799,10 @@ function PlaybackButton({ const autoPick = pickAuto(directLinks); if (autoPick) { markStarted(); - await openAsVideo(autoPick, fallbackEmbedUrl, directLinks); + await openAsVideo(autoPick, fallbackEmbedUrl, directLinks, 'backend'); return; } + setQualityVia('backend'); setQualityLinks(directLinks); } catch (e) { // 410 = backend zaznaczył link jako PERMANENT dead (HosterDead exception @@ -855,7 +863,7 @@ function PlaybackButton({ // direct link dał 403. `qualityLinks` zawiera tylko stream_url-e, więc // szukamy embed'a w `links` na resolve response wcześniej; tu już go nie // mamy, ale to kompromis (rzadki przypadek multi-quality + dead direct). - await openAsVideo(link, undefined, qualityLinks || undefined); + await openAsVideo(link, undefined, qualityLinks || undefined, qualityVia); } else if (link.embed_url) { let refererHost: string | undefined; try {