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.
203 lines
5.6 KiB
JavaScript
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)
|
|
}
|