diff --git a/src-tauri/src/download_cache/commands.rs b/src-tauri/src/download_cache/commands.rs index 6ea0e662a..89ae9afd5 100644 --- a/src-tauri/src/download_cache/commands.rs +++ b/src-tauri/src/download_cache/commands.rs @@ -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 { + 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, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c2231ec12..91ab32001 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src/lib/components/views/HomeView.svelte b/src/lib/components/views/HomeView.svelte index 5e9eb3e98..331210dc6 100644 --- a/src/lib/components/views/HomeView.svelte +++ b/src/lib/components/views/HomeView.svelte @@ -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; + 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>(new Set()); + // Download status tracking + let albumDownloadStatuses = $state>(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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4c6f13018..8674cc002 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -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('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(); + + async function checkAlbumFullyDownloaded(albumId: string): Promise { + // Trigger reactivity with downloadStateVersion + void downloadStateVersion; + + try { + const isDownloaded = await invoke('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'}