Add script for importing match schedule and logging
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
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.
This commit is contained in:
202
server/utils/spielplan-import.js
Normal file
202
server/utils/spielplan-import.js
Normal file
@@ -0,0 +1,202 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user