mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
fix(dz): implement alternative track fallback for error 2002
("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
This commit is contained in:
@@ -438,7 +438,7 @@ export class DeezerAPI {
|
||||
}
|
||||
|
||||
// Get track download URL
|
||||
async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string, retryCount: number = 0): Promise<string | null> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user