118 lines
3.5 KiB
Python
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"]
|