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
This commit is contained in:
vicrodh committed 2026-01-23 12:14:39 -06:00
1 parent 84a2164079
commit 6d5d264778
15 files changed
+348 -343

No files matched your search

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