UNPKG

@qatadaazzeh/atcoder-api

Version:

Unofficial AtCoder API client for fetching contest and user data

195 lines (189 loc) 6.99 kB
// 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 };