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

+40
View File
@@ -194,6 +194,46 @@ export async function removeRomajiOverride(kanji) {
} catch (e) { console.warn('removeRomajiOverride failed:', e); }
}
// ─── ARTIST OVERRIDES (pin a local artist → a specific Deezer artist; shared) ───
// Fixes same-name collisions on the artist page (fans / top songs / albums / image).
let _artistOverridesCache = null;
export function getArtistOverrides() { return _artistOverridesCache || {}; }
export function getArtistOverride(ndArtistId) { return (_artistOverridesCache || {})[String(ndArtistId)] || null; }
export async function loadArtistOverrides() {
if (!S.cfg?.backendUrl) { _artistOverridesCache = {}; return; }
try {
const r = await fetch(`${S.cfg.backendUrl}/api/artist-overrides`);
_artistOverridesCache = r.ok ? await r.json() : {};
} catch { _artistOverridesCache = {}; }
}
export async function setArtistOverride(ndArtistId, deezer) {
if (!S.cfg?.backendUrl || !ndArtistId || !deezer?.deezerArtistId) return;
const entry = {
deezerArtistId: String(deezer.deezerArtistId),
name: deezer.name || '',
image: deezer.image || '',
fanCount: deezer.fanCount ?? null,
};
try {
await fetch(`${S.cfg.backendUrl}/api/artist-overrides`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ndArtistId: String(ndArtistId), ...entry }),
});
if (!_artistOverridesCache) _artistOverridesCache = {};
_artistOverridesCache[String(ndArtistId)] = entry;
} catch (e) { console.warn('setArtistOverride failed:', e); }
}
export async function removeArtistOverride(ndArtistId) {
if (!S.cfg?.backendUrl || !ndArtistId) return;
try {
await fetch(`${S.cfg.backendUrl}/api/artist-overrides/${encodeURIComponent(ndArtistId)}`, { method: 'DELETE' });
if (_artistOverridesCache) delete _artistOverridesCache[String(ndArtistId)];
} catch (e) { console.warn('removeArtistOverride failed:', e); }
}
export function dispArtistById(id, name) {
if (!S.cfg.showRomaji) return name || '';
const ov = getRomajiOverrides();