fix(wip): add fallback format support for unavailable tracks

Add support for selecting a fallback audio format if the requested
track format is unavailable. Users can now choose to fall back
to MP3_320, MP3_128, or the highest available format, or opt to fail
if the requested format is not found. The queue manager and downloader
now fetch fresh track data just-in-time, handle dz fallback
parameters, and ensure the correct track ID is used for decryption.
Settings UI and store are updated to allow configuring the fallback format.
This commit is contained in:
2025-10-03 10:28:56 -04:00
parent d74bf7e828
commit 90053b67c5
6 changed files with 220 additions and 33 deletions

View File

@@ -142,6 +142,13 @@ export class DeezerAPI {
const errorObj = resultJson.error as any;
console.error(`[ERROR] API returned error for ${method}:`, errorStr);
// Handle fallback parameters (Deezer provides alternative parameters to retry with)
if ((resultJson as any).payload?.FALLBACK) {
console.log(`[DEBUG] Using FALLBACK parameters for ${method}:`, (resultJson as any).payload.FALLBACK);
const fallbackArgs = { ...args, ...(resultJson as any).payload.FALLBACK };
return this.apiCall(method, fallbackArgs, params, retryCount);
}
// Handle rate limiting (error codes 4 and 700) - wait 5 seconds and retry
if (errorObj.code && [4, 700].includes(errorObj.code)) {
console.log(`[DEBUG] Rate limited (code ${errorObj.code}), waiting 5s before retry...`);
@@ -250,6 +257,29 @@ export class DeezerAPI {
return this.apiCall('song.getData', { SNG_ID: trackId });
}
// Get track page data (includes more complete track token info)
async getTrackPage(trackId: string): Promise<any> {
return this.apiCall('deezer.pageTrack', { SNG_ID: trackId });
}
// Get track with fallback (tries pageTrack first, then getData)
async getTrackWithFallback(trackId: string): Promise<any> {
try {
const pageData = await this.getTrackPage(trackId);
if (pageData && pageData.DATA) {
console.log(`[DEBUG] pageTrack returned for ${trackId}:`, {
FILESIZE_FLAC: pageData.DATA.FILESIZE_FLAC,
FALLBACK: pageData.DATA.FALLBACK,
SNG_ID: pageData.DATA.SNG_ID
});
return pageData.DATA;
}
} catch (error) {
console.log(`[DEBUG] pageTrack failed for ${trackId}, falling back to getData`);
}
return this.getTrack(trackId);
}
// Search tracks using public API (no authentication required)
async searchTracks(query: string, limit: number = 25): Promise<any> {
const url = `https://api.deezer.com/search/track?q=${encodeURIComponent(query)}&limit=${limit}`;
@@ -412,8 +442,8 @@ export class DeezerAPI {
console.log('[DEBUG] Getting track download URL...', { trackToken, format, licenseToken });
try {
// media.deezer.com ONLY needs arl cookie, not sid or other cookies
const cookieHeader = this.arl ? `arl=${this.arl}` : '';
// Use all cookies, not just arl - sid (session ID) is also required
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', {
@@ -444,6 +474,15 @@ export class DeezerAPI {
if (result.data && result.data.length > 0) {
const trackData = result.data[0];
// Check for errors in the response
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;
}
if (trackData.media && trackData.media.length > 0) {
const url = trackData.media[0].sources[0].url;
console.log('[DEBUG] Got download URL:', url);