mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41: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:
146
src-tauri/Cargo.lock
generated
146
src-tauri/Cargo.lock
generated
@@ -8,9 +8,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"blowfish",
|
||||
"byteorder",
|
||||
"futures-util",
|
||||
"id3",
|
||||
"md5",
|
||||
"metaflac",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -22,6 +24,7 @@ dependencies = [
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-sql",
|
||||
"tauri-plugin-store",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -659,7 +662,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1192,6 +1195,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1199,7 +1211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1213,6 +1225,12 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1844,6 +1862,22 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.17"
|
||||
@@ -2464,6 +2498,23 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2858,6 +2909,50 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -3579,10 +3674,12 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -3593,6 +3690,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -3755,6 +3853,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -3818,6 +3925,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
@@ -4116,7 +4246,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -5085,6 +5215,16 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
|
||||
@@ -33,4 +33,7 @@ tauri-plugin-process = "2"
|
||||
blowfish = "0.9"
|
||||
md5 = "0.7"
|
||||
byteorder = "1.5.0"
|
||||
reqwest = { version = "0.12.23", features = ["stream", "rustls-tls"] }
|
||||
tokio = { version = "1.47.1", features = ["fs", "io-util"] }
|
||||
futures-util = "0.3.31"
|
||||
|
||||
|
||||
@@ -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