From c405465f5c72793014733fc291e26949fff4b848 Mon Sep 17 00:00:00 2001 From: Honey Date: Thu, 28 May 2026 00:17:41 +0200 Subject: [PATCH] fix: wire _findFileByStem into both file resolution fallback chains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handles "File not found" for songs with non-standard folder/artist names (e.g. artist separator • or year-only album folders) by doing a deep recursive title-stem search as a last resort after rebase and dir scan. Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index a339efc..4a87bda 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -91,6 +91,31 @@ function _findFileInDir(dir, basename) { return null; } +// Last-resort: deep recursive search by title stem (handles folder renames after metadata edit). +// Convention: MusicRoot/AlbumArtist/Album/{01 - }Artist - Title.ext +// The title stem is everything after the optional "NN - Artist - " prefix. +// We match any audio file whose stem ends with the same title words. +function _findFileByStem(dir, titleStem, depth = 0) { + if (depth > 4) return null; + 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)) { + // Strip extension, strip leading "NN - ", then strip optional "Artist - " prefix + const bare = entry.name.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, ''); + // bare may be "Artist - Title" or just "Title" — match from the end + const bareStem = bare.toLowerCase(); + if (bareStem === titleStem || bareStem.endsWith(' - ' + titleStem) || bareStem.endsWith(titleStem)) { + return path.join(dir, entry.name); + } + } + } + } catch {} + return null; +} + // ─── QUEUE ──────────────────────────────────────────────────────────────────── let activeDownloads = 0; const downloadQueue = []; @@ -701,9 +726,11 @@ app.get('/api/song-sorttags', async (req, res) => { const fileName = path.basename(filePath); const rebased = _rebasePath(filePath, CONFIG.musicPath); const rebasedDir = rebased ? path.dirname(rebased) : null; + const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase(); const found = rebased || _findFileRecursive(CONFIG.musicPath, fileName) - || _findFileInDir(rebasedDir || path.dirname(filePath), fileName); + || _findFileInDir(rebasedDir || path.dirname(filePath), fileName) + || _findFileByStem(CONFIG.musicPath, titleStem); if (found) filePath = found; else return res.status(404).json({ error: 'File not found' }); } try { @@ -834,9 +861,11 @@ app.post('/api/edit-metadata', async (req, res) => { // Also check the original dir — the file may have been renamed (e.g. artist added to filename) // but Navidrome's index is stale and still points to the old name. const originalDir = path.dirname(filePath); + const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase(); const found = rebased || _findFileRecursive(CONFIG.musicPath, fileName) - || _findFileInDir(rebasedDir || originalDir, fileName); + || _findFileInDir(rebasedDir || originalDir, fileName) + || _findFileByStem(CONFIG.musicPath, titleStem); if (found) { filePath = found; } else return res.status(404).json({ error: `File not found: ${filePath} (music root: ${CONFIG.musicPath})` }); }