Files
rippy/docker/api/clients/jikan.py
T
Hitonabi 37711f87c1
Ampel / ampel (push) Failing after 39s
Jikan-Retry bei 504 + Cache-Verfall fuer mittelgute Prescan-Treffer
Jikan (Gratis-Dienst) wirft sporadisch 504 — ein Retry mit 2s Pause;
aktuell ist der Dienst ganz down, die Kette faellt sauber auf OMDb.
Prescan-Cache: 0.6-0.9 verfaellt nach 1 Tag, ab 0.9 nach 7 Tagen —
sonst wuerde ein frueher OMDb-Treffer bessere Quellen fuer immer blocken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:25:10 +02:00

92 lines
3.3 KiB
Python

"""Jikan API Client (MyAnimeList) — kostenlose Anime-Datenbank OHNE API-Key.
Commander-Fund 23.07.: Für Anime-Discs (häufiger Fall im Heimlab) ist MAL die
präziseste Quelle — „Evangelion: 2.22" existiert dort exakt, mit Poster und
Beschreibung. Kein Key, Rate-Limit 3 req/s (für Disc-Erkennung irrelevant).
Doku: https://docs.api.jikan.moe/
"""
from difflib import SequenceMatcher
from typing import Dict, Optional
import requests
from cache import get, set as cache_set
JIKAN_BASE_URL = "https://api.jikan.moe/v4"
def titel_aehnlichkeit(a: str, b: str) -> float:
"""Normalisierte Titel-Ähnlichkeit 0..1 (pure Funktion, testbar)."""
def norm(t: str) -> str:
return "".join(c for c in t.lower() if c.isalnum() or c == " ").strip()
a_n, b_n = norm(a), norm(b)
if not a_n or not b_n:
return 0.0
return SequenceMatcher(None, a_n, b_n).ratio()
class JikanClient:
MINDEST_AEHNLICHKEIT = 0.55
def __init__(self):
self.session = requests.Session()
def lookup(self, title: str) -> Optional[Dict]:
"""Sucht per Titel; nimmt den ÄHNLICHSTEN Treffer, nicht blind den ersten."""
cache_key = f"jikan:{title.lower()}"
cached = get(cache_key)
if cached:
return cached
# Jikan wirft sporadisch 504 (Gratis-Dienst) — ein Retry rettet die
# meisten Fälle, mehr wäre Belästigung.
eintraege = None
for versuch in range(2):
try:
response = self.session.get(
f"{JIKAN_BASE_URL}/anime",
params={"q": title, "limit": 5, "sfw": "true"},
timeout=10,
)
response.raise_for_status()
eintraege = response.json().get("data") or []
break
except Exception as e:
print(f"Jikan API Error (Versuch {versuch + 1}): {e}")
if versuch == 0:
import time
time.sleep(2)
if eintraege is None:
return None
bester, bester_score = None, 0.0
for eintrag in eintraege:
varianten = [eintrag.get("title") or ""]
if eintrag.get("title_english"):
varianten.append(eintrag["title_english"])
score = max(titel_aehnlichkeit(title, v) for v in varianten)
if score > bester_score:
bester, bester_score = eintrag, score
if not bester or bester_score < self.MINDEST_AEHNLICHKEIT:
return None
bilder = (bester.get("images") or {}).get("jpg") or {}
ergebnis = {
"type": "movie" if bester.get("type") == "Movie" else "tv",
"id": f"mal-{bester.get('mal_id', '')}",
"title": bester.get("title_english") or bester.get("title") or title,
"year": bester.get("year"),
"overview": (bester.get("synopsis") or "")[:600],
"poster_path": bilder.get("large_image_url") or bilder.get("image_url") or "",
"backdrop_path": "",
"runtime": 0,
"genres": [g.get("name", "") for g in (bester.get("genres") or []) if g.get("name")],
"source": "jikan",
"match_score": round(bester_score, 2),
}
cache_set(cache_key, ergebnis)
return ergebnis