"""Klipy do feedu Swipe (sekcja w stylu TikToka). Osobny wertykał, NIE sceny. Klip to 5-120 s pionowego wideo od twórcy-amatora (RedGifs, Pornhub Shorties, nsfwswipe), bez obsady z kanonu TPDB, bez dedupu ze scenami i bez filtra 60 s, którym grid ukrywa krótkie rzeczy. Dlatego własne tabele zamiast wciskania tego w `scenes`: inne kolumny (twórca, audio, lajki, nisze), inny cykl życia (twórcy kasują klipy, więc reaper co dobę), inna taksonomia (tagi RedGifs to nie tagi Goona, 1495 vs nasze). `(source, source_id)` identyfikuje klip u źródła (`redgifs` + id gifa). Adresy plików RedGifs są publiczne i bez podpisu (`media.redgifs.com/.mp4`), więc trzymamy je wprost i telefon gra direct, bez ekstraktora i bez VPS-a. Pornhub Shorties (etap późniejszy) będzie miał `hd_url` puste i resolve na żądanie. Tagi w osobnej tabeli z indeksem `(tag, clip_id)`: feed dobiera kandydatów skanem po tagu od losowego uuid (jak `sort=random` w `list_scenes`), więc para w tej kolejności to jego główny indeks. Nisze (społeczności RedGifs) jako tablica, bo służą tylko onboardingowi i blacklistom, nie skanom. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.dialects.postgresql import ARRAY, UUID from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, TimestampMixin, UUIDPKMixin class Clip(UUIDPKMixin, TimestampMixin, Base): __tablename__ = "clips" __table_args__ = ( UniqueConstraint("source", "source_id", name="uq_clips_source_source_id"), Index("ix_clips_published_at", "published_at"), Index("ix_clips_creator", "creator"), ) #: `redgifs` | `pornhub` | `nsfwswipe` source: Mapped[str] = mapped_column(String(16), nullable=False) source_id: Mapped[str] = mapped_column(String(64), nullable=False) title: Mapped[str | None] = mapped_column(Text, nullable=True) #: Pseudonim twórcy u źródła (RedGifs `userName`). Nie łączymy z `performers`. creator: Mapped[str | None] = mapped_column(String(128), nullable=True) creator_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) duration_sec: Mapped[float | None] = mapped_column(Float, nullable=True) width: Mapped[int | None] = mapped_column(Integer, nullable=True) height: Mapped[int | None] = mapped_column(Integer, nullable=True) has_audio: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) #: RedGifs `contentType`, np. "Solo Female". content_type: Mapped[str | None] = mapped_column(String(32), nullable=True) niches: Mapped[list[str]] = mapped_column(ARRAY(String(64)), nullable=False, default=list) hd_url: Mapped[str | None] = mapped_column(Text, nullable=True) sd_url: Mapped[str | None] = mapped_column(Text, nullable=True) poster_url: Mapped[str | None] = mapped_column(Text, nullable=True) thumb_url: Mapped[str | None] = mapped_column(Text, nullable=True) #: Źródło deklaruje wariant HLS (RedGifs `hls`). Informacyjne, gramy mp4. hls: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) likes: Mapped[int] = mapped_column(Integer, nullable=False, default=0) views: Mapped[int] = mapped_column(Integer, nullable=False, default=0) #: Data publikacji u źródła (RedGifs `createDate`), do odznaki „nowe" i sortu. published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) dead_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) dead_reason: Mapped[str | None] = mapped_column(String(128), nullable=True) class ClipTag(Base): __tablename__ = "clip_tags" __table_args__ = (Index("ix_clip_tags_tag_clip", "tag", "clip_id"),) clip_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("clips.id", ondelete="CASCADE"), primary_key=True ) #: Slug tagu u źródła, małe litery, spacje jako myślniki ("big-tits"). tag: Mapped[str] = mapped_column(String(64), primary_key=True)