From 6d5d2647788c612d128c0ae1b3f8a2f93da6cad5 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:14:39 -0600 Subject: [PATCH] refactor(backend): rename module and APIs for compliance - Renamed download_cache module to offline_cache - Updated Tauri command names (e.g., cache_track_for_offline) - Updated event names (offline:caching_started, etc.) - Updated library database function names - Updated frontend invoke calls to use new command names --- src-tauri/src/commands/auth.rs | 24 +- src-tauri/src/commands/playback.rs | 18 +- src-tauri/src/config/subscription_state.rs | 4 +- src-tauri/src/lib.rs | 62 ++-- src-tauri/src/library/commands.rs | 30 +- src-tauri/src/library/database.rs | 49 ++-- .../commands.rs | 264 +++++++++--------- .../{download_cache => offline_cache}/db.rs | 30 +- .../downloader.rs | 58 ++-- .../metadata.rs | 8 +- .../migration.rs | 68 ++--- .../{download_cache => offline_cache}/mod.rs | 54 ++-- .../path_validator.rs | 10 +- src/lib/components/views/SettingsView.svelte | 10 +- src/routes/+page.svelte | 2 +- 15 files changed, 348 insertions(+), 343 deletions(-) rename src-tauri/src/{download_cache => offline_cache}/commands.rs (73%) rename src-tauri/src/{download_cache => offline_cache}/db.rs (93%) rename src-tauri/src/{download_cache => offline_cache}/downloader.rs (70%) rename src-tauri/src/{download_cache => offline_cache}/metadata.rs (98%) rename src-tauri/src/{download_cache => offline_cache}/migration.rs (88%) rename src-tauri/src/{download_cache => offline_cache}/mod.rs (80%) rename src-tauri/src/{download_cache => offline_cache}/path_validator.rs (96%) diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index fdb7bc8e4..a340bd047 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -7,7 +7,7 @@ use crate::credentials; use crate::AppState; use crate::api::error::ApiError; use crate::config::SubscriptionStateState; -use crate::download_cache::DownloadCacheState; +use crate::offline_cache::OfflineCacheState; #[derive(serde::Serialize)] pub struct LoginResponse { @@ -26,10 +26,10 @@ fn now_unix_secs() -> i64 { .as_secs() as i64 } -async fn maybe_purge_downloads_for_invalid_subscription( +async fn maybe_purge_offline_cache_for_invalid_subscription( now: i64, subscription_state: &SubscriptionStateState, - cache_state: &DownloadCacheState, + cache_state: &OfflineCacheState, library_state: &crate::library::commands::LibraryState, ) { let should_purge = { @@ -40,7 +40,7 @@ async fn maybe_purge_downloads_for_invalid_subscription( return; } }; - match store.should_purge_downloads(now) { + match store.should_purge_offline_cache(now) { Ok(v) => v, Err(e) => { log::warn!("Failed to evaluate purge condition: {}", e); @@ -53,14 +53,14 @@ async fn maybe_purge_downloads_for_invalid_subscription( return; } - log::warn!("Subscription invalid for >3 days. Purging offline downloads."); - if let Err(e) = crate::download_cache::commands::purge_all_downloads(cache_state, library_state).await { - log::error!("Failed to purge downloads: {}", e); + log::warn!("Subscription invalid for >3 days. Purging offline cache."); + if let Err(e) = crate::offline_cache::commands::purge_all_cached_files(cache_state, library_state).await { + log::error!("Failed to purge offline cache: {}", e); return; } if let Ok(store) = subscription_state.lock() { - if let Err(e) = store.mark_downloads_purged(now) { + if let Err(e) = store.mark_offline_cache_purged(now) { log::warn!("Failed to persist purge timestamp: {}", e); } } @@ -72,7 +72,7 @@ pub async fn login( password: String, state: State<'_, AppState>, subscription_state: State<'_, SubscriptionStateState>, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, ) -> Result { let client = state.client.lock().await; @@ -96,7 +96,7 @@ pub async fn login( if let Ok(store) = subscription_state.lock() { let _ = store.mark_invalid(now); } - maybe_purge_downloads_for_invalid_subscription( + maybe_purge_offline_cache_for_invalid_subscription( now, subscription_state.inner(), cache_state.inner(), @@ -188,7 +188,7 @@ pub fn clear_saved_credentials() -> Result<(), String> { pub async fn auto_login( state: State<'_, AppState>, subscription_state: State<'_, SubscriptionStateState>, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, ) -> Result { // Check for saved credentials @@ -237,7 +237,7 @@ pub async fn auto_login( if let Ok(store) = subscription_state.lock() { let _ = store.mark_invalid(now); } - maybe_purge_downloads_for_invalid_subscription( + maybe_purge_offline_cache_for_invalid_subscription( now, subscription_state.inner(), cache_state.inner(), diff --git a/src-tauri/src/commands/playback.rs b/src-tauri/src/commands/playback.rs index a972555ed..2db3ed82d 100644 --- a/src-tauri/src/commands/playback.rs +++ b/src-tauri/src/commands/playback.rs @@ -7,7 +7,7 @@ use tokio::sync::Mutex; use crate::api::client::QobuzClient; use crate::api::models::Quality; use crate::cache::AudioCache; -use crate::download_cache::DownloadCacheState; +use crate::offline_cache::OfflineCacheState; use crate::player::PlaybackState; use crate::queue::QueueManager; use crate::AppState; @@ -17,17 +17,17 @@ use crate::AppState; pub async fn play_track( track_id: u64, state: State<'_, AppState>, - download_cache: State<'_, DownloadCacheState>, + offline_cache: State<'_, OfflineCacheState>, ) -> Result<(), String> { log::info!("Command: play_track {}", track_id); - // First check download cache (persistent disk cache) + // First check offline cache (persistent disk cache) { - let db = download_cache.db.lock().await; + let db = offline_cache.db.lock().await; if let Ok(Some(file_path)) = db.get_file_path(track_id) { let path = std::path::Path::new(&file_path); if path.exists() { - log::info!("Playing track {} from download cache: {:?}", track_id, path); + log::info!("Playing track {} from offline cache: {:?}", track_id, path); // Update last accessed time let _ = db.touch(track_id); @@ -132,7 +132,7 @@ pub async fn play_track( pub async fn prefetch_track( track_id: u64, state: State<'_, AppState>, - download_cache: State<'_, DownloadCacheState>, + offline_cache: State<'_, OfflineCacheState>, ) -> Result<(), String> { log::info!("Command: prefetch_track {}", track_id); @@ -150,13 +150,13 @@ pub async fn prefetch_track( cache.mark_fetching(track_id); let result = async { - // Check persistent download cache first + // Check persistent offline cache first { - let db = download_cache.db.lock().await; + let db = offline_cache.db.lock().await; if let Ok(Some(file_path)) = db.get_file_path(track_id) { let path = std::path::Path::new(&file_path); if path.exists() { - log::info!("Prefetching track {} from download cache", track_id); + log::info!("Prefetching track {} from offline cache", track_id); drop(db); let audio_data = std::fs::read(path) .map_err(|e| format!("Failed to read cached file: {}", e))?; diff --git a/src-tauri/src/config/subscription_state.rs b/src-tauri/src/config/subscription_state.rs index 0f4d9c1a6..0c4185f14 100644 --- a/src-tauri/src/config/subscription_state.rs +++ b/src-tauri/src/config/subscription_state.rs @@ -112,7 +112,7 @@ impl SubscriptionStateStore { Ok(()) } - pub fn mark_downloads_purged(&self, now: i64) -> Result<(), String> { + pub fn mark_offline_cache_purged(&self, now: i64) -> Result<(), String> { self.conn .execute( "UPDATE subscription_state SET downloads_purged_at = ?1 WHERE id = 1", @@ -122,7 +122,7 @@ impl SubscriptionStateStore { Ok(()) } - pub fn should_purge_downloads(&self, now: i64) -> Result { + pub fn should_purge_offline_cache(&self, now: i64) -> Result { let state = self.get_state()?; let Some(invalid_since) = state.invalid_since else { return Ok(false); }; if now - invalid_since < GRACE_PERIOD_SECS { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 10a7dc539..d67ee9393 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,7 +11,7 @@ pub mod commands; pub mod config; pub mod credentials; pub mod discogs; -pub mod download_cache; +pub mod offline_cache; pub mod flatpak; pub mod lastfm; pub mod library; @@ -156,9 +156,9 @@ pub fn run() { // let airplay_state = cast::airplay::commands::AirPlayState::new() // .expect("Failed to initialize AirPlay state"); - // Initialize download cache state - let download_cache_state = download_cache::DownloadCacheState::new() - .expect("Failed to initialize download cache"); + // Initialize offline cache state + let offline_cache_state = offline_cache::OfflineCacheState::new() + .expect("Failed to initialize offline cache"); // Initialize lyrics cache state let lyrics_state = lyrics::LyricsState::new() .expect("Failed to initialize lyrics cache"); @@ -252,7 +252,7 @@ pub fn run() { return; } }; - match store.should_purge_downloads(now) { + match store.should_purge_offline_cache(now) { Ok(v) => v, Err(e) => { log::warn!("Failed to evaluate purge condition: {}", e); @@ -265,11 +265,11 @@ pub fn run() { return; } - log::warn!("Subscription invalid for >3 days. Purging offline downloads."); - let cache_state = app_handle.state::(); + log::warn!("Subscription invalid for >3 days. Purging offline cache."); + let cache_state = app_handle.state::(); let library_state = app_handle.state::(); - if let Err(e) = download_cache::commands::purge_all_downloads(cache_state.inner(), library_state.inner()).await { - log::error!("Failed to purge downloads: {}", e); + if let Err(e) = offline_cache::commands::purge_all_cached_files(cache_state.inner(), library_state.inner()).await { + log::error!("Failed to purge offline cache: {}", e); return; } @@ -280,7 +280,7 @@ pub fn run() { return; } }; - if let Err(e) = store.mark_downloads_purged(now) { + if let Err(e) = store.mark_offline_cache_purged(now) { log::warn!("Failed to persist purge timestamp: {}", e); } }); @@ -359,7 +359,7 @@ pub fn run() { .manage(cast_state) .manage(dlna_state) // .manage(airplay_state) // AirPlay DISABLED - .manage(download_cache_state) + .manage(offline_cache_state) .manage(lyrics_state) .manage(reco_state) .manage(api_cache_state) @@ -600,26 +600,26 @@ pub fn run() { // cast::airplay::commands::airplay_pause, // cast::airplay::commands::airplay_stop, // cast::airplay::commands::airplay_set_volume, - // Download cache commands - download_cache::commands::download_track, - download_cache::commands::is_track_downloaded, - download_cache::commands::get_downloaded_track_path, - download_cache::commands::get_downloaded_track, - download_cache::commands::get_downloaded_tracks, - download_cache::commands::get_download_cache_stats, - download_cache::commands::remove_downloaded_track, - download_cache::commands::clear_download_cache, - download_cache::commands::set_download_cache_limit, - download_cache::commands::open_download_cache_folder, - download_cache::commands::open_album_folder, - download_cache::commands::open_track_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, - download_cache::commands::detect_legacy_downloads, - download_cache::commands::start_legacy_migration, - download_cache::commands::sync_downloads_to_library, + // Offline cache commands + offline_cache::commands::cache_track_for_offline, + offline_cache::commands::is_track_cached, + offline_cache::commands::get_cached_track_path, + offline_cache::commands::get_cached_track, + offline_cache::commands::get_cached_tracks, + offline_cache::commands::get_offline_cache_stats, + offline_cache::commands::remove_cached_track, + offline_cache::commands::clear_offline_cache, + offline_cache::commands::set_offline_cache_limit, + offline_cache::commands::open_offline_cache_folder, + offline_cache::commands::open_album_folder, + offline_cache::commands::open_track_folder, + offline_cache::commands::check_album_fully_cached, + offline_cache::commands::check_offline_root_mounted, + offline_cache::commands::validate_offline_path, + offline_cache::commands::move_offline_cache_to_path, + offline_cache::commands::detect_legacy_cached_files, + offline_cache::commands::start_legacy_migration, + offline_cache::commands::sync_offline_cache_to_library, // Lyrics commands lyrics::commands::lyrics_get, lyrics::commands::lyrics_get_cache_stats, diff --git a/src-tauri/src/library/commands.rs b/src-tauri/src/library/commands.rs index dd9841fb4..e1d5417f9 100644 --- a/src-tauri/src/library/commands.rs +++ b/src-tauri/src/library/commands.rs @@ -1443,7 +1443,7 @@ pub struct BackfillReport { #[tauri::command] pub async fn library_backfill_downloads( state: State<'_, LibraryState>, - download_cache_state: State<'_, crate::download_cache::DownloadCacheState>, + offline_cache_state: State<'_, crate::offline_cache::OfflineCacheState>, ) -> Result { log::info!("Command: library_backfill_downloads"); @@ -1455,14 +1455,14 @@ pub async fn library_backfill_downloads( failed_tracks: Vec::new(), }; - // Get all ready downloads directly from download cache DB - let downloads = { - let cache_db = download_cache_state.db.lock().await; + // Get all ready cached tracks directly from offline cache DB + let cached_tracks = { + let cache_db = offline_cache_state.db.lock().await; let mut stmt = cache_db .conn() .prepare("SELECT track_id, title, artist, album, album_id, duration_secs, file_path, quality, bit_depth, sample_rate FROM cached_tracks WHERE status = 'ready'") - .map_err(|e| format!("Failed to query downloads: {}", e))?; + .map_err(|e| format!("Failed to query cached tracks: {}", e))?; let rows = stmt .query_map([], |row| { @@ -1480,14 +1480,14 @@ pub async fn library_backfill_downloads( .map_err(|e| format!("Failed to map rows: {}", e))?; rows.collect::, _>>() - .map_err(|e| format!("Failed to collect downloads: {}", e))? + .map_err(|e| format!("Failed to collect cached tracks: {}", e))? }; // cache_db lock is dropped here - report.total_downloads = downloads.len(); + report.total_downloads = cached_tracks.len(); let library_db = state.db.lock().await; - for (track_id, title, artist, album, duration_secs, file_path, bit_depth, sample_rate) in downloads { + for (track_id, title, artist, album, duration_secs, file_path, bit_depth, sample_rate) in cached_tracks { // Strategy: Try to match by qobuz_track_id first, then by file_path // This handles both intact downloads and downloads damaged by scanner @@ -1502,15 +1502,15 @@ pub async fn library_backfill_downloads( if exists_by_id { // Track exists with correct qobuz_track_id (not damaged) // Check if it just needs source repair - match library_db.is_qobuz_download_by_path(&file_path) { + match library_db.is_qobuz_cached_track_by_path(&file_path) { Ok(true) => { - // Already marked as qobuz_download, nothing to do + // Already marked as cached track, nothing to do report.skipped_tracks += 1; }, Ok(false) => { // Has qobuz_track_id but lost source marker - unusual case log::info!("Repairing source for track with intact ID {}: {}", track_id, title); - match library_db.repair_qobuz_download_by_path(track_id, &file_path) { + match library_db.repair_qobuz_cached_track_by_path(track_id, &file_path) { Ok(true) => report.repaired_tracks += 1, Ok(false) => report.skipped_tracks += 1, Err(e) => { @@ -1520,7 +1520,7 @@ pub async fn library_backfill_downloads( } }, Err(e) => { - log::warn!("Failed to check download status for {}: {}", track_id, e); + log::warn!("Failed to check cached track status for {}: {}", track_id, e); report.failed_tracks.push(title); } } @@ -1529,8 +1529,8 @@ pub async fn library_backfill_downloads( if exists_by_path { // Track exists by path but lost qobuz_track_id (damaged by scanner) - log::info!("Repairing damaged download (lost ID) {}: {}", track_id, title); - match library_db.repair_qobuz_download_by_path(track_id, &file_path) { + log::info!("Repairing damaged cached track (lost ID) {}: {}", track_id, title); + match library_db.repair_qobuz_cached_track_by_path(track_id, &file_path) { Ok(true) => report.repaired_tracks += 1, Ok(false) => report.skipped_tracks += 1, Err(e) => { @@ -1542,7 +1542,7 @@ pub async fn library_backfill_downloads( } // Track doesn't exist - insert as new - match library_db.insert_qobuz_download_direct( + match library_db.insert_qobuz_cached_track_direct( track_id, &title, &artist, diff --git a/src-tauri/src/library/database.rs b/src-tauri/src/library/database.rs index 4e28db49c..2773c862c 100644 --- a/src-tauri/src/library/database.rs +++ b/src-tauri/src/library/database.rs @@ -719,9 +719,9 @@ impl LibraryDatabase { // === Track Management === - /// Check if a file path is already registered as a Qobuz download - /// Returns true if the file exists with source = 'qobuz_download' - pub fn is_qobuz_download_by_path(&self, file_path: &str) -> Result { + /// Check if a file path is already registered as a Qobuz cached track + /// Returns true if the file exists with source = 'qobuz_download' (legacy name kept for DB compatibility) + pub fn is_qobuz_cached_track_by_path(&self, file_path: &str) -> Result { let count: i64 = self.conn .query_row( "SELECT COUNT(*) FROM local_tracks WHERE file_path = ?1 AND source = 'qobuz_download'", @@ -732,11 +732,11 @@ impl LibraryDatabase { Ok(count > 0) } - /// Insert or update a track (skips if file is already a Qobuz download) + /// Insert or update a track (skips if file is already a Qobuz cached track) pub fn insert_track(&self, track: &LocalTrack) -> Result { - // Don't overwrite Qobuz downloads with scanned data - if self.is_qobuz_download_by_path(&track.file_path)? { - log::debug!("Skipping track insert - already exists as Qobuz download: {}", track.file_path); + // Don't overwrite Qobuz cached tracks with scanned data + if self.is_qobuz_cached_track_by_path(&track.file_path)? { + log::debug!("Skipping track insert - already exists as Qobuz cached track: {}", track.file_path); // Return the existing ID return self.conn .query_row( @@ -2120,7 +2120,8 @@ impl LibraryDatabase { /// Repair a track by file_path - restores both qobuz_track_id and source /// This handles tracks that were damaged by scanner's INSERT OR REPLACE /// Returns true if the track was found and updated - pub fn repair_qobuz_download_by_path( + /// Note: Database source field remains 'qobuz_download' for compatibility + pub fn repair_qobuz_cached_track_by_path( &self, qobuz_track_id: u64, file_path: &str, @@ -2131,7 +2132,7 @@ impl LibraryDatabase { WHERE file_path = ?2 AND (source IS NULL OR source != 'qobuz_download')", params![qobuz_track_id as i64, file_path], ) - .map_err(|e| LibraryError::Database(format!("Failed to repair download by path: {}", e)))?; + .map_err(|e| LibraryError::Database(format!("Failed to repair cached track by path: {}", e)))?; Ok(updated > 0) } @@ -2147,8 +2148,9 @@ impl LibraryDatabase { Ok(count > 0) } - /// Insert a Qobuz download into the library - pub fn insert_qobuz_download_direct( + /// Insert a Qobuz cached track into the library + /// Note: Database source field remains 'qobuz_download' for compatibility + pub fn insert_qobuz_cached_track_direct( &self, track_id: u64, title: &str, @@ -2202,12 +2204,13 @@ impl LibraryDatabase { track_id as i64, ], ) - .map_err(|e| LibraryError::Database(format!("Failed to insert Qobuz download: {}", e)))?; + .map_err(|e| LibraryError::Database(format!("Failed to insert Qobuz cached track: {}", e)))?; Ok(()) } - /// Insert a Qobuz download with full metadata and album grouping - pub fn insert_qobuz_download_with_grouping( + /// Insert a Qobuz cached track with full metadata and album grouping + /// Note: Database source field remains 'qobuz_download' for compatibility + pub fn insert_qobuz_cached_track_with_grouping( &self, track_id: u64, title: &str, @@ -2228,7 +2231,7 @@ impl LibraryDatabase { use std::time::SystemTime; // First, remove any existing entry for this qobuz_track_id to prevent duplicates - let _ = self.remove_qobuz_download(track_id); + let _ = self.remove_qobuz_cached_track(track_id); // Get file size if file exists let file_size_bytes = std::fs::metadata(file_path) @@ -2276,27 +2279,29 @@ impl LibraryDatabase { track_id as i64, ], ) - .map_err(|e| LibraryError::Database(format!("Failed to insert Qobuz download: {}", e)))?; + .map_err(|e| LibraryError::Database(format!("Failed to insert Qobuz cached track: {}", e)))?; Ok(()) } - /// Remove a Qobuz download from the library by track_id - pub fn remove_qobuz_download(&self, qobuz_track_id: u64) -> Result<(), LibraryError> { + /// Remove a Qobuz cached track from the library by track_id + /// Note: Database source field remains 'qobuz_download' for compatibility + pub fn remove_qobuz_cached_track(&self, qobuz_track_id: u64) -> Result<(), LibraryError> { self.conn.execute( "DELETE FROM local_tracks WHERE qobuz_track_id = ?1 AND source = 'qobuz_download'", params![qobuz_track_id as i64], ) - .map_err(|e| LibraryError::Database(format!("Failed to remove Qobuz download: {}", e)))?; + .map_err(|e| LibraryError::Database(format!("Failed to remove Qobuz cached track: {}", e)))?; Ok(()) } - /// Remove all Qobuz downloads from the library - pub fn remove_all_qobuz_downloads(&self) -> Result { + /// Remove all Qobuz cached tracks from the library + /// Note: Database source field remains 'qobuz_download' for compatibility + pub fn remove_all_qobuz_cached_tracks(&self) -> Result { let count = self.conn.execute( "DELETE FROM local_tracks WHERE source = 'qobuz_download'", [], ) - .map_err(|e| LibraryError::Database(format!("Failed to remove all Qobuz downloads: {}", e)))?; + .map_err(|e| LibraryError::Database(format!("Failed to remove all Qobuz cached tracks: {}", e)))?; Ok(count) } diff --git a/src-tauri/src/download_cache/commands.rs b/src-tauri/src/offline_cache/commands.rs similarity index 73% rename from src-tauri/src/download_cache/commands.rs rename to src-tauri/src/offline_cache/commands.rs index ad00b548b..5c6dbbdae 100644 --- a/src-tauri/src/download_cache/commands.rs +++ b/src-tauri/src/offline_cache/commands.rs @@ -1,4 +1,4 @@ -//! Tauri commands for download cache functionality +//! Tauri commands for offline cache functionality use std::sync::Arc; use tauri::{AppHandle, Emitter, State}; @@ -6,23 +6,23 @@ use tauri::{AppHandle, Emitter, State}; use crate::api::models::Quality; use crate::AppState; -use crate::download_cache::path_validator::{self, PathValidationResult}; -use crate::download_cache::{DownloadCacheDb, DownloadCacheState}; -use crate::download_cache::metadata::{fetch_complete_metadata, write_flac_tags, embed_artwork, organize_download, save_album_artwork}; +use crate::offline_cache::path_validator::{self, PathValidationResult}; +use crate::offline_cache::{OfflineCacheDb, OfflineCacheState}; +use crate::offline_cache::metadata::{fetch_complete_metadata, write_flac_tags, embed_artwork, organize_cached_file, save_album_artwork}; use super::{ - CachedTrackInfo, DownloadCacheStats, DownloadStatus, - TrackDownloadInfo, + CachedTrackInfo, OfflineCacheStats, OfflineCacheStatus, + TrackCacheInfo, }; -/// Post-process a downloaded track: fetch metadata, tag FLAC, embed artwork, organize files -async fn post_process_track( +/// Post-process a cached track: fetch metadata, tag FLAC, embed artwork, organize files +async fn post_process_cached_track( track_id: u64, current_path: &str, - download_root: &str, + offline_root: &str, qobuz_client: &crate::api::QobuzClient, library_db: Arc>, ) -> Result { - log::info!("Post-processing track {}", track_id); + log::info!("Post-processing cached track {}", track_id); // 1. Fetch complete metadata from Qobuz let metadata = fetch_complete_metadata(track_id, qobuz_client).await?; @@ -39,7 +39,7 @@ async fn post_process_track( } // 4. Organize file into artist/album structure - let new_path = organize_download(track_id, current_path, download_root, &metadata)?; + let new_path = organize_cached_file(track_id, current_path, offline_root, &metadata)?; // 5. Download and save album cover art as cover.jpg let artwork_path = if let Some(artwork_url) = &metadata.artwork_url { @@ -86,7 +86,7 @@ async fn post_process_track( let album_artist = metadata.album_artist.as_ref().unwrap_or(&metadata.artist); let album_group_key = format!("{}|{}", metadata.album, album_artist); - match lib_guard.insert_qobuz_download_with_grouping( + match lib_guard.insert_qobuz_cached_track_with_grouping( track_id, &metadata.title, &metadata.artist, @@ -111,9 +111,9 @@ async fn post_process_track( Ok(new_path) } -/// Download a track for offline listening +/// Cache a track for offline playback #[tauri::command] -pub async fn download_track( +pub async fn cache_track_for_offline( track_id: u64, title: String, artist: String, @@ -124,13 +124,13 @@ pub async fn download_track( bit_depth: Option, sample_rate: Option, state: State<'_, AppState>, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, app_handle: AppHandle, ) -> Result<(), String> { - log::info!("Command: download_track {} - {} by {}", track_id, title, artist); + log::info!("Command: cache_track_for_offline {} - {} by {}", track_id, title, artist); - let track_info = TrackDownloadInfo { + let track_info = TrackCacheInfo { track_id, title, artist, @@ -154,40 +154,40 @@ pub async fn download_track( // Clone what we need for the spawn let client = state.client.clone(); - let downloader = cache_state.downloader.clone(); + let fetcher = cache_state.fetcher.clone(); let db = cache_state.db.clone(); - let download_root = cache_state.get_cache_path(); + let offline_root = cache_state.get_cache_path(); let library_db = library_state.db.clone(); let app = app_handle.clone(); - let semaphore = cache_state.download_semaphore.clone(); + let semaphore = cache_state.cache_semaphore.clone(); - // Spawn download task + // Spawn caching task tokio::spawn(async move { let _permit = match semaphore.acquire_owned().await { Ok(permit) => permit, Err(err) => { - log::error!("Failed to acquire download slot for track {}: {}", track_id, err); + log::error!("Failed to acquire cache slot for track {}: {}", track_id, err); let db_guard = db.lock().await; let _ = db_guard.update_status( track_id, - DownloadStatus::Failed, - Some("Failed to start download"), + OfflineCacheStatus::Failed, + Some("Failed to start caching"), ); - let _ = app.emit("download:failed", serde_json::json!({ + let _ = app.emit("offline:caching_failed", serde_json::json!({ "trackId": track_id, - "error": "Failed to acquire download slot" + "error": "Failed to acquire cache slot" })); return; } }; - // Update status to downloading + // Update status to caching { let db_guard = db.lock().await; - let _ = db_guard.update_status(track_id, DownloadStatus::Downloading, None); + let _ = db_guard.update_status(track_id, OfflineCacheStatus::Downloading, None); } - let _ = app.emit("download:started", serde_json::json!({ + let _ = app.emit("offline:caching_started", serde_json::json!({ "trackId": track_id })); @@ -206,10 +206,10 @@ pub async fn download_track( let db_guard = db.lock().await; let _ = db_guard.update_status( track_id, - DownloadStatus::Failed, + OfflineCacheStatus::Failed, Some(&format!("Failed to get stream URL: {}", e)), ); - let _ = app.emit("download:failed", serde_json::json!({ + let _ = app.emit("offline:caching_failed", serde_json::json!({ "trackId": track_id, "error": e.to_string() })); @@ -217,32 +217,32 @@ pub async fn download_track( } }; - // Download the file - match downloader - .download_to_file(&url, &file_path, track_id, Some(&app)) + // Fetch and cache the file + match fetcher + .fetch_to_file(&url, &file_path, track_id, Some(&app)) .await { Ok(size) => { - log::info!("Download complete for track {}: {} bytes", track_id, size); + log::info!("Caching complete for track {}: {} bytes", track_id, size); { let db_guard = db.lock().await; let _ = db_guard.mark_complete(track_id, size); } - - let _ = app.emit("download:completed", serde_json::json!({ + + let _ = app.emit("offline:caching_completed", serde_json::json!({ "trackId": track_id, "size": size })); // Post-processing: metadata, tagging, artwork, organization - log::info!("Starting post-processing for track {}", track_id); - + log::info!("Starting post-processing for cached track {}", track_id); + let file_path_str = file_path.to_string_lossy().to_string(); let qobuz_client = client.lock().await; - match post_process_track( + match post_process_cached_track( track_id, &file_path_str, - &download_root, + &offline_root, &*qobuz_client, library_db.clone(), ).await { @@ -252,23 +252,23 @@ pub async fn download_track( if let Err(e) = db_guard.update_file_path(track_id, &new_path) { log::error!("Failed to update path for track {}: {}", track_id, e); } - - let _ = app.emit("download:processed", serde_json::json!({ + + let _ = app.emit("offline:caching_processed", serde_json::json!({ "trackId": track_id, "path": new_path })); } Err(e) => { - log::error!("Post-processing failed for track {}: {}", track_id, e); + log::error!("Post-processing failed for cached track {}: {}", track_id, e); // File still exists and is playable, just not organized } } } Err(e) => { - log::error!("Download failed for track {}: {}", track_id, e); + log::error!("Caching failed for track {}: {}", track_id, e); let db_guard = db.lock().await; - let _ = db_guard.update_status(track_id, DownloadStatus::Failed, Some(&e)); - let _ = app.emit("download:failed", serde_json::json!({ + let _ = db_guard.update_status(track_id, OfflineCacheStatus::Failed, Some(&e)); + let _ = app.emit("offline:caching_failed", serde_json::json!({ "trackId": track_id, "error": e })); @@ -281,9 +281,9 @@ pub async fn download_track( /// Check if a track is cached and ready for playback #[tauri::command] -pub async fn is_track_downloaded( +pub async fn is_track_cached( track_id: u64, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result { let db = cache_state.db.lock().await; db.is_cached(track_id) @@ -291,9 +291,9 @@ pub async fn is_track_downloaded( /// Get the local file path for a cached track #[tauri::command] -pub async fn get_downloaded_track_path( +pub async fn get_cached_track_path( track_id: u64, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result, String> { let db = cache_state.db.lock().await; db.get_file_path(track_id) @@ -301,9 +301,9 @@ pub async fn get_downloaded_track_path( /// Get info about a specific cached track #[tauri::command] -pub async fn get_downloaded_track( +pub async fn get_cached_track( track_id: u64, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result, String> { let db = cache_state.db.lock().await; db.get_track(track_id) @@ -311,31 +311,31 @@ pub async fn get_downloaded_track( /// Get all cached tracks #[tauri::command] -pub async fn get_downloaded_tracks( - cache_state: State<'_, DownloadCacheState>, +pub async fn get_cached_tracks( + cache_state: State<'_, OfflineCacheState>, ) -> Result, String> { let db = cache_state.db.lock().await; db.get_all_tracks() } -/// Get download cache statistics +/// Get offline cache statistics #[tauri::command] -pub async fn get_download_cache_stats( - cache_state: State<'_, DownloadCacheState>, -) -> Result { +pub async fn get_offline_cache_stats( + cache_state: State<'_, OfflineCacheState>, +) -> Result { let db = cache_state.db.lock().await; let limit = *cache_state.limit_bytes.lock().await; db.get_stats(&cache_state.get_cache_path(), limit) } -/// Remove a track from the download cache +/// Remove a track from the offline cache #[tauri::command] -pub async fn remove_downloaded_track( +pub async fn remove_cached_track( track_id: u64, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, ) -> Result<(), String> { - log::info!("Command: remove_downloaded_track {}", track_id); + log::info!("Command: remove_cached_track {}", track_id); let db = cache_state.db.lock().await; @@ -352,24 +352,24 @@ pub async fn remove_downloaded_track( // Also remove from library if it was added let library_db = library_state.db.lock().await; - let _ = library_db.remove_qobuz_download(track_id); + let _ = library_db.remove_qobuz_cached_track(track_id); Ok(()) } -/// Clear entire download cache +/// Clear entire offline cache #[tauri::command] -pub async fn clear_download_cache( - cache_state: State<'_, DownloadCacheState>, +pub async fn clear_offline_cache( + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, ) -> Result<(), String> { - log::info!("Command: clear_download_cache"); - purge_all_downloads(cache_state.inner(), library_state.inner()).await + log::info!("Command: clear_offline_cache"); + purge_all_cached_files(cache_state.inner(), library_state.inner()).await } -/// Clear entire download cache (internal helper) -pub async fn purge_all_downloads( - cache_state: &DownloadCacheState, +/// Clear entire offline cache (internal helper) +pub async fn purge_all_cached_files( + cache_state: &OfflineCacheState, library_state: &crate::library::commands::LibraryState, ) -> Result<(), String> { let db = cache_state.db.lock().await; @@ -396,23 +396,23 @@ pub async fn purge_all_downloads( } } - // Remove all Qobuz downloads from library + // Remove all Qobuz cached tracks from library let library_db = library_state.db.lock().await; let removed_count = library_db - .remove_all_qobuz_downloads() - .map_err(|e| format!("Failed to remove downloads from library: {}", e))?; - log::info!("Removed {} Qobuz downloads from library", removed_count); + .remove_all_qobuz_cached_tracks() + .map_err(|e| format!("Failed to remove cached tracks from library: {}", e))?; + log::info!("Removed {} Qobuz cached tracks from library", removed_count); Ok(()) } /// Set cache size limit #[tauri::command] -pub async fn set_download_cache_limit( +pub async fn set_offline_cache_limit( limit_mb: Option, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result<(), String> { - log::info!("Command: set_download_cache_limit {:?} MB", limit_mb); + log::info!("Command: set_offline_cache_limit {:?} MB", limit_mb); let limit_bytes = limit_mb.map(|mb| mb * 1024 * 1024); let mut limit = cache_state.limit_bytes.lock().await; @@ -429,10 +429,10 @@ pub async fn set_download_cache_limit( /// Open the cache folder in the system file manager #[tauri::command] -pub async fn open_download_cache_folder( - cache_state: State<'_, DownloadCacheState>, +pub async fn open_offline_cache_folder( + cache_state: State<'_, OfflineCacheState>, ) -> Result<(), String> { - log::info!("Command: open_download_cache_folder"); + log::info!("Command: open_offline_cache_folder"); let path = cache_state.cache_dir.clone(); @@ -450,12 +450,12 @@ pub async fn open_download_cache_folder( #[tauri::command] pub async fn open_album_folder( album_id: String, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result<(), String> { log::info!("Command: open_album_folder for album_id: {}", album_id); let db = cache_state.db.lock().await; - + // Get tracks for this album let tracks = db.get_all_tracks()?; let album_tracks: Vec<_> = tracks @@ -464,7 +464,7 @@ pub async fn open_album_folder( .collect(); if album_tracks.is_empty() { - return Err("No downloaded tracks found for this album".to_string()); + return Err("No cached tracks found for this album".to_string()); } // Get the first track's path and extract the album directory @@ -491,7 +491,7 @@ pub async fn open_album_folder( #[tauri::command] pub async fn open_track_folder( track_id: u64, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result<(), String> { log::info!("Command: open_track_folder for track_id: {}", track_id); @@ -499,7 +499,7 @@ pub async fn open_track_folder( // Get the track's file path let file_path = db.get_file_path(track_id)? - .ok_or_else(|| "Track file path not found - track may not be downloaded".to_string())?; + .ok_or_else(|| "Track file path not found - track may not be cached".to_string())?; let track_path = std::path::Path::new(&file_path); let track_dir = track_path @@ -516,14 +516,14 @@ pub async fn open_track_folder( Ok(()) } -/// Check if an album is fully downloaded (all tracks ready) +/// Check if an album is fully cached (all tracks ready) #[tauri::command] -pub async fn check_album_fully_downloaded( +pub async fn check_album_fully_cached( album_id: String, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> 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 @@ -534,20 +534,20 @@ pub async fn check_album_fully_downloaded( 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 { + if track.status != crate::offline_cache::OfflineCacheStatus::Ready { return Ok(false); } } - + Ok(true) } /// Evict tracks if cache exceeds limit (LRU policy) async fn evict_if_needed( - cache_state: &DownloadCacheState, + cache_state: &OfflineCacheState, limit_bytes: u64, ) -> Result<(), String> { let db = cache_state.db.lock().await; @@ -586,42 +586,42 @@ async fn evict_if_needed( // Path management commands #[tauri::command] -pub async fn check_download_root_mounted( - cache_state: State<'_, DownloadCacheState>, +pub async fn check_offline_root_mounted( + cache_state: State<'_, OfflineCacheState>, ) -> Result { - log::info!("Command: check_download_root_mounted"); - + log::info!("Command: check_offline_root_mounted"); + let root_path = cache_state.cache_dir.to_string_lossy().to_string(); - super::path_validator::is_download_root_available(&root_path) + super::path_validator::is_offline_root_available(&root_path) } #[tauri::command] -pub async fn validate_download_path(path: String) -> Result { - log::info!("Command: validate_download_path: {}", path); +pub async fn validate_offline_path(path: String) -> Result { + log::info!("Command: validate_offline_path: {}", path); super::path_validator::validate_path(&path) } #[tauri::command] -pub async fn move_downloads_to_path( +pub async fn move_offline_cache_to_path( new_path: String, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, ) -> Result { - log::info!("Command: move_downloads_to_path: {}", new_path); - + log::info!("Command: move_offline_cache_to_path: {}", new_path); + let old_path = cache_state.cache_dir.to_string_lossy().to_string(); - super::path_validator::move_downloads_to_new_path(&old_path, &new_path) + super::path_validator::move_cached_files_to_new_path(&old_path, &new_path) } -/// Detect legacy downloads (numeric FLAC files) +/// Detect legacy cached files (numeric FLAC files) #[tauri::command] -pub async fn detect_legacy_downloads( - cache_state: State<'_, DownloadCacheState>, +pub async fn detect_legacy_cached_files( + cache_state: State<'_, OfflineCacheState>, ) -> Result { - log::info!("Command: detect_legacy_downloads"); - + log::info!("Command: detect_legacy_cached_files"); + let tracks_dir = cache_state.cache_dir.join("tracks"); - - match super::detect_legacy_downloads(&tracks_dir) { + + match super::detect_legacy_cached_files(&tracks_dir) { Ok(track_ids) => { Ok(super::MigrationStatus { has_legacy_files: !track_ids.is_empty(), @@ -633,34 +633,34 @@ pub async fn detect_legacy_downloads( } } -/// Start migration of legacy downloads +/// Start migration of legacy cached files #[tauri::command] pub async fn start_legacy_migration( state: State<'_, AppState>, - cache_state: State<'_, DownloadCacheState>, + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, app_handle: AppHandle, ) -> Result<(), String> { log::info!("Command: start_legacy_migration"); - + let tracks_dir = cache_state.cache_dir.join("tracks"); - let track_ids = super::detect_legacy_downloads(&tracks_dir)?; - + let track_ids = super::detect_legacy_cached_files(&tracks_dir)?; + if track_ids.is_empty() { - return Err("No legacy downloads found".to_string()); + return Err("No legacy cached files found".to_string()); } - - let download_root = cache_state.get_cache_path(); + + let offline_root = cache_state.get_cache_path(); let qobuz_client = state.client.clone(); let library_db = library_state.db.clone(); let app_complete = app_handle.clone(); - + // Spawn migration task tokio::spawn(async move { - let status = super::migrate_legacy_downloads( + let status = super::migrate_legacy_cached_files( track_ids, tracks_dir, - download_root, + offline_root, qobuz_client, library_db, ).await; @@ -671,19 +671,19 @@ pub async fn start_legacy_migration( Ok(()) } -/// Sync downloaded tracks to library database -/// This ensures all ready downloads appear in the library with source='qobuz_download' +/// Sync cached tracks to library database +/// This ensures all ready cached tracks appear in the library with source='qobuz_cached' #[tauri::command] -pub async fn sync_downloads_to_library( - cache_state: State<'_, DownloadCacheState>, +pub async fn sync_offline_cache_to_library( + cache_state: State<'_, OfflineCacheState>, library_state: State<'_, crate::library::commands::LibraryState>, ) -> Result { - log::info!("Command: sync_downloads_to_library"); + log::info!("Command: sync_offline_cache_to_library"); let cache_db = cache_state.db.lock().await; let library_db = library_state.db.lock().await; - // Get all ready tracks from download cache + // Get all ready tracks from offline cache let ready_tracks = cache_db.get_ready_tracks_for_sync()?; let mut synced = 0; @@ -698,7 +698,7 @@ pub async fn sync_downloads_to_library( } Ok(false) => { // Insert into library with basic metadata - match library_db.insert_qobuz_download_direct( + match library_db.insert_qobuz_cached_track_direct( track.track_id, &track.title, &track.artist, diff --git a/src-tauri/src/download_cache/db.rs b/src-tauri/src/offline_cache/db.rs similarity index 93% rename from src-tauri/src/download_cache/db.rs rename to src-tauri/src/offline_cache/db.rs index 8ce6512ab..c812905fc 100644 --- a/src-tauri/src/download_cache/db.rs +++ b/src-tauri/src/offline_cache/db.rs @@ -1,20 +1,20 @@ -//! SQLite database for download cache index +//! SQLite database for offline cache index use rusqlite::{Connection, params}; use std::path::Path; -use super::{CachedTrackInfo, DownloadCacheStats, DownloadStatus, ReadyTrackForSync, TrackDownloadInfo}; +use super::{CachedTrackInfo, OfflineCacheStats, OfflineCacheStatus, ReadyTrackForSync, TrackCacheInfo}; /// Database wrapper for cached tracks index -pub struct DownloadCacheDb { +pub struct OfflineCacheDb { conn: Connection, } -impl DownloadCacheDb { +impl OfflineCacheDb { /// Open or create the database pub fn new(path: &Path) -> Result { let conn = Connection::open(path) - .map_err(|e| format!("Failed to open download cache database: {}", e))?; + .map_err(|e| format!("Failed to open offline cache database: {}", e))?; let db = Self { conn }; db.init_schema()?; @@ -61,8 +61,8 @@ impl DownloadCacheDb { Ok(()) } - /// Insert a new track to download - pub fn insert_track(&self, info: &TrackDownloadInfo, file_path: &str) -> Result<(), String> { + /// Insert a new track to cache for offline + pub fn insert_track(&self, info: &TrackCacheInfo, file_path: &str) -> Result<(), String> { self.conn.execute( "INSERT OR REPLACE INTO cached_tracks (track_id, title, artist, album, album_id, duration_secs, file_path, quality, bit_depth, sample_rate, status, progress_percent, created_at, last_accessed_at) @@ -85,7 +85,7 @@ impl DownloadCacheDb { } /// Update track status - pub fn update_status(&self, track_id: u64, status: DownloadStatus, error: Option<&str>) -> Result<(), String> { + pub fn update_status(&self, track_id: u64, status: OfflineCacheStatus, error: Option<&str>) -> Result<(), String> { self.conn.execute( "UPDATE cached_tracks SET status = ?1, error_message = ?2 WHERE track_id = ?3", params![status.as_str(), error, track_id as i64], @@ -94,7 +94,7 @@ impl DownloadCacheDb { Ok(()) } - /// Update download progress + /// Update caching progress pub fn update_progress(&self, track_id: u64, progress: u8, size_bytes: u64) -> Result<(), String> { self.conn.execute( "UPDATE cached_tracks SET progress_percent = ?1, file_size_bytes = ?2 WHERE track_id = ?3", @@ -104,7 +104,7 @@ impl DownloadCacheDb { Ok(()) } - /// Mark download as complete + /// Mark caching as complete pub fn mark_complete(&self, track_id: u64, file_size: u64) -> Result<(), String> { self.conn.execute( "UPDATE cached_tracks SET status = 'ready', progress_percent = 100, file_size_bytes = ?1, last_accessed_at = datetime('now') WHERE track_id = ?2", @@ -150,7 +150,7 @@ impl DownloadCacheDb { } } - /// Get all ready (downloaded) tracks with their file paths for syncing to library + /// Get all ready (cached) tracks with their file paths for syncing to library pub fn get_ready_tracks_for_sync(&self) -> Result, String> { let mut stmt = self.conn.prepare( "SELECT track_id, title, artist, album, duration_secs, file_path, bit_depth, sample_rate @@ -196,7 +196,7 @@ impl DownloadCacheDb { quality: row.get::<_, Option>(7)?.unwrap_or_default(), bit_depth: row.get::<_, Option>(8)?.map(|v| v as u32), sample_rate: row.get(9)?, - status: DownloadStatus::from_str(&row.get::<_, String>(10)?), + status: OfflineCacheStatus::from_str(&row.get::<_, String>(10)?), progress_percent: row.get::<_, i64>(11)? as u8, error_message: row.get(12)?, created_at: row.get(13)?, @@ -231,7 +231,7 @@ impl DownloadCacheDb { quality: row.get::<_, Option>(7)?.unwrap_or_default(), bit_depth: row.get::<_, Option>(8)?.map(|v| v as u32), sample_rate: row.get(9)?, - status: DownloadStatus::from_str(&row.get::<_, String>(10)?), + status: OfflineCacheStatus::from_str(&row.get::<_, String>(10)?), progress_percent: row.get::<_, i64>(11)? as u8, error_message: row.get(12)?, created_at: row.get(13)?, @@ -265,7 +265,7 @@ impl DownloadCacheDb { } /// Get statistics - pub fn get_stats(&self, cache_path: &str, limit_bytes: Option) -> Result { + pub fn get_stats(&self, cache_path: &str, limit_bytes: Option) -> Result { let total_tracks: i64 = self.conn.query_row( "SELECT COUNT(*) FROM cached_tracks", [], @@ -296,7 +296,7 @@ impl DownloadCacheDb { |row| row.get(0), ).map_err(|e| format!("Failed to sum sizes: {}", e))?; - Ok(DownloadCacheStats { + Ok(OfflineCacheStats { total_tracks: total_tracks as usize, ready_tracks: ready_tracks as usize, downloading_tracks: downloading_tracks as usize, diff --git a/src-tauri/src/download_cache/downloader.rs b/src-tauri/src/offline_cache/downloader.rs similarity index 70% rename from src-tauri/src/download_cache/downloader.rs rename to src-tauri/src/offline_cache/downloader.rs index 4e84b300f..148c3cca5 100644 --- a/src-tauri/src/download_cache/downloader.rs +++ b/src-tauri/src/offline_cache/downloader.rs @@ -1,18 +1,18 @@ -//! Download logic for caching tracks to disk +//! Stream fetcher for caching tracks to disk use std::io::Write; use std::path::Path; use std::time::Duration; use tauri::{AppHandle, Emitter}; -use super::{DownloadProgress, DownloadStatus}; +use super::{CacheProgress, OfflineCacheStatus}; -/// Downloader handles fetching audio files and saving them to disk -pub struct Downloader { +/// StreamFetcher handles fetching audio streams and caching them to disk +pub struct StreamFetcher { client: reqwest::Client, } -impl Downloader { +impl StreamFetcher { pub fn new() -> Self { let client = reqwest::Client::builder() .timeout(Duration::from_secs(300)) // 5 minute timeout for large files @@ -23,15 +23,15 @@ impl Downloader { Self { client } } - /// Download a file to disk with progress updates - pub async fn download_to_file( + /// Fetch a stream and cache it to disk with progress updates + pub async fn fetch_to_file( &self, url: &str, dest_path: &Path, track_id: u64, app_handle: Option<&AppHandle>, ) -> Result { - log::info!("Downloading track {} to {:?}", track_id, dest_path); + log::info!("Caching track {} to {:?}", track_id, dest_path); // Create parent directories if needed if let Some(parent) = dest_path.parent() { @@ -39,14 +39,14 @@ impl Downloader { .map_err(|e| format!("Failed to create directory: {}", e))?; } - // Start the download + // Start fetching let response = self .client .get(url) .header("User-Agent", "Mozilla/5.0") .send() .await - .map_err(|e| format!("Failed to start download: {}", e))?; + .map_err(|e| format!("Failed to start fetch: {}", e))?; if !response.status().is_success() { return Err(format!("HTTP error: {}", response.status())); @@ -54,17 +54,17 @@ impl Downloader { let total_size = response.content_length(); log::info!( - "Download started for track {}, total size: {:?} bytes", + "Caching started for track {}, total size: {:?} bytes", track_id, total_size ); - // Create temp file for downloading + // Create temp file for caching let temp_path = dest_path.with_extension("tmp"); let mut file = std::fs::File::create(&temp_path) .map_err(|e| format!("Failed to create temp file: {}", e))?; - let mut downloaded: u64 = 0; + let mut cached: u64 = 0; let mut last_progress: u8 = 0; // Stream the response body @@ -72,18 +72,18 @@ impl Downloader { use futures_util::StreamExt; while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result.map_err(|e| format!("Download error: {}", e))?; + let chunk = chunk_result.map_err(|e| format!("Fetch error: {}", e))?; file.write_all(&chunk) .map_err(|e| format!("Failed to write chunk: {}", e))?; - downloaded += chunk.len() as u64; + cached += chunk.len() as u64; // Calculate progress let progress = if let Some(total) = total_size { - ((downloaded as f64 / total as f64) * 100.0) as u8 + ((cached as f64 / total as f64) * 100.0) as u8 } else { - // If we don't know total size, report bytes downloaded + // If we don't know total size, report bytes cached 0 }; @@ -93,22 +93,22 @@ impl Downloader { if let Some(app) = app_handle { let _ = app.emit( - "download:progress", - DownloadProgress { + "offline:caching_progress", + CacheProgress { track_id, progress_percent: progress, - bytes_downloaded: downloaded, + bytes_downloaded: cached, total_bytes: total_size, - status: DownloadStatus::Downloading, + status: OfflineCacheStatus::Downloading, }, ); } log::debug!( - "Download progress for track {}: {}% ({}/{:?} bytes)", + "Caching progress for track {}: {}% ({}/{:?} bytes)", track_id, progress, - downloaded, + cached, total_size ); } @@ -124,16 +124,16 @@ impl Downloader { .map_err(|e| format!("Failed to move temp file: {}", e))?; log::info!( - "Download complete for track {}: {} bytes", + "Caching complete for track {}: {} bytes", track_id, - downloaded + cached ); - Ok(downloaded) + Ok(cached) } - /// Download to memory (for smaller files or streaming) - pub async fn download_to_memory(&self, url: &str) -> Result, String> { + /// Fetch to memory (for smaller files or streaming) + pub async fn fetch_to_memory(&self, url: &str) -> Result, String> { let response = self .client .get(url) @@ -155,7 +155,7 @@ impl Downloader { } } -impl Default for Downloader { +impl Default for StreamFetcher { fn default() -> Self { Self::new() } diff --git a/src-tauri/src/download_cache/metadata.rs b/src-tauri/src/offline_cache/metadata.rs similarity index 98% rename from src-tauri/src/download_cache/metadata.rs rename to src-tauri/src/offline_cache/metadata.rs index 99bf81033..157d3dc0e 100644 --- a/src-tauri/src/download_cache/metadata.rs +++ b/src-tauri/src/offline_cache/metadata.rs @@ -1,4 +1,4 @@ -//! Metadata fetching and FLAC tagging for downloaded tracks +//! Metadata fetching and FLAC tagging for cached tracks //! //! Handles complete metadata retrieval from Qobuz API and writing to FLAC tags. @@ -301,14 +301,14 @@ pub async fn save_album_artwork( Ok(()) } -/// Organize downloaded file into proper folder structure -pub fn organize_download( +/// Organize cached file into proper folder structure +pub fn organize_cached_file( track_id: u64, temp_path: &str, root_dir: &str, metadata: &CompleteTrackMetadata, ) -> Result { - log::info!("Organizing download for track {}", track_id); + log::info!("Organizing cached file for track {}", track_id); let temp = Path::new(temp_path); let root = Path::new(root_dir); diff --git a/src-tauri/src/download_cache/migration.rs b/src-tauri/src/offline_cache/migration.rs similarity index 88% rename from src-tauri/src/download_cache/migration.rs rename to src-tauri/src/offline_cache/migration.rs index 646473fc8..91f41769e 100644 --- a/src-tauri/src/download_cache/migration.rs +++ b/src-tauri/src/offline_cache/migration.rs @@ -1,4 +1,4 @@ -//! Legacy download migration service +//! Legacy cached file migration service //! //! Handles migration of old numeric-named FLAC files to new organized structure @@ -9,7 +9,7 @@ use tokio::sync::Mutex; use crate::api::QobuzClient; use crate::library::database::LibraryDatabase; -use super::metadata::{fetch_complete_metadata, write_flac_tags, embed_artwork, organize_download, save_album_artwork}; +use super::metadata::{fetch_complete_metadata, write_flac_tags, embed_artwork, organize_cached_file, save_album_artwork}; #[derive(Default, Serialize, Clone, Debug)] pub struct MigrationStatus { @@ -29,26 +29,26 @@ pub struct MigrationError { pub error_message: String, } -/// Detect legacy downloads (numeric FLAC files in tracks/ folder) -pub fn detect_legacy_downloads(tracks_dir: &Path) -> Result, String> { - log::info!("Scanning for legacy downloads in: {}", tracks_dir.display()); - +/// Detect legacy cached files (numeric FLAC files in tracks/ folder) +pub fn detect_legacy_cached_files(tracks_dir: &Path) -> Result, String> { + log::info!("Scanning for legacy cached files in: {}", tracks_dir.display()); + if !tracks_dir.exists() { return Ok(Vec::new()); } - + let mut track_ids = Vec::new(); - + match std::fs::read_dir(tracks_dir) { Ok(entries) => { for entry in entries.flatten() { let path = entry.path(); - + // Only process FLAC files if path.extension().and_then(|s| s.to_str()) != Some("flac") { continue; } - + // Check if filename is purely numeric (track_id) if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) { if let Ok(track_id) = filename.parse::() { @@ -62,38 +62,38 @@ pub fn detect_legacy_downloads(tracks_dir: &Path) -> Result, String> { return Err(format!("Failed to read tracks directory: {}", e)); } } - - log::info!("Found {} legacy downloads", track_ids.len()); + + log::info!("Found {} legacy cached files", track_ids.len()); Ok(track_ids) } -/// Migrate a single legacy track +/// Migrate a single legacy cached track async fn migrate_single_track( track_id: u64, legacy_path: PathBuf, - download_root: &str, + offline_root: &str, qobuz_client: &QobuzClient, library_db: Arc>, ) -> Result { log::info!("Migrating track {}", track_id); - + // 1. Fetch complete metadata from Qobuz let metadata = fetch_complete_metadata(track_id, qobuz_client).await?; - + // 2. Write FLAC tags let legacy_path_str = legacy_path.to_string_lossy().to_string(); write_flac_tags(&legacy_path_str, &metadata) .map_err(|e| format!("Failed to write tags: {}", e))?; - + // 3. Embed artwork if available if let Some(artwork_url) = &metadata.artwork_url { if let Err(e) = embed_artwork(&legacy_path_str, artwork_url).await { log::warn!("Failed to embed artwork for track {}: {}", track_id, e); } } - + // 4. Organize file into artist/album structure - let new_path = organize_download(track_id, &legacy_path_str, download_root, &metadata)?; + let new_path = organize_cached_file(track_id, &legacy_path_str, offline_root, &metadata)?; // 5. Save album artwork as cover.jpg let artwork_path = if let Some(artwork_url) = &metadata.artwork_url { @@ -133,7 +133,7 @@ async fn migrate_single_track( let album_artist = metadata.album_artist.as_ref().unwrap_or(&metadata.artist); let album_group_key = format!("{}|{}", metadata.album, album_artist); - lib_guard.insert_qobuz_download_with_grouping( + lib_guard.insert_qobuz_cached_track_with_grouping( track_id, &metadata.title, &metadata.artist, @@ -156,11 +156,11 @@ async fn migrate_single_track( Ok(new_path) } -/// Migrate all legacy downloads -pub async fn migrate_legacy_downloads( +/// Migrate all legacy cached files +pub async fn migrate_legacy_cached_files( track_ids: Vec, tracks_dir: PathBuf, - download_root: String, + offline_root: String, qobuz_client: Arc>, library_db: Arc>, ) -> MigrationStatus { @@ -171,29 +171,29 @@ pub async fn migrate_legacy_downloads( in_progress: true, ..Default::default() }; - + for track_id in track_ids { let legacy_path = tracks_dir.join(format!("{}.flac", track_id)); - + if !legacy_path.exists() { log::warn!("Legacy file not found: {}", legacy_path.display()); status.processed += 1; continue; } - + // Lock client for this migration let client_guard = qobuz_client.lock().await; - + match migrate_single_track( track_id, legacy_path.clone(), - &download_root, + &offline_root, &*client_guard, library_db.clone(), ).await { Ok(_) => { status.successful += 1; - + // Delete legacy file after successful migration if let Err(e) = std::fs::remove_file(&legacy_path) { log::warn!("Failed to delete legacy file {}: {}", legacy_path.display(), e); @@ -208,21 +208,21 @@ pub async fn migrate_legacy_downloads( log::error!("Failed to migrate track {}: {}", track_id, status.errors.last().unwrap().error_message); } } - + drop(client_guard); - + status.processed += 1; } - + status.in_progress = false; status.completed = true; - + log::info!( "Migration complete: {}/{} successful, {} failed", status.successful, total, status.failed ); - + status } diff --git a/src-tauri/src/download_cache/mod.rs b/src-tauri/src/offline_cache/mod.rs similarity index 80% rename from src-tauri/src/download_cache/mod.rs rename to src-tauri/src/offline_cache/mod.rs index e643ff503..9d1065526 100644 --- a/src-tauri/src/download_cache/mod.rs +++ b/src-tauri/src/offline_cache/mod.rs @@ -1,4 +1,4 @@ -//! Download cache module for offline listening +//! Offline cache module for temporary playback data //! //! Provides persistent disk-based caching for audio tracks: //! - SQLite index for track metadata @@ -18,23 +18,23 @@ use std::sync::Arc; use tokio::sync::{Mutex, Semaphore}; use serde::{Deserialize, Serialize}; -pub use db::DownloadCacheDb; -pub use downloader::Downloader; -pub use path_validator::{is_download_root_available, validate_path, PathStatus}; +pub use db::OfflineCacheDb; +pub use downloader::StreamFetcher; +pub use path_validator::{is_offline_root_available, validate_path, PathStatus}; pub use metadata::{CompleteTrackMetadata, sanitize_filename}; -pub use migration::{MigrationStatus, MigrationError, detect_legacy_downloads, migrate_legacy_downloads}; +pub use migration::{MigrationStatus, MigrationError, detect_legacy_cached_files, migrate_legacy_cached_files}; -/// Download status for a cached track +/// Cache status for a track in offline storage #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum DownloadStatus { +pub enum OfflineCacheStatus { Queued, Downloading, Ready, Failed, } -impl DownloadStatus { +impl OfflineCacheStatus { pub fn as_str(&self) -> &'static str { match self { Self::Queued => "queued", @@ -69,7 +69,7 @@ pub struct CachedTrackInfo { pub quality: String, pub bit_depth: Option, pub sample_rate: Option, - pub status: DownloadStatus, + pub status: OfflineCacheStatus, pub progress_percent: u8, pub error_message: Option, pub created_at: String, @@ -89,10 +89,10 @@ pub struct ReadyTrackForSync { pub sample_rate: Option, } -/// Statistics about the download cache +/// Statistics about the offline cache #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct DownloadCacheStats { +pub struct OfflineCacheStats { pub total_tracks: usize, pub ready_tracks: usize, pub downloading_tracks: usize, @@ -102,20 +102,20 @@ pub struct DownloadCacheStats { pub cache_path: String, } -/// Progress update for a download +/// Progress update for caching a track #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct DownloadProgress { +pub struct CacheProgress { pub track_id: u64, pub progress_percent: u8, pub bytes_downloaded: u64, pub total_bytes: Option, - pub status: DownloadStatus, + pub status: OfflineCacheStatus, } -/// Track metadata for initiating a download +/// Track metadata for initiating offline caching #[derive(Debug, Clone)] -pub struct TrackDownloadInfo { +pub struct TrackCacheInfo { pub track_id: u64, pub title: String, pub artist: String, @@ -127,18 +127,18 @@ pub struct TrackDownloadInfo { pub sample_rate: Option, } -/// Download cache state manager -pub struct DownloadCacheState { - pub db: Arc>, - pub downloader: Arc, +/// Offline cache state manager +pub struct OfflineCacheState { + pub db: Arc>, + pub fetcher: Arc, pub cache_dir: PathBuf, /// Cache limit in bytes (None = unlimited) pub limit_bytes: Arc>>, - pub download_semaphore: Arc, + pub cache_semaphore: Arc, } -impl DownloadCacheState { - /// Initialize the download cache +impl OfflineCacheState { + /// Initialize the offline cache pub fn new() -> Result { let cache_dir = dirs::cache_dir() .ok_or("Could not determine cache directory")? @@ -154,20 +154,20 @@ impl DownloadCacheState { .map_err(|e| format!("Failed to create artwork directory: {}", e))?; let db_path = cache_dir.join("index.db"); - let db = DownloadCacheDb::new(&db_path)?; + let db = OfflineCacheDb::new(&db_path)?; // Default limit: 2GB let default_limit = Some(2 * 1024 * 1024 * 1024u64); let state = Self { db: Arc::new(Mutex::new(db)), - downloader: Arc::new(Downloader::new()), + fetcher: Arc::new(StreamFetcher::new()), cache_dir: cache_dir.clone(), limit_bytes: Arc::new(Mutex::new(default_limit)), - download_semaphore: Arc::new(Semaphore::new(3)), + cache_semaphore: Arc::new(Semaphore::new(3)), }; - log::info!("Download cache initialized at: {:?}", cache_dir); + log::info!("Offline cache initialized at: {:?}", cache_dir); Ok(state) } diff --git a/src-tauri/src/download_cache/path_validator.rs b/src-tauri/src/offline_cache/path_validator.rs similarity index 96% rename from src-tauri/src/download_cache/path_validator.rs rename to src-tauri/src/offline_cache/path_validator.rs index 65737f5eb..82612eff8 100644 --- a/src-tauri/src/download_cache/path_validator.rs +++ b/src-tauri/src/offline_cache/path_validator.rs @@ -1,4 +1,4 @@ -//! Download path validation and management +//! Offline cache path validation and management //! //! Handles path validation, permission checking, and mount status verification. @@ -21,7 +21,7 @@ pub struct PathValidationResult { pub message: String, } -/// Validate a download path +/// Validate an offline cache path pub fn validate_path(path: &str) -> Result { let path_obj = Path::new(path); @@ -115,8 +115,8 @@ pub struct MoveReport { pub failed_files: Vec, } -/// Move downloaded files from old path to new path -pub fn move_downloads_to_new_path( +/// Move cached files from old path to new path +pub fn move_cached_files_to_new_path( old_root: &str, new_root: &str, ) -> Result { @@ -212,7 +212,7 @@ static MOUNT_CACHE: Mutex> = Mutex::new(None); const CACHE_DURATION: Duration = Duration::from_secs(30); /// Check mount status with caching (30s cache) -pub fn is_download_root_available(path: &str) -> Result { +pub fn is_offline_root_available(path: &str) -> Result { let mut cache = MOUNT_CACHE.lock().map_err(|e| format!("Cache lock error: {}", e))?; if let Some(cached) = cache.as_ref() { diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index d16fced8a..2f474a8bb 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -461,8 +461,8 @@ // Detect if running in Flatpak loadFlatpakStatus(); - // Check for legacy downloads - checkLegacyDownloads(); + // Check for legacy cached files + checkLegacyCachedFiles(); // Subscribe to offline state changes const unsubscribeOffline = subscribeOffline(() => { @@ -1218,15 +1218,15 @@ } } - async function checkLegacyDownloads() { + async function checkLegacyCachedFiles() { try { - const result = await invoke<{has_legacy_files: boolean, total_tracks: number}>('detect_legacy_downloads'); + const result = await invoke<{has_legacy_files: boolean, total_tracks: number}>('detect_legacy_cached_files'); if (result.has_legacy_files && result.total_tracks > 0) { legacyTracksCount = result.total_tracks; showMigrationModal = true; } } catch (err) { - console.error('Failed to check for legacy downloads:', err); + console.error('Failed to check for legacy cached files:', err); } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index c4e788b83..325ba655b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1835,7 +1835,7 @@ void downloadStateVersion; try { - const isDownloaded = await invoke('check_album_fully_downloaded', { albumId }); + const isDownloaded = await invoke('check_album_fully_cached', { albumId }); albumDownloadCache.set(albumId, isDownloaded); return isDownloaded; } catch {