Initial commit: bot.js v24/05/2026

- Telegram bot met beschikbaarheid, scores, matching
- Peakz scraper voor Atoomweg/Euroborg/Suikerterrein
- SQLite backend via API
This commit is contained in:
Nova Coder
2026-05-24 20:39:43 +00:00
commit 75912faae1
2 changed files with 2047 additions and 0 deletions
+1950
View File
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
const { chromium } = require('playwright');
const LOCATIONS = [
{ name: 'Atoomweg', city: 'Groningen', indoor: true, courtTypeIds: '13' },
{ name: 'Euroborg', city: 'Groningen', indoor: true, courtTypeIds: '5' },
{ name: 'Suikerterrein', city: 'Groningen', indoor: false, courtTypeIds: '' },
];
const PLAYING_TIME = '90';
function findLocation(nameOrAlias) {
const lower = nameOrAlias.toLowerCase();
for (const loc of LOCATIONS) {
if (loc.name.toLowerCase() === lower || loc.city.toLowerCase() === lower) return loc;
}
if (lower === 'ataomweg' || lower === 'atoom') return LOCATIONS[0];
if (lower === 'euro') return LOCATIONS[1];
if (lower === 'suiker') return LOCATIONS[2];
return null;
}
async function getAvailability(dateStr, locationName) {
const loc = findLocation(locationName) || LOCATIONS[0];
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-gpu'], executablePath: '/usr/bin/chromium-browser' });
const context = await browser.newContext({ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' });
const page = await context.newPage();
try {
// Bouw de URL met de juiste parameters per locatie
let url = `https://www.peakzpadel.nl/reserveren/court-booking/reservation?daypart=---&date=${encodeURIComponent(dateStr)}&location=${encodeURIComponent(loc.name)}&playingTimes=${PLAYING_TIME}`;
if (loc.courtTypeIds) {
url += '&courtTypeIds=' + loc.courtTypeIds;
}
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(3000);
const slots = await page.evaluate(() => {
const buttons = document.querySelectorAll('button');
const results = [];
for (const btn of buttons) {
const text = btn.textContent.trim();
if (text.match(/^\d{2}:\d{2}/)) {
const price = text.match(/€\s*([\d,]+)/);
results.push({
time: text.split(' ')[0],
price: price ? price[1] : null,
available: !btn.disabled
});
}
}
return results;
});
return {
date: dateStr,
location: loc.name,
indoor: loc.indoor,
slots,
available: slots.filter(s => s.available),
unavailable: slots.filter(s => !s.available)
};
} catch (err) {
return { error: err.message };
} finally {
await browser.close();
}
}
const dateArg = process.argv[2] || new Date().toISOString().split('T')[0];
const locationArg = process.argv[3] || 'Atoomweg';
getAvailability(dateArg, locationArg).then(result => {
if (result.error) { console.error('Fout:', result.error); process.exit(1); }
if (process.argv.includes('--json')) { console.log(JSON.stringify(result)); process.exit(0); }
const dayNames = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'];
const d = new Date(result.date);
const dayLabel = dayNames[d.getDay()] + ' ' + d.getDate() + '/' + (d.getMonth()+1);
const typeIcon = result.indoor ? '🏢' : '🌳';
const typeLabel = result.indoor ? 'Indoor' : 'Buiten';
console.log(`\n${typeIcon} Peakz Padel — ${result.location} (${typeLabel})`);
console.log(`📅 ${dayLabel}`);
console.log(`🟢 ${result.available.length} banen vrij`);
console.log(`🔴 ${result.unavailable.length} banen bezet`);
console.log('');
if (result.available.length > 0) {
console.log('Vrije tijden:');
for (const s of result.available.slice(0, 15)) {
console.log(` 🟢 ${s.time}${s.price ? ' — €' + s.price : ''}`);
}
if (result.available.length > 15) console.log(` ... en ${result.available.length - 15} meer`);
}
console.log('');
process.exit(0);
});