const cheerio = require("cheerio"); const fs = require("fs"); const autoprefixer = require("autoprefixer"); const postcss = require("postcss"); const tailwindcss = require("tailwindcss"); const { getSportIcon } = require("./sports"); const { isValidNOC, getNOCName, getNOCFlag } = require("./nocs"); const { generateICS } = require("./ics"); const downloadSchedule = async (sportKey) => { console.log(`Checking schedule for ${sportKey}`); const cacheFile = `${__dirname}/../cache/${sportKey}.html`; if (!fs.existsSync(cacheFile)) { console.log(`Downloading schedule for ${sportKey}`); const response = await fetch(`https://olympics.com/en/paris-2024/schedule/${sportKey}`); const content = await response.text(); fs.writeFileSync(cacheFile, content); } const html = fs.readFileSync(cacheFile, "utf-8"); const $ = cheerio.load(html); return JSON.parse($("#__NEXT_DATA__").text()); }; const EVENTS = []; const NOCS = []; const SPORTS = []; const addNOC = (noc) => { if (!NOCS.includes(noc)) { NOCS.push(noc); } }; const addSport = (sportKey, sportName) => { if (!SPORTS.find((sport) => sport.key === sportKey)) { SPORTS.push({ key: sportKey, name: sportName, NOCS: [] }); } }; const addSportNOC = (sportKey, sportName, noc) => { addSport(sportKey, sportName); const sport = SPORTS.find((sport) => sport.key === sportKey); if (!sport.NOCS.includes(noc)) { sport.NOCS.push(noc); } }; const generateCalendars = () => { SPORTS .sort((a, b) => a.name > b.name ? 1 : -1) .forEach((sport) => { let events = EVENTS .filter((event) => event._SPORT === sport.key) .sort((a, b) => a.UID > b.UID ? 1 : -1); let key = `${sport.key}/general`; let title = `${getSportIcon(sport.key)} ${sport.name} | Paris 2024`; generateICS(title, key, events); events = EVENTS .filter((event) => event._SPORT === sport.key && event._MEDAL) .sort((a, b) => a.UID > b.UID ? 1 : -1); key = `${sport.key}/medals`; title = `${getSportIcon(sport.key)} ${sport.name} 🏅 | Paris 2024`; generateICS(title, key, events); sport.NOCS.forEach((noc) => { events = EVENTS .filter((event) => event._SPORT === sport.key && event._NOCS.includes(noc)) .sort((a, b) => a.UID > b.UID ? 1 : -1); key = `${sport.key}/${noc}`; title = `${getNOCFlag(noc)} ${getNOCName(noc)} ${sport.name} | Paris 2024`; generateICS(title, key, events); }); }); NOCS.sort() .forEach((noc) => { let events = EVENTS .filter((event) => event._NOCS.includes(noc)) .sort((a, b) => a.UID > b.UID ? 1 : -1); let key = `general/${noc}`; let title = `${getNOCFlag(noc)} ${getNOCName(noc)} | Paris 2024`; generateICS(title, key, events); events = EVENTS .filter((event) => event._NOCS.includes(noc) && event._MEDAL) .sort((a, b) => a.UID > b.UID ? 1 : -1); if (events.length) { key = `medals/${noc}`; title = `${getNOCFlag(noc)} ${getNOCName(noc)} 🏅 | Paris 2024`; generateICS(title, key, events); } }); const events = EVENTS .sort((a, b) => a.UID > b.UID ? 1 : -1); const key = "general/general"; const title = "Paris 2024"; generateICS(title, key, events); const medalEvents = EVENTS .filter((event) => event._MEDAL) .sort((a, b) => a.UID > b.UID ? 1 : -1); const medalKey = "medals/general"; const medalTitle = "🏅 Paris 2024"; if (medalEvents.length) { generateICS(medalTitle, medalKey, medalEvents); } }; const slugify = (text) => text.toLowerCase().replace(/\s/g, "-") .replace(/[^a-z0-9-]/g, "") .replace(/-+/g, "-"); const extractSportCalendar = async (sportKey) => { const data = await downloadSchedule(sportKey); const sportName = data.query.pDisciplineLabel; const sportIcon = getSportIcon(sportKey); addSport(sportKey, sportName); data.props.pageProps.scheduleDataSource.initialSchedule.units.forEach(unit => { unit.startDateTimeUtc = new Date(unit.startDate).toISOString().replace(".000", ""); unit.endDateTimeUtc = new Date(unit.endDate).toISOString().replace(".000", ""); const event = { UID: `${unit.startDateTimeUtc.replace(/[:-]/g, "")}-${sportKey}-${slugify(unit.eventUnitName).toUpperCase()}`, DTSTAMP: unit.startDateTimeUtc.replace(/[:-]/g, ""), DTSTART: unit.startDateTimeUtc.replace(/[:-]/g, ""), DTEND: unit.endDateTimeUtc.replace(/[:-]/g, ""), DESCRIPTION: `${sportName} - ${unit.eventUnitName}`, SUMMARY: `${sportIcon} ${unit.eventUnitName}`.trim(), LOCATION: unit.venueDescription, _SPORT: sportKey, _NOCS: [], _COMPETITORS: [], _UNITNAME: unit.eventUnitName, _MEDAL: !!unit.medalFlag, _GENDER: unit.genderCode, }; if (unit.competitors) { const competitors = unit.competitors .filter((competitor) => competitor.noc && isValidNOC(competitor.noc)) .sort((a, b) => a.order > b.order ? 1 : -1); event._NOCS = competitors.map((competitor) => { addSportNOC(sportKey, sportName, competitor.noc); addNOC(competitor.noc); return competitor.noc; }); // two competitors, we put them in the summary if (competitors.length === 2) { const competitor1 = competitors.shift(); const competitor2 = competitors.shift(); event.UID += `-${competitor1.noc}-${competitor2.noc}`; if (competitor1.name !== getNOCName(competitor1.noc)) { event.SUMMARY = `${sportIcon} ${competitor1.name} ${getNOCFlag(competitor1.noc)} - ${getNOCFlag(competitor2.noc)} ${competitor2.name}`; } else { event.SUMMARY = `${sportIcon} ${competitor1.noc} ${getNOCFlag(competitor1.noc)} - ${getNOCFlag(competitor2.noc)} ${competitor2.noc}`; } } else if (competitors.length !== 0) { // more than two, we put them in the description competitors .sort((a, b) => a.name > b.name ? 1 : -1) .forEach((competitor) => { if (competitor.name !== getNOCName(competitor.noc)) { event.DESCRIPTION += `\\n${getNOCFlag(competitor.noc)} ${competitor.name}`; event._COMPETITORS.push({ noc: competitor.noc, name: `${getNOCFlag(competitor.noc)} ${competitor.name}` }); } else { event.DESCRIPTION += `\\n${getNOCFlag(competitor.noc)} ${competitor.noc}`; } }); } } EVENTS.push(event); }); }; const generateCeremoniesEvents = () => { let startDateUtc = new Date("2024-07-26T17:30:00Z").toISOString().replace(".000", ""); let endDateUtc = new Date("2024-07-26T21:00:00Z").toISOString().replace(".000", ""); const openingCeremony = { UID: `${startDateUtc.replace(/[:-]/g, "")}-opening-ceremony`, DTSTAMP: startDateUtc.replace(/[:-]/g, ""), DTSTART: startDateUtc.replace(/[:-]/g, ""), DTEND: endDateUtc.replace(/[:-]/g, ""), DESCRIPTION: "Paris 2024 - Opening ceremony", SUMMARY: "Paris 2024 - Opening ceremony", Location: "Paris", _NOCS: NOCS, _MEDAL: false, }; startDateUtc = new Date("2024-08-11T19:00:00Z").toISOString().replace(".000", ""); endDateUtc = new Date("2024-08-11T21:15:00Z").toISOString().replace(".000", ""); const closingCeremony = { UID: `${startDateUtc.replace(/[:-]/g, "")}-closing-ceremony`, DTSTAMP: startDateUtc.replace(/[:-]/g, ""), DTSTART: startDateUtc.replace(/[:-]/g, ""), DTEND: endDateUtc.replace(/[:-]/g, ""), DESCRIPTION: "Paris 2024 - Closing ceremony", SUMMARY: "Paris 2024 - Closing ceremony", Location: "Stade de France, Saint-Denis", _NOCS: NOCS, _MEDAL: false, }; EVENTS.push(openingCeremony); EVENTS.push(closingCeremony); }; const generateOutputPage = () => { const html = []; const linkClass = "inline-block bg-slate-400 hover:bg-blue-400 text-white px-2 py-1 my-px whitespace-nowrap rounded-lg text-base"; html.push("
| All sports"); html.push(" | ");
html.push(`Full schedule`);
if (fs.existsSync(`${__dirname}/../docs/medals/general.ics`)) {
html.push(` 🏅 Medal events`); } html.push(" | ");
html.push(""); NOCS.sort().forEach((noc) => { html.push(`${getNOCFlag(noc)} ${noc}`); }); html.push(" | "); html.push("
|---|---|---|
| 🏅 Medal events"); html.push(" | "); html.push(`Full schedule`); html.push(" | "); html.push(""); fs.readdirSync(`${__dirname}/../docs/medals`) .filter((ics) => ics !== "general.ics") .forEach((ics) => { const noc = ics.replace(".ics", ""); html.push(`${getNOCFlag(noc)} ${noc}`); }); html.push(" | "); html.push("
| ${getSportIcon(sport.key)} ${sport.name}`); html.push(" | ");
html.push(`Full schedule`);
if (fs.existsSync(`${__dirname}/../docs/${sport.key}/medals.ics`)) {
html.push(` 🏅 Medal events`); } html.push(" | ");
html.push(""); sport.NOCS.sort().forEach((noc) => { html.push(`${getNOCFlag(noc)} ${noc}`); }); html.push(" | "); html.push("