@@ -58,19 +58,27 @@ main();
5858
5959async function main ( ) {
6060 // Fetch Game Pass game ID's and properties for each pass type and market specified in the configuration
61- // We do this in parallel to speed up the process
62- // While the functions do return the formatted properties, we currently do not use them here, as writing the output files is handled by the functions themselves
61+ // The tasks run in parallel to speed up the process
62+ // Each one writes its own output file
63+ const tasks = [ ] ;
6364 for ( const market of CONFIG . markets ) {
64- if ( CONFIG . platformsToFetch . includes ( "console" ) ) {
65- const consoleFormattedProperties = runScriptForPassTypeAndMarket ( "console" , market ) ;
66- }
67- if ( CONFIG . platformsToFetch . includes ( "pc" ) ) {
68- const pcFormattedProperties = runScriptForPassTypeAndMarket ( "pc" , market ) ;
69- }
70- if ( CONFIG . platformsToFetch . includes ( "eaPlay" ) ) {
71- const eaPlayFormattedProperties = runScriptForPassTypeAndMarket ( "eaPlay" , market ) ;
65+ for ( const passType of [ "console" , "pc" , "eaPlay" ] ) {
66+ if ( CONFIG . platformsToFetch . includes ( passType ) ) {
67+ tasks . push (
68+ runScriptForPassTypeAndMarket ( passType , market ) . catch ( ( error ) => {
69+ console . error ( `\nError fetching ${ passType } games for market "${ market } ": ${ error . message ?? error } ` ) ;
70+ return { failed : true } ;
71+ } )
72+ ) ;
73+ }
7274 }
7375 }
76+
77+ const failures = ( await Promise . all ( tasks ) ) . filter ( ( result ) => result && result . failed ) . length ;
78+ if ( failures > 0 ) {
79+ console . error ( `\n${ failures } of ${ tasks . length } fetch task(s) failed. See the errors above.` ) ;
80+ process . exit ( 1 ) ;
81+ }
7482}
7583
7684async function runScriptForPassTypeAndMarket ( passType , market ) {
@@ -94,32 +102,50 @@ async function fetchGameIDs(passType, market) {
94102 }
95103
96104 console . log ( `Fetching ${ passType } Game Pass game ID's for market "${ market } "...` ) ;
97- let gameIds = await fetch ( `https://catalog.gamepass.com/sigls/v2?id=${ APIIds [ passType ] } &language=${ CONFIG . language } &market=${ market } ` )
98- . then ( ( response ) => response . json ( ) )
99- . then ( ( data ) => data . filter ( ( entry ) => entry . id ) . map ( ( entry ) => entry . id ) ) ;
105+ const response = await fetch ( `https://catalog.gamepass.com/sigls/v2?id=${ APIIds [ passType ] } &language=${ CONFIG . language } &market=${ market } ` ) ;
106+ if ( ! response . ok ) {
107+ throw new Error ( `The Game Pass catalog API responded with status ${ response . status } ${ response . statusText ? ` ${ response . statusText } ` : "" } .` ) ;
108+ }
109+
110+ let data ;
111+ try {
112+ data = await response . json ( ) ;
113+ } catch ( error ) {
114+ throw new Error ( `Could not parse the Game Pass catalog API response as JSON: ${ error . message ?? error } ` ) ;
115+ }
100116
101- return gameIds ;
117+ return data . filter ( ( entry ) => entry . id ) . map ( ( entry ) => entry . id ) ;
102118}
103119
104120async function fetchGameProperties ( gameIds , passType , market ) {
105121 console . log ( `Fetching game properties for ${ gameIds . length } ${ passType } games for market "${ market } "...` ) ;
106- return await fetch ( `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${ gameIds } &market=${ market } &languages=${ CONFIG . language } ` )
107- . then ( ( response ) => response . json ( ) )
108- . then ( ( data ) => {
109- if ( CONFIG . keepCompleteProperties ) {
110- fs . writeFileSync ( `./output/completeGameProperties_${ passType } _${ market } .json` , JSON . stringify ( data , null , 2 ) ) ;
111- }
112- return data ;
113- } ) ;
122+ const response = await fetch ( `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${ gameIds } &market=${ market } &languages=${ CONFIG . language } ` ) ;
123+ if ( ! response . ok ) {
124+ throw new Error ( `The Microsoft display catalog API responded with status ${ response . status } ${ response . statusText ? ` ${ response . statusText } ` : "" } .` ) ;
125+ }
126+
127+ let data ;
128+ try {
129+ data = await response . json ( ) ;
130+ } catch ( error ) {
131+ throw new Error ( `Could not parse the Microsoft display catalog API response as JSON: ${ error . message ?? error } ` ) ;
132+ }
133+
134+ if ( CONFIG . keepCompleteProperties ) {
135+ fs . writeFileSync ( `./output/completeGameProperties_${ passType } _${ market } .json` , JSON . stringify ( data , null , 2 ) ) ;
136+ }
137+
138+ return data ;
114139}
115140
116141// Format the data according to the configuration
117142function formatData ( gameProperties , passType ) {
118- console . log ( `Formatting game properties for ${ gameProperties . Products . length } ${ passType } games...` ) ;
143+ const products = gameProperties . Products ?? [ ] ;
144+ console . log ( `Formatting game properties for ${ products . length } ${ passType } games...` ) ;
119145
120146 let formattedData = CONFIG . outputFormat === "array" ? [ ] : { } ;
121147
122- for ( const game of gameProperties . Products ) {
148+ for ( const game of products ) {
123149 let index ;
124150 switch ( CONFIG . outputFormat ) {
125151 case "array" :
@@ -202,42 +228,42 @@ function getPropertyValue(game, property, propertyValue) {
202228function getProductTitle ( game , productTitleProperty ) {
203229 if ( ! productTitleProperty ) { return undefined ; }
204230
205- return game . LocalizedProperties [ 0 ] . ProductTitle . length > 0
231+ return game . LocalizedProperties ?. [ 0 ] ? .ProductTitle ? .length > 0
206232 ? game . LocalizedProperties [ 0 ] . ProductTitle
207233 : emptyValuePlaceholder ;
208234}
209235
210236function getProductId ( game , productIdProperty ) {
211237 if ( ! productIdProperty ) { return undefined ; }
212238
213- return game . ProductId . length > 0
239+ return game . ProductId ? .length > 0
214240 ? game . ProductId
215241 : emptyValuePlaceholder ;
216242}
217243
218244function getDeveloperName ( game , developerNameProperty ) {
219245 if ( ! developerNameProperty ) { return undefined ; }
220246
221- return game . LocalizedProperties [ 0 ] . DeveloperName . length > 0
247+ return game . LocalizedProperties ?. [ 0 ] ? .DeveloperName ? .length > 0
222248 ? game . LocalizedProperties [ 0 ] . DeveloperName
223249 : emptyValuePlaceholder ;
224250}
225251
226252function getPublisherName ( game , publisherNameProperty ) {
227253 if ( ! publisherNameProperty ) { return undefined ; }
228254
229- return game . LocalizedProperties [ 0 ] . PublisherName . length > 0
255+ return game . LocalizedProperties ?. [ 0 ] ? .PublisherName ? .length > 0
230256 ? game . LocalizedProperties [ 0 ] . PublisherName
231257 : emptyValuePlaceholder ;
232258}
233259
234260function getProductDescription ( game , productDescriptionProperty ) {
235261 if ( ! productDescriptionProperty . enabled ) { return undefined ; }
236262
237- if ( productDescriptionProperty . preferShort && game . LocalizedProperties [ 0 ] . ShortDescription ?. length > 0 ) {
263+ if ( productDescriptionProperty . preferShort && game . LocalizedProperties ?. [ 0 ] ? .ShortDescription ?. length > 0 ) {
238264 return game . LocalizedProperties [ 0 ] . ShortDescription ;
239265 } else {
240- return game . LocalizedProperties [ 0 ] . ProductDescription ?. length > 0
266+ return game . LocalizedProperties ?. [ 0 ] ? .ProductDescription ?. length > 0
241267 ? game . LocalizedProperties [ 0 ] . ProductDescription
242268 : emptyValuePlaceholder ;
243269 }
@@ -259,29 +285,37 @@ function getImages(game, imageProperty) {
259285 "FeaturePromotionalSquareArt" : 0
260286 } ;
261287
262- for ( const image of game . LocalizedProperties [ 0 ] . Images ) {
263- if ( imageProperty . imageTypes [ image . ImagePurpose ] && ( imageProperty . imageTypes [ image . ImagePurpose ] === - 1 || numImagesByType [ image . ImagePurpose ] < imageProperty . imageTypes [ image . ImagePurpose ] ) ) {
264- if ( ! images [ image . ImagePurpose ] ) {
265- images [ image . ImagePurpose ] = [ ] ;
266- }
267- images [ image . ImagePurpose ] . push ( image . Uri . startsWith ( 'https:' ) ? image . Uri : `https:${ image . Uri } ` ) ;
268- numImagesByType [ image . ImagePurpose ] = numImagesByType [ image . ImagePurpose ] ? numImagesByType [ image . ImagePurpose ] + 1 : 1 ;
288+ for ( const image of game . LocalizedProperties ?. [ 0 ] ?. Images ?? [ ] ) {
289+ const limit = imageProperty . imageTypes [ image . ImagePurpose ] ;
290+ if ( ! limit || ( limit !== - 1 && numImagesByType [ image . ImagePurpose ] >= limit ) ) {
291+ continue ;
269292 }
293+
294+ const uri = image . Uri . startsWith ( 'https:' ) ? image . Uri : `https:${ image . Uri } ` ;
295+
296+ // Skip true duplicates - the same URL can appear more than once in the API response
297+ if ( images [ image . ImagePurpose ] ?. includes ( uri ) ) {
298+ continue ;
299+ }
300+
301+ ( images [ image . ImagePurpose ] ??= [ ] ) . push ( uri ) ;
302+ numImagesByType [ image . ImagePurpose ] = ( numImagesByType [ image . ImagePurpose ] ?? 0 ) + 1 ;
270303 }
271304 return images ;
272305}
273306
274307function getReleaseDate ( game , releaseDateProperty ) {
275308 if ( ! releaseDateProperty . enabled ) { return undefined ; }
276309
310+ const releaseDate = game . MarketProperties ?. [ 0 ] ?. OriginalReleaseDate ;
311+ if ( ! releaseDate || releaseDate . length === 0 ) {
312+ return emptyValuePlaceholder ;
313+ }
314+
277315 if ( releaseDateProperty . format === "date" ) {
278- return game . MarketProperties [ 0 ] . OriginalReleaseDate . length > 0
279- ? game . MarketProperties [ 0 ] . OriginalReleaseDate ?. split ( "T" ) [ 0 ]
280- : emptyValuePlaceholder ;
316+ return releaseDate . split ( "T" ) [ 0 ] ;
281317 } else if ( releaseDateProperty . format === "dateTime" ) {
282- return game . MarketProperties [ 0 ] . OriginalReleaseDate . length > 0
283- ? game . MarketProperties [ 0 ] . OriginalReleaseDate
284- : emptyValuePlaceholder ;
318+ return releaseDate ;
285319 } else {
286320 // Due to our config validation, this should never happen, but just in case...
287321 console . log ( `Invalid release date format: ${ releaseDateProperty . format } ` ) ;
@@ -299,7 +333,12 @@ function getUserRating(game, userRatingProperty) {
299333 }
300334
301335 // Get the x-out-of-5 stars rating
302- let userRating = game . MarketProperties [ 0 ] . UsageData [ intervalMapping [ userRatingProperty . aggregationInterval ] ] ?. AverageRating ;
336+ let userRating = game . MarketProperties ?. [ 0 ] ?. UsageData ?. [ intervalMapping [ userRatingProperty . aggregationInterval ] ] ?. AverageRating ;
337+
338+ // Games without any rating data for the requested interval
339+ if ( typeof userRating !== "number" ) {
340+ return emptyValuePlaceholder ;
341+ }
303342
304343 // Convert to a percentage if requested
305344 if ( userRatingProperty . format === "percentage" ) {
@@ -330,12 +369,13 @@ function getPricing(game, pricingProperty) {
330369 }
331370
332371 let prices = { } ;
333- prices [ "currencyCode" ] = game . DisplaySkuAvailabilities [ 0 ] ?. Availabilities [ 0 ] ?. OrderManagementData ?. Price ?. CurrencyCode ;
372+ const price = game . DisplaySkuAvailabilities ?. [ 0 ] ?. Availabilities ?. [ 0 ] ?. OrderManagementData ?. Price ;
373+ prices [ "currencyCode" ] = price ?. CurrencyCode ;
334374
335375 for ( const priceType of pricingProperty . priceTypes ) {
336376 // Small workaround to not exclude 0-values
337- prices [ priceType ] = typeof game . DisplaySkuAvailabilities [ 0 ] . Availabilities [ 0 ] . OrderManagementData . Price [ priceType ] === 'number'
338- ? game . DisplaySkuAvailabilities [ 0 ] . Availabilities [ 0 ] . OrderManagementData . Price [ priceType ]
377+ prices [ priceType ] = typeof price ?. [ priceType ] === 'number'
378+ ? price [ priceType ]
339379 : missingPricePlaceholder ;
340380 }
341381
@@ -345,13 +385,12 @@ function getPricing(game, pricingProperty) {
345385function getCategories ( game , categoriesProperty ) {
346386 if ( ! categoriesProperty ) { return undefined ; }
347387
348- let categories = [ ] ;
349- if ( game . Properties . Categories ) {
350- categories = game . Properties . Categories ;
351- }
388+ const properties = game . Properties ?? { } ;
389+ let categories = Array . isArray ( properties . Categories ) ? [ ...properties . Categories ] : [ ] ;
390+
352391 // Each game also has a "main" category, which may or may not be included in the list of categories
353- if ( ! categories . includes ( game . Properties . Category ) ) {
354- categories . push ( game . Properties . Category ) ;
392+ if ( properties . Category && ! categories . includes ( properties . Category ) ) {
393+ categories . push ( properties . Category ) ;
355394 }
356395
357396 return categories ;
@@ -360,7 +399,7 @@ function getCategories(game, categoriesProperty) {
360399function getStorePageUrl ( game , storePageUrlProperty ) {
361400 if ( ! storePageUrlProperty ) { return undefined ; }
362401
363- if ( ! game . LocalizedProperties [ 0 ] . ProductTitle || ! game . ProductId ) {
402+ if ( ! game . LocalizedProperties ?. [ 0 ] ? .ProductTitle || ! game . ProductId ) {
364403 return undefined ;
365404 }
366405
0 commit comments