Files
horror/backend/app/tmdb.py
2026-08-12 19:30:41 +02:00

118 lines
3.5 KiB
Python

import httpx
from .config import (
GENRE_HORROR,
LANGUAGE,
PROVIDER_NETFLIX,
REGION,
TMDB_BASE,
TMDB_TOKEN,
)
HEADERS = {"Authorization": f"Bearer {TMDB_TOKEN}", "accept": "application/json"}
DISCOVER_PARAMS = {
"with_watch_providers": PROVIDER_NETFLIX,
"watch_region": REGION,
"with_genres": GENRE_HORROR,
"with_watch_monetization_types": "flatrate",
"language": LANGUAGE,
"include_adult": "false",
# JustWatch hängt unbekannte Titel gelegentlich an die Netflix-ID eines
# gleichnamigen bekannten Films. Betroffen waren ausschließlich Einträge
# mit ein oder zwei Stimmen — "LIENZO" bekam die ID von "Die Leinwand".
"vote_count.gte": 3,
}
DETAIL_PARAMS = {
"language": LANGUAGE,
"append_to_response": "images,videos,translations,watch/providers",
"include_image_language": "de,en,null",
"include_video_language": "de,en",
}
def client():
return httpx.AsyncClient(base_url=TMDB_BASE, headers=HEADERS, timeout=30.0)
async def discover_page(http, page):
resp = await http.get("/discover/movie", params={**DISCOVER_PARAMS, "page": page})
resp.raise_for_status()
return resp.json()
async def discover_all(http):
"""Alle Netflix-DE-Horrorfilme durchpaginieren."""
first = await discover_page(http, 1)
movies = list(first["results"])
for page in range(2, first["total_pages"] + 1):
data = await discover_page(http, page)
movies.extend(data["results"])
return movies
async def detail(http, movie_id):
resp = await http.get(f"/movie/{movie_id}", params=DETAIL_PARAMS)
resp.raise_for_status()
return resp.json()
def pick_overview(data):
"""Deutsche Synopsis, sonst englische aus den Translations."""
if data.get("overview"):
return data["overview"]
for entry in data.get("translations", {}).get("translations", []):
if entry.get("iso_639_1") == "en":
text = entry.get("data", {}).get("overview")
if text:
return text
return ""
def pick_backdrops(data):
"""Alle Szenenbilder. Duplikate wirft der Sync raus.
Ein gesetztes iso_639_1 heißt: auf dem Bild steht Text, meist der
Filmtitel. Das sind Werbemotive, keine Szenen. Hat ein Film nur solche
Bilder, nehmen wir sie trotzdem — sonst bliebe die Detailseite leer.
"""
backdrops = data.get("images", {}).get("backdrops", [])
textfree = [b for b in backdrops if b.get("iso_639_1") is None]
usable = textfree or backdrops
ranked = sorted(
usable,
key=lambda b: (-(b.get("vote_average") or 0), -(b.get("width") or 0)),
)
return [b["file_path"] for b in ranked]
def pick_watch_link(data):
"""TMDBs Watch-Seite für Deutschland.
Sie führt zur echten Netflix-Titelseite. Eine Netflix-Suche nach dem
TMDB-Titel geht ins Leere, sobald Netflix den Film anders nennt —
"LIENZO" heißt dort "CANVAS".
"""
providers = data.get("watch/providers", {}).get("results", {})
return (providers.get(REGION) or {}).get("link")
def pick_trailer(data):
"""Trailer bevorzugt, sonst Teaser. Sprache egal."""
videos = [
v for v in data.get("videos", {}).get("results", []) if v.get("site") == "YouTube"
]
if not videos:
return None
order = {"Trailer": 0, "Teaser": 1, "Clip": 2}
def rank(video):
return (
order.get(video.get("type"), 3),
0 if video.get("iso_639_1") == "de" else 1,
0 if video.get("official") else 1,
)
return sorted(videos, key=rank)[0]["key"]