From 2ae439bfd2b07d9b21f7075dd1f3a6074277736f Mon Sep 17 00:00:00 2001 From: Honey Date: Wed, 27 May 2026 21:48:05 +0200 Subject: [PATCH] fix: resolve Navidrome path mismatch across Docker containers for metadata edit When Navidrome and the backend run in separate containers, Navidrome returns absolute paths rooted at its own mount (e.g. /opt/navidrome/music) which don't exist in the backend container (mounted at /music). Add three-stage fallback: 1. _rebasePath: strip leading path components and try under CONFIG.musicPath 2. _findFileRecursive: exact filename search under CONFIG.musicPath 3. _findFileInDir: fuzzy search in album dir by track-number prefix or title stem (handles files renamed to include artist in the filename) Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 44 ++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index abffff0..67b3a62 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -62,6 +62,35 @@ function _findFileRecursive(dir, name, depth = 0) { return null; } +// When Navidrome runs in a different container its paths start with a different root +// (e.g. /opt/navidrome/music) than the backend's mount (e.g. /music). +// Strip leading components from the absolute path and try joining with CONFIG.musicPath. +function _rebasePath(absolutePath, musicPath) { + const parts = absolutePath.split('/').filter(Boolean); + for (let i = 1; i < parts.length; i++) { + const candidate = path.join(musicPath, ...parts.slice(i)); + if (fs.existsSync(candidate)) return candidate; + } + return null; +} + +// When exact filename fails (e.g. file was renamed to include artist), +// look in the same dir for any audio file sharing the track-number prefix or title stem. +const AUDIO_EXT_RE = /\.(mp3|flac|opus|m4a|ogg|wav)$/i; +function _findFileInDir(dir, basename) { + try { + const m = basename.match(/^(\d+\s*-\s*)/); + const prefix = m ? m[1] : null; + const stem = basename.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase(); + for (const entry of fs.readdirSync(dir)) { + if (!AUDIO_EXT_RE.test(entry)) continue; + if (prefix && entry.startsWith(prefix)) return path.join(dir, entry); + if (entry.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase() === stem) return path.join(dir, entry); + } + } catch {} + return null; +} + // ─── QUEUE ──────────────────────────────────────────────────────────────────── let activeDownloads = 0; const downloadQueue = []; @@ -652,7 +681,9 @@ app.get('/api/song-sorttags', async (req, res) => { } catch (e) { return res.status(502).json({ error: e.message }); } if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath); if (!fs.existsSync(filePath)) { - const found = _findFileRecursive(CONFIG.musicPath, path.basename(filePath)); + const rebased = _rebasePath(filePath, CONFIG.musicPath); + const found = rebased || _findFileRecursive(CONFIG.musicPath, path.basename(filePath)) + || _findFileInDir(path.dirname(rebased || path.join(CONFIG.musicPath, path.basename(filePath))), path.basename(filePath)); if (found) filePath = found; else return res.status(404).json({ error: 'File not found' }); } try { @@ -710,10 +741,17 @@ app.post('/api/edit-metadata', async (req, res) => { // If not absolute, try relative to music root if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath); - // Fallback: if Navidrome path stale/wrong, search for the file by name under music root + // Fallback: Navidrome may return a path from a different container mount. + // 1. Rebase: strip leading path components and rejoin under CONFIG.musicPath. + // 2. Exact recursive search under CONFIG.musicPath. + // 3. Fuzzy dir search: same dir, matching track-number prefix or title stem. if (!fs.existsSync(filePath)) { + const rebased = _rebasePath(filePath, CONFIG.musicPath); const fileName = path.basename(filePath); - const found = _findFileRecursive(CONFIG.musicPath, fileName); + const rebasedDir = rebased ? path.dirname(rebased) : null; + const found = rebased + || _findFileRecursive(CONFIG.musicPath, fileName) + || (rebasedDir && _findFileInDir(rebasedDir, fileName)); if (found) { filePath = found; } else return res.status(404).json({ error: `File not found: ${filePath}` }); }