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 --- routers/stats.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'routers/stats.py') diff --git a/routers/stats.py b/routers/stats.py index 0604646..67b5141 100644 --- a/routers/stats.py +++ b/routers/stats.py @@ -15,6 +15,11 @@ from services.countries import ( ) from services.film_people import split_credit_names +def split_genre_names(genre_str: str | None) -> list[str]: + if not genre_str: + return [] + return [name.strip() for name in genre_str.split(",") if name.strip()] + router = APIRouter(tags=["stats"]) templates = Jinja2Templates(directory="templates") @@ -22,7 +27,9 @@ templates = Jinja2Templates(directory="templates") def _build_stats_payload(films: list[Film]) -> dict: countries = Counter() country_codes = Counter() + decades = Counter() directors = Counter() + genres = Counter() star_counts = Counter({0: 0, 1: 0, 2: 0, 3: 0}) months = Counter() days = Counter() @@ -36,6 +43,12 @@ def _build_stats_payload(films: list[Film]) -> dict: if iso_numeric is not None: country_codes[iso_numeric] += 1 + if film.year: + decade = (film.year // 10) * 10 + decades[f"{decade}s"] += 1 + + genres.update(split_genre_names(film.genre)) + directors.update(split_credit_names(film.director)) stars = film.stars if film.stars in {0, 1, 2, 3} else 0 @@ -83,6 +96,14 @@ def _build_stats_payload(films: list[Film]) -> dict: {"director": director, "count": count} for director, count in sorted(directors.items(), key=lambda item: (-item[1], item[0])) ], + "films_per_decade": [ + {"decade": decade, "count": count} + for decade, count in sorted(decades.items(), key=lambda item: (item[0], -item[1])) + ], + "films_per_genre": [ + {"genre": genre, "count": count} + for genre, count in sorted(genres.items(), key=lambda item: (-item[1], item[0])) + ], "star_distribution": [{"stars": stars, "count": star_counts[stars]} for stars in (0, 1, 2, 3)], "films_per_month": [ {"month": month, "count": count} @@ -170,6 +191,8 @@ def _year_review_payload(db: Session, year: int | None) -> dict: "total_watched": 0, "average_stars": 0, "most_watched_directors": [], + "films_per_decade": [], + "films_per_genre": [], "star_distribution": [{"stars": stars, "count": 0} for stars in (0, 1, 2, 3)], "films_per_month": [{"month": month_name[index], "count": 0} for index in range(1, 13)], "rewatch_rate": {"rewatched": 0, "total_watched": 0, "rate": 0}, @@ -186,13 +209,19 @@ def _year_review_payload(db: Session, year: int | None) -> dict: year_films = _films_for_year(diary_films, selected_year) countries = Counter() + decades = Counter() directors = Counter() + genres = Counter() star_counts = Counter({0: 0, 1: 0, 2: 0, 3: 0}) months = Counter({month_index: 0 for month_index in range(1, 13)}) watched_with = Counter() for film in year_films: countries.update(split_country_names(film.country)) + if film.year: + decade = (film.year // 10) * 10 + decades[f"{decade}s"] += 1 + genres.update(split_genre_names(film.genre)) directors.update(split_credit_names(film.director)) stars = film.stars if film.stars in {0, 1, 2, 3} else 0 star_counts[stars] += 1 @@ -272,6 +301,14 @@ def _year_review_payload(db: Session, year: int | None) -> dict: {"month": month_name[index], "count": months[index]} for index in range(1, 13) ], + "films_per_decade": [ + {"decade": decade, "count": count} + for decade, count in sorted(decades.items(), key=lambda item: (item[0], -item[1])) + ], + "films_per_genre": [ + {"genre": genre, "count": count} + for genre, count in sorted(genres.items(), key=lambda item: (-item[1], item[0])) + ], "star_distribution": [{"stars": stars, "count": star_counts[stars]} for stars in (0, 1, 2, 3)], "most_watched_directors": [ {"director": director, "count": count} -- cgit v1.3-2-g0d8e