@@ -29,44 +29,132 @@ const moviesTmdbToAll = new Map<number, IdMap>();
2929
3030let isInitialized = false ;
3131
32- async function downloadCsv ( url : string , cachePath : string , etagKey : string ) : Promise < string > {
32+ async function downloadCsv ( url : string , cachePath : string , etagKey : string , maxRetries : number = 3 ) : Promise < string > {
3333 // Check ETag first
3434 if ( redis && redis . status === 'ready' ) {
3535 try {
3636 const savedEtag = await redis . get ( etagKey ) ;
3737 if ( savedEtag && fs . existsSync ( cachePath ) ) {
38- const { statusCode, headers } = await request ( url , { method : 'HEAD' } ) ;
39- const remoteEtag = headers . etag ;
40- if ( savedEtag === remoteEtag ) {
41- console . log ( `[Wiki Mapper] Using cache: ${ cachePath } ` ) ;
42- return fs . readFileSync ( cachePath , 'utf8' ) ;
38+ try {
39+ const { statusCode, headers } = await request ( url , { method : 'HEAD' } ) ;
40+ if ( statusCode === 200 && headers . etag ) {
41+ const remoteEtag = headers . etag ;
42+ if ( savedEtag === remoteEtag ) {
43+ console . log ( `[Wiki Mapper] Using cache: ${ cachePath } ` ) ;
44+ return fs . readFileSync ( cachePath , 'utf8' ) ;
45+ }
46+ } else if ( statusCode === 429 ) {
47+ // Rate limited on HEAD request, use cached data
48+ console . warn ( `[Wiki Mapper] Rate limited (429) on ETag check, using cached data: ${ cachePath } ` ) ;
49+ return fs . readFileSync ( cachePath , 'utf8' ) ;
50+ }
51+ } catch ( error : any ) {
52+ // If HEAD request fails (e.g., network error, 429), check if we have cached data to use
53+ const statusCode = error . statusCode || ( error . response && error . response . statusCode ) ;
54+ if ( statusCode === 429 || error . code === 'ECONNREFUSED' || error . code === 'ETIMEDOUT' || error . code ?. startsWith ( 'UND_ERR_' ) ) {
55+ console . warn ( `[Wiki Mapper] ETag check failed (${ statusCode || error . code } ), attempting to use cached data` ) ;
56+ if ( fs . existsSync ( cachePath ) ) {
57+ try {
58+ const cachedData = fs . readFileSync ( cachePath , 'utf8' ) ;
59+ console . log ( `[Wiki Mapper] Using cached data due to download error: ${ cachePath } ` ) ;
60+ return cachedData ;
61+ } catch ( cacheError : any ) {
62+ console . warn ( `[Wiki Mapper] Cached data unreadable: ${ cacheError . message } ` ) ;
63+ }
64+ }
65+ }
66+ console . warn ( `[Wiki Mapper] ETag check failed: ${ error . message } ` ) ;
4367 }
4468 }
45- } catch ( error ) {
69+ } catch ( error : any ) {
4670 console . warn ( `[Wiki Mapper] ETag check failed: ${ error . message } ` ) ;
4771 }
4872 }
4973
50- // Download fresh data using undici for speed
51- console . log ( `[Wiki Mapper] Downloading: ${ url } ` ) ;
52- const { statusCode, headers, body } = await request ( url ) ;
74+ // Download fresh data using undici with retry logic for 429 errors
75+ let lastError : Error | null = null ;
5376
54- if ( statusCode < 200 || statusCode >= 300 ) {
55- throw new Error ( `HTTP ${ statusCode } ` ) ;
56- }
57-
58- const csvData = await body . text ( ) ;
77+ for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
78+ try {
79+ if ( attempt > 0 ) {
80+ // Exponential backoff: 1s, 2s, 4s, 8s... (capped at 30s)
81+ const delay = Math . min ( 1000 * Math . pow ( 2 , attempt - 1 ) , 30000 ) ;
82+ console . log ( `[Wiki Mapper] Retrying download (attempt ${ attempt + 1 } /${ maxRetries + 1 } ) after ${ delay } ms: ${ url } ` ) ;
83+ await new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
84+ } else {
85+ console . log ( `[Wiki Mapper] Downloading: ${ url } ` ) ;
86+ }
87+
88+ const { statusCode, headers, body } = await request ( url ) ;
89+
90+ if ( statusCode === 429 ) {
91+ // Rate limited - will retry if attempts remain
92+ lastError = new Error ( `HTTP ${ statusCode } ` ) ;
93+ if ( attempt < maxRetries ) {
94+ continue ;
95+ }
96+ // Last attempt failed with 429, try to use cache
97+ console . warn ( `[Wiki Mapper] Rate limited (429) after ${ maxRetries + 1 } attempts, falling back to cache` ) ;
98+ if ( fs . existsSync ( cachePath ) ) {
99+ try {
100+ const cachedData = fs . readFileSync ( cachePath , 'utf8' ) ;
101+ console . log ( `[Wiki Mapper] Using cached data after rate limit: ${ cachePath } ` ) ;
102+ return cachedData ;
103+ } catch ( cacheError : any ) {
104+ console . error ( `[Wiki Mapper] Cached data unreadable: ${ cacheError . message } ` ) ;
105+ throw lastError ;
106+ }
107+ }
108+ throw lastError ;
109+ }
110+
111+ if ( statusCode < 200 || statusCode >= 300 ) {
112+ throw new Error ( `HTTP ${ statusCode } ` ) ;
113+ }
114+
115+ const csvData = await body . text ( ) ;
59116
60- // Save to cache
61- fs . mkdirSync ( path . dirname ( cachePath ) , { recursive : true } ) ;
62- fs . writeFileSync ( cachePath , csvData ) ;
117+ // Save to cache
118+ fs . mkdirSync ( path . dirname ( cachePath ) , { recursive : true } ) ;
119+ fs . writeFileSync ( cachePath , csvData ) ;
63120
64- // Save ETag
65- if ( redis && redis . status === 'ready' && headers . etag ) {
66- await redis . set ( etagKey , headers . etag ) ;
67- }
121+ // Save ETag
122+ if ( redis && redis . status === 'ready' && headers . etag ) {
123+ await redis . set ( etagKey , headers . etag ) ;
124+ }
68125
69- return csvData ;
126+ return csvData ;
127+ } catch ( error : any ) {
128+ lastError = error ;
129+
130+ // Check if it's a 429 error (either from statusCode check above or from error message)
131+ const is429 = error . message ?. includes ( 'HTTP 429' ) ||
132+ error . statusCode === 429 ||
133+ ( error . response && error . response . statusCode === 429 ) ;
134+
135+ // If it's a 429 and we have retries left, continue the loop
136+ if ( is429 && attempt < maxRetries ) {
137+ continue ;
138+ }
139+
140+ // If it's not a 429 or we're out of retries, try to use cached data
141+ if ( fs . existsSync ( cachePath ) ) {
142+ try {
143+ console . warn ( `[Wiki Mapper] Download failed (${ error . message } ), falling back to cached data: ${ cachePath } ` ) ;
144+ const cachedData = fs . readFileSync ( cachePath , 'utf8' ) ;
145+ return cachedData ;
146+ } catch ( cacheError : any ) {
147+ console . error ( `[Wiki Mapper] Cached data also unreadable: ${ cacheError . message } ` ) ;
148+ }
149+ }
150+
151+ // If no cache available or cache read failed, throw the original error
152+ throw error ;
153+ }
154+ }
155+
156+ // Should never reach here, but TypeScript needs it
157+ throw lastError || new Error ( 'Download failed after all retries' ) ;
70158}
71159
72160function loadMappings ( csvData : string , maps : { imdb : Map < string , IdMap > , tvdb : Map < number , IdMap > , tmdb : Map < number , IdMap > , tvmaze ?: Map < number , IdMap > } ) {
@@ -155,9 +243,11 @@ async function initialize() {
155243
156244 isInitialized = true ;
157245 console . log ( '[Wiki Mapper] Initialization complete' ) ;
158- } catch ( error ) {
159- console . error ( '[Wiki Mapper] Initialization failed:' , error ) ;
160- throw error ;
246+ } catch ( error : any ) {
247+ const errorMessage = error ?. message || String ( error ) ;
248+ console . error ( `[Wiki Mapper] Initialization failed: ${ errorMessage } ` ) ;
249+ // Re-throw with a more descriptive error message
250+ throw new Error ( `Wiki Mappings failed to initialize: ${ errorMessage } ` ) ;
161251 }
162252}
163253
0 commit comments