mirror of
https://github.com/markuryy/shark.git
synced 2026-07-26 15:01:02 +00:00
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:
@@ -97,6 +97,69 @@ pub fn decrypt_track(data: &[u8], track_id: &str) -> Vec<u8> {
|
||||
result
|
||||
}
|
||||
|
||||
/// Streaming decryption state for processing data chunk-by-chunk
|
||||
pub struct StreamingDecryptor {
|
||||
blowfish_key: Vec<u8>,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl StreamingDecryptor {
|
||||
const CHUNK_SIZE: usize = 2048;
|
||||
const WINDOW_SIZE: usize = Self::CHUNK_SIZE * 3; // 6144
|
||||
|
||||
pub fn new(track_id: &str) -> Self {
|
||||
Self {
|
||||
blowfish_key: generate_blowfish_key(track_id),
|
||||
buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process incoming data and return decrypted output
|
||||
/// May return less data than input as it buffers to maintain 6144-byte windows
|
||||
pub fn process(&mut self, data: &[u8]) -> Vec<u8> {
|
||||
self.buffer.extend_from_slice(data);
|
||||
let mut output = Vec::new();
|
||||
|
||||
// Process complete windows
|
||||
while self.buffer.len() >= Self::WINDOW_SIZE {
|
||||
let encrypted_chunk = &self.buffer[0..Self::CHUNK_SIZE];
|
||||
let plain_part = &self.buffer[Self::CHUNK_SIZE..Self::WINDOW_SIZE];
|
||||
|
||||
let decrypted = decrypt_chunk(encrypted_chunk, &self.blowfish_key);
|
||||
output.extend_from_slice(&decrypted);
|
||||
output.extend_from_slice(plain_part);
|
||||
|
||||
self.buffer.drain(0..Self::WINDOW_SIZE);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Finalize decryption and return any remaining buffered data
|
||||
pub fn finalize(self) -> Vec<u8> {
|
||||
if self.buffer.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let remaining = self.buffer.len();
|
||||
|
||||
if remaining >= Self::CHUNK_SIZE {
|
||||
// Partial window: decrypt first 2048 bytes, keep rest as-is
|
||||
let encrypted_chunk = &self.buffer[0..Self::CHUNK_SIZE];
|
||||
let plain_part = &self.buffer[Self::CHUNK_SIZE..];
|
||||
|
||||
let decrypted = decrypt_chunk(encrypted_chunk, &self.blowfish_key);
|
||||
let mut output = Vec::with_capacity(remaining);
|
||||
output.extend_from_slice(&decrypted);
|
||||
output.extend_from_slice(plain_part);
|
||||
output
|
||||
} else {
|
||||
// Less than 2048 bytes: keep as-is
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user