mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 19:51:01 +00:00
feat(auth): purple app track fetch
This commit is contained in:
195
src/lib/services/deezer/downloader.ts
Normal file
195
src/lib/services/deezer/downloader.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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 { generateBlowfishKey, decryptChunk } from './crypto';
|
||||
import { generateTrackPath } from './paths';
|
||||
import type { DeezerTrack } from '$lib/types/deezer';
|
||||
|
||||
export interface DownloadProgress {
|
||||
downloaded: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (progress: DownloadProgress) => void;
|
||||
|
||||
/**
|
||||
* Download and decrypt a single track
|
||||
*/
|
||||
export async function downloadTrack(
|
||||
track: DeezerTrack,
|
||||
downloadURL: string,
|
||||
musicFolder: string,
|
||||
format: string,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<string> {
|
||||
// Generate paths
|
||||
const paths = generateTrackPath(track, musicFolder, format, false);
|
||||
|
||||
// Ensure temp folder exists
|
||||
const tempFolder = `${musicFolder}/_temp`;
|
||||
try {
|
||||
await mkdir(tempFolder, { recursive: true });
|
||||
} catch (error) {
|
||||
// Folder might already exist
|
||||
}
|
||||
|
||||
// Ensure target folder exists
|
||||
try {
|
||||
await mkdir(paths.filepath, { recursive: true });
|
||||
} catch (error) {
|
||||
// Folder might already exist
|
||||
}
|
||||
|
||||
// Download to temp file
|
||||
console.log('Downloading track:', track.title);
|
||||
console.log('Download URL:', downloadURL);
|
||||
console.log('Temp path:', paths.tempPath);
|
||||
|
||||
try {
|
||||
// Fetch the track with streaming
|
||||
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'
|
||||
}
|
||||
});
|
||||
|
||||
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/');
|
||||
|
||||
// Get the response as array buffer
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const encryptedData = new Uint8Array(arrayBuffer);
|
||||
|
||||
console.log(`Downloaded ${encryptedData.length} bytes, encrypted: ${isCrypted}`);
|
||||
|
||||
// Decrypt if needed
|
||||
let decryptedData: Uint8Array;
|
||||
|
||||
if (isCrypted) {
|
||||
console.log('Decrypting track...');
|
||||
decryptedData = await decryptTrackData(encryptedData, track.id.toString());
|
||||
} else {
|
||||
decryptedData = encryptedData;
|
||||
}
|
||||
|
||||
// Write to temp file
|
||||
console.log('Writing to temp file...');
|
||||
await writeFile(paths.tempPath, decryptedData);
|
||||
|
||||
// Move to final location
|
||||
const finalPath = `${paths.filepath}/${paths.filename}`;
|
||||
console.log('Moving to final location:', finalPath);
|
||||
|
||||
// Check if file already exists
|
||||
if (await exists(finalPath)) {
|
||||
console.log('File already exists, removing...');
|
||||
await remove(finalPath);
|
||||
}
|
||||
|
||||
await rename(paths.tempPath, finalPath);
|
||||
|
||||
console.log('Download complete!');
|
||||
return finalPath;
|
||||
|
||||
} catch (error) {
|
||||
// Clean up temp file on error
|
||||
try {
|
||||
if (await exists(paths.tempPath)) {
|
||||
await remove(paths.tempPath);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('Error cleaning up temp file:', cleanupError);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt track data using Blowfish CBC
|
||||
* Deezer encrypts every 3rd chunk of 2048 bytes
|
||||
*/
|
||||
async function decryptTrackData(data: Uint8Array, trackId: string): Promise<Uint8Array> {
|
||||
const chunkSize = 2048;
|
||||
const blowfishKey = generateBlowfishKey(trackId);
|
||||
const result: Uint8Array[] = [];
|
||||
|
||||
let offset = 0;
|
||||
let chunkIndex = 0;
|
||||
|
||||
// Skip initial padding (null bytes before actual data)
|
||||
while (offset < data.length && data[offset] === 0) {
|
||||
offset++;
|
||||
}
|
||||
|
||||
// If we found padding, check if next bytes are 'ftyp' (MP4) or 'ID3' (MP3) or 'fLaC' (FLAC)
|
||||
if (offset > 0 && offset + 8 < data.length) {
|
||||
const header = String.fromCharCode(...data.slice(offset + 4, offset + 8));
|
||||
if (header === 'ftyp') {
|
||||
// Skip the null padding
|
||||
result.push(data.slice(0, offset));
|
||||
} else {
|
||||
// Reset if we didn't find expected header
|
||||
offset = 0;
|
||||
}
|
||||
} else {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
while (offset < data.length) {
|
||||
const remainingBytes = data.length - offset;
|
||||
const currentChunkSize = Math.min(chunkSize, remainingBytes);
|
||||
const chunk = data.slice(offset, offset + currentChunkSize);
|
||||
|
||||
// Decrypt every 3rd chunk (0, 3, 6, 9, ...)
|
||||
if (chunkIndex % 3 === 0 && chunk.length === chunkSize) {
|
||||
try {
|
||||
const decrypted = decryptChunk(chunk, blowfishKey);
|
||||
result.push(decrypted);
|
||||
} catch (error) {
|
||||
console.error('Error decrypting chunk:', error);
|
||||
result.push(chunk); // Use original if decryption fails
|
||||
}
|
||||
} else {
|
||||
result.push(chunk);
|
||||
}
|
||||
|
||||
offset += currentChunkSize;
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
// Combine all chunks
|
||||
const totalLength = result.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const combined = new Uint8Array(totalLength);
|
||||
let position = 0;
|
||||
|
||||
for (const chunk of result) {
|
||||
combined.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a track file already exists
|
||||
*/
|
||||
export async function trackExists(
|
||||
track: DeezerTrack,
|
||||
musicFolder: string,
|
||||
format: string
|
||||
): Promise<boolean> {
|
||||
const paths = generateTrackPath(track, musicFolder, format, false);
|
||||
const finalPath = `${paths.filepath}/${paths.filename}`;
|
||||
|
||||
return await exists(finalPath);
|
||||
}
|
||||
Reference in New Issue
Block a user