mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 12:01:01 +00:00
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:
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user