diff --git a/app/api/scenes.py b/app/api/scenes.py index 0b73403..265a0f1 100644 --- a/app/api/scenes.py +++ b/app/api/scenes.py @@ -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) 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) is_fav = session.get(FavoriteScene, scene.id) is not None diff --git a/app/extractors/__init__.py b/app/extractors/__init__.py index 3e70dec..ade775d 100644 --- a/app/extractors/__init__.py +++ b/app/extractors/__init__.py @@ -193,6 +193,15 @@ def supported_sitetags() -> tuple[str, ...]: 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__ = [ "try_extract", "supported_sitetags", diff --git a/app/extractors/tubes/sxyprn.py b/app/extractors/tubes/sxyprn.py index 30cc570..1bc5c54 100644 --- a/app/extractors/tubes/sxyprn.py +++ b/app/extractors/tubes/sxyprn.py @@ -21,7 +21,7 @@ import logging import re 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__) @@ -48,6 +48,12 @@ def _boo(ss: int, es: int) -> str: def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: 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) if not m: log.warning("sxyprn: no data-vnfo in %s", page_url)