enkanetwork
Version:
API wrapper for enka.network written on TypeScript which provides localization, caching and convenience
201 lines (200 loc) • 9.93 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AssetsUpdater = void 0;
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
// @ts-ignore: JSON IMPORT OUT OF BASE-DIR
const config_json_1 = __importDefault(require("../../../assets/config.json"));
const errors_1 = require("../../errors");
const constants_1 = require("./constants");
async function request(url) {
const res = await fetch(constants_1.BASE_URL + url);
if (!res.ok)
throw new errors_1.AssetsUpdateError(url, res);
return res.json();
}
class AssetsUpdater {
languages;
isFetching;
dataManager;
localizationManager;
constructor({ languages = ["EN"], checkInterval = 30 * 60 * 1000, // 0.5 hour
instant = false, }, dataManager, localizationManager) {
if (!languages.length)
throw new Error("The languages value must contain at least 1 language");
if (languages.filter((language) => !constants_1.localizationLanguages.includes(language))
.length)
throw new Error(`Use only supported languages! (${constants_1.localizationLanguages.join(", ")})`);
this.dataManager = dataManager;
this.localizationManager = localizationManager;
// This is done so that the languages are not repeated and are not requested again
this.languages = [...new Set(languages)];
this.isFetching = false;
if (instant)
this.fetchAssets();
if (checkInterval)
setInterval(() => this.fetchAssets().catch(() => { }), checkInterval);
}
// function that checks the relevance of assets data and localization
async fetchAssets() {
if (this.isFetching)
throw new Error("Content is already fetching!");
this.isFetching = true;
const res = await fetch(`${constants_1.PROJECT_GITLAB_URL}?since=${config_json_1.default.lastUpdate}`);
const data = await res.json();
if (!data.length &&
!this.languages.filter((language) => !config_json_1.default.languages.includes(language)).length &&
config_json_1.default.languages.length) {
this.isFetching = false;
return;
}
const skillIds = [];
const [charactersData, costumesData, constellationsData, skillsetsData, skillsData, weaponsData, namecardsData, reliquaryData, reliquarySetData, reliquaryAffixData,] = await Promise.all([
request(constants_1.CHARACTER_DATA_URL),
request(constants_1.COSTUME_DATA_URL),
request(constants_1.CONSTELLATION_DATA_URL),
request(constants_1.SKILLSET_DATA_URL),
request(constants_1.SKILL_DATA_URL),
request(constants_1.WEAPON_DATA_URL),
request(constants_1.NAMECARD_DATA_URL),
request(constants_1.RELIQUARY_DATA_URL),
request(constants_1.RELIQUARY_SET_DATA_URL),
request(constants_1.RELIQUARY_AFFIX_DATA_URL),
]);
const characters = charactersData
.filter((character) => !constants_1.excludeCharacters.includes(character.id))
.map((character) => {
const skillset = skillsetsData.find((x) => x.id === character.skillDepotId);
if (!skillset)
return null;
const energyBurst = skillsData.find((x) => x.id === skillset?.energySkill);
if (!energyBurst)
return null;
skillIds.push(...skillset.skills.concat(skillset?.energySkill).filter(Boolean));
return {
id: character.id,
skillDepotId: character.skillDepotId,
nameTextMapHash: character.nameTextMapHash,
iconName: character.iconName,
sideIconName: character.sideIconName,
gachaIcon: `UI_Gacha_AvatarImg_${character.iconName
.split("_")
.at(-1)}`,
qualityStars: constants_1.qualityTypesStars[character.qualityType],
element: constants_1.elements[energyBurst.costElemType],
skills: skillset.skills.concat(skillset?.energySkill).filter(Boolean),
talents: skillset.talents.filter(Boolean),
};
})
.filter(Boolean);
// Aether and Lumine problem
for (const character of charactersData) {
if (character.id !== 10000005 && character.id !== 10000007)
continue;
for (const skillDepotId of character.candSkillDepotIds) {
const skillset = skillsetsData.find((x) => x.id === skillDepotId);
if (!skillset)
continue;
const energyBurst = skillsData.find((x) => x.id === skillset?.energySkill);
if (!energyBurst)
continue;
skillIds.push(...skillset.skills.filter(Boolean));
characters.push({
id: character.id,
skillDepotId,
nameTextMapHash: character.nameTextMapHash,
iconName: character.iconName,
sideIconName: character.sideIconName,
gachaIcon: `UI_Gacha_AvatarImg_${character.iconName
.split("_")
.at(-1)}`,
qualityStars: constants_1.qualityTypesStars[character.qualityType],
element: constants_1.elements[energyBurst.costElemType],
skills: skillset.skills.filter(Boolean),
talents: skillset.talents.filter(Boolean),
});
}
}
const skills = skillsData
.filter((skill) => skillIds.includes(skill.id))
.map((skill) => ({
id: skill.id,
nameTextMapHash: skill.nameTextMapHash,
skillIcon: skill.skillIcon,
proudSkillGroupId: skill.proudSkillGroupId,
}));
const constellations = constellationsData.map((constellation) => ({
id: constellation.talentId,
nameTextMapHash: constellation.nameTextMapHash,
icon: constellation.icon,
}));
const costumes = costumesData.map((costume) => ({
// id: Object.values(costume).at(0),
id: costume.skinId,
iconName: costume.sideIconName.replace("_Side", ""),
sideIconName: costume.sideIconName,
gachaIcon: `UI_Costume_${costume.sideIconName.split("_").at(-1)}`,
nameTextMapHash: costume.nameTextMapHash,
}));
const namecards = namecardsData
.filter((namecard) => namecard.materialType === "MATERIAL_NAMECARD")
.map((namecard) => ({
id: namecard.id,
nameTextMapHash: namecard.nameTextMapHash,
icon: namecard.icon,
navbar: namecard.picPath.at(0) || null,
banner: namecard.picPath.at(1),
}));
await Promise.all([
this.dataManager.writeCharacters(characters),
this.dataManager.writeSkills(skills),
this.dataManager.writeConstellations(constellations),
this.dataManager.writeCostumes(costumes),
this.dataManager.writeNamecards(namecards),
]);
const reliquarySets = reliquarySetData
.map((reliquarySet) => {
const reliquaryAffix = reliquaryAffixData.find((x) => x.id === reliquarySet.equipAffixId && x.level === 1);
return reliquaryAffix?.nameTextMapHash;
})
// Why doesn't the typescript compiler understand that it won't be undefined anymore ???
// Issue - https://github.com/microsoft/TypeScript/issues/45097
.filter(Boolean);
//lang imports
const nameTextHashMaps = {
characters: characters.map((character) => character?.nameTextMapHash),
costumes: costumes.map((costume) => costume.nameTextMapHash),
constellations: constellations.map((constellation) => constellation.nameTextMapHash),
namecards: namecards.map((namecard) => namecard.nameTextMapHash),
skills: skills.map((skill) => skill.nameTextMapHash),
weapons: weaponsData.map((weapon) => weapon.nameTextMapHash),
reliquary: reliquaryData.map((reliquary) => reliquary.nameTextMapHash),
reliquarySets,
};
const languagesData = await Promise.all(this.languages.map(async (lang) => {
return request(`${constants_1.LOCALIZATION_BASE_URL + lang}.json`);
}));
for (const dataType in nameTextHashMaps) {
const assetType = dataType;
const nameTextHashes = nameTextHashMaps[assetType];
const localization = nameTextHashes.reduce((acc, nameTextHash) => {
acc[String(nameTextHash)] = {};
this.languages.forEach((lang, index) => {
// I refer to the languagesData by index since Promise.all guarantees the preservation of order
acc[String(nameTextHash)][lang] =
languagesData[index][nameTextHash] ?? "UNKNOWN";
});
return acc;
}, {});
await this.localizationManager.write(assetType, localization);
}
this.isFetching = false;
config_json_1.default.lastUpdate = new Date().toISOString();
config_json_1.default.languages = this.languages;
await promises_1.default.writeFile(node_path_1.default.resolve(constants_1.ASSETS_PATH, "config.json"), JSON.stringify(config_json_1.default, null, 4));
}
}
exports.AssetsUpdater = AssetsUpdater;