1 755 421 scen nie ma dlugosci ani na scenie, ani na zrodle, wiec wypadaja z feedu: w SQL "duration_sec >= 60" nie jest prawda dla NULL, przez co scena o nieznanej dlugosci znika tak samo jak dziesieciosekundowy smiec. Filtr zostaje, bo odcina smieci, uzupelniamy dane. Swiadomie waskie originy. Uniwersalny regex trafia w smieci: porntrex ma w HTML "duration: 5" reklamy, mypornerleak dlugosc ciastka 720, a xhamster i porntrex wypisuja dlugosci filmow z paska obok (naiwne dopasowanie dalo 198s zamiast 240s). Zla dlugosc jest gorsza niz brak, bo dedup opiera sie na niej najmocniej. - xvideos, xnxx: VideoObject LD-JSON, 5/5 na probce - xhamster: videoEntity z kontrola zgodnosci id z URL-a - porntrex pominiety: strona szczegolow nie podaje wlasnej dlugosci Kolejka jak przy phashu (plan raz, mielenie paczkami, przezywa restart): 845 899 scen, 500 stron co 5 min, czyli ok. 6 dni. Plan powtarza sie co 24h, zeby swieze sceny bez dlugosci tez trafialy do kolejki. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
3.4 KiB
Python
67 lines
3.4 KiB
Python
"""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":<id z URL>,…,"duration":<sekundy>}`, 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
|
|
)
|