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:
1 parent
760d8d5e1f
commit
2ae439bfd2
1 file changed
+41
-3
@@ -62,6 +62,35 @@ function _findFileRecursive(dir, name, depth = 0) {
|
|||||||
return null;
|
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 ────────────────────────────────────────────────────────────────────
|
// ─── QUEUE ────────────────────────────────────────────────────────────────────
|
||||||
let activeDownloads = 0;
|
let activeDownloads = 0;
|
||||||
const downloadQueue = [];
|
const downloadQueue = [];
|
||||||
@@ -652,7 +681,9 @@ app.get('/api/song-sorttags', async (req, res) => {
|
|||||||
} catch (e) { return res.status(502).json({ error: e.message }); }
|
} catch (e) { return res.status(502).json({ error: e.message }); }
|
||||||
if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath);
|
if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath);
|
||||||
if (!fs.existsSync(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' });
|
if (found) filePath = found; else return res.status(404).json({ error: 'File not found' });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -710,10 +741,17 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
// If not absolute, try relative to music root
|
// If not absolute, try relative to music root
|
||||||
if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath);
|
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)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
|
const rebased = _rebasePath(filePath, CONFIG.musicPath);
|
||||||
const fileName = path.basename(filePath);
|
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; }
|
if (found) { filePath = found; }
|
||||||
else return res.status(404).json({ error: `File not found: ${filePath}` });
|
else return res.status(404).json({ error: `File not found: ${filePath}` });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user