#!/usr/bin/env node // Manual update script for semester timetable data // Run this script manually each semester to update timetable IDs and dates // Usage: node src/scripts/update-semester-data.js import https from 'https'; import fs from 'fs'; // Get current academic year function getCurrentAcademicYear() { const now = new Date(); const currentYear = now.getFullYear(); const currentMonth = now.getMonth(); // 0-based // Academic year starts in September, but August belongs to the new year const startYear = currentMonth >= 7 ? currentYear : currentYear - 1; const endYear = startYear + 1; return { startYear, endYear }; } // Fetch webpage content function fetchPage(url) { return new Promise((resolve, reject) => { https.get(url, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { resolve(data); }); }).on('error', (err) => { reject(err); }); }); } // Parse timetable IDs from the schedule page async function parseTimetableIds() { try { const html = await fetchPage('https://www.dm.unipi.it/didattica/lezioni-esami/orario-delle-lezioni/'); const ids = {}; // Extract links with their text to match correctly const linkPattern = /]*href="[^"]*linkCalendarioId=([a-f0-9]{24})"[^>]*>([^<]+)<\/a>/g; const links = [...html.matchAll(linkPattern)]; for (const [fullMatch, id, text] of links) { const cleanText = text.trim(); if (cleanText === 'Triennale I anno') { ids['anno-1'] = id; } else if (cleanText === 'Triennale II anno') { ids['anno-2'] = id; } else if (cleanText === 'Triennale III anno') { ids['anno-3'] = id; } else if (cleanText === 'Magistrale') { ids['magistrale'] = id; } } return Object.keys(ids).length > 0 ? ids : null; } catch (error) { console.error('Error parsing timetable IDs:', error.message); return null; } } // Parse semester start date from calendar page async function parseSemesterStartDate() { try { const { startYear, endYear } = getCurrentAcademicYear(); const currentMonth = new Date().getMonth(); const currentSemester = currentMonth >= 1 && currentMonth <= 6 ? 2 : 1; const url = `https://www.dm.unipi.it/didattica/lezioni-esami/calendario-delle-attivita-didattiche/calendario-delle-attivita-didattiche-a-a-${startYear}-${endYear.toString().slice(-2)}/`; const html = await fetchPage(url); // Look for the appropriate semester start date const semesterText = currentSemester === 1 ? 'I semestre' : 'II semestre'; const semesterRegex = new RegExp(`Lezioni\\s+${semesterText}.*?(\\d{1,2}\\s+\\w+\\s+\\d{4})`, 'si'); const match = html.match(semesterRegex); if (match) { return match[1]; } else { console.error(`Could not find ${semesterText} start date in calendar`); return null; } } catch (error) { console.error('Error parsing semester start date:', error.message); return null; } } // Find the first Monday after a given date string function getFirstMondayAfter(dateStr) { // Parse Italian date format (e.g., "24 settembre 2025") const [day, monthName, year] = dateStr.split(' '); const monthMap = { 'gennaio': 0, 'febbraio': 1, 'marzo': 2, 'aprile': 3, 'maggio': 4, 'giugno': 5, 'luglio': 6, 'agosto': 7, 'settembre': 8, 'ottobre': 9, 'novembre': 10, 'dicembre': 11 }; const month = monthMap[monthName.toLowerCase()]; if (month === undefined) { throw new Error(`Unknown month: ${monthName}`); } const startDate = new Date(parseInt(year), month, parseInt(day)); const dayOfWeek = startDate.getDay(); // 0=Sunday, 1=Monday, etc. // Calculate days to add to get to the next Monday let daysToAdd; if (dayOfWeek === 0) { // Sunday daysToAdd = 1; // Next day is Monday } else if (dayOfWeek === 1) { // Monday daysToAdd = 7; // Next Monday } else { // Tuesday-Saturday (2-6) daysToAdd = 8 - dayOfWeek; // Days until next Monday } const result = new Date(startDate); result.setDate(startDate.getDate() + daysToAdd); // Create a new date in UTC to avoid timezone issues with the calendar return new Date(Date.UTC(result.getFullYear(), result.getMonth(), result.getDate())); } // Format date for button text (Italian format) function formatDateForButton(date) { const day = date.getUTCDate(); const monthNames = [ 'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre' ]; const month = monthNames[date.getUTCMonth()]; return `${day} ${month}`; } // Main function async function updateAcademicData() { const [timetableIds, semesterStartDateStr] = await Promise.all([ parseTimetableIds(), parseSemesterStartDate() ]); // Fail fast if parsing failed if (!timetableIds) { throw new Error('Failed to parse timetable IDs from the website'); } if (!semesterStartDateStr) { throw new Error('Failed to parse semester start date from the calendar'); } const firstMonday = getFirstMondayAfter(semesterStartDateStr); const semesterData = { timetableIds, semesterStartDateStr, firstMondayDate: firstMonday.toISOString(), buttonText: `Vai al ${formatDateForButton(firstMonday)}! 🚀`, lastUpdated: new Date().toISOString(), academicYear: getCurrentAcademicYear() }; // Save to src/semester-data.json fs.writeFileSync('src/semester-data.json', JSON.stringify(semesterData, null, 2)); console.log('✅ Semester data updated successfully'); } // Run the script updateAcademicData().catch(error => { console.error('💥 Script failed:', error); process.exit(1); });