implement JellyStat API for watch history, genre summary, and user summary; add PostgreSQL connection pool and update requirements
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user