init
This commit is contained in:
128
backend/app/canon.py
Normal file
128
backend/app/canon.py
Normal 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)
|
||||
Reference in New Issue
Block a user