goon/app/models/source_stats.py
jtrzupek c154deab37 feat(sources): 0-5★ ranking on Sites (freshness/metadata/plays) + playback telemetry
Rates each source on three axes the user asked for:
- freshness: how recently/often new content arrives (newest age + 7d volume)
- richness: metadata coverage (thumbnail/tags/performers/description/studio/duration)
- plays: does it actually play — from real playback telemetry when available,
  else a proxy from the resolve mechanism. 0★ = offline (gates the overall stars,
  so a fresh+rich source that doesn't play still ranks bottom — the hqfap/4k69 case)

Backend:
- playback_events: fire-and-forget telemetry POST from the app per playback attempt
  (origin + success/error + time-to-first-frame), append-only, 30d retention
- source_stats: per-origin computed scores, refreshed by a scheduler job (6h);
  /sources joins it and sorts by stars
- models + local migration 0025; new GOON_SCHED_SOURCE_STATS_HOURS setting

Mobile:
- Sites rows show ★ rating; tap the stars for a breakdown (axes + metadata %, plus
  whether "plays" is measured or estimated)
- PlayerScreen reports playback success/failure per source (native path only —
  symmetric, conservative); origin threaded through Scene/Movie play callsites

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:00:59 +02:00

40 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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
)