diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-09 01:41:48 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-09 01:41:48 -0700 |
| commit | 2f3a891d0944a3b200d3dda949475bf9e1742f56 (patch) | |
| tree | 4c843a5efdf6c6f804341e6f2b36c91febacbd50 | |
| parent | f252b534afeb22b4b88208552810901ea607d0f5 (diff) | |
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 <noreply@anthropic.com>
| -rw-r--r-- | routers/films.py | 9 | ||||
| -rw-r--r-- | services/omdb.py | 51 | ||||
| -rw-r--r-- | static/styles.css | 32 | ||||
| -rw-r--r-- | templates/detail.html | 23 |
4 files changed, 114 insertions, 1 deletions
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 %} </p> + {% if ratings %} + <div class="ratings-row"> + {% if ratings.imdb %} + <div class="rating-chip"> + <span class="rating-chip-label">IMDb</span> + <span class="rating-chip-value">{{ ratings.imdb }}</span> + </div> + {% endif %} + {% if ratings.rt %} + <div class="rating-chip"> + <span class="rating-chip-label">Rotten Tomatoes</span> + <span class="rating-chip-value">{{ ratings.rt }}</span> + </div> + {% endif %} + {% if ratings.metacritic %} + <div class="rating-chip"> + <span class="rating-chip-label">Metacritic</span> + <span class="rating-chip-value">{{ ratings.metacritic }}</span> + </div> + {% endif %} + </div> + {% endif %} + <section class="detail-grid"> <article class="detail-panel"> <p class="eyebrow">Watch log</p> |
