fix(locallibrary): show offline/folder album covers + add source badge

Two fixes from the first Albums-grid test pass.

Offline/folder covers (the reported "cover exists in the folder but isn't
shown"): some albums — offline-cached CMAF bundles and folder downloads —
have no artwork_path in the index even though a cover.jpg sits in the track
folder (older offline-cache downloads didn't backfill it). Add a frontend-
agnostic LibraryDatabase::resolve_album_cover_fallback(group_key) that finds
a representative track, derives its folder (the path itself for a CMAF bundle
dir, else the parent), and returns <folder>/cover.jpg when it exists. The
Slint Albums mapper calls it whenever the album has no artwork. Verified
against the live index: it resolves the folder covers that exist on disk
(FLAC-layout + CMAF-with-cover). Older CMAF bundles that never downloaded a
cover (segments only) still show the placeholder — those need a cover
re-fetch (offline-cache maintenance), tracked separately.

Source badge: the Local Library album card now shows the always-visible
source badge inside the artwork (bottom-right) — a hard-drive glyph for local
files, the Qobuz logo for offline copies, the Plex logo for Plex. Added
AlbumCard.show-source-badge (default off; threaded through AlbumCollectionView
/AlbumGrid) + AlbumCard.source; album_map now carries the real source and
map_local_album maps user->local / qobuz_download / plex. Small rounded
square, not a pill (ADR-008). Logos copied into the Slint icon assets.

Built green (cargo check -p qbz-slint); runtime smoke-test pending.

cc
This commit is contained in:
vicrodh committed 2026-05-28 23:40:58 -06:00
1 parent 835a6cad85
commit 3fc724af84
8 files changed
+834 -8

No files matched your search

+7
View File
@@ -26,6 +26,9 @@ pub struct AlbumCard {
pub artwork_url: String,
// List-row extras (AlbumListRow columns; ignored by the grid card).
pub release_type: String, // "Album" | "EP" | "Single" (TYPE column)
// "local" | "qobuz_download" | "plex" | "" — SOURCE column + the
// always-visible source badge on the Local Library grid card.
pub source: String,
pub quality_detail: String, // "24-bit / 96 kHz"
pub track_count: String, // "12"
pub plain_year: String, // "1973"
@@ -94,6 +97,9 @@ pub fn map_album(album: Album) -> AlbumCard {
quality_label,
artwork_url: album.image.best().cloned().unwrap_or_default(),
release_type,
// Qobuz album surfaces (Discover / Favorites / Label) hide the SOURCE
// column and the badge, so leave it empty (preserves prior behavior).
source: String::new(),
quality_detail,
track_count,
plain_year,
@@ -189,6 +195,7 @@ pub fn to_item(card: AlbumCard) -> AlbumCardItem {
// List-row extras — feed the AlbumListRow columns (TYPE / QUALITY /
// TRACKS / YEAR) for the list view toggle.
release_type: card.release_type.into(),
source: card.source.into(),
quality_detail: card.quality_detail.into(),
track_count: card.track_count.into(),
plain_year: card.plain_year.into(),
+32 -8
View File
@@ -109,6 +109,14 @@ pub fn map_local_album(a: qbz_library::LocalAlbum) -> crate::album_map::AlbumCar
} else {
String::new()
};
// Real source for the SOURCE column + the always-visible card badge:
// user files -> local, offline copies -> qobuz_download, Plex -> plex.
let source = match a.source.as_str() {
"plex" => "plex",
"qobuz_download" => "qobuz_download",
_ => "local",
}
.to_string();
crate::album_map::AlbumCard {
id: a.id,
title: a.title,
@@ -120,6 +128,7 @@ pub fn map_local_album(a: qbz_library::LocalAlbum) -> crate::album_map::AlbumCar
quality_label,
artwork_url: a.artwork_path.unwrap_or_default(),
release_type: crate::album_map::classify_release_type(Some(a.track_count)).to_string(),
source,
quality_detail,
track_count,
plain_year: year,
@@ -178,8 +187,8 @@ fn fetch_albums_page(
} else {
Some(trimmed)
};
let page = crate::library_db::with_db(|db| {
db.get_albums_metadata_page(
crate::library_db::with_db(|db| {
let page = db.get_albums_metadata_page(
offset,
ALBUMS_PAGE,
search_opt,
@@ -193,12 +202,27 @@ fn fetch_albums_page(
// filtering and Plex are their own later slices.
false,
None,
)
})?;
let total = page.total;
let has_more = offset + (page.albums.len() as u64) < total;
let cards = page.albums.into_iter().map(map_local_album).collect();
Some((cards, total, has_more))
)?;
let total = page.total;
let has_more = offset + (page.albums.len() as u64) < total;
let cards: Vec<crate::album_map::AlbumCard> = page
.albums
.into_iter()
.map(|a| {
let mut card = map_local_album(a);
// Offline-cached / folder albums sometimes have no backfilled
// artwork_path though a cover.jpg sits in the track folder —
// resolve it so the cover that exists on disk actually shows.
if card.artwork_url.is_empty() {
if let Some(cover) = db.resolve_album_cover_fallback(&card.id) {
card.artwork_url = cover;
}
}
card
})
.collect();
Ok((cards, total, has_more))
})
}
/// Build local-cover artwork jobs for a page of cards. The job url carries