"""Kolejka scen, którym trzeba doliczyć długość ze strony tuba. Filtr `min_duration_sec=60` odcina śmieciowe klipy i tak ma zostać, ale w SQL-u `duration_sec >= 60` NIE jest prawdą dla NULL, więc scena o NIEZNANEJ długości znika z feedu tak samo jak dziesięciosekundowy śmieć. Audyt 2026-08-20: **1 755 421 scen** nie ma długości ani na scenie, ani na źródle, czyli scraper nigdy jej nie zebrał. Największe zaległości to porntrex (676 tys.), xhamster (361 tys.), xvideos (259 tys.) i xnxx (226 tys.). **Dlaczego nie jeden uniwersalny regex po stronie szczegółów.** Bo trafia w śmieci. Na próbce: porntrex ma w HTML `duration: '5'` (długość reklamy), mypornerleak `disclaimer_cookie_duration":"720"` (ciastko), a xhamster i porntrex wypisują długości WSZYSTKICH filmów powiązanych z paska obok. Naiwne dopasowanie przy jednym xhamsterze dało 198 s zamiast prawdziwych 240 s. Zła długość jest gorsza niż jej brak, bo dedup traktuje długość jako najmocniejszy pojedynczy sygnał i skleiłby obce sceny. Dlatego backfill obsługuje tylko originy z POTWIERDZONYM, punktowym wzorcem (`app/extractors/duration_extract.py`), a nie wszystko, co się nawinie: * xvideos, xnxx: `VideoObject` LD-JSON z ISO 8601, sprawdzone 5/5 * xhamster: `"videoEntity":{"id":,…,"duration":}`, z kontrolą zgodności id, bo bez niej łapie film z paska obok porntrex świadomie NIE jest tu obsługiwany: jego strona szczegółów w ogóle nie podaje własnej długości (`video_duration` w flashvars nie występuje), a jedyne `durations` w HTML należą do filmów powiązanych. Dla niego drogą jest listing, bo kafelek długość pokazuje i `porntrex_browse.py` już ją stamtąd czyta. Kolejka jest tabelą z tego samego powodu co przy phashu: wyznaczenie kandydatów to ciężki przegląd całego katalogu, a mielenie ma przeżyć restart workera i pokazywać postęp. Patrz [[phash_backfill_queue]] dla bliźniaczego wzorca. """ 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 DurationBackfillQueue(Base): __tablename__ = "duration_backfill_queue" scene_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("scenes.id", ondelete="CASCADE"), primary_key=True, ) #: Strona tuba, z której czytamy długość. Zapisana przy planowaniu, żeby mielenie #: nie musiało co paczkę wracać do `playback_sources`. page_url: Mapped[str] = mapped_column(String(1024), nullable=False) #: Decyduje, KTÓRY punktowy wzorzec zostanie użyty. Bez tego wracamy do zgadywania. origin: Mapped[str] = mapped_column(String(64), nullable=False) #: Rośnie przy każdej nieudanej próbie; po wyczerpaniu limitu wiersz jest odkładany, #: żeby skasowana strona nie blokowała kolejki w nieskończoność. attempts: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0") #: Ustawione po sukcesie ALBO po wyczerpaniu prób (wtedy długość została pusta). 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 )