Nowy wertykal krotkich pionowych klipow tworcow, osobno od scen: inne kolumny (tworca, audio, lajki, nisze), inny cykl zycia, inna taksonomia, brak filtra 60 s. Tabele `clips` (unikalne source+source_id) i `clip_tags` z indeksem (tag, clip_id) pod skan po tagu od losowego uuid w przyszlym feedzie. Zrodlo nr 1: RedGifs, oficjalne API v2 z tokenem tymczasowym (zwiazany z IP i UA, na 401 odnowienie). Globalne `order=latest` bez search_text daje ~100 na strone, kazde zapytanie widzi maks. 10 000 wynikow, wiec katalog chodzi sie po niszach (skrypt backfillu). Pliki media.redgifs.com sa publiczne i bez podpisu (206 z VPS-a i z telefonu), do bazy wchodzi gotowy adres. Scheduler: job clips-latest co godzine (5 stron), zatrzymuje sie na stronie bez nowych. Reaper (HEAD, 403/404/410 -> dead_at) jako skrypt, upsert odzywia klip, ktory zrodlo znow podaje. API: GET /clips i /clips/stats do pilota; wlasciwy feed, pamiec widzianych i telemetria to etap 3. Sprawdzone tego dnia i odlozone: nsfwswipe (lustro v.redd.it, bez audio, stoi od 19.08), Pornhub Shorties (zrodlo nr 2, HLS z tokenem 2 h, Referer na segmentach, 0/110 performerow z kanonu).
85 lines
4.1 KiB
Python
85 lines
4.1 KiB
Python
"""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/<Id>.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)
|