From 2ca7b672e039434d3d632a4bab0f31a8c4e81eb6 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:23:45 -0600 Subject: [PATCH] =?UTF-8?q?fix(qt):=20album=20carousel=20cards=20=E2=80=94?= =?UTF-8?q?=20artist=20fallback=20and=20similar-album=20genre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/qbz-external-reco/src/types.rs | 5 ++++ crates/qbz-external-reco/src/validate.rs | 5 ++++ crates/qbz-qt/src/album_qt.rs | 33 +++++++++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/qbz-external-reco/src/types.rs b/crates/qbz-external-reco/src/types.rs index 3e28a2f0d..ac653fde2 100644 --- a/crates/qbz-external-reco/src/types.rs +++ b/crates/qbz-external-reco/src/types.rs @@ -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, } diff --git a/crates/qbz-external-reco/src/validate.rs b/crates/qbz-external-reco/src/validate.rs index e2bc59833..9ff834ddf 100644 --- a/crates/qbz-external-reco/src/validate.rs +++ b/crates/qbz-external-reco/src/validate.rs @@ -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, } } diff --git a/crates/qbz-qt/src/album_qt.rs b/crates/qbz-qt/src/album_qt.rs index 0d68b05f8..60f23d248 100644 --- a/crates/qbz-qt/src/album_qt.rs +++ b/crates/qbz-qt/src/album_qt.rs @@ -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(),