Files
harheimertc/server/utils/spielplan-import.js
Torsten Schulz (local) 0849c625cb
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
Add script for importing match schedule and logging
- 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.
2026-05-19 16:23:28 +02:00

203 lines
5.6 KiB
JavaScript

import { promises as fs } from 'fs'
import path from 'path'
import { getServerDataPath } from './paths.js'
const DEFAULT_CONFIG = {
association: 'HeTTV',
clubId: '43030',
clubName: 'Harheimer_TC'
}
const OUTPUT_DIR = getServerDataPath('spielplan-import')
const JSON_FILE = path.join(OUTPUT_DIR, 'harheimer_tc_spielplan.json')
const HTML_FILE = path.join(OUTPUT_DIR, 'harheimer_tc_spielplan.html')
function pad2(value) {
return String(value).padStart(2, '0')
}
export function getSpieljahrForDate(date = new 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`
}
}
export function buildSpielplanUrl(season, config = DEFAULT_CONFIG) {
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 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
}
return Object.entries(value).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' || 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) {
const dateTimestamp = match.date ? Date.parse(match.date) : NaN
return {
day,
date: match.date ?? null,
timestamp: Number.isNaN(dateTimestamp) ? null : Math.floor(dateTimestamp / 1000),
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
}
}
export function parseSpielplanHtml(html, source) {
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,
loaderDataPath: result.path.join('.'),
matchCount: matches.length,
matchesByDay,
matches
}
}
export async function importSpielplan(options = {}) {
const today = options.today ?? new Date()
const season = getSpieljahrForDate(today)
const url = buildSpielplanUrl(season)
const response = await fetch(url, {
headers: {
'accept': 'text/html,application/xhtml+xml',
'accept-language': 'de-DE,de;q=0.9'
}
})
if (!response.ok) {
throw new Error(`Spielplan-Download fehlgeschlagen: HTTP ${response.status}`)
}
const html = await response.text()
const parsed = parseSpielplanHtml(html, {
url,
clubId: DEFAULT_CONFIG.clubId,
clubName: DEFAULT_CONFIG.clubName,
association: DEFAULT_CONFIG.association,
season
})
await fs.mkdir(OUTPUT_DIR, { recursive: true })
await fs.writeFile(HTML_FILE, html, 'utf8')
await fs.writeFile(JSON_FILE, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8')
return {
jsonFile: JSON_FILE,
htmlFile: HTML_FILE,
...parsed
}
}
export async function readImportedSpielplan() {
const content = await fs.readFile(JSON_FILE, 'utf8')
return JSON.parse(content)
}