fix(discover): preserve sources on existing Most Played album cards

Use one mapper for the carousel and full history page. Carry the stored routing source and normalized badge vocabulary through both surfaces, including existing rows, downloads and media servers.

Cover existing snapshots and catalog aliases in the Qt suite; all 741 tests pass with temporary XDG data.

cx
This commit is contained in:
vicrodh committed 2026-09-06 11:43:49 -06:00
1 parent 8f959fc757
commit 97545feae7
2 files changed
+64 -32

No files matched your search

+1 -19
View File
@@ -777,25 +777,7 @@ fn load_history(generation: u64) {
|| r.title.to_lowercase().contains(&needle) || r.title.to_lowercase().contains(&needle)
|| r.artist.to_lowercase().contains(&needle) || r.artist.to_lowercase().contains(&needle)
}) })
.map(|r| HomeCard { .map(crate::home_qt::map_played_album)
is_pinned: crate::sidebar_qt::is_pinned("album", &r.album_id),
// Heart, same seed as every other album card — this
// page's rows are built here rather than through
// `home_qt::map_*`, so it needs its own stamp.
is_favorite: crate::fav_cache_qt::is_album_favorite(&r.album_id),
id: r.album_id,
title: r.title,
artist: r.artist,
artist_id: r.artist_id,
year: r.year,
quality_tier: r.quality_tier,
quality_label: r.quality_label,
art_url: r.artwork_url,
// The 20px the 286px card is taller than every other
// album card (AlbumCard.slint:508).
plays: r.plays,
..HomeCard::default()
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
} else { } else {
crate::recently_qt::load_albums() crate::recently_qt::load_albums()
+63 -13
View File
@@ -878,19 +878,7 @@ fn build_candidates(
// (`album_play_history`, the shared per-app SQLite store). No network. // (`album_play_history`, the shared per-app SQLite store). No network.
let most_played: Vec<HomeCard> = qbz_app::settings::album_play_history::top_albums(20) let most_played: Vec<HomeCard> = qbz_app::settings::album_play_history::top_albums(20)
.into_iter() .into_iter()
.map(|r| HomeCard { .map(map_played_album)
is_pinned: crate::sidebar_qt::is_pinned("album", &r.album_id),
is_favorite: crate::fav_cache_qt::is_album_favorite(&r.album_id),
id: r.album_id,
title: r.title,
artist: r.artist,
artist_id: r.artist_id,
year: r.year,
quality_tier: r.quality_tier,
quality_label: r.quality_label,
art_url: r.artwork_url,
..HomeCard::default()
})
.collect(); .collect();
if !most_played.is_empty() { if !most_played.is_empty() {
out.push(HomeSection { out.push(HomeSection {
@@ -2080,6 +2068,41 @@ pub(crate) fn map_flat_album(album: Album) -> HomeCard {
} }
} }
/// History stores keep their raw routing source. Badge data uses the shared
/// display vocabulary, including old rows, without requiring another play.
fn history_source_badges(source: &str) -> Vec<String> {
if source.is_empty() {
return Vec::new(); // Legacy history means catalog, never inferred local.
}
vec![if source == "qobuz_purchase" {
source.to_string()
} else {
qbz_source::SourceBadge::from_word(Some(source))
.as_str()
.into()
}]
}
/// Shared by the Most Played carousel and its complete, filtered page.
pub(crate) fn map_played_album(r: qbz_app::settings::album_play_history::AlbumPlayRow) -> HomeCard {
HomeCard {
is_pinned: crate::sidebar_qt::is_pinned("album", &r.album_id),
is_favorite: crate::fav_cache_qt::is_album_favorite(&r.album_id),
sources: history_source_badges(&r.source),
source: r.source,
id: r.album_id,
title: r.title,
artist: r.artist,
artist_id: r.artist_id,
year: r.year,
quality_tier: r.quality_tier,
quality_label: r.quality_label,
art_url: r.artwork_url,
plays: r.plays,
..HomeCard::default()
}
}
/// Map one recently-played album (local history) onto a card. The stored ISO /// Map one recently-played album (local history) onto a card. The stored ISO
/// release date is localized here exactly as the discover cards do. /// release date is localized here exactly as the discover cards do.
pub(crate) fn map_recent_album(a: crate::recently_qt::RecentAlbum) -> HomeCard { pub(crate) fn map_recent_album(a: crate::recently_qt::RecentAlbum) -> HomeCard {
@@ -2099,6 +2122,7 @@ pub(crate) fn map_recent_album(a: crate::recently_qt::RecentAlbum) -> HomeCard {
}, },
quality_tier: a.quality_tier, quality_tier: a.quality_tier,
quality_label: a.quality_label, quality_label: a.quality_label,
sources: history_source_badges(&a.source),
source: a.source, source: a.source,
art_url: a.artwork_url, art_url: a.artwork_url,
..HomeCard::default() ..HomeCard::default()
@@ -2384,6 +2408,32 @@ mod tests {
} }
} }
#[test]
fn old_most_played_rows_keep_their_source_on_both_card_surfaces() {
for (source, badge) in [
("local", "local"),
("plex", "plex"),
("jellyfin", "jellyfin"),
("navidrome", "subsonic"),
("qobuz_download", "offline"),
("qobuz_purchase", "qobuz_purchase"),
("qobuz", "qobuz"),
("qobuz_connect_remote", "qobuz"),
] {
let card = map_played_album(qbz_app::settings::album_play_history::AlbumPlayRow {
album_id: "stored-before-source-badge-fix".into(),
source: source.into(),
plays: 23,
..Default::default()
});
let wire = serde_json::to_value(card).unwrap();
assert_eq!(wire["source"], source);
assert_eq!(wire["sources"], serde_json::json!([badge]));
assert_eq!(wire["plays"], 23);
}
assert!(history_source_badges("").is_empty());
}
#[test] #[test]
fn quality_tier_mapping() { fn quality_tier_mapping() {
assert_eq!(quality_tier(None), ""); assert_eq!(quality_tier(None), "");