diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt index 2c8e78d..ef3695d 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt @@ -112,12 +112,15 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV }.getOrNull() val artists = searchResp?.results?.filter { it.wrapperType == "artist" } ?: emptyList() - // artists are sorted by fan count (ItunesClient), so the first name match is the most popular - // one — disambiguates same-name artists (5x "Joji"). + // artists are fan-sorted (ItunesClient), so firstOrNull gives the most popular match. + // Prefer an EXACT normalized-name match: "Flay!" must not resolve to the far more popular + // "K.FLAY" (its name merely contains "flay"). Only fall back to a loose contains-match. + fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^a-z0-9]"), "") + val q = norm(artistName) val iArtist = ( - artists.find { it.artistName?.equals(artistName, ignoreCase = true) == true } - ?: artists.find { - val n = it.artistName ?: return@find false + artists.firstOrNull { norm(it.artistName) == q } + ?: artists.firstOrNull { + val n = it.artistName ?: return@firstOrNull false n.contains(artistName, ignoreCase = true) || artistName.contains(n, ignoreCase = true) } ?: artists.firstOrNull() diff --git a/public/sw.js b/public/sw.js index 63c8dbd..547e102 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v59'; +const CACHE = 'ns-v60'; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. diff --git a/src/views/detail.js b/src/views/detail.js index 7a276ec..ebd388b 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -142,14 +142,20 @@ export async function loadArtistDetail(artist) { const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]); const _normWords = s => new Set((s || '').toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(Boolean)); + const _normName = s => (s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const _queryWords = _normWords(artist.name); + const _qNorm = _normName(artist.name); const iArtistResults = iArtistRes.status === 'fulfilled' ? (iArtistRes.value || []) : []; - // Name-matching candidates, already ordered by fanCount (itunesSearch sorts musicArtist by nb_fan). - const nameMatches = iArtistResults.filter(r => { + // Prefer EXACT normalized-name matches. "Flay!" must not resolve to the far more popular "K.FLAY" + // (whose name merely contains the word "flay") — exact "flay" == "flay" wins over "kflay". Only fall + // back to the looser word-subset match when there's no exact match. Both lists are fan-sorted already. + const exactMatches = iArtistResults.filter(r => _normName(r.artistName) === _qNorm); + const looseMatches = iArtistResults.filter(r => { if (_queryWords.size === 0) return true; const rw = _normWords(r.artistName); return [..._queryWords].every(w => rw.has(w)); }); + const nameMatches = exactMatches.length ? exactMatches : looseMatches; // Default pick = most popular name match (fixes "5× Joji": Deezer's order buries the real one). let iArtistMatch = nameMatches[0] || null;