diff --git a/app/database.py b/app/database.py index 3c0e625..ada1b1c 100644 --- a/app/database.py +++ b/app/database.py @@ -59,6 +59,15 @@ async def init_database(): key TEXT PRIMARY KEY, value TEXT ); + + CREATE TABLE IF NOT EXISTS search_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + mood TEXT NOT NULL, + results TEXT NOT NULL, + meta TEXT, + created_at TEXT NOT NULL + ); """) await db.commit() finally: diff --git a/app/routers/mood.py b/app/routers/mood.py index 2882edd..64bab76 100644 --- a/app/routers/mood.py +++ b/app/routers/mood.py @@ -1,11 +1,12 @@ import json import logging import time +from datetime import datetime, timezone from fastapi import APIRouter, HTTPException, Request from app.config import settings -from app.database import get_unwatched_movies +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 @@ -89,12 +90,70 @@ async def get_mood_recommendations(request: Request, body: MoodRequest): elapsed_ms = int((time.time() - start_time) * 1000) - return MoodResponse( - recommendations=recommendations, - meta={ - "total_unwatched": total_unwatched, - "candidates_evaluated": len(candidates), - "processing_time_ms": elapsed_ms, - "mood": body.mood, - }, - ) + 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() diff --git a/app/static/app.js b/app/static/app.js index 3d23338..9274f79 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -54,6 +54,7 @@ function showMainScreen() { selectedUserIds = [currentUser.id]; loadUsers(); loadStats(); + loadHistory(); } // --- Users --- @@ -113,6 +114,7 @@ async function findMovies() { const data = await res.json(); renderResults(data); + loadHistory(); } catch (err) { document.getElementById('loading').classList.add('hidden'); document.getElementById('error-message').textContent = err.message; @@ -171,6 +173,86 @@ function renderResults(data) { document.getElementById('results').classList.remove('hidden'); } +// --- Search History --- + +async function loadHistory() { + try { + const res = await fetch(`${API}/api/history`); + if (!res.ok) return; + const history = await res.json(); + const list = document.getElementById('history-list'); + list.innerHTML = ''; + + if (history.length === 0) { + document.getElementById('history-section').classList.add('hidden'); + document.getElementById('history-toggle').classList.add('hidden'); + return; + } + + history.forEach(entry => { + const date = new Date(entry.created_at); + const timeStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + + ' ' + date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); + const movieTitles = entry.results.slice(0, 3).map(r => r.title).join(', '); + const more = entry.results.length > 3 ? ` +${entry.results.length - 3} more` : ''; + + const item = document.createElement('div'); + item.className = 'history-item bg-dark-200 border border-gray-800 rounded-lg p-3 cursor-pointer hover:border-gray-600 transition-colors'; + item.innerHTML = ` +
"${entry.mood}"
+${movieTitles}${more}
+