chuvsu-js 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ export { Schedule } from "./tt/schedule.js";
2
+ export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
3
+ export { Period, EducationType } from "./common/types.js";
4
+ export type { Time, WeekRange, Teacher, } from "./common/types.js";
5
+ export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, } from "./tt/types.js";
@@ -0,0 +1,2 @@
1
+ export { Schedule } from "./tt/schedule.js";
2
+ export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { LkClient } from "./lk/client.js";
2
2
  export { TtClient } from "./tt/client.js";
3
3
  export { Schedule } from "./tt/schedule.js";
4
4
  export type { CacheEntry } from "./common/cache.js";
5
- export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
5
+ export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
6
6
  export { Period, EducationType, AuthError, ParseError } from "./common/types.js";
7
7
  export type { Time, WeekRange, Teacher, } from "./common/types.js";
8
8
  export type { PersonalData, } from "./lk/types.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { LkClient } from "./lk/client.js";
2
2
  export { TtClient } from "./tt/client.js";
3
3
  export { Schedule } from "./tt/schedule.js";
4
- export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
4
+ export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, } from "./tt/utils.js";
5
5
  export { AuthError, ParseError } from "./common/types.js";
@@ -21,13 +21,19 @@ export declare class TtClient {
21
21
  private relogin;
22
22
  private authGet;
23
23
  private authPost;
24
- getSchedule(opts: {
24
+ private fetchSchedule;
25
+ /**
26
+ * Get schedule for all periods. The returned Schedule automatically
27
+ * routes queries to the correct period based on the date.
28
+ */
29
+ getSchedule(groupId: number): Promise<Schedule>;
30
+ /**
31
+ * Get schedule for a specific period only.
32
+ */
33
+ getScheduleForPeriod(opts: {
25
34
  groupId: number;
26
- period?: Period;
35
+ period: Period;
27
36
  }): Promise<Schedule>;
28
- getCurrentPeriod(opts?: {
29
- date?: Date;
30
- }): Period;
31
37
  getFaculties(): Promise<Faculty[]>;
32
38
  getGroupsForFaculty(opts: {
33
39
  facultyId: number;
package/dist/tt/client.js CHANGED
@@ -5,6 +5,12 @@ import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullS
5
5
  import { Schedule } from "./schedule.js";
6
6
  const BASE = "https://tt.chuvsu.ru";
7
7
  const AUTH_URL = `${BASE}/auth`;
8
+ const ALL_PERIODS = [
9
+ 1 /* Period.FallSemester */,
10
+ 2 /* Period.WinterSession */,
11
+ 3 /* Period.SpringSemester */,
12
+ 4 /* Period.SummerSession */,
13
+ ];
8
14
  export class TtClient {
9
15
  http = new HttpClient();
10
16
  educationType;
@@ -91,44 +97,40 @@ export class TtClient {
91
97
  return res;
92
98
  }
93
99
  // --- Schedule ---
94
- async getSchedule(opts) {
95
- const period = opts.period ?? this.getCurrentPeriod();
96
- const cacheKey = `${opts.groupId}:${period}`;
100
+ async fetchSchedule(groupId, period) {
101
+ const cacheKey = `${groupId}:${period}`;
97
102
  const cached = this.cache?.get("schedule", cacheKey);
98
- let days;
99
- if (cached) {
100
- days = cached;
101
- }
102
- else {
103
- const url = `${BASE}/index/grouptt/gr/${opts.groupId}`;
104
- let body;
105
- if (opts.period !== undefined) {
106
- ({ body } = await this.authPost(url, { htype: String(opts.period) }));
107
- }
108
- else {
109
- ({ body } = await this.authGet(url));
110
- }
111
- days = parseFullSchedule(body);
112
- this.cache?.set("schedule", cacheKey, days);
103
+ if (cached)
104
+ return cached;
105
+ const url = `${BASE}/index/grouptt/gr/${groupId}`;
106
+ const { body } = await this.authPost(url, { htype: String(period) });
107
+ const days = parseFullSchedule(body);
108
+ this.cache?.set("schedule", cacheKey, days);
109
+ return days;
110
+ }
111
+ /**
112
+ * Get schedule for all periods. The returned Schedule automatically
113
+ * routes queries to the correct period based on the date.
114
+ */
115
+ async getSchedule(groupId) {
116
+ const schedules = new Map();
117
+ const results = await Promise.all(ALL_PERIODS.map(async (period) => {
118
+ const days = await this.fetchSchedule(groupId, period);
119
+ return { period, days };
120
+ }));
121
+ for (const { period, days } of results) {
122
+ schedules.set(period, days);
113
123
  }
114
- return new Schedule(opts.groupId, period, days);
115
- }
116
- // --- Period ---
117
- getCurrentPeriod(opts) {
118
- const date = opts?.date ?? new Date();
119
- const month = date.getMonth();
120
- const day = date.getDate();
121
- // Dec 25+ and Jan → Winter session (зимняя сессия)
122
- if (month === 0 || (month === 11 && day >= 25))
123
- return 2 /* Period.WinterSession */;
124
- // Feb–May → Spring semester (весенний семестр)
125
- if (month >= 1 && month <= 4)
126
- return 3 /* Period.SpringSemester */;
127
- // Jun–Aug → Summer session (летняя сессия)
128
- if (month >= 5 && month <= 7)
129
- return 4 /* Period.SummerSession */;
130
- // Sep – Dec 24 → Fall semester (осенний семестр)
131
- return 1 /* Period.FallSemester */;
124
+ return new Schedule(groupId, schedules);
125
+ }
126
+ /**
127
+ * Get schedule for a specific period only.
128
+ */
129
+ async getScheduleForPeriod(opts) {
130
+ const days = await this.fetchSchedule(opts.groupId, opts.period);
131
+ const schedules = new Map();
132
+ schedules.set(opts.period, days);
133
+ return new Schedule(opts.groupId, schedules, opts.period);
132
134
  }
133
135
  // --- Search / Discovery ---
134
136
  async getFaculties() {
package/dist/tt/parse.js CHANGED
@@ -56,6 +56,14 @@ export function parseTeacherButtons(html) {
56
56
  }
57
57
  export function parseFullSchedule(html) {
58
58
  const doc = parseHtml(html);
59
+ // Session layout has date-based cells with ids like "trd20251224"
60
+ if (doc.querySelector('td[id^="trd2"]')) {
61
+ return parseSessionSchedule(doc);
62
+ }
63
+ return parseSemesterSchedule(doc);
64
+ }
65
+ // --- Semester schedule parsing (weekday-based, repeating weekly) ---
66
+ function parseSemesterSchedule(doc) {
59
67
  const days = [];
60
68
  const rows = doc.querySelectorAll("tr");
61
69
  let currentDay = null;
@@ -86,7 +94,7 @@ export function parseFullSchedule(html) {
86
94
  continue;
87
95
  const entries = [];
88
96
  for (const entryRow of dataCell.querySelectorAll("table tr")) {
89
- const entry = parseScheduleEntry(entryRow);
97
+ const entry = parseSemesterEntry(entryRow);
90
98
  if (entry)
91
99
  entries.push(entry);
92
100
  }
@@ -99,7 +107,7 @@ export function parseFullSchedule(html) {
99
107
  }
100
108
  return days;
101
109
  }
102
- function parseScheduleEntry(el) {
110
+ function parseSemesterEntry(el) {
103
111
  const td = el.querySelector("td") ?? el;
104
112
  const fullHtml = td.innerHTML ?? "";
105
113
  const plainText = text(td);
@@ -125,3 +133,80 @@ function parseScheduleEntry(el) {
125
133
  weekParity,
126
134
  };
127
135
  }
136
+ // --- Session schedule parsing (date-based, specific dates) ---
137
+ function parseSessionSchedule(doc) {
138
+ const days = [];
139
+ for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
140
+ // Parse date from cell id: trd20251224 -> 2025-12-24
141
+ const id = dateCell.getAttribute("id") ?? "";
142
+ const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
143
+ if (!dateMatch)
144
+ continue;
145
+ const year = parseInt(dateMatch[1]);
146
+ const month = parseInt(dateMatch[2]) - 1;
147
+ const dayNum = parseInt(dateMatch[3]);
148
+ const date = new Date(year, month, dayNum);
149
+ // Extract weekday from after <br>
150
+ const cellHtml = dateCell.innerHTML ?? "";
151
+ const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
152
+ const weekday = brMatch ? brMatch[1].trim() : "";
153
+ // Data cell is the next td.trdata sibling in the same row
154
+ const row = dateCell.parentElement;
155
+ if (!row)
156
+ continue;
157
+ const dataCell = row.querySelector("td.trdata:not(.trfd)");
158
+ if (!dataCell)
159
+ continue;
160
+ // Parse entries
161
+ const slots = [];
162
+ let slotNumber = 1;
163
+ for (const entryRow of dataCell.querySelectorAll("table tr")) {
164
+ const td = entryRow.querySelector("td") ?? entryRow;
165
+ const entry = parseSessionEntry(td);
166
+ if (!entry)
167
+ continue;
168
+ slots.push({
169
+ number: slotNumber++,
170
+ timeStart: entry.timeStart,
171
+ timeEnd: entry.timeEnd,
172
+ entries: [entry.entry],
173
+ });
174
+ }
175
+ if (slots.length > 0) {
176
+ days.push({ weekday, date, slots });
177
+ }
178
+ }
179
+ return days;
180
+ }
181
+ function parseSessionEntry(td) {
182
+ const fullHtml = td.innerHTML ?? "";
183
+ const plainText = text(td);
184
+ if (!plainText)
185
+ return null;
186
+ // Subject from blue span
187
+ const subjectEl = td.querySelector('span[style*="color: blue"]');
188
+ const subject = subjectEl ? text(subjectEl) : "";
189
+ if (!subject)
190
+ return null;
191
+ // Room: text before the first <span
192
+ const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
193
+ const room = roomMatch ? roomMatch[1].trim() : "";
194
+ // Type: parenthesized text after </span>, case-insensitive
195
+ const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
196
+ const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
197
+ // Time: after <br>, format HH:MM - HH:MM
198
+ const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
199
+ if (!timeMatch)
200
+ return null;
201
+ return {
202
+ entry: {
203
+ room,
204
+ subject,
205
+ type,
206
+ weeks: { from: 0, to: 0 },
207
+ teacher: { name: "" },
208
+ },
209
+ timeStart: parseTime(timeMatch[1]),
210
+ timeEnd: parseTime(timeMatch[2]),
211
+ };
212
+ }
@@ -2,11 +2,21 @@ import type { FullScheduleDay, SemesterWeek, Lesson } from "./types.js";
2
2
  import { Period } from "../common/types.js";
3
3
  export declare class Schedule {
4
4
  readonly groupId: number;
5
- readonly period: Period;
6
- readonly days: FullScheduleDay[];
7
- constructor(groupId: number, period: Period, days: FullScheduleDay[]);
5
+ private scheduleMap;
6
+ private _period?;
7
+ constructor(groupId: number, schedules: Map<number, FullScheduleDay[]>, period?: Period);
8
+ /** Current (or fixed) period for this schedule. */
9
+ get period(): Period;
10
+ /** Days for the current period. */
11
+ get days(): FullScheduleDay[];
12
+ /** All periods that have data in this schedule. */
13
+ get periods(): Period[];
14
+ /** Get days for a specific period. */
15
+ getDays(period: Period): FullScheduleDay[];
8
16
  private getSlotsForWeekday;
9
17
  private getDateForWeekday;
18
+ private static isSameDay;
19
+ private getSessionLessonsForDate;
10
20
  forDay(weekday: number, opts?: {
11
21
  subgroup?: number;
12
22
  week?: number;
@@ -1,23 +1,40 @@
1
- import { getWeekdayName, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, filterSlots, slotsToLessons, } from "./utils.js";
1
+ import { getCurrentPeriod, isSessionPeriod, getWeekdayName, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, filterSlots, slotsToLessons, } from "./utils.js";
2
2
  export class Schedule {
3
3
  groupId;
4
- period;
5
- days;
6
- constructor(groupId, period, days) {
4
+ scheduleMap;
5
+ _period;
6
+ constructor(groupId, schedules, period) {
7
7
  this.groupId = groupId;
8
- this.period = period;
9
- this.days = days;
8
+ this.scheduleMap = schedules;
9
+ this._period = period;
10
10
  }
11
- getSlotsForWeekday(weekday, opts) {
11
+ /** Current (or fixed) period for this schedule. */
12
+ get period() {
13
+ return this._period ?? getCurrentPeriod();
14
+ }
15
+ /** Days for the current period. */
16
+ get days() {
17
+ return this.scheduleMap.get(this.period) ?? [];
18
+ }
19
+ /** All periods that have data in this schedule. */
20
+ get periods() {
21
+ return [...this.scheduleMap.keys()];
22
+ }
23
+ /** Get days for a specific period. */
24
+ getDays(period) {
25
+ return this.scheduleMap.get(period) ?? [];
26
+ }
27
+ // --- Semester helpers (weekday-based) ---
28
+ getSlotsForWeekday(weekday, days, opts) {
12
29
  const dayName = getWeekdayName(weekday);
13
- const day = this.days.find((d) => d.weekday.toLowerCase() === dayName.toLowerCase());
30
+ const day = days.find((d) => d.weekday.toLowerCase() === dayName.toLowerCase());
14
31
  if (!day)
15
32
  return [];
16
33
  return filterSlots(day.slots, opts);
17
34
  }
18
- getDateForWeekday(weekday, week) {
35
+ getDateForWeekday(weekday, period, week) {
19
36
  if (week != null) {
20
- const startMonday = getMonday(getSemesterStart({ period: this.period }));
37
+ const startMonday = getMonday(getSemesterStart({ period }));
21
38
  const date = new Date(startMonday);
22
39
  date.setDate(startMonday.getDate() +
23
40
  (week - 1) * 7 +
@@ -32,24 +49,85 @@ export class Schedule {
32
49
  date.setHours(0, 0, 0, 0);
33
50
  return date;
34
51
  }
52
+ // --- Session helpers (date-based) ---
53
+ static isSameDay(a, b) {
54
+ return (a.getFullYear() === b.getFullYear() &&
55
+ a.getMonth() === b.getMonth() &&
56
+ a.getDate() === b.getDate());
57
+ }
58
+ getSessionLessonsForDate(days, date) {
59
+ const day = days.find((d) => d.date && Schedule.isSameDay(d.date, date));
60
+ if (!day)
61
+ return [];
62
+ return slotsToLessons(day.slots, date);
63
+ }
64
+ // --- Public query methods ---
35
65
  forDay(weekday, opts) {
36
- const slots = this.getSlotsForWeekday(weekday, opts);
37
- const date = this.getDateForWeekday(weekday, opts?.week);
66
+ const period = this.period;
67
+ const days = this.getDays(period);
68
+ if (isSessionPeriod(period)) {
69
+ // For sessions, return all entries on days matching this weekday
70
+ const lessons = [];
71
+ const dayName = getWeekdayName(weekday);
72
+ for (const d of days) {
73
+ if (d.weekday.toLowerCase() === dayName.toLowerCase() && d.date) {
74
+ lessons.push(...slotsToLessons(d.slots, d.date));
75
+ }
76
+ }
77
+ return lessons;
78
+ }
79
+ const slots = this.getSlotsForWeekday(weekday, days, opts);
80
+ const date = this.getDateForWeekday(weekday, period, opts?.week);
38
81
  return slotsToLessons(slots, date);
39
82
  }
40
83
  forDate(date, opts) {
84
+ const period = getCurrentPeriod({ date });
85
+ const days = this.getDays(period);
86
+ if (isSessionPeriod(period)) {
87
+ const lessons = this.getSessionLessonsForDate(days, date);
88
+ if (lessons.length > 0)
89
+ return lessons;
90
+ }
91
+ // Try session periods for exact date match (sessions can have entries
92
+ // on dates that fall outside their "official" period boundaries)
93
+ for (const [p, d] of this.scheduleMap) {
94
+ if (p === period)
95
+ continue;
96
+ const match = d.find((day) => day.date && Schedule.isSameDay(day.date, date));
97
+ if (match)
98
+ return slotsToLessons(match.slots, date);
99
+ }
100
+ if (isSessionPeriod(period))
101
+ return [];
41
102
  const weekday = date.getDay();
42
- const week = getWeekNumber({ period: this.period, date });
43
- const slots = this.getSlotsForWeekday(weekday, {
103
+ const week = getWeekNumber({ period, date });
104
+ const slots = this.getSlotsForWeekday(weekday, days, {
44
105
  subgroup: opts?.subgroup,
45
106
  week,
46
107
  });
47
108
  return slotsToLessons(slots, date);
48
109
  }
49
110
  forWeek(week, opts) {
50
- const effectiveWeek = week ?? getWeekNumber({ period: this.period });
111
+ const period = this.period;
112
+ const days = this.getDays(period);
113
+ if (isSessionPeriod(period)) {
114
+ // For sessions, return all entries within this calendar week
115
+ const now = new Date();
116
+ const monday = getMonday(now);
117
+ const sunday = new Date(monday);
118
+ sunday.setDate(monday.getDate() + 6);
119
+ sunday.setHours(23, 59, 59, 999);
120
+ const lessons = [];
121
+ for (const d of days) {
122
+ if (d.date && d.date >= monday && d.date <= sunday) {
123
+ lessons.push(...slotsToLessons(d.slots, d.date));
124
+ }
125
+ }
126
+ return lessons;
127
+ }
128
+ const effectiveWeek = week ?? getWeekNumber({ period });
51
129
  const semesterWeeks = getSemesterWeeks({
52
- period: this.period,
130
+ period,
53
131
  weekCount: effectiveWeek,
54
132
  });
55
133
  const weekData = semesterWeeks.find((w) => w.week === effectiveWeek);
@@ -60,7 +138,7 @@ export class Schedule {
60
138
  date.setDate(mondayDate.getDate() + i);
61
139
  date.setHours(0, 0, 0, 0);
62
140
  const weekday = i === 6 ? 0 : i + 1;
63
- const slots = this.getSlotsForWeekday(weekday, {
141
+ const slots = this.getSlotsForWeekday(weekday, days, {
64
142
  subgroup: opts?.subgroup,
65
143
  week: effectiveWeek,
66
144
  });
@@ -26,6 +26,7 @@ export interface FullScheduleSlot {
26
26
  }
27
27
  export interface FullScheduleDay {
28
28
  weekday: string;
29
+ date?: Date;
29
30
  slots: FullScheduleSlot[];
30
31
  }
31
32
  export interface LessonTimeSlot {
@@ -1,5 +1,9 @@
1
1
  import type { FullScheduleSlot, SemesterWeek, Lesson } from "./types.js";
2
2
  import { Period } from "../common/types.js";
3
+ export declare function getCurrentPeriod(opts?: {
4
+ date?: Date;
5
+ }): Period;
6
+ export declare function isSessionPeriod(period: Period): boolean;
3
7
  export declare function getWeekdayName(weekday: number): string;
4
8
  export declare function getMonday(date: Date): Date;
5
9
  /**
package/dist/tt/utils.js CHANGED
@@ -1,3 +1,22 @@
1
+ export function getCurrentPeriod(opts) {
2
+ const date = opts?.date ?? new Date();
3
+ const month = date.getMonth();
4
+ const day = date.getDate();
5
+ // Dec 25+ and Jan → Winter session (зимняя сессия)
6
+ if (month === 0 || (month === 11 && day >= 25))
7
+ return 2 /* Period.WinterSession */;
8
+ // Feb–May → Spring semester (весенний семестр)
9
+ if (month >= 1 && month <= 4)
10
+ return 3 /* Period.SpringSemester */;
11
+ // Jun–Aug → Summer session (летняя сессия)
12
+ if (month >= 5 && month <= 7)
13
+ return 4 /* Period.SummerSession */;
14
+ // Sep – Dec 24 → Fall semester (осенний семестр)
15
+ return 1 /* Period.FallSemester */;
16
+ }
17
+ export function isSessionPeriod(period) {
18
+ return period === 2 /* Period.WinterSession */ || period === 4 /* Period.SummerSession */;
19
+ }
1
20
  const WEEKDAY_NAMES = [
2
21
  "Воскресенье",
3
22
  "Понедельник",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Node.js library for ChuvSU student portal (lk.chuvsu.ru) and schedule (tt.chuvsu.ru)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
+ },
13
+ "./browser": {
14
+ "import": "./dist/browser.js",
15
+ "types": "./dist/browser.d.ts"
12
16
  }
13
17
  },
14
18
  "files": [