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

@@ -23,6 +23,51 @@ export class DeezerQueueManager {
private abortController: AbortController | null = null;
private albumCoverCache: Map<string, Uint8Array> = new Map();
/**
* Fetch fresh track data with valid token right before download
* Uses pageTrack first for complete token data, falls back to getData
* Handles FALLBACK.SNG_ID when requested format is unavailable
*/
private async getValidTrackData(trackId: string, requestedFormat: 'FLAC' | 'MP3_320' | 'MP3_128' = 'FLAC'): Promise<any> {
console.log(`[DeezerQueueManager] Fetching fresh track data for ID: ${trackId}`);
const trackData = await deezerAPI.getTrackWithFallback(trackId);
if (!trackData || !trackData.TRACK_TOKEN) {
throw new Error('Failed to get track token');
}
// Log important fields for debugging
console.log(`[DeezerQueueManager] Track data:`, {
TRACK_TOKEN: trackData.TRACK_TOKEN ? 'present' : 'missing',
TRACK_TOKEN_EXPIRE: trackData.TRACK_TOKEN_EXPIRE,
FILESIZE_FLAC: trackData.FILESIZE_FLAC,
FILESIZE_MP3_320: trackData.FILESIZE_MP3_320,
FILESIZE_MP3_128: trackData.FILESIZE_MP3_128,
FALLBACK: trackData.FALLBACK,
SNG_ID: trackData.SNG_ID
});
// Log token expiration for debugging
if (trackData.TRACK_TOKEN_EXPIRE) {
const expireDate = new Date(trackData.TRACK_TOKEN_EXPIRE * 1000);
console.log(`[DeezerQueueManager] Track token expires at: ${expireDate.toISOString()}`);
}
// Check if requested format is available
const filesizeField = `FILESIZE_${requestedFormat}`;
const filesize = trackData[filesizeField];
const isAvailable = filesize && filesize !== "0" && filesize !== 0;
if (!isAvailable && trackData.FALLBACK?.SNG_ID) {
console.log(`[DeezerQueueManager] ${requestedFormat} not available (FILESIZE=${filesize}), using FALLBACK track ID: ${trackData.FALLBACK.SNG_ID}`);
// Recursively fetch the fallback track
return this.getValidTrackData(trackData.FALLBACK.SNG_ID.toString(), requestedFormat);
}
return trackData;
}
/**
* Start processing the queue
*/
@@ -130,6 +175,9 @@ 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;
@@ -138,15 +186,37 @@ export class DeezerQueueManager {
throw new Error('License token not found');
}
// Get download URL
const downloadURL = await deezerAPI.getTrackDownloadUrl(
track.trackToken!,
// Get download URL using fresh token
let downloadURL = await deezerAPI.getTrackDownloadUrl(
trackData.TRACK_TOKEN,
appSettings.deezerFormat,
licenseToken
);
// 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) {
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;
}
} 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
);
}
}
if (!downloadURL) {
throw new Error('Failed to get download URL');
throw new Error('Failed to get download URL from Deezer');
}
// Update progress
@@ -158,7 +228,7 @@ export class DeezerQueueManager {
}
});
// Download the track
// Download the track (use trackData.SNG_ID for decryption in case it's a fallback track)
const filePath = await downloadTrack(
track,
downloadURL,
@@ -174,7 +244,9 @@ export class DeezerQueueManager {
progress: progress.percentage
}
});
}
},
0,
trackData.SNG_ID
);
console.log(`[DeezerQueueManager] Downloaded: ${filePath}`);
@@ -219,6 +291,9 @@ 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;
@@ -226,21 +301,46 @@ export class DeezerQueueManager {
throw new Error('License token not found');
}
const downloadURL = await deezerAPI.getTrackDownloadUrl(
track.trackToken!,
let downloadURL = await deezerAPI.getTrackDownloadUrl(
trackData.TRACK_TOKEN,
appSettings.deezerFormat,
licenseToken
);
// 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) {
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;
}
} 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
);
}
}
if (!downloadURL) {
throw new Error('Failed to get download URL');
throw new Error('Failed to get download URL from Deezer');
}
const filePath = await downloadTrack(
track,
downloadURL,
appSettings.musicFolder!,
appSettings.deezerFormat
appSettings.deezerFormat,
undefined,
0,
trackData.SNG_ID
);
results.push(filePath);
@@ -261,8 +361,9 @@ export class DeezerQueueManager {
failedTracks: failedCount
});
// Continue with next track
// Rate limiting: Add delay between downloads to avoid API throttling
if (queue.length > 0) {
await new Promise(resolve => setTimeout(resolve, 500));
await downloadNext();
}
};