From 2f3a891d0944a3b200d3dda949475bf9e1742f56 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Sat, 9 May 2026 01:41:48 -0700 Subject: Add IMDb, Rotten Tomatoes, and Metacritic ratings to film detail Fetches ratings from OMDB API in parallel with TMDB context. Displays three side-by-side chips between the subtitle and watch log panels. Requires OMDB_API_KEY in .env; degrades silently if missing or no match. Co-Authored-By: Claude Sonnet 4.6 --- routers/films.py | 9 ++++++++- services/omdb.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ static/styles.css | 32 ++++++++++++++++++++++++++++++++ templates/detail.html | 23 +++++++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 services/omdb.py diff --git a/routers/films.py b/routers/films.py index 333eaef..449fd44 100644 --- a/routers/films.py +++ b/routers/films.py @@ -1,3 +1,4 @@ +import asyncio from collections import Counter from datetime import date, datetime from itertools import groupby @@ -12,6 +13,7 @@ from sqlalchemy.orm import Session from database import get_db from models import Film +from services import omdb from services.film_people import director_href, normalize_name, split_credit_names from services.tmdb import TMDBNotConfiguredError, detail_context as tmdb_detail_context, movie_detail @@ -442,7 +444,6 @@ def get_films_partial( @router.get("/films/{film_id}") async def film_detail(film_id: int, request: Request, db: Session = Depends(get_db)): film = _get_film_or_404(db, film_id) - tmdb_context = await _film_tmdb_context(film) rewatch_filter = ( Film.tmdb_id == film.tmdb_id if film.tmdb_id is not None else Film.title == film.title @@ -454,6 +455,11 @@ async def film_detail(film_id: int, request: Request, db: Session = Depends(get_ .all() ) + tmdb_context, ratings = await asyncio.gather( + _film_tmdb_context(film), + omdb.fetch_ratings(title=film.title, year=film.year), + ) + return templates.TemplateResponse( request=request, name="detail.html", @@ -463,6 +469,7 @@ async def film_detail(film_id: int, request: Request, db: Session = Depends(get_ "active_shelf": film.shelf, "tmdb_context": tmdb_context, "rewatch_history": rewatch_history, + "ratings": ratings, }, ) diff --git a/services/omdb.py b/services/omdb.py new file mode 100644 index 0000000..8e574a1 --- /dev/null +++ b/services/omdb.py @@ -0,0 +1,51 @@ +import os + +import httpx + +OMDB_URL = "http://www.omdbapi.com/" + + +def _api_key() -> str | None: + return os.getenv("OMDB_API_KEY", "").strip() or None + + +async def fetch_ratings( + *, + title: str | None = None, + year: int | None = None, +) -> dict | None: + key = _api_key() + if not key or not title: + return None + + params: dict = {"apikey": key, "t": title} + if year: + params["y"] = str(year) + + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(OMDB_URL, params=params) + data = response.json() + except Exception: + return None + + if data.get("Response") == "False": + return None + + result: dict = {} + for rating in data.get("Ratings", []): + source = rating.get("Source", "") + value = rating.get("Value", "") + if source == "Internet Movie Database": + result["imdb"] = value + elif source == "Rotten Tomatoes": + result["rt"] = value + elif source == "Metacritic": + result["metacritic"] = value + + if not result.get("imdb"): + raw = data.get("imdbRating", "") + if raw and raw != "N/A": + result["imdb"] = f"{raw}/10" + + return result or None diff --git a/static/styles.css b/static/styles.css index 0c48501..c7be672 100644 --- a/static/styles.css +++ b/static/styles.css @@ -548,6 +548,38 @@ h2 { color: var(--accent); } +.ratings-row { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 16px 0 20px; +} + +.rating-chip { + display: flex; + flex-direction: column; + gap: 3px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + padding: 10px 14px; + min-width: 90px; +} + +.rating-chip-label { + color: var(--muted); + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.rating-chip-value { + color: var(--text); + font-size: 1.05rem; + font-weight: 600; +} + .detail-tagline { margin: 0 0 12px; color: var(--accent-strong); diff --git a/templates/detail.html b/templates/detail.html index 83bd26a..5bf4b3f 100644 --- a/templates/detail.html +++ b/templates/detail.html @@ -62,6 +62,29 @@ {% endif %}

+ {% if ratings %} +
+ {% if ratings.imdb %} +
+ IMDb + {{ ratings.imdb }} +
+ {% endif %} + {% if ratings.rt %} +
+ Rotten Tomatoes + {{ ratings.rt }} +
+ {% endif %} + {% if ratings.metacritic %} +
+ Metacritic + {{ ratings.metacritic }} +
+ {% endif %} +
+ {% endif %} +

Watch log

-- cgit v1.3-2-g0d8e