This commit is contained in:
team3
2026-09-18 23:47:25 +02:00
parent db77c1f985
commit a90044b0a7
21 changed files with 1111 additions and 253 deletions

View File

@@ -7,6 +7,7 @@ landen nicht in der Datenbank.
import asyncio
import random
import time
import unicodedata
import httpx
@@ -16,6 +17,7 @@ from .tmdb import (
DETAIL_PARAMS,
pick_backdrops,
pick_overview,
pick_rentable,
pick_title,
pick_trailer,
)
@@ -67,7 +69,15 @@ async def details_nachladen(http, treffer, je_film=4):
Das alles steckt in der Detailansicht, ein Abruf je Film reicht.
"""
# Nicht alle auf einmal: bei einer langen Filmografie wären das
# über hundert gleichzeitige Abrufe
bremse = asyncio.Semaphore(8)
async def hole(film):
async with bremse:
await hole_einen(film)
async def hole_einen(film):
try:
resp = await http.get(
f"/movie/{film['id']}",
@@ -106,6 +116,27 @@ SORTIERUNG = {
# Weiter hinten liegen nur noch Filme, die kaum jemand kennt
MAX_SEITE = 20
# TMDB liefert zwanzig je Seite; bis hundert sind es fünf Abrufe
MAX_TREFFER = 100
JE_SEITE = 20
async def seiten(http, pfad, params, limit, start=1):
"""So viele Seiten, wie das Limit braucht — alle auf einmal. Was
fehlschlägt, fehlt eben; die Reihenfolge der Seiten bleibt."""
anzahl = -(-limit // JE_SEITE)
async def eine(nummer):
try:
resp = await http.get(pfad, params={**params, "page": nummer})
resp.raise_for_status()
return resp.json()
except httpx.HTTPError:
return {}
antworten = await asyncio.gather(*(eine(start + i) for i in range(anzahl)))
results = [r for a in antworten for r in a.get("results", [])]
return results, antworten[0] if antworten else {}
async def discover(filter_, limit=40):
@@ -149,22 +180,16 @@ async def discover(filter_, limit=40):
params["primary_release_date.lte"] = f"{filter_['jahr_max']}-12-31"
async with client() as http:
resp = await http.get("/discover/movie", params=params)
resp.raise_for_status()
daten = resp.json()
results, erste = await seiten(http, "/discover/movie", params, limit)
# Bei "Zufällig" nochmal von einer beliebigen Seite holen — sonst
# stehen bei jedem Aufruf dieselben zwanzig Filme oben
if filter_.get("sort") == "random":
seiten = min(daten.get("total_pages") or 1, MAX_SEITE)
if seiten > 1:
params["page"] = random.randint(1, seiten)
resp = await http.get("/discover/movie", params=params)
resp.raise_for_status()
daten = resp.json()
results = daten.get("results", [])
# Bei "Zufällig" nochmal von einer beliebigen Stelle holen — sonst
# stehen bei jedem Aufruf dieselben Filme oben
if filter_.get("sort") == "random":
gesamt = min(erste.get("total_pages") or 1, MAX_SEITE)
noetig = -(-limit // JE_SEITE)
if gesamt > noetig:
start = random.randint(1, gesamt - noetig + 1)
results, _ = await seiten(http, "/discover/movie", params, limit, start)
random.shuffle(results)
usable = [r for r in results if r.get("poster_path")]
@@ -187,19 +212,13 @@ async def search(query, limit=24):
begriffe = [query] if schlicht == query else [query, schlicht]
async def einmal(begriff):
try:
resp = await http.get(
"/search/movie",
params={
"query": begriff,
"language": LANGUAGE,
"include_adult": "false",
},
)
resp.raise_for_status()
return resp.json().get("results", [])
except httpx.HTTPError:
return []
results, _ = await seiten(
http,
"/search/movie",
{"query": begriff, "language": LANGUAGE, "include_adult": "false"},
limit,
)
return results
async with client() as http:
results = []
@@ -343,6 +362,114 @@ async def aehnliche(movie_id, limit=12, voll=False, filter_=None):
return treffer
# ── Personen: Schauspieler und Regisseure ────────────────────────────────
MAX_PERSONEN = 12
# Laufzeit und Szenenbilder kosten je Film einen Abruf — bei einer langen
# Filmografie ein paar Sekunden. Einmal geholt, bleibt es einen Tag im
# Speicher; so oft ändert sich eine Filmografie nicht.
PERSONEN_CACHE = {}
PERSONEN_FRIST = 24 * 3600
FACH = {
"Acting": "Schauspiel",
"Directing": "Regie",
"Writing": "Drehbuch",
"Production": "Produktion",
}
async def personen(query):
"""Leute suchen. Wer ein Foto hat, steht vorn — sonst die Bekannten."""
async with client() as http:
try:
resp = await http.get(
"/search/person",
params={"query": query, "language": LANGUAGE, "include_adult": "false"},
)
resp.raise_for_status()
roh = resp.json().get("results", [])
except httpx.HTTPError:
return []
roh.sort(key=lambda p: (not p.get("profile_path"), -(p.get("popularity") or 0)))
return [
{
"id": p["id"],
"name": p.get("name"),
"fach": FACH.get(p.get("known_for_department"), p.get("known_for_department") or ""),
"profile_path": p.get("profile_path"),
"bekannt_fuer": [
k.get("title") or k.get("name")
for k in p.get("known_for", [])
if k.get("title") or k.get("name")
][:3],
}
for p in roh[:MAX_PERSONEN]
]
def credits_aufbereiten(daten):
"""Aus den movie_credits zwei Listen: was jemand gespielt und wo er
Regie geführt hat. Ohne Poster oder doppelt fliegt raus, die
bekanntesten Filme stehen vorn. Kurzfilme und Making-ofs bleiben
drin — die sortiert die Filterleiste aus, und zwar sichtbar."""
def liste(eintraege):
raus, dabei = [], set()
for e in sorted(eintraege, key=lambda e: -(e.get("popularity") or 0)):
if not e.get("poster_path") or e["id"] in dabei:
continue
dabei.add(e["id"])
raus.append(as_result(e))
return raus
return {
"gespielt": liste(daten.get("cast", [])),
"regie": liste([c for c in daten.get("crew", []) if c.get("job") == "Director"]),
}
# Mehr Namen passen nicht in die Filmansicht, und ab da sind es Nebenrollen
MAX_CAST = 8
async def credits(movie_id):
"""Regie und die wichtigsten Darsteller — als Sprungbrett zur
Personensuche. Fehlt etwas, bleiben die Listen leer."""
async with client() as http:
try:
# Ohne Sprache: auf Deutsch kämen russische Namen in Kyrillisch
# und eingedeutschte Umschriften wie "Ilja Wiktorowitsch"
resp = await http.get(f"/movie/{movie_id}/credits", params={"language": "en-US"})
resp.raise_for_status()
daten = resp.json()
except httpx.HTTPError:
return {"regie": [], "cast": []}
def person(p, fach):
return {"id": p["id"], "name": p.get("name"), "fach": fach, "profile_path": p.get("profile_path")}
regie, dabei = [], set()
for c in daten.get("crew", []):
if c.get("job") == "Director" and c["id"] not in dabei:
dabei.add(c["id"])
regie.append(person(c, "Regie"))
cast = sorted(daten.get("cast", []), key=lambda c: c.get("order", 999))[:MAX_CAST]
return {"regie": regie, "cast": [person(c, "Schauspiel") for c in cast]}
async def person_filme(person_id):
gemerkt = PERSONEN_CACHE.get(person_id)
if gemerkt and time.monotonic() - gemerkt[0] < PERSONEN_FRIST:
return gemerkt[1]
async with client() as http:
resp = await http.get(f"/person/{person_id}/movie_credits", params={"language": LANGUAGE})
resp.raise_for_status()
rollen = credits_aufbereiten(resp.json())
await details_nachladen(http, rollen["gespielt"] + rollen["regie"])
PERSONEN_CACHE[person_id] = (time.monotonic(), rollen)
return rollen
async def titel_richten(http, film):
"""Originalschrift durch den englischen Titel ersetzen."""
if not any(
@@ -387,6 +514,7 @@ async def detail(movie_id):
"images": pick_backdrops(data)[:12],
"trailer_key": pick_trailer(data),
"imdb_id": data.get("imdb_id"),
"rentable": pick_rentable(data),
"netflix_id": None,
"netflix_url": None,
"on_netflix": 0,