diff --git a/README.md b/README.md index 49fbe94..f9a745a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Monitor changes to responses from various Arknights Endfield APIs and record them in this repository. -Updates are checked hourly and automatically pushed via GitHub Actions. +Updates are checked about every 30 minutes and automatically pushed to GitHub Actions. API outputs are stored in the [`output`](/output/) directory. The APIs currently being monitored are as follows: diff --git a/src/cmds/test.ts b/src/cmds/test.ts index 5295727..b735656 100644 --- a/src/cmds/test.ts +++ b/src/cmds/test.ts @@ -7,24 +7,76 @@ import appConfig from '../utils/config.js'; import logger from '../utils/logger.js'; import mathUtils from '../utils/math.js'; +function getObjectDiff(obj1: any, obj2: any) { + const diff: any = {}; + const keys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]); + for (const key of keys) { + const val1 = obj1?.[key]; + const val2 = obj2?.[key]; + if (JSON.stringify(val1) !== JSON.stringify(val2)) { + if (typeof val1 === 'object' && val1 !== null && typeof val2 === 'object' && val2 !== null) { + const nestedDiff = getObjectDiff(val1, val2); + if (Object.keys(nestedDiff).length > 0) { + diff[key] = nestedDiff; + } + } else { + diff[key] = { old: val1, new: val2 }; + } + } + } + return diff; +} + +async function saveResult( + subPaths: string[], + version: string, + data: { req: any; rsp: any }, + saveLatest: boolean = true, +) { + const outputDir = argvUtils.getArgv()['outputDir']; + const filePathBase = path.join(outputDir, ...subPaths); + + const filesToCheck = [path.join(filePathBase, `v${version}.json`)]; + if (saveLatest) { + filesToCheck.push(path.join(filePathBase, 'latest.json')); + } + + const dataStr = JSON.stringify(data, null, 2); + const dataMinified = JSON.stringify(data); + + for (const filePath of filesToCheck) { + const file = Bun.file(filePath); + const exists = await file.exists(); + let currentData: any = null; + if (exists) { + currentData = await file.json(); + } + if (!exists || JSON.stringify(currentData) !== dataMinified) { + if (exists) { + logger.trace(`Diff detected in ${filePath}:`, JSON.stringify(getObjectDiff(currentData, data), null, 2)); + } + await Bun.write(filePath, dataStr); + } + } + + const allFilePath = path.join(filePathBase, 'all.json'); + const allFile = Bun.file(allFilePath); + let allData: any[] = []; + if (await allFile.exists()) { + allData = await allFile.json(); + } + + const exists = allData.some((e: any) => JSON.stringify({ req: e.req, rsp: e.rsp }) === dataMinified); + + if (!exists) { + allData.push({ updatedAt: DateTime.now().toISO(), ...data }); + await Bun.write(allFilePath, JSON.stringify(allData, null, 2)); + } +} + async function mainCmdHandler() { const cfg = appConfig.network.api.akEndfield; - - // await (async () => { - // const channel = appConfig.network.api.akEndfield.channel.osWinRel; - // logger.debug('apiAkEndfield.launcher.web fetching ...'); - // for (const fn of [ - // apiUtils.apiAkEndfield.launcher.web.sidebar, - // apiUtils.apiAkEndfield.launcher.web.singleEnt, - // apiUtils.apiAkEndfield.launcher.web.mainBgImage, - // apiUtils.apiAkEndfield.launcher.web.banner, - // apiUtils.apiAkEndfield.launcher.web.announcement, - // ]) { - // console.dir(await fn(appConfig.network.api.akEndfield.appCode.osWinRel, channel, channel, 'ja-jp'), { - // depth: null, - // }); - // } - // })(); + const channelStr = String(cfg.channel.osWinRel); await (async () => { logger.debug('Fetching latestGame ...'); @@ -59,54 +111,31 @@ async function mainCmdHandler() { }, rsp, }; - const filePathBase = path.join( - argvUtils.getArgv()['outputDir'], - 'akEndfield', - 'launcher', - 'game', - String(cfg.channel.osWinRel), - ); - for (const filePath of [path.join(filePathBase, 'latest.json'), path.join(filePathBase, `v${rsp.version}.json`)]) { - if ( - (await Bun.file(filePath).exists()) === false || - JSON.stringify(await Bun.file(filePath).json()) !== JSON.stringify(prettyRsp) - ) { - await Bun.write(filePath, JSON.stringify(prettyRsp, null, 2)); - } - } - await (async () => { - let needWrite: boolean = true; - const tmp: ({ updatedAt: string } & typeof prettyRsp)[] = await Bun.file( - path.join(filePathBase, 'all.json'), - ).json(); - for (const dataStr of tmp.map((e) => JSON.stringify({ req: e.req, rsp: e.rsp }))) { - if (dataStr === JSON.stringify(prettyRsp)) { - needWrite = false; - } - } - if (needWrite) { - tmp.push({ updatedAt: DateTime.now().toISO(), ...prettyRsp }); - await Bun.write(path.join(filePathBase, 'all.json'), JSON.stringify(tmp, null, 2)); - } - })(); + + await saveResult(['akEndfield', 'launcher', 'game', channelStr], rsp.version, prettyRsp); })(); await (async () => { logger.debug('Fetching latestGameRes ...'); + const gameAllJsonPath = path.join( + argvUtils.getArgv()['outputDir'], + 'akEndfield', + 'launcher', + 'game', + channelStr, + 'all.json', + ); + + if (!(await Bun.file(gameAllJsonPath).exists())) { + logger.warn('Skipping latestGameRes: game/all.json not found'); + return; + } + const versionInfoList = ( - ( - await Bun.file( - path.join( - argvUtils.getArgv()['outputDir'], - 'akEndfield', - 'launcher', - 'game', - String(cfg.channel.osWinRel), - 'all.json', - ), - ).json() - ).map((e: any) => e.rsp) as Awaited>[] + (await Bun.file(gameAllJsonPath).json()).map((e: any) => e.rsp) as Awaited< + ReturnType + >[] ) .map((e) => ({ version: e.version, @@ -114,13 +143,6 @@ async function mainCmdHandler() { randStr: /_([^/]+)\/.+?$/.exec(e.pkg.file_path)![1], })) .sort((a, b) => semver.compare(b.version, a.version)); - const filePathBase = path.join( - argvUtils.getArgv()['outputDir'], - 'akEndfield', - 'launcher', - 'game_resources', - String(cfg.channel.osWinRel), - ); let isLatestWrote: boolean = false; for (const versionInfoEntry of versionInfoList) { if (!versionInfoEntry.randStr) throw new Error('version rand_str not found'); @@ -140,36 +162,14 @@ async function mainCmdHandler() { }, rsp, }; - if (isLatestWrote === false) { - if ( - (await Bun.file(path.join(filePathBase, 'latest.json')).exists()) === false || - JSON.stringify(await Bun.file(path.join(filePathBase, 'latest.json')).json()) !== JSON.stringify(prettyRsp) - ) { - await Bun.write(path.join(filePathBase, 'latest.json'), JSON.stringify(prettyRsp, null, 2)); - } - isLatestWrote = true; - } - for (const filePath of [path.join(filePathBase, `v${versionInfoEntry.version}.json`)]) { - if ( - (await Bun.file(filePath).exists()) === false || - JSON.stringify(await Bun.file(filePath).json()) !== JSON.stringify(prettyRsp) - ) { - await Bun.write(filePath, JSON.stringify(prettyRsp, null, 2)); - } - } - await (async () => { - let needWrite: boolean = true; - const tmp: ({ updatedAt: string } & typeof prettyRsp)[] = await Bun.file( - path.join(filePathBase, 'all.json'), - ).json(); - for (const dataStr of tmp.map((e) => JSON.stringify({ req: e.req, rsp: e.rsp }))) { - if (dataStr === JSON.stringify(prettyRsp)) needWrite = false; - } - if (needWrite) { - tmp.push({ updatedAt: DateTime.now().toISO(), ...prettyRsp }); - await Bun.write(path.join(filePathBase, 'all.json'), JSON.stringify(tmp, null, 2)); - } - })(); + + await saveResult( + ['akEndfield', 'launcher', 'game_resources', channelStr], + versionInfoEntry.version, + prettyRsp, + !isLatestWrote, + ); + isLatestWrote = true; } })(); @@ -194,40 +194,12 @@ async function mainCmdHandler() { }, rsp, }; - const filePathBase = path.join( - argvUtils.getArgv()['outputDir'], - 'akEndfield', - 'launcher', - 'launcher', - launcherTargetAppEntry, - String(cfg.channel.osWinRel), + + await saveResult( + ['akEndfield', 'launcher', 'launcher', launcherTargetAppEntry, channelStr], + rsp.version, + prettyRsp, ); - for (const filePath of [ - path.join(filePathBase, 'latest.json'), - path.join(filePathBase, `v${rsp.version}.json`), - ]) { - if ( - (await Bun.file(filePath).exists()) === false || - JSON.stringify(await Bun.file(filePath).json()) !== JSON.stringify(prettyRsp) - ) { - await Bun.write(filePath, JSON.stringify(prettyRsp, null, 2)); - } - } - await (async () => { - let needWrite: boolean = true; - const tmp: ({ updatedAt: string } & typeof prettyRsp)[] = await Bun.file( - path.join(filePathBase, 'all.json'), - ).json(); - for (const dataStr of tmp.map((e) => JSON.stringify({ req: e.req, rsp: e.rsp }))) { - if (dataStr === JSON.stringify(prettyRsp)) { - needWrite = false; - } - } - if (needWrite) { - tmp.push({ updatedAt: DateTime.now().toISO(), ...prettyRsp }); - await Bun.write(path.join(filePathBase, 'all.json'), JSON.stringify(tmp, null, 2)); - } - })(); } })(); }