fix: keep-screen-on podczas odtwarzania + on-demand miniaturki youporn
Some checks failed
Backend tests / test (push) Has been cancelled
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 & 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:
parent
0c5148b6e8
commit
55da508f9c
5 changed files with 171 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (`&`) — 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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
54
mobile/src/lib/keepAwake.ts
Normal file
54
mobile/src/lib/keepAwake.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }) =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue