From 6fff93fe45985bca4756a22d7498d2fd8dcc797e Mon Sep 17 00:00:00 2001 From: Markury Date: Fri, 3 Oct 2025 11:43:11 -0400 Subject: [PATCH] fix(dz): implement alternative track fallback for error 2002 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ("Track token has no sufficient rights on requested media"). Previous behavior: - Only tried format fallback (FLAC → MP3_320 → MP3_128) - Used same track token for all format attempts - Failed when error 2002 occurred even if alternative tracks existed New behavior: - When error 2002 occurs, fetches FALLBACK.SNG_ID and gets fresh token - Retries with same format but different track ID - Loops through all alternative track IDs before trying format fallback - Only after exhausting alternatives does it fall back to lower quality formats --- src/lib/services/deezer.ts | 10 +- src/lib/services/deezer/queueManager.ts | 201 +++++++++++++++++------- 2 files changed, 148 insertions(+), 63 deletions(-) diff --git a/src/lib/services/deezer.ts b/src/lib/services/deezer.ts index 8fc0af9..ad9661c 100644 --- a/src/lib/services/deezer.ts +++ b/src/lib/services/deezer.ts @@ -438,7 +438,7 @@ export class DeezerAPI { } // Get track download URL - async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string, retryCount: number = 0): Promise { + async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string, retryCount: number = 0): Promise<{ url: string | null; errorCode?: number; errorMessage?: string }> { console.log('[DEBUG] Getting track download URL...', { trackToken, format, licenseToken }); try { @@ -479,19 +479,19 @@ export class DeezerAPI { if (trackData.errors && trackData.errors.length > 0) { const error = trackData.errors[0]; console.error('[ERROR] Deezer media API error:', error); - // Return null to trigger fallback, don't throw - let caller handle it - return null; + // Return error details so caller can handle alternative track fallback + return { url: null, errorCode: error.code, errorMessage: error.message }; } if (trackData.media && trackData.media.length > 0) { const url = trackData.media[0].sources[0].url; console.log('[DEBUG] Got download URL:', url); - return url; + return { url }; } } console.error('[ERROR] No download URL in response:', result); - return null; + return { url: null }; } catch (error: any) { console.error('[ERROR] Failed to get track download URL:', error); diff --git a/src/lib/services/deezer/queueManager.ts b/src/lib/services/deezer/queueManager.ts index 0d34bba..4601a59 100644 --- a/src/lib/services/deezer/queueManager.ts +++ b/src/lib/services/deezer/queueManager.ts @@ -68,6 +68,46 @@ export class DeezerQueueManager { return trackData; } + /** + * Get download URL with alternative track fallback (Deemix-compliant) + * Tries alternative track IDs (FALLBACK.SNG_ID) with same format before trying different formats + * Matches Deemix's getPreferredBitrate logic for handling error 2002 + */ + private async getDownloadUrlWithAlternatives( + trackId: string, + requestedFormat: 'FLAC' | 'MP3_320' | 'MP3_128', + licenseToken: string + ): Promise<{ url: string; finalTrackId: string; finalFormat: string }> { + console.log(`[DeezerQueueManager] Getting download URL with alternatives for track ${trackId}, format ${requestedFormat}`); + + // Fetch track data (handles filesize=0 fallback) + let trackData = await this.getValidTrackData(trackId, requestedFormat); + + // Try to get download URL with current track token + let result = await deezerAPI.getTrackDownloadUrl(trackData.TRACK_TOKEN, requestedFormat, licenseToken); + + // If error 2002 (insufficient rights) and has alternative track, try fallback track(s) + while (!result.url && result.errorCode === 2002 && trackData.FALLBACK?.SNG_ID) { + const fallbackId = trackData.FALLBACK.SNG_ID.toString(); + console.log(`[DeezerQueueManager] Error 2002 for track ${trackData.SNG_ID}, trying alternative track ID: ${fallbackId}`); + + // Fetch fallback track data (with fresh token) + trackData = await this.getValidTrackData(fallbackId, requestedFormat); + + // Try to get download URL with fallback track token (same format) + result = await deezerAPI.getTrackDownloadUrl(trackData.TRACK_TOKEN, requestedFormat, licenseToken); + } + + // If we got a URL, return it + if (result.url) { + return { url: result.url, finalTrackId: trackData.SNG_ID, finalFormat: requestedFormat }; + } + + // If no URL after exhausting alternatives, throw error + const errorMsg = result.errorMessage || 'Failed to get download URL'; + throw new Error(`${errorMsg} (code: ${result.errorCode || 'unknown'})`); + } + /** * Start processing the queue */ @@ -175,9 +215,6 @@ export class DeezerQueueManager { } deezerAPI.setArl(authState.arl); - // Fetch fresh track data with valid token just-in-time - const trackData = await this.getValidTrackData(track.id.toString(), appSettings.deezerFormat); - // Get user data for license token const userData = await deezerAPI.getUserData(); const licenseToken = userData.USER?.OPTIONS?.license_token; @@ -186,37 +223,62 @@ export class DeezerQueueManager { throw new Error('License token not found'); } - // Get download URL using fresh token - let downloadURL = await deezerAPI.getTrackDownloadUrl( - trackData.TRACK_TOKEN, - appSettings.deezerFormat, - licenseToken - ); + // Try to get download URL with alternative track fallback (error 2002 handling) + let downloadURL: string | undefined; + let finalTrackId: string | undefined; + let finalFormat: string | undefined; - // Apply fallback strategy based on user settings - if (!downloadURL && appSettings.deezerFallbackFormat !== 'none') { - if (appSettings.deezerFallbackFormat === 'highest') { - // Try formats in order: FLAC -> MP3_320 -> MP3_128 - const formats: ('FLAC' | 'MP3_320' | 'MP3_128')[] = ['FLAC', 'MP3_320', 'MP3_128']; - for (const format of formats) { + try { + const result = await this.getDownloadUrlWithAlternatives( + track.id.toString(), + appSettings.deezerFormat, + licenseToken + ); + downloadURL = result.url; + finalTrackId = result.finalTrackId; + finalFormat = result.finalFormat; + } catch (error) { + // If alternative track fallback failed and user wants format fallback, try different formats + if (appSettings.deezerFallbackFormat !== 'none') { + console.log(`[DeezerQueueManager] Alternative track fallback failed for "${track.title}", trying format fallback...`); + + const formatsToTry: ('FLAC' | 'MP3_320' | 'MP3_128')[] = + appSettings.deezerFallbackFormat === 'highest' + ? ['FLAC', 'MP3_320', 'MP3_128'] + : [appSettings.deezerFallbackFormat]; + + let succeeded = false; + for (const format of formatsToTry) { if (format === appSettings.deezerFormat) continue; // Skip already tried format - console.log(`[DeezerQueueManager] ${appSettings.deezerFormat} not available for "${track.title}", trying ${format}...`); - downloadURL = await deezerAPI.getTrackDownloadUrl(trackData.TRACK_TOKEN, format, licenseToken); - if (downloadURL) break; + + try { + console.log(`[DeezerQueueManager] Trying format ${format} for "${track.title}"...`); + const result = await this.getDownloadUrlWithAlternatives( + track.id.toString(), + format, + licenseToken + ); + downloadURL = result.url; + finalTrackId = result.finalTrackId; + finalFormat = result.finalFormat; + succeeded = true; + break; + } catch (formatError) { + console.log(`[DeezerQueueManager] Format ${format} also failed for "${track.title}"`); + } + } + + if (!succeeded) { + throw new Error('Failed to get download URL from Deezer - all alternatives exhausted'); } } else { - // Try specific fallback format - console.log(`[DeezerQueueManager] ${appSettings.deezerFormat} not available for "${track.title}", trying ${appSettings.deezerFallbackFormat}...`); - downloadURL = await deezerAPI.getTrackDownloadUrl( - trackData.TRACK_TOKEN, - appSettings.deezerFallbackFormat, - licenseToken - ); + throw error; } } - if (!downloadURL) { - throw new Error('Failed to get download URL from Deezer'); + // These should be defined if we get here without throwing + if (!downloadURL || !finalTrackId || !finalFormat) { + throw new Error('Failed to get download URL - unexpected state'); } // Update progress @@ -228,12 +290,12 @@ export class DeezerQueueManager { } }); - // Download the track (use trackData.SNG_ID for decryption in case it's a fallback track) + // Download the track (use finalTrackId for decryption - might be original or fallback track) const filePath = await downloadTrack( track, downloadURL, appSettings.musicFolder, - appSettings.deezerFormat, + finalFormat, (progress) => { // Update progress in queue updateQueueItem(item.id, { @@ -246,7 +308,7 @@ export class DeezerQueueManager { }); }, 0, - trackData.SNG_ID + finalTrackId ); console.log(`[DeezerQueueManager] Downloaded: ${filePath}`); @@ -291,9 +353,6 @@ export class DeezerQueueManager { } }); - // Fetch fresh track data with valid token just-in-time - const trackData = await this.getValidTrackData(track.id.toString(), appSettings.deezerFormat); - const userData = await deezerAPI.getUserData(); const licenseToken = userData.USER?.OPTIONS?.license_token; @@ -301,43 +360,69 @@ export class DeezerQueueManager { throw new Error('License token not found'); } - let downloadURL = await deezerAPI.getTrackDownloadUrl( - trackData.TRACK_TOKEN, - appSettings.deezerFormat, - licenseToken - ); + // Try to get download URL with alternative track fallback (error 2002 handling) + let downloadURL: string | undefined; + let finalTrackId: string | undefined; + let finalFormat: string | undefined; - // Apply fallback strategy based on user settings - if (!downloadURL && appSettings.deezerFallbackFormat !== 'none') { - if (appSettings.deezerFallbackFormat === 'highest') { - // Try formats in order: FLAC -> MP3_320 -> MP3_128 - const formats: ('FLAC' | 'MP3_320' | 'MP3_128')[] = ['FLAC', 'MP3_320', 'MP3_128']; - for (const format of formats) { + try { + const result = await this.getDownloadUrlWithAlternatives( + track.id.toString(), + appSettings.deezerFormat, + licenseToken + ); + downloadURL = result.url; + finalTrackId = result.finalTrackId; + finalFormat = result.finalFormat; + } catch (error) { + // If alternative track fallback failed and user wants format fallback, try different formats + if (appSettings.deezerFallbackFormat !== 'none') { + console.log(`[DeezerQueueManager] Alternative track fallback failed for "${track.title}", trying format fallback...`); + + const formatsToTry: ('FLAC' | 'MP3_320' | 'MP3_128')[] = + appSettings.deezerFallbackFormat === 'highest' + ? ['FLAC', 'MP3_320', 'MP3_128'] + : [appSettings.deezerFallbackFormat]; + + let succeeded = false; + for (const format of formatsToTry) { if (format === appSettings.deezerFormat) continue; // Skip already tried format - console.log(`[DeezerQueueManager] ${appSettings.deezerFormat} not available for "${track.title}", trying ${format}...`); - downloadURL = await deezerAPI.getTrackDownloadUrl(trackData.TRACK_TOKEN, format, licenseToken); - if (downloadURL) break; + + try { + console.log(`[DeezerQueueManager] Trying format ${format} for "${track.title}"...`); + const result = await this.getDownloadUrlWithAlternatives( + track.id.toString(), + format, + licenseToken + ); + downloadURL = result.url; + finalTrackId = result.finalTrackId; + finalFormat = result.finalFormat; + succeeded = true; + break; + } catch (formatError) { + console.log(`[DeezerQueueManager] Format ${format} also failed for "${track.title}"`); + } + } + + if (!succeeded) { + throw new Error('Failed to get download URL from Deezer - all alternatives exhausted'); } } else { - // Try specific fallback format - console.log(`[DeezerQueueManager] ${appSettings.deezerFormat} not available for "${track.title}", trying ${appSettings.deezerFallbackFormat}...`); - downloadURL = await deezerAPI.getTrackDownloadUrl( - trackData.TRACK_TOKEN, - appSettings.deezerFallbackFormat, - licenseToken - ); + throw error; } } - if (!downloadURL) { - throw new Error('Failed to get download URL from Deezer'); + // These should be defined if we get here without throwing + if (!downloadURL || !finalTrackId || !finalFormat) { + throw new Error('Failed to get download URL - unexpected state'); } const filePath = await downloadTrack( track, downloadURL, appSettings.musicFolder!, - appSettings.deezerFormat, + finalFormat, (progress) => { // Update progress in queue updateQueueItem(item.id, { @@ -350,7 +435,7 @@ export class DeezerQueueManager { }); }, 0, - trackData.SNG_ID + finalTrackId ); results.push(filePath);