From ead38fdb13abb406065cef0743d7e411cb27eaf3 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Wed, 6 May 2026 18:05:07 -0700 Subject: Add genre tracking and year-in-review improvements Adds genre field to Film model with TMDB enrichment. Genres populate from TMDB detail fetch during add/edit and bulk enrichment. Genre metadata displays on film cards, detail page (Production section), stats page (top genres panel), and year-in-review (by decade and genre breakdowns). Auto-detects rewatches when adding films via TMDB autocomplete - if a film with the same TMDB ID already exists in diary, pre-fills rewatch checkbox and count. Rewatch count now displays on film cards as "Rewatch #N". Stats page now shows: - Top genres (most watched) - Film decades (sorted chronologically) - Already shows: directors, companions, star distribution, rewatch rate Year-in-review shows decades and genres alongside monthly activity and companions. Bulk enrichment endpoint (/data/enrich-posters) now fetches missing genre metadata along with posters and TMDB IDs. Co-Authored-By: Claude Haiku 4.5 --- static/app.js | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) (limited to 'static') diff --git a/static/app.js b/static/app.js index 7cd501a..fae2dbd 100644 --- a/static/app.js +++ b/static/app.js @@ -95,7 +95,7 @@ const renderMessage = (message) => { tmdbResults.appendChild(node); }; -const applyResult = (film) => { +const applyResult = async (film) => { setValue("#title", film.title || ""); setValue("#original_title", film.original_title || ""); setValue("#director", film.director || ""); @@ -105,6 +105,39 @@ const applyResult = (film) => { setValue("#runtime", film.runtime || ""); setValue("#tmdb_id", film.tmdb_id || ""); setPoster(film.poster_url); + + // Check if this is the add form (not edit) + const isAddForm = window.location.pathname.endsWith("/films/new"); + + if (film.tmdb_id) { + try { + // Fetch full detail for genre and check for rewatches in parallel + const [detailResponse, rewatchResponse] = await Promise.all([ + fetch(`/tmdb/detail/${film.tmdb_id}`), + isAddForm ? fetch(`/films/check-rewatch?tmdb_id=${film.tmdb_id}`) : Promise.resolve(null), + ]); + + if (detailResponse.ok) { + const detail = await detailResponse.json(); + setValue("#genre", detail.genre || ""); + } + + if (rewatchResponse && rewatchResponse.ok) { + const rw = await rewatchResponse.json(); + if (rw.count > 0) { + const rewatchCheckbox = document.getElementById("rewatch"); + if (rewatchCheckbox) { + rewatchCheckbox.checked = true; + setValue("#rewatch_count", String(rw.count)); + } + } + } + } catch (error) { + // Fail silently if detail/rewatch fetch fails + console.error("Failed to fetch details", error); + } + } + clearResults(); }; @@ -120,7 +153,14 @@ const renderResults = (films) => { const button = document.createElement("button"); button.type = "button"; button.className = "tmdb-result"; - button.addEventListener("click", () => applyResult(film)); + button.addEventListener("click", async () => { + button.disabled = true; + try { + await applyResult(film); + } finally { + button.disabled = false; + } + }); if (film.poster_url) { const image = document.createElement("img"); -- cgit v1.3-2-g0d8e