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:
@@ -28,7 +28,19 @@ def titel_aehnlichkeit(a: str, b: str) -> float:
|
|||||||
|
|
||||||
|
|
||||||
class JikanClient:
|
class JikanClient:
|
||||||
|
# Unter dieser Ähnlichkeit gibt lookup() gar nichts zurück.
|
||||||
MINDEST_AEHNLICHKEIT = 0.55
|
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):
|
def __init__(self):
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ from clients.thetvdb import TheTVDBClient
|
|||||||
from cache import get, set as cache_set
|
from cache import get, set as cache_set
|
||||||
from cache.keys import generate_prescan_key
|
from cache.keys import generate_prescan_key
|
||||||
|
|
||||||
|
|
||||||
def normalize_disc_label(label: str) -> str:
|
def normalize_disc_label(label: str) -> str:
|
||||||
"""Disc-Labels wie 'PULP_FICTION_DE' → 'Pulp Fiction De' (pure Funktion).
|
"""Disc-Labels wie 'PULP_FICTION_DE' → 'Pulp Fiction De' (pure Funktion).
|
||||||
|
|
||||||
@@ -442,14 +441,42 @@ class PreScan:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Fallback 1: Jikan/MyAnimeList (kostenlos, KEIN Key) — für Anime die
|
# 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:
|
if not matched:
|
||||||
for kandidat in kandidaten:
|
for kandidat in kandidaten:
|
||||||
jikan_treffer = self.jikan.lookup(kandidat)
|
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
|
confidence = 0.85
|
||||||
metadata = jikan_treffer
|
metadata = jikan_treffer
|
||||||
matched = True
|
matched = True
|
||||||
|
else:
|
||||||
|
jikan_unscharf = jikan_treffer
|
||||||
break
|
break
|
||||||
|
|
||||||
# Fallback 2: OMDb (eigene Datenbasis — findet oft, was TMDB nicht
|
# Fallback 2: OMDb (eigene Datenbasis — findet oft, was TMDB nicht
|
||||||
@@ -464,6 +491,15 @@ class PreScan:
|
|||||||
matched = True
|
matched = True
|
||||||
break
|
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
|
# Fallback 2: bester TMDB-Vorschlag ohne exakten Treffer — als
|
||||||
# VORSCHLAG gekennzeichnet (niedrige Confidence, Nutzer korrigiert)
|
# VORSCHLAG gekennzeichnet (niedrige Confidence, Nutzer korrigiert)
|
||||||
if not matched and movies:
|
if not matched and movies:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -47,3 +47,96 @@ def test_omdb_jahr_parsing():
|
|||||||
assert parse_year("2005–2012") == 2005 # Serien-Zeitspanne
|
assert parse_year("2005–2012") == 2005 # Serien-Zeitspanne
|
||||||
assert parse_year("N/A") is None
|
assert parse_year("N/A") is None
|
||||||
assert parse_year("") 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 == {}
|
||||||
|
|||||||
Reference in New Issue
Block a user