fix(playback): mark deleted sxyprn posts dead + rank native sources first

Two bug-report fixes (2026-06-07):
- sxyprn returns HTTP 200 "Post Not Found" for deleted posts (soft-404), so the
  extractor returned None → resolve treated it as transient and never marked the
  source dead, leaving a dead link offered forever. Now raise HosterDead on the
  marker so resolve marks it dead.
- Scene playback sources were ordered alphabetically by origin, so a WebView-
  fallback hoster (fpoxxx, IP-bound + ad-heavy) ranked above a working native
  source (freshporno) on the same scene. Add is_vps_blocked_fallback() and sort
  native-resolve origins ahead of WebView-fallback ones.

Verified on prod: sxyprn dead URL → HosterDead; scene sources reorder
freshpornoorg before fpoxxx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-07 14:09:01 +02:00
parent 4d14f3946b
commit 8c0edbdf7b
3 changed files with 30 additions and 1 deletions

View file

@ -653,6 +653,20 @@ def _build_scene_out(session: Session, scene: Scene) -> SceneOut:
out.animated_thumbnail_url = _wrap_image_proxy(out.animated_thumbnail_url, p.page_url) out.animated_thumbnail_url = _wrap_image_proxy(out.animated_thumbnail_url, p.page_url)
playback_out.append(out) playback_out.append(out)
# Rank natywne-resolve źródła PRZED WebView-fallback (IP-bound/ad-heavy: fpoxxx,
# pornxpph, pornhub...). Query był alfabetyczny po origin, więc np. fpoxxx-WebView
# pokazywał się przed działającym freshporno (bug-report 2026-06-07). Stabilny sort:
# natywne (0) → fallback (1), tie-break po origin.
from app.extractors import is_vps_blocked_fallback
def _resolve_rank(origin: str | None) -> int:
if not origin:
return 1
sitetag = origin.split(":", 1)[1] if ":" in origin else origin
return 1 if is_vps_blocked_fallback(sitetag) else 0
playback_out.sort(key=lambda o: (_resolve_rank(o.origin), o.origin or ""))
progress = session.get(ScenePlayProgress, scene.id) progress = session.get(ScenePlayProgress, scene.id)
is_fav = session.get(FavoriteScene, scene.id) is not None is_fav = session.get(FavoriteScene, scene.id) is not None

View file

@ -193,6 +193,15 @@ def supported_sitetags() -> tuple[str, ...]:
return tuple(_REGISTRY.keys()) return tuple(_REGISTRY.keys())
def is_vps_blocked_fallback(sitetag: str) -> bool:
"""True gdy sitetag resolvuje się TYLKO przez WebView fallback (IP-bound CDN /
ad-heavy / CAPTCHA np. fpoxxx, pornxpph, pornhubcom). Takie źródła dają gorszy
UX (reklamy, czarny ekran) niż natywny KVS/direct resolve, więc UI powinien je
rankować NIŻEJ gdy scena ma też natywne źródło (bug-report 2026-06-07: scena
pokazywała fpoxxx-WebView przed działającym freshporno bo sort był alfabetyczny)."""
return _REGISTRY.get(sitetag) is _vps_blocked_fallback.extract
__all__ = [ __all__ = [
"try_extract", "try_extract",
"supported_sitetags", "supported_sitetags",

View file

@ -21,7 +21,7 @@ import logging
import re import re
from app.extractors._fetch import fetch_tube_html from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource from app.extractors._models import HosterDead, StreamSource
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -48,6 +48,12 @@ def _boo(ss: int, es: int) -> str:
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html = fetch_tube_html(page_url, timeout=timeout) html = fetch_tube_html(page_url, timeout=timeout)
# sxyprn soft-404: usunięty post zwraca HTTP 200 ze stroną "Post Not Found"
# (nie 404), więc bez tego extractor zwracał None → resolve traktował jako
# transient i NIGDY nie oznaczał źródła dead → user wciąż dostawał martwy link
# (bug-report 2026-06-07, scena 75aa3316). Raise HosterDead → resolve mark-dead.
if "Post Not Found" in html:
raise HosterDead(f"sxyprn {page_url}: post deleted (Post Not Found)")
m = _VNFO_RE.search(html) m = _VNFO_RE.search(html)
if not m: if not m:
log.warning("sxyprn: no data-vnfo in %s", page_url) log.warning("sxyprn: no data-vnfo in %s", page_url)