// Usage: Replace the 4 constants below, then run `await generateMarkdownTable()` to create markdown // Requires the event to be an `event series`; `39c3ht` is not so this will fail: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series const token = ""; // generate using https://docs.pretix.eu/dev/api/tokenauth.html#obtaining-an-api-token const baseUrl = 'https://tickets.hamburg.ccc.de/api/v2'; const organizer = "hackertours"; // example extracted from https://tickets.hamburg.ccc.de/hackertours/39c3ht const eventName = "39c3ht"; // example extracted from https://tickets.hamburg.ccc.de/hackertours/39c3ht async function generateMarkdownTable() { async function fetchAllSubevents() { const results = []; let url = `${baseUrl}/organizers/${organizer}/events/${eventName}/subevents/`; while (url) { const resp = await fetch(url, { headers: { 'Authorization': `Token ${token}`, 'Accept': 'application/json' } }); if (!resp.ok) { throw new Error(`Failed to fetch subevents: ${resp.status} ${await resp.text()}`); } const json = await resp.json(); results.push(...json.results); url = json.next; } return results; } function slugify(name) { return name .toLowerCase() .replace(/[“”"']/g, '') .replace(/\(.*?\)/g, '') .replace(/&/g, 'and') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); } function langTags(title) { const tags = []; const hasGerman = /\(german\)|\bgerman\b|[äöüß]|stadtrundgang|rote flora|gängeviertel/.test(title); const hasEnglish = /\(english\)|\benglish\b/.test(title); const bilingual = /\b(bilingual)\b/.test(title); if (bilingual) { tags.push('{{< lang de >}}', '{{< lang en >}}'); } else { if (hasGerman) tags.push('{{< lang de >}}'); if (hasEnglish) tags.push('{{< lang en >}}'); } return tags.join(' '); } function formatDay(date) { return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } const subevents = await fetchAllSubevents(); if (!subevents.length) return '| Zeit | | |\n|------|---|---|'; const dayMap = new Map(); for (const s of subevents) { if (!s.date_from) continue; const d = new Date(s.date_from); const dayKey = d.toISOString().slice(0, 10); if (!dayMap.has(dayKey)) { dayMap.set(dayKey, { dateObj: d, events: [] }); } const hour = d.getUTCHours(); const minute = d.getUTCMinutes(); let title = ((s.name && (s.name.en || Object.values(s.name)[0])) || 'Untitled').replace(/^.*?-\s*/, ''); const slug = slugify(title); const tags = langTags(title); title = title.replace("(German)", "").replace("(English)", "").replace("(Bilingual)", ""); const md = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} [${title}](./${slug}/)${tags ? ' ' + tags : ''}`; dayMap.get(dayKey).events.push({ hour, minute, md }); } const days = Array.from(dayMap.entries()).sort((a, b) => a[1].dateObj - b[1].dateObj); const hourSet = new Set(); for (const [, { events }] of days) { for (const e of events) hourSet.add(e.hour); } const hoursSorted = Array.from(hourSet).sort((a, b) => a - b); const baseDate = new Date('2025-12-26T00:00:00Z'); const headerCells = days.map(([key, val]) => { const dayNumber = Math.floor((val.dateObj - baseDate) / (1000 * 60 * 60 * 24)); return `Tag ${dayNumber} (${formatDay(val.dateObj)})`; }); let md = '| Zeit | ' + headerCells.join(' | ') + ' |\n'; md += '|------|' + headerCells.map(() => '---').join('|') + '|\n'; for (const hour of hoursSorted) { const hourLabel = `${hour.toString().padStart(2, '0')}:00`; const perDay = days.map(([, { events }]) => events .filter(e => e.hour === hour) .sort((a, b) => a.minute - b.minute) ); const maxRows = Math.max(0, ...perDay.map(list => list.length)); if (maxRows === 0) continue; for (let i = 0; i < maxRows; i++) { const rowCells = perDay.map(list => list[i] ? list[i].md : '-'); const zeit = i === 0 ? hourLabel : ''; md += `| ${zeit} | ${rowCells.join(' | ')} |\n`; } } return md; }