# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview "Movie Night" — an AI-powered media discovery web app for a household with ~53TB of movies on a self-hosted Jellyfin media server. Users describe their mood ("pizza night with the kids", "light fun movie after a hard week") and get AI-ranked recommendations from their unwatched library, with posters and one-click deep links to Jellyfin. **Users:** Kenny and his wife — the primary use case is "what should we watch tonight?" ## Architecture ``` Frontend (Tailwind + vanilla JS) → FastAPI backend → LLM (Claude/OpenAI/Ollama) ↓ Jellyfin API → SQLite cache ``` - **Backend:** Python 3.12 + FastAPI (app/) - **Frontend:** Single-page HTML/CSS/JS with Tailwind CDN (app/static/) - **LLM:** Modular provider — Anthropic Claude, OpenAI, or Ollama (app/services/llm/) - **Auth:** Jellyfin credentials (like Jellyseerr) — no separate user database - **Data:** SQLite cache of movie metadata synced from Jellyfin, persisted at /data/library.db - **Deployment:** Docker Compose on bondelie-media (192.168.5.254), port 5210 ## Key Files - `app/main.py` — FastAPI app entry, lifespan (DB init + background sync), poster proxy, static mount - `app/config.py` — Settings via pydantic-settings (env vars) - `app/database.py` — SQLite schema and query helpers - `app/routers/auth.py` — Jellyfin-based login/logout/session management - `app/routers/mood.py` — POST /api/mood — the core recommendation flow - `app/services/jellyfin.py` — Jellyfin API client (auth, library fetch, watch state, posters) - `app/services/prefilter.py` — Tier-1 keyword/genre scoring to narrow candidates before LLM - `app/services/llm/` — Modular LLM providers (anthropic, openai, ollama) - `app/services/library_sync.py` — Background sync of movie metadata + watch state - `app/static/index.html` — Frontend: login, mood input, result cards - `app/static/app.js` — Frontend logic ## Related Infrastructure Integrates with the media server documented in `../media-server-migration/`: | Service | Port | Purpose | |-------------|-------|--------------------------------------------| | Jellyfin | 8096 | Library metadata, watch state, posters | | Movie Night | 5210 | This app | **Server:** bondelie-media (192.168.5.254) — Ubuntu 24.04 LTS **Proxy:** `movienight.internal.bondelie.net` ## Recommendation Flow 1. User types mood text 2. Backend fetches unwatched movies from SQLite cache (filtered by selected users' watch state) 3. `prefilter.py` scores movies by genre/keyword match, returns top 200 candidates 4. LLM provider ranks candidates and returns top 6 with reasoning (structured JSON) 5. Backend validates jellyfin_ids, enriches with poster URLs and deep links 6. Frontend renders movie cards with posters, metadata, reasoning, and "Watch" button ## Secrets Store in `.env` (gitignored), never commit: - `JELLYFIN_API_KEY` — Jellyfin API key (Settings → API Keys) - `LLM_API_KEY` — Anthropic/OpenAI API key (not needed for Ollama) - `SESSION_SECRET` — Random string for cookie signing ## Development Commands ```bash # Local development (requires Python 3.12+) pip install -r requirements.txt cp .env.example .env # then fill in API keys uvicorn app.main:app --reload --port 5210 # Docker build and run docker compose up --build # Deploy to bondelie-media scp -r . kenny@192.168.5.254:/srv/stacks/movie-night/ ssh kenny@192.168.5.254 "cd /srv/stacks/movie-night && docker compose up --build -d" ``` ## Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | JELLYFIN_URL | Yes | http://192.168.5.254:8096 | Jellyfin server URL | | JELLYFIN_API_KEY | Yes | — | Jellyfin API key | | JELLYFIN_EXTERNAL_URL | Yes | https://jellyfin.internal.bondelie.net | URL for deep links | | LLM_PROVIDER | No | anthropic | anthropic, openai, or ollama | | LLM_API_KEY | Yes* | — | API key (*not needed for ollama) | | LLM_MODEL | No | varies by provider | Model name override | | LLM_BASE_URL | No | — | Base URL override (for ollama) | | SESSION_SECRET | Yes | — | Random string for sessions |