feat(downloads): add album download status to grid overlays (partial)

- Add backend command to check if album is fully downloaded
- Add frontend functions to check album download status
- Pass download handlers to HomeView
- Update HomeView AlbumCards with download status props

cp
This commit is contained in:
vicrodh committed 2026-01-15 00:23:58 -06:00
1 parent 932d13829e
commit 7952bbb7d6
4 files changed
+165 -8

No files matched your search

+29
View File
@@ -479,6 +479,35 @@ pub async fn open_album_folder(
Ok(())
}
/// Check if an album is fully downloaded (all tracks ready)
#[tauri::command]
pub async fn check_album_fully_downloaded(
album_id: String,
cache_state: State<'_, DownloadCacheState>,
) -> Result<bool, String> {
let db = cache_state.db.lock().await;
// Get all tracks for this album
let tracks = db.get_all_tracks()?;
let album_tracks: Vec<_> = tracks
.into_iter()
.filter(|t| t.album_id.as_deref() == Some(&album_id))
.collect();
if album_tracks.is_empty() {
return Ok(false);
}
// Check if all tracks are ready
for track in album_tracks {
if track.status != crate::download_cache::DownloadStatus::Ready {
return Ok(false);
}
}
Ok(true)
}
/// Evict tracks if cache exceeds limit (LRU policy)
async fn evict_if_needed(
cache_state: &DownloadCacheState,
+1
View File
@@ -443,6 +443,7 @@ pub fn run() {
download_cache::commands::set_download_cache_limit,
download_cache::commands::open_download_cache_folder,
download_cache::commands::open_album_folder,
download_cache::commands::check_album_fully_downloaded,
download_cache::commands::check_download_root_mounted,
download_cache::commands::validate_download_path,
download_cache::commands::move_downloads_to_path,
+55 -7
View File
@@ -54,6 +54,10 @@
onAlbumShareQobuz?: (albumId: string) => void;
onAlbumShareSonglink?: (albumId: string) => void;
onAlbumDownload?: (albumId: string) => void;
onOpenAlbumFolder?: (albumId: string) => void;
onReDownloadAlbum?: (albumId: string) => void;
checkAlbumFullyDownloaded?: (albumId: string) => Promise<boolean>;
downloadStateVersion?: number;
onArtistClick?: (artistId: number) => void;
onTrackPlay?: (track: DisplayTrack) => void;
}
@@ -67,6 +71,10 @@
onAlbumShareQobuz,
onAlbumShareSonglink,
onAlbumDownload,
onOpenAlbumFolder,
onReDownloadAlbum,
checkAlbumFullyDownloaded,
downloadStateVersion,
onArtistClick,
onTrackPlay
}: Props = $props();
@@ -132,6 +140,25 @@
let failedArtistImages = $state<Set<number>>(new Set());
// Download status tracking
let albumDownloadStatuses = $state<Map<string, boolean>>(new Map());
async function loadAlbumDownloadStatus(albumId: string) {
if (!checkAlbumFullyDownloaded) return false;
try {
const isDownloaded = await checkAlbumFullyDownloaded(albumId);
albumDownloadStatuses.set(albumId, isDownloaded);
return isDownloaded;
} catch {
return false;
}
}
function isAlbumDownloaded(albumId: string): boolean {
void downloadStateVersion;
return albumDownloadStatuses.get(albumId) || false;
}
const hasContent = $derived(
newReleases.length > 0
|| pressAwards.length > 0
@@ -510,7 +537,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
@@ -534,7 +564,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
@@ -558,7 +591,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
@@ -582,7 +618,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
@@ -606,7 +645,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
@@ -630,7 +672,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
@@ -711,7 +756,10 @@
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
onclick={() => onAlbumClick?.(album.id)}
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
/>
{/each}
<div class="spacer"></div>
+80 -1
View File
@@ -1012,6 +1012,48 @@
}
}
async function openAlbumFolderById(albumId: string) {
try {
await openAlbumFolder(albumId);
} catch (err) {
console.error('Failed to open album folder:', err);
showToast('Failed to open album folder', 'error');
}
}
async function reDownloadAlbumById(albumId: string) {
try {
const album = await invoke<QobuzAlbum>('get_album', { albumId });
if (!album || !album.tracks || album.tracks.data.length === 0) {
showToast('Failed to load album for re-download', 'error');
return;
}
showToast(`Re-downloading all tracks from "${album.title}"`, 'info');
for (const track of album.tracks.data) {
try {
await downloadTrack({
id: track.id,
title: track.title,
artist: track.performer?.name || album.artist?.name || 'Unknown',
album: album.title,
albumId: album.id,
durationSecs: track.duration,
quality: track.hires ? 'Hi-Res' : '-',
bitDepth: track.maximum_bit_depth,
sampleRate: track.maximum_sampling_rate,
});
} catch (err) {
console.error(`Failed to queue re-download for "${track.title}":`, err);
}
}
} catch (err) {
console.error('Failed to load album:', err);
showToast('Failed to load album for re-download', 'error');
}
}
function getTrackDownloadStatus(trackId: number) {
// Access downloadStateVersion to trigger reactivity
void downloadStateVersion;
@@ -1398,6 +1440,28 @@
// Download state update trigger
let downloadStateVersion = $state(0);
// Cache for album download statuses
const albumDownloadCache = new Map<string, boolean>();
async function checkAlbumFullyDownloaded(albumId: string): Promise<boolean> {
// Trigger reactivity with downloadStateVersion
void downloadStateVersion;
try {
const isDownloaded = await invoke<boolean>('check_album_fully_downloaded', { albumId });
albumDownloadCache.set(albumId, isDownloaded);
return isDownloaded;
} catch {
albumDownloadCache.set(albumId, false);
return false;
}
}
function getAlbumDownloadStatus(albumId: string): boolean {
void downloadStateVersion;
return albumDownloadCache.get(albumId) || false;
}
onMount(() => {
// Bootstrap app (theme, mouse nav, Last.fm restore)
const { cleanup: cleanupBootstrap } = bootstrapApp();
@@ -1719,6 +1783,10 @@
onAlbumShareQobuz={shareAlbumQobuzLinkById}
onAlbumShareSonglink={shareAlbumSonglinkById}
onAlbumDownload={downloadAlbumById}
onOpenAlbumFolder={openAlbumFolderById}
onReDownloadAlbum={reDownloadAlbumById}
checkAlbumFullyDownloaded={checkAlbumFullyDownloaded}
{downloadStateVersion}
onArtistClick={handleArtistClick}
onTrackPlay={handleDisplayTrackPlay}
/>
@@ -1731,6 +1799,10 @@
onAlbumShareQobuz={shareAlbumQobuzLinkById}
onAlbumShareSonglink={shareAlbumSonglinkById}
onAlbumDownload={downloadAlbumById}
onOpenAlbumFolder={openAlbumFolderById}
onReDownloadAlbum={reDownloadAlbumById}
checkAlbumFullyDownloaded={checkAlbumFullyDownloaded}
{downloadStateVersion}
onTrackPlay={handleTrackPlay}
onTrackPlayNext={queueQobuzTrackNext}
onTrackPlayLater={queueQobuzTrackLater}
@@ -1790,6 +1862,10 @@
onAlbumShareQobuz={shareAlbumQobuzLinkById}
onAlbumShareSonglink={shareAlbumSonglinkById}
onAlbumDownload={downloadAlbumById}
onOpenAlbumFolder={openAlbumFolderById}
onReDownloadAlbum={reDownloadAlbumById}
checkAlbumFullyDownloaded={checkAlbumFullyDownloaded}
{downloadStateVersion}
onLoadMore={loadMoreArtistAlbums}
isLoadingMore={isArtistAlbumsLoading}
onTrackPlay={handleDisplayTrackPlay}
@@ -1845,6 +1921,10 @@
onAlbumShareQobuz={shareAlbumQobuzLinkById}
onAlbumShareSonglink={shareAlbumSonglinkById}
onAlbumDownload={downloadAlbumById}
onOpenAlbumFolder={openAlbumFolderById}
onReDownloadAlbum={reDownloadAlbumById}
checkAlbumFullyDownloaded={checkAlbumFullyDownloaded}
{downloadStateVersion}
onTrackPlay={handleDisplayTrackPlay}
onArtistClick={handleArtistClick}
onTrackPlayNext={queuePlaylistTrackNext}
@@ -1858,7 +1938,6 @@
onTrackDownload={handleDisplayTrackDownload}
onTrackRemoveDownload={handleTrackRemoveDownload}
getTrackDownloadStatus={getTrackDownloadStatus}
{downloadStateVersion}
/>
{:else if activeView === 'playlist-manager'}
<PlaylistManagerView