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>
49 lines
1.7 KiB
Python
49 lines
1.7 KiB
Python
"""Playback telemetry — POST /playback-events (fire-and-forget z apki).
|
|
|
|
Apka po każdej próbie odtwarzania woła to z origin sceny + wynikiem (success/error).
|
|
Sygnał zasila health/szybkość w rankingu źródeł (run_source_stats agreguje okno 7d).
|
|
Best-effort: apka NIE czeka i ignoruje błąd — telemetria nie może psuć playbacku.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Annotated, Literal
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.device import get_device_id
|
|
from app.auth import require_api_key
|
|
from app.db import get_session
|
|
from app.models.playback_event import PlaybackEvent
|
|
|
|
router = APIRouter(prefix="/playback-events", tags=["telemetry"], dependencies=[Depends(require_api_key)])
|
|
|
|
|
|
class PlaybackEventIn(BaseModel):
|
|
# 'tube:<sitetag>' — zgodne z playback_sources.origin. Bez tego ping bezużyteczny.
|
|
origin: str = Field(min_length=1, max_length=64)
|
|
status: Literal["success", "error"]
|
|
scene_id: uuid.UUID | None = None
|
|
error_kind: str | None = Field(default=None, max_length=64)
|
|
ttff_ms: int | None = Field(default=None, ge=0, le=600_000)
|
|
|
|
|
|
@router.post("", status_code=204)
|
|
def post_playback_event(
|
|
body: PlaybackEventIn,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
device_id: Annotated[str, Depends(get_device_id)],
|
|
) -> None:
|
|
session.add(
|
|
PlaybackEvent(
|
|
origin=body.origin,
|
|
scene_id=body.scene_id,
|
|
status=body.status,
|
|
error_kind=body.error_kind,
|
|
ttff_ms=body.ttff_ms if body.status == "success" else None,
|
|
device_id=device_id,
|
|
)
|
|
)
|
|
session.commit()
|