summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md3
-rw-r--r--CLAUDE.md51
-rw-r--r--static/app.js60
3 files changed, 78 insertions, 36 deletions
diff --git a/AGENTS.md b/AGENTS.md
index bf95f64..a84dcce 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,8 +15,5 @@ Use 4-space indentation and keep Python code straightforward and type-friendly.
## Testing Guidelines
There is no committed `tests/` package yet, so add targeted tests alongside any non-trivial feature work. Use `pytest` if you introduce automated tests, with filenames like `test_imports.py` and test names such as `test_watchlist_import_sets_queue_shelf`. For now, every change should at least pass the `py_compile` check and a local smoke test of the affected route in the browser.
-## Commit & Pull Request Guidelines
-Git history is not readable from this workspace, so use short imperative commit subjects such as `Add queue watchlist import`. Keep commits focused on one behavior change. Pull requests should explain the user-facing change, note any schema or import implications, list verification steps, and include screenshots for template or CSS updates.
-
## Security & Configuration
Keep secrets in `.env`, especially `TMDB_API_KEY`, and never hardcode credentials in Python, templates, or JavaScript. Treat `lumiere.db` and backup database files as local artifacts unless a task explicitly requires fixture data.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..a646370
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,51 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What This Is
+Lumiere is a personal cinema diary — a server-rendered FastAPI web app backed by SQLite. Users track films across three shelves: `diary`, `queue`, and `abandoned`. TMDB enriches film metadata (posters, directors, runtime, countries).
+
+## Commands
+
+```bash
+# Setup
+python -m venv .venv
+.venv/bin/pip install -r requirements.txt
+
+# Run dev server
+.venv/bin/uvicorn main:app --reload
+
+# Syntax check (run before any PR)
+.venv/bin/python -m py_compile main.py database.py models.py routers/*.py services/*.py
+
+# Tests (no suite yet — use pytest when adding tests)
+.venv/bin/pytest
+```
+
+Requires a `.env` file with `TMDB_API_KEY=...`. App runs at `http://127.0.0.1:8000`.
+
+## Architecture
+
+**Request flow:** Browser → FastAPI router → SQLAlchemy (SQLite) → Jinja2 template → HTML response. `static/app.js` handles star ratings (JSON PATCH) and TMDB search autocomplete without a build step.
+
+**Key files:**
+- `main.py` — app entry, router registration
+- `database.py` — SQLite engine, sessions, self-healing schema migration (no Alembic; renames old table on column mismatch and migrates data)
+- `models.py` — single `Film` ORM model with a `shelf` column
+- `routers/films.py` — shelf listing, CRUD, director page, queue random pick, star rating API
+- `routers/imports.py` — Letterboxd and watchlist CSV import, deduplication, enrichment
+- `routers/stats.py` — all-time stats and year-in-review (pure Python aggregation, no SQL GROUP BY)
+- `routers/tmdb.py` — `/tmdb/search?q=` proxy for JS autocomplete
+- `services/tmdb.py` — TMDB API client: search, detail fetch, metadata application
+- `services/film_people.py` — director name normalization and URL slug helpers
+
+**Patterns:**
+- Route logic stays in `routers/`, shared service logic in `services/`
+- Stats computed in-memory from full query results (appropriate for personal-scale SQLite)
+- TMDB fields are additive/optional; all core film fields work without enrichment
+- Import pipeline dedupes by TMDB ID first, then normalized title+year+date
+
+## Coding Conventions
+- 4-space indentation; `snake_case` for functions/variables, `PascalCase` for ORM models, uppercase for shelf/status constants
+- Jinja templates stay minimal; defer behavior to `static/app.js`
+- `lumiere.db` and `*.db.bak-*` files are local artifacts — do not drive code changes from them
diff --git a/static/app.js b/static/app.js
index e2213f3..b942d7e 100644
--- a/static/app.js
+++ b/static/app.js
@@ -161,39 +161,6 @@ document.querySelectorAll("form[data-confirm]").forEach((form) => {
});
});
-document.addEventListener("click", async (event) => {
- const button = event.target.closest(".star-button");
- if (!button) return;
-
- const control = button.closest(".star-control");
- if (!control || !control.dataset.filmId) return;
-
- const stars = Number(button.dataset.stars || 0);
- const filmId = control.dataset.filmId;
-
- button.disabled = true;
- try {
- const response = await fetch(`/films/${filmId}/stars`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ stars }),
- });
-
- const data = await response.json();
- if (!response.ok) {
- return;
- }
-
- syncStarControl(control, Number(data.stars || 0));
- } catch (error) {
- console.error("Failed to update stars", error);
- } finally {
- button.disabled = false;
- }
-});
-
document.querySelectorAll(".star-control").forEach((control) => {
syncStarControl(control, Number(control.dataset.currentStars || 0));
control.addEventListener("pointerleave", () => clearStarPreview(control));
@@ -205,5 +172,32 @@ document.querySelectorAll(".star-control").forEach((control) => {
control.querySelectorAll(".star-button").forEach((button) => {
button.addEventListener("pointerenter", () => previewStarControl(control, Number(button.dataset.stars || 0)));
button.addEventListener("focus", () => previewStarControl(control, Number(button.dataset.stars || 0)));
+ button.addEventListener("click", async () => {
+ const currentStars = Number(control.dataset.currentStars || 0);
+ const selectedStars = Number(button.dataset.stars || 0);
+ const nextStars = currentStars === selectedStars ? 0 : selectedStars;
+
+ button.disabled = true;
+ try {
+ const response = await fetch(`/films/${control.dataset.filmId}/stars`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ stars: nextStars }),
+ });
+
+ const data = await response.json();
+ if (!response.ok) {
+ return;
+ }
+
+ syncStarControl(control, Number(data.stars || 0));
+ } catch (error) {
+ console.error("Failed to update stars", error);
+ } finally {
+ button.disabled = false;
+ }
+ });
});
});