splatoon3.ink/app/data/updaters/FestivalUpdater.mjs
Matt Isenhower 8780463c66 Replace deprecated and unmaintained packages
- Removed mkdirp in favor of native fs.mkdir({ recursive: true })
- Replaced ecstatic (unmaintained since 2021) with sirv
- Replaced jsonpath (security vulnerability) with jsonpath-plus
  - Added jsonpathQuery/jsonpathApply helpers in app/common/util.mjs
- Updated sharp: 0.32.6 → 0.34.5
- Updated puppeteer-core: 23.8.0 → 24.37.3

Vulnerabilities reduced from 40 to 1 (only remaining: axios in
threads-api transitive dependency).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 11:05:49 -08:00

166 lines
4.4 KiB
JavaScript

import fs from 'fs/promises';
import pLimit from 'p-limit';
import { getFestId, jsonpathQuery, jsonpathApply } from '../../common/util.mjs';
import ValueCache from '../../common/ValueCache.mjs';
import { regionTokens } from '../../splatnet/NsoClient.mjs';
import FestivalRankingUpdater from './FestivalRankingUpdater.mjs';
import DataUpdater from './DataUpdater.mjs';
const limit = pLimit(1);
function generateFestUrl(id) {
return process.env.DEBUG ?
`https://s.nintendo.com/av5ja-lp1/znca/game/4834290508791808?p=/fest_record/${id}` :
`${process.env.SITE_URL ?? ''}/nso/f/${id}`;
}
export default class FestivalUpdater extends DataUpdater
{
name = 'Festivals';
filename = 'festivals';
calendarName = 'Splatoon 3 Splatfests';
calendarFilename = 'festivals';
constructor(region = null) {
super(region);
this.calendarName += ` (${region})`;
this.calendarFilename += `.${region}`;
}
imagePaths = [
'$..image.url',
];
localizations = [
{
key: 'festivals',
nodes: [
'$..festRecords.nodes.*',
],
id: '__splatoon3ink_id',
values: [
'title',
'teams.0.teamName',
'teams.1.teamName',
'teams.2.teamName',
],
},
];
shouldUpdate() {
const tokens = regionTokens();
if (!tokens[this.region]) {
this.console.log(`No token provided for ${this.region} region, skipping...`);
return false;
}
return super.shouldUpdate();
}
async getData(locale) {
let cursor = null;
let hasNextPage = true;
let result = { data: { festRecords: { nodes: [] } } };
while (hasNextPage) {
let data = await this.splatnet(locale).getFestRecordDataPage(cursor);
// Grab the nodes from the current page
result.data.festRecords.nodes.push(...jsonpathQuery(data, '$..festRecords.edges.*.node'));
// Update the cursor and next page indicator
cursor = data.data.festRecords.pageInfo.endCursor;
hasNextPage = data.data.festRecords.pageInfo.hasNextPage;
}
this.deriveFestivalIds(result);
// Get the detailed data for each Splatfest
// (unless we're getting localization-specific data)
if (locale === this.defaultLocale) {
await Promise.all(result.data.festRecords.nodes.map(async node => {
let detailResult = await this.getFestivalDetails(node);
Object.assign(node, detailResult.data.fest);
if (!this.settings.disableFestivalRankings && node.teams.find(t => t.result)) {
let rankingUpdater = new FestivalRankingUpdater(this.region, node.id, node.endTime);
try {
await rankingUpdater.updateIfNeeded();
} catch (e) {
this.console.error(e);
}
}
}));
}
return result;
}
deriveFestivalIds(data) {
jsonpathApply(data, '$..nodes.*', node => ({
'__splatoon3ink_id': getFestId(node.id),
...node,
}));
}
async getFestivalDetails(node) {
let cache = new ValueCache(`festivals.${node.id}`);
// We don't need to use the locale for this data
// since localization data retrieval happens elsewhere.
let data = await cache.getData();
let cachedAt = await cache.getCachedAt();
// How long until this festival ends/ended?
// We want to retrieve the latest data until 4 hours after the Splatfest ends
let cutoff = new Date(node.endTime);
cutoff.setHours(cutoff.getHours() + 4);
if (!data || cachedAt < cutoff) {
this.console.info(`Getting festival details for ${node.id}`);
data = await this.splatnet().getFestDetailData(node.id);
await cache.setData(data);
}
return data;
}
formatDataForWrite(data) {
return limit(() => this._formatDataForWrite(data));
}
async _formatDataForWrite(data) {
// Combine this region's data with the other regions' data.
let result = null;
try {
result = await fs.readFile(this.getPath(this.filename));
} catch (e) {
//
}
result = result ? JSON.parse(result) : {};
result[this.region] = data;
return super.formatDataForWrite(result);
}
*getCalendarEntries(data) {
for (const fest of data.data.festRecords.nodes) {
yield {
id: getFestId(fest.id),
title: `Splatfest (${this.region}): ${fest.teams.map(t => t.teamName).join(' vs. ')}`,
url: generateFestUrl(fest.id),
imageUrl: fest.image.url,
start: fest.startTime,
end: fest.endTime,
};
}
}
}