implement JellyStat API for watch history, genre summary, and user summary; add PostgreSQL connection pool and update requirements

This commit is contained in:
2026-05-25 13:54:30 +02:00
parent 4b87b817a8
commit 0151c8210e
10 changed files with 743 additions and 35 deletions
+106
View File
@@ -0,0 +1,106 @@
"""JellyStat REST API — watch history, genre summary, and user summary."""
from __future__ import annotations
import asyncpg
from fastapi import APIRouter, Depends, Query
from gateway.jellystat.db import get_pool
from gateway.jellystat.models import (
GenreSummaryResponse,
UserSummaryResponse,
WatchHistoryResponse,
)
router = APIRouter(prefix="/jellystat", tags=["jellystat"])
DEFAULT_WINDOW_MINUTES = 10080 # 7 days
# ---------------------------------------------------------------------------
# GET /jellystat/history/{user_id}
# ---------------------------------------------------------------------------
@router.get("/history/{user_id}", response_model=WatchHistoryResponse)
async def get_watch_history(
user_id: str,
minutes: int = Query(
default=DEFAULT_WINDOW_MINUTES, ge=1, description="Time window in minutes"
),
pool: asyncpg.Pool = Depends(get_pool),
):
"""Return watch history grouped by title, ordered by most-watched first."""
rows = await pool.fetch(
"SELECT * FROM fn_user_watch_history($1, $2)", user_id, minutes
)
return WatchHistoryResponse(
user_id=user_id,
window_minutes=minutes,
items=[
{
"title": r["title"],
"watch_time_sec": float(r["watch_time_sec"]),
"media_type": r["media_type"],
}
for r in rows
],
)
# ---------------------------------------------------------------------------
# GET /jellystat/genres/{user_id}
# ---------------------------------------------------------------------------
@router.get("/genres/{user_id}", response_model=GenreSummaryResponse)
async def get_genre_summary(
user_id: str,
minutes: int = Query(
default=DEFAULT_WINDOW_MINUTES, ge=1, description="Time window in minutes"
),
pool: asyncpg.Pool = Depends(get_pool),
):
"""Return total watch time per genre, ordered by most-watched first."""
rows = await pool.fetch(
"SELECT * FROM fn_user_genre_summary($1, $2)", user_id, minutes
)
return GenreSummaryResponse(
user_id=user_id,
window_minutes=minutes,
genres=[
{"genre": r["genre"], "watch_time_sec": float(r["watch_time_sec"])}
for r in rows
],
)
# ---------------------------------------------------------------------------
# GET /jellystat/summary/{user_id}
# ---------------------------------------------------------------------------
@router.get("/summary/{user_id}", response_model=UserSummaryResponse)
async def get_user_summary(
user_id: str,
pool: asyncpg.Pool = Depends(get_pool),
):
"""Return all-time summary: total watch time, most-watched titles, top genres."""
rows = await pool.fetch("SELECT * FROM fn_user_summary($1)", user_id)
# fn_user_summary returns key-value rows — build a dict
# asyncpg already deserialises JSONB → Python objects
metrics: dict[str, object] = {r["metric"]: r["value"] for r in rows}
top_genres_raw = metrics.get("top_genres", [])
top_genres: list[str] = top_genres_raw if isinstance(top_genres_raw, list) else []
return UserSummaryResponse(
user_id=user_id,
total_watch_time_sec=float(metrics.get("total_watch_time", 0)),
most_watched_series=metrics.get("most_watched_series"),
most_watched_movie=metrics.get("most_watched_movie"),
total_last_30d_sec=float(metrics.get("total_last_30d", 0)),
total_last_7d_sec=float(metrics.get("total_last_7d", 0)),
top_genres=top_genres,
)