diff --git a/README.md b/README.md index 400b5fd..a13af66 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ Reskin of https://lk.ks.psuti.ru/ since it lacks mobile support. Tools used: pnpm, eslint, react-icons. Deployed with Netlify and supported by Cloudflare. +## Known issues + +- Previous week cannot be accessed if you enter from main "/" +Workaround: Locate to next week, then enter previous twice. + ## Development ### Prerequisites @@ -106,7 +111,9 @@ Install the application directly on a Linux system as a systemd service: # Clone the repository git clone cd kspguti-schedule - +# Copy example and edit .env +cp .env.production.example .env +nano .env # Run the installation script sudo ./scripts/install.sh ``` diff --git a/src/app/agregator/schedule.ts b/src/app/agregator/schedule.ts index 6791127..6be8a00 100644 --- a/src/app/agregator/schedule.ts +++ b/src/app/agregator/schedule.ts @@ -13,22 +13,32 @@ export type ScheduleResult = { } // ПС-7: 146 -export async function getSchedule(groupID: number, groupName: string, wk?: number): Promise { +export async function getSchedule(groupID: number, groupName: string, wk?: number, parseWeekNavigation: boolean = true): Promise { const url = `${PROXY_URL}/?mn=2&obj=${groupID}${wk ? `&wk=${wk}` : ''}` const page = await fetch(url) // const page = { text: async () => mockContent, status: 200, headers: { get: (s: string) => s && 'text/html' } } const content = await page.text() const contentType = page.headers.get('content-type') if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') { + let dom: JSDOM | null = null try { - const root = new JSDOM(content, { url }).window.document - const result = parsePage(root, groupName, url) - return { + dom = new JSDOM(content, { url }) + const root = dom.window.document + const result = parsePage(root, groupName, url, parseWeekNavigation) + const scheduleResult = { days: result.days, currentWk: result.currentWk || wk, availableWeeks: result.availableWeeks } + // Явно очищаем JSDOM для освобождения памяти + dom.window.close() + dom = null + return scheduleResult } catch(e) { + // Очищаем JSDOM даже в случае ошибки + if (dom) { + dom.window.close() + } console.error(`Error while parsing ${PROXY_URL}`) reportParserError(new Date().toISOString(), 'Не удалось сделать парсинг для группы', groupName) throw e diff --git a/src/app/parser/schedule.ts b/src/app/parser/schedule.ts index c0badff..a236c48 100644 --- a/src/app/parser/schedule.ts +++ b/src/app/parser/schedule.ts @@ -29,34 +29,24 @@ function parseWeekNavigation(document: Document, currentWeekNumber: number, curr const wkToWeekNumber = new Map() // Ищем все ссылки, которые содержат параметр wk + // Используем более специфичные селекторы вместо перебора всех элементов const links = Array.from(document.querySelectorAll('a[href*="wk="]')) - // Также ищем ссылки в onclick и других атрибутах + // Также ищем ссылки в onclick (только для ссылок, не всех элементов) const linksWithOnclick = Array.from(document.querySelectorAll('a[onclick*="wk="], a[onclick*="wk"]')) // Ищем в формах const forms = Array.from(document.querySelectorAll('form[action*="wk="], form input[name="wk"]')) - // Ищем во всех элементах, которые могут содержать URL с wk - const allElements = Array.from(document.querySelectorAll('*')) - const elementsWithWk: Element[] = [] + // Ищем в элементах с data-атрибутами (только те, которые могут содержать ссылки) + const elementsWithDataHref = Array.from(document.querySelectorAll('[data-href*="wk="]')) - for (const el of allElements) { - const href = el.getAttribute('href') - const onclick = el.getAttribute('onclick') - const action = el.getAttribute('action') - const dataHref = el.getAttribute('data-href') - - if ((href && href.includes('wk=')) || - (onclick && onclick.includes('wk=')) || - (action && action.includes('wk=')) || - (dataHref && dataHref.includes('wk='))) { - elementsWithWk.push(el) - } - } - - // Объединяем все найденные элементы - const allLinkElements = [...links, ...linksWithOnclick, ...elementsWithWk] + // Объединяем все найденные элементы (убираем дубликаты) + const allLinkElementsSet = new Set() + links.forEach(el => allLinkElementsSet.add(el)) + linksWithOnclick.forEach(el => allLinkElementsSet.add(el)) + elementsWithDataHref.forEach(el => allLinkElementsSet.add(el)) + const allLinkElements = Array.from(allLinkElementsSet) for (const link of allLinkElements) { // Пробуем извлечь wk из разных атрибутов @@ -332,7 +322,7 @@ const parseLesson = (row: Element): Lesson | null => { } } -export function parsePage(document: Document, groupName: string, url?: string): ParseResult { +export function parsePage(document: Document, groupName: string, url?: string, shouldParseWeekNavigation: boolean = true): ParseResult { const tables = Array.from(document.querySelectorAll('body > table')) const table = tables.find(table => table.querySelector(':scope > tbody > tr:first-child')?.textContent?.trim() === groupName) const rows = Array.from(table!.children[0].children).filter(el => el.tagName === 'TR').slice(2) @@ -382,11 +372,11 @@ export function parsePage(document: Document, groupName: string, url?: string): } } - // Парсим навигацию по неделям + // Парсим навигацию по неделям только если включена навигация let availableWeeks: WeekInfo[] | undefined let finalCurrentWk = currentWk - if (currentWeekNumber) { + if (shouldParseWeekNavigation && currentWeekNumber) { availableWeeks = parseWeekNavigation(document, currentWeekNumber, currentWk) // Если не нашли ссылки, но есть текущий wk, добавляем текущую неделю diff --git a/src/pages/[group].tsx b/src/pages/[group].tsx index 6787ff5..ab5ddc9 100644 --- a/src/pages/[group].tsx +++ b/src/pages/[group].tsx @@ -82,6 +82,33 @@ export default function HomePage(props: NextSerialized) { const cachedSchedules = new Map() const maxCacheDurationInMS = 1000 * 60 * 60 +const maxCacheSize = 50 // Максимальное количество записей в кэше (только текущие недели) + +// Очистка старых записей из кэша +function cleanupCache() { + const now = Date.now() + const entriesToDelete: string[] = [] + + // Находим устаревшие записи + for (const [key, value] of cachedSchedules.entries()) { + if (now - value.lastFetched.getTime() >= maxCacheDurationInMS) { + entriesToDelete.push(key) + } + } + + // Удаляем устаревшие записи + entriesToDelete.forEach(key => cachedSchedules.delete(key)) + + // Если кэш все еще слишком большой, удаляем самые старые записи + if (cachedSchedules.size > maxCacheSize) { + const sortedEntries = Array.from(cachedSchedules.entries()) + .sort((a, b) => a[1].lastFetched.getTime() - b[1].lastFetched.getTime()) + + const toRemove = sortedEntries.slice(0, cachedSchedules.size - maxCacheSize) + toRemove.forEach(([key]) => cachedSchedules.delete(key)) + } +} + export async function getServerSideProps(context: GetServerSidePropsContext<{ group: string }>): Promise>> { const groups = loadGroups() const settings = loadSettings() @@ -93,9 +120,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ gr let scheduleResult: ScheduleResult let parsedAt - // Ключ кэша включает группу и неделю - const cacheKey = wk ? `${group}_${wk}` : group - const cachedSchedule = cachedSchedules.get(cacheKey) + // Очищаем старые записи из кэша перед использованием + cleanupCache() + + // Кэшируем только текущую неделю (без параметра wk) + // Если запрашивается конкретная неделя (wk указан), не используем кэш + const useCache = !wk + const cacheKey = group // Ключ кэша - только группа (текущая неделя) + const cachedSchedule = useCache ? cachedSchedules.get(cacheKey) : undefined if (cachedSchedule?.lastFetched && Date.now() - cachedSchedule.lastFetched.getTime() < maxCacheDurationInMS) { scheduleResult = cachedSchedule.results @@ -103,9 +135,16 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ gr } else { try { const groupInfo = groups[group] - scheduleResult = await getSchedule(groupInfo.parseId, groupInfo.name, wk) + // Передаем настройки в getSchedule для условного парсинга навигации + scheduleResult = await getSchedule(groupInfo.parseId, groupInfo.name, wk, settings.weekNavigationEnabled) parsedAt = new Date() - cachedSchedules.set(cacheKey, { lastFetched: new Date(), results: scheduleResult }) + + // Кэшируем только текущую неделю + if (useCache) { + cachedSchedules.set(cacheKey, { lastFetched: new Date(), results: scheduleResult }) + // Очищаем кэш после добавления новой записи, если он стал слишком большим + cleanupCache() + } } catch(e) { if (cachedSchedule?.lastFetched) { scheduleResult = cachedSchedule.results