diff --git a/edu_master/webinar-checker/checker.py b/edu_master/webinar-checker/checker.py
index df23329..80fbddc 100644
--- a/edu_master/webinar-checker/checker.py
+++ b/edu_master/webinar-checker/checker.py
@@ -115,6 +115,7 @@ TRANSLATIONS = {
'month': '📅 Весь месяц',
'no_events': 'Нет событий',
'no_lessons': 'Нет уроков',
+ 'time_unknown': 'неизвестно',
'free_period': 'Свободно',
'unsubscribed': '🔕 Вы отписались от уведомлений.',
'diary_title': '📅 Дневник — выберите период:',
@@ -186,6 +187,7 @@ TRANSLATIONS = {
'month': '📅 Весь місяць',
'no_events': 'Немає подій',
'no_lessons': 'Немає уроків',
+ 'time_unknown': 'невідомо',
'free_period': 'Вільно',
'unsubscribed': '🔕 Ви відписалися від сповіщень.',
'diary_title': '📅 Щоденник — виберіть період:',
@@ -257,6 +259,7 @@ TRANSLATIONS = {
'month': '📅 Whole month',
'no_events': 'No events',
'no_lessons': 'No lessons',
+ 'time_unknown': 'unknown',
'free_period': 'Free',
'unsubscribed': '🔕 You have unsubscribed from notifications.',
'diary_title': '📅 Diary — choose a period:',
@@ -490,7 +493,10 @@ def get_diary_keyboard(user_id: int | None = None):
def _parse_calendar_html(table_html: str) -> tuple:
- """Parse calendar HTML table into (month_text, {day_num: {weekday, events}})."""
+ """Parse calendar HTML table into (month_text, {day_num: {weekday, events}}).
+
+ Each event is a dict: {'title': str, 'id': site event id | None, 'time': 'HH:MM' | None}.
+ """
days = {}
weekdays = []
rows = re.findall(r'
]*>(.*?)
', table_html, re.DOTALL)
@@ -522,22 +528,101 @@ def _parse_calendar_html(table_html: str) -> tuple:
continue
day_num = dm.group(1)
- # Extract events: title attribute (full name) of ALL tags inside the cell
+ # Extract events: title attribute (full name) of ALL tags inside the cell.
+ # Each event keeps its site data-event-id and a 'time' slot (filled later
+ # from the AJAX popup by fetch_diary_data). 'time' is 'HH:MM' or None.
events = []
for a_match in re.finditer(r']*>(.*?)', cell, re.DOTALL):
a_tag = a_match.group(0)
# Prefer the title attribute (contains full name, not truncated)
title_m = re.search(r'title\s*=\s*"([^"]*)"', a_tag)
et = title_m.group(1).strip() if title_m else re.sub(r'<[^>]+>', '', a_match.group(1)).strip()
- if et:
- events.append(et)
+ if not et:
+ continue
+ id_m = re.search(r'data-event-id\s*=\s*"?(\d+)"?', a_tag)
+ events.append(
+ {
+ 'title': et,
+ 'id': id_m.group(1) if id_m else None,
+ 'time': None,
+ }
+ )
weekday = weekdays[col_idx] if col_idx < len(weekdays) else ''
- days[day_num] = {'weekday': weekday, 'events': events}
+ days[day_num] = {'weekday': weekday, 'weekday_idx': col_idx, 'events': events}
return month_text, days
+async def _collect_event_times(page) -> dict:
+ """Click every calendar `a.event-link` and read the time from the AJAX popup.
+
+ The site shows event details only after a click fills the fancybox container
+ (`div.event-full-info span.data`). Returns {event_id: 'HH:MM'} for events
+ that have a date; events without one are simply skipped. A failed click on a
+ single event never breaks the whole diary parse.
+ """
+ times_by_id: dict[str, str] = {}
+ try:
+ links = await page.query_selector_all('a.event-link')
+ except Exception as e:
+ logger.warning(f'Failed to list diary event links: {e}')
+ return times_by_id
+
+ for link in links:
+ event_id = None
+ event_deadline = time.monotonic() + 10
+ try:
+ event_id = await link.get_attribute('data-event-id')
+ if not event_id or event_id in times_by_id:
+ continue
+ await link.evaluate('(el) => el.click()')
+ await page.wait_for_selector(
+ f'div.event-full-info[data-event-full-info-id="{event_id}"] span.data',
+ timeout=3000,
+ state='visible',
+ )
+ if time.monotonic() > event_deadline:
+ break
+ time_text = await page.evaluate(
+ """(id) => {
+ const nodes = document.querySelectorAll(
+ 'div.event-full-info[data-event-full-info-id="' + id + '"] span.data'
+ );
+ for (const el of nodes) {
+ if (el.getClientRects().length > 0 || el.offsetParent !== null) {
+ return el.textContent.trim();
+ }
+ }
+ return null;
+ }""",
+ event_id,
+ )
+ if time_text:
+ m = re.search(r'(\d{1,2}:\d{2})', time_text)
+ if m:
+ times_by_id[event_id] = m.group(1)
+ except Exception as e:
+ logger.debug(f'Failed to read time for diary event {event_id}: {e}')
+ if time.monotonic() > event_deadline:
+ break
+ finally:
+ # Close the fancybox popup (click the close button, else Escape).
+ closed = False
+ with contextlib.suppress(Exception):
+ close_btn = await page.query_selector('#fancybox-close')
+ if close_btn is not None:
+ await close_btn.click(timeout=2000)
+ closed = True
+ if not closed:
+ with contextlib.suppress(Exception):
+ await page.keyboard.press('Escape')
+ await page.wait_for_timeout(150)
+
+ logger.info(f'Diary event times collected for {len(times_by_id)} events')
+ return times_by_id
+
+
async def fetch_diary_data(phpsessid: str) -> dict | None:
logger.info('Fetching diary data via Playwright...')
try:
@@ -570,6 +655,16 @@ async def fetch_diary_data(phpsessid: str) -> dict | None:
f.write(table_html)
month_text, days = _parse_calendar_html(table_html)
+
+ # Read event times by opening each event's AJAX popup.
+ times_by_id = await _collect_event_times(page)
+ if times_by_id:
+ for day_data in days.values():
+ for ev in day_data.get('events', []):
+ eid = ev.get('id')
+ if eid and eid in times_by_id:
+ ev['time'] = times_by_id[eid]
+
logger.info(
f'Diary parsed: month={month_text!r}, days_with_events={sum(1 for d in days.values() if d["events"])}/{len(days)}'
)
@@ -596,6 +691,25 @@ def _parse_diary_month(text: str) -> str:
return text.strip()
+def _format_event_line(event, lang: str) -> str:
+ """Render one diary event line: 📌 title, with time in parens when known.
+
+ '08:00' is the site's placeholder for "no time specified" — show a localized
+ 'time_unknown' mark instead. Unknown/absent time renders as before, no parens.
+ """
+ if isinstance(event, dict):
+ title = escape(str(event.get('title') or ''))
+ tme = event.get('time')
+ else:
+ title = escape(str(event))
+ tme = None
+ if tme == '08:00':
+ return f'📌 {title} ({_tr(lang, "time_unknown")})'
+ if tme:
+ return f'📌 {title} ({escape(str(tme))})'
+ return f'📌 {title}'
+
+
def format_diary_day(data: dict, day_num: int, lang: str = 'en') -> str:
days = data.get('days', {})
month_str = _parse_diary_month(data.get('monthFullText', ''))
@@ -605,7 +719,7 @@ def format_diary_day(data: dict, day_num: int, lang: str = 'en') -> str:
lines.append(_tr(lang, 'no_events'))
else:
for e in day_data['events']:
- lines.append(f'📌 {escape(e)}')
+ lines.append(_format_event_line(e, lang))
lines.append(f'\n🔗 {DIARY_URL}')
return '\n'.join(lines)
@@ -614,12 +728,12 @@ def format_diary_week(data: dict, today: datetime, lang: str = 'en') -> str:
days = data.get('days', {})
_parse_diary_month(data.get('monthFullText', ''))
monday = today - timedelta(days=today.weekday())
- sunday = monday + timedelta(days=6)
+ friday = monday + timedelta(days=4)
start = f'{monday.day}.{monday.month}'
- end = f'{sunday.day}.{sunday.month}'
+ end = f'{friday.day}.{friday.month}'
short = DIARY_WEEKDAYS_SHORT.get(lang, DIARY_WEEKDAYS_SHORT['en'])
lines = [_tr(lang, 'diary_week_title', start=start, end=end) + '\n']
- for i in range(7):
+ for i in range(5):
d = monday + timedelta(days=i)
day_data = days.get(str(d.day))
lines.append(f'─ {short[i]} {d.day}.{d.month} ─')
@@ -627,7 +741,7 @@ def format_diary_week(data: dict, today: datetime, lang: str = 'en') -> str:
lines.append(_tr(lang, 'no_events') + '\n')
else:
for e in day_data['events']:
- lines.append(f'📌 {escape(e)}')
+ lines.append(_format_event_line(e, lang))
lines.append('')
lines.append(f'🔗 {DIARY_URL}')
return '\n'.join(lines)
@@ -639,6 +753,17 @@ def format_diary_month(data: dict, lang: str = 'en') -> str:
lines = [f'📅 {month_str}\n']
for day_num in sorted(days.keys(), key=int):
day_data = days[day_num]
+ if day_data.get('weekday_idx', 0) >= 5:
+ continue
+ if day_data.get('weekday', '').strip().lower() in (
+ 'субота',
+ 'суббота',
+ 'saturday',
+ 'неділя',
+ 'воскресенье',
+ 'sunday',
+ ):
+ continue
events = day_data.get('events', [])
weekday = day_data.get('weekday', '')
lines.append(f'─ {weekday} {day_num} ─')
@@ -646,7 +771,7 @@ def format_diary_month(data: dict, lang: str = 'en') -> str:
lines.append(_tr(lang, 'no_events') + '\n')
else:
for e in events:
- lines.append(f'📌 {escape(e)}')
+ lines.append(_format_event_line(e, lang))
lines.append('')
lines.append(f'🔗 {DIARY_URL}')
return '\n'.join(lines)
@@ -676,7 +801,7 @@ def get_schedule_day_keyboard(user_id: int | None = None):
tomorrow = today + timedelta(days=1)
today_label = _tr(lang, 'today')
tomorrow_label = _tr(lang, 'tomorrow')
- short_keys = ['day_mon', 'day_tue', 'day_wed', 'day_thu', 'day_fri', 'day_sat', 'day_sun']
+ short_keys = ['day_mon', 'day_tue', 'day_wed', 'day_thu', 'day_fri']
keyboard = [
[
InlineKeyboardButton(
@@ -706,14 +831,6 @@ def get_schedule_day_keyboard(user_id: int | None = None):
InlineKeyboardButton(
_tr(lang, short_keys[4]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[4]}'
),
- InlineKeyboardButton(
- _tr(lang, short_keys[5]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[5]}'
- ),
- ],
- [
- InlineKeyboardButton(
- _tr(lang, short_keys[6]), callback_data=f'schedule_day_{SCHEDULE_WEEKDAYS_CANONICAL[6]}'
- ),
],
]
return InlineKeyboardMarkup(keyboard)