129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
"""Die Kanon-Liste gegen TMDB auflösen.
|
||
|
||
Die Vorlage nennt deutsche Verleihtitel. TMDB kennt die meisten, aber nicht
|
||
alle — deshalb mehrere Anläufe pro Titel, vom genauesten zum gröbsten.
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
import re
|
||
|
||
import httpx
|
||
|
||
from .canon_list import CANON
|
||
from .config import LANGUAGE, TMDB_BASE, TMDB_TOKEN
|
||
|
||
log = logging.getLogger("canon")
|
||
|
||
HEADERS = {"Authorization": f"Bearer {TMDB_TOKEN}", "accept": "application/json"}
|
||
YEAR = re.compile(r"\s*\((\d{4})\)\s*$")
|
||
CONCURRENCY = 8
|
||
|
||
# Wo die Titelsuche danebengreift, steht die TMDB-ID fest.
|
||
OVERRIDES = {
|
||
"Us": 458723, # sonst "Forgive Us All" von 2025
|
||
"Nightmare 2 – Die Rache": 10014, # sonst "Blade Master"
|
||
"REC 2": 10664, # sonst ein Freddy-Verschnitt von 2025
|
||
"Salò oder die 120 Tage von Sodom": 5336,
|
||
"Freitag der 13. Teil 3": 9728,
|
||
"Freitag der 13. Teil 7 – Jason im Blutrausch": 10281,
|
||
# Reihen, in denen die Suche Teile durcheinanderwarf: ohne Jahresangabe
|
||
# liefert TMDB den jüngsten Titel zuerst, also festnageln
|
||
"Phantasm – Das Böse": 9638,
|
||
"Phantasm: Ravager – Das Böse V": 262848,
|
||
"Freitag der 13.": 4488,
|
||
"Freitag der 13. (2009)": 13207,
|
||
"Final Destination": 9532,
|
||
"The Final Destination": 19912,
|
||
"Final Destination Bloodlines": 574475,
|
||
"A Quiet Place": 447332,
|
||
"A Quiet Place 2": 520763,
|
||
# "Das Böse" ist auf Deutsch sowohl Phantasm als auch Amityville Horror
|
||
"Das Böse (1979)": 11449,
|
||
# Gemeint ist das japanische Ring 2, nicht das US-Remake "The Ring Two"
|
||
"Ring 2": 9669,
|
||
# Traf einen gleichnamigen Film von 1976 statt Romeros Teil von 2007
|
||
"Diary of the Dead": 13025,
|
||
}
|
||
|
||
|
||
def parse_entry(raw):
|
||
"""'Psycho (1960)' → ('Psycho', 1960). Das Jahr trennt Gleichnamige."""
|
||
match = YEAR.search(raw)
|
||
if not match:
|
||
return raw.strip(), None
|
||
return YEAR.sub("", raw).strip(), int(match.group(1))
|
||
|
||
|
||
def variants(title):
|
||
"""Vom vollen Titel zu immer gröberen Formen."""
|
||
yield title
|
||
# Deutsche Verleihtitel hängen oft einen Untertitel an
|
||
for sep in (" – ", " - ", ": "):
|
||
if sep in title:
|
||
head = title.split(sep)[0].strip()
|
||
if len(head) > 2:
|
||
yield head
|
||
plain = title.replace("…", "").replace("’", "'").strip()
|
||
if plain != title:
|
||
yield plain
|
||
|
||
|
||
async def search(http, title, year):
|
||
for variant in variants(title):
|
||
params = {"query": variant, "language": LANGUAGE, "include_adult": "true"}
|
||
if year:
|
||
params["primary_release_year"] = year
|
||
resp = await http.get("/search/movie", params=params)
|
||
resp.raise_for_status()
|
||
results = resp.json().get("results")
|
||
if results:
|
||
return results[0], variant
|
||
# Jahr kann in der Vorlage abweichen — zweiter Anlauf ohne
|
||
if year:
|
||
params.pop("primary_release_year")
|
||
resp = await http.get("/search/movie", params=params)
|
||
resp.raise_for_status()
|
||
results = resp.json().get("results")
|
||
if results:
|
||
near = [
|
||
r
|
||
for r in results
|
||
if (r.get("release_date") or "")[:4].isdigit()
|
||
and abs(int(r["release_date"][:4]) - year) <= 1
|
||
]
|
||
if near:
|
||
return near[0], variant
|
||
return None, None
|
||
|
||
|
||
def client():
|
||
return httpx.AsyncClient(base_url=TMDB_BASE, headers=HEADERS, timeout=30.0)
|
||
|
||
|
||
async def resolve_all():
|
||
"""Gibt je Kanon-Eintrag die Liste der gefundenen TMDB-Treffer zurück."""
|
||
semaphore = asyncio.Semaphore(CONCURRENCY)
|
||
|
||
async def one(http, raw):
|
||
if raw in OVERRIDES:
|
||
async with semaphore:
|
||
resp = await http.get(
|
||
f"/movie/{OVERRIDES[raw]}", params={"language": LANGUAGE}
|
||
)
|
||
resp.raise_for_status()
|
||
return {"raw": raw, "hit": resp.json(), "used": "override"}
|
||
title, year = parse_entry(raw)
|
||
async with semaphore:
|
||
hit, used = await search(http, title, year)
|
||
return {"raw": raw, "title": title, "year": year, "hit": hit, "used": used}
|
||
|
||
async with client() as http:
|
||
groups = await asyncio.gather(
|
||
*(
|
||
asyncio.gather(*(one(http, raw) for raw in group))
|
||
for group in CANON
|
||
)
|
||
)
|
||
return list(groups)
|