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

+24 -5
View File
@@ -1,6 +1,7 @@
import { S } from '../state.js';
import { esc, toast, fmtCount } from '../utils.js';
import { api, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap } from '../api.js';
import { api, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap, getArtistOverride } from '../api.js';
import { openArtistPin } from '../ui/artistPin.js';
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
import { showCtxMenu, openDlModal } from '../ui/ui.js';
@@ -87,12 +88,30 @@ export async function loadArtistDetail(artist) {
if(albEl) albEl.innerHTML = '<div class="spinner"></div>';
const hasND = !!artist.id;
const knownDeezer = artist.deezerArtistId || null;
const knownFans = artist.deezerFanCount ?? null;
if (knownFans) _showFans(knownFans); // from the global-search card — show immediately
// Manual pin (same-name fix): use the pinned Deezer artist for id/fans/image instead of guessing.
const _pin = hasND ? getArtistOverride(artist.id) : null;
const knownDeezer = _pin?.deezerArtistId || artist.deezerArtistId || null;
const knownFans = _pin?.fanCount ?? artist.deezerFanCount ?? null;
if (knownFans) _showFans(knownFans); // from the global-search card / pin — show immediately
// "Falscher Künstler?" pin button (local artists only) — fix a wrong same-name Deezer match.
if (hasND) {
const infoEl = hero.querySelector('.artist-hero-info');
infoEl?.querySelector('.artist-pin-btn')?.remove();
if (infoEl) {
const pinBtn = document.createElement('button');
pinBtn.className = 'artist-pin-btn';
pinBtn.textContent = _pin ? '⚙ Künstler gepinnt' : '⚙ Falscher Künstler?';
pinBtn.title = 'Richtigen Deezer-Künstler wählen';
pinBtn.style.cssText = 'margin-top:8px;padding:5px 12px;border-radius:999px;border:1px solid var(--border2);background:rgba(0,0,0,.35);color:#fff;font-family:var(--font);font-size:12px;font-weight:600;cursor:pointer';
pinBtn.onclick = () => openArtistPin(artist.id, artist.name);
infoEl.appendChild(pinBtn);
}
}
// Fire all requests immediately so they run in parallel.
const imgP = hasND ? getArtistImage(artist.id) : Promise.resolve(null);
const imgP = _pin?.image ? Promise.resolve(_pin.image)
: hasND ? getArtistImage(artist.id) : Promise.resolve(null);
const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null);
const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null);
const topP = hasND ? api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null);