File size: 4,432 Bytes
3d23b0f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
import { httpClient } from '../../shared/utils/http-client';
import * as cheerio from 'cheerio';
import { UserHistory, ContestStandings, AtCoderUserRating, ContestResult } from './types';
import { ATCODER_BASE_URL, ATCODER_SELECTORS, ATCODER_LABELS } from './constants';
export async function fetchUserRating(username: string): Promise<AtCoderUserRating> {
const url = `${ATCODER_BASE_URL}/users/${username}`;
try {
const { data } = await httpClient.get(url);
const $ = cheerio.load(data);
// Check for 404 or "User not found"
if ($('title').text().includes('404') || $('body').text().includes('User not found')) {
throw new Error(`User '${username}' not found on AtCoder`);
}
const extractText = (label: string) => {
const th = $(`th:contains('${label}')`);
if (th.length === 0) return null;
return th
.next('td')
.text()
.replace(/\s+/g, ' ')
.trim();
};
const rawRating = extractText(ATCODER_LABELS.RATING);
if (rawRating === null) {
throw new Error('AtCoder schema change detected: Rating label not found');
}
const rating = parseInt(rawRating.split(' ')[0]) || 0;
const max_rating = parseInt(extractText(ATCODER_LABELS.MAX_RATING)?.split(' ')[0] || '0') || 0;
const rank = extractText(ATCODER_LABELS.RANK) || 'N/A';
const contests_participated = parseInt(extractText(ATCODER_LABELS.RATED_MATCHES) || '0') || 0;
const last_competed = extractText(ATCODER_LABELS.LAST_COMPETED) || 'N/A';
const country = extractText(ATCODER_LABELS.COUNTRY) || 'N/A';
const birth_year = extractText(ATCODER_LABELS.BIRTH_YEAR) || 'N/A';
const avatarAttr = $(ATCODER_SELECTORS.AVATAR).attr('src');
const avatar = avatarAttr
? avatarAttr.startsWith('//') ? 'https:' + avatarAttr : avatarAttr
: '';
const display_name = $(ATCODER_SELECTORS.USERNAME).first().text().trim();
const kyu = $(ATCODER_SELECTORS.KYU).first().text().trim();
// Fetch rating history from the direct JSON endpoint
let rating_history: UserHistory[] = [];
try {
rating_history = await fetchUserHistory(username);
} catch (e) {
// Fallback to scraping if JSON endpoint fails
$('script').each((i, script) => {
const content = $(script).text();
if (content.includes('var rating_history =')) {
const match = content.match(/var rating_history\s*=\s*(\[.*?\]);/);
if (match) {
try {
rating_history = JSON.parse(match[1]);
} catch (err) { }
}
}
});
}
return {
rating,
max_rating,
rank,
contests_participated,
last_competed,
country,
birth_year,
avatar,
display_name: display_name || username,
kyu,
rating_history,
};
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(`User '${username}' not found on AtCoder`);
}
throw error;
}
}
export async function fetchUserHistory(username: string): Promise<UserHistory[]> {
const url = `https://atcoder.jp/users/${username}/history/json`;
const { data } = await httpClient.get(url);
return data;
}
export async function fetchContestStandings(contestId: string, extended: boolean = false): Promise<ContestStandings> {
const url = `https://atcoder.jp/contests/${contestId}/standings/${extended ? 'extended/' : ''}json`;
const { data } = await httpClient.get(url);
return data;
}
export async function fetchContestResults(contestId: string): Promise<ContestResult[]> {
const url = `https://atcoder.jp/contests/${contestId}/results/json`;
const { data } = await httpClient.get(url);
return data;
}
export async function fetchVirtualStandings(contestId: string, showGhost: boolean = true): Promise<ContestStandings> {
const url = `https://atcoder.jp/contests/${contestId}/standings/virtual/json?showGhost=${showGhost}`;
const { data } = await httpClient.get(url);
return data;
}
|