Skip to content

Instantly share code, notes, and snippets.

@remybach
Created January 28, 2022 09:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save remybach/a788337b5580e8aeb08393845c76cd94 to your computer and use it in GitHub Desktop.
Save remybach/a788337b5580e8aeb08393845c76cd94 to your computer and use it in GitHub Desktop.
Board Game Geek Server side JS helper

BGG JS Helper

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.

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