feat(crypto): track download handling

This commit is contained in:
2025-09-30 22:50:59 -04:00
parent 4ebb77f341
commit b57164a4f7
3 changed files with 55 additions and 21 deletions

View File

@@ -16,14 +16,16 @@
"store:default", "store:default",
"dialog:default", "dialog:default",
"fs:default", "fs:default",
"fs:allow-write-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
{ {
"identifier": "fs:scope", "identifier": "fs:scope",
"allow": [ "allow": [
{ {
"path": "$HOME/**/*" "path": "**"
},
{
"path": "$APPDATA/**/*"
} }
] ]
}, },

View File

@@ -273,12 +273,17 @@ export class DeezerAPI {
// Get track download URL // Get track download URL
async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string): Promise<string | null> { async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string): Promise<string | null> {
console.log('[DEBUG] Getting track download URL...', { trackToken, format, licenseToken });
try { try {
const cookieHeader = this.getCookieHeader();
console.log('[DEBUG] Sending request to media.deezer.com with cookies:', cookieHeader);
const response = await fetch('https://media.deezer.com/v1/get_url', { const response = await fetch('https://media.deezer.com/v1/get_url', {
method: 'POST', method: 'POST',
headers: { headers: {
...this.httpHeaders, ...this.httpHeaders,
'Cookie': this.arl ? `arl=${this.arl}` : '' 'Cookie': cookieHeader
}, },
body: JSON.stringify({ body: JSON.stringify({
license_token: licenseToken, license_token: licenseToken,
@@ -287,22 +292,33 @@ export class DeezerAPI {
formats: [{ cipher: 'BF_CBC_STRIPE', format }] formats: [{ cipher: 'BF_CBC_STRIPE', format }]
}], }],
track_tokens: [trackToken] track_tokens: [trackToken]
}) }),
connectTimeout: 30000
}); });
console.log('[DEBUG] Download URL response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json(); const result = await response.json();
console.log('[DEBUG] Download URL response:', result);
if (result.data && result.data.length > 0) { if (result.data && result.data.length > 0) {
const trackData = result.data[0]; const trackData = result.data[0];
if (trackData.media && trackData.media.length > 0) { if (trackData.media && trackData.media.length > 0) {
return trackData.media[0].sources[0].url; const url = trackData.media[0].sources[0].url;
console.log('[DEBUG] Got download URL:', url);
return url;
} }
} }
console.error('[ERROR] No download URL in response:', result);
return null; return null;
} catch (error) { } catch (error) {
console.error('Error getting track URL:', error); console.error('[ERROR] Failed to get track download URL:', error);
return null; throw error; // Re-throw to let caller handle it
} }
} }
} }

View File

@@ -203,15 +203,31 @@ export function generateBlowfishKey(trackId: string): Uint8Array {
* Decrypt a chunk using Blowfish CBC * Decrypt a chunk using Blowfish CBC
*/ */
export function decryptChunk(chunk: Uint8Array, blowfishKey: Uint8Array): Uint8Array { export function decryptChunk(chunk: Uint8Array, blowfishKey: Uint8Array): Uint8Array {
const iv = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]);
try { try {
// Convert Uint8Array to the format blowfish-node expects
const bf = new Blowfish(blowfishKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL); const bf = new Blowfish(blowfishKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL);
// Set IV as Uint8Array
const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
bf.setIv(iv); bf.setIv(iv);
const decrypted = bf.decode(Buffer.from(chunk), Blowfish.TYPE.UINT8_ARRAY);
// Decode and return as Uint8Array
const decrypted = bf.decode(chunk, Blowfish.TYPE.UINT8_ARRAY);
// Verify decryption worked
if (!decrypted || decrypted.length === 0) {
throw new Error('Decryption returned empty result');
}
return new Uint8Array(decrypted); return new Uint8Array(decrypted);
} catch (error) { } catch (error) {
console.error('Error decrypting chunk:', error); // Only log the first few errors to avoid spam
if (Math.random() < 0.01) { // Log ~1% of errors
console.error('Error decrypting chunk (sample):', error, {
chunkLength: chunk.length,
keyLength: blowfishKey.length
});
}
return chunk; // Return original if decryption fails return chunk; // Return original if decryption fails
} }
} }
@@ -219,8 +235,8 @@ export function decryptChunk(chunk: Uint8Array, blowfishKey: Uint8Array): Uint8A
/** /**
* Generate stream path for download URL * Generate stream path for download URL
*/ */
export function generateStreamPath(sngID: string, md5: string, mediaVersion: string, format: string): string { export function generateStreamPath(sngID: string, md5Origin: string, mediaVersion: string, format: string): string {
let urlPart = `${md5}¤${format}¤${sngID}¤${mediaVersion}`; let urlPart = `${md5Origin}¤${format}¤${sngID}¤${mediaVersion}`;
const md5val = md5(urlPart); const md5val = md5(urlPart);
let step2 = `${md5val}¤${urlPart}¤`; let step2 = `${md5val}¤${urlPart}¤`;
step2 += '.'.repeat(16 - (step2.length % 16)); step2 += '.'.repeat(16 - (step2.length % 16));
@@ -234,15 +250,15 @@ export function generateStreamPath(sngID: string, md5: string, mediaVersion: str
/** /**
* Generate download URL from track info * Generate download URL from track info
*/ */
export function generateStreamURL(sngID: string, md5: string, mediaVersion: string, format: string): string { export function generateStreamURL(sngID: string, md5Origin: string, mediaVersion: string, format: string): string {
const urlPart = generateStreamPath(sngID, md5, mediaVersion, format); const urlPart = generateStreamPath(sngID, md5Origin, mediaVersion, format);
return `https://cdns-proxy-${md5[0]}.dzcdn.net/api/1/${urlPart}`; return `https://cdns-proxy-${md5Origin[0]}.dzcdn.net/api/1/${urlPart}`;
} }
/** /**
* Generate crypted stream URL (for encrypted streams) * Generate crypted stream URL (for encrypted streams)
*/ */
export function generateCryptedStreamURL(sngID: string, md5: string, mediaVersion: string, format: string): string { export function generateCryptedStreamURL(sngID: string, md5Origin: string, mediaVersion: string, format: string): string {
const urlPart = generateStreamPath(sngID, md5, mediaVersion, format); const urlPart = generateStreamPath(sngID, md5Origin, mediaVersion, format);
return `https://e-cdns-proxy-${md5[0]}.dzcdn.net/mobile/1/${urlPart}`; return `https://e-cdns-proxy-${md5Origin[0]}.dzcdn.net/mobile/1/${urlPart}`;
} }