fix(v5): Erkennung vergleicht auch den Originaltitel und ignoriert Akzente

WAS: TMDb antwortet mit language=de-DE auf Deutsch („Der Patriot", „Déjà Vu
– Wettlauf gegen die Zeit"), die Disc trägt den Originaltitel
(THE_PATRIOT, DEJA_VU). Der wörtliche Vergleich prüft jetzt title UND
original_title (name/original_name bei Serien); Akzente fallen vor dem
Vergleich (NFD, ohneAkzente) — auch bei Wort-Überlappung und Abdeckung.
WARUM: Durchsicht 12.09.2026, Fund F14 (Sonde 1): „Déjà Vu" ≠ „Deja Vu",
„Der Patriot" ≠ „The Patriot" — nur 60 % statt 95 %, die Automatik
wartete. Tests mit genau diesen Titeln.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 21:41:11 +02:00
co-authored by Claude Opus 5
parent c1ee339446
commit 3dacd7cb4f
3 changed files with 79 additions and 6 deletions
+5
View File
@@ -25,6 +25,11 @@ export interface FilmTreffer {
id: number
title?: string
name?: string
/** 5.7.0 (Durchsicht F14): TMDb antwortet mit language=de-DE auf Deutsch
* („Der Patriot"), die Disc trägt meist den Originaltitel („The
* Patriot") — der wörtliche Vergleich braucht beide. */
original_title?: string
original_name?: string
poster_path?: string | null
backdrop_path?: string | null
release_date?: string
+15 -6
View File
@@ -153,11 +153,19 @@ export function istStub(details: FilmDetails): boolean {
return !beschreibung && !bild && !bewertet
}
/** Akzente und Umlaut-Striche fallen (5.7.0, Durchsicht F14): „Déjà Vu"
* auf der Disc heißt „DEJA_VU", bei TMDb „Déjà Vu" — dasselbe Wort. NFD
* zerlegt „é" in „e" + Akzent, \p{M} entfernt den Akzent. Gemessen an
* Sonde 1 der Durchsicht: vorher kein wörtlicher Treffer. */
export function ohneAkzente(t: string): string {
return t.normalize('NFD').replace(/\p{M}+/gu, '').replace(/ß/g, 'ss')
}
/** Titel → Wortmenge, Satzzeichen fallen weg. Grundlage von Überlappung,
* Abdeckung und Titelvergleich — alle drei sollen dasselbe „Wort" meinen. */
function worte(t: string): Set<string> {
return new Set(
t
ohneAkzente(t)
.toLowerCase()
.split(/[^\p{L}\p{N}]+/u)
.filter((w) => w.length > 0),
@@ -185,7 +193,7 @@ export function gleichBedeutend(a: string, b: string): boolean {
// Wortmenge wäre hier falsch: Sie machte 'New York, New York' gleich
// 'New York'. Verglichen wird die Wort-FOLGE, nur ohne Satzzeichen.
const nackt = (t: string): string =>
t
ohneAkzente(t)
.toLowerCase()
.split(/[^\p{L}\p{N}]+/u)
.filter((w) => w.length > 0)
@@ -298,9 +306,10 @@ export async function zuordnen(
const filme = nurSerie ? [] : ((await tmdb.searchMovie(kandidat.text)) ?? [])
if (filme.length > 0 && ersteFilme.length === 0) ersteFilme = filme
// Wörtlicher Filmtreffer (0,95)
// Wörtlicher Filmtreffer (0,95) — auf den deutschen ODER den Originaltitel
// (F14: „The Patriot" auf der Disc, „Der Patriot" bei TMDb).
for (const film of filme) {
if (gleichBedeutend(film.title ?? '', kandidat.text)) {
if (gleichBedeutend(film.title ?? '', kandidat.text) || gleichBedeutend(film.original_title ?? '', kandidat.text)) {
const details = await tmdb.movieDetails(film.id)
if (details !== null && !istStub(details)) {
const meta = ausDetails('movie', film.id, details)
@@ -312,9 +321,9 @@ export async function zuordnen(
const serien = (await tmdb.searchTv(kandidat.text)) ?? []
if (serien.length > 0 && ersteSerien.length === 0) ersteSerien = serien
// Wörtlicher Serientreffer (0,90)
// Wörtlicher Serientreffer (0,90) — deutscher oder Originalname.
for (const serie of serien) {
if (gleichBedeutend(serie.name ?? '', kandidat.text)) {
if (gleichBedeutend(serie.name ?? '', kandidat.text) || gleichBedeutend(serie.original_name ?? '', kandidat.text)) {
const details = await tmdb.tvDetails(serie.id)
if (details !== null && !istStub(details)) {
const meta = ausDetails('tv', serie.id, details)