diff --git a/crates/qbz-qt/src/album_qt.rs b/crates/qbz-qt/src/album_qt.rs index 679e6d01c..6afd3c243 100644 --- a/crates/qbz-qt/src/album_qt.rs +++ b/crates/qbz-qt/src/album_qt.rs @@ -41,6 +41,9 @@ const RELEASE_PAGE_SIZE: u32 = 20; #[derive(Clone, Default, Serialize)] pub struct TrackRow { + /// Featured performers already joined ("X, Y"); "" when none. + #[serde(rename = "featured")] + pub featured: String, pub id: String, pub number: String, pub title: String, @@ -509,6 +512,15 @@ fn build_credits(album: &Album) -> Vec<(String, String, String)> { } /// album.rs `map_track` (with work headers for classical albums). +/// "X, Y" for the row's `feat.` suffix (empty = nothing to add). Qobuz rows +/// only — local / media-server rows never carry `performers`. +pub(crate) fn featured_join(performers: Option<&str>, artist: &str, title: &str) -> String { + let Some(performers) = performers else { + return String::new(); + }; + qbz_qobuz::performers::featured_artists(performers, artist, title).join(", ") +} + fn map_track(track: &Track) -> TrackRow { let work = track .work @@ -536,6 +548,7 @@ fn map_track(track: &Track) -> TrackRow { .map(|p| (p.name.clone(), p.id.to_string())) .unwrap_or_default(); TrackRow { + featured: featured_join(track.performers.as_deref(), &artist, &track.title), is_favorite: crate::fav_cache_qt::contains_track(track.id), id: track.id.to_string(), number: track.track_number.to_string(), diff --git a/crates/qbz-qt/src/artist_qt.rs b/crates/qbz-qt/src/artist_qt.rs index 377cb2f08..728a2efd5 100644 --- a/crates/qbz-qt/src/artist_qt.rs +++ b/crates/qbz-qt/src/artist_qt.rs @@ -594,6 +594,7 @@ fn map_track(index: usize, track: PageArtistTrack) -> TrackRow { .and_then(|r| r.streamable) .unwrap_or(true); TrackRow { + featured: String::new(), is_favorite: crate::fav_cache_qt::contains_track(track.id), id: track.id.to_string(), cache_status: if crate::offline_qt::is_cached(&track.id.to_string()) { diff --git a/crates/qbz-qt/src/foryou_qt.rs b/crates/qbz-qt/src/foryou_qt.rs index 202ebb11d..29018a9f0 100644 --- a/crates/qbz-qt/src/foryou_qt.rs +++ b/crates/qbz-qt/src/foryou_qt.rs @@ -1048,6 +1048,7 @@ fn to_row(track: &Track) -> TrackRow { let bit_depth = track.maximum_bit_depth; let sample_rate = track.maximum_sampling_rate; TrackRow { + featured: String::new(), id: track.id.to_string(), title, artist: track diff --git a/crates/qbz-qt/src/label_qt.rs b/crates/qbz-qt/src/label_qt.rs index cf2d0f8cf..de050ec2d 100644 --- a/crates/qbz-qt/src/label_qt.rs +++ b/crates/qbz-qt/src/label_qt.rs @@ -1358,6 +1358,7 @@ fn parse_top_track(index: usize, raw: &Value) -> TrackRow { raw.get("release_date_stream").and_then(|v| v.as_str()), ); TrackRow { + featured: String::new(), is_favorite: id .parse::() .map(crate::fav_cache_qt::contains_track) diff --git a/crates/qbz-qt/src/local_playlist_qt.rs b/crates/qbz-qt/src/local_playlist_qt.rs index f97b20259..d77f4690b 100644 --- a/crates/qbz-qt/src/local_playlist_qt.rs +++ b/crates/qbz-qt/src/local_playlist_qt.rs @@ -806,6 +806,7 @@ pub(crate) fn row_to_display(item: &RowItem) -> (PlaylistTrackRow, Option { let art = artwork_path.clone().unwrap_or_default(); let row = PlaylistTrackRow { + featured: String::new(), id: track_id.to_string(), playlist_track_id: *track_id, title: title.clone(), @@ -880,6 +881,7 @@ pub(crate) fn row_to_display(item: &RowItem) -> (PlaylistTrackRow, Option (PlaylistTrackRow, Option (PlaylistTrackRow, Option ( PlaylistTrackRow { + featured: String::new(), id: format!("{kind}:{reference}"), title: qbz_i18n::t("Unavailable track"), source: (*kind).to_string(), diff --git a/crates/qbz-qt/src/playlist_qt.rs b/crates/qbz-qt/src/playlist_qt.rs index 65d68139a..0aa2d69b8 100644 --- a/crates/qbz-qt/src/playlist_qt.rs +++ b/crates/qbz-qt/src/playlist_qt.rs @@ -59,6 +59,9 @@ use serde::Serialize; #[derive(Clone, Default, Serialize)] pub struct PlaylistTrackRow { + /// Featured performers already joined ("X, Y"); "" when none. + #[serde(rename = "featured")] + pub featured: String, pub id: String, /// The playlist membership row id (== catalog track id for Qobuz /// playlists — what remove_tracks_from_playlist takes). @@ -862,6 +865,7 @@ pub(crate) fn map_track(track: &Track) -> PlaylistTrackRow { .map(|p| (p.name, p.id.to_string())) .unwrap_or_default(); PlaylistTrackRow { + featured: crate::album_qt::featured_join(track.performers.as_deref(), &artist, &track.title), // Heart state at build time, from the favourite-id cache — the same // O(1) read `album_qt` / `artist_qt` / `label_qt` rows use. It was // never stamped here, so `TrackRow.qml` saw `undefined` on every diff --git a/crates/qbz-qt/src/search_qt.rs b/crates/qbz-qt/src/search_qt.rs index e97b27738..c4312c31c 100644 --- a/crates/qbz-qt/src/search_qt.rs +++ b/crates/qbz-qt/src/search_qt.rs @@ -194,6 +194,9 @@ pub struct CardRow { #[derive(Clone, Default, Serialize)] pub struct TrackRow { + /// Featured performers already joined ("X, Y"); "" when none. + #[serde(rename = "featured")] + pub featured: String, pub id: String, pub title: String, pub artist: String, @@ -404,6 +407,7 @@ fn map_track(track: &Track) -> TrackRow { .map(|p| (p.name, p.id.to_string())) .unwrap_or_default(); TrackRow { + featured: crate::album_qt::featured_join(track.performers.as_deref(), &artist, &track.title), // Same story as `map_album`: the field was declared (line 186) and // never stamped, so every search track row drew the empty heart and // the first click sent `favorite/delete` on a track the user had