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:
1 parent
b788fedec3
commit
02960cf886
6 files changed
+204
-7
No files matched your search
@@ -0,0 +1,94 @@
|
||||
// ─── Artist pin picker ───────────────────────────────────────────────────────
|
||||
// Opened from the artist page ("⚙ Falscher Künstler?"). Lets the user pin a local
|
||||
// (Navidrome) artist to a specific Deezer artist, fixing same-name collisions where
|
||||
// NovaSonic would otherwise guess the wrong one (fans / top songs / albums / image).
|
||||
// The mapping is stored server-side (shared across users) — see api.js.
|
||||
|
||||
import { deezerSearch, setArtistOverride, removeArtistOverride, getArtistOverride } from '../api.js';
|
||||
import { esc, fmtCount } from '../utils.js';
|
||||
|
||||
let _overlay = null;
|
||||
let _ctx = null; // { ndArtistId, ndName }
|
||||
|
||||
export function openArtistPin(ndArtistId, ndName) {
|
||||
_ctx = { ndArtistId: String(ndArtistId), ndName: ndName || '' };
|
||||
if (!_overlay) _build();
|
||||
_overlay.classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
_render();
|
||||
}
|
||||
|
||||
export function closeArtistPin() {
|
||||
if (_overlay) _overlay.classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function _build() {
|
||||
_overlay = document.createElement('div');
|
||||
_overlay.className = 'modal-overlay';
|
||||
_overlay.id = 'artist-pin-overlay';
|
||||
_overlay.addEventListener('click', e => { if (e.target === _overlay) closeArtistPin(); });
|
||||
_overlay.innerHTML = `
|
||||
<div class="modal-card" style="max-width:520px;width:calc(100vw - 32px);max-height:86vh;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--border2);border-radius:18px;box-shadow:0 40px 80px #0009;overflow:hidden">
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid var(--border)">
|
||||
<span style="font-size:18px">⚙</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:16px;font-weight:800">Richtigen Künstler wählen</div>
|
||||
<div id="artist-pin-sub" style="font-size:12px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>
|
||||
</div>
|
||||
<button onclick="closeArtistPin()" style="background:none;border:none;color:var(--text2);font-size:18px;cursor:pointer;padding:6px 10px">✕</button>
|
||||
</div>
|
||||
<div id="artist-pin-body" style="padding:12px 14px;overflow-y:auto"></div>
|
||||
</div>`;
|
||||
document.body.appendChild(_overlay);
|
||||
}
|
||||
|
||||
async function _render() {
|
||||
_overlay.querySelector('#artist-pin-sub').textContent = `Für „${_ctx.ndName}"`;
|
||||
const body = _overlay.querySelector('#artist-pin-body');
|
||||
const cur = getArtistOverride(_ctx.ndArtistId);
|
||||
const clearBtn = cur
|
||||
? `<button onclick="clearArtistPin()" style="width:100%;padding:10px;margin-bottom:10px;border-radius:9px;border:1px solid var(--border);background:transparent;color:var(--danger);font-family:var(--font);font-weight:600;cursor:pointer">Pin entfernen (aktuell: ${esc(cur.name || 'gepinnt')})</button>`
|
||||
: '';
|
||||
body.innerHTML = clearBtn + `<div style="text-align:center;color:var(--text2);padding:24px 0">Suche…</div>`;
|
||||
|
||||
let results = [];
|
||||
try { results = (await deezerSearch(_ctx.ndName, 'musicArtist', 8)) || []; } catch {}
|
||||
|
||||
const rows = results.map(r => `
|
||||
<div class="artist-pin-cand" data-id="${esc(r.artistId)}" data-name="${esc(r.artistName || '')}" data-img="${esc(r.artworkUrl100 || '')}" data-fans="${r.fanCount ?? ''}"
|
||||
style="display:flex;align-items:center;gap:12px;padding:8px;border-radius:10px;cursor:pointer" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background='none'">
|
||||
<div style="width:48px;height:48px;border-radius:50%;background:var(--surface2);overflow:hidden;flex-shrink:0">
|
||||
${r.artworkUrl100 ? `<img src="${esc(r.artworkUrl100)}" loading="lazy" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">` : ''}
|
||||
</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(r.artistName || '—')}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">${r.fanCount != null ? fmtCount(r.fanCount) + ' Fans' : 'Deezer'}</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
|
||||
body.innerHTML = clearBtn + (rows || `<div style="text-align:center;color:var(--text2);padding:24px 0">Keine Deezer-Künstler gefunden.</div>`);
|
||||
body.querySelectorAll('.artist-pin-cand').forEach(el => el.addEventListener('click', () => _pick(el.dataset)));
|
||||
}
|
||||
|
||||
async function _pick(ds) {
|
||||
await setArtistOverride(_ctx.ndArtistId, {
|
||||
deezerArtistId: ds.id,
|
||||
name: ds.name,
|
||||
image: ds.img,
|
||||
fanCount: ds.fans !== '' ? Number(ds.fans) : null,
|
||||
});
|
||||
closeArtistPin();
|
||||
_reloadArtist();
|
||||
}
|
||||
|
||||
export async function clearArtistPin() {
|
||||
if (_ctx) await removeArtistOverride(_ctx.ndArtistId);
|
||||
closeArtistPin();
|
||||
_reloadArtist();
|
||||
}
|
||||
|
||||
async function _reloadArtist() {
|
||||
const { navigate } = await import('../views/views.js');
|
||||
navigate('artist-detail', { id: _ctx.ndArtistId, name: _ctx.ndName });
|
||||
}
|
||||
Reference in new issue
Block a user