const { test, before, after } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { findExistingTrack } = require('../lib/metadata'); let ROOT; const touch = (artist, album, file) => { const dir = path.join(ROOT, artist, album); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, file), ''); }; before(() => { ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'ns-dedup-')); // KH single: normal "01 - " prefix, title contains multiple " - " touch('Geek Music', 'Kingdom Hearts 2 - Dearly Beloved - Main Theme - Single', '01 - Kingdom Hearts 2 - Dearly Beloved - Main Theme.mp3'); // KH album variant: garbage "01-01" artist prefix, no track number touch('Geek Music', 'Kingdom Hearts 2 - Dearly Beloved - Main Theme', '01-01 - Kingdom Hearts 2 - Dearly Beloved - Main Theme.mp3'); // Normal "Artist - Title" file touch('Some Artist', 'Therapy - Single', '01 - Some Artist - Therapy.mp3'); // Akzent-Schreibweise: liegt MIT Akzent auf der Platte touch('Cécile Corbel', 'SongBook Vol. 2', '03 - Cécile Corbel - Arrietty\u2019s Song.mp3'); // Soundtrack: liegt unter dem ALBUM-Kuenstler, nicht unter dem Titel-Kuenstler touch('Arcane', 'Arcane League of Legends (Soundtrack)', '01 - Bea Miller - Playground.mp3'); // Zwei verschiedene Kuenstler mit demselben Songtitel — duerfen sich NIE gegenseitig treffen touch('Cro', 'Raop', '03 - Cro - Easy.mp3'); touch('Kraftklub', 'Mit K', '05 - Kraftklub - Easy.mp3'); // Aehnlicher, aber anderer Kuenstlername touch('Cecil Taylor', 'Unit Structures', '01 - Cecil Taylor - Steps.mp3'); }); after(() => { try { fs.rmSync(ROOT, { recursive: true, force: true }); } catch {} }); test('detects a re-download when the title itself contains " - " (KH bug)', () => { assert.ok(findExistingTrack(ROOT, 'Geek Music', 'Kingdom Hearts 2 - Dearly Beloved - Main Theme')); }); test('detects it even with a garbage "01-01 - " artist prefix', () => { // Both KH files should resolve; at least one must be found → dedup skips the re-download. const hit = findExistingTrack(ROOT, 'Geek Music', 'Kingdom Hearts 2 - Dearly Beloved - Main Theme'); assert.ok(hit && hit.includes('Kingdom Hearts 2 - Dearly Beloved - Main Theme')); }); test('normal "Artist - Title" filename still matches on title alone', () => { assert.ok(findExistingTrack(ROOT, 'Some Artist', 'Therapy')); }); test('no false positive: a different title is not matched', () => { assert.equal(findExistingTrack(ROOT, 'Geek Music', 'Some Unrelated Song'), null); assert.equal(findExistingTrack(ROOT, 'Some Artist', 'Kingdom Hearts 2'), null); }); test('remix stays distinct from the base track (no regression from the version-marker fix)', () => { assert.equal(findExistingTrack(ROOT, 'Some Artist', 'Therapy (Remix)'), null); assert.ok(findExistingTrack(ROOT, 'Some Artist', 'Therapy')); // base still found }); // ── Akzente (Cécile Corbel) ──────────────────────────────────────────────────── test('findet den Song auch ohne Akzent in der Anfrage', () => { assert.ok(findExistingTrack(ROOT, 'Cecile Corbel', "Arrietty's Song"), 'ASCII-Schreibweise muss die Datei mit Akzent finden'); }); test('findet ihn ebenso mit Akzent', () => { assert.ok(findExistingTrack(ROOT, 'Cécile Corbel', "Arrietty's Song")); }); test('kein Fehlalarm: aehnlicher Name ist nicht dieselbe Person', () => { // "Cecil Taylor" darf nicht auf "Cécile Corbel" passen — und umgekehrt. assert.equal(findExistingTrack(ROOT, 'Cécile Corbel', 'Steps'), null); assert.equal(findExistingTrack(ROOT, 'Cecil Taylor', "Arrietty's Song"), null); }); test('kein Fehlalarm: Akzent-Normalisierung kollabiert keine echten Titel', () => { assert.equal(findExistingTrack(ROOT, 'Cecile Corbel', 'Ein anderer Song'), null); }); // ── Gleicher Titel, verschiedene Kuenstler ───────────────────────────────────── test('kein Fehlalarm: gleicher Titel bei zwei Kuenstlern bleibt getrennt', () => { const cro = findExistingTrack(ROOT, 'Cro', 'Easy'); const kk = findExistingTrack(ROOT, 'Kraftklub', 'Easy'); assert.ok(cro && cro.includes('Cro'), 'Cro-Version muss im Cro-Ordner gefunden werden'); assert.ok(kk && kk.includes('Kraftklub'), 'Kraftklub-Version im Kraftklub-Ordner'); assert.notEqual(cro, kk, 'die beiden duerfen nicht dieselbe Datei sein'); // Ein dritter Kuenstler mit demselben Titel findet nichts. assert.equal(findExistingTrack(ROOT, 'Irgendwer', 'Easy'), null); }); // ── Soundtrack: Album-Kuenstler vs. Titel-Kuenstler ──────────────────────────── test('Soundtrack unter dem Album-Kuenstler wird ueber diesen gefunden', () => { assert.ok(findExistingTrack(ROOT, 'Arcane', 'Playground')); }); test('unter dem Titel-Kuenstler allein wird er NICHT gefunden (Grund fuer die Zweitpruefung)', () => { // Genau diese Luecke erzeugte die Dublette: geprueft wurde nur "Bea Miller". assert.equal(findExistingTrack(ROOT, 'Bea Miller', 'Playground'), null); }); test('die Zweitpruefung schliesst die Luecke, ohne Fremdes zu treffen', () => { const pruefe = (albumArtist, trackArtist, titel) => findExistingTrack(ROOT, albumArtist, titel) || (trackArtist !== albumArtist ? findExistingTrack(ROOT, trackArtist, titel) : null); // Soundtrack-Fall: wird jetzt erkannt. assert.ok(pruefe('Bea Miller', 'Bea Miller', 'Playground') === null, 'ohne Album-Kuenstler weiterhin nichts — richtig, die Datei liegt unter Arcane'); assert.ok(pruefe('Arcane', 'Bea Miller', 'Playground'), 'mit Album-Kuenstler gefunden'); // Fremder Titel im selben Ordner darf trotz zweier Durchlaeufe nicht anschlagen. assert.equal(pruefe('Arcane', 'Bea Miller', 'Irgendein anderer Titel'), null); // Remix bleibt auch ueber zwei Durchlaeufe hinweg eigenstaendig. assert.equal(pruefe('Arcane', 'Bea Miller', 'Playground (Remix)'), null); }); // ─── Dateisuche: Titelstamm und Schraegstrich im Namen ─────────────────────── // Regression zu "K/DA – VILLAIN": Navidromes Tag-Pfad wurde zu "K/DA/ALL OUT/…", auf der Platte // steht "K_DA/ALL OUT/…". Der Pfad fand nichts, und die Aehnlichkeitssuche lieferte // "Bad Omens - Like A Villain", weil deren Stamm auf "villain" ENDET. const { findFileByStem, albumDirFromTags } = require('../lib/metadata'); let SLASH_ROOT; before(() => { SLASH_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'ns-slash-')); const put = (...segs) => { const file = segs.pop(); const dir = path.join(SLASH_ROOT, ...segs); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, file), ''); }; put('K_DA', 'ALL OUT', '03 - K_DA - VILLAIN.mp3'); put('K_DA', 'POP_STARS', '01 - K_DA - POP_STARS.mp3'); put('Bad Omens', 'THE DEATH OF PEACE OF MIND', '06 - Bad Omens - Like A Villain.mp3'); }); after(() => { try { fs.rmSync(SLASH_ROOT, { recursive: true, force: true }); } catch {} }); test('Titelstamm trifft nicht auf blosse Endsilbe', () => { // "Like A Villain" darf NICHT als "VILLAIN" durchgehen const t = findFileByStem(path.join(SLASH_ROOT, 'Bad Omens'), 'villain'); assert.equal(t, null); }); test('Titelstamm trifft "Kuenstler - Titel" weiterhin', () => { const t = findFileByStem(path.join(SLASH_ROOT, 'Bad Omens'), 'like a villain'); assert.ok(t && t.endsWith('06 - Bad Omens - Like A Villain.mp3')); }); test('Ganze Bibliothek: VILLAIN findet K_DA, nicht Bad Omens', () => { const t = findFileByStem(SLASH_ROOT, 'villain'); assert.ok(t && t.includes(path.join('K_DA', 'ALL OUT')), `unerwartet: ${t}`); }); test('Schraegstrich im Kuenstlernamen fuehrt zum richtigen Ordner', () => { assert.equal(albumDirFromTags(SLASH_ROOT, 'K/DA', 'ALL OUT'), path.join(SLASH_ROOT, 'K_DA', 'ALL OUT')); assert.equal(albumDirFromTags(SLASH_ROOT, 'K/DA', 'POP/STARS'), path.join(SLASH_ROOT, 'K_DA', 'POP_STARS')); }); test('Unbekanntes Album faellt auf den Kuenstlerordner zurueck', () => { assert.equal(albumDirFromTags(SLASH_ROOT, 'K/DA', 'Gibt Es Nicht'), path.join(SLASH_ROOT, 'K_DA')); }); test('Mehrere Kuenstler: Ordner haengt am ersten', () => { assert.equal( albumDirFromTags(SLASH_ROOT, 'K/DA, Madison Beer, Kim Petras', 'ALL OUT'), path.join(SLASH_ROOT, 'K_DA', 'ALL OUT'), ); }); test('Kein Treffer bleibt null', () => { assert.equal(albumDirFromTags(SLASH_ROOT, 'Niemand', 'Nichts'), null); });