summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--routers/films.py9
-rw-r--r--services/omdb.py51
-rw-r--r--static/styles.css32
-rw-r--r--templates/detail.html23
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>