fix(qt): album carousel cards — artist fallback and similar-album genre

Two card-data gaps on the album page's bottom rails:

- "Listening suggestions" (/album/suggest) rendered no artist line:
  the Qt mapping read only the flat `artist` field, while Slint's
  album_map::album_artist falls back to the artists[] array (main-artist
  role, else first entry) — the shape /album/suggest actually returns for
  some albums. The fallback is ported as card_artist.
- "Similar albums" (Last.fm row) showed no genre in the hover overlay:
  AlbumReco carried no genre at all. build_album_reco has the resolving
  Qobuz Album in hand, so the field now exists (serde-defaulted, so
  cached reco JSON keeps working) and is mapped through. Slint's
  external_reco::album_card can map it too after the next merge — its
  checkout is a different branch and is untouched here.

km
This commit is contained in:
vicrodh committed 2026-08-10 20:23:45 -06:00
1 parent 98dfcd8173
commit 2ca7b672e0
3 files changed
+40 -3

No files matched your search

+5
View File
@@ -76,6 +76,11 @@ pub struct AlbumReco {
pub artwork_url: String,
#[serde(default)]
pub subtitle: String,
/// Genre name from the resolving Qobuz album — feeds the card hover
/// overlay. Defaulted for backward compatibility with cached JSON
/// written before the field existed.
#[serde(default)]
pub genre: String,
#[serde(default = "default_source")]
pub source: RecoSource,
}
+5
View File
@@ -284,6 +284,11 @@ pub fn build_album_reco(album: &Album, subtitle: String, source: RecoSource) ->
quality_label,
artwork_url: album.image.best().cloned().unwrap_or_default(),
subtitle,
genre: album
.genre
.as_ref()
.map(|g| g.name.clone())
.unwrap_or_default(),
source,
}
}
+30 -3
View File
@@ -635,6 +635,32 @@ pub(crate) fn format_album_title(title: &str, version: Option<&str>) -> String {
}
}
/// album_map.rs `album_artist` (Slint `qbz/src/album_map.rs:168-187`):
/// `artist.name` first, then the `artists[]` array (the entry with the
/// `main-artist` role, else the first entry). `/album/suggest` returns items
/// whose flat `artist` is empty but whose `artists[]` is populated — without
/// the fallback the "Listening suggestions" cards render no artist line.
fn card_artist(album: &Album) -> (String, String) {
if !album.artist.name.is_empty() {
return (album.artist.name.clone(), album.artist.id.to_string());
}
if let Some(list) = album.artists.as_ref() {
let pick = list
.iter()
.find(|a| {
a.roles
.as_ref()
.map(|r| r.iter().any(|role| role == "main-artist"))
.unwrap_or(false)
})
.or_else(|| list.first());
if let Some(a) = pick {
return (a.name.clone(), a.id.to_string());
}
}
(String::new(), String::new())
}
/// artist.rs `map_release` for carousel cards (deduped vs the open album).
fn map_release_card(release: &qbz_models::PageArtistRelease) -> AlbumCardData {
let artist = release
@@ -727,7 +753,7 @@ fn reco_to_card(r: qbz_external_reco::AlbumReco) -> AlbumCardData {
title: r.title,
artist: r.artist,
artist_id: r.artist_id,
genre: String::new(),
genre: r.genre,
year: r.year,
quality_tier: r.quality_tier,
quality_detail: r.quality_label,
@@ -767,13 +793,14 @@ pub async fn load_suggestions(
.as_ref()
.and_then(|d| d.original.clone().or(d.download.clone()).or(d.stream.clone()))
.or(a.release_date_original.clone());
let (artist, card_artist_id) = card_artist(a);
AlbumCardData {
is_pinned: crate::sidebar_qt::is_pinned("album", &a.id),
is_favorite: crate::fav_cache_qt::is_album_favorite(&a.id),
id: a.id.clone(),
title: a.title.clone(),
artist: a.artist.name.clone(),
artist_id: a.artist.id.to_string(),
artist,
artist_id: card_artist_id,
genre: a.genre.as_ref().map(|g| g.name.clone()).unwrap_or_default(),
year: qbz_text_utils::dates::release_label(date.as_deref()),
quality_tier: home_qt::quality_tier_from_depth(bit_depth).to_string(),