mirror of
https://github.com/fabrice404/olympics-calendar.git
synced 2026-02-14 20:49:04 +00:00
official website update
This commit is contained in:
@ -5,7 +5,7 @@ export class Cache {
|
||||
private debug = Debug("olympics-calendar:cache");
|
||||
|
||||
private cachePath = (key: string): string => {
|
||||
return `./cache/${key}.cached`;
|
||||
return `./cache/${key}.json`;
|
||||
};
|
||||
|
||||
public get(key: string): string | null {
|
||||
|
||||
@ -13,6 +13,23 @@ export class ICSGenerator {
|
||||
this.calendar = calendar;
|
||||
}
|
||||
|
||||
private cleanLine(line: string): string {
|
||||
if (line.length <= 75) {
|
||||
return line;
|
||||
}
|
||||
const chunks: string[] = [];
|
||||
let index = 0;
|
||||
while (index < line.length) {
|
||||
let chunk = line.slice(index, index + 75);
|
||||
if (index > 0) {
|
||||
chunk = " " + chunk.trim();
|
||||
}
|
||||
chunks.push(chunk);
|
||||
index += 75;
|
||||
}
|
||||
return chunks.join("\n");
|
||||
}
|
||||
|
||||
private generateICSFile(
|
||||
sportKey: string | null,
|
||||
nocKey: string | null,
|
||||
@ -22,27 +39,28 @@ export class ICSGenerator {
|
||||
sportKey || "all-sports",
|
||||
nocKey || "all-nocs",
|
||||
);
|
||||
this.calendar.languages.forEach((lang) => {
|
||||
|
||||
for (const lang of this.calendar.languages) {
|
||||
const pathSportKey = sportKey ? sportKey : "all-sports";
|
||||
const pathNocKey = nocKey ? nocKey : "calendar";
|
||||
|
||||
const filepath = `./output/${lang.code.toLowerCase()}/${pathSportKey.toLowerCase()}/${pathNocKey.toLowerCase()}.ics`;
|
||||
mkdirSync(filepath.split("/").slice(0, -1).join("/"), { recursive: true });
|
||||
|
||||
const titleComponents = [];
|
||||
const titleComponents: string[] = [];
|
||||
if (nocKey) {
|
||||
titleComponents.push(
|
||||
`${this.calendar.nocs.find((n) => n.key === nocKey)!.name[lang.code]}`,
|
||||
);
|
||||
}
|
||||
if (sportKey) {
|
||||
titleComponents.push(this.calendar.sports.find((s) => s.key === sportKey)!.name[lang.code]);
|
||||
titleComponents.push(this.calendar.sports.find((s) => s.key === sportKey)!.name[lang.code] || "");
|
||||
}
|
||||
titleComponents.push("Milano Cortina 2026");
|
||||
|
||||
const title = titleComponents.join(" - ");
|
||||
|
||||
const lines = [];
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("BEGIN:VCALENDAR");
|
||||
lines.push("VERSION:2.0");
|
||||
@ -54,17 +72,11 @@ export class ICSGenerator {
|
||||
|
||||
this.calendar.events
|
||||
.filter((event) => {
|
||||
if (sportKey && event.sport !== sportKey) return false;
|
||||
if (nocKey) {
|
||||
if (event.match) {
|
||||
const team1Key = event.match.team1.key;
|
||||
const team2Key = event.match.team2.key;
|
||||
if (team1Key !== nocKey && team2Key !== nocKey) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if ((sportKey && event.sport !== sportKey) || event.sport === "CER") {
|
||||
return false;
|
||||
}
|
||||
if (nocKey && !event.nocs.includes(nocKey)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
@ -79,27 +91,54 @@ export class ICSGenerator {
|
||||
const sport = this.calendar.sports.find(
|
||||
(s) => s.key === event.sport,
|
||||
)!;
|
||||
lines.push(`DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ""}`);
|
||||
const summary = `SUMMARY:${event.name[lang.code] || ""}`;
|
||||
let description = `DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ""}`;
|
||||
let summary = `SUMMARY:${event.name[lang.code] || ""}`;
|
||||
|
||||
if (event.match) {
|
||||
const team1Name = event.match.team1.name[lang.code] || event.match.team1.key;
|
||||
const team1Flag = getFlag(event.match.team1.key);
|
||||
const team2Name = event.match.team2.name[lang.code] || event.match.team2.key;
|
||||
const team2Flag = getFlag(event.match.team2.key);
|
||||
if (team1Name && team2Name) {
|
||||
lines.push(`SUMMARY:${team1Flag} ${team1Name} - ${team2Name} ${team2Flag}`);
|
||||
if (event.competitors?.length === 2) {
|
||||
const competitor1 = this.getCompetitor(event.competitors[0]!, lang.code);
|
||||
const competitor2 = this.getCompetitor(event.competitors[1]!, lang.code);
|
||||
|
||||
if (competitor1 && competitor2) {
|
||||
summary = `SUMMARY:${competitor1?.flag} ${competitor1.name} - ${competitor2?.name} ${competitor2.flag}`;
|
||||
}
|
||||
} else if (event.competitors?.length > 0) {
|
||||
const competitors = event.competitors
|
||||
.map((competitorId) => this.getCompetitor(competitorId, lang.code))
|
||||
.map((competitor) => `\\n${competitor.flag} ${competitor.name}`).join("");
|
||||
description += `${competitors}`;
|
||||
|
||||
}
|
||||
|
||||
lines.push(summary);
|
||||
lines.push(this.cleanLine(description));
|
||||
lines.push("END:VEVENT");
|
||||
});
|
||||
|
||||
lines.push("END:VCALENDAR");
|
||||
|
||||
writeFileSync(filepath, lines.join("\n"));
|
||||
});
|
||||
if (lines.length <= 10) {
|
||||
this.debug("Skipping empty ICS file:", filepath);
|
||||
} else {
|
||||
writeFileSync(filepath, lines.join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getCompetitor(competitorId: string, lang: string) {
|
||||
if (competitorId.startsWith("team:")) {
|
||||
const team = this.calendar.nocs.find(noc => noc.key === competitorId.replace("team:", ""));
|
||||
return {
|
||||
noc: team?.key,
|
||||
name: team?.name[lang] || team?.key || competitorId,
|
||||
flag: getFlag(team?.key || ""),
|
||||
};
|
||||
}
|
||||
const competitor = this.calendar.competitors.find(comp => comp.code === competitorId)!;
|
||||
return {
|
||||
noc: competitor.noc,
|
||||
name: competitor.name,
|
||||
flag: getFlag(competitor.noc),
|
||||
};
|
||||
}
|
||||
|
||||
public generate(): void {
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { removeSync } from "fs-extra/esm";
|
||||
import nodeCron from "node-cron";
|
||||
|
||||
import { Scraper } from "./scraper";
|
||||
|
||||
const main = async () => {
|
||||
nodeCron.schedule("* * * * *", async () => {
|
||||
const main = () => {
|
||||
nodeCron.schedule("*/1 * * * *", () => {
|
||||
removeSync("./cache/schedules");
|
||||
const scraper = new Scraper();
|
||||
await scraper.scrape();
|
||||
scraper.scrape();
|
||||
});
|
||||
|
||||
nodeCron.schedule("0 0 * * *", () => {
|
||||
removeSync("./cache/disciplinesevents");
|
||||
removeSync("./cache/nocs");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
42
scraper/package-lock.json
generated
42
scraper/package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"debug": "^4.4.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"ts-node": "^10.9.2"
|
||||
@ -1268,6 +1269,20 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
||||
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -1366,6 +1381,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@ -1541,6 +1562,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -2135,6 +2168,15 @@
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"debug": "^4.4.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"ts-node": "^10.9.2"
|
||||
|
||||
@ -1,129 +1,123 @@
|
||||
import { get } from "axios";
|
||||
import Debug from "debug";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
import { Cache } from "./cache";
|
||||
import { ICSGenerator } from "./ics-generator";
|
||||
import { Calendar, Event, Language, PageData, Sport, Team } from "./types";
|
||||
import { Calendar, Event, Language, Sport, NOC, Competitor } from "./types";
|
||||
|
||||
const BASE_URL = "https://www.olympics.com";
|
||||
const BASE_SCHEDULE_PATH = "milano-cortina-2026/schedule/overview";
|
||||
|
||||
export class Scraper {
|
||||
private cache = new Cache();
|
||||
private debug = Debug("olympics-calendar:scraper");
|
||||
private readonly cache = new Cache();
|
||||
|
||||
private events: Event[] = [];
|
||||
private languages: Language[] = [];
|
||||
private nocs: Team[] = [];
|
||||
private sports: Sport[] = [];
|
||||
private readonly competitors: Competitor[] = [];
|
||||
|
||||
private async getPageData(path: string): Promise<PageData> {
|
||||
this.debug(`getPageData: path=${path}`);
|
||||
if (!this.cache.has(path)) {
|
||||
const url = `${BASE_URL}${path}`;
|
||||
this.debug(url);
|
||||
const response = await get(url, {
|
||||
headers: {
|
||||
private readonly debug = Debug("olympics-calendar:scraper");
|
||||
|
||||
private readonly events: Event[] = [];
|
||||
private readonly languages: Language[] = [
|
||||
{ code: "en", name: "English", code3: "ENG" },
|
||||
{ code: "it", name: "Italiano", code3: "ITA" },
|
||||
{ code: "fr", name: "Français", code3: "FRA" },
|
||||
{ code: "de", name: "Deutsch", code3: "DEU" },
|
||||
{ code: "pt", name: "Português", code3: "POR" },
|
||||
{ code: "es", name: "Español", code3: "SPA" },
|
||||
{ code: "ja", name: "日本語", code3: "JPN" },
|
||||
{ code: "zh", name: "中文", code3: "CHI" },
|
||||
{ code: "hi", name: "हिन्दी", code3: "HIN" },
|
||||
{ code: "ko", name: "한국어", code3: "KOR" },
|
||||
{ code: "ru", name: "Русский", code3: "RUS" },
|
||||
];
|
||||
|
||||
private readonly nocs: NOC[] = [];
|
||||
private readonly sports: Sport[] = [];
|
||||
|
||||
private async getJSONData(url: string, cacheKey: string): Promise<any> {
|
||||
this.debug(`getJSONData: url=${url}`);
|
||||
|
||||
if (!this.cache.has(cacheKey)) {
|
||||
const response = await fetch(url, {
|
||||
"headers": {
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
|
||||
"accept-language": "en-US,en;q=0.9,fr-FR;q=0.8,fr;q=0.7",
|
||||
"cache-control": "max-age=0",
|
||||
"priority": "u=0, i",
|
||||
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Google Chrome\";v=\"144\"",
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": "\"macOS\"",
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"sec-fetch-site": "none",
|
||||
"sec-fetch-user": "?1",
|
||||
"upgrade-insecure-requests": "1",
|
||||
},
|
||||
"body": null,
|
||||
"method": "GET"
|
||||
});
|
||||
const page = await response.data;
|
||||
const dataMatch = page.match(
|
||||
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
|
||||
);
|
||||
if (!dataMatch) {
|
||||
throw new Error(
|
||||
`Could not find __NEXT_DATA__ script tag for URL: ${url}`,
|
||||
);
|
||||
}
|
||||
const data = dataMatch[1];
|
||||
if (data) {
|
||||
this.cache.set(path, JSON.stringify(JSON.parse(data), null, 2));
|
||||
}
|
||||
const result = await response.json();
|
||||
this.cache.set(cacheKey, JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
return JSON.parse(this.cache.get(path)!);
|
||||
return JSON.parse(this.cache.get(cacheKey)!);
|
||||
}
|
||||
|
||||
private saveCalendar(): void {
|
||||
this.debug("saveCalendar");
|
||||
const calendar = this.getCalendar();
|
||||
writeFileSync("./output/calendar.json", JSON.stringify(calendar));
|
||||
writeFileSync("./output/calendar.json", JSON.stringify(calendar, null, 2));
|
||||
}
|
||||
|
||||
private async scrapeEvents(): Promise<void> {
|
||||
this.debug("scrapeEvents");
|
||||
for (const sport of this.sports) {
|
||||
for (const lang of this.languages) {
|
||||
const data = await this.getPageData(
|
||||
`/${lang.code}/milano-cortina-2026/schedule/${sport.key}`,
|
||||
);
|
||||
const scheduleList = data.props.pageProps.page.items
|
||||
.find(
|
||||
(item) => item.type === "module" && item.name === "scheduleList",
|
||||
)!
|
||||
.data.schedules.map((schedule) => schedule.units)
|
||||
.flat();
|
||||
|
||||
for (const scheduleElement of scheduleList) {
|
||||
if (
|
||||
this.events.find((e) => e.key === scheduleElement.unitCode) == null
|
||||
) {
|
||||
for (const lang of this.languages) {
|
||||
this.debug(`Scraping events: ${lang.code}`);
|
||||
|
||||
for (let i = 3; i <= 23; i++) {
|
||||
|
||||
const url = `https://www.olympics.com/wmr-owg2026/schedules/api/${lang.code3}/schedule/lite/day/2026-02-${i.toString().padStart(2, "0")}`;
|
||||
const data = await this.getJSONData(url, `schedules/day/2026-02-${i.toString().padStart(2, "0")}/${lang.code3}`);
|
||||
|
||||
|
||||
for (const event of data.units) {
|
||||
const { id: key } = event;
|
||||
if (!this.events.some((e) => e.key === key)) {
|
||||
this.events.push({
|
||||
key: scheduleElement.unitCode,
|
||||
sport: sport.key,
|
||||
start: scheduleElement.startDateTimeUtc,
|
||||
end: scheduleElement.endDateTimeUtc,
|
||||
isTraining: scheduleElement.isTraining,
|
||||
medal: scheduleElement.medal,
|
||||
key,
|
||||
sport: event.disciplineCode,
|
||||
start: event.startDate,
|
||||
end: event.endDate,
|
||||
medal: event.medalFlag.toString(),
|
||||
name: {},
|
||||
location: {},
|
||||
nocs: [],
|
||||
competitors: [],
|
||||
});
|
||||
}
|
||||
const event = this.events.find(
|
||||
(e) => e.key === scheduleElement.unitCode,
|
||||
)!;
|
||||
event.name[lang.code] = scheduleElement.description;
|
||||
event.location[lang.code] = scheduleElement.venue?.description || "";
|
||||
|
||||
if (scheduleElement.match) {
|
||||
if (event.match == null) {
|
||||
event.match = {
|
||||
team1: {
|
||||
key: scheduleElement.match.team1.teamCode.replace(
|
||||
/[^A-Z]/gi,
|
||||
"",
|
||||
),
|
||||
name: {},
|
||||
},
|
||||
team2: {
|
||||
key: scheduleElement.match.team2.teamCode.replace(
|
||||
/[^A-Z]/gi,
|
||||
"",
|
||||
),
|
||||
name: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
event.match.team1.name[lang.code] = (
|
||||
scheduleElement.match.team1.description || ""
|
||||
).replace(/,/gi, "");
|
||||
event.match.team2.name[lang.code] = (
|
||||
scheduleElement.match.team2.description || ""
|
||||
).replace(/,/gi, "");
|
||||
const calendarEvent = this.events.find((e) => e.key === key)!;
|
||||
calendarEvent.name[lang.code] = event.eventUnitName;
|
||||
calendarEvent.location[lang.code] = event.venueDescription;
|
||||
|
||||
for (const team of [
|
||||
scheduleElement.match.team1,
|
||||
scheduleElement.match.team2,
|
||||
]) {
|
||||
const nocKey = team.teamCode.replace(/[^A-Z]/gi, "");
|
||||
if (this.nocs.find((n) => n.key === nocKey) == null) {
|
||||
this.nocs.push({ key: nocKey, name: {} });
|
||||
if (event.competitors) {
|
||||
for (const competitor of event.competitors) {
|
||||
const { code, name, noc, competitorType } = competitor;
|
||||
if (!calendarEvent.nocs.some((n) => n === noc)) {
|
||||
calendarEvent.nocs.push(noc);
|
||||
}
|
||||
|
||||
if (competitorType) {
|
||||
if (!calendarEvent.competitors.some((c) => c === code)) {
|
||||
calendarEvent.competitors.push(code);
|
||||
}
|
||||
this.setCompetitor(code, noc, name);
|
||||
this.setNoc(noc, "", lang.code);
|
||||
} else {
|
||||
const key = `team:${noc}`;
|
||||
if (!calendarEvent.competitors.some((c) => c === key)) {
|
||||
calendarEvent.competitors.push(key);
|
||||
}
|
||||
this.setNoc(noc, name, lang.code);
|
||||
}
|
||||
const noc = this.nocs.find((n) => n.key === nocKey)!;
|
||||
noc.name[lang.code] = (team.description || "").replace(/,/gi, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,47 +125,59 @@ export class Scraper {
|
||||
}
|
||||
}
|
||||
|
||||
private async scrapeLanguages(): Promise<void> {
|
||||
this.debug("scrapeLanguages");
|
||||
const pageData = await this.getPageData(`/en/${BASE_SCHEDULE_PATH}`);
|
||||
const languagesData =
|
||||
pageData.props.pageProps.page.template.properties.header.mainNav
|
||||
.languages;
|
||||
private async scrapeNOCs(): Promise<void> {
|
||||
this.debug("scrapeNOCs");
|
||||
for (const lang of this.languages) {
|
||||
this.debug(`Scraping NOCs: ${lang.code}`);
|
||||
const url = `https://www.olympics.com/wmr-owg2026/info/api/${lang.code3}/nocs`;
|
||||
const data = await this.getJSONData(url, `nocs/${lang.code3}`);
|
||||
|
||||
this.languages = languagesData
|
||||
.filter((lang) =>
|
||||
lang.link.match(/\/milano-cortina-2026\/schedule\/overview$/),
|
||||
)
|
||||
.map((lang) => ({
|
||||
code: lang.lang,
|
||||
name: lang.label,
|
||||
}));
|
||||
for (const noc of this.nocs) {
|
||||
const found = data.nocs.find((n) => n.id === noc.key);
|
||||
this.setNoc(found.id, found.name, lang.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async scrapeSports(): Promise<void> {
|
||||
this.debug("scrapeSports");
|
||||
for (const lang of this.languages) {
|
||||
this.debug(`Scraping language: ${lang.code}`);
|
||||
const pageData = await this.getPageData(
|
||||
`/${lang.code}/${BASE_SCHEDULE_PATH}`,
|
||||
);
|
||||
this.debug(`Scraping sports: ${lang.code}`);
|
||||
|
||||
const disciplines = pageData.props.pageProps.page.items.find(
|
||||
(item) => item.type === "module" && item.name === "scheduleGrid",
|
||||
)!.data.disciplines;
|
||||
|
||||
for (const discipline of disciplines.filter(
|
||||
(d) => d.disciplineCode.toLowerCase() !== "cer",
|
||||
)) {
|
||||
const key = discipline.disciplineCode.toLowerCase();
|
||||
if (this.sports.find((s) => s.key === key) == null) {
|
||||
this.sports.push({ key, name: {}, order: -1 });
|
||||
const url = `https://www.olympics.com/wmr-owg2026/info/api/${lang.code3}/disciplinesevents`;
|
||||
const data = await this.getJSONData(url, `disciplinesevents/${lang.code3}`);
|
||||
for (const discipline of data.disciplines) {
|
||||
const { id, name } = discipline;
|
||||
if (!this.sports.some((s) => s.key === id)) {
|
||||
this.sports.push({ key: id, name: {}, order: 0 });
|
||||
}
|
||||
const sport = this.sports.find((s) => s.key === key)!;
|
||||
sport.name[lang.code] = discipline.description;
|
||||
sport.order = discipline.order;
|
||||
const sport = this.sports.find((s) => s.key === id)!;
|
||||
sport.name[lang.code] = name;
|
||||
}
|
||||
}
|
||||
|
||||
this.sports
|
||||
.toSorted((a, b) => (a.order < b.order ? -1 : 1))
|
||||
.forEach((sport, index) => {
|
||||
sport.order = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
private setCompetitor(code: string, noc: string, name: string): void {
|
||||
if (!this.competitors.some((c) => c.code === code)) {
|
||||
this.competitors.push({ code, noc, name });
|
||||
}
|
||||
}
|
||||
|
||||
private setNoc(key: string, name: string, langCode: string): void {
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
if (!this.nocs.some((n) => n.key === key)) {
|
||||
this.nocs.push({ key, name: {} });
|
||||
}
|
||||
const noc = this.nocs.find((n) => n.key === key)!;
|
||||
noc.name[langCode] = name;
|
||||
}
|
||||
|
||||
public getCalendar(): Calendar {
|
||||
@ -179,15 +185,17 @@ export class Scraper {
|
||||
languages: this.languages,
|
||||
sports: this.sports,
|
||||
nocs: this.nocs,
|
||||
competitors: this.competitors,
|
||||
events: this.events,
|
||||
};
|
||||
}
|
||||
|
||||
public async scrape(): Promise<void> {
|
||||
this.debug("scrape");
|
||||
await this.scrapeLanguages();
|
||||
|
||||
await this.scrapeSports();
|
||||
await this.scrapeEvents();
|
||||
await this.scrapeNOCs();
|
||||
|
||||
this.saveCalendar();
|
||||
new ICSGenerator(this.getCalendar()).generate();
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
"noImplicitAny": false,
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
@ -36,6 +37,6 @@
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
73
scraper/types.d.ts
vendored
73
scraper/types.d.ts
vendored
@ -5,6 +5,7 @@ export interface MultilingualString {
|
||||
export interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
code3: string;
|
||||
}
|
||||
|
||||
export interface Sport {
|
||||
@ -13,14 +14,15 @@ export interface Sport {
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
export interface NOC {
|
||||
key: string;
|
||||
name: MultilingualString;
|
||||
}
|
||||
|
||||
export interface Match {
|
||||
team1: Team;
|
||||
team2: Team;
|
||||
export interface Competitor {
|
||||
noc: string;
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
@ -28,72 +30,17 @@ export interface Event {
|
||||
start: string;
|
||||
end: string;
|
||||
sport: string;
|
||||
isTraining: boolean;
|
||||
medal: "0" | "1" | "3";
|
||||
name: MultilingualString;
|
||||
location: MultilingualString;
|
||||
match?: Match;
|
||||
nocs: string[];
|
||||
competitors: string[];
|
||||
}
|
||||
|
||||
export interface Calendar {
|
||||
languages: Language[];
|
||||
sports: Sport[];
|
||||
nocs: NOC[];
|
||||
competitors: Competitor[];
|
||||
events: Event[];
|
||||
nocs: Team[];
|
||||
}
|
||||
|
||||
export interface PageData {
|
||||
props: {
|
||||
pageProps: {
|
||||
page: {
|
||||
template: {
|
||||
properties: {
|
||||
header: {
|
||||
mainNav: {
|
||||
languages: {
|
||||
link: string;
|
||||
lang: string;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
items: {
|
||||
type: string;
|
||||
name: string;
|
||||
data: {
|
||||
disciplines: {
|
||||
disciplineCode: string;
|
||||
order: number;
|
||||
description: string;
|
||||
}[];
|
||||
schedules: {
|
||||
units: {
|
||||
unitCode: string;
|
||||
startDateTimeUtc: string;
|
||||
endDateTimeUtc: string;
|
||||
isTraining: boolean;
|
||||
medal: "0" | "1" | "3";
|
||||
description: string;
|
||||
venue: {
|
||||
description: string;
|
||||
};
|
||||
match?: {
|
||||
team1: {
|
||||
teamCode: string;
|
||||
description: string;
|
||||
};
|
||||
team2: {
|
||||
teamCode: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user