feat(artist): pin a local artist to a specific Deezer artist (same-name fix, shared)

Fixes same-name collisions where NovaSonic guessed the wrong Deezer artist (e.g.
'Nana' → a random DJ): fans, top songs, missing albums and the hero image were all
off. A '⚙ Falscher Künstler?' button on the artist page opens a picker (same-name
Deezer candidates with image + fan count); the chosen mapping is stored SERVER-SIDE
(shared across users, like the romaji overrides) in /data/deezer-artist-overrides.json.

Once pinned, the artist page short-circuits the name search to the pinned Deezer id
(drives albums/top-songs/fans) and overrides the hero image with the pinned artist's
Deezer picture (NovaSonic-side, so it works even for tag-merged same-name artists —
Navidrome can't disambiguate them without an MBID). New endpoints
GET/POST/DELETE /api/artist-overrides; api.js load/get/set/remove; loaded at startup.
SW cache -> ns-v74. Needs on-device runtime test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-12 21:38:42 +02:00
1 parent b788fedec3
commit 02960cf886
6 files changed
+204 -7

No files matched your search

+41
View File
@@ -233,6 +233,47 @@ app.delete('/api/romaji-overrides/:kanji', (req, res) => {
res.json({ ok: true });
});
// ─── ARTIST OVERRIDES (pin a local artist → a specific Deezer artist; shared) ──
// Fixes same-name collisions where NovaSonic would otherwise guess the wrong Deezer artist
// (fans / top songs / albums / image). Keyed by the Navidrome artist id.
const _artistOverridesFile = path.join(CONFIG.dataPath, 'deezer-artist-overrides.json');
function _loadArtistOverrides() {
try { return JSON.parse(fs.readFileSync(_artistOverridesFile, 'utf8')); } catch { return {}; }
}
function _saveArtistOverrides(map) {
try {
fs.mkdirSync(CONFIG.dataPath, { recursive: true });
fs.writeFileSync(_artistOverridesFile, JSON.stringify(map, null, 2), 'utf8');
} catch (e) { console.error('Failed to save artist overrides:', e.message); }
}
// GET — full map { ndArtistId: { deezerArtistId, name, image, fanCount } }
app.get('/api/artist-overrides', (_req, res) => res.json(_loadArtistOverrides()));
// POST — upsert a pin { ndArtistId, deezerArtistId, name, image, fanCount }
app.post('/api/artist-overrides', (req, res) => {
const { ndArtistId, deezerArtistId, name, image, fanCount } = req.body || {};
if (!ndArtistId || !deezerArtistId) return res.status(400).json({ error: 'ndArtistId and deezerArtistId required' });
const map = _loadArtistOverrides();
map[String(ndArtistId)] = {
deezerArtistId: String(deezerArtistId),
name: name || '',
image: image || '',
fanCount: (fanCount ?? null),
};
_saveArtistOverrides(map);
res.json({ ok: true });
});
// DELETE /api/artist-overrides/:ndArtistId — remove a pin
app.delete('/api/artist-overrides/:ndArtistId', (req, res) => {
const id = decodeURIComponent(req.params.ndArtistId);
const map = _loadArtistOverrides();
delete map[id];
_saveArtistOverrides(map);
res.json({ ok: true });
});
// POST /api/romaji-convert — convert an array of strings to romaji
app.post('/api/romaji-convert', (req, res) => {
const { texts } = req.body;