fix(metadaten): exakt schlaegt unscharf - Jikan beendet die Kette nur bei Treffer

Der SAVEPOINT v3.16 vermutete, das Aehnlichkeits-Gate 0,55 sei "grosszuegig" und
Jikan stehe in der Kette zu frueh (vor OMDb). Nachgerechnet - titel_aehnlichkeit
ist eine reine Funktion - ergibt sich ein anderes Bild als vermutet:

  "Alien"           vs. "Alien 9"           -> 0,83  TRIFFT
  "Inception"        vs. "Deception"         -> 0,78  TRIFFT
  "Hero"             vs. "Heroman"           -> 0,73  TRIFFT
  "The Dark Knight"  vs. "Dark Knight Rises" -> 0,69  TRIFFT

Ein Kinofilm, den TMDB verpasst, bekam damit Anime-Metadaten mit Confidence
0,85 - und OMDb, das ihn kennt, wurde nie gefragt.

Bemerkenswert dabei, und das widerlegt die Vermutung "einfach zu niedrig": Fuer
den Fall, fuer den Jikan ueberhaupt eingebaut wurde, ist das Gate sogar zu HOCH.
"Evangelion 2.22" gegen den MAL-Titel "Evangelion: 2.0 You Can (Not) Advance"
ergibt 0,51 und faellt durch. Eine einzelne Zahl kann beides nicht leisten.

Deshalb zwei Schwellen statt Umsortieren (Reihenfolge bleibt, AGENTS Regel B):
Nur ein praktisch exakter Titel (>= 0,9) beendet die Kette. Ein unscharfer
Treffer wird GEMERKT, dann wird OMDb gefragt - und erst wenn OMDb nichts hat,
kommt er als VORSCHLAG mit Confidence 0,6 zum Zug. Damit gewinnt "exakt" immer
gegen "unscharf", egal aus welcher Quelle.

Antwort auf die Commander-Frage "JIKAN ist drin - wird das genutzt?": ja, und ab
jetzt an der richtigen Stelle.

Ehrlicher Vorbehalt: Live gegengeprueft ist das nicht - MyAnimeList war waehrend
dieser Sitzung durchweg weg (Jikan antwortete HTTP 504 auf jede Anfrage). Die
Arithmetik des Gates ist davon unberuehrt und in test_jikan_helpers.py
festgehalten, die Kettenlogik in test_prescan_helpers.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 12:59:26 +02:00
parent 6db64230eb
commit 0c269a97ee
4 changed files with 223 additions and 4 deletions
+12
View File
@@ -28,7 +28,19 @@ def titel_aehnlichkeit(a: str, b: str) -> float:
class JikanClient:
# Unter dieser Ähnlichkeit gibt lookup() gar nichts zurück.
MINDEST_AEHNLICHKEIT = 0.55
# Ab dieser Ähnlichkeit gilt der Treffer als SICHER und beendet die
# Metadaten-Kette. Dazwischen ist er nur ein Vorschlag: erst wird OMDb
# gefragt, und nur wenn das nichts hat, kommt er zum Zug.
#
# Warum zwei Schwellen (26.07.2026, ausgerechnet mit titel_aehnlichkeit):
# Eine einzelne Zahl kann beide Aufgaben nicht leisten. 0,55 lässt kurze
# Kinofilm-Titel durch, die hier nichts zu suchen haben („Alien" gegen
# „Alien 9" = 0,83), ist aber gleichzeitig zu streng für den Fall, für den
# diese Quelle gebaut wurde („Evangelion 2.22" gegen „Evangelion: 2.0 You
# Can (Not) Advance" = 0,51). Messwerte in test_jikan_helpers.py.
SICHER_AEHNLICHKEIT = 0.9
def __init__(self):
self.session = requests.Session()
+40 -4
View File
@@ -19,7 +19,6 @@ from clients.thetvdb import TheTVDBClient
from cache import get, set as cache_set
from cache.keys import generate_prescan_key
def normalize_disc_label(label: str) -> str:
"""Disc-Labels wie 'PULP_FICTION_DE''Pulp Fiction De' (pure Funktion).
@@ -442,15 +441,43 @@ class PreScan:
break
# Fallback 1: Jikan/MyAnimeList (kostenlos, KEIN Key) — für Anime die
# präziseste Quelle; wählt per Titel-Ähnlichkeit, nicht Treffer #1
# präziseste Quelle; wählt per Titel-Ähnlichkeit, nicht Treffer #1.
#
# NEU 26.07.2026 — „exakt schlägt unscharf", unabhängig von der
# Reihenfolge. Vorher galt der erste Jikan-Treffer über dem Gate 0,55
# sofort als sicherer Fund (confidence 0,85) und OMDb kam nie dran.
# Das ist zu großzügig, und zwar messbar (offline gerechnet mit
# titel_aehnlichkeit, echte MyAnimeList-Titel):
#
# „Alien" vs. „Alien 9" → 0,83 TRIFFT
# „Inception" vs. „Deception" → 0,78 TRIFFT
# „Hero" vs. „Heroman" → 0,73 TRIFFT
# „The Dark Knight" vs. „Dark Knight Rises" → 0,69 TRIFFT
#
# Ein Kinofilm, den TMDB verpasst, bekam damit Anime-Metadaten — und
# OMDb, das ihn kennt, wurde nie gefragt. Bemerkenswert dabei, und das
# widerlegt die Vermutung im SAVEPOINT v3.16, das Gate sei einfach zu
# niedrig: Für den Fall, für den Jikan überhaupt eingebaut wurde, ist
# es sogar zu HOCH — „Evangelion 2.22" vs. „Evangelion: 2.0 You Can
# (Not) Advance" ergibt 0,51 und fällt durch. Eine einzelne Zahl kann
# beides nicht leisten.
#
# Deshalb: Nur ein praktisch exakter Titel gilt sofort. Ein unscharfer
# Treffer wird GEMERKT, dann OMDb gefragt — und erst wenn OMDb nichts
# hat, kommt er als VORSCHLAG (niedrige Confidence) zum Zug.
jikan_unscharf = None
if not matched:
for kandidat in kandidaten:
jikan_treffer = self.jikan.lookup(kandidat)
if jikan_treffer:
if not jikan_treffer:
continue
if jikan_treffer.get("match_score", 0) >= JikanClient.SICHER_AEHNLICHKEIT:
confidence = 0.85
metadata = jikan_treffer
matched = True
break
else:
jikan_unscharf = jikan_treffer
break
# Fallback 2: OMDb (eigene Datenbasis — findet oft, was TMDB nicht
# exakt trifft; braucht OMDB_API_KEY, sonst überspringt es sich selbst)
@@ -464,6 +491,15 @@ class PreScan:
matched = True
break
# Fallback 2b: der unscharfe Jikan-Treffer — besser als nichts, aber
# ehrlich als Vorschlag ausgewiesen (0,6 = dieselbe Stufe wie ein
# TMDB-Vorschlag ohne exakten Treffer; das UI zeigt die Prozentzahl an
# und der Nutzer kann korrigieren).
if not matched and jikan_unscharf:
confidence = 0.6
metadata = jikan_unscharf
matched = True
# Fallback 2: bester TMDB-Vorschlag ohne exakten Treffer — als
# VORSCHLAG gekennzeichnet (niedrige Confidence, Nutzer korrigiert)
if not matched and movies:
+78
View File
@@ -0,0 +1,78 @@
"""Was das Ähnlichkeits-Gate von Jikan wirklich durchlässt — gemessen.
Hintergrund (26.07.2026): Der SAVEPOINT v3.16 vermutete, das Gate 0,55 sei
„großzügig" und Jikan stehe in der Metadaten-Kette zu früh (vor OMDb). Beides
war nachrechenbar, weil `titel_aehnlichkeit` eine reine Funktion ist — und die
Rechnung ergab ein anderes Bild als die Vermutung:
- Für kurze Kinofilm-Titel ist das Gate WIRKLICH zu locker (Alien → 0,83).
- Für den Fall, für den Jikan eingebaut wurde, ist es sogar zu STRENG
(„Evangelion 2.22" gegen den MAL-Titel → 0,51, fällt durch).
Eine einzelne Zahl kann also beides nicht leisten. Die Folge steht in
prescan._scan_video: „exakt schlägt unscharf" statt „erster gewinnt".
Die Anime-Titel unten existieren auf MyAnimeList; die Zahlen sind ausgerechnet,
nicht geschätzt. MyAnimeList selbst war bei dieser Messung nicht erreichbar
(Jikan antwortete durchweg HTTP 504) — die Live-Abfrage steht damit weiter aus,
die Arithmetik des Gates ist davon aber unberührt.
"""
from clients.jikan import JikanClient, titel_aehnlichkeit
def test_kurze_kinofilm_titel_reissen_das_gate_0_55():
"""Diese vier würden ohne die Änderung Anime-Metadaten bekommen, obwohl
OMDb die Filme kennt — es wurde nur nie gefragt."""
faelle = [
("Alien", "Alien 9", 0.83),
("Inception", "Deception", 0.78),
("Hero", "Heroman", 0.73),
("The Dark Knight", "Dark Knight Rises", 0.69),
]
for disc, anime, erwartet in faelle:
score = titel_aehnlichkeit(disc, anime)
assert round(score, 2) == erwartet, f"{disc} vs {anime}: {score}"
# ... liegt über dem Gate (Jikan gibt also einen Treffer zurück) ...
assert score >= JikanClient.MINDEST_AEHNLICHKEIT
# ... aber unter „sicher", darf die Kette also nicht beenden.
assert score < JikanClient.SICHER_AEHNLICHKEIT
def test_echter_treffer_gilt_weiter_als_sicher():
"""Exakter Titel → 1,0. Der Normalfall eines echten Anime-Fundes darf durch
die Verschärfung NICHT verlorengehen (AGENTS Regel B)."""
assert titel_aehnlichkeit("Monster", "Monster") == 1.0
assert titel_aehnlichkeit("Cowboy Bebop", "COWBOY BEBOP") == 1.0
# Interpunktion und Kleinschreibung sind egal — norm() wirft sie weg
assert titel_aehnlichkeit("Akira", "AKIRA!") == 1.0
for a, b in (("Monster", "Monster"), ("Akira", "AKIRA!")):
assert titel_aehnlichkeit(a, b) >= JikanClient.SICHER_AEHNLICHKEIT
def test_untertitel_zaehlt_noch_als_sicher_genug():
"""Discs tragen oft den Kurztitel, MAL den vollen. Das muss ein sicherer
Treffer bleiben, sonst hilft die Quelle bei Anime nicht mehr."""
assert titel_aehnlichkeit("Steins Gate", "Steins;Gate") >= JikanClient.SICHER_AEHNLICHKEIT
def test_der_fall_fuer_den_jikan_gebaut_wurde_faellt_heute_durch():
"""Gegenbeweis zur Vermutung „das Gate ist zu großzügig": Für Evangelion
2.22 ist es zu STRENG. Der Wert ist dokumentiert, damit ein späteres
Anheben des Gates nicht unbemerkt auch diesen Fall killt."""
score = titel_aehnlichkeit(
"Evangelion 2.22", "Evangelion: 2.0 You Can (Not) Advance"
)
assert round(score, 2) == 0.51
assert score < JikanClient.MINDEST_AEHNLICHKEIT
def test_voellig_anderer_titel_wird_verworfen():
score = titel_aehnlichkeit("Blade", "Blade of the Immortal")
assert score < JikanClient.MINDEST_AEHNLICHKEIT
def test_leere_titel_geben_null():
assert titel_aehnlichkeit("", "Monster") == 0.0
assert titel_aehnlichkeit("Monster", "") == 0.0
assert titel_aehnlichkeit("!!!", "???") == 0.0
+93
View File
@@ -47,3 +47,96 @@ def test_omdb_jahr_parsing():
assert parse_year("20052012") == 2005 # Serien-Zeitspanne
assert parse_year("N/A") is None
assert parse_year("") is None
# --- Reihenfolge der Metadaten-Quellen: exakt schlägt unscharf ---------------
#
# Änderung vom 26.07.2026. Vorher galt der erste Jikan-Treffer über dem Gate
# 0,55 sofort als sicherer Fund, und OMDb kam nie dran — ein Kinofilm, den TMDB
# verpasst, bekam damit Anime-Metadaten. Die Messwerte dazu stehen in
# test_jikan_helpers.py („Alien" gegen „Alien 9" = 0,83).
class _StummerClient:
"""Ein Client, der auf jede Frage dasselbe antwortet — und mitzählt."""
def __init__(self, antwort=None):
self.antwort = antwort
self.aufrufe = 0
def lookup(self, *a, **k):
self.aufrufe += 1
return self.antwort
def search_movie(self, *a, **k):
return []
def search_tv(self, *a, **k):
return []
def get_movie_details(self, *a, **k):
return None
def find_by_imdb(self, *a, **k):
return None
def _scanner(monkeypatch, jikan_antwort, omdb_antwort):
"""PreScan ohne __init__ (das bräuchte DB + API-Keys), Cache stillgelegt."""
from prescan import prescan as modul
monkeypatch.setattr(modul, "get", lambda k: None)
monkeypatch.setattr(modul, "cache_set", lambda *a, **k: None)
scan = modul.PreScan.__new__(modul.PreScan)
scan.tmdb = _StummerClient()
scan.jikan = _StummerClient(jikan_antwort)
scan.omdb = _StummerClient(omdb_antwort)
return scan
def _jikan(score):
return {"type": "movie", "id": "mal-1", "title": "Alien 9",
"source": "jikan", "match_score": score}
def _omdb():
return {"type": "movie", "id": "tt0078748", "title": "Alien",
"source": "omdb", "year": 1979}
def test_unscharfer_jikan_treffer_laesst_omdb_zum_zug(monkeypatch):
"""Der eigentliche Fehler: „Alien" traf mit 0,83 auf den Anime „Alien 9",
und der Film wurde nie gesucht."""
scan = _scanner(monkeypatch, _jikan(0.83), _omdb())
ergebnis = scan._scan_video("/dev/sr0", {"title": "Alien", "disc_type": "Blu-ray"})
assert ergebnis.metadata["source"] == "omdb"
assert ergebnis.confidence == 0.8
assert scan.omdb.aufrufe > 0
def test_exakter_jikan_treffer_gewinnt_sofort(monkeypatch):
"""Der Fall, für den Jikan gebaut wurde, darf NICHT verlorengehen
(AGENTS Regel B): Bei einem sicheren Treffer wird OMDb nicht mehr gefragt."""
scan = _scanner(monkeypatch, _jikan(1.0), _omdb())
ergebnis = scan._scan_video("/dev/sr0", {"title": "Alien 9", "disc_type": "Blu-ray"})
assert ergebnis.metadata["source"] == "jikan"
assert ergebnis.confidence == 0.85
assert scan.omdb.aufrufe == 0
def test_unscharfer_jikan_treffer_bleibt_als_vorschlag(monkeypatch):
"""Hat OMDb nichts (kein Key, kein Treffer), ist der unscharfe Jikan-Treffer
besser als nichts — aber er wird als VORSCHLAG ausgewiesen (0,6), nicht als
sicherer Fund."""
scan = _scanner(monkeypatch, _jikan(0.83), None)
ergebnis = scan._scan_video("/dev/sr0", {"title": "Alien", "disc_type": "Blu-ray"})
assert ergebnis.metadata["source"] == "jikan"
assert ergebnis.confidence == 0.6
def test_ohne_jede_quelle_bleibt_es_unbekannt(monkeypatch):
scan = _scanner(monkeypatch, None, None)
ergebnis = scan._scan_video("/dev/sr0", {"title": "Nix Da", "disc_type": "DVD"})
assert ergebnis.confidence == 0.0
assert ergebnis.metadata == {}