From 03046cd631276b230e8c2157e5dab44520074956 Mon Sep 17 00:00:00 2001 From: Honey Date: Thu, 9 Jul 2026 10:04:48 +0200 Subject: [PATCH] fix(android): show artist images in Apple-Music search (Erkunden) iTunes artist entities carry no artwork, so search results rendered as bare grey circles. Mirror the web PWA: after showing results, fetch one album per artist and borrow its cover (entity=album, limit=1), applied only if the user is still viewing the same artist set. Also give ArtistCard a Person-icon fallback instead of an empty grey dot, and fall back to the artist id for Subsonic (library) artist covers. Co-Authored-By: Claude Opus 4.8 --- .../novasonic/app/ui/screen/ExploreScreen.kt | 26 ++++++++++++---- .../app/viewmodel/ExploreViewModel.kt | 30 ++++++++++++++++++- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt index df32757..2c299c1 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt @@ -283,7 +283,9 @@ private fun LibrarySearchContent( horizontalArrangement = Arrangement.spacedBy(16.dp), ) { items(artists, key = { it.id }) { a -> - ArtistCard(name = a.name, imageUrl = coverUrl(a.coverArt)) { onArtistClick(a) } + // search3 artist results usually omit coverArt → fall back to the artist + // id, which Navidrome's getCoverArt accepts and resolves to the artist image. + ArtistCard(name = a.name, imageUrl = coverUrl(a.coverArt ?: a.id)) { onArtistClick(a) } } } Spacer(Modifier.height(8.dp)) @@ -597,15 +599,27 @@ private fun ArtistCard(name: String, imageUrl: String?, onClick: () -> Unit) { .width(80.dp) .clickable(onClick = onClick), ) { - AsyncImage( - model = imageUrl, - contentDescription = name, - contentScale = ContentScale.Crop, + Box( + contentAlignment = Alignment.Center, modifier = Modifier .size(72.dp) .clip(androidx.compose.foundation.shape.CircleShape) .background(NsSurface2), - ) + ) { + if (imageUrl != null) { + AsyncImage( + model = imageUrl, + contentDescription = name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } else { + Icon( + Icons.Default.Person, contentDescription = null, + tint = NsOnSurface2, modifier = Modifier.size(32.dp), + ) + } + } Spacer(Modifier.height(6.dp)) Text( name, fontFamily = Outfit, fontSize = 12.sp, fontWeight = FontWeight.Medium, diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt index efd051e..961308c 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt @@ -110,18 +110,46 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) { try { val resp = ItunesClient.search(term) val results = resp.results + val artists = results.filter { it.wrapperType == "artist" } _state.value = _state.value.copy( - artists = results.filter { it.wrapperType == "artist" }, + artists = artists, albums = results.filter { it.wrapperType == "collection" }, tracks = results.filter { it.wrapperType == "track" && it.kind == "song" }, isSearching = false, ) + enrichArtistArt(artists) } catch (e: Exception) { _state.value = _state.value.copy(isSearching = false, error = e.message) } } } + /** + * iTunes artist entities carry no artwork, so search results render as grey circles. Mirror the + * web PWA: fetch one album per artist and borrow its cover. Runs after the main results are shown + * (list appears instantly) and only applies if the user is still viewing the same artist set. + */ + private fun enrichArtistArt(artists: List) { + if (artists.none { it.artworkUrl100.isNullOrBlank() && it.artistName != null }) return + viewModelScope.launch { + val enriched = artists.map { a -> + async { + if (!a.artworkUrl100.isNullOrBlank()) return@async a + val name = a.artistName ?: return@async a + val art = runCatching { + ItunesClient.search(name, entity = "album", limit = 1).results + .firstOrNull { it.wrapperType == "collection" }?.artworkUrl100 + }.getOrNull() + if (art != null) a.copy(artworkUrl100 = art) else a + } + }.awaitAll() + val cur = _state.value.artists + if (cur.map { it.artistId } == artists.map { it.artistId }) { + _state.value = _state.value.copy(artists = enriched) + } + } + } + fun loadAlbum(album: ItunesItem) { val id = album.collectionId ?: return viewModelScope.launch {