This commit is contained in:
team3
2026-08-12 19:30:41 +02:00
commit 38d5a8cfae
30 changed files with 4157 additions and 0 deletions

128
backend/app/canon.py Normal file
View File

@@ -0,0 +1,128 @@
"""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)