feat: artist separator normalization + separator hints in metadata editor
- Normalize •, ·, ;, , separators on save (both edit and download paths) - Fix ndSong scope bug (was inaccessible outside try block in edit-metadata) - Apply _normalizeArtists to artist/albumArtist before ffmpeg tags and file rename - Storage format: Artist A;Artist B (no space, Navidrome/MusicBrainz style) - Display format: Artist A; Artist B (space, via _fmtArtists in frontend) - Show separator hint below label in metadata editor fields - Remove hover effect on player bar song title Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ba5b8ce9a
commit
4842c89c4e
5 files changed
+40
-19
No files matched your search
@@ -240,6 +240,13 @@ app.post('/api/spotify/resolve', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Normalize user-supplied artist strings — accepts •, ·, ;, or , as separators
|
||||
// and rejoins with the canonical sep ("; " for track artist, ", " for album artist).
|
||||
function _normalizeArtists(str, sep) {
|
||||
if (!str) return str;
|
||||
return str.split(/\s*[•·;,]\s*/).map(s => s.trim()).filter(Boolean).join(sep);
|
||||
}
|
||||
|
||||
// Strip " - Topic" suffix YouTube Music appends to artist channel names
|
||||
function _cleanYtArtist(name) {
|
||||
return (name || '').replace(/\s*-\s*Topic$/i, '').trim();
|
||||
@@ -596,15 +603,20 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
discNum, discTotal, genre, artworkBase64, artworkMime, artworkUrl: newArtworkUrl } = req.body;
|
||||
if (!ndUrl || !user || !pass || !songId) return res.status(400).json({ error: 'ndUrl, user, pass, songId required' });
|
||||
|
||||
// Normalize artist separators — accept •, ·, ;, , and rejoin canonically
|
||||
const normalArtist = _normalizeArtists(artist, ';');
|
||||
const normalAlbumArtist = _normalizeArtists(albumArtist, ', ');
|
||||
|
||||
// Resolve file path from Navidrome
|
||||
let filePath;
|
||||
let ndSong;
|
||||
try {
|
||||
const salt = Math.random().toString(36).slice(2, 10);
|
||||
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
||||
const ndApi = `${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=novasonic&f=json`;
|
||||
const ndRes = await fetch(ndApi, { signal: AbortSignal.timeout(8000) });
|
||||
const ndData = await ndRes.json();
|
||||
const ndSong = ndData?.['subsonic-response']?.song;
|
||||
ndSong = ndData?.['subsonic-response']?.song;
|
||||
filePath = ndSong?.path;
|
||||
if (!filePath) return res.status(404).json({ error: 'Song not found in Navidrome' });
|
||||
} catch (e) {
|
||||
@@ -659,10 +671,10 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
ffArgs.push('-map', '0:v?'); // '?' = optional, no error if file has no cover
|
||||
}
|
||||
ffArgs.push('-c', 'copy', '-map_metadata', '-1', '-id3v2_version', '3');
|
||||
if (title) ffArgs.push('-metadata', `title=${title}`);
|
||||
if (artist) ffArgs.push('-metadata', `artist=${artist}`);
|
||||
const resolvedAlbumArtist = albumArtist || (artist ? artist.split(';')[0].trim() : null);
|
||||
if (resolvedAlbumArtist) ffArgs.push('-metadata', `album_artist=${resolvedAlbumArtist}`);
|
||||
if (title) ffArgs.push('-metadata', `title=${title}`);
|
||||
if (normalArtist) ffArgs.push('-metadata', `artist=${normalArtist}`);
|
||||
const resolvedAlbumArtist = normalAlbumArtist || (normalArtist ? normalArtist.split(';')[0].trim() : null);
|
||||
if (resolvedAlbumArtist) ffArgs.push('-metadata', `album_artist=${resolvedAlbumArtist}`);
|
||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
||||
if (year) ffArgs.push('-metadata', `date=${year}`);
|
||||
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
||||
@@ -691,7 +703,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
// 1. Rename file if title changed — convention: {01 - }?{Artist} - {Title}.ext
|
||||
if (title) {
|
||||
const safeTitle = sanitize(title);
|
||||
const fileArtist = (artist || ndSong?.artist || '').replace(/;/g, ',');
|
||||
const fileArtist = (normalArtist || ndSong?.artist || '').replace(/;/g, ',');
|
||||
const safeArtistPart = fileArtist ? sanitize(fileArtist) + ' - ' : '';
|
||||
const prefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : '';
|
||||
const newName = `${prefix}${safeArtistPart}${safeTitle}${fileExt}`;
|
||||
@@ -705,8 +717,8 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
// 2. Move to new artist/album folder when albumArtist or album changed
|
||||
const oldAlbumDir = path.dirname(currentFile);
|
||||
const oldArtistDir = path.dirname(oldAlbumDir);
|
||||
const newAlbumName = album ? sanitize(album) : path.basename(oldAlbumDir);
|
||||
const newArtistName = albumArtist ? sanitize(albumArtist.split(/[,;]/)[0].trim()) : path.basename(oldArtistDir);
|
||||
const newAlbumName = album ? sanitize(album) : path.basename(oldAlbumDir);
|
||||
const newArtistName = normalAlbumArtist ? sanitize(normalAlbumArtist.split(/[,;]/)[0].trim()) : path.basename(oldArtistDir);
|
||||
const newDir = path.join(CONFIG.musicPath, newArtistName, newAlbumName);
|
||||
|
||||
if (newDir !== oldAlbumDir) {
|
||||
|
||||
Reference in new issue
Block a user