communitybuilds-node
Version:
Node.js API for Genshin Impact communitiy builds spreadsheet
181 lines • 6.77 kB
JavaScript
import { googleSheetsApidomain, httpHeaders, spreadsheetPath } from './constants.js';
import { LRUCache } from 'lru-cache';
import myHttp from './http.js';
import * as TE from 'fp-ts/TaskEither';
let key;
let params;
let config;
let cache;
const mapping = {
pyro: 'Pyro%20',
hydro: 'Hydro%20',
anemo: 'Anemo%20',
electro: 'Electro%20',
dendro: 'Dendro',
cryo: 'Cryo%20',
geo: 'Geo%20',
};
/**
* Make api key available at module level
* @param apiKey Google console api key
*/
export const setApiKey = (apiKey) => {
if (apiKey.length < 39)
throw Error("Must set a valid Google API key");
key = apiKey;
params = `alt=json&key=${key}`;
};
/**
* Make configs available at module level
* @param aconfig Config object
*/
export const setConfig = (aconfig) => {
config = aconfig;
if (!config.eludeCaching) {
cache = new LRUCache({
max: 10,
ttl: config.cacheTTLms ?? 1000 * 60,
allowStale: false,
updateAgeOnGet: false,
updateAgeOnHas: false,
});
}
};
/**
* Retrieve all characters build by element by parsing Google Spreadsheets JSON values.
* @param element Genshin Impact element
* @returns Standard api response
*/
async function getBuildsByElement(element) {
if (!key) {
throw `no api key provided`;
}
if (!config.eludeCaching && cache.peek(element)) {
return cache.get(element);
}
// retrieve the spreadsheet as a JSON provided by Google
const res = await myHttp.json(googleSheetsApidomain, {
path: `${spreadsheetPath}${mapping[element]}?${params}`,
headers: httpHeaders
});
// the response will be an array of characters' builds
const response = [];
/* represent the offset between a build to another. This because the json
response is highly inconsistent */
const offsets = [];
// Skip the first 4 line as they're table headers
for (let i = 4; i < res.length; i++) {
/* get the lenght of the sub stats array as it will vary from
character to character */
if (res[i].length === 7) {
if (res[i][6] === 'SUBSTATS') {
offsets.push(i);
}
}
// calculate the offset between sections
if (res[i].length === 9) {
if (res[i][2] === 'ROLE') {
offsets.push(i);
}
}
}
/* build the response object start iterate the sub arrays
generated between sections */
for (let i = 0; i < offsets.length; i++) {
// calculate how many builds there are in a section
let nBuilds = (offsets[i + 1] - offsets[i]) + 1;
/* we reached the end of the main array, so the last offset is
is calculated from the end of the main array */
if (Number.isNaN(nBuilds)) {
nBuilds = (res.length - offsets[i]);
}
// generate the sub-array / silce
const builds = res.slice(offsets[i] + 2, offsets[i] + nBuilds + 3);
const notes = builds.at(-1)?.at(-1);
// buffer for the intermediate output
const buildsBuff = [];
// charachter name
const name = String(res[offsets[i]][1]).toLowerCase().trim();
const nextName = String(res[offsets[i + 1]]?.at(1)).toLowerCase().trim();
// generate the build by role
for (let i = 0; i < builds.length; i++) {
if (builds[i + 1]?.at(2) == "ROLE" && nextName != name)
break;
const role = builds[i][2];
const equipment = builds[i][3];
const artifacts = builds[i][4];
const artifactsMainStats = builds[i][5];
const artifactsSubStats = builds[i][6];
const talentPriority = builds[i][7];
let _artifactsMainStats = (artifactsMainStats ?? "")
.split("\n")
.map(x => x.split(' - '));
const _artifactsMainsStatsObj = {
sands: _artifactsMainStats ? _artifactsMainStats.at(0)?.at(1) : '',
goblet: _artifactsMainStats ? _artifactsMainStats.at(1)?.at(1) : '',
circlet: _artifactsMainStats ? _artifactsMainStats.at(2)?.at(1) : '',
};
// format the output
if (!(name && role && equipment && artifacts))
continue;
if (name && role && equipment && artifacts) {
buildsBuff.push({
role: role
.replace("✩", "")
.replace("\n", "")
.toLowerCase()
.trim(),
equipment: equipment
.replace(/(\d{1,2}\.)/gm, "")
.replace(/\([4-5]✩\)/gm, "")
.split("\n")
.filter(Boolean)
.map((a) => a.trim()),
artifacts: artifacts
.replace(/(\d{1,2}\.)/gm, "")
.replace(/\([4-5]✩\)/gm, "")
.split('\n')
.filter(Boolean)
.map((a) => a.trim()),
artifactsSubStats: artifactsSubStats
.replace(/(\d{1,2}\.)/gm, "")
.replace(/\([4-5]✩\)/gm, "")
.split('\n')
.filter(Boolean)
.map((a) => a.trim()),
artifactsMainStats: _artifactsMainsStatsObj,
talentPriority: talentPriority
.replace(/(\d{1,2}\.)/gm, "")
.replace(/\([4-5]✩\)/gm, "")
.split('\n')
.filter(Boolean)
.map((a) => a.trim()),
optimal: role.includes("✩"),
});
}
}
// each object is composed by name and the array of the relative builds
if (name != "") {
response.push({
name: name.replace('\n', ' '),
builds: buildsBuff,
notes: notes,
});
}
}
// if there is output try to resolve the promise, alternatively reject it
if (response.length > 0) {
if (!config.eludeCaching) {
cache.set(element, { data: response });
}
return { data: response };
}
else {
if (!config.eludeCaching) {
cache.clear();
}
throw `${element} has zero builds`;
}
}
export const getBuildsByElementTask = TE.tryCatchK(getBuildsByElement, (e) => `failed to fetch builds for element, error: ${e}`);
//# sourceMappingURL=retrieve.js.map