diff --git a/src/lib/services/deezer.ts b/src/lib/services/deezer.ts index 3728e74..454b87f 100644 --- a/src/lib/services/deezer.ts +++ b/src/lib/services/deezer.ts @@ -139,8 +139,16 @@ export class DeezerAPI { if (hasError) { const errorStr = JSON.stringify(resultJson.error); + const errorObj = resultJson.error as any; console.error(`[ERROR] API returned error for ${method}:`, errorStr); + // 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...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + return this.apiCall(method, args, params, retryCount); + } + // Handle invalid token - retry with new token (max 2 retries) if ((errorStr.includes('invalid api token') || errorStr.includes('Invalid CSRF token')) && retryCount < 2) { console.log(`[DEBUG] Invalid token, fetching new token... (retry ${retryCount + 1}/2)`); @@ -167,8 +175,17 @@ export class DeezerAPI { console.log(`[DEBUG] Returning results for ${method}`); return resultJson.results; - } catch (error) { + } catch (error: any) { console.error('[ERROR] deezer.gw', method, args, error); + + // Retry on network errors (connection issues, timeouts, etc.) + const networkErrors = ['ECONNABORTED', 'ECONNREFUSED', 'ECONNRESET', 'ENETRESET', 'ETIMEDOUT']; + if (error.code && networkErrors.includes(error.code)) { + console.log(`[DEBUG] Network error (${error.code}), waiting 2s before retry...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return this.apiCall(method, args, params, retryCount); + } + throw error; } } @@ -272,7 +289,7 @@ export class DeezerAPI { } // Get track download URL - async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string): Promise { + async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string, retryCount: number = 0): Promise { console.log('[DEBUG] Getting track download URL...', { trackToken, format, licenseToken }); try { @@ -316,9 +333,18 @@ export class DeezerAPI { console.error('[ERROR] No download URL in response:', result); return null; - } catch (error) { + } catch (error: any) { console.error('[ERROR] Failed to get track download URL:', error); - throw error; // Re-throw to let caller handle it + + // Retry on network errors + const networkErrors = ['ECONNABORTED', 'ECONNREFUSED', 'ECONNRESET', 'ENETRESET', 'ETIMEDOUT']; + if (error.code && networkErrors.includes(error.code)) { + console.log(`[DEBUG] Network error (${error.code}) getting download URL, waiting 2s before retry...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return this.getTrackDownloadUrl(trackToken, format, licenseToken, retryCount); + } + + throw error; } } } diff --git a/src/lib/services/deezer/downloader.ts b/src/lib/services/deezer/downloader.ts index 30c2aa2..0ef038a 100644 --- a/src/lib/services/deezer/downloader.ts +++ b/src/lib/services/deezer/downloader.ts @@ -24,7 +24,8 @@ export async function downloadTrack( downloadURL: string, musicFolder: string, format: string, - onProgress?: ProgressCallback + onProgress?: ProgressCallback, + retryCount: number = 0 ): Promise { // Generate paths const paths = generateTrackPath(track, musicFolder, format, false); @@ -50,14 +51,20 @@ export async function downloadTrack( console.log('Temp path:', paths.tempPath); try { - // Fetch the track with streaming + // Fetch the track with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout + const response = await fetch(downloadURL, { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36' - } + }, + signal: controller.signal }); + clearTimeout(timeoutId); + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -100,7 +107,7 @@ export async function downloadTrack( console.log('Download complete!'); return finalPath; - } catch (error) { + } catch (error: any) { // Clean up temp file on error try { if (await exists(paths.tempPath)) { @@ -110,6 +117,18 @@ export async function downloadTrack( console.error('Error cleaning up temp file:', cleanupError); } + // Retry on network errors or timeout (max 3 retries) + const networkErrors = ['ECONNABORTED', 'ECONNREFUSED', 'ECONNRESET', 'ENETRESET', 'ETIMEDOUT']; + const isNetworkError = error.code && networkErrors.includes(error.code); + const isTimeout = error.name === 'AbortError'; + + if ((isNetworkError || isTimeout) && retryCount < 3) { + const errorType = isTimeout ? 'timeout' : error.code; + console.log(`[DEBUG] Download ${errorType}, waiting 2s before retry (${retryCount + 1}/3)...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return downloadTrack(track, downloadURL, musicFolder, format, onProgress, retryCount + 1); + } + throw error; } }