diff --git a/build.sh b/build.sh index 7b1a709..c138002 100644 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ -node index.js; +ts-node --files --esm index.ts; if [ -d $STATIC_DIR -a $STATIC_DIR ]; then \cp -r $STATIC_DIR/* dist diff --git a/index.js b/index.js deleted file mode 100644 index a6a459a..0000000 --- a/index.js +++ /dev/null @@ -1,117 +0,0 @@ -import fetch from "node-fetch"; -import fs from "fs"; - -function deleteDirectory(path) { - if (fs.existsSync(path)) { - let list = fs.readdirSync(path); - list.forEach(file => { - let filePath = path + '/' + file; - if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath); - else deleteDirectory(filePath); - }); - fs.rmdirSync(path); - } -} - -function makeDist() { - deleteDirectory('dist'); - fs.mkdirSync('dist'); -} - -async function getObject(url) { - const response = await fetch(url); - return await response.json(); -} - -async function resolveContests() { - let contests = {}; - const data = await getObject("https://kenkoooo.com/atcoder/resources/contests.json"); - for (const i in data) { - const contest = data[i]; - contests[contest.id] = contest; - contests[contest.id].problems = {}; - } - return contests; -} - -async function resolveProblems() { - let problems = {}; - const data = await getObject("https://kenkoooo.com/atcoder/resources/problems.json"); - for (const i in data) { - const problem = data[i]; - problems[problem.id] = problem; - problems[problem.id].difficulty = null; - } - return problems; -} - -async function resolveDifficulties(problems) { - let resolved = problems; - const data = await getObject("https://kenkoooo.com/atcoder/resources/problem-models.json"); - for (const i in data) { - const problem = data[i]; - if (resolved[i] && problem.difficulty) resolved[i].difficulty = problem.difficulty; - } - return resolved; -} - -async function mergeData(contests, problems) { - let merged = contests; - const data = await getObject("https://kenkoooo.com/atcoder/resources/contest-problem.json"); - for (const id in data) { - const cur = data[id]; - let prb = problems[cur.problem_id]; - if (prb == undefined) { - console.log("Problem '" + cur.problem_id + "' belongs to contest '" + cur.contest_id + "' not found."); - continue; - } - merged[cur.contest_id].problems[cur.problem_id] = Object.assign({}, prb); - merged[cur.contest_id].problems[cur.problem_id].problem_index = cur.problem_index; - } - return merged; -} - -async function get() { - let contests = await resolveContests(); - let problems = await resolveProblems(); - problems = await resolveDifficulties(problems); - contests = await mergeData(contests, problems); - let data = { - abc: {}, - arc: {}, - agc: {}, - ahc: {}, - abc_like: {}, - arc_like: {}, - agc_like: {}, - others: {} - }; - for (const id in contests) { - const contest = contests[id]; - if (id.slice(0, 3) === "abc") data.abc[id] = contest; - else if (id.slice(0, 3) === "arc") data.arc[id] = contest; - else if (id.slice(0, 3) === "agc") data.agc[id] = contest; - else if (id.slice(0, 3) === "ahc") data.ahc[id] = contest; - else { - try { - let ratedRange = contest.rate_change, rightRange = ratedRange.split("~")[1]; - if (rightRange == " " || ratedRange == "All") rightRange = "9999"; - if (rightRange < 2000) data.abc_like[id] = contest; - else if (rightRange < 2800) data.arc_like[id] = contest; - else if (rightRange != undefined) data.agc_like[id] = contest; - else data.others[id] = contest; - } catch { - console.log("Failed to filter " + id + " failed."); - data.others[id] = contest; - } - } - } - return data; -} - -makeDist(); - -get() - .then(data => fs.promises.writeFile("dist/data.json", JSON.stringify(data))) - .then(() => console.log("done")) - .catch(e => console.log(e)); diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..d700456 --- /dev/null +++ b/index.ts @@ -0,0 +1,42 @@ +import fs from "fs"; +import { modules } from "./src/modules.ts"; + +function deleteDirectory(path: string) { + if (fs.existsSync(path)) { + let list = fs.readdirSync(path); + list.forEach(file => { + let filePath = path + '/' + file; + if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath); + else deleteDirectory(filePath); + }); + fs.rmdirSync(path); + } +} + +function makeDist() { + deleteDirectory('dist'); + fs.mkdirSync('dist'); +} + +async function build() { + let siteList = {} as {[id: string]: Info}; + for (let site in modules) { + console.log('Processing site %s...', site); + try { + let siteModule = modules[site]; + let data = await siteModule.getData(); + fs.mkdirSync('dist/' + site); + fs.writeFileSync('dist/' + site + '/data.json', JSON.stringify(data)); + siteList[site] = await siteModule.getInfo(); + } catch (e) { + console.log(e); + } finally { + console.log('Done.'); + } + } + fs.writeFileSync('dist/list.json', JSON.stringify(siteList)); +} + +makeDist(); +build(); + diff --git a/package-lock.json b/package-lock.json index 19e10bb..fe594bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,95 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@types/node": "^20.3.3", "env-cmd": "^10.1.0", - "node-fetch": "^3.2.10" + "node-fetch": "^3.2.10", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/node": { + "version": "20.3.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==" + }, + "node_modules/acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", @@ -21,6 +106,11 @@ "node": ">= 6" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -42,6 +132,14 @@ "node": ">= 12" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/env-cmd": { "version": "10.1.0", "resolved": "https://registry.npmmirror.com/env-cmd/-/env-cmd-10.1.0.tgz", @@ -85,6 +183,11 @@ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", @@ -133,6 +236,65 @@ "node": ">=8" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", @@ -154,14 +316,94 @@ "engines": { "node": ">= 8" } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } } }, "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "@types/node": { + "version": "20.3.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==" + }, + "acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -177,6 +419,11 @@ "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, "env-cmd": { "version": "10.1.0", "resolved": "https://registry.npmmirror.com/env-cmd/-/env-cmd-10.1.0.tgz", @@ -208,6 +455,11 @@ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", @@ -241,6 +493,36 @@ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", @@ -253,6 +535,11 @@ "requires": { "isexe": "^2.0.0" } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/package.json b/package.json index fb5fb65..43202e0 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,16 @@ "build": "bash build.sh", "build:cloudflare-pages": "env-cmd -f .env.cloudflare-pages bash build.sh" }, + "main": "index.ts", "author": "", "license": "MIT", + "experimental-specifier-resolution": "node", "dependencies": { + "@types/node": "^20.3.3", "env-cmd": "^10.1.0", - "node-fetch": "^3.2.10" + "node-fetch": "^3.2.10", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" }, "type": "module" } diff --git a/src/atcoder/index.ts b/src/atcoder/index.ts new file mode 100644 index 0000000..0325f9c --- /dev/null +++ b/src/atcoder/index.ts @@ -0,0 +1,156 @@ +import fetch from "node-fetch"; +import { NativeContest, NativeContestProblem, NativeDifficultySet, NativeProblem } from "./types.ts"; + +async function getObject(url: string) { + const response = await fetch(url); + return await response.json(); +} + +async function resolveContests(): Promise { + function getIndexes(indexes: string[]) { + let obj = {} as {[name: string]: string}; + for (const name of indexes) { + const regex = name; + regex.replace(/\//g, '|'); + obj[name] = `(${regex})\\d\*`; + } + return obj; + } + let categories = { + abc: { id: "abc", title: "ABC", color: "#00f", contests: [], indexes: getIndexes(["A", "B", "C", "D", "E", "F", "G", "H/Ex"]) }, + arc: { id: "arc", title: "ARC", color: "#ff8000", contests: [], indexes: getIndexes(["A", "B", "C", "D", "E", "F"]) }, + agc: { id: "agc", title: "AGC", color: "#ff1818", contests: [], indexes: getIndexes(["A", "B", "C", "D", "E", "F"]) }, + abc_like: { id: "abc_like", title: "ABC Like", color: "#00f", contests: [], indexes: getIndexes(["A", "B", "C", "D", "E", "F", "G", "H/Ex"]) }, + arc_like: { id: "arc_like", title: "ARC Like", color: "#ff8000", contests: [], indexes: getIndexes(["A", "B", "C", "D", "E", "F"]) }, + agc_like: { id: "agc_like", title: "AGC Like", color: "#ff1818", contests: [], indexes: getIndexes(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) }, + ahc: { id: "ahc", title: "AHC", color: "#181818", contests: [] }, + others: { id: "others", title: "其他", color: "#181818", contests: [] } + } as {[id: string]: Category}; + let data = { + categories: [], + contests: [], + problems: [] + } as Data; + const contests = await getObject("https://kenkoooo.com/atcoder/resources/contests.json") as NativeContest[]; + for (const i in contests) { + const contest = contests[i], id = contest.id; + data.contests.push({ id: id, title: contest.title, problems: [], link: "https://atcoder.jp/contests/" + contest.id }); + let prefix = id.slice(0, 3); + if (prefix === "abc" || prefix === "arc" || prefix === "agc" || prefix === "ahc") categories[prefix].contests.push(id); + else { + try { + let ratedRange = contest.rate_change, rightRangeStr = ratedRange.split("~")[1]; + if (rightRangeStr == " " || ratedRange == "All") rightRangeStr = "9999"; + if (!rightRangeStr) categories.others.contests.push(id); + let rightRange = parseInt(rightRangeStr); + if (rightRange < 2000) categories.abc_like.contests.push(id); + else if (rightRange < 2800) categories.arc_like.contests.push(id); + else categories.agc_like.contests.push(id); + } catch { + console.log("Failed to filter " + id + " failed."); + categories.others.contests.push(id); + } + } + } + for (const id in categories) data.categories.push(categories[id]); + return data; +} + +async function resolveProblems(): Promise { + let problems = [] as Problem[]; + const data = await getObject("https://kenkoooo.com/atcoder/resources/problems.json") as NativeProblem[]; + for (const i in data) { + const problem = data[i]; + problems.push({ id: problem.id, title: problem.name, link: null, difficulty: null }); + } + return problems.reverse(); +} + +function resolveDifficulty(difficulty: number): Difficulty { + function getTextColor(difficulty: number): string { + if (difficulty < 400) return 'rgb(128, 128, 128)'; + if (difficulty < 800) return 'rgb(128, 64, 0)'; + if (difficulty < 1200) return 'rgb(0, 128, 0)'; + if (difficulty < 1600) return 'rgb(0, 192, 192)'; + if (difficulty < 2000) return 'rgb(0, 0, 255)'; + if (difficulty < 2400) return 'rgb(192, 192, 0)'; + if (difficulty < 2800) return 'rgb(255, 128, 0)'; + return 'rgb(255, 0, 0)'; + } + function getDifficultyColor(difficulty: number): string { + if (difficulty < 400) return 'rgb(128, 128, 128)'; + if (difficulty < 800) return 'rgb(128, 64, 0)'; + if (difficulty < 1200) return 'rgb(0, 128, 0)'; + if (difficulty < 1600) return 'rgb(0, 192, 192)'; + if (difficulty < 2000) return 'rgb(0, 0, 255)'; + if (difficulty < 2400) return 'rgb(192, 192, 0)'; + if (difficulty < 2800) return 'rgb(255, 128, 0)'; + if (difficulty < 3200) return 'rgb(255, 0, 0)'; + if (difficulty < 3600) return 'rgb(150, 92, 44)'; + if (difficulty < 4000) return 'rgb(128, 128, 128)'; + return 'rgb(255, 215, 0)'; + } + function getDifficultyRate(difficulty: number): number { + let displayDifficulty = difficulty; + if (displayDifficulty < 400) displayDifficulty = Math.round(400 / Math.exp(1 - displayDifficulty / 400)); + if (displayDifficulty >= 3200) return 1; + return (displayDifficulty % 400) / 400; + } + return { + type: difficulty >= 3200 ? 'medal' : 'normal', + color: getDifficultyColor(difficulty), + textColor: getTextColor(difficulty), + rate: getDifficultyRate(difficulty), + value: difficulty + }; +} +async function resolveDifficulties(problems: Problem[]): Promise { + const data = await getObject("https://kenkoooo.com/atcoder/resources/problem-models.json") as NativeDifficultySet; + for (const i in problems) { + const problem = data[problems[i].id]; + if (problem && problem.difficulty) problems[i].difficulty = resolveDifficulty(problem.difficulty); + } + return problems; +} + +async function mergeData(data: Data, problems: Problem[]): Promise { + const contestProblem = await getObject("https://kenkoooo.com/atcoder/resources/contest-problem.json") as NativeContestProblem[]; + const contestProblemSet = {} as {[id: string]: ContestProblem[]}; + const problemSet = new Set(); + const linkSet = {} as {[id: string]: string}; + for (const problem of problems) problemSet.add(problem.id); + for (const i in contestProblem) { + const cur = contestProblem[i]; + if (!problemSet.has(cur.problem_id)) { + console.log("Problem '" + cur.problem_id + "' belongs to contest '" + cur.contest_id + "' not found."); + continue; + } + if (!contestProblemSet[cur.contest_id]) contestProblemSet[cur.contest_id] = []; + contestProblemSet[cur.contest_id].push({id: cur.problem_id, index: cur.problem_index}); + linkSet[cur.problem_id] = `https://atcoder.jp/contests/${cur.contest_id}/tasks/${cur.problem_id}`; + } + for (const i in data.contests) { + const contest = data.contests[i]; + if (contestProblemSet[contest.id]) data.contests[i].problems = contestProblemSet[contest.id]; + } + data.problems = problems; + for (let i = 0; i < data.problems.length; i++) { + data.problems[i].link = linkSet[data.problems[i].id] || null; + } + return data; +} + +export async function getData(): Promise { + let data = await resolveContests(); + let problems = await resolveProblems(); + problems = await resolveDifficulties(problems); + return await mergeData(data, problems); +} + +export async function getInfo(): Promise { + return { + title: 'Atcoder', + icon: 'https://img.atcoder.jp/assets/logo.png', + link: 'https://atcoder.jp/' + }; +} diff --git a/src/atcoder/types.ts b/src/atcoder/types.ts new file mode 100644 index 0000000..27a153b --- /dev/null +++ b/src/atcoder/types.ts @@ -0,0 +1,35 @@ +export type NativeContest = { + id: string, + start_epoch_second: number, + duration_second: number, + title: string, + rate_change: string +}; + +export type NativeProblem = { + id: string, + contest_id: string, + problem_index: string, + name: string, + title: string +}; + +export type NativeDifficulty = { + slope: number, + intercept: number, + variance: number, + difficulty: number, + discrimination: number, + irt_loglikelihood: number, + irt_users: number, + is_experimental: boolean +}; +export type NativeDifficultySet = { + [id: string]: NativeDifficulty +}; + +export type NativeContestProblem = { + contest_id: string, + problem_id: string, + problem_index: string +}; diff --git a/src/codeforces/index.ts b/src/codeforces/index.ts new file mode 100644 index 0000000..ceef3c8 --- /dev/null +++ b/src/codeforces/index.ts @@ -0,0 +1,129 @@ +import fetch from "node-fetch"; +import { NativeContestsData, NativeProblemsData } from "./types.ts"; + +function resolveDifficulty(rating: number | undefined): Difficulty | null { + if (!rating) return null; + if (rating < 1200) return { + type: 'normal', + color: '#ccc', + rate: (rating - 800) / (1200 - 800), + value: rating, + textColor: '#ccc' + }; + if (rating < 1400) return { + type: 'normal', + color: '#7f7', + rate: (rating - 1200) / (1400 - 1200), + value: rating, + textColor: '#7f7' + }; + if (rating < 1600) return { + type: 'normal', + color: '#7db', + rate: (rating - 1400) / (1600 - 1400), + value: rating, + textColor: '#7db' + }; + if (rating < 1900) return { + type: 'normal', + color: '#aaf', + rate: (rating - 1600) / (1900 - 1600), + value: rating, + textColor: '#aaf' + }; + if (rating < 2100) return { + type: 'normal', + color: '#f8f', + rate: (rating - 1900) / (2100 - 1900), + value: rating, + textColor: '#f8f' + }; + if (rating < 2300) return { + type: 'normal', + color: '#fc8', + rate: (rating - 2100) / (2300 - 2100), + value: rating, + textColor: '#fc8' + }; + if (rating < 2400) return { + type: 'normal', + color: '#fb5', + rate: (rating - 2300) / (2400 - 2300), + value: rating, + textColor: '#fb5' + }; + if (rating < 2600) return { + type: 'normal', + color: '#f77', + rate: (rating - 2400) / (2600 - 2400), + value: rating, + textColor: '#f77' + }; + if (rating < 3000) return { + type: 'normal', + color: '#f33', + rate: (rating - 2600) / (3000 - 2600), + value: rating, + textColor: '#f33' + }; + if (rating < 3200) return { + type: 'medal', + color: 'rgb(150, 92, 44)', + rate: 1, + value: rating, + textColor: 'a00' + }; + if (rating < 3400) return { + type: 'medal', + color: 'rgb(128, 128, 128)', + rate: 1, + value: rating, + textColor: 'a00' + }; + return { + type: 'medal', + color: 'rgb(255, 215, 0)', + rate: 1, + value: rating, + textColor: '#a00' + }; +} + +export async function getData(): Promise { + let nativeProblems = await (await fetch('https://codeforces.com/api/problemset.problems')).json() as NativeProblemsData; + let problems = [] as Problem[]; + let contestSet = {} as {[id: string]: Contest}; + if (nativeProblems.status !== 'OK') throw nativeProblems; + for (const problem of nativeProblems.result.problems) { + problems.push({ + id: `${problem.contestId}${problem.index}`, + title: problem.name, + link: `https://codeforces.com/problemset/problem/${problem.contestId}/${problem.index}`, + difficulty: resolveDifficulty(problem.rating) + }); + if (!contestSet[`${problem.contestId}`]) { + contestSet[`${problem.contestId}`] = { + id: `${problem.contestId}`, + title: '', + link: `https://codeforces.com/contest/${problem.contestId}`, + problems: [] + }; + } + contestSet[`${problem.contestId}`].problems.push({id: `${problem.contestId}${problem.index}`, index: `${problem.index}`}); + } + let nativeContests = await (await fetch('https://codeforces.com/api/contest.list')).json() as NativeContestsData; + for (const contest of nativeContests.result) { + if (contestSet[`${contest.id}`]) contestSet[`${contest.id}`].title = contest.name; + } + let contests = [] as Contest[]; + for (const id in contestSet) contestSet[id].problems = contestSet[id].problems.reverse(), contests.push(contestSet[id]); + let categories = [] as Category[]; + return {categories: categories, contests: contests, problems: problems}; +} +export async function getInfo(): Promise { + return { + title: 'Codeforces', + icon: 'https://codeforces.com/favicon.png', + link: 'https://codeforces.com/' + } +} diff --git a/src/codeforces/types.ts b/src/codeforces/types.ts new file mode 100644 index 0000000..89ad3bd --- /dev/null +++ b/src/codeforces/types.ts @@ -0,0 +1,23 @@ +export type NativeProblem = { + contestId: number, + index: string, + name: string, + type: string, + rating?: number +}; +export type NativeContest = { + id: string, + name: string, + type: string +}; + +export type NativeProblemsData = { + status: string, + result: { + problems: NativeProblem[] + } +}; +export type NativeContestsData = { + status: string, + result: NativeContest[] +}; \ No newline at end of file diff --git a/src/modules.ts b/src/modules.ts new file mode 100644 index 0000000..cef3677 --- /dev/null +++ b/src/modules.ts @@ -0,0 +1,7 @@ +import * as atcoder from './atcoder/index.ts'; +import * as codeforces from './codeforces/index.ts'; + +export const modules = { + atcoder: atcoder, + codeforces: codeforces +} as Modules; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2b022e5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "ES2016" + ], + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "module": "ESNext", + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": [ + "." + ] +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..4dabaf6 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,49 @@ +type Difficulty = { + type: 'normal' | 'medal', + color: string, + textColor: string, + rate: number, + value: number +}; + +type ContestProblem = { + id: string, + index: string +}; +type Problem = { + id: string, + title: string, + difficulty: Difficulty | null, + link: string | null +} + +type Contest = { + id: string, + title: string, + link: string | null, + problems: ContestProblem[]; +}; + +type Category = { + id: string, + title: string, + color: string, + contests: string[], + indexes?: {[name: string]: string} +}; + +type Data = { + categories: Category[], + contests: Contest[], + problems: Problem[] +}; + +type Modules = { + [id: string]: any +}; + +type Info = { + title: string, + icon: string, + link: string +}