"""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 )