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) }