fix(backend): Schraegstrich im Namen und zu lockere Titelsuche fuehrten auf die falsche Datei

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-20 20:35:36 +02:00
1 parent eb72e5c27d
commit 24558497da
4 files changed
+193 -53

No files matched your search

+65
View File
@@ -118,6 +118,70 @@ function findExistingTrack(musicRoot, firstArtist, cleanTitle) {
return null;
}
const AUDIO_EXT_RE = /\.(mp3|flac|opus|m4a|ogg|wav)$/i;
// Vereinheitlicht einen Dateinamen-Stamm fuer den lockeren Vergleich:
// 1. fuehrende Track-Praefixe wie "01-15 – " oder "01 - " abschneiden
// 2. alle Strichvarianten (en/em dash) auf " - " bringen
function normStem(s) {
return s
.replace(/^\d[\d\-–—]*\s*[-–—]\s*/, '')
.replace(/\s*[–—]\s*/g, ' - ');
}
// Tiefensuche nach einer Audiodatei, deren Stamm zum Titel passt. Letzter Ausweg, wenn der von
// Navidrome gemeldete Pfad nicht existiert (Ordner umbenannt, Tag-Pfad statt echtem Pfad).
//
// WICHTIG: der Titel muss ein VOLLSTAENDIGES Feld des Dateinamens sein, nie nur dessen Ende.
// Ein blosses endsWith(titel) liess "Bad Omens - Like A Villain" als Treffer fuer den Titel
// "VILLAIN" durchgehen — ein voellig anderer Song derselben Endsilbe. Genau solche Fehlgriffe
// landen sonst im Metadaten-Editor oder, schlimmer, im Loeschen-Endpunkt.
function findFileByStem(dir, titleStem, depth = 0) {
if (depth > 4) return null;
const normTitle = normStem(titleStem);
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
const found = findFileByStem(path.join(dir, entry.name), titleStem, depth + 1);
if (found) return found;
} else if (AUDIO_EXT_RE.test(entry.name)) {
const bare = entry.name.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '');
const bareStem = normStem(bare.toLowerCase());
// Treffer: exakt, "Kuenstler - Titel", oder Release-Suffix-Versatz ("… - Single").
if (bareStem === normTitle ||
bareStem.endsWith(' - ' + normTitle) ||
bareStem.startsWith(normTitle + ' - ') ||
normTitle.startsWith(bareStem + ' - ')) {
return path.join(dir, entry.name);
}
}
}
} catch {}
return null;
}
// Navidrome gibt ueber die Subsonic-API standardmaessig keinen echten Pfad heraus, sondern baut
// einen aus den Tags: {AlbumArtist}/{Album}/{DD}-{TT} - {Titel}.{Endung}. Steht ein "/" IM Namen
// — "K/DA", "POP/STARS" —, wird daraus eine zusaetzliche Verzeichnisebene, waehrend auf der Platte
// sanitize() ein "_" geschrieben hat ("K_DA/POP_STARS"). Der gemeldete Pfad existiert dann nicht,
// und die Suche faellt auf Aehnlichkeit zurueck. Diese Funktion baut den Ordner stattdessen genau
// so zusammen, wie der Downloader ihn angelegt haette.
function albumDirFromTags(musicRoot, albumArtist, album) {
if (!musicRoot || !albumArtist) return null;
// Der Downloader legt den Ordner unter dem ERSTEN Kuenstler an (mainArtist.split(/[,;&]/)[0]).
const ersterKuenstler = String(albumArtist).split(/[,;&]/)[0].trim();
const kandidaten = [];
for (const k of new Set([ersterKuenstler, String(albumArtist).trim()])) {
if (!k) continue;
if (album) kandidaten.push(path.join(musicRoot, sanitize(k), sanitize(album)));
kandidaten.push(path.join(musicRoot, sanitize(k)));
}
for (const c of kandidaten) {
try { if (fs.statSync(c).isDirectory()) return c; } catch {}
}
return null;
}
// Fetch plain + synced lyrics from LRCLIB
async function fetchLyrics(artist, title, album) {
try {
@@ -140,4 +204,5 @@ async function fetchLyrics(artist, title, album) {
module.exports = {
sanitize, navidromeArtist,
downloadArtwork, findExistingTrack,
normStem, findFileByStem, albumDirFromTags,
};