/* * Copyright (C) 2025 Jakub Žitník * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ import axios from "axios"; import { CookieJar } from "tough-cookie"; import { wrapper } from "axios-cookiejar-support"; import * as cheerio from "cheerio"; import { URLSearchParams } from "url"; import fs from "fs"; const BASE = "https://www.spsejecna.cz"; const PATHS = { SET_ROLE: "/user/role", LOGIN: "/user/login", TEACHERS: "/ucitel", TEACHER: teacherCode => `/ucitel/${teacherCode}` }; const DB_PATH = "db/persistent/timetables.json"; const jar = new CookieJar(); const client = wrapper(axios.create({ baseURL: BASE, jar, withCredentials: true, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } })); globalThis.File = class File {}; async function login(username, password) { await client.get("/"); await client.get(PATHS.SET_ROLE, { params: { role: "student" } }); const token3Res = await client.get("/"); const token3 = token3Res.data.match(/"token3"\s+value="(\d+)"/)[1]; const form = new URLSearchParams(); form.append('user', username); form.append('pass', password); form.append('token3', token3); form.append('submit', 'Přihlásit+se'); try { const response = await client.post(PATHS.LOGIN, form.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded" }, maxRedirects: 0 }); if (response.status == 200) { console.log("INVALID CREDENTIALS!"); process.exit(1); } } catch {} } async function getAllTeacherCodes() { const list = new Set(); const response = await client.get(PATHS.TEACHERS); const $ = cheerio.load(response.data); $("main .contentLeftColumn li, main .contentRightColumn li").each((_, el) => { const link = $(el).find("a"); const href = link.attr("href"); if (href) { const key = href.split("/").pop().toLowerCase(); list.add(key); } }); return list; } async function constructSchedules(allTeachers) { const classes = {}; function setupClass(className) { function generateArray(width, height) { return Array.from({ length: height }, () => Array.from({ length: width }, () => [])); } classes[className] = generateArray(10, 5); } let idk = 0; for (const key of allTeachers) { idk++; const response = await client.get(PATHS.TEACHER(key)); const $ = cheerio.load(response.data); const tbody = $('table.timetable > tbody'); if (!tbody.length) { console.log(`ERROR: ${key}`) continue; } tbody.find('tr').slice(1).each((dayIndex, tr) => { const $tr = $(tr); let currentHour = 0; $tr.find('td').each((_, td) => { const $td = $(td); const colspan = parseInt($td.attr('colspan') || '1', 10); const $subject = $td.find('span.subject'); const $class = $td.find('span.class'); const $group = $td.find('span.group'); const $room = $td.find('a.room'); const $employee = $td.find('a.employee'); const hasData = $subject.length && $class.length && $room.length && $employee.length; let cellData = null; let classText = ''; if (hasData) { classText = $class.text().trim(); cellData = { subject: $subject.text().trim(), title: $subject.attr('title')?.trim() || '', group: $group.length ? $group.text().trim() : null, room: $room.text().trim(), teacher: { code: $employee.text().trim().toLowerCase(), name: $employee.attr('title')?.trim() || '' } }; } for (let i = 0; i < colspan; i++) { if (currentHour >= 10) { break; } if (hasData && cellData) { if (classes[classText] === undefined) { setupClass(classText); } classes[classText][dayIndex][currentHour].push(cellData); } currentHour++; } }); }); console.log(`DONE: ${idk}/${allTeachers.size}`) } return classes; } await login(process.env.USERNAME, process.env.PASSWORD); const allTeachers = await getAllTeacherCodes(); const schedule = await constructSchedules(allTeachers) const str = JSON.stringify(schedule); fs.writeFileSync(DB_PATH, str, { encoding: "utf8" });