Add runtime filter, kid-friendly toggle, surprise me, and re-roll

- Runtime quick-select buttons (Any/90m/2h/2.5h) filter movies by length
- Kid-friendly toggle forces PG-13 max and boosts Family/Animation genres
- Surprise Me picks a random mood prompt from 20 curated options
- Show Me More re-rolls same mood excluding already-shown movies
- Re-roll appends new results to the existing search history entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:07:05 -07:00
parent d8c8b473ad
commit 9f96a91986
6 changed files with 194 additions and 18 deletions
+42 -11
View File
@@ -32,6 +32,16 @@ async def get_mood_recommendations(request: Request, body: MoodRequest):
# Get unwatched movies from cache
unwatched_raw = await get_unwatched_movies(user_ids)
# Exclude previously shown movies (re-roll)
if body.exclude_ids:
exclude_set = set(body.exclude_ids)
unwatched_raw = [m for m in unwatched_raw if m["jellyfin_id"] not in exclude_set]
# Filter by max runtime
if body.max_runtime:
unwatched_raw = [m for m in unwatched_raw if not m.get("runtime_minutes") or m["runtime_minutes"] <= body.max_runtime]
total_unwatched = len(unwatched_raw)
if total_unwatched == 0:
@@ -47,7 +57,8 @@ async def get_mood_recommendations(request: Request, body: MoodRequest):
# Pre-filter to narrow candidates
candidates = prefilter_candidates(
unwatched_raw, body.mood, max_candidates=settings.max_candidates
unwatched_raw, body.mood, max_candidates=settings.max_candidates,
kid_friendly=body.kid_friendly,
)
logger.info(f"Pre-filtered {total_unwatched} movies to {len(candidates)} candidates for mood: '{body.mood}'")
@@ -98,25 +109,45 @@ async def get_mood_recommendations(request: Request, body: MoodRequest):
}
# Save to search history
history_id = None
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(),
),
)
if body.history_id:
# Re-roll: append new results to existing history entry
cursor = await db.execute(
"SELECT results FROM search_history WHERE id = ? AND user_id = ?",
(body.history_id, user["id"]),
)
row = await cursor.fetchone()
if row:
existing = json.loads(row["results"])
combined = existing + [r.model_dump() for r in recommendations]
await db.execute(
"UPDATE search_history SET results = ?, meta = ? WHERE id = ? AND user_id = ?",
(json.dumps(combined), json.dumps(meta), body.history_id, user["id"]),
)
history_id = body.history_id
else:
# Fallback: create new entry if original not found
cursor = 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()),
)
history_id = cursor.lastrowid
else:
cursor = 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()),
)
history_id = cursor.lastrowid
await db.commit()
finally:
await db.close()
except Exception as e:
logger.warning(f"Failed to save search history: {e}")
meta["history_id"] = history_id
return MoodResponse(recommendations=recommendations, meta=meta)