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::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<LoginResponse, String> {
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<LoginResponse, String> {
// 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(),
+9 -9
View File
@@ -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))?;
+2 -2
View File
@@ -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<bool, String> {
pub fn should_purge_offline_cache(&self, now: i64) -> Result<bool, String> {
let state = self.get_state()?;
let Some(invalid_since) = state.invalid_since else { return Ok(false); };
if now - invalid_since < GRACE_PERIOD_SECS {
+31 -31
View File
@@ -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::<download_cache::DownloadCacheState>();
log::warn!("Subscription invalid for >3 days. Purging offline cache.");
let cache_state = app_handle.state::<offline_cache::OfflineCacheState>();
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 {
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,
+15 -15
View File
@@ -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<BackfillReport, String> {
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::<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
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,
+27 -22
View File
@@ -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<bool, LibraryError> {
/// 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<bool, LibraryError> {
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<i64, LibraryError> {
// 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<usize, LibraryError> {
/// 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<usize, LibraryError> {
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)
}
@@ -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<tokio::sync::Mutex<crate::library::database::LibraryDatabase>>,
) -> Result<String, String> {
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<u32>,
sample_rate: Option<f64>,
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<bool, String> {
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<Option<String>, 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<Option<CachedTrackInfo>, 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<Vec<CachedTrackInfo>, 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<DownloadCacheStats, String> {
pub async fn get_offline_cache_stats(
cache_state: State<'_, OfflineCacheState>,
) -> Result<OfflineCacheStats, String> {
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<u64>,
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<bool, String> {
let db = cache_state.db.lock().await;
// Get all tracks for this album
let tracks = db.get_all_tracks()?;
let album_tracks: Vec<_> = tracks
@@ -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<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();
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<super::path_validator::PathValidationResult, String> {
log::info!("Command: validate_download_path: {}", path);
pub async fn validate_offline_path(path: String) -> Result<super::path_validator::PathValidationResult, String> {
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<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();
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<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");
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<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 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,
@@ -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<Self, String> {
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<Vec<ReadyTrackForSync>, 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<String>>(7)?.unwrap_or_default(),
bit_depth: row.get::<_, Option<i64>>(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<String>>(7)?.unwrap_or_default(),
bit_depth: row.get::<_, Option<i64>>(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<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(
"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,
@@ -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<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
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<Vec<u8>, String> {
/// Fetch to memory (for smaller files or streaming)
pub async fn fetch_to_memory(&self, url: &str) -> Result<Vec<u8>, 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()
}
@@ -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<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 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
@@ -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<Vec<u64>, 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<Vec<u64>, 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::<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));
}
}
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<Mutex<LibraryDatabase>>,
) -> Result<String, String> {
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<u64>,
tracks_dir: PathBuf,
download_root: String,
offline_root: String,
qobuz_client: Arc<Mutex<QobuzClient>>,
library_db: Arc<Mutex<LibraryDatabase>>,
) -> 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
}
@@ -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<u32>,
pub sample_rate: Option<f64>,
pub status: DownloadStatus,
pub status: OfflineCacheStatus,
pub progress_percent: u8,
pub error_message: Option<String>,
pub created_at: String,
@@ -89,10 +89,10 @@ pub struct ReadyTrackForSync {
pub sample_rate: Option<f64>,
}
/// 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<u64>,
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<f64>,
}
/// Download cache state manager
pub struct DownloadCacheState {
pub db: Arc<Mutex<DownloadCacheDb>>,
pub downloader: Arc<Downloader>,
/// Offline cache state manager
pub struct OfflineCacheState {
pub db: Arc<Mutex<OfflineCacheDb>>,
pub fetcher: Arc<StreamFetcher>,
pub cache_dir: PathBuf,
/// Cache limit in bytes (None = unlimited)
pub limit_bytes: Arc<Mutex<Option<u64>>>,
pub download_semaphore: Arc<Semaphore>,
pub cache_semaphore: Arc<Semaphore>,
}
impl DownloadCacheState {
/// Initialize the download cache
impl OfflineCacheState {
/// Initialize the offline cache
pub fn new() -> Result<Self, String> {
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)
}
@@ -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<PathValidationResult, String> {
let path_obj = Path::new(path);
@@ -115,8 +115,8 @@ pub struct MoveReport {
pub failed_files: Vec<String>,
}
/// 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<MoveReport, String> {
@@ -212,7 +212,7 @@ static MOUNT_CACHE: Mutex<Option<MountStatusCache>> = 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<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))?;
if let Some(cached) = cache.as_ref() {