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:
1 parent
84a2164079
commit
6d5d264778
15 files changed
+348
-343
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+132
-132
@@ -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,
|
||||
+29
-29
@@ -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);
|
||||
+34
-34
@@ -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)
|
||||
}
|
||||
+5
-5
@@ -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() {
|
||||
Reference in new issue
Block a user