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

@@ -2,7 +2,6 @@
* Deezer track downloader with streaming and decryption
*/
import { fetch } from '@tauri-apps/plugin-http';
import { writeFile, mkdir, remove, rename, exists } from '@tauri-apps/plugin-fs';
import { invoke } from '@tauri-apps/api/core';
import { generateTrackPath } from './paths';
@@ -56,86 +55,21 @@ export async function downloadTrack(
console.log('Temp path:', paths.tempPath);
try {
// Fetch the track with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
const response = await fetch(downloadURL, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const totalSize = parseInt(response.headers.get('content-length') || '0');
const isCrypted = downloadURL.includes('/mobile/') || downloadURL.includes('/media/');
// Stream the response with progress tracking
const reader = response.body!.getReader();
const chunks: Uint8Array[] = [];
let downloadedBytes = 0;
let lastReportedPercentage = 0;
// Use the provided decryption track ID (for fallback tracks) or the original track ID
const trackIdForDecryption = decryptionTrackId ? decryptionTrackId.toString() : track.id.toString();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Download and decrypt in Rust backend (streaming, no memory accumulation)
console.log('Downloading and decrypting track in Rust backend...');
await invoke('download_and_decrypt_track', {
url: downloadURL,
trackId: trackIdForDecryption,
outputPath: paths.tempPath,
isEncrypted: isCrypted
});
chunks.push(value);
downloadedBytes += value.length;
// Call progress callback every 5%
if (onProgress && totalSize > 0) {
const percentage = (downloadedBytes / totalSize) * 100;
const roundedPercentage = Math.floor(percentage / 5) * 5;
if (roundedPercentage > lastReportedPercentage || percentage === 100) {
lastReportedPercentage = roundedPercentage;
onProgress({
downloaded: downloadedBytes,
total: totalSize,
percentage
});
}
}
}
// Combine chunks into single Uint8Array
const encryptedData = new Uint8Array(downloadedBytes);
let offset = 0;
for (const chunk of chunks) {
encryptedData.set(chunk, offset);
offset += chunk.length;
}
console.log(`Downloaded ${encryptedData.length} bytes, encrypted: ${isCrypted}`);
// Yield to the browser to keep UI responsive
await new Promise(resolve => setTimeout(resolve, 0));
// Decrypt if needed
let decryptedData: Uint8Array;
if (isCrypted) {
console.log('Decrypting track...');
// Use the provided decryption track ID (for fallback tracks) or the original track ID
const trackIdForDecryption = decryptionTrackId ? decryptionTrackId.toString() : track.id.toString();
console.log(`Decrypting with track ID: ${trackIdForDecryption}`);
// Call Rust decryption function - Tauri returns Vec<u8> as number[]
const decryptedArray = await invoke<number[]>('decrypt_deezer_track', {
data: encryptedData,
trackId: trackIdForDecryption
});
decryptedData = new Uint8Array(decryptedArray);
} else {
decryptedData = encryptedData;
}
console.log('Download and decryption complete!');
// Get user settings
const appSettings = get(settings);
@@ -151,10 +85,7 @@ export async function downloadTrack(
}
}
// Write untagged file to temp first
console.log('Writing untagged file to temp...');
await writeFile(paths.tempPath, decryptedData);
// File is already written to temp by Rust backend
// Move to final location
const finalPath = `${paths.filepath}/${paths.filename}`;
console.log('Moving to final location:', finalPath);