Files
movie-night/app/routers/mood.py
T

160 lines
5.2 KiB
Python
Raw Normal View History

import json
import logging
import time
2026-03-14 19:57:50 -07:00
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException, Request
from app.config import settings
2026-03-14 19:57:50 -07:00
from app.database import get_db, get_unwatched_movies
from app.models import MoodRequest, MoodResponse, Recommendation
from app.routers.auth import get_current_user
from app.services.jellyfin import get_deep_link, get_poster_url
from app.services.llm import get_llm_provider
from app.services.prefilter import prefilter_candidates
logger = logging.getLogger("movie-night.mood")
router = APIRouter()
@router.post("/mood", response_model=MoodResponse)
async def get_mood_recommendations(request: Request, body: MoodRequest):
user = await get_current_user(request)
if not body.mood.strip():
raise HTTPException(status_code=400, detail="Please describe what you're in the mood for")
start_time = time.time()
# Build list of user IDs to check watch state against
user_ids = [user["id"]] + body.additional_user_ids
# Get unwatched movies from cache
unwatched_raw = await get_unwatched_movies(user_ids)
total_unwatched = len(unwatched_raw)
if total_unwatched == 0:
return MoodResponse(
recommendations=[],
meta={
"total_unwatched": 0,
"candidates_evaluated": 0,
"processing_time_ms": 0,
"mood": body.mood,
},
)
# Pre-filter to narrow candidates
candidates = prefilter_candidates(
unwatched_raw, body.mood, max_candidates=settings.max_candidates
)
logger.info(f"Pre-filtered {total_unwatched} movies to {len(candidates)} candidates for mood: '{body.mood}'")
# Get recommendations from LLM
try:
llm = get_llm_provider()
raw_recs = await llm.get_recommendations(
body.mood, candidates, max_results=settings.max_recommendations
)
except Exception as e:
logger.error(f"LLM error: {e}")
raise HTTPException(status_code=502, detail="Failed to get recommendations from AI. Please try again.")
# Validate and enrich recommendations
candidate_map = {m.jellyfin_id: m for m in candidates}
recommendations = []
for rec in raw_recs:
jf_id = rec.get("jellyfin_id")
if not jf_id or jf_id not in candidate_map:
logger.warning(f"LLM returned invalid jellyfin_id: {jf_id}")
continue
movie = candidate_map[jf_id]
recommendations.append(
Recommendation(
jellyfin_id=movie.jellyfin_id,
title=movie.title,
year=movie.year,
genres=movie.genres,
community_rating=movie.community_rating,
runtime_minutes=movie.runtime_minutes,
content_rating=movie.content_rating,
poster_url=get_poster_url(movie.jellyfin_id),
deep_link=get_deep_link(movie.jellyfin_id),
reasoning=rec.get("reasoning", "A great match for your mood!"),
match_score=rec.get("match_score", 0.5),
)
)
elapsed_ms = int((time.time() - start_time) * 1000)
2026-03-14 19:57:50 -07:00
meta = {
"total_unwatched": total_unwatched,
"candidates_evaluated": len(candidates),
"processing_time_ms": elapsed_ms,
"mood": body.mood,
}
# Save to search history
try:
db = await get_db()
try:
await db.execute(
"INSERT INTO search_history (user_id, mood, results, meta, created_at) VALUES (?, ?, ?, ?, ?)",
(
user["id"],
body.mood,
json.dumps([r.model_dump() for r in recommendations]),
json.dumps(meta),
datetime.now(timezone.utc).isoformat(),
),
)
await db.commit()
finally:
await db.close()
except Exception as e:
logger.warning(f"Failed to save search history: {e}")
return MoodResponse(recommendations=recommendations, meta=meta)
@router.get("/history")
async def get_search_history(request: Request):
user = await get_current_user(request)
db = await get_db()
try:
cursor = await db.execute(
"SELECT id, mood, results, meta, created_at FROM search_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 20",
(user["id"],),
)
rows = await cursor.fetchall()
return [
{
"id": row["id"],
"mood": row["mood"],
"results": json.loads(row["results"]),
"meta": json.loads(row["meta"]) if row["meta"] else {},
"created_at": row["created_at"],
}
for row in rows
]
finally:
await db.close()
@router.delete("/history/{entry_id}")
async def delete_history_entry(entry_id: int, request: Request):
user = await get_current_user(request)
db = await get_db()
try:
await db.execute(
"DELETE FROM search_history WHERE id = ? AND user_id = ?",
(entry_id, user["id"]),
)
await db.commit()
return {"ok": True}
finally:
await db.close()