@qatadaazzeh/atcoder-api
Version:
Unofficial AtCoder API client for fetching contest and user data
195 lines (189 loc) • 6.99 kB
JavaScript
// src/api/contests.ts
import * as cheerio from "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
import * as cheerio2 from "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)}`);
}
}
export {
fetchContestList,
fetchRecentContests,
fetchUpcomingContests,
fetchUserContestList,
fetchUserInfo
};