This is a JS helper that I wrote for interacting with the BGG API. Currently only fetches the requested game(s), but should be a good base for expanding upon.
Created
January 28, 2022 09:27
-
-
Save remybach/a788337b5580e8aeb08393845c76cd94 to your computer and use it in GitHub Desktop.
Board Game Geek Server side JS helper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fetch = require("@adobe/node-fetch-retry"); | |
const fetchres = require("fetchres"); | |
const { parseStringPromise } = require("xml2js"); | |
// It appears that the node-fetch-retry lib modifies the retryOptions on this object if passed by reference so when using it do it using `{...fetchOptions}` | |
// Spreading no longer needs to happen once https://github.com/adobe/node-fetch-retry/pull/52 is merged and released. | |
const fetchOptions = { | |
retryOptions: { | |
retryInitialDelay: 1000, | |
retryOnHttpResponse: response => response.status >= 400 // retry on all 5xx and all 4xx errors | |
} | |
}; | |
/** | |
* Board Game Geek Schema | |
* | |
* @typedef {Object} BoardGameGeekItem | |
* @property {number} bggRating The BGG Rating for this Board Game | |
* @property {number} minAge Minimum recommended age | |
* @property {number} minPlayers Minimum players for this Board Game | |
* @property {number} maxPlayers Maximum players for this Board Game | |
* @property {number} playingTime Playing time for this Board Game | |
* @property {number} minPlayingTime Minimum playing time for this Board Game | |
* @property {number} maxPlayingTime Maximum playing time for this Board Game | |
* @property {number} numberOfPlays Number of plays logged for this Board Game on Board Game Geek | |
* @property {number} bggWeight The BGG Weight Rating for this Board Game | |
*/ | |
/** | |
* Board Game Geek Schema | |
* | |
* @typedef {Object.<string, BoardGameGeekItem>} BoardGameGeekItems | |
*/ | |
/** | |
* Drills down and extracts a number from the given BGG game data | |
* | |
* @param {Object} gameData The response data from the BGG API | |
* @param {string} property The property to drill into to extract the data from | |
* @param {string} [key] [Optional] The final key to extract the number from (default: value) | |
* @returns {number|null} Either returns the number or null depending on whether that datum was able to be extracted. | |
*/ | |
function extractNumber(gameData, property, key = "value") { | |
if (gameData && gameData[property] && gameData[property].length) { | |
const value = gameData[property][0].$[key]; | |
return value ? Number(value) : null; | |
} | |
return; | |
} | |
/** | |
* Drills down into the statistics->ratings of the given BGG game data and extracts a number. | |
* | |
* @param {Object} gameData The response data from the BGG API | |
* @param {string} property The property to drill into to extract the data from | |
* @returns {number|null} Either returns the number or null depending on whether that datum was able to be extracted. | |
*/ | |
function extractRating(gameData, property) { | |
if (gameData && gameData.statistics && gameData.statistics[0].ratings) { | |
const rating = extractNumber(gameData.statistics[0].ratings[0], property); | |
// Round to 1 decimal to avoid having to update for every minor change in rating. | |
return Math.round((rating + Number.EPSILON) * 10) / 10; | |
} | |
return; | |
} | |
/** | |
* Call the boardgamegeek.com API for this Board Game using the provided BGG ID. | |
* | |
* @param {string} bggID The ID for the Board Games on BGG. | |
* | |
* @returns {BoardGameGeekItems} An object containing Board Games info with the BGG ID as the key. | |
*/ | |
async function fetchPlayTotal(bggID) { | |
if (!bggID) { | |
throw new Error("Missing `bggID` when calling fetchPlayTotal"); | |
} | |
const playsDataResponse = await fetch(`https://www.boardgamegeek.com/xmlapi2/plays?username=${process.env.BGG_USERNAME}&id=${bggID}`, {...fetchOptions}); | |
const xmlPlaysData = await fetchres.text(playsDataResponse); | |
const playsData = await parseStringPromise(xmlPlaysData); | |
return playsData?.plays?.$?.total ? Number(playsData?.plays?.$?.total) : null; | |
} | |
/** | |
* Call the boardgamegeek.com API for the requested board games using the provided BGG ID. | |
* | |
* @param {string} bggIDs The comma separated string of IDs for the Board Games on BGG. | |
* | |
* @returns {BoardGameGeekItems} An object containing Board Games info with the BGG ID as the key. | |
*/ | |
async function fetchData(bggIDs) { | |
if (!bggIDs) { | |
throw new Error("Missing `bggIDs` when calling fetchData"); | |
} | |
console.time("fetchData"); | |
let boardGames = {}; | |
const gameDataResponse = await fetch(`https://www.boardgamegeek.com/xmlapi2/thing?id=${bggIDs}&stats=1`, {...fetchOptions}); | |
const xmlGameData = await fetchres.text(gameDataResponse); | |
let gameData = await parseStringPromise(xmlGameData); | |
for (let game of gameData?.items?.item) { | |
boardGames[game.$.id] = { | |
bggRating: extractRating(game, "average"), | |
bggWeight: extractRating(game, "averageweight"), | |
minAge: extractNumber(game, "minage"), | |
minPlayers: extractNumber(game, "minplayers"), | |
maxPlayers: extractNumber(game, "maxplayers"), | |
playingTime: extractNumber(game, "playingtime"), | |
minPlayingTime: extractNumber(game, "minplaytime"), | |
maxPlayingTime: extractNumber(game, "maxplaytime"), | |
numberOfPlays: await fetchPlayTotal(game.$.id) | |
} | |
} | |
console.timeEnd("fetchData"); | |
return boardGames; | |
} | |
/** | |
* Return the boardgamegeek.com API data for this Board Game using the provided BGG ID. | |
* | |
* @param {string[]} bggIDs The ID for this Board Game on BGG. | |
* | |
* @returns {BoardGameGeekItem} | |
*/ | |
const get = async (bggIDs) => { | |
return fetchData(bggIDs); | |
}; | |
module.exports = { | |
get, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment