"""Source ranking — policzona ocena 0-5★ per origin (tube źródło) dla Sites screen. User request: ranking stron-źródeł wg (1) częstotliwości odświeżania, (2) bogactwa metadanych (miniaturka/tagi/desc/aktorzy), (3) szybkości działania (czy realnie gra). 0★ = offline. Liczone okresowo przez `run_source_stats` (scheduler), bo richness to agregat po ~2M live playback_sources × scenach — za drogie na request /sources. Jeden wiersz per origin. `components` (JSONB) trzyma surowe składowe do rozkładu w UI (% thumb/tag/perf/desc/studio/dur, telemetria health, basis). Top-level kolumny to gotowe gwiazdki, żeby /sources tylko czytał i sortował. """ from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, Integer, SmallInteger, String, func from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base class SourceStats(Base): __tablename__ = "source_stats" origin: Mapped[str] = mapped_column(String(64), primary_key=True) # Ocena ogólna 0-5 (0 = offline). Gate: health==0 → stars 0. stars: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0) freshness: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0) richness: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0) # None gdy brak jakiegokolwiek sygnału (nie powinno się zdarzyć — proxy zawsze daje). health: Mapped[int | None] = mapped_column(SmallInteger, nullable=True) scenes: Mapped[int] = mapped_column(Integer, nullable=False, default=0) new_7d: Mapped[int] = mapped_column(Integer, nullable=False, default=0) newest_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # Surowe składowe do rozkładu w UI + diagnostyki. Patrz run_source_stats. components: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) computed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False )