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:
co-authored by
Claude Opus 5
parent
c1ee339446
commit
3dacd7cb4f
@@ -25,6 +25,11 @@ export interface FilmTreffer {
|
|||||||
id: number
|
id: number
|
||||||
title?: string
|
title?: string
|
||||||
name?: 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
|
poster_path?: string | null
|
||||||
backdrop_path?: string | null
|
backdrop_path?: string | null
|
||||||
release_date?: string
|
release_date?: string
|
||||||
|
|||||||
@@ -153,11 +153,19 @@ export function istStub(details: FilmDetails): boolean {
|
|||||||
return !beschreibung && !bild && !bewertet
|
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,
|
/** Titel → Wortmenge, Satzzeichen fallen weg. Grundlage von Überlappung,
|
||||||
* Abdeckung und Titelvergleich — alle drei sollen dasselbe „Wort" meinen. */
|
* Abdeckung und Titelvergleich — alle drei sollen dasselbe „Wort" meinen. */
|
||||||
function worte(t: string): Set<string> {
|
function worte(t: string): Set<string> {
|
||||||
return new Set(
|
return new Set(
|
||||||
t
|
ohneAkzente(t)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(/[^\p{L}\p{N}]+/u)
|
.split(/[^\p{L}\p{N}]+/u)
|
||||||
.filter((w) => w.length > 0),
|
.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
|
// Wortmenge wäre hier falsch: Sie machte 'New York, New York' gleich
|
||||||
// 'New York'. Verglichen wird die Wort-FOLGE, nur ohne Satzzeichen.
|
// 'New York'. Verglichen wird die Wort-FOLGE, nur ohne Satzzeichen.
|
||||||
const nackt = (t: string): string =>
|
const nackt = (t: string): string =>
|
||||||
t
|
ohneAkzente(t)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(/[^\p{L}\p{N}]+/u)
|
.split(/[^\p{L}\p{N}]+/u)
|
||||||
.filter((w) => w.length > 0)
|
.filter((w) => w.length > 0)
|
||||||
@@ -298,9 +306,10 @@ export async function zuordnen(
|
|||||||
const filme = nurSerie ? [] : ((await tmdb.searchMovie(kandidat.text)) ?? [])
|
const filme = nurSerie ? [] : ((await tmdb.searchMovie(kandidat.text)) ?? [])
|
||||||
if (filme.length > 0 && ersteFilme.length === 0) ersteFilme = filme
|
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) {
|
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)
|
const details = await tmdb.movieDetails(film.id)
|
||||||
if (details !== null && !istStub(details)) {
|
if (details !== null && !istStub(details)) {
|
||||||
const meta = ausDetails('movie', film.id, details)
|
const meta = ausDetails('movie', film.id, details)
|
||||||
@@ -312,9 +321,9 @@ export async function zuordnen(
|
|||||||
const serien = (await tmdb.searchTv(kandidat.text)) ?? []
|
const serien = (await tmdb.searchTv(kandidat.text)) ?? []
|
||||||
if (serien.length > 0 && ersteSerien.length === 0) ersteSerien = serien
|
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) {
|
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)
|
const details = await tmdb.tvDetails(serie.id)
|
||||||
if (details !== null && !istStub(details)) {
|
if (details !== null && !istStub(details)) {
|
||||||
const meta = ausDetails('tv', serie.id, details)
|
const meta = ausDetails('tv', serie.id, details)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// 5.7.0 (Durchsicht 12.09.2026, F14): Der wörtliche Vergleich kennt jetzt
|
||||||
|
// Akzente und den Originaltitel. Die drei Filme des Commanders (Der
|
||||||
|
// Patriot, Déjà Vu, Training Day) tragen englische Labels, TMDb antwortet
|
||||||
|
// auf Deutsch — vorher ein Vorschlag mit 60 %, die Automatik wartete.
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { gleichBedeutend, ohneAkzente, wortUeberlappung, zuordnen } from '../src/kern/metadaten/zuordnung'
|
||||||
|
|
||||||
|
const keinOmdb = { verfuegbar: false, lookup: async () => null }
|
||||||
|
|
||||||
|
function tmdb(filme: Array<{ id: number; title: string; original_title?: string }>) {
|
||||||
|
return {
|
||||||
|
verfuegbar: true,
|
||||||
|
searchMovie: async () => filme,
|
||||||
|
searchTv: async () => [],
|
||||||
|
movieDetails: async (id: number) => {
|
||||||
|
const f = filme.find((x) => x.id === id)
|
||||||
|
return f === undefined ? null : { id, title: f.title, release_date: '2000-06-28', overview: 'Ü', vote_count: 100 }
|
||||||
|
},
|
||||||
|
tvDetails: async () => null,
|
||||||
|
findByImdb: async () => null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ohneAkzente und gleichBedeutend', () => {
|
||||||
|
it('Déjà Vu ist Deja Vu', () => {
|
||||||
|
expect(ohneAkzente('Déjà Vu')).toBe('Deja Vu')
|
||||||
|
expect(gleichBedeutend('Déjà Vu', 'Deja Vu')).toBe(true)
|
||||||
|
expect(gleichBedeutend('Amélie', 'AMELIE')).toBe(true)
|
||||||
|
expect(wortUeberlappung('Déjà Vu', 'Deja Vu')).toBe(1)
|
||||||
|
})
|
||||||
|
it('Umlaute bleiben unterscheidbar genug (ä → a, ß → ss)', () => {
|
||||||
|
expect(gleichBedeutend('Der Baader Meinhof Komplex', 'Der Baader Meinhof Komplex')).toBe(true)
|
||||||
|
expect(gleichBedeutend('Das Boot', 'Das Bot')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('zuordnen mit Originaltitel', () => {
|
||||||
|
it('Label „The Patriot" trifft „Der Patriot" (original_title) wörtlich — 0,95', async () => {
|
||||||
|
const z = await zuordnen('The Patriot', tmdb([{ id: 8489, title: 'Der Patriot', original_title: 'The Patriot' }]), keinOmdb)
|
||||||
|
expect(z.confidence).toBe(0.95)
|
||||||
|
expect(z.titel).toBe('Der Patriot')
|
||||||
|
})
|
||||||
|
it('Label „Deja Vu" trifft „Déjà Vu – Wettlauf gegen die Zeit" über den Originaltitel', async () => {
|
||||||
|
const z = await zuordnen(
|
||||||
|
'Deja Vu',
|
||||||
|
tmdb([
|
||||||
|
{ id: 7551, title: 'Déjà Vu – Wettlauf gegen die Zeit', original_title: 'Deja Vu' },
|
||||||
|
{ id: 1, title: 'Déjà Vu', original_title: 'Déjà Vu' },
|
||||||
|
]),
|
||||||
|
keinOmdb,
|
||||||
|
)
|
||||||
|
expect(z.confidence).toBe(0.95)
|
||||||
|
expect(z.titel).toBe('Déjà Vu – Wettlauf gegen die Zeit')
|
||||||
|
})
|
||||||
|
it('ohne Originaltitel und ohne wörtlichen Treffer bleibt es ein Vorschlag', async () => {
|
||||||
|
const z = await zuordnen('The Patriot', tmdb([{ id: 1, title: 'Der Patriot' }, { id: 2, title: 'Patriot Games' }, { id: 3, title: 'Der Patriot 2' }, { id: 4, title: 'Patrioten' }]), keinOmdb)
|
||||||
|
expect(z.confidence).toBe(0.6)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user