refactor: move download/decryption to backend to fix UI freezing

Now implements streaming download+decryption entirely in Rust:
- Added reqwest/tokio/futures-util dependencies
- Created StreamingDecryptor for chunk-by-chunk decryption
- New download_and_decrypt_track command streams to disk directly
- Frontend simplified to single invoke() call
This commit is contained in:
2025-10-04 20:53:59 -04:00
parent e4586f6497
commit 96a01bdced
5 changed files with 307 additions and 86 deletions

View File

@@ -36,7 +36,7 @@ fn read_audio_metadata(path: String) -> Result<metadata::AudioMetadata, String>
metadata::read_audio_metadata(&path)
}
/// Decrypt Deezer track data
/// Decrypt Deezer track data (legacy - kept for backwards compatibility)
#[tauri::command]
async fn decrypt_deezer_track(data: Vec<u8>, track_id: String) -> Result<Vec<u8>, String> {
// Run decryption on a background thread to avoid blocking the UI
@@ -49,6 +49,89 @@ async fn decrypt_deezer_track(data: Vec<u8>, track_id: String) -> Result<Vec<u8>
Ok(result)
}
/// Download and decrypt a Deezer track, streaming directly to disk
#[tauri::command]
async fn download_and_decrypt_track(
url: String,
track_id: String,
output_path: String,
is_encrypted: bool,
) -> Result<(), String> {
use tokio::io::AsyncWriteExt;
use tokio::fs::File;
use deezer_crypto::StreamingDecryptor;
// Build HTTP client
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
// Start download
let response = client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36")
.send()
.await
.map_err(|e| format!("Download failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()));
}
// Open output file
let mut file = File::create(&output_path)
.await
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Stream download with optional decryption
if is_encrypted {
let mut decryptor = StreamingDecryptor::new(&track_id);
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|e| format!("Download stream error: {}", e))?;
// Decrypt chunk and write to file
let decrypted = decryptor.process(&chunk);
if !decrypted.is_empty() {
file.write_all(&decrypted)
.await
.map_err(|e| format!("Failed to write to file: {}", e))?;
}
}
// Write any remaining buffered data
let final_data = decryptor.finalize();
if !final_data.is_empty() {
file.write_all(&final_data)
.await
.map_err(|e| format!("Failed to write final data: {}", e))?;
}
} else {
// No encryption - just stream directly
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|e| format!("Download stream error: {}", e))?;
file.write_all(&chunk)
.await
.map_err(|e| format!("Failed to write to file: {}", e))?;
}
}
// Ensure all data is flushed
file.flush()
.await
.map_err(|e| format!("Failed to flush file: {}", e))?;
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let library_migrations = vec![Migration {
@@ -171,7 +254,8 @@ pub fn run() {
greet,
tag_audio_file,
read_audio_metadata,
decrypt_deezer_track
decrypt_deezer_track,
download_and_decrypt_track
])
.run(tauri::generate_context!())
.expect("error while running tauri application");