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"]