Files
horror/backend/app/tmdb.py
2026-09-18 23:47:25 +02:00

164 lines
5.0 KiB
Python

import unicodedata
import httpx
from .config import (
LANGUAGE,
PROVIDER_NETFLIX,
REGION,
TMDB_BASE,
TMDB_TOKEN,
)
HEADERS = {"Authorization": f"Bearer {TMDB_TOKEN}", "accept": "application/json"}
DISCOVER_PARAMS = {
"watch_region": REGION,
"with_watch_monetization_types": "flatrate",
"language": LANGUAGE,
"include_adult": "false",
# Der volle Katalog hat 5648 Titel, das meiste davon nie gesehen.
# Die Grenze hält auch die Fehlzuordnungen draußen: JustWatch hängt
# unbekannte Titel gelegentlich an die Netflix-ID eines gleichnamigen
# bekannten Films — "LIENZO" bekam die ID von "Die Leinwand".
"vote_count.gte": 50,
}
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,
"with_watch_providers": PROVIDER_NETFLIX,
"page": page,
},
)
resp.raise_for_status()
return resp.json()
async def discover_all(http):
"""Alle Film-IDs im Netflix-DE-Abo."""
first = await discover_page(http, 1)
ids = {m["id"] for m in first["results"]}
for page in range(2, first["total_pages"] + 1):
data = await discover_page(http, page)
ids |= {m["id"] for m in data["results"]}
return ids
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 _fremde_schrift(text):
"""Steht der Titel in einer nichtlateinischen Schrift?"""
for zeichen in text or "":
if zeichen.isalpha() and "LATIN" not in unicodedata.name(zeichen, ""):
return True
return False
def pick_title(data):
"""Deutscher Titel, sonst der englische aus den Translations.
Für indische, chinesische und koreanische Filme hat TMDB oft keine
deutsche Fassung und gibt den Originaltitel zurück — in Originalschrift.
Damit kann hier niemand etwas anfangen; IMDb zeigt an der Stelle den
umschriebenen Titel, und den liefert TMDB in den Translations mit.
"""
titel = data.get("title") or data.get("original_title") or ""
if titel and not _fremde_schrift(titel):
return titel
for entry in data.get("translations", {}).get("translations", []):
if entry.get("iso_639_1") != "en":
continue
englisch = entry.get("data", {}).get("title")
if englisch and not _fremde_schrift(englisch):
return englisch
return titel or "Ohne Titel"
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_rentable(data):
"""Ob man den Film in Deutschland leihen kann — laut JustWatch, über
TMDB. None, wenn gar keine Anbieterdaten mitkamen."""
results = data.get("watch/providers", {}).get("results")
if results is None:
return None
return 1 if (results.get(REGION) or {}).get("rent") else 0
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"]