Root cause zgloszenia 9d272101 ("5 duplikatow, czemu nie zmergowane?"):
phash mialo tylko 37,8% grywalnych scen. Licza go WYLACZNIE niektore
konektory w momencie ingestu, wiec najwieksze zrodla maja miniaturke dla
~100% scen, a odcisk dla 1-2% (xvideos 1,8%, xnxx 1,4%, eporner 1,0%).
Material jest zapisany, brakowalo tylko obliczenia.
W zglaszanym przypadku 7 wierszy o identycznej dlugosci 2324 s, a dwa
kafelki z IDENTYCZNA okladka nie mialy phasha wcale, wiec jedyny sygnal,
ktory by je sklail, nigdy nie powstal.
Zakres CELOWANY, nie caly katalog: sceny w klastrze (performer, dokladna
dlugosc) o licznosci >1, bez phasha, ale z zapisana miniaturka = 238 704
pozycje (wobec ~1,4 mln scen bez phasha). Reszta nie ma sie z czym skleic.
Kolejka jako TABELA, nie zapytanie w locie: wyznaczenie kandydatow to
ciezki agregat po calym katalogu, ktorego nie chcemy powtarzac przy kazdej
paczce, a kolejka przezywa restart workera i widac z niej postep.
Weryfikacja end-to-end na zglaszanej scenie: ce81a110 dostal phash
85c1bac2f8c60ecf, czyli DOKLADNIE ten sam, ktory mial juz 8373bbb5 (dwa
z kafelkow uzytkownika) -> istniejacy phash_exact je teraz sklei.
Nie zapisujemy wartosci z phash_blacklist (zaslepki dzielone przez setki
scen). Throttling swiadomy: 800 co 15 min, ~3 dni na nadrobienie.
Dwie pulapki SQL zapisane w kodzie: `referer` ma 255 znakow wiec origin
wycinamy w SQL (pelny page_url wywalal planowanie), a rzutowanie musi isc
przez CAST() bo text() SQLAlchemy nie podstawia parametru za ktorym stoi
`::`.
55 lines
2.6 KiB
Python
55 lines
2.6 KiB
Python
"""Kolejka scen do policzenia phasha z ZAPISANEJ już miniaturki.
|
|
|
|
Phash liczą wyłącznie niektóre konektory, w momencie ingestu. Reszta zapisuje
|
|
`thumbnail_url`, ale odcisku nie wylicza, więc audyt 2026-08-13 pokazał **37,8%**
|
|
pokrycia wśród grywalnych scen. Największe źródła mają miniaturkę dla ~100% scen,
|
|
a phash dla 1-2% (xvideos 1,8%, xnxx 1,4%, eporner 1,0%). Materiał jest, brakuje
|
|
tylko obliczenia.
|
|
|
|
Skutek widać w zgłoszeniu `9d272101` (strona Tiffani Time): siedem wierszy o
|
|
identycznej długości 2324 s, ta sama scena. Dwa kafelki miały IDENTYCZNĄ okładkę,
|
|
ale **żaden z nich nie miał phasha**, więc jedyny sygnał, który by je skleił, nigdy
|
|
nie powstał.
|
|
|
|
**Dlaczego kolejka jest tabelą, a nie zapytaniem liczonym w locie.** Wyznaczenie
|
|
kandydatów (klaster performer + dokładna długość, liczność > 1) to ciężki agregat po
|
|
całym katalogu, którego nie chcemy powtarzać przy każdej paczce. Liczymy go raz przy
|
|
`plan`, a `process` bierze potem tanie `LIMIT` po indeksie. Przy okazji kolejka
|
|
przeżywa restart workera i widać z niej postęp.
|
|
|
|
**Zakres celowany, nie cały katalog.** Kandydaci to sceny, które w ogóle mają z czym
|
|
się skleić: 241 763 pozycje wobec ~1,4 mln scen bez phasha. Reszta może go nie mieć,
|
|
bo i tak nie ma pary.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.models.base import Base
|
|
|
|
|
|
class PhashBackfillQueue(Base):
|
|
__tablename__ = "phash_backfill_queue"
|
|
|
|
scene_id: Mapped[uuid.UUID] = mapped_column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("scenes.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
#: Miniaturka zapisana przy ingeście; nie odpytujemy o nią strony ponownie.
|
|
thumbnail_url: Mapped[str] = mapped_column(String(1024), nullable=False)
|
|
#: Bez Referera część CDN-ów oddaje 403 (ta sama przyczyna co przy `/proxy/img`).
|
|
referer: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
#: Rośnie przy każdej nieudanej próbie; po `_MAX_ATTEMPTS` wiersz jest pomijany,
|
|
#: żeby martwy CDN nie blokował kolejki w nieskończoność.
|
|
attempts: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
|
|
#: Ustawione po sukcesie ALBO po wyczerpaniu prób (wtedy `phash` został pusty).
|
|
done_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|