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
|
// 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 });
|
console.log('[DEBUG] Getting track download URL...', { trackToken, format, licenseToken });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -479,19 +479,19 @@ export class DeezerAPI {
|
|||||||
if (trackData.errors && trackData.errors.length > 0) {
|
if (trackData.errors && trackData.errors.length > 0) {
|
||||||
const error = trackData.errors[0];
|
const error = trackData.errors[0];
|
||||||
console.error('[ERROR] Deezer media API error:', error);
|
console.error('[ERROR] Deezer media API error:', error);
|
||||||
// Return null to trigger fallback, don't throw - let caller handle it
|
// Return error details so caller can handle alternative track fallback
|
||||||
return null;
|
return { url: null, errorCode: error.code, errorMessage: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackData.media && trackData.media.length > 0) {
|
if (trackData.media && trackData.media.length > 0) {
|
||||||
const url = trackData.media[0].sources[0].url;
|
const url = trackData.media[0].sources[0].url;
|
||||||
console.log('[DEBUG] Got download URL:', url);
|
console.log('[DEBUG] Got download URL:', url);
|
||||||
return url;
|
return { url };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('[ERROR] No download URL in response:', result);
|
console.error('[ERROR] No download URL in response:', result);
|
||||||
return null;
|
return { url: null };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[ERROR] Failed to get track download URL:', error);
|
console.error('[ERROR] Failed to get track download URL:', error);
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,46 @@ export class DeezerQueueManager {
|
|||||||
return trackData;
|
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
|
* Start processing the queue
|
||||||
*/
|
*/
|
||||||
@@ -175,9 +215,6 @@ export class DeezerQueueManager {
|
|||||||
}
|
}
|
||||||
deezerAPI.setArl(authState.arl);
|
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
|
// Get user data for license token
|
||||||
const userData = await deezerAPI.getUserData();
|
const userData = await deezerAPI.getUserData();
|
||||||
const licenseToken = userData.USER?.OPTIONS?.license_token;
|
const licenseToken = userData.USER?.OPTIONS?.license_token;
|
||||||
@@ -186,37 +223,62 @@ export class DeezerQueueManager {
|
|||||||
throw new Error('License token not found');
|
throw new Error('License token not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get download URL using fresh token
|
// Try to get download URL with alternative track fallback (error 2002 handling)
|
||||||
let downloadURL = await deezerAPI.getTrackDownloadUrl(
|
let downloadURL: string | undefined;
|
||||||
trackData.TRACK_TOKEN,
|
let finalTrackId: string | undefined;
|
||||||
appSettings.deezerFormat,
|
let finalFormat: string | undefined;
|
||||||
licenseToken
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply fallback strategy based on user settings
|
try {
|
||||||
if (!downloadURL && appSettings.deezerFallbackFormat !== 'none') {
|
const result = await this.getDownloadUrlWithAlternatives(
|
||||||
if (appSettings.deezerFallbackFormat === 'highest') {
|
track.id.toString(),
|
||||||
// Try formats in order: FLAC -> MP3_320 -> MP3_128
|
appSettings.deezerFormat,
|
||||||
const formats: ('FLAC' | 'MP3_320' | 'MP3_128')[] = ['FLAC', 'MP3_320', 'MP3_128'];
|
licenseToken
|
||||||
for (const format of formats) {
|
);
|
||||||
|
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
|
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);
|
try {
|
||||||
if (downloadURL) break;
|
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 {
|
} else {
|
||||||
// Try specific fallback format
|
throw error;
|
||||||
console.log(`[DeezerQueueManager] ${appSettings.deezerFormat} not available for "${track.title}", trying ${appSettings.deezerFallbackFormat}...`);
|
|
||||||
downloadURL = await deezerAPI.getTrackDownloadUrl(
|
|
||||||
trackData.TRACK_TOKEN,
|
|
||||||
appSettings.deezerFallbackFormat,
|
|
||||||
licenseToken
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!downloadURL) {
|
// These should be defined if we get here without throwing
|
||||||
throw new Error('Failed to get download URL from Deezer');
|
if (!downloadURL || !finalTrackId || !finalFormat) {
|
||||||
|
throw new Error('Failed to get download URL - unexpected state');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update progress
|
// 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(
|
const filePath = await downloadTrack(
|
||||||
track,
|
track,
|
||||||
downloadURL,
|
downloadURL,
|
||||||
appSettings.musicFolder,
|
appSettings.musicFolder,
|
||||||
appSettings.deezerFormat,
|
finalFormat,
|
||||||
(progress) => {
|
(progress) => {
|
||||||
// Update progress in queue
|
// Update progress in queue
|
||||||
updateQueueItem(item.id, {
|
updateQueueItem(item.id, {
|
||||||
@@ -246,7 +308,7 @@ export class DeezerQueueManager {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
trackData.SNG_ID
|
finalTrackId
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[DeezerQueueManager] Downloaded: ${filePath}`);
|
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 userData = await deezerAPI.getUserData();
|
||||||
const licenseToken = userData.USER?.OPTIONS?.license_token;
|
const licenseToken = userData.USER?.OPTIONS?.license_token;
|
||||||
|
|
||||||
@@ -301,43 +360,69 @@ export class DeezerQueueManager {
|
|||||||
throw new Error('License token not found');
|
throw new Error('License token not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadURL = await deezerAPI.getTrackDownloadUrl(
|
// Try to get download URL with alternative track fallback (error 2002 handling)
|
||||||
trackData.TRACK_TOKEN,
|
let downloadURL: string | undefined;
|
||||||
appSettings.deezerFormat,
|
let finalTrackId: string | undefined;
|
||||||
licenseToken
|
let finalFormat: string | undefined;
|
||||||
);
|
|
||||||
|
|
||||||
// Apply fallback strategy based on user settings
|
try {
|
||||||
if (!downloadURL && appSettings.deezerFallbackFormat !== 'none') {
|
const result = await this.getDownloadUrlWithAlternatives(
|
||||||
if (appSettings.deezerFallbackFormat === 'highest') {
|
track.id.toString(),
|
||||||
// Try formats in order: FLAC -> MP3_320 -> MP3_128
|
appSettings.deezerFormat,
|
||||||
const formats: ('FLAC' | 'MP3_320' | 'MP3_128')[] = ['FLAC', 'MP3_320', 'MP3_128'];
|
licenseToken
|
||||||
for (const format of formats) {
|
);
|
||||||
|
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
|
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);
|
try {
|
||||||
if (downloadURL) break;
|
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 {
|
} else {
|
||||||
// Try specific fallback format
|
throw error;
|
||||||
console.log(`[DeezerQueueManager] ${appSettings.deezerFormat} not available for "${track.title}", trying ${appSettings.deezerFallbackFormat}...`);
|
|
||||||
downloadURL = await deezerAPI.getTrackDownloadUrl(
|
|
||||||
trackData.TRACK_TOKEN,
|
|
||||||
appSettings.deezerFallbackFormat,
|
|
||||||
licenseToken
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!downloadURL) {
|
// These should be defined if we get here without throwing
|
||||||
throw new Error('Failed to get download URL from Deezer');
|
if (!downloadURL || !finalTrackId || !finalFormat) {
|
||||||
|
throw new Error('Failed to get download URL - unexpected state');
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = await downloadTrack(
|
const filePath = await downloadTrack(
|
||||||
track,
|
track,
|
||||||
downloadURL,
|
downloadURL,
|
||||||
appSettings.musicFolder!,
|
appSettings.musicFolder!,
|
||||||
appSettings.deezerFormat,
|
finalFormat,
|
||||||
(progress) => {
|
(progress) => {
|
||||||
// Update progress in queue
|
// Update progress in queue
|
||||||
updateQueueItem(item.id, {
|
updateQueueItem(item.id, {
|
||||||
@@ -350,7 +435,7 @@ export class DeezerQueueManager {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
trackData.SNG_ID
|
finalTrackId
|
||||||
);
|
);
|
||||||
|
|
||||||
results.push(filePath);
|
results.push(filePath);
|
||||||
|
|||||||
Reference in New Issue
Block a user