feat: disambiguate same-name artists by Deezer fan count + show fans

Deezer's artist search doesn't rank by popularity, so a name collision (5 artists named
"Joji") returned a niche one first (62 fans) instead of the real Joji (559k) — the artist
page then loaded the wrong discography and owned songs didn't show as downloaded.

- api.js: _dzArtist carries fanCount (nb_fan); artist search sorts by fanCount desc.
- search.js: artist cards show the fan count as subtitle; fan count passed to the artist page.
- detail.js: pick the most-popular name match (not the first); for local Navidrome artists
  with a real collision (>=2 same-name candidates), tie-break by SONG OVERLAP — prefer the
  Deezer candidate whose top tracks actually overlap the library (each artist has different
  songs, so this nails the right person). Costs a few calls only on a collision.
- Artist hero shows "<n> Fans" (immediately for global-search artists, after match for local).
- Android: DzArtist.nb_fan + /search/artist sorted by nb_fan so the right artist is first.
- Also: web global-search "unavailable" message now says Deezer, not Apple. SW ns-v52.

Verified live against Deezer: both fan-sort and overlap select the real Joji (id 5078097).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-31 16:11:45 +02:00
1 parent 1e69831dd8
commit 065d2f7319
9 files changed
+69 -19

No files matched your search

+12 -13
View File
@@ -1,5 +1,5 @@
import { S } from '../state.js';
import { esc, toast } from '../utils.js';
import { esc, toast, fmtCount } from '../utils.js';
import { api, itunesSearch, itunesLastError, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js';
import { playPreview } from '../player/player.js';
@@ -130,8 +130,9 @@ export async function doGlobalSearch(q) {
const s = mkSection('Künstler');
const row = document.createElement('div'); row.className = 'scroll-row';
artists.forEach(a => {
const c = makeCard(a.artistName, 'Deezer Künstler', a.artworkUrl100 || null, '🎤');
c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName);
const sub = a.fanCount ? `${fmtCount(a.fanCount)} Fans` : 'Deezer Künstler';
const c = makeCard(a.artistName, sub, a.artworkUrl100 || null, '🎤');
c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName, a.fanCount);
row.appendChild(c);
if (a.artworkUrl100) return; // Deezer artists already carry a photo — no extra lookup
itunesSearch(a.artistName,'album',1).then(res => {
@@ -178,14 +179,12 @@ export async function doGlobalSearch(q) {
if(!artists.length && !albums.length && !trackResults.length) {
const err = itunesLastError();
if (err) {
// Distinguish "genuinely no results" from "Apple search is down" so the user knows where the
// problem is. Apple currently blocks the Search API (/search) with 403 from many server IPs.
const detail = err === 403
? 'Apple blockt gerade die Such-API (Fehler 403). Das liegt an Apple, nicht an dir — Downloads per Direktlink funktionieren weiter.'
: err === 'network'
? 'Keine Verbindung zur Apple-Such-API (Netzwerk/Backend). Läuft dein Backend?'
: `Apple-Suche nicht verfügbar (Fehler ${err}).`;
el.innerHTML = `<div class="empty-state"><div class="icon">⚠️</div><p>Apple-Suche gerade nicht verfügbar</p><p style="font-size:12px;color:var(--text3);max-width:420px;margin:8px auto 0">${detail}</p></div>`;
// Distinguish "genuinely no results" from "search backend is down" so the user knows where
// the problem is.
const detail = err === 'network'
? 'Keine Verbindung zur Deezer-Such-API (Netzwerk/Backend). Läuft dein Backend?'
: `Deezer-Suche nicht verfügbar (Fehler ${err}).`;
el.innerHTML = `<div class="empty-state"><div class="icon">⚠️</div><p>Deezer-Suche gerade nicht verfügbar</p><p style="font-size:12px;color:var(--text3);max-width:420px;margin:8px auto 0">${detail}</p></div>`;
} else {
el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>Keine Ergebnisse gefunden</p></div>';
}
@@ -215,8 +214,8 @@ export async function loadItunesAlbum(album) {
} catch(e) { toast('Fehler: ' + e.message); }
}
export function loadItunesArtistResults(artistId, artistName) {
navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId });
export function loadItunesArtistResults(artistId, artistName, fanCount) {
navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId, itunesFanCount: fanCount });
}
// ─── SEARCH HELPERS ───