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
|
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:
|
def _is_rotting_thumb(url: str) -> bool:
|
||||||
"""sxyprn/trafficdeposit miniaturki są czasowo podpisane i rotują (asset 404 po
|
"""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
|
~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 = {}
|
anim_by_scene: dict = {}
|
||||||
for sid, thumb, anim, page_url in pb_light:
|
for sid, thumb, anim, page_url in pb_light:
|
||||||
sxy = _sxyprn_thumb_url(page_url)
|
sxy = _sxyprn_thumb_url(page_url)
|
||||||
|
yp = _youporn_thumb_url(page_url, thumb)
|
||||||
if sxy:
|
if sxy:
|
||||||
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
|
# sxyprn → żywy on-demand resolver (martwy stored URL ignorujemy),
|
||||||
# tier fallback: użyty tylko gdy scena nie ma stabilniejszej miniatury.
|
# tier fallback: użyty tylko gdy scena nie ma stabilniejszej miniatury.
|
||||||
thumb_fallback.setdefault(sid, (sxy, page_url))
|
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:
|
elif thumb:
|
||||||
if _is_rotting_thumb(thumb):
|
if _is_rotting_thumb(thumb):
|
||||||
thumb_fallback.setdefault(sid, (thumb, page_url))
|
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;
|
# "na liście jest miniaturka, w scenie nie"). Podmieniamy na żywy resolver;
|
||||||
# inne martwe rotting-thumby zerujemy → mobile bierze kolejne źródło / placeholder.
|
# inne martwe rotting-thumby zerujemy → mobile bierze kolejne źródło / placeholder.
|
||||||
sxy = _sxyprn_thumb_url(p.page_url)
|
sxy = _sxyprn_thumb_url(p.page_url)
|
||||||
|
yp = _youporn_thumb_url(p.page_url, out.thumbnail_url)
|
||||||
if sxy:
|
if sxy:
|
||||||
out.thumbnail_url = sxy
|
out.thumbnail_url = sxy
|
||||||
|
elif yp:
|
||||||
|
out.thumbnail_url = yp
|
||||||
elif out.thumbnail_url and _is_rotting_thumb(out.thumbnail_url):
|
elif out.thumbnail_url and _is_rotting_thumb(out.thumbnail_url):
|
||||||
out.thumbnail_url = None
|
out.thumbnail_url = None
|
||||||
if out.thumbnail_url and _needs_proxy(out.thumbnail_url):
|
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
|
# 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.
|
# thumbnail" nie pomagał (sxyprn IP-bound, enrich z VPS pada). Report 2026-07-22.
|
||||||
sxy = _sxyprn_thumb_url(p.page_url)
|
sxy = _sxyprn_thumb_url(p.page_url)
|
||||||
|
yp = _youporn_thumb_url(p.page_url, out.thumbnail_url)
|
||||||
if sxy:
|
if sxy:
|
||||||
out.thumbnail_url = sxy
|
out.thumbnail_url = sxy
|
||||||
|
elif yp:
|
||||||
|
out.thumbnail_url = yp
|
||||||
elif out.thumbnail_url and _is_rotting_thumb(out.thumbnail_url):
|
elif out.thumbnail_url and _is_rotting_thumb(out.thumbnail_url):
|
||||||
out.thumbnail_url = None
|
out.thumbnail_url = None
|
||||||
# Wrap thumbnail URL-e przez backend image proxy gdy CDN wymaga Refera
|
# 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}$")
|
_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}")
|
@router.get("/sxyprn-thumb/{post_id}")
|
||||||
async def sxyprn_thumb(post_id: str) -> Response:
|
async def sxyprn_thumb(post_id: str) -> Response:
|
||||||
"""On-demand poster sxyprn. URL stabilny per post_id (klient cache'uje bajty);
|
"""On-demand poster sxyprn. URL stabilny per post_id (klient cache'uje bajty);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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',
|
id: '2026-07-26',
|
||||||
date: 'July 2026',
|
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 { useVideoPlayer, VideoView, type VideoSource } from 'expo-video';
|
||||||
import * as Clipboard from 'expo-clipboard';
|
import * as Clipboard from 'expo-clipboard';
|
||||||
import { setLastPlayback } from '../lib/lastPlayback';
|
import { setLastPlayback } from '../lib/lastPlayback';
|
||||||
|
import { allowScreenOff, keepScreenOn } from '../lib/keepAwake';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
|
@ -211,6 +212,15 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
const playingEvent = useEvent(player, 'playingChange', { isPlaying: player.playing });
|
const playingEvent = useEvent(player, 'playingChange', { isPlaying: player.playing });
|
||||||
const isPlaying = playingEvent?.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,
|
// 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.
|
// pozycja, ostatni błąd) bez pytania usera. Aktualizowany na zmianę źródła i statusu.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -1432,6 +1442,13 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Player'>>();
|
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Player'>>();
|
||||||
const { url, sceneId, entityKind, durationSec, refererHost } = params;
|
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).
|
// Dispatch dla movie vs scene progress endpoint (jak w NativeVideoPlayer).
|
||||||
const upsertWatchProgress = React.useCallback(
|
const upsertWatchProgress = React.useCallback(
|
||||||
(body: { position_sec: number; duration_sec?: number; finished?: boolean }) =>
|
(body: { position_sec: number; duration_sec?: number; finished?: boolean }) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue