Some checks failed
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m2s
Code Analysis and Production Deploy / analyze (pull_request) Failing after 33s
Code Analysis and Production Deploy / deploy-production (pull_request) Has been skipped
Code Analysis and Production Deploy / deploy-test (pull_request) Has been skipped
Require Package Version Change / check (pull_request) Failing after 10s
- Created `import-spielplan.js` to fetch and parse the match schedule from the specified URL, saving the output as JSON. - Added `run-spielplan-import.sh` to automate the execution of the import script and log output. - Introduced `spielplan.html` file to store the downloaded HTML content for further processing.
237 lines
6.2 KiB
JavaScript
Executable File
237 lines
6.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const CONFIG = {
|
|
association: "HeTTV",
|
|
clubId: "43030",
|
|
clubName: "Harheimer_TC",
|
|
outputDir: path.join(__dirname, "data"),
|
|
outputFile: "harheimer_tc_spielplan.json",
|
|
htmlFile: "harheimer_tc_spielplan.html",
|
|
};
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let i = 2; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (arg.startsWith("--")) {
|
|
const key = arg.slice(2);
|
|
const next = argv[i + 1];
|
|
if (!next || next.startsWith("--")) {
|
|
args[key] = true;
|
|
} else {
|
|
args[key] = next;
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function pad2(value) {
|
|
return String(value).padStart(2, "0");
|
|
}
|
|
|
|
function seasonForDate(date) {
|
|
const year = date.getFullYear();
|
|
const startYear = date.getMonth() >= 6 ? year : year - 1;
|
|
const endYear = startYear + 1;
|
|
|
|
return {
|
|
startYear,
|
|
endYear,
|
|
seasonSlug: `${String(startYear).slice(-2)}--${String(endYear).slice(-2)}`,
|
|
dateStart: `${startYear}-07-01`,
|
|
dateEnd: `${endYear}-06-30`,
|
|
};
|
|
}
|
|
|
|
function buildUrl(season) {
|
|
const base = `https://www.mytischtennis.de/click-tt/${CONFIG.association}/${season.seasonSlug}/verein/${CONFIG.clubId}/${CONFIG.clubName}/spielplan`;
|
|
return `${base}?date_start=${season.dateStart}&date_end=${season.dateEnd}`;
|
|
}
|
|
|
|
function readHtml(args, url) {
|
|
if (args.input) {
|
|
return fs.readFileSync(path.resolve(args.input), "utf8");
|
|
}
|
|
|
|
const html = execFileSync("curl", ["-fsSL", "--compressed", url], {
|
|
encoding: "utf8",
|
|
maxBuffer: 20 * 1024 * 1024,
|
|
});
|
|
|
|
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
fs.writeFileSync(path.join(CONFIG.outputDir, CONFIG.htmlFile), html);
|
|
return html;
|
|
}
|
|
|
|
function extractRemixContext(html) {
|
|
const marker = "window.__remixContext = ";
|
|
const start = html.indexOf(marker);
|
|
if (start === -1) {
|
|
throw new Error("window.__remixContext nicht gefunden");
|
|
}
|
|
|
|
const jsonStart = start + marker.length;
|
|
let depth = 0;
|
|
let inString = false;
|
|
let escaped = false;
|
|
|
|
for (let i = jsonStart; i < html.length; i += 1) {
|
|
const char = html[i];
|
|
|
|
if (inString) {
|
|
if (escaped) {
|
|
escaped = false;
|
|
} else if (char === "\\") {
|
|
escaped = true;
|
|
} else if (char === "\"") {
|
|
inString = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (char === "\"") {
|
|
inString = true;
|
|
} else if (char === "{") {
|
|
depth += 1;
|
|
} else if (char === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
return JSON.parse(html.slice(jsonStart, i + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error("Ende von window.__remixContext nicht gefunden");
|
|
}
|
|
|
|
function looksLikeScheduleByDate(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return false;
|
|
}
|
|
|
|
const entries = Object.entries(value);
|
|
if (entries.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return entries.some(([key, list]) => (
|
|
/^\d{4}-\d{2}-\d{2}$/.test(key)
|
|
&& Array.isArray(list)
|
|
&& list.some((item) => item && item.team_home && item.team_away && item.meeting_id)
|
|
));
|
|
}
|
|
|
|
function findSchedule(value, trail = []) {
|
|
if (looksLikeScheduleByDate(value)) {
|
|
return { schedule: value, path: trail };
|
|
}
|
|
|
|
if (!value || typeof value !== "object") {
|
|
return null;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
|
|
for (const [key, child] of Object.entries(value)) {
|
|
const result = findSchedule(child, trail.concat(key));
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function normalizeMatch(day, match) {
|
|
return {
|
|
day,
|
|
date: match.date ?? null,
|
|
formattedDay: match.formattedDay ?? null,
|
|
formattedTime: match.formattedTime ?? null,
|
|
state: match.state ?? null,
|
|
meetingId: match.meeting_id ?? null,
|
|
meetingNumber: match.meeting_number ?? null,
|
|
leagueId: match.league_id ?? null,
|
|
leagueName: match.league_name ?? null,
|
|
leagueShortName: match.league_short_name ?? null,
|
|
leagueOrgShortName: match.league_org_short_name ?? null,
|
|
roundName: match.round_name ?? null,
|
|
teamHome: match.team_home ?? null,
|
|
teamHomeId: match.team_home_id ?? null,
|
|
teamHomeClubId: match.team_home_club_id ?? null,
|
|
teamAway: match.team_away ?? null,
|
|
teamAwayId: match.team_away_id ?? null,
|
|
teamAwayClubId: match.team_away_club_id ?? null,
|
|
result: match.matches_won != null && match.matches_lost != null
|
|
? `${match.matches_won}:${match.matches_lost}`
|
|
: null,
|
|
isConfirmed: match.is_confirmed ?? null,
|
|
isComplete: match.is_meeting_complete ?? null,
|
|
originalDate: match.original_date ?? null,
|
|
location: match.location ?? null,
|
|
pdfUrl: match.pdf_url ?? null,
|
|
};
|
|
}
|
|
|
|
function parseSchedule(html, meta) {
|
|
const context = extractRemixContext(html);
|
|
const result = findSchedule(context.state?.loaderData);
|
|
if (!result) {
|
|
throw new Error("Keinen Spielplan im Remix loaderData gefunden");
|
|
}
|
|
|
|
const matchesByDay = result.schedule;
|
|
const matches = Object.keys(matchesByDay)
|
|
.sort()
|
|
.flatMap((day) => matchesByDay[day].map((match) => normalizeMatch(day, match)));
|
|
|
|
return {
|
|
importedAt: new Date().toISOString(),
|
|
source: meta,
|
|
loaderDataPath: result.path.join("."),
|
|
matchCount: matches.length,
|
|
matchesByDay,
|
|
matches,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv);
|
|
const today = args.today ? new Date(`${args.today}T12:00:00`) : new Date();
|
|
if (Number.isNaN(today.getTime())) {
|
|
throw new Error(`Ungueltiges Datum fuer --today: ${args.today}`);
|
|
}
|
|
|
|
const season = seasonForDate(today);
|
|
const url = buildUrl(season);
|
|
const html = readHtml(args, url);
|
|
const parsed = parseSchedule(html, {
|
|
url,
|
|
clubId: CONFIG.clubId,
|
|
clubName: CONFIG.clubName,
|
|
association: CONFIG.association,
|
|
season,
|
|
});
|
|
|
|
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
const outputPath = path.join(CONFIG.outputDir, CONFIG.outputFile);
|
|
fs.writeFileSync(outputPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
|
|
console.log(`Spielplan gespeichert: ${outputPath}`);
|
|
console.log(`Spiele: ${parsed.matchCount}`);
|
|
console.log(`Zeitraum: ${season.dateStart} bis ${season.dateEnd}`);
|
|
}
|
|
|
|
main();
|