fix: wire _findFileByStem into both file resolution fallback chains

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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-28 00:17:41 +02:00
1 parent 8989b15767
commit c405465f5c
1 file changed
+31 -2
+31 -2
View File
@@ -91,6 +91,31 @@ function _findFileInDir(dir, basename) {
return null; 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 ──────────────────────────────────────────────────────────────────── // ─── QUEUE ────────────────────────────────────────────────────────────────────
let activeDownloads = 0; let activeDownloads = 0;
const downloadQueue = []; const downloadQueue = [];
@@ -701,9 +726,11 @@ app.get('/api/song-sorttags', async (req, res) => {
const fileName = path.basename(filePath); const fileName = path.basename(filePath);
const rebased = _rebasePath(filePath, CONFIG.musicPath); const rebased = _rebasePath(filePath, CONFIG.musicPath);
const rebasedDir = rebased ? path.dirname(rebased) : null; const rebasedDir = rebased ? path.dirname(rebased) : null;
const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase();
const found = rebased const found = rebased
|| _findFileRecursive(CONFIG.musicPath, fileName) || _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' }); if (found) filePath = found; else return res.status(404).json({ error: 'File not found' });
} }
try { 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) // 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. // but Navidrome's index is stale and still points to the old name.
const originalDir = path.dirname(filePath); const originalDir = path.dirname(filePath);
const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase();
const found = rebased const found = rebased
|| _findFileRecursive(CONFIG.musicPath, fileName) || _findFileRecursive(CONFIG.musicPath, fileName)
|| _findFileInDir(rebasedDir || originalDir, fileName); || _findFileInDir(rebasedDir || originalDir, fileName)
|| _findFileByStem(CONFIG.musicPath, titleStem);
if (found) { filePath = found; } if (found) { filePath = found; }
else return res.status(404).json({ error: `File not found: ${filePath} (music root: ${CONFIG.musicPath})` }); else return res.status(404).json({ error: `File not found: ${filePath} (music root: ${CONFIG.musicPath})` });
} }