@qatadaazzeh/atcoder-api
Version:
Unofficial AtCoder API client for fetching contest and user data
236 lines (228 loc) • 8.84 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
fetchContestList: () => fetchContestList,
fetchRecentContests: () => fetchRecentContests,
fetchUpcomingContests: () => fetchUpcomingContests,
fetchUserContestList: () => fetchUserContestList,
fetchUserInfo: () => fetchUserInfo
});
module.exports = __toCommonJS(index_exports);
// src/api/contests.ts
var cheerio = __toESM(require("cheerio"));
// utils/httpClient.ts
var lastRequestTime = 0;
var MIN_REQUEST_INTERVAL = 1e3;
var cache = /* @__PURE__ */ new Map();
var CACHE_TTL = 5 * 60 * 1e3;
async function rateLimitedFetch(url, options) {
const now = Date.now();
if (now - lastRequestTime < MIN_REQUEST_INTERVAL) {
await new Promise(
(resolve) => setTimeout(resolve, MIN_REQUEST_INTERVAL - (now - lastRequestTime))
);
}
lastRequestTime = Date.now();
return fetch(url, options);
}
async function fetchJson(url, options, useCache = true) {
const cacheKey = `json:${url}`;
const now = Date.now();
const cached = useCache ? cache.get(cacheKey) : void 0;
if (cached && now - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await rateLimitedFetch(url, {
...options,
headers: {
"Accept": "application/json",
...options?.headers
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
if (response.status === 204 || response.status === 205 || response.status === 304) {
return {};
}
const data = await response.json();
if (useCache) {
cache.set(cacheKey, { data, timestamp: now });
}
return data;
}
async function fetchHtml(url, options, useCache = true) {
const cacheKey = `html:${url}`;
const now = Date.now();
const cached = useCache ? cache.get(cacheKey) : void 0;
if (cached && now - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await rateLimitedFetch(url, {
...options,
headers: {
"Accept": "text/html",
...options?.headers
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
const html = await response.text();
if (useCache) {
cache.set(cacheKey, { data: html, timestamp: now });
}
return html;
}
// src/api/contests.ts
async function fetchContestList(type = "upcoming") {
try {
const url = "https://atcoder.jp/contests";
const html = await fetchHtml(url);
const $ = cheerio.load(html);
const contests = [];
const tableSelector = type === "upcoming" ? "#contest-table-upcoming" : "#contest-table-recent";
const contestRows = $(`${tableSelector} tbody tr`);
if (contestRows.length === 0) {
console.warn(`No ${type} contests found. The HTML structure may have changed or there are no contests.`);
}
contestRows.each((_, el) => {
try {
const contestTime = $(el).find("td:nth-child(1)").text().trim();
const contestName = $(el).find("td:nth-child(2) a").text().trim();
const isRated = $(el).find("td:last-child").text().trim() !== "-";
const contestDuration = $(el).find("td:nth-child(3)").text().trim();
const contestType = $(el).find("td:nth-child(2) span").text().trim() === "\u24B6" ? "Algorithm" : "Heuristic";
const contestIdElement = $(el).find("td:nth-child(2) a").attr("href");
if (!contestIdElement) return;
const contestId = contestIdElement.split("/").pop() || "";
const contestUrl = "https://atcoder.jp" + contestIdElement;
contests.push({
contestName,
isRated,
contestTime,
contestDuration,
contestType,
contestUrl,
contestId
});
} catch (rowError) {
console.error("Error parsing contest row:", rowError);
}
});
return contests;
} catch (error) {
console.error("Error fetching contest list:", error);
throw new Error(`Failed to fetch contests: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function fetchUpcomingContests() {
return fetchContestList("upcoming");
}
async function fetchRecentContests() {
return fetchContestList("recent");
}
// src/api/user.ts
var cheerio2 = __toESM(require("cheerio"));
// src/api/userContest.ts
async function fetchUserContestList(userId) {
try {
const url = `https://atcoder.jp/users/${userId}/history/json`;
const response = await fetchJson(url);
const contests = response.map((item) => ({
userRank: item.Place,
userOldRating: item.OldRating,
userNewRating: item.NewRating,
userRatingChange: item.NewRating - item.OldRating,
contestName: item.ContestName,
userPerformance: item.Performance,
contestEndTime: item.EndTime,
isRated: item.IsRated,
contestId: item.ContestScreenName
}));
return contests;
} catch (error) {
console.error("Error fetching user contest list:", error);
throw new Error(`Error fetching user contest list: ${error instanceof Error ? error.message : String(error)}`);
}
}
// src/api/user.ts
async function fetchUserInfo(userId) {
try {
const url = `https://atcoder.jp/users/${userId}`;
const html = await fetchHtml(url);
const $ = cheerio2.load(html);
const container = $("#main-container .row").first();
const userName = container.find(".col-md-3.col-sm-12 h3 .username span").first().text().trim();
if (!userName) {
throw new Error(`User '${userId}' not found or profile page has changed structure`);
}
const currentRank = container.find(".col-md-3.col-sm-12 h3 b").text().trim();
const userAvatar = container.find(".col-md-3.col-sm-12 .avatar").attr("src")?.trim() || "";
const userRankElement = container.find(".col-md-9.col-sm-12 .dl-table tbody tr:nth-child(1) td").text().trim();
const userRank = Number(userRankElement.replace(/[^\d]/g, "")) || 0;
const userRatingElement = container.find(".col-md-9.col-sm-12 .dl-table tbody tr:nth-child(2) td span:nth-child(2)").text().trim();
const userRating = Number(userRatingElement) || 0;
const userMaxRatingElement = container.find(".col-md-9.col-sm-12 .dl-table tbody tr:nth-child(3) td span:nth-child(2)").text().trim();
const userMaxRating = Number(userMaxRatingElement) || 0;
const userLastCompeted = container.find(".col-md-9.col-sm-12 .dl-table tbody tr:nth-child(5) td").text().trim();
let userContestCount = 0;
const userContestCountElement = container.find(".col-md-9.col-sm-12 .dl-table tbody tr:nth-child(4) td").text().trim();
if (userContestCountElement) {
userContestCount = Number(userContestCountElement) || 0;
}
const userContests = await fetchUserContestList(userId);
return {
userName,
currentRank,
userAvatar,
userRank,
userRating,
userMaxRating,
userLastCompeted,
userContestCount,
userContests
};
} catch (error) {
console.error("Error fetching user info:", error);
throw new Error(`Failed to fetch user '${userId}': ${error instanceof Error ? error.message : String(error)}`);
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
fetchContestList,
fetchRecentContests,
fetchUpcomingContests,
fetchUserContestList,
fetchUserInfo
});
;