"""Linki do odtwarzania scen z zewnętrznych tube/agregatorów. Jedna kanoniczna scena może mieć N source'ów (jedna scena pojawia się na 5 tube'ach). Każdy source ma `page_url` (zawsze) i opcjonalnie `embed_url` / `stream_url` jeśli scraper potrafi je odkryć. Dedup — po (origin, page_url) — żeby ten sam scrap nie tworzył duplikatów. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( DateTime, ForeignKey, Integer, String, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, TimestampMixin, UUIDPKMixin class PlaybackSource(UUIDPKMixin, TimestampMixin, Base): __tablename__ = "playback_sources" __table_args__ = (UniqueConstraint("origin", "page_url"),) scene_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("scenes.id", ondelete="CASCADE"), nullable=False, index=True, ) origin: Mapped[str] = mapped_column(String(64), nullable=False, index=True) """Krótka nazwa źródła, np. 'xmoviesforyou', 'yourdailypornvideos'.""" page_url: Mapped[str] = mapped_column(String(2048), nullable=False) """Strona oryginalna z odtwarzaczem (deep link do scene page).""" embed_url: Mapped[str | None] = mapped_column(String(2048)) """URL playera w iframe (jeśli scraper go znalazł). Może być None.""" stream_url: Mapped[str | None] = mapped_column(String(2048)) """Bezpośredni URL do strumienia (m3u8/mp4) jeśli scraper potrafił go wyciągnąć. Tylko z tym polem mobile może odpalić native player.""" quality: Mapped[str | None] = mapped_column(String(16)) """np. '720p', '1080p', '4k'. None = unknown.""" duration_sec: Mapped[int | None] = mapped_column(Integer) thumbnail_url: Mapped[str | None] = mapped_column(String(2048)) animated_thumbnail_url: Mapped[str | None] = mapped_column(String(2048)) last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) dead_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, index=True ) """Set gdy resolve endpoint dostanie HosterDead / 404 'Video is offline'/'deleted' z tube'a. API filtruje takie sources z listy. None = aktywny.""" dead_reason: Mapped[str | None] = mapped_column(String(512), nullable=True)