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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-27 21:48:05 +02:00
1 parent 760d8d5e1f
commit 2ae439bfd2
1 file changed
+41 -3
+41 -3
View File
@@ -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}` });
}