refactor(stores): rename store to offlineCacheState

- Create offlineCacheState.ts with compliant terminology
- Update all component imports and type references
- Add compatibility aliases for gradual migration
- Remove deprecated downloadState.ts

cc
This commit is contained in:
vicrodh committed 2026-01-23 11:57:53 -06:00
1 parent 393589862d
commit 84a2164079
10 files changed
+358 -303

No files matched your search

+3 -2
View File
@@ -6,10 +6,11 @@
isOffline as checkIsOffline isOffline as checkIsOffline
} from '$lib/stores/offlineStore'; } from '$lib/stores/offlineStore';
type DownloadStatus = 'none' | 'queued' | 'downloading' | 'ready' | 'failed'; // Offline cache status for tracks
type OfflineCacheStatus = 'none' | 'queued' | 'downloading' | 'ready' | 'failed';
interface Props { interface Props {
status?: DownloadStatus; status?: OfflineCacheStatus;
progress?: number; progress?: number;
size?: number; size?: number;
onDownload?: () => void; onDownload?: () => void;
+3 -2
View File
@@ -10,7 +10,8 @@
} from '$lib/stores/favoritesStore'; } from '$lib/stores/favoritesStore';
import { togglePlay } from '$lib/stores/playerStore'; import { togglePlay } from '$lib/stores/playerStore';
type DownloadStatus = 'none' | 'queued' | 'downloading' | 'ready' | 'failed'; // Offline cache status for tracks
type OfflineCacheStatus = 'none' | 'queued' | 'downloading' | 'ready' | 'failed';
interface Props { interface Props {
trackId?: number; // Optional - required for favorites functionality unless hideFavorite=true trackId?: number; // Optional - required for favorites functionality unless hideFavorite=true
@@ -23,7 +24,7 @@
isPlaying?: boolean; isPlaying?: boolean;
isLocal?: boolean; // Whether this is a local library track isLocal?: boolean; // Whether this is a local library track
isFavoriteOverride?: boolean; // Optional override for favorite state isFavoriteOverride?: boolean; // Optional override for favorite state
downloadStatus?: DownloadStatus; downloadStatus?: OfflineCacheStatus;
downloadProgress?: number; downloadProgress?: number;
hideDownload?: boolean; hideDownload?: boolean;
hideFavorite?: boolean; hideFavorite?: boolean;
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import TrackRow from './TrackRow.svelte'; import TrackRow from './TrackRow.svelte';
import type { DownloadStatus } from '$lib/stores/downloadState'; import type { OfflineCacheStatus } from '$lib/stores/offlineCacheState';
// Use generic types to match whatever caller passes // Use generic types to match whatever caller passes
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -44,7 +44,7 @@
hideFavorite?: boolean; hideFavorite?: boolean;
isFavoriteOverride?: boolean; isFavoriteOverride?: boolean;
// Download status support for Favorites/Search // Download status support for Favorites/Search
getDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number }; getOfflineCacheStatus?: (trackId: number) => { status: OfflineCacheStatus; progress: number };
onDownload?: (track: Track) => void; onDownload?: (track: Track) => void;
onRemoveDownload?: (trackId: number) => void; onRemoveDownload?: (trackId: number) => void;
// Additional menu actions for Favorites // Additional menu actions for Favorites
@@ -85,7 +85,7 @@
hideDownload = true, hideDownload = true,
hideFavorite = true, hideFavorite = true,
isFavoriteOverride, isFavoriteOverride,
getDownloadStatus, getOfflineCacheStatus,
onDownload, onDownload,
onRemoveDownload, onRemoveDownload,
onShareQobuz, onShareQobuz,
@@ -305,7 +305,7 @@
{@const albumKey = getTrackAlbumKey(item.track)} {@const albumKey = getTrackAlbumKey(item.track)}
{@const artistId = getArtistId?.(item.track)} {@const artistId = getArtistId?.(item.track)}
{@const albumId = getAlbumId?.(item.track)} {@const albumId = getAlbumId?.(item.track)}
{@const downloadInfo = getDownloadStatus?.(trackId) ?? { status: 'none' as const, progress: 0 }} {@const downloadInfo = getOfflineCacheStatus?.(trackId) ?? { status: 'none' as const, progress: 0 }}
{@const isTrackDownloaded = downloadInfo.status === 'ready'} {@const isTrackDownloaded = downloadInfo.status === 'ready'}
<TrackRow <TrackRow
trackId={trackId} trackId={trackId}
@@ -3,7 +3,7 @@
import { ArrowLeft, Play, Shuffle, Heart } from 'lucide-svelte'; import { ArrowLeft, Play, Shuffle, Heart } from 'lucide-svelte';
import TrackRow from '../TrackRow.svelte'; import TrackRow from '../TrackRow.svelte';
import AlbumMenu from '../AlbumMenu.svelte'; import AlbumMenu from '../AlbumMenu.svelte';
import { getDownloadState, type DownloadStatus, isAlbumFullyDownloaded } from '$lib/stores/downloadState'; import { getOfflineCacheState, type OfflineCacheStatus, isAlbumFullyCached } from '$lib/stores/offlineCacheState';
import { consumeContextTrackFocus } from '$lib/stores/playbackContextStore'; import { consumeContextTrackFocus } from '$lib/stores/playbackContextStore';
import { import {
subscribe as subscribeAlbumFavorites, subscribe as subscribeAlbumFavorites,
@@ -60,7 +60,7 @@
onTrackDownload?: (track: Track) => void; onTrackDownload?: (track: Track) => void;
onTrackRemoveDownload?: (trackId: number) => void; onTrackRemoveDownload?: (trackId: number) => void;
onTrackReDownload?: (track: Track) => void; onTrackReDownload?: (track: Track) => void;
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number }; getTrackOfflineCacheStatus?: (trackId: number) => { status: OfflineCacheStatus; progress: number };
onDownloadAlbum?: () => void; onDownloadAlbum?: () => void;
onShareAlbumQobuz?: () => void; onShareAlbumQobuz?: () => void;
onShareAlbumSonglink?: () => void; onShareAlbumSonglink?: () => void;
@@ -92,7 +92,7 @@
onTrackDownload, onTrackDownload,
onTrackRemoveDownload, onTrackRemoveDownload,
onTrackReDownload, onTrackReDownload,
getTrackDownloadStatus, getTrackOfflineCacheStatus,
onDownloadAlbum, onDownloadAlbum,
onShareAlbumQobuz, onShareAlbumQobuz,
onShareAlbumSonglink, onShareAlbumSonglink,
@@ -109,7 +109,7 @@
let scrollContainer: HTMLDivElement | null = $state(null); let scrollContainer: HTMLDivElement | null = $state(null);
const albumFullyDownloaded = $derived( const albumFullyDownloaded = $derived(
isAlbumFullyDownloaded(album.tracks.map(t => t.id)) isAlbumFullyCached(album.tracks.map(t => t.id))
); );
const isVariousArtists = $derived( const isVariousArtists = $derived(
@@ -227,7 +227,7 @@
onShareQobuz={onShareAlbumQobuz} onShareQobuz={onShareAlbumQobuz}
onShareSonglink={onShareAlbumSonglink} onShareSonglink={onShareAlbumSonglink}
onDownload={onDownloadAlbum} onDownload={onDownloadAlbum}
isAlbumFullyDownloaded={albumFullyDownloaded} isAlbumFullyCached={albumFullyDownloaded}
onOpenContainingFolder={onOpenAlbumFolder} onOpenContainingFolder={onOpenAlbumFolder}
onReDownloadAlbum={onReDownloadAlbum} onReDownloadAlbum={onReDownloadAlbum}
/> />
@@ -251,7 +251,7 @@
<!-- Track Rows --> <!-- Track Rows -->
<div class="tracks"> <div class="tracks">
{#each album.tracks as track, trackIndex (`${track.id}-${downloadStateVersion}`)} {#each album.tracks as track, trackIndex (`${track.id}-${downloadStateVersion}`)}
{@const downloadInfo = getTrackDownloadStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }} {@const downloadInfo = getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
{@const isTrackDownloaded = downloadInfo.status === 'ready'} {@const isTrackDownloaded = downloadInfo.status === 'ready'}
<TrackRow <TrackRow
trackId={track.id} trackId={track.id}
+13 -13
View File
@@ -7,7 +7,7 @@
import VirtualizedTrackList from '../VirtualizedTrackList.svelte'; import VirtualizedTrackList from '../VirtualizedTrackList.svelte';
import PlaylistCollage from '../PlaylistCollage.svelte'; import PlaylistCollage from '../PlaylistCollage.svelte';
import FavoritesEditModal from '../FavoritesEditModal.svelte'; import FavoritesEditModal from '../FavoritesEditModal.svelte';
import { type DownloadStatus } from '$lib/stores/downloadState'; import { type OfflineCacheStatus } from '$lib/stores/offlineCacheState';
import { consumeContextTrackFocus, setPlaybackContext } from '$lib/stores/playbackContextStore'; import { consumeContextTrackFocus, setPlaybackContext } from '$lib/stores/playbackContextStore';
import { normalizeFavoritesTabOrder } from '$lib/utils/favorites'; import { normalizeFavoritesTabOrder } from '$lib/utils/favorites';
import type { FavoritesPreferences } from '$lib/types'; import type { FavoritesPreferences } from '$lib/types';
@@ -77,7 +77,7 @@
onTrackDownload?: (track: DisplayTrack) => void; onTrackDownload?: (track: DisplayTrack) => void;
onTrackRemoveDownload?: (trackId: number) => void; onTrackRemoveDownload?: (trackId: number) => void;
onTrackReDownload?: (track: DisplayTrack) => void; onTrackReDownload?: (track: DisplayTrack) => void;
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number }; getTrackOfflineCacheStatus?: (trackId: number) => { status: OfflineCacheStatus; progress: number };
onPlaylistSelect?: (playlistId: number) => void; onPlaylistSelect?: (playlistId: number) => void;
selectedTab?: TabType; selectedTab?: TabType;
onTabNavigate?: (tab: TabType) => void; onTabNavigate?: (tab: TabType) => void;
@@ -127,7 +127,7 @@
onTrackDownload, onTrackDownload,
onTrackRemoveDownload, onTrackRemoveDownload,
onTrackReDownload, onTrackReDownload,
getTrackDownloadStatus, getTrackOfflineCacheStatus,
onPlaylistSelect, onPlaylistSelect,
selectedTab, selectedTab,
onTabNavigate, onTabNavigate,
@@ -163,27 +163,27 @@
}); });
// Download status tracking // Download status tracking
let albumDownloadStatuses = $state<Map<string, boolean>>(new Map()); let albumOfflineCacheStatuses = $state<Map<string, boolean>>(new Map());
async function loadAlbumDownloadStatus(albumId: string) { async function loadAlbumOfflineCacheStatus(albumId: string) {
if (!checkAlbumFullyDownloaded) return false; if (!checkAlbumFullyDownloaded) return false;
try { try {
const isDownloaded = await checkAlbumFullyDownloaded(albumId); const isDownloaded = await checkAlbumFullyDownloaded(albumId);
albumDownloadStatuses.set(albumId, isDownloaded); albumOfflineCacheStatuses.set(albumId, isDownloaded);
return isDownloaded; return isDownloaded;
} catch { } catch {
return false; return false;
} }
} }
async function loadAllAlbumDownloadStatuses(albums: { id: string }[]) { async function loadAllAlbumOfflineCacheStatuses(albums: { id: string }[]) {
if (!checkAlbumFullyDownloaded || albums.length === 0) return; if (!checkAlbumFullyDownloaded || albums.length === 0) return;
await Promise.all(albums.map(album => loadAlbumDownloadStatus(album.id))); await Promise.all(albums.map(album => loadAlbumOfflineCacheStatus(album.id)));
} }
function isAlbumDownloaded(albumId: string): boolean { function isAlbumDownloaded(albumId: string): boolean {
void downloadStateVersion; void downloadStateVersion;
return albumDownloadStatuses.get(albumId) || false; return albumOfflineCacheStatuses.get(albumId) || false;
} }
let error = $state<string | null>(null); let error = $state<string | null>(null);
@@ -449,7 +449,7 @@
favoriteTracks = items as FavoriteTrack[]; favoriteTracks = items as FavoriteTrack[];
} else if (type === 'albums') { } else if (type === 'albums') {
favoriteAlbums = items as FavoriteAlbum[]; favoriteAlbums = items as FavoriteAlbum[];
await loadAllAlbumDownloadStatuses(favoriteAlbums); await loadAllAlbumOfflineCacheStatuses(favoriteAlbums);
} else if (type === 'artists') { } else if (type === 'artists') {
favoriteArtists = items as FavoriteArtist[]; favoriteArtists = items as FavoriteArtist[];
} }
@@ -1133,7 +1133,7 @@
hideDownload={false} hideDownload={false}
hideFavorite={false} hideFavorite={false}
isFavoriteOverride={true} isFavoriteOverride={true}
getDownloadStatus={getTrackDownloadStatus} getOfflineCacheStatus={getTrackOfflineCacheStatus}
onDownload={onTrackDownload ? (t) => onTrackDownload(buildDisplayTrackFromFavorite(t)) : undefined} onDownload={onTrackDownload ? (t) => onTrackDownload(buildDisplayTrackFromFavorite(t)) : undefined}
onRemoveDownload={onTrackRemoveDownload} onRemoveDownload={onTrackRemoveDownload}
onShareQobuz={onTrackShareQobuz} onShareQobuz={onTrackShareQobuz}
@@ -1204,7 +1204,7 @@
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined} onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined} onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
{downloadStateVersion} {downloadStateVersion}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }} onclick={() => { onAlbumClick?.(album.id); loadAlbumOfflineCacheStatus(album.id); }}
/> />
{/each} {/each}
</div> </div>
@@ -1275,7 +1275,7 @@
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined} onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined} onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
{downloadStateVersion} {downloadStateVersion}
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }} onclick={() => { onAlbumClick?.(album.id); loadAlbumOfflineCacheStatus(album.id); }}
/> />
{/each} {/each}
</div> </div>
@@ -7,7 +7,7 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import TrackRow from '../TrackRow.svelte'; import TrackRow from '../TrackRow.svelte';
import { type DownloadStatus } from '$lib/stores/downloadState'; import { type OfflineCacheStatus } from '$lib/stores/offlineCacheState';
import { import {
subscribe as subscribeOffline, subscribe as subscribeOffline,
getStatus as getOfflineStatus, getStatus as getOfflineStatus,
@@ -129,7 +129,7 @@
onTrackDownload?: (track: DisplayTrack) => void; onTrackDownload?: (track: DisplayTrack) => void;
onTrackRemoveDownload?: (trackId: number) => void; onTrackRemoveDownload?: (trackId: number) => void;
onTrackReDownload?: (track: DisplayTrack) => void; onTrackReDownload?: (track: DisplayTrack) => void;
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number }; getTrackOfflineCacheStatus?: (trackId: number) => { status: OfflineCacheStatus; progress: number };
downloadStateVersion?: number; downloadStateVersion?: number;
onLocalTrackPlay?: (track: LocalLibraryTrack) => void; onLocalTrackPlay?: (track: LocalLibraryTrack) => void;
onLocalTrackPlayNext?: (track: LocalLibraryTrack) => void; onLocalTrackPlayNext?: (track: LocalLibraryTrack) => void;
@@ -157,7 +157,7 @@
onTrackDownload, onTrackDownload,
onTrackRemoveDownload, onTrackRemoveDownload,
onTrackReDownload, onTrackReDownload,
getTrackDownloadStatus, getTrackOfflineCacheStatus,
downloadStateVersion, downloadStateVersion,
onLocalTrackPlay, onLocalTrackPlay,
onLocalTrackPlayNext, onLocalTrackPlayNext,
@@ -1099,7 +1099,7 @@
</div> </div>
{#each displayTracks as track, idx (`${idx}-${track.id}-${downloadStateVersion}`)} {#each displayTracks as track, idx (`${idx}-${track.id}-${downloadStateVersion}`)}
{@const downloadInfo = track.isLocal ? { status: 'none' as const, progress: 0 } : (getTrackDownloadStatus?.(track.id) ?? { status: 'none' as const, progress: 0 })} {@const downloadInfo = track.isLocal ? { status: 'none' as const, progress: 0 } : (getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none' as const, progress: 0 })}
{@const isActiveTrack = ( {@const isActiveTrack = (
track.isLocal track.isLocal
? (track.localTrackId !== undefined && activeTrackId === track.localTrackId) ? (track.localTrackId !== undefined && activeTrackId === track.localTrackId)
+7 -7
View File
@@ -7,10 +7,10 @@
import Dropdown from '../Dropdown.svelte'; import Dropdown from '../Dropdown.svelte';
import VolumeSlider from '../VolumeSlider.svelte'; import VolumeSlider from '../VolumeSlider.svelte';
import { import {
getDownloadCacheStats, getOfflineCacheStats,
clearDownloadCache, clearOfflineCache,
type DownloadCacheStats type OfflineCacheStats
} from '$lib/stores/downloadState'; } from '$lib/stores/offlineCacheState';
import { notifyDownloadSettingsChanged } from '$lib/stores/downloadSettingsStore'; import { notifyDownloadSettingsChanged } from '$lib/stores/downloadSettingsStore';
import { clearCache as clearLyricsCache } from '$lib/stores/lyricsStore'; import { clearCache as clearLyricsCache } from '$lib/stores/lyricsStore';
import { import {
@@ -94,7 +94,7 @@
let isClearing = $state(false); let isClearing = $state(false);
// Download cache state (offline storage) // Download cache state (offline storage)
let downloadStats = $state<DownloadCacheStats | null>(null); let downloadStats = $state<OfflineCacheStats | null>(null);
let isClearingDownloads = $state(false); let isClearingDownloads = $state(false);
let isRepairingDownloads = $state(false); let isRepairingDownloads = $state(false);
@@ -1165,7 +1165,7 @@
async function loadDownloadStats() { async function loadDownloadStats() {
try { try {
downloadStats = await getDownloadCacheStats(); downloadStats = await getOfflineCacheStats();
} catch (err) { } catch (err) {
console.error('Failed to load download stats:', err); console.error('Failed to load download stats:', err);
} }
@@ -1266,7 +1266,7 @@
if (isClearingDownloads) return; if (isClearingDownloads) return;
isClearingDownloads = true; isClearingDownloads = true;
try { try {
await clearDownloadCache(); await clearOfflineCache();
await loadDownloadStats(); await loadDownloadStats();
} catch (err) { } catch (err) {
console.error('Failed to clear download cache:', err); console.error('Failed to clear download cache:', err);
-233
View File
@@ -1,233 +0,0 @@
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
export type DownloadStatus = 'none' | 'queued' | 'downloading' | 'ready' | 'failed';
export interface DownloadInfo {
status: DownloadStatus;
progress: number;
error?: string;
}
export interface CachedTrackInfo {
trackId: number;
title: string;
artist: string;
album?: string;
albumId?: string;
durationSecs: number;
fileSizeBytes: number;
quality: string;
bitDepth?: number;
sampleRate?: number;
status: DownloadStatus;
progressPercent: number;
errorMessage?: string;
createdAt: string;
lastAccessedAt: string;
}
export interface DownloadCacheStats {
totalTracks: number;
readyTracks: number;
downloadingTracks: number;
failedTracks: number;
totalSizeBytes: number;
limitBytes?: number;
cachePath: string;
}
// Track download states by track ID
const downloadStates = new Map<number, DownloadInfo>();
// Listeners for state changes
const listeners = new Set<() => void>();
// Event unsubscribe functions
let unlisteners: UnlistenFn[] = [];
export function getDownloadState(trackId: number): DownloadInfo {
return downloadStates.get(trackId) || { status: 'none', progress: 0 };
}
export function setDownloadState(trackId: number, info: DownloadInfo): void {
downloadStates.set(trackId, info);
notifyListeners();
}
// Check if all tracks of an album are downloaded
export function isAlbumFullyDownloaded(trackIds: number[]): boolean {
if (trackIds.length === 0) return false;
return trackIds.every(trackId => {
const state = getDownloadState(trackId);
return state.status === 'ready';
});
}
export function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
function notifyListeners(): void {
for (const listener of listeners) {
listener();
}
}
// Initialize download states from backend
export async function initDownloadStates(): Promise<void> {
try {
const tracks = await invoke<CachedTrackInfo[]>('get_downloaded_tracks');
for (const track of tracks) {
downloadStates.set(track.trackId, {
status: track.status,
progress: track.progressPercent,
error: track.errorMessage,
});
}
notifyListeners();
} catch (err) {
console.error('Failed to init download states:', err);
}
}
// Start listening for download events
export async function startDownloadEventListeners(): Promise<void> {
try {
const unlistenStarted = await listen<{ trackId: number }>('download:started', (event) => {
console.log('Download started:', event.payload.trackId);
setDownloadState(event.payload.trackId, { status: 'downloading', progress: 0 });
});
const unlistenProgress = await listen<{
trackId: number;
progressPercent: number;
bytesDownloaded: number;
totalBytes?: number;
status: string;
}>('download:progress', (event) => {
const { trackId, progressPercent } = event.payload;
setDownloadState(trackId, { status: 'downloading', progress: progressPercent });
});
const unlistenCompleted = await listen<{ trackId: number; size: number }>('download:completed', (event) => {
console.log('Download completed:', event.payload.trackId);
setDownloadState(event.payload.trackId, { status: 'ready', progress: 100 });
});
const unlistenFailed = await listen<{ trackId: number; error: string }>('download:failed', (event) => {
console.error('Download failed:', event.payload.trackId, event.payload.error);
setDownloadState(event.payload.trackId, {
status: 'failed',
progress: 0,
error: event.payload.error,
});
});
unlisteners = [unlistenStarted, unlistenProgress, unlistenCompleted, unlistenFailed];
} catch (err) {
console.error('Failed to setup download event listeners:', err);
}
}
// Stop listening for events
export function stopDownloadEventListeners(): void {
for (const unlisten of unlisteners) {
unlisten();
}
unlisteners = [];
}
// Start a download
export async function downloadTrack(track: {
id: number;
title: string;
artist: string;
album?: string;
albumId?: string;
durationSecs: number;
quality: string;
bitDepth?: number;
sampleRate?: number;
}): Promise<void> {
try {
setDownloadState(track.id, { status: 'queued', progress: 0 });
await invoke('download_track', {
trackId: track.id,
title: track.title,
artist: track.artist,
album: track.album,
albumId: track.albumId,
durationSecs: track.durationSecs,
quality: track.quality,
bitDepth: track.bitDepth,
sampleRate: track.sampleRate,
});
} catch (err) {
console.error('Failed to start download:', err);
setDownloadState(track.id, { status: 'failed', progress: 0, error: String(err) });
throw err;
}
}
// Remove a download
export async function removeDownload(trackId: number): Promise<void> {
try {
await invoke('remove_downloaded_track', { trackId });
downloadStates.delete(trackId);
notifyListeners();
} catch (err) {
console.error('Failed to remove download:', err);
throw err;
}
}
// Get cache stats
export async function getDownloadCacheStats(): Promise<DownloadCacheStats> {
return invoke<DownloadCacheStats>('get_download_cache_stats');
}
// Clear all downloads
export async function clearDownloadCache(): Promise<void> {
await invoke('clear_download_cache');
downloadStates.clear();
notifyListeners();
}
// Open cache folder
export async function openDownloadCacheFolder(): Promise<void> {
await invoke('open_download_cache_folder');
}
// Set cache limit
export async function setDownloadCacheLimit(limitMb: number | null): Promise<void> {
await invoke('set_download_cache_limit', { limitMb });
}
// Open containing folder for a specific album
export async function openAlbumFolder(albumId: string): Promise<void> {
await invoke('open_album_folder', { albumId });
}
// Open containing folder for a specific track
export async function openTrackFolder(trackId: number): Promise<void> {
await invoke('open_track_folder', { trackId });
}
// Re-download a track (downloads again, overwriting if exists)
export async function reDownloadTrack(track: {
id: number;
title: string;
artist: string;
album?: string;
albumId?: string;
durationSecs: number;
quality: string;
bitDepth?: number;
sampleRate?: number;
}): Promise<void> {
// Just call downloadTrack - backend handles overwriting
await downloadTrack(track);
}
+286
View File
@@ -0,0 +1,286 @@
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
export type OfflineCacheStatus = 'none' | 'queued' | 'downloading' | 'ready' | 'failed';
export interface OfflineCacheInfo {
status: OfflineCacheStatus;
progress: number;
error?: string;
}
export interface CachedTrackInfo {
trackId: number;
title: string;
artist: string;
album?: string;
albumId?: string;
durationSecs: number;
fileSizeBytes: number;
quality: string;
bitDepth?: number;
sampleRate?: number;
status: OfflineCacheStatus;
progressPercent: number;
errorMessage?: string;
createdAt: string;
lastAccessedAt: string;
}
export interface OfflineCacheStats {
totalTracks: number;
readyTracks: number;
downloadingTracks: number;
failedTracks: number;
totalSizeBytes: number;
limitBytes?: number;
cachePath: string;
}
// Track offline cache states by track ID
const offlineCacheStates = new Map<number, OfflineCacheInfo>();
// Listeners for state changes
const listeners = new Set<() => void>();
// Event unsubscribe functions
let unlisteners: UnlistenFn[] = [];
export function getOfflineCacheState(trackId: number): OfflineCacheInfo {
return offlineCacheStates.get(trackId) || { status: 'none', progress: 0 };
}
export function setOfflineCacheState(trackId: number, info: OfflineCacheInfo): void {
offlineCacheStates.set(trackId, info);
notifyListeners();
}
// Check if all tracks of an album are cached for offline
export function isAlbumFullyCached(trackIds: number[]): boolean {
if (trackIds.length === 0) return false;
return trackIds.every(trackId => {
const state = getOfflineCacheState(trackId);
return state.status === 'ready';
});
}
export function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
function notifyListeners(): void {
for (const listener of listeners) {
listener();
}
}
// Initialize offline cache states from backend
export async function initOfflineCacheStates(): Promise<void> {
try {
const tracks = await invoke<CachedTrackInfo[]>('get_cached_tracks');
for (const track of tracks) {
offlineCacheStates.set(track.trackId, {
status: track.status,
progress: track.progressPercent,
error: track.errorMessage,
});
}
notifyListeners();
} catch (err) {
console.error('Failed to init offline cache states:', err);
}
}
// Start listening for offline cache events
export async function startOfflineCacheEventListeners(): Promise<void> {
try {
const unlistenStarted = await listen<{ trackId: number }>('offline:caching_started', (event) => {
console.log('Offline caching started:', event.payload.trackId);
setOfflineCacheState(event.payload.trackId, { status: 'downloading', progress: 0 });
});
const unlistenProgress = await listen<{
trackId: number;
progressPercent: number;
bytesDownloaded: number;
totalBytes?: number;
status: string;
}>('offline:caching_progress', (event) => {
const { trackId, progressPercent } = event.payload;
setOfflineCacheState(trackId, { status: 'downloading', progress: progressPercent });
});
const unlistenCompleted = await listen<{ trackId: number; size: number }>('offline:cached', (event) => {
console.log('Offline caching completed:', event.payload.trackId);
setOfflineCacheState(event.payload.trackId, { status: 'ready', progress: 100 });
});
const unlistenFailed = await listen<{ trackId: number; error: string }>('offline:cache_failed', (event) => {
console.error('Offline caching failed:', event.payload.trackId, event.payload.error);
setOfflineCacheState(event.payload.trackId, {
status: 'failed',
progress: 0,
error: event.payload.error,
});
});
unlisteners = [unlistenStarted, unlistenProgress, unlistenCompleted, unlistenFailed];
} catch (err) {
console.error('Failed to setup offline cache event listeners:', err);
}
}
// Stop listening for events
export function stopOfflineCacheEventListeners(): void {
for (const unlisten of unlisteners) {
unlisten();
}
unlisteners = [];
}
// Cache a track for offline listening
export async function cacheTrackForOffline(track: {
id: number;
title: string;
artist: string;
album?: string;
albumId?: string;
durationSecs: number;
quality: string;
bitDepth?: number;
sampleRate?: number;
}): Promise<void> {
try {
setOfflineCacheState(track.id, { status: 'queued', progress: 0 });
await invoke('cache_track_for_offline', {
trackId: track.id,
title: track.title,
artist: track.artist,
album: track.album,
albumId: track.albumId,
durationSecs: track.durationSecs,
quality: track.quality,
bitDepth: track.bitDepth,
sampleRate: track.sampleRate,
});
} catch (err) {
console.error('Failed to cache track for offline:', err);
setOfflineCacheState(track.id, { status: 'failed', progress: 0, error: String(err) });
throw err;
}
}
// Remove a track from offline cache
export async function removeCachedTrack(trackId: number): Promise<void> {
try {
await invoke('remove_cached_track', { trackId });
offlineCacheStates.delete(trackId);
notifyListeners();
} catch (err) {
console.error('Failed to remove cached track:', err);
throw err;
}
}
// Get offline cache stats
export async function getOfflineCacheStats(): Promise<OfflineCacheStats> {
return invoke<OfflineCacheStats>('get_offline_cache_stats');
}
// Clear all offline cache
export async function clearOfflineCache(): Promise<void> {
await invoke('clear_offline_cache');
offlineCacheStates.clear();
notifyListeners();
}
// Open offline cache folder
export async function openOfflineCacheFolder(): Promise<void> {
await invoke('open_offline_cache_folder');
}
// Set offline cache limit
export async function setOfflineCacheLimit(limitMb: number | null): Promise<void> {
await invoke('set_offline_cache_limit', { limitMb });
}
// Open containing folder for a specific album
export async function openAlbumFolder(albumId: string): Promise<void> {
await invoke('open_album_folder', { albumId });
}
// Open containing folder for a specific track
export async function openTrackFolder(trackId: number): Promise<void> {
await invoke('open_track_folder', { trackId });
}
// Refresh a cached track (re-cache, overwriting if exists)
export async function refreshCachedTrack(track: {
id: number;
title: string;
artist: string;
album?: string;
albumId?: string;
durationSecs: number;
quality: string;
bitDepth?: number;
sampleRate?: number;
}): Promise<void> {
// Just call cacheTrackForOffline - backend handles overwriting
await cacheTrackForOffline(track);
}
// ============================================================================
// COMPATIBILITY ALIASES - These maintain backward compatibility during refactor
// Will be removed once all references are updated
// ============================================================================
/** @deprecated Use OfflineCacheStatus instead */
export type DownloadStatus = OfflineCacheStatus;
/** @deprecated Use OfflineCacheInfo instead */
export type DownloadInfo = OfflineCacheInfo;
/** @deprecated Use OfflineCacheStats instead */
export type DownloadCacheStats = OfflineCacheStats;
/** @deprecated Use getOfflineCacheState instead */
export const getDownloadState = getOfflineCacheState;
/** @deprecated Use setOfflineCacheState instead */
export const setDownloadState = setOfflineCacheState;
/** @deprecated Use isAlbumFullyCached instead */
export const isAlbumFullyDownloaded = isAlbumFullyCached;
/** @deprecated Use initOfflineCacheStates instead */
export const initDownloadStates = initOfflineCacheStates;
/** @deprecated Use startOfflineCacheEventListeners instead */
export const startDownloadEventListeners = startOfflineCacheEventListeners;
/** @deprecated Use stopOfflineCacheEventListeners instead */
export const stopDownloadEventListeners = stopOfflineCacheEventListeners;
/** @deprecated Use cacheTrackForOffline instead */
export const downloadTrack = cacheTrackForOffline;
/** @deprecated Use removeCachedTrack instead */
export const removeDownload = removeCachedTrack;
/** @deprecated Use getOfflineCacheStats instead */
export const getDownloadCacheStats = getOfflineCacheStats;
/** @deprecated Use clearOfflineCache instead */
export const clearDownloadCache = clearOfflineCache;
/** @deprecated Use openOfflineCacheFolder instead */
export const openDownloadCacheFolder = openOfflineCacheFolder;
/** @deprecated Use setOfflineCacheLimit instead */
export const setDownloadCacheLimit = setOfflineCacheLimit;
/** @deprecated Use refreshCachedTrack instead */
export const reDownloadTrack = refreshCachedTrack;
+32 -32
View File
@@ -4,19 +4,19 @@
import { listen, emitTo, type UnlistenFn } from '@tauri-apps/api/event'; import { listen, emitTo, type UnlistenFn } from '@tauri-apps/api/event';
import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { writeText } from '@tauri-apps/plugin-clipboard-manager';
// Download state management // Offline cache state management
import { import {
initDownloadStates, initOfflineCacheStates,
startDownloadEventListeners, startOfflineCacheEventListeners,
stopDownloadEventListeners, stopOfflineCacheEventListeners,
downloadTrack, cacheTrackForOffline,
removeDownload, removeCachedTrack,
getDownloadState, getOfflineCacheState,
openAlbumFolder, openAlbumFolder,
openTrackFolder, openTrackFolder,
subscribe as subscribeDownloads, subscribe as subscribeOfflineCache,
type DownloadStatus type OfflineCacheStatus
} from '$lib/stores/downloadState'; } from '$lib/stores/offlineCacheState';
// Toast state management // Toast state management
import { import {
@@ -652,7 +652,7 @@
if (!album) return; if (!album) return;
const tracksToDownload = album.tracks.filter(track => { const tracksToDownload = album.tracks.filter(track => {
const status = getDownloadState(track.id).status; const status = getOfflineCacheState(track.id).status;
return status === 'none' || status === 'failed'; return status === 'none' || status === 'failed';
}); });
@@ -665,7 +665,7 @@
for (const track of tracksToDownload) { for (const track of tracksToDownload) {
try { try {
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.artist || album.artist || 'Unknown', artist: track.artist || album.artist || 'Unknown',
@@ -1216,7 +1216,7 @@
// Download handlers // Download handlers
async function handleTrackDownload(track: Track) { async function handleTrackDownload(track: Track) {
try { try {
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.artist || selectedAlbum?.artist || 'Unknown', artist: track.artist || selectedAlbum?.artist || 'Unknown',
@@ -1236,7 +1236,7 @@
async function handleTrackRemoveDownload(trackId: number) { async function handleTrackRemoveDownload(trackId: number) {
try { try {
await removeDownload(trackId); await removeCachedTrack(trackId);
showToast('Removed from offline library', 'info'); showToast('Removed from offline library', 'info');
} catch (err) { } catch (err) {
console.error('Failed to remove from offline:', err); console.error('Failed to remove from offline:', err);
@@ -1256,7 +1256,7 @@
async function handleTrackReDownload(track: Track | DisplayTrack) { async function handleTrackReDownload(track: Track | DisplayTrack) {
try { try {
// Re-download uses the same download function - backend handles overwriting // Re-download uses the same download function - backend handles overwriting
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.artist || selectedAlbum?.artist || 'Unknown', artist: track.artist || selectedAlbum?.artist || 'Unknown',
@@ -1275,14 +1275,14 @@
} }
function checkTrackDownloaded(trackId: number): boolean { function checkTrackDownloaded(trackId: number): boolean {
return getDownloadState(trackId).status === 'ready'; return getOfflineCacheState(trackId).status === 'ready';
} }
async function handleDownloadAlbum() { async function handleDownloadAlbum() {
if (!selectedAlbum) return; if (!selectedAlbum) return;
const tracksToDownload = selectedAlbum.tracks.filter(track => { const tracksToDownload = selectedAlbum.tracks.filter(track => {
const status = getDownloadState(track.id).status; const status = getOfflineCacheState(track.id).status;
return status === 'none' || status === 'failed'; return status === 'none' || status === 'failed';
}); });
@@ -1295,7 +1295,7 @@
for (const track of tracksToDownload) { for (const track of tracksToDownload) {
try { try {
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.artist || selectedAlbum.artist || 'Unknown', artist: track.artist || selectedAlbum.artist || 'Unknown',
@@ -1330,7 +1330,7 @@
for (const track of selectedAlbum.tracks) { for (const track of selectedAlbum.tracks) {
try { try {
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.artist || selectedAlbum.artist || 'Unknown', artist: track.artist || selectedAlbum.artist || 'Unknown',
@@ -1368,7 +1368,7 @@
for (const track of album.tracks.data) { for (const track of album.tracks.data) {
try { try {
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.performer?.name || album.artist?.name || 'Unknown', artist: track.performer?.name || album.artist?.name || 'Unknown',
@@ -1389,10 +1389,10 @@
} }
} }
function getTrackDownloadStatus(trackId: number) { function getTrackOfflineCacheStatus(trackId: number) {
// Access downloadStateVersion to trigger reactivity // Access downloadStateVersion to trigger reactivity
void downloadStateVersion; void downloadStateVersion;
return getDownloadState(trackId); return getOfflineCacheState(trackId);
} }
async function handleDisplayTrackDownload(track: PlaylistTrack) { async function handleDisplayTrackDownload(track: PlaylistTrack) {
@@ -1402,7 +1402,7 @@
: track.hires : track.hires
? 'Hi-Res' ? 'Hi-Res'
: '-'; : '-';
await downloadTrack({ await cacheTrackForOffline({
id: track.id, id: track.id,
title: track.title, title: track.title,
artist: track.artist || 'Unknown', artist: track.artist || 'Unknown',
@@ -1844,7 +1844,7 @@
} }
} }
function getAlbumDownloadStatus(albumId: string): boolean { function getAlbumOfflineCacheStatus(albumId: string): boolean {
void downloadStateVersion; void downloadStateVersion;
return albumDownloadCache.get(albumId) || false; return albumDownloadCache.get(albumId) || false;
} }
@@ -1871,8 +1871,8 @@
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
// Initialize download states // Initialize download states
initDownloadStates(); initOfflineCacheStates();
startDownloadEventListeners(); startOfflineCacheEventListeners();
// Initialize playback context and preferences stores // Initialize playback context and preferences stores
initPlaybackContextStore(); initPlaybackContextStore();
@@ -1892,7 +1892,7 @@
// This prevents API calls before authentication is complete // This prevents API calls before authentication is complete
// Subscribe to download state changes to trigger reactivity // Subscribe to download state changes to trigger reactivity
const unsubscribeDownloads = subscribeDownloads(() => { const unsubscribeOfflineCache = subscribeOfflineCache(() => {
downloadStateVersion++; downloadStateVersion++;
}); });
@@ -2109,8 +2109,8 @@
document.removeEventListener('keydown', handleKeydown); document.removeEventListener('keydown', handleKeydown);
window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('visibilitychange', handleVisibilityChange);
stopDownloadEventListeners(); stopOfflineCacheEventListeners();
unsubscribeDownloads(); unsubscribeOfflineCache();
unsubscribeToast(); unsubscribeToast();
unsubscribeUI(); unsubscribeUI();
unsubscribeAuth(); unsubscribeAuth();
@@ -2330,7 +2330,7 @@
onTrackDownload={handleTrackDownload} onTrackDownload={handleTrackDownload}
onTrackRemoveDownload={handleTrackRemoveDownload} onTrackRemoveDownload={handleTrackRemoveDownload}
onTrackReDownload={handleTrackReDownload} onTrackReDownload={handleTrackReDownload}
getTrackDownloadStatus={getTrackDownloadStatus} getTrackOfflineCacheStatus={getTrackOfflineCacheStatus}
onDownloadAlbum={handleDownloadAlbum} onDownloadAlbum={handleDownloadAlbum}
onShareAlbumQobuz={shareAlbumQobuzLink} onShareAlbumQobuz={shareAlbumQobuzLink}
onShareAlbumSonglink={shareAlbumSonglink} onShareAlbumSonglink={shareAlbumSonglink}
@@ -2398,7 +2398,7 @@
onTrackDownload={handleDisplayTrackDownload} onTrackDownload={handleDisplayTrackDownload}
onTrackRemoveDownload={handleTrackRemoveDownload} onTrackRemoveDownload={handleTrackRemoveDownload}
onTrackReDownload={handleDisplayTrackDownload} onTrackReDownload={handleDisplayTrackDownload}
getTrackDownloadStatus={getTrackDownloadStatus} getTrackOfflineCacheStatus={getTrackOfflineCacheStatus}
{downloadStateVersion} {downloadStateVersion}
onLocalTrackPlay={handleLocalTrackPlay} onLocalTrackPlay={handleLocalTrackPlay}
onLocalTrackPlayNext={queueLocalTrackNext} onLocalTrackPlayNext={queueLocalTrackNext}
@@ -2445,7 +2445,7 @@
onTrackDownload={handleDisplayTrackDownload} onTrackDownload={handleDisplayTrackDownload}
onTrackRemoveDownload={handleTrackRemoveDownload} onTrackRemoveDownload={handleTrackRemoveDownload}
onTrackReDownload={handleDisplayTrackDownload} onTrackReDownload={handleDisplayTrackDownload}
getTrackDownloadStatus={getTrackDownloadStatus} getTrackOfflineCacheStatus={getTrackOfflineCacheStatus}
onPlaylistSelect={selectPlaylist} onPlaylistSelect={selectPlaylist}
selectedTab={getFavoritesTabFromView(activeView) ?? favoritesDefaultTab} selectedTab={getFavoritesTabFromView(activeView) ?? favoritesDefaultTab}
onTabNavigate={(tab) => navigateToFavorites(tab)} onTabNavigate={(tab) => navigateToFavorites(tab)}