fix(backend): Dublettenpruefung erkennt Akzente und Soundtrack-Ordner
Drei Luecken derselben Familie, die dazu fuehrten, dass dieselben Songs ein zweites Mal geladen wurden: - Kuenstlernamen wurden zeichengenau verglichen: "Cécile Corbel" und "Cecile Corbel" galten als zwei Personen. Welche Schreibweise ankommt, haengt an der Quelle (Deezer vs. Spotify). Der Vergleich normalisiert jetzt Akzente weg — nur der Vergleich, Tags und Dateinamen behalten das Original. - Typografische Apostrophe: "Arrietty’s Song" (U+2019) und "Arrietty's Song" fielen auseinander. Fiel beim Testen auf, betrifft viele Soundtracks. - findExistingTrack durchsucht nur den Ordner des uebergebenen Kuenstlers. Bei Soundtracks weicht der Album- vom Titel-Kuenstler ab, der Song landet je nach Ladeweg unter dem einen oder anderen — geprueft wurde jeweils der falsche. Weicht der Titel-Kuenstler ab, wird nun auch dessen Ordner geprueft (~0,6 ms bei 28.801 Dateien, gegenueber 3-9 s pro Download). Sieben neue Tests zielen gezielt auf Fehlalarme: aehnliche Namen (Cecil Taylor vs. Cécile Corbel), gleicher Titel bei verschiedenen Kuenstlern, Remix gegen Basistitel, fremder Titel im Soundtrack-Ordner — alle bleiben getrennt. Gesamte Suite: 42 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f62b248bb6
commit
3c99778567
4 files changed
+96
-3
No files matched your search
@@ -116,6 +116,17 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an
|
||||
unverändert. Genau deshalb ging es bei Songs ohne Dublette immer und bei anderen nie. Der über
|
||||
Ähnlichkeit gefundene Kandidat wird jetzt gegen die Tags geprüft, die Navidrome für den Song
|
||||
meldet, und bei Widerspruch mit einer erklärenden Meldung abgebrochen, statt zu raten.
|
||||
- **Dieselbe Playlist zweimal geladen ⇒ Songs doppelt auf der Platte.** Die Dublettenprüfung
|
||||
verglich Künstlernamen zeichengenau, sodass „Cécile Corbel" und „Cecile Corbel" als zwei
|
||||
verschiedene Personen galten — welche Schreibweise ankommt, hängt davon ab, ob die Quelle Deezer
|
||||
oder Spotify war. Der Vergleich ignoriert jetzt Akzente (nur der **Vergleich** — Tags und
|
||||
Dateinamen behalten die Originalschreibweise) und ebenso typografische Apostrophe, an denen
|
||||
`Arrietty’s Song` und `Arrietty's Song` bisher auseinanderfielen.
|
||||
- **Soundtracks landeten doppelt** — die Prüfung durchsucht nur den Ordner des Album-Künstlers.
|
||||
Bei Soundtracks und Compilations weicht der vom Titel-Künstler ab: als Album geladen landet der
|
||||
Song unter `Arcane/`, einzeln geladen unter `Bea Miller/` — die Prüfung sah jeweils im falschen
|
||||
Ordner nach. Weicht der Titel-Künstler ab, wird jetzt auch dessen Ordner geprüft. Gemessen an
|
||||
28.801 Dateien kostet der zweite Durchlauf ~0,6 ms, gegenüber 3–9 s pro Download.
|
||||
- **Stumm übersprungene Umbenennungen** — lag am Zielnamen schon eine Datei, blieb der Song unter
|
||||
altem Namen bzw. im alten Ordner liegen, ohne Hinweis. Solche Fälle kommen jetzt als Warnung
|
||||
zurück; der Editor zeigt sie samt bearbeitetem Dateipfad an und bleibt offen.
|
||||
|
||||
@@ -61,14 +61,21 @@ const _VERSION_KW = /\b(?:remix|live|acoustic|instrumental|version|vip|flip|boot
|
||||
// release suffixes and track prefixes. Handles DJ OKAWARI vs DJ Okawari, and
|
||||
// "recognize - Single.mp3" vs "recognize.mp3" (drift between runs).
|
||||
function findExistingTrack(musicRoot, firstArtist, cleanTitle) {
|
||||
const norm = s => (s || '').toLowerCase()
|
||||
const norm = s => (s || '')
|
||||
// Akzente abtrennen und verwerfen: dieselbe Kuenstlerin kommt je nach Quelle als
|
||||
// "Cécile Corbel" oder "Cecile Corbel" — ohne das galt sie als zwei Personen und derselbe
|
||||
// Song wurde ein zweites Mal geladen. Betrifft nur den VERGLEICH, nicht Tags oder Dateinamen.
|
||||
.normalize('NFD').replace(/\p{M}+/gu, '')
|
||||
.toLowerCase()
|
||||
// Strip bracketed noise (feat. X, Official Video, Prod. by …) but KEEP version markers
|
||||
// (Remix/Live/Acoustic/…) so distinct recordings aren't collapsed onto the base track.
|
||||
.replace(/[\[((]([^\]))]*)[\]))]/g, (m, inner) => _VERSION_KW.test(inner) ? m : '')
|
||||
// Filesystem-sanitized chars (/ \ : * ? " < > |) become "_" on disk — map those AND "_"
|
||||
// to a space so a title tag "Kwik Flip | YTPMV" matches the file "Kwik Flip _ YTPMV".
|
||||
.replace(/[/\\:*?"<>|_]/g, ' ')
|
||||
.replace(/[,.'!]/g, '')
|
||||
// Auch die typografischen Apostrophe: Anbieter schreiben "Arrietty’s Song" (U+2019), eine
|
||||
// spaetere Anfrage kommt mit geradem "'" — ohne das galten beide als verschiedene Titel.
|
||||
.replace(/[,.!'\u2018\u2019\u02bc\u00b4`]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const normArtist = norm(firstArtist);
|
||||
|
||||
+11
-1
@@ -144,7 +144,17 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
|
||||
const safeArtistForFile = sanitize((artist || '').replace(/;/g, ','));
|
||||
const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`;
|
||||
|
||||
const existing = findExistingTrack(outputDir || CONFIG.musicPath, safeArtist, cleanTitle);
|
||||
// Dublettenpruefung: findExistingTrack durchsucht NUR den Ordner des uebergebenen Kuenstlers.
|
||||
// Bei Soundtracks und Compilations weicht der Album-Kuenstler vom Titel-Kuenstler ab, und je
|
||||
// nachdem, ob der Song als Album oder einzeln geladen wird, landet er unter dem einen ODER dem
|
||||
// anderen. Die Pruefung sah dann jeweils im falschen Ordner nach und lud ihn erneut. Deshalb wird
|
||||
// bei abweichendem Titel-Kuenstler auch dessen Ordner geprueft — zweiter Durchlauf nur in genau
|
||||
// diesem Fall, gemessen ~0,6 ms zusaetzlich bei knapp 29.000 Dateien.
|
||||
const trackArtist = sanitize((artist || '').split(/[,;&]/)[0].trim());
|
||||
const existing = findExistingTrack(outputDir || CONFIG.musicPath, safeArtist, cleanTitle)
|
||||
|| (trackArtist && trackArtist !== safeArtist
|
||||
? findExistingTrack(outputDir || CONFIG.musicPath, trackArtist, cleanTitle)
|
||||
: null);
|
||||
if (existing) {
|
||||
log(jobId, `⏭ Bereits vorhanden: ${path.basename(existing)} — übersprungen`);
|
||||
return { success: true, dir, skipped: true };
|
||||
|
||||
@@ -22,6 +22,15 @@ before(() => {
|
||||
'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 {} });
|
||||
|
||||
@@ -48,3 +57,59 @@ test('remix stays distinct from the base track (no regression from the version-m
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user