fix: keep-screen-on podczas odtwarzania + on-demand miniaturki youporn
Some checks failed
Backend tests / test (push) Has been cancelled

1) Ekran gasl w trakcie ogladania (bug-reports fcbaa933 2026-07-18, c40fb79f
2026-07-23, dwoch userow). Android wygasza po timeoucie, bo dotyk nie pada, a samo
granie wideo nie trzyma wake-locka. expo-keep-awake NIE jest w top-level
node_modules (siedzi w node_modules/expo/node_modules), wiec metro nie rozwiaze
importu, ale natywny modul ExpoKeepAwake JEST w APK (expo go autolinkuje) -
siegamy po niego przez requireNativeModule z expo-modules-core (top-level).
Dzieki temu leci OTA, bez nowego APK. Wake-lock tylko gdy realnie gra (przy pauzie
ekran gasnie normalnie); WebView-player trzyma przez caly czas zycia ekranu, bo
tam nie ma sygnalu play/pause.

2) Miniaturki youporn (bug-report bebf8c92): youporn przeszedl dla czesci katalogu
na imgproxy z PODPISANYM URL-em (?hash=...&validto=<ts>), ktory WYGASA - zapisany
w bazie po czasie zwraca 410 (8666 z 62930 zrodel). Statyczne .../original/N.jpg
dzialaja bezterminowo. Nowy endpoint /proxy/youporn-thumb/<video_id> resolwuje
swiezy poster przy serwowaniu (jak sxyprn-thumb), z html.unescape bo og:image ma
&amp; i psulo to podpis (403). Podmiana TYLKO dla podpisanych - statycznych nie
ruszamy, zero dodatkowych fetchy.

Zweryfikowane: 8/8 wczesniej-410 teraz 200 (57-142KB), statyczne bez regresji.
This commit is contained in:
goon-foss 2026-07-26 17:00:31 +02:00
parent 0c5148b6e8
commit 55da508f9c
5 changed files with 171 additions and 0 deletions

View file

@ -650,6 +650,20 @@ def _sxyprn_thumb_url(page_url: str | None) -> str | None:
return f"/proxy/sxyprn-thumb/{m.group(1)}" if m else None
_YOUPORN_WATCH_RE = re.compile(r"youporn\.com/watch/(\d+)", re.IGNORECASE)
def _youporn_thumb_url(page_url: str | None, thumb: str | None) -> str | None:
"""Dla youporn z PODPISANYM posterem (imgproxy `?hash=...&validto=`) zwraca stabilny
on-demand endpoint `/proxy/youporn-thumb/<video_id>` zapisany podpis wygasa i CDN
oddaje 410 (bug-report bebf8c92; 8666 z 62930 źródeł). Statycznych `.../original/N.jpg`
NIE ruszamy (działają bezterminowo, zero dodatkowych fetchy)."""
if not page_url or not thumb or "hash=" not in thumb:
return None
m = _YOUPORN_WATCH_RE.search(page_url)
return f"/proxy/youporn-thumb/{m.group(1)}" if m else None
def _is_rotting_thumb(url: str) -> bool:
"""sxyprn/trafficdeposit miniaturki są czasowo podpisane i rotują (asset 404 po
~tygodniach, nie odświeżalne server-side; bug 2026-06-10). De-prioritize je w wyborze
@ -782,10 +796,15 @@ def _build_scenes_out_batch(
anim_by_scene: dict = {}
for sid, thumb, anim, page_url in pb_light:
sxy = _sxyprn_thumb_url(page_url)
yp = _youporn_thumb_url(page_url, thumb)
if sxy:
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
# tier fallback: użyty tylko gdy scena nie ma stabilniejszej miniatury.
thumb_fallback.setdefault(sid, (sxy, page_url))
elif yp:
# youporn z podpisanym (wygasającym) posterem → on-demand resolver.
# Też tier fallback: statyczna miniatura z innego źródła jest tańsza.
thumb_fallback.setdefault(sid, (yp, page_url))
elif thumb:
if _is_rotting_thumb(thumb):
thumb_fallback.setdefault(sid, (thumb, page_url))
@ -833,8 +852,11 @@ def _build_scenes_out_batch(
# "na liście jest miniaturka, w scenie nie"). Podmieniamy na żywy resolver;
# inne martwe rotting-thumby zerujemy → mobile bierze kolejne źródło / placeholder.
sxy = _sxyprn_thumb_url(p.page_url)
yp = _youporn_thumb_url(p.page_url, out.thumbnail_url)
if sxy:
out.thumbnail_url = sxy
elif yp:
out.thumbnail_url = yp
elif out.thumbnail_url and _is_rotting_thumb(out.thumbnail_url):
out.thumbnail_url = None
if out.thumbnail_url and _needs_proxy(out.thumbnail_url):
@ -985,8 +1007,11 @@ def _build_scene_out(session: Session, scene: Scene, *, device_id: str = LEGACY_
# detal sceny pokazywał martwą miniaturę sxyprn mimo że lista działała, i "refresh
# thumbnail" nie pomagał (sxyprn IP-bound, enrich z VPS pada). Report 2026-07-22.
sxy = _sxyprn_thumb_url(p.page_url)
yp = _youporn_thumb_url(p.page_url, out.thumbnail_url)
if sxy:
out.thumbnail_url = sxy
elif yp:
out.thumbnail_url = yp
elif out.thumbnail_url and _is_rotting_thumb(out.thumbnail_url):
out.thumbnail_url = None
# Wrap thumbnail URL-e przez backend image proxy gdy CDN wymaga Refera

View file

@ -368,6 +368,73 @@ _VID_POSTER_RE = re.compile(r"<video[^>]*poster=[\"']([^\"']+)", re.IGNORECASE)
_SXYPRN_PID_RE = re.compile(r"^[0-9a-f]{6,40}$")
# youporn on-demand thumbnail (bug-report bebf8c92 2026-07-25 "miniaturki z youporn
# nie działają"). youporn przeszedł dla części katalogu na imgproxy z PODPISANYM URL-em
# (`.../original_<id>.mov/plain/rs:fit:1280:720/vts:N?hash=...&validto=<ts>`) — taki URL
# wygasa, więc zapisany w bazie po czasie zwraca 410 (audit 2026-07-26: 8666 z 62930
# źródeł youporn). Reszta katalogu ma stare statyczne `.../original/N.jpg`, które działają
# bezterminowo — tych NIE ruszamy (żaden dodatkowy fetch). Tu, jak w sxyprn, resolvujemy
# świeży URL przy serwowaniu; klient dostaje stabilny `/proxy/youporn-thumb/<video_id>`.
_YOUPORN_POSTER_CACHE: dict[str, tuple[str, float]] = {}
_YOUPORN_POSTER_TTL = 1800
_YOUPORN_VID_RE = re.compile(r"^\d{4,12}$")
_YP_JSONLD_THUMB_RE = re.compile(r'"thumbnailUrl"\s*:\s*"([^"]+)"', re.IGNORECASE)
@router.get("/youporn-thumb/{video_id}")
async def youporn_thumb(video_id: str) -> Response:
"""On-demand poster youporn (podpisany URL imgproxy wygasa, więc nie da się go
trzymać w bazie). Stabilny URL per video_id klient cache'uje bajty."""
import html as _htmllib
vid = video_id.split(".")[0]
if not _YOUPORN_VID_RE.match(vid):
raise HTTPException(status_code=400, detail="bad video_id")
now = time.time()
cached = _YOUPORN_POSTER_CACHE.get(vid)
poster = cached[0] if (cached and cached[1] > now) else None
timeout = httpx.Timeout(connect=10.0, read=20.0, write=10.0, pool=5.0)
page = f"https://www.youporn.com/watch/{vid}/"
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
if poster is None:
try:
r = await client.get(page, headers=_build_headers(None))
except Exception as e:
log.info("youporn-thumb page fetch failed %s: %s", vid, e)
return Response(content=b"", status_code=502, media_type="image/jpeg")
html_txt = r.text
m = (
_YP_JSONLD_THUMB_RE.search(html_txt)
or _OG_IMG_RE.search(html_txt)
or _OG_IMG_RE2.search(html_txt)
or _VID_POSTER_RE.search(html_txt)
)
if not m:
return Response(content=b"", status_code=404, media_type="image/jpeg")
# JSON-LD escapuje `\/`, a og:image ma encje HTML (`&amp;`) — bez unescape
# podpis w query jest zepsuty i CDN oddaje 403.
poster = _htmllib.unescape(m.group(1).strip().replace("\\/", "/"))
if poster.startswith("//"):
poster = "https:" + poster
_YOUPORN_POSTER_CACHE[vid] = (poster, now + _YOUPORN_POSTER_TTL)
if len(_YOUPORN_POSTER_CACHE) > 8000:
for k in [k for k, v in list(_YOUPORN_POSTER_CACHE.items()) if v[1] < now]:
_YOUPORN_POSTER_CACHE.pop(k, None)
try:
pr = await client.get(poster, headers=_build_headers(page))
except Exception as e:
log.info("youporn-thumb poster fetch failed %s: %s", vid, e)
return Response(content=b"", status_code=502, media_type="image/jpeg")
if pr.status_code >= 400:
_YOUPORN_POSTER_CACHE.pop(vid, None) # wygasły podpis → re-resolve następnym razem
return Response(content=b"", status_code=502, media_type="image/jpeg")
return Response(
content=pr.content,
media_type=pr.headers.get("content-type", "image/jpeg"),
headers={"Cache-Control": "public, max-age=604800"},
)
@router.get("/sxyprn-thumb/{post_id}")
async def sxyprn_thumb(post_id: str) -> Response:
"""On-demand poster sxyprn. URL stabilny per post_id (klient cache'uje bajty);

View file

@ -16,6 +16,14 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-07-26b',
date: 'July 2026',
items: [
'The screen no longer turns off while a video is playing. It still dims normally when you pause.',
'youporn thumbnails that had gone blank now load again.',
],
},
{
id: '2026-07-26',
date: 'July 2026',

View file

@ -0,0 +1,54 @@
/**
* Keep-screen-on podczas odtwarzania (bug-reports 2026-07-18 `fcbaa933` i 2026-07-23
* `c40fb79f`: "ekran gaśnie w trakcie oglądania" Android wygasza ekran po timeoucie,
* bo dotyk nie pada, a samo granie wideo nie trzyma wake-locka).
*
* Dlaczego nie `expo-keep-awake` z importu: pakiet NIE jest w top-level `node_modules`
* (siedzi w `node_modules/expo/node_modules/`), więc metro nie rozwiąże go z naszego
* kodu, a dokładanie zależności do package.json nie przeszłoby przez OTA gdyby wersja
* rozjechała się z APK. Natywny moduł `ExpoKeepAwake` (activate/deactivate po tagu) JEST
* jednak w buildzie, bo `expo` bundluje go i autolinkuje więc sięgamy po niego wprost
* przez `requireNativeModule` z `expo-modules-core` (ten jest top-level). Dzięki temu
* całość leci OTA, bez nowego APK.
*
* Wszystko best-effort: gdyby modułu zabrakło (inny build), no-op zamiast wywalenia
* playera.
*/
import { requireNativeModule } from 'expo-modules-core';
const TAG = 'goon-player';
type KeepAwakeNative = {
activate?: (tag: string) => Promise<unknown>;
deactivate?: (tag: string) => Promise<unknown>;
};
let _mod: KeepAwakeNative | null | undefined;
function nativeModule(): KeepAwakeNative | null {
if (_mod !== undefined) return _mod;
try {
_mod = requireNativeModule<KeepAwakeNative>('ExpoKeepAwake');
} catch {
_mod = null; // brak modułu w tym buildzie → no-op
}
return _mod;
}
/** Trzymaj ekran włączony (idempotentne — natywka trzyma set tagów). */
export function keepScreenOn(): void {
try {
void nativeModule()?.activate?.(TAG)?.catch?.(() => {});
} catch {
// ignore
}
}
/** Pozwól ekranowi gasnąć normalnie. */
export function allowScreenOff(): void {
try {
void nativeModule()?.deactivate?.(TAG)?.catch?.(() => {});
} catch {
// ignore
}
}

View file

@ -8,6 +8,7 @@ import * as ScreenOrientation from 'expo-screen-orientation';
import { useVideoPlayer, VideoView, type VideoSource } from 'expo-video';
import * as Clipboard from 'expo-clipboard';
import { setLastPlayback } from '../lib/lastPlayback';
import { allowScreenOff, keepScreenOn } from '../lib/keepAwake';
import React from 'react';
import {
ActivityIndicator,
@ -211,6 +212,15 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
const playingEvent = useEvent(player, 'playingChange', { isPlaying: player.playing });
const isPlaying = playingEvent?.isPlaying ?? player.playing;
// Ekran nie gaśnie w trakcie odtwarzania (bug-reports fcbaa933 / c40fb79f). Wake-lock
// TYLKO gdy realnie gra — przy pauzie ekran ma gasnąć normalnie, żeby nie trzymać
// wybudzenia gdy user odłożył telefon.
React.useEffect(() => {
if (isPlaying) keepScreenOn();
else allowScreenOff();
}, [isPlaying]);
React.useEffect(() => allowScreenOff, []); // unmount → zawsze zwolnij
// Zapis kontekstu odtwarzania → BugReportFAB dokleja go do zgłoszeń (który serwer/host,
// pozycja, ostatni błąd) bez pytania usera. Aktualizowany na zmianę źródła i statusu.
React.useEffect(() => {
@ -1432,6 +1442,13 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
const client = useClient();
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Player'>>();
const { url, sceneId, entityKind, durationSec, refererHost } = params;
// Keep-screen-on dla ścieżki WebView: nie mamy tu sygnału play/pause (wideo gra
// wewnątrz strony hostera), więc trzymamy wake-lock przez cały czas życia ekranu.
React.useEffect(() => {
keepScreenOn();
return allowScreenOff;
}, []);
// Dispatch dla movie vs scene progress endpoint (jak w NativeVideoPlayer).
const upsertWatchProgress = React.useCallback(
(body: { position_sec: number; duration_sec?: number; finished?: boolean }) =>