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