fix(backend): consistent album for no-album downloads + tolerant editor resolve
Two-part fix for "[Unknown Album]" vs "Singles" mismatch that broke the
metadata editor (File not found):
- Download: always write an album tag, defaulting to "Singles" so it matches
the on-disk folder (album || 'Singles'). No-album tracks no longer show as
"[Unknown Album]" while living in a "Singles" folder. (Future downloads.)
- Editor resolve: Subsonic getSong returns a *virtual* tag-built path (e.g.
"[Unknown Album]/Title.mp3" with raw "|"), not the physical file ("Singles/
Artist - Title_YTPMV.mp3"). Add findExistingTrack as a final fallback, and
make its normalizer map the sanitized chars (/ \ : * ? " < > | and _) to a
space so a title tag matches the sanitized filename. (Fixes existing tracks.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
63a7260c94
commit
445a7bf583
3 files changed
+14
-4
No files matched your search
@@ -58,7 +58,10 @@ const _RELEASE_SUFFIX_RE = /\s*[-–]\s*(?:(?:official\s+)?(?:music\s+)?(?:singl
|
|||||||
function findExistingTrack(musicRoot, firstArtist, cleanTitle) {
|
function findExistingTrack(musicRoot, firstArtist, cleanTitle) {
|
||||||
const norm = s => (s || '').toLowerCase()
|
const norm = s => (s || '').toLowerCase()
|
||||||
.replace(/[\[((][^\]))]*[\]))]/g, '')
|
.replace(/[\[((][^\]))]*[\]))]/g, '')
|
||||||
.replace(/[,.'!?]/g, '')
|
// Filesystem-sanitized chars (/ \ : * ? " < > |) become "_" on disk — map those AND "_"
|
||||||
|
// to a space so a title tag "Kwik Flip | YTPMV" matches the file "Kwik Flip _ YTPMV".
|
||||||
|
.replace(/[/\\:*?"<>|_]/g, ' ')
|
||||||
|
.replace(/[,.'!]/g, '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
const normArtist = norm(firstArtist);
|
const normArtist = norm(firstArtist);
|
||||||
|
|||||||
@@ -359,7 +359,10 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ra
|
|||||||
'-metadata', `artist=${tagArtist}`,
|
'-metadata', `artist=${tagArtist}`,
|
||||||
'-metadata', `album_artist=${tagAlbumArtist}`,
|
'-metadata', `album_artist=${tagAlbumArtist}`,
|
||||||
);
|
);
|
||||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
// Always write an album tag, defaulting to "Singles" so it matches the on-disk folder
|
||||||
|
// (album || 'Singles'). Without this, no-album tracks land in a "Singles" folder but show
|
||||||
|
// as "[Unknown Album]" in Navidrome — a tag/folder mismatch that breaks the metadata editor.
|
||||||
|
ffArgs.push('-metadata', `album=${album || 'Singles'}`);
|
||||||
// Sort tags — only write when they differ from main (no-op for non-Japanese tracks)
|
// Sort tags — only write when they differ from main (no-op for non-Japanese tracks)
|
||||||
if (sortTitle !== title) ffArgs.push('-metadata', `TITLESORT=${sortTitle}`);
|
if (sortTitle !== title) ffArgs.push('-metadata', `TITLESORT=${sortTitle}`);
|
||||||
if (sortArtist !== tagArtist) ffArgs.push('-metadata', `ARTISTSORT=${sortArtist}`);
|
if (sortArtist !== tagArtist) ffArgs.push('-metadata', `ARTISTSORT=${sortArtist}`);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const crypto = require('crypto');
|
|||||||
|
|
||||||
const CONFIG = require('./lib/config');
|
const CONFIG = require('./lib/config');
|
||||||
const { jobs, updateJob, log } = require('./lib/jobs');
|
const { jobs, updateJob, log } = require('./lib/jobs');
|
||||||
const { sanitize, lookupItunesTrackMeta, fetchItunesTracks, searchItunesAlbumTracks } = require('./lib/metadata');
|
const { sanitize, lookupItunesTrackMeta, fetchItunesTracks, searchItunesAlbumTracks, findExistingTrack } = require('./lib/metadata');
|
||||||
const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader');
|
const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader');
|
||||||
const { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs, romajiForStorage } = require('./lib/romaji');
|
const { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs, romajiForStorage } = require('./lib/romaji');
|
||||||
|
|
||||||
@@ -1178,7 +1178,11 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
const found = rebased
|
const found = rebased
|
||||||
|| _findFileRecursive(CONFIG.musicPath, fileName)
|
|| _findFileRecursive(CONFIG.musicPath, fileName)
|
||||||
|| _findFileInDir(rebasedDir || originalDir, fileName)
|
|| _findFileInDir(rebasedDir || originalDir, fileName)
|
||||||
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem);
|
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem)
|
||||||
|
// Last resort: Subsonic getSong returns a *virtual* (tag-built) path, so the folder
|
||||||
|
// can be "[Unknown Album]" while the file actually sits in "Singles" with an artist
|
||||||
|
// prefix. findExistingTrack matches by normalized artist+title across all album folders.
|
||||||
|
|| findExistingTrack(CONFIG.musicPath, ndSong.albumArtist || ndSong.artist || '', ndSong.title || '');
|
||||||
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})` });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user