fix(sites): sortuj po dacie premiery z fallbackiem na date dodania + napraw miniaturki
Zgloszenie 1df50de3 ("hqporner: brak nowych scen ze strony, brak miniaturek").
Oba objawy mialy inne przyczyny i oba sie potwierdzily.
1. Kolejnosc. Sceny byly i ingest dzialal (91 nowych w 7 dni, ostatnia tego dnia),
ale tuby daty premiery zwykle NIE podaja: xhamster 99,2% scen bez daty, xnxx 97,7%,
porntrex 95,8%, hqporner 93,5%, sxyprn 85,7%. Przy release_date DESC NULLS LAST
wszystkie bezdatowe ladowaly ZA datowanymi, wiec 82 z tych 91 scen siedzialy za
3,4 tys. starszych pozycji. Teraz sortujemy po dacie premiery, a gdy jej brak, po
dniu trafienia do katalogu; sceny z prawdziwa data (kanon) bez zmian. Wyrazenie jest
immutable (jawne AT TIME ZONE 'UTC'), wiec wchodzi do indeksu, migracja 0033.
2. Miniaturki. Kafelki bez obrazka byly podpisane FULLPORNER, nie HQPORNER. Picker
bral pierwsze zrodlo alfabetycznie, a tube:fullporner < tube:hqpornercom. Dwie wady
naraz: czesc miniaturek fullporner to 153-bajtowa atrapa oddawana z kodem 200, a
czesc wskazuje CDN hqporner, ktory odrzuca Referer fullporner.com (ten sam plik:
Referer fullporner.com 403, wlasny host obrazka 200, 30 kB). Wiec: przegladany
serwis wygrywa wybor miniaturki, a proxy obrazkow po 403 ponawia raz z wlasnym
originem obrazka.
Pomiar na 40 kafelkach hqporner: przed 38 ladowalo i 1 nie, po 39 ladowalo i 0 nie,
wszystkie podpisane wlasciwym serwisem. Glowny feed bez zmian.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
34276afb71
commit
fa7fb668b6
3 changed files with 125 additions and 7 deletions
48
alembic/versions/20260820_0033_effective_date_idx.py
Normal file
48
alembic/versions/20260820_0033_effective_date_idx.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""indeks pod sortowanie po dacie premiery z fallbackiem na date dodania
|
||||||
|
|
||||||
|
Zgłoszenie 1df50de3 („hqporner: brak nowych scen"). Sceny były i ingest działał, tylko
|
||||||
|
tuby daty premiery zwykle NIE podają: xhamster 99,2% scen bez daty, xnxx 97,7%,
|
||||||
|
porntrex 95,8%, hqporner 93,5%, sxyprn 85,7%. Przy `release_date DESC NULLS LAST`
|
||||||
|
wszystkie bezdatowe lądowały ZA datowanymi, więc 82 z 91 scen dodanych w hqporner
|
||||||
|
przez tydzień siedziały za 3,4 tys. starszych pozycji i użytkownik widział pustkę.
|
||||||
|
|
||||||
|
Sortujemy więc po dacie premiery, a gdy jej nie ma, po dniu trafienia sceny do
|
||||||
|
katalogu. Sceny z prawdziwą datą (kanon z TPDB/StashDB) zachowują dotychczasową
|
||||||
|
kolejność.
|
||||||
|
|
||||||
|
Wyrażenie MUSI być immutable, żeby weszło do indeksu. `created_at::date` zależy od
|
||||||
|
strefy czasowej sesji (stable, nie immutable), stąd jawne `AT TIME ZONE 'UTC'`.
|
||||||
|
Musi też być IDENTYCZNE z `_effective_date_desc()` w `app/api/scenes.py`, inaczej
|
||||||
|
planner indeksu nie użyje i wróci do pełnego skanu.
|
||||||
|
|
||||||
|
Pomiar po założeniu: hqporner Index Scan bez sortowania, 0,49 s; sxyprn, xhamster
|
||||||
|
i porntrex po 0,02 s, wszystkie z dzisiejszymi scenami na górze.
|
||||||
|
|
||||||
|
`IF NOT EXISTS`, bo na produkcji założony od razu (CONCURRENTLY, bez blokowania ruchu).
|
||||||
|
|
||||||
|
Revision ID: 0033_effective_date_idx
|
||||||
|
Revises: 0032_origin_release_idx
|
||||||
|
Create Date: 2026-08-20
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0033_effective_date_idx"
|
||||||
|
down_revision: str | None = "0032_origin_release_idx"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_scenes_effective_date_desc ON scenes ("
|
||||||
|
" (COALESCE(release_date, (created_at AT TIME ZONE 'UTC')::date)) DESC,"
|
||||||
|
" created_at DESC,"
|
||||||
|
" id DESC"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_scenes_effective_date_desc")
|
||||||
|
|
@ -8,7 +8,7 @@ from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import distinct, exists, false, func, literal_column, select
|
from sqlalchemy import Date, distinct, exists, false, func, literal_column, select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
@ -42,6 +42,20 @@ router = APIRouter(prefix="/scenes", tags=["scenes"], dependencies=[Depends(requ
|
||||||
|
|
||||||
_VALID_SORTS = {"created_at", "release_date", "title", "studio"}
|
_VALID_SORTS = {"created_at", "release_date", "title", "studio"}
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_date_desc():
|
||||||
|
"""`release_date`, a gdy go brak, dzien trafienia sceny do katalogu, malejaco.
|
||||||
|
|
||||||
|
MUSI byc identyczne z wyrazeniem w `ix_scenes_effective_date_desc`, inaczej planner
|
||||||
|
indeksu nie uzyje i wroci do pelnego skanu `scenes`. `created_at::date` NIE nadaje
|
||||||
|
sie do indeksu, bo zalezy od strefy czasowej sesji (stable, nie immutable), stad
|
||||||
|
jawne AT TIME ZONE 'UTC'.
|
||||||
|
"""
|
||||||
|
return func.coalesce(
|
||||||
|
Scene.release_date,
|
||||||
|
func.cast(func.timezone("UTC", Scene.created_at), Date),
|
||||||
|
).desc()
|
||||||
|
|
||||||
# TTL-cache dla count'u scen-z-żywym-playback (default lista bez filtra). Full-scan
|
# TTL-cache dla count'u scen-z-żywym-playback (default lista bez filtra). Full-scan
|
||||||
# 1.69M scen + EXISTS ~950ms; liczba zmienia się wolno i jest przybliżona (header
|
# 1.69M scen + EXISTS ~950ms; liczba zmienia się wolno i jest przybliżona (header
|
||||||
# paginacji), więc 10-min cache w pamięci procesu API jest akceptowalny trade-off.
|
# paginacji), więc 10-min cache w pamięci procesu API jest akceptowalny trade-off.
|
||||||
|
|
@ -438,8 +452,18 @@ def list_scenes(
|
||||||
# więc kolejne strony nakładają się na siebie → feed "powtarza się" przy scrollu
|
# więc kolejne strony nakładają się na siebie → feed "powtarza się" przy scrollu
|
||||||
# (report 304c9258). created_at desc dalej pierwszy, więc index-walk zostaje.
|
# (report 304c9258). created_at desc dalej pierwszy, więc index-walk zostaje.
|
||||||
if sort == "release_date":
|
if sort == "release_date":
|
||||||
|
# Data premiery LUB, gdy jej nie ma, data trafienia do katalogu. Czyste
|
||||||
|
# `release_date DESC NULLS LAST` chowalo swiezosc, bo tuby daty premiery zwykle
|
||||||
|
# NIE podaja: xhamster 99,2% scen bez daty, xnxx 97,7%, porntrex 95,8%, hqporner
|
||||||
|
# 93,5%, sxyprn 85,7%. Wszystkie NULL-e ladowaly za datowanymi, wiec 82 z 91 scen
|
||||||
|
# dodanych w hqporner przez tydzien siedzialo za 3,4 tys. starszych pozycji i
|
||||||
|
# user widzial "brak nowych scen" (zgloszenie 1df50de3), mimo ze ingest dzialal.
|
||||||
|
# Sceny z prawdziwa data (kanon z TPDB/StashDB) sortuja sie jak dotad.
|
||||||
|
# Wyrazenie musi byc IMMUTABLE, zeby weszlo do indeksu: `created_at::date` zalezy
|
||||||
|
# od strefy sesji, stad jawne AT TIME ZONE 'UTC'. Indeks:
|
||||||
|
# ix_scenes_effective_date_desc (migracja 0033).
|
||||||
ordered = base.order_by(
|
ordered = base.order_by(
|
||||||
Scene.release_date.desc().nullslast(), Scene.created_at.desc(), Scene.id.desc()
|
_effective_date_desc(), Scene.created_at.desc(), Scene.id.desc()
|
||||||
)
|
)
|
||||||
elif sort == "title":
|
elif sort == "title":
|
||||||
ordered = base.order_by(
|
ordered = base.order_by(
|
||||||
|
|
@ -451,7 +475,7 @@ def list_scenes(
|
||||||
base.outerjoin(Studio, Studio.id == Scene.studio_id)
|
base.outerjoin(Studio, Studio.id == Scene.studio_id)
|
||||||
.order_by(
|
.order_by(
|
||||||
Studio.name_normalized.asc().nullslast(),
|
Studio.name_normalized.asc().nullslast(),
|
||||||
Scene.release_date.desc().nullslast(),
|
_effective_date_desc(),
|
||||||
Scene.created_at.desc(),
|
Scene.created_at.desc(),
|
||||||
Scene.id.desc(),
|
Scene.id.desc(),
|
||||||
)
|
)
|
||||||
|
|
@ -485,7 +509,9 @@ def list_scenes(
|
||||||
total = (page - 1) * per_page + len(rows)
|
total = (page - 1) * per_page + len(rows)
|
||||||
total_capped = has_more
|
total_capped = has_more
|
||||||
|
|
||||||
items = _build_scenes_out_batch(session, list(rows), light=True, device_id=device_id)
|
items = _build_scenes_out_batch(
|
||||||
|
session, list(rows), light=True, device_id=device_id, prefer_origin=origin
|
||||||
|
)
|
||||||
|
|
||||||
return SceneListOut(
|
return SceneListOut(
|
||||||
items=items,
|
items=items,
|
||||||
|
|
@ -748,6 +774,17 @@ def _flaky_rank(url: str) -> int:
|
||||||
return 0 # nie-chwiejny (martwy rotting-thumb) — bez preferencji
|
return 0 # nie-chwiejny (martwy rotting-thumb) — bez preferencji
|
||||||
|
|
||||||
|
|
||||||
|
def _is_preferred_origin(origin: str | None, prefer_origin: str | None) -> bool:
|
||||||
|
"""Czy to origin, ktory user wlasnie przeglada (`/scenes?origin=`).
|
||||||
|
|
||||||
|
Porownanie substringiem, bo parametr API jest substringowy: `hqporner` ma zlapac
|
||||||
|
`tube:hqpornercom`.
|
||||||
|
"""
|
||||||
|
if not prefer_origin or not origin:
|
||||||
|
return False
|
||||||
|
return prefer_origin.lower() in origin.lower()
|
||||||
|
|
||||||
|
|
||||||
def _needs_proxy(url: str) -> bool:
|
def _needs_proxy(url: str) -> bool:
|
||||||
"""Wszystkie thumbnaile z playback_sources są proxowane przez backend.
|
"""Wszystkie thumbnaile z playback_sources są proxowane przez backend.
|
||||||
Większość CDN-ów porn-tube'ów wymaga Refera (hqporner, mypornerleak/58img,
|
Większość CDN-ów porn-tube'ów wymaga Refera (hqporner, mypornerleak/58img,
|
||||||
|
|
@ -771,7 +808,12 @@ def _wrap_image_proxy(url: str, referer: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _build_scenes_out_batch(
|
def _build_scenes_out_batch(
|
||||||
session: Session, scenes: list[Scene], *, light: bool = False, device_id: str = LEGACY_DEVICE
|
session: Session,
|
||||||
|
scenes: list[Scene],
|
||||||
|
*,
|
||||||
|
light: bool = False,
|
||||||
|
device_id: str = LEGACY_DEVICE,
|
||||||
|
prefer_origin: str | None = None,
|
||||||
) -> list[SceneOut]:
|
) -> list[SceneOut]:
|
||||||
"""Batch-fetch wszystkich relacji dla N scen w 7 zapytaniach (zamiast 7×N).
|
"""Batch-fetch wszystkich relacji dla N scen w 7 zapytaniach (zamiast 7×N).
|
||||||
|
|
||||||
|
|
@ -899,9 +941,16 @@ def _build_scenes_out_batch(
|
||||||
prev = thumb_fallback.get(sid)
|
prev = thumb_fallback.get(sid)
|
||||||
if prev is None or _flaky_rank(thumb) < _flaky_rank(prev[0]):
|
if prev is None or _flaky_rank(thumb) < _flaky_rank(prev[0]):
|
||||||
thumb_fallback[sid] = (thumb, page_url, origin)
|
thumb_fallback[sid] = (thumb, page_url, origin)
|
||||||
elif sid not in thumb_by_scene:
|
elif sid not in thumb_by_scene or _is_preferred_origin(origin, prefer_origin):
|
||||||
|
# Gdy user przeglada KONKRETNY serwis, jego miniaturka wygrywa.
|
||||||
|
# Wczesniej decydowala kolejnosc alfabetyczna originu, wiec na liscie
|
||||||
|
# hqporner kafelki scen wspoldzielonych z fullpornerem bralo
|
||||||
|
# `tube:fullporner` (< `tube:hqpornercom`), a czesc tych miniaturek to
|
||||||
|
# 153-bajtowa atrapa z imgs.xiaoshenke.net oddawana z kodem 200. Efekt:
|
||||||
|
# puste kafelki podpisane FULLPORNER na liscie hqporner, mimo ze ta
|
||||||
|
# sama scena miala obok zdrowa miniaturke hqporner (zgloszenie 1df50de3).
|
||||||
thumb_by_scene[sid] = (thumb, page_url, origin)
|
thumb_by_scene[sid] = (thumb, page_url, origin)
|
||||||
if sid not in anim_by_scene and anim:
|
if anim and (sid not in anim_by_scene or _is_preferred_origin(origin, prefer_origin)):
|
||||||
anim_by_scene[sid] = (anim, page_url, origin)
|
anim_by_scene[sid] = (anim, page_url, origin)
|
||||||
# Uzupełnij scenami które mają TYLKO rotting thumbnail (sxyprn-only).
|
# Uzupełnij scenami które mają TYLKO rotting thumbnail (sxyprn-only).
|
||||||
for sid, val in thumb_fallback.items():
|
for sid, val in thumb_fallback.items():
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,27 @@ async def proxy_image(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("img proxy fetch failed for %s: %s", target, e)
|
log.warning("img proxy fetch failed for %s: %s", target, e)
|
||||||
raise HTTPException(status_code=502, detail=f"img fetch failed: {e}") from e
|
raise HTTPException(status_code=502, detail=f"img fetch failed: {e}") from e
|
||||||
|
if r.status_code in (401, 403):
|
||||||
|
# Referer moze byc z INNEGO serwisu niz host obrazka. Zdarza sie, gdy tube
|
||||||
|
# zaciaga miniaturki cudzego CDN-a: fullporner zapisuje pliki z
|
||||||
|
# fastporndelivery.hqporner.com, a my dokladalismy Referer fullporner.com,
|
||||||
|
# ktorego CDN nie akceptuje. Pomiar 2026-08-20 na tym samym pliku:
|
||||||
|
# Referer fullporner.com 403 (153 B), brak Referera 403, Referer hqporner.com
|
||||||
|
# 200 (30 kB), Referer rowny wlasnemu hostowi obrazka 200. Skutkiem byly
|
||||||
|
# puste kafelki podpisane FULLPORNER na liscie hqporner (zgloszenie 1df50de3).
|
||||||
|
# Ponawiamy raz z wlasnym originem obrazka, bo to dziala dla calej tej klasy
|
||||||
|
# przypadkow i nie zmienia zachowania tam, gdzie pierwsza proba sie udaje.
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
parts = urlsplit(str(r.url) or target)
|
||||||
|
own = f"{parts.scheme}://{parts.netloc}/"
|
||||||
|
if own != (referer or ""):
|
||||||
|
try:
|
||||||
|
r2 = await client.get(target, headers=_build_headers(own))
|
||||||
|
if r2.status_code < 400:
|
||||||
|
r = r2
|
||||||
|
except Exception as e:
|
||||||
|
log.info("img proxy retry z wlasnym originem padl %s: %s", target, e)
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
# Upstream 4xx/5xx dla thumba, degraded zamiast raise (placeholder w mobile).
|
# Upstream 4xx/5xx dla thumba, degraded zamiast raise (placeholder w mobile).
|
||||||
# GOON-5 (Cloudflare 523) i GOON-D, bezsensowny noise w Sentry, lepiej
|
# GOON-5 (Cloudflare 523) i GOON-D, bezsensowny noise w Sentry, lepiej
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue