223 lines
6.1 KiB
JavaScript
223 lines
6.1 KiB
JavaScript
import { promises as fs } from 'fs'
|
|
import path from 'path'
|
|
import { getServerDataPath, getProjectPath } from './paths.js'
|
|
import { getSpieljahrForDate } from './spielplan-import.js'
|
|
import { parseDelimitedLine } from './spielplan-data.js'
|
|
|
|
const OUTPUT_DIR = getServerDataPath('spielplan-import')
|
|
const DEFAULT_ASSOCIATION = 'HeTTV'
|
|
|
|
async function fileExists(filePath) {
|
|
try {
|
|
await fs.access(filePath)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
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 getTableRoutePayload(loaderData) {
|
|
const exactKey = 'routes/click-tt+/$association+/$season+/$type+/$groupname.gruppe.$urlid+/tabelle.$filter'
|
|
if (loaderData?.[exactKey]) {
|
|
return { routeKey: exactKey, payload: loaderData[exactKey] }
|
|
}
|
|
|
|
const fallbackKey = Object.keys(loaderData || {}).find((key) => key.includes('/tabelle.$filter'))
|
|
if (fallbackKey && loaderData[fallbackKey]) {
|
|
return { routeKey: fallbackKey, payload: loaderData[fallbackKey] }
|
|
}
|
|
|
|
throw new Error('Tabellen-Route in loaderData nicht gefunden')
|
|
}
|
|
|
|
function normalizeTableData(url, routeKey, payload) {
|
|
const data = payload?.data || {}
|
|
const leagueTable = Array.isArray(data.league_table) ? data.league_table : []
|
|
|
|
return {
|
|
url,
|
|
routeKey,
|
|
fetchedAt: new Date().toISOString(),
|
|
association: payload?.association || null,
|
|
season: payload?.season || null,
|
|
groupname: payload?.groupname || null,
|
|
urlid: payload?.urlid || null,
|
|
filter: payload?.filter || null,
|
|
headInfos: Array.isArray(data.head_infos) ? data.head_infos : [],
|
|
setCount: data.set_count ?? null,
|
|
gameCount: data.game_count ?? null,
|
|
noMeetings: data.no_meetings ?? null,
|
|
gamesPerSet: data.games_per_set ?? null,
|
|
leagueTtrAvg: data.league_ttr_avg ?? null,
|
|
subTableInfo: data.sub_table_info ?? null,
|
|
meetingsExcerpt: data.meetings_excerpt ?? null,
|
|
rowCount: leagueTable.length,
|
|
leagueTable
|
|
}
|
|
}
|
|
|
|
function extractSeasonSlugFromUrl(url) {
|
|
const match = String(url || '').match(/\/click-tt\/[^/]+\/(\d{2}--\d{2})\//)
|
|
return match?.[1] || null
|
|
}
|
|
|
|
async function resolveMannschaftenCsvPath() {
|
|
const candidates = [
|
|
getServerDataPath('public-data', 'mannschaften.csv'),
|
|
getServerDataPath('mannschaften.csv'),
|
|
getProjectPath('.output', 'public', 'data', 'mannschaften.csv'),
|
|
getProjectPath('public', 'data', 'mannschaften.csv')
|
|
]
|
|
|
|
for (const candidate of candidates) {
|
|
if (await fileExists(candidate)) return candidate
|
|
}
|
|
|
|
throw new Error('Mannschaften-Datei nicht gefunden')
|
|
}
|
|
|
|
function extractRowsWithTableUrl(csvContent) {
|
|
const lines = csvContent.split(/\r?\n/).filter((line) => line.trim() !== '')
|
|
const rows = []
|
|
|
|
for (const line of lines) {
|
|
if (!line.includes('mytischtennis.de')) continue
|
|
|
|
const values = parseDelimitedLine(line, ',')
|
|
const tableUrl = values.find((value) => /^https?:\/\/www\.mytischtennis\.de\//.test(value))
|
|
if (!tableUrl) continue
|
|
|
|
rows.push({
|
|
teamName: values[0] || null,
|
|
leagueName: values[1] || null,
|
|
tableUrl
|
|
})
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
async function fetchTable(url) {
|
|
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(`Tabellen-Download fehlgeschlagen: HTTP ${response.status}`)
|
|
}
|
|
|
|
const html = await response.text()
|
|
const context = extractRemixContext(html)
|
|
const loaderData = context?.state?.loaderData || {}
|
|
const { routeKey, payload } = getTableRoutePayload(loaderData)
|
|
return normalizeTableData(url, routeKey, payload)
|
|
}
|
|
|
|
export async function importLeagueTables(options = {}) {
|
|
const today = options.today ?? new Date()
|
|
const season = getSpieljahrForDate(today)
|
|
const csvPath = await resolveMannschaftenCsvPath()
|
|
const csvContent = await fs.readFile(csvPath, 'utf8')
|
|
const teams = extractRowsWithTableUrl(csvContent)
|
|
|
|
const entries = []
|
|
const errors = []
|
|
|
|
for (const team of teams) {
|
|
try {
|
|
const table = await fetchTable(team.tableUrl)
|
|
entries.push({
|
|
teamName: team.teamName,
|
|
leagueName: team.leagueName,
|
|
table
|
|
})
|
|
} catch (error) {
|
|
errors.push({
|
|
teamName: team.teamName,
|
|
leagueName: team.leagueName,
|
|
tableUrl: team.tableUrl,
|
|
error: error?.message || String(error)
|
|
})
|
|
}
|
|
}
|
|
|
|
const fallbackSeasonSlug = teams.length > 0 ? extractSeasonSlugFromUrl(teams[0].tableUrl) : null
|
|
const seasonSlug = fallbackSeasonSlug || season.seasonSlug
|
|
const outputFile = path.join(OUTPUT_DIR, `tables_${seasonSlug}.json`)
|
|
|
|
const payload = {
|
|
format: 'harheimertc.tables.v1',
|
|
importedAt: new Date().toISOString(),
|
|
source: {
|
|
association: DEFAULT_ASSOCIATION,
|
|
season: {
|
|
seasonSlug,
|
|
dateStart: season.dateStart,
|
|
dateEnd: season.dateEnd
|
|
},
|
|
csvPath
|
|
},
|
|
teamCount: teams.length,
|
|
importedCount: entries.length,
|
|
errorCount: errors.length,
|
|
tables: entries,
|
|
errors
|
|
}
|
|
|
|
await fs.mkdir(OUTPUT_DIR, { recursive: true })
|
|
await fs.writeFile(outputFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
|
|
|
|
return {
|
|
outputFile,
|
|
seasonSlug,
|
|
teamCount: teams.length,
|
|
importedCount: entries.length,
|
|
errorCount: errors.length,
|
|
tables: entries,
|
|
errors
|
|
}
|
|
} |