164 lines
5.0 KiB
Python
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"]
|