diff --git a/rippy-windows/src/kern/metadaten/tmdb.ts b/rippy-windows/src/kern/metadaten/tmdb.ts index 2d57c99..c5f0901 100644 --- a/rippy-windows/src/kern/metadaten/tmdb.ts +++ b/rippy-windows/src/kern/metadaten/tmdb.ts @@ -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 diff --git a/rippy-windows/src/kern/metadaten/zuordnung.ts b/rippy-windows/src/kern/metadaten/zuordnung.ts index 3a643a4..d0c18fc 100644 --- a/rippy-windows/src/kern/metadaten/zuordnung.ts +++ b/rippy-windows/src/kern/metadaten/zuordnung.ts @@ -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 { 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) diff --git a/rippy-windows/test/metadaten-570.test.ts b/rippy-windows/test/metadaten-570.test.ts new file mode 100644 index 0000000..643b172 --- /dev/null +++ b/rippy-windows/test/metadaten-570.test.ts @@ -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) + }) +})