chuvsu-js 4.1.2 → 4.1.5

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.
package/README.md CHANGED
@@ -18,6 +18,18 @@ npm install chuvsu-js
18
18
 
19
19
  ## Быстрый старт
20
20
 
21
+ ### Проверка расписаний на живых данных
22
+
23
+ Укажите `TT_EMAIL` и `TT_PASSWORD` в `.env`, затем запустите:
24
+
25
+ ```bash
26
+ pnpm test:live:schedules
27
+ ```
28
+
29
+ По умолчанию проверяются 50 групп. Доступны `--limit=100`, `--all` и
30
+ `--group=КТ-41-24` (аргументы передаются после `--`). Значения авторизации
31
+ скрипт не выводит.
32
+
21
33
  ### Расписание (TtClient)
22
34
 
23
35
  ```ts
@@ -8,6 +8,7 @@ export declare class TtClient {
8
8
  private cache;
9
9
  private blobAdapter;
10
10
  private loginMode;
11
+ private timetableContext;
11
12
  constructor(opts?: TtClientOptions);
12
13
  private get pertt();
13
14
  clearCache(category?: keyof CacheConfig): Promise<void>;
@@ -23,6 +24,8 @@ export declare class TtClient {
23
24
  private authGet;
24
25
  private authGetBuffer;
25
26
  private authPost;
27
+ /** Resolve the academic year and active period from tt.chuvsu.ru itself. */
28
+ private getTimetableContext;
26
29
  private fetchSchedule;
27
30
  /**
28
31
  * Get schedule for all periods. The returned Schedule automatically
package/dist/tt/client.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { HttpClient } from "../common/http.js";
2
2
  import { HybridCache } from "../common/cache.js";
3
- import { AuthError } from "../common/types.js";
4
- import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, parseWebinars, } from "./parse/index.js";
3
+ import { AuthError, ParseError } from "../common/types.js";
4
+ import { parseAcademicYearFromPage, parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, parsePeriodFromPage, parseWebinars, } from "./parse/index.js";
5
5
  import { Schedule } from "./schedule.js";
6
6
  const BASE = "https://tt.chuvsu.ru";
7
7
  const AUTH_URL = `${BASE}/auth`;
8
+ const TIMETABLE_CONTEXT_TTL_MS = 15 * 60 * 1000;
8
9
  const ALL_PERIODS = [
9
10
  1 /* Period.FallSemester */,
10
11
  2 /* Period.WinterSession */,
@@ -32,17 +33,13 @@ function formatDate(date) {
32
33
  const day = String(date.getDate()).padStart(2, "0");
33
34
  return `${year}-${month}-${day}`;
34
35
  }
35
- function getAcademicYearKey(date = new Date()) {
36
- const year = date.getFullYear();
37
- const start = date.getMonth() >= 8 ? year : year - 1;
38
- return `${start}-${start + 1}`;
39
- }
40
36
  export class TtClient {
41
37
  http = new HttpClient();
42
38
  educationType;
43
39
  cache;
44
40
  blobAdapter = undefined;
45
41
  loginMode = null;
42
+ timetableContext = null;
46
43
  constructor(opts) {
47
44
  this.educationType = opts?.educationType ?? 1 /* EducationType.HigherEducation */;
48
45
  this.blobAdapter = opts?.blobAdapter;
@@ -83,6 +80,7 @@ export class TtClient {
83
80
  throw new AuthError("TT login failed");
84
81
  }
85
82
  this.loginMode = { type: "credentials", ...opts };
83
+ this.timetableContext = null;
86
84
  }
87
85
  async loginAsGuest() {
88
86
  const res = await this.http.post(AUTH_URL, { guest: "Войти гостем", hfac: "0", pertt: this.pertt }, false);
@@ -90,6 +88,7 @@ export class TtClient {
90
88
  throw new AuthError("TT guest login failed");
91
89
  }
92
90
  this.loginMode = { type: "guest" };
91
+ this.timetableContext = null;
93
92
  }
94
93
  isSessionExpired(body) {
95
94
  return body.includes('name="wname"');
@@ -123,9 +122,32 @@ export class TtClient {
123
122
  }
124
123
  return res;
125
124
  }
125
+ /** Resolve the academic year and active period from tt.chuvsu.ru itself. */
126
+ async getTimetableContext(pageUrl) {
127
+ if (this.timetableContext &&
128
+ Date.now() - this.timetableContext.resolvedAt < TIMETABLE_CONTEXT_TTL_MS) {
129
+ return this.timetableContext;
130
+ }
131
+ const { body } = await this.authGet(pageUrl);
132
+ const academicYearStartYear = parseAcademicYearFromPage(body);
133
+ if (academicYearStartYear == null) {
134
+ throw new ParseError("TT page does not expose the current academic year");
135
+ }
136
+ const period = parsePeriodFromPage(body);
137
+ if (period == null) {
138
+ throw new ParseError("TT page does not expose the active timetable period");
139
+ }
140
+ const context = {
141
+ academicYearStartYear,
142
+ period,
143
+ resolvedAt: Date.now(),
144
+ };
145
+ this.timetableContext = context;
146
+ return context;
147
+ }
126
148
  // --- Schedule ---
127
- async fetchSchedule(groupId, period) {
128
- const cacheKey = `${groupId}:${period}:${getAcademicYearKey()}`;
149
+ async fetchSchedule(groupId, period, academicYearStartYear) {
150
+ const cacheKey = `${groupId}:${period}:${academicYearStartYear}-${academicYearStartYear + 1}`;
129
151
  const cached = await this.cache?.get("schedule", cacheKey);
130
152
  if (cached)
131
153
  return cached;
@@ -141,23 +163,25 @@ export class TtClient {
141
163
  */
142
164
  async getSchedule(groupId) {
143
165
  const schedules = new Map();
166
+ const context = await this.getTimetableContext(`${BASE}/index/grouptt/gr/${groupId}`);
144
167
  const results = await Promise.all(ALL_PERIODS.map(async (period) => {
145
- const days = await this.fetchSchedule(groupId, period);
168
+ const days = await this.fetchSchedule(groupId, period, context.academicYearStartYear);
146
169
  return { period, days };
147
170
  }));
148
171
  for (const { period, days } of results) {
149
172
  schedules.set(period, days);
150
173
  }
151
- return new Schedule(groupId, schedules, undefined, this.educationType);
174
+ return new Schedule(groupId, schedules, context.period, this.educationType, undefined, undefined, undefined, context.academicYearStartYear);
152
175
  }
153
176
  /**
154
177
  * Get schedule for a specific period only.
155
178
  */
156
179
  async getScheduleForPeriod(opts) {
157
- const days = await this.fetchSchedule(opts.groupId, opts.period);
180
+ const context = await this.getTimetableContext(`${BASE}/index/grouptt/gr/${opts.groupId}`);
181
+ const days = await this.fetchSchedule(opts.groupId, opts.period, context.academicYearStartYear);
158
182
  const schedules = new Map();
159
183
  schedules.set(opts.period, days);
160
- return new Schedule(opts.groupId, schedules, opts.period, this.educationType);
184
+ return new Schedule(opts.groupId, schedules, opts.period, this.educationType, undefined, undefined, undefined, context.academicYearStartYear);
161
185
  }
162
186
  async getWebinars(opts) {
163
187
  const date = opts?.date ?? new Date();
@@ -317,8 +341,8 @@ export class TtClient {
317
341
  await this.cache?.set("audienceInfo", String(audienceId), info);
318
342
  return info;
319
343
  }
320
- async fetchAudienceSchedule(audienceId, period) {
321
- const cacheKey = `audience:${audienceId}:${period}`;
344
+ async fetchAudienceSchedule(audienceId, period, academicYearStartYear) {
345
+ const cacheKey = `audience:${audienceId}:${period}:${academicYearStartYear}-${academicYearStartYear + 1}`;
322
346
  const cached = await this.cache?.get("schedule", cacheKey);
323
347
  if (cached)
324
348
  return cached;
@@ -336,20 +360,22 @@ export class TtClient {
336
360
  }
337
361
  async getAudienceSchedule(audienceId) {
338
362
  const schedules = new Map();
363
+ const context = await this.getTimetableContext(`${BASE}/index/audtt/aud/${audienceId}`);
339
364
  const results = await Promise.all(ALL_PERIODS.map(async (period) => {
340
- const days = await this.fetchAudienceSchedule(audienceId, period);
365
+ const days = await this.fetchAudienceSchedule(audienceId, period, context.academicYearStartYear);
341
366
  return { period, days };
342
367
  }));
343
368
  for (const { period, days } of results) {
344
369
  schedules.set(period, days);
345
370
  }
346
- return new Schedule(audienceId, schedules, undefined, this.educationType);
371
+ return new Schedule(audienceId, schedules, context.period, this.educationType, undefined, undefined, undefined, context.academicYearStartYear);
347
372
  }
348
373
  async getAudienceScheduleForPeriod(opts) {
349
- const days = await this.fetchAudienceSchedule(opts.audienceId, opts.period);
374
+ const context = await this.getTimetableContext(`${BASE}/index/audtt/aud/${opts.audienceId}`);
375
+ const days = await this.fetchAudienceSchedule(opts.audienceId, opts.period, context.academicYearStartYear);
350
376
  const schedules = new Map();
351
377
  schedules.set(opts.period, days);
352
- return new Schedule(opts.audienceId, schedules, opts.period, this.educationType);
378
+ return new Schedule(opts.audienceId, schedules, opts.period, this.educationType, undefined, undefined, undefined, context.academicYearStartYear);
353
379
  }
354
380
  async getCachedAudienceImage(cacheKey, fetchUrl) {
355
381
  const cached = this.cache?.getLocal("audienceImages", cacheKey) ??
@@ -433,8 +459,8 @@ export class TtClient {
433
459
  await this.cache?.set("teachers", "all", data);
434
460
  return data;
435
461
  }
436
- async fetchTeacherSchedule(teacherId, period) {
437
- const cacheKey = `teacher:${teacherId}:${period}`;
462
+ async fetchTeacherSchedule(teacherId, period, academicYearStartYear) {
463
+ const cacheKey = `teacher:${teacherId}:${period}:${academicYearStartYear}-${academicYearStartYear + 1}`;
438
464
  const cached = await this.cache?.get("schedule", cacheKey);
439
465
  if (cached)
440
466
  return cached;
@@ -452,20 +478,22 @@ export class TtClient {
452
478
  }
453
479
  async getTeacherSchedule(teacherId) {
454
480
  const schedules = new Map();
481
+ const context = await this.getTimetableContext(`${BASE}/index/techtt/tech/${teacherId}`);
455
482
  const results = await Promise.all(ALL_PERIODS.map(async (period) => {
456
- const days = await this.fetchTeacherSchedule(teacherId, period);
483
+ const days = await this.fetchTeacherSchedule(teacherId, period, context.academicYearStartYear);
457
484
  return { period, days };
458
485
  }));
459
486
  for (const { period, days } of results) {
460
487
  schedules.set(period, days);
461
488
  }
462
- return new Schedule(teacherId, schedules, undefined, this.educationType, undefined, undefined, true);
489
+ return new Schedule(teacherId, schedules, context.period, this.educationType, undefined, undefined, true, context.academicYearStartYear);
463
490
  }
464
491
  async getTeacherScheduleForPeriod(opts) {
465
- const days = await this.fetchTeacherSchedule(opts.teacherId, opts.period);
492
+ const context = await this.getTimetableContext(`${BASE}/index/techtt/tech/${opts.teacherId}`);
493
+ const days = await this.fetchTeacherSchedule(opts.teacherId, opts.period, context.academicYearStartYear);
466
494
  const schedules = new Map();
467
495
  schedules.set(opts.period, days);
468
- return new Schedule(opts.teacherId, schedules, opts.period, this.educationType, undefined, undefined, true);
496
+ return new Schedule(opts.teacherId, schedules, opts.period, this.educationType, undefined, undefined, true, context.academicYearStartYear);
469
497
  }
470
498
  async getTeacherInfo(teacherId) {
471
499
  const cached = await this.cache?.get("teacherInfo", String(teacherId));
@@ -1,5 +1,5 @@
1
1
  export { parseGroupsString } from "./groups.js";
2
- export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGroupButtons, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
2
+ export { parseAcademicYearFromPage, parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGroupButtons, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
3
3
  export { parseFullSchedule } from "./full-schedule.js";
4
4
  export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
5
5
  export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
@@ -1,5 +1,5 @@
1
1
  export { parseGroupsString } from "./groups.js";
2
- export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGroupButtons, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
2
+ export { parseAcademicYearFromPage, parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGroupButtons, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
3
3
  export { parseFullSchedule } from "./full-schedule.js";
4
4
  export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
5
5
  export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
@@ -1,6 +1,8 @@
1
1
  import { Period } from "../../common/types.js";
2
2
  import type { Audience, Faculty, Group } from "../types.js";
3
3
  export declare function parsePeriodFromPage(html: string): Period | null;
4
+ /** Parse the start year from labels like "2026/2027 учебный год". */
5
+ export declare function parseAcademicYearFromPage(html: string): number | null;
4
6
  export declare function parseGroupButtons(html: string): Group[];
5
7
  export declare function parseFacultyButtons(html: string): Faculty[];
6
8
  export declare function parseAudienceButtons(html: string): Audience[];
@@ -6,12 +6,34 @@ const PERIOD_LABELS = {
6
6
  "летняя сессия": 4,
7
7
  };
8
8
  export function parsePeriodFromPage(html) {
9
+ const doc = parseHtml(html);
10
+ // Schedule pages expose the active period as the checked radio button.
11
+ const checked = doc.querySelector('input[name="pertype"][checked]');
12
+ const checkedValue = Number(checked?.getAttribute("value"));
13
+ if (checkedValue >= 1 && checkedValue <= 4) {
14
+ return checkedValue;
15
+ }
16
+ // Some pages keep the selected period only in the hidden form field.
17
+ const hiddenValue = Number(doc.querySelector('#htype')?.getAttribute("value"));
18
+ if (hiddenValue >= 1 && hiddenValue <= 4) {
19
+ return hiddenValue;
20
+ }
21
+ // Legacy markup used an italic textual marker.
9
22
  const match = html.match(/идет\s+(.+?)\s*</i);
10
23
  if (!match)
11
24
  return null;
12
25
  const label = match[1].toLowerCase().trim();
13
26
  return PERIOD_LABELS[label] ?? null;
14
27
  }
28
+ /** Parse the start year from labels like "2026/2027 учебный год". */
29
+ export function parseAcademicYearFromPage(html) {
30
+ const match = html.match(/(\d{4})\s*[\/-]\s*(\d{4})\s*учебный\s+год/i);
31
+ if (!match)
32
+ return null;
33
+ const start = Number(match[1]);
34
+ const end = Number(match[2]);
35
+ return end === start + 1 ? start : null;
36
+ }
15
37
  export function parseGroupButtons(html) {
16
38
  const doc = parseHtml(html);
17
39
  const groups = [];
@@ -11,8 +11,10 @@ export declare class Schedule {
11
11
  readonly holidays: Holiday[];
12
12
  /** Government decree day-off transfers (Постановление Правительства). */
13
13
  readonly holidayTransfers: HolidayTransfer[];
14
+ /** Academic year represented by this schedule (e.g. 2026 for 2026/2027). */
15
+ readonly academicYearStartYear: number;
14
16
  private _period?;
15
- constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null, holidayTransfers?: HolidayTransfer[], isTeacherSchedule?: boolean);
17
+ constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null, holidayTransfers?: HolidayTransfer[], isTeacherSchedule?: boolean, academicYearStartYear?: number);
16
18
  /** Current (or fixed) period for this schedule. */
17
19
  get period(): Period;
18
20
  /** Days for the current period. */
@@ -21,6 +23,7 @@ export declare class Schedule {
21
23
  get periods(): Period[];
22
24
  /** Get days for a specific period. */
23
25
  getDays(period: Period): FullScheduleDay[];
26
+ private getSemesterYear;
24
27
  private getSlotsForWeekday;
25
28
  private getDateForWeekday;
26
29
  private isInCurrentAcademicYear;
@@ -9,8 +9,10 @@ export class Schedule {
9
9
  holidays;
10
10
  /** Government decree day-off transfers (Постановление Правительства). */
11
11
  holidayTransfers;
12
+ /** Academic year represented by this schedule (e.g. 2026 for 2026/2027). */
13
+ academicYearStartYear;
12
14
  _period;
13
- constructor(groupId, scheduleMap, period, educationType, holidays, holidayTransfers, isTeacherSchedule) {
15
+ constructor(groupId, scheduleMap, period, educationType, holidays, holidayTransfers, isTeacherSchedule, academicYearStartYear) {
14
16
  this.groupId = groupId;
15
17
  this.scheduleMap = scheduleMap;
16
18
  this.educationType = educationType ?? 1 /* EducationType.HigherEducation */;
@@ -18,6 +20,14 @@ export class Schedule {
18
20
  this._period = period;
19
21
  this.holidays = holidays ?? RUSSIAN_HOLIDAYS;
20
22
  this.holidayTransfers = holidayTransfers ?? [];
23
+ if (academicYearStartYear != null) {
24
+ this.academicYearStartYear = academicYearStartYear;
25
+ }
26
+ else {
27
+ const now = new Date();
28
+ this.academicYearStartYear =
29
+ now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
30
+ }
21
31
  }
22
32
  /** Current (or fixed) period for this schedule. */
23
33
  get period() {
@@ -35,6 +45,14 @@ export class Schedule {
35
45
  getDays(period) {
36
46
  return this.scheduleMap.get(period) ?? [];
37
47
  }
48
+ getSemesterYear(period) {
49
+ const semester = isSessionPeriod(period)
50
+ ? getAdjacentSemester(period)
51
+ : period;
52
+ return semester === 1 /* Period.FallSemester */
53
+ ? this.academicYearStartYear
54
+ : this.academicYearStartYear + 1;
55
+ }
38
56
  // --- Semester helpers (weekday-based) ---
39
57
  getSlotsForWeekday(weekday, days, opts) {
40
58
  const dayName = getWeekdayName(weekday);
@@ -45,7 +63,7 @@ export class Schedule {
45
63
  }
46
64
  getDateForWeekday(weekday, period, week) {
47
65
  if (week != null) {
48
- const startMonday = getMonday(getSemesterStart({ period }));
66
+ const startMonday = getMonday(getSemesterStart({ period, year: this.getSemesterYear(period) }));
49
67
  const date = new Date(startMonday);
50
68
  date.setDate(startMonday.getDate() +
51
69
  (week - 1) * 7 +
@@ -61,10 +79,7 @@ export class Schedule {
61
79
  return date;
62
80
  }
63
81
  isInCurrentAcademicYear(date) {
64
- const now = new Date();
65
- const startYear = now.getMonth() >= 8
66
- ? now.getFullYear()
67
- : now.getFullYear() - 1;
82
+ const startYear = this.academicYearStartYear;
68
83
  const start = new Date(startYear, 8, 1);
69
84
  const end = new Date(startYear + 1, 7, 31, 23, 59, 59, 999);
70
85
  return date >= start && date <= end;
@@ -115,8 +130,12 @@ export class Schedule {
115
130
  : period;
116
131
  const semesterDays = this.getDays(semesterPeriod);
117
132
  if (semesterDays.length > 0) {
118
- const week = getWeekNumber({ period: semesterPeriod, date });
119
- if (week >= 0 && week <= 17) {
133
+ const week = getWeekNumber({
134
+ period: semesterPeriod,
135
+ year: this.getSemesterYear(semesterPeriod),
136
+ date,
137
+ });
138
+ if (week >= 1 && week <= 17) {
120
139
  const weekday = date.getDay();
121
140
  const slots = this.getSlotsForWeekday(weekday, semesterDays, {
122
141
  subgroup: opts?.subgroup,
@@ -140,6 +159,7 @@ export class Schedule {
140
159
  if (week != null && !isSessionPeriod(period)) {
141
160
  const semesterWeeks = getSemesterWeeks({
142
161
  period,
162
+ year: this.getSemesterYear(period),
143
163
  weekCount: week,
144
164
  });
145
165
  const weekData = semesterWeeks.find((w) => w.week === week);
@@ -182,12 +202,32 @@ export class Schedule {
182
202
  return null;
183
203
  }
184
204
  getWeekNumber(date) {
185
- return getWeekNumber({ period: this.period, date });
205
+ const period = isSessionPeriod(this.period)
206
+ ? getAdjacentSemester(this.period)
207
+ : this.period;
208
+ return getWeekNumber({
209
+ period,
210
+ year: this.getSemesterYear(period),
211
+ date,
212
+ });
186
213
  }
187
214
  getSemesterWeeks(weekCount) {
188
- return getSemesterWeeks({ period: this.period, weekCount });
215
+ const period = isSessionPeriod(this.period)
216
+ ? getAdjacentSemester(this.period)
217
+ : this.period;
218
+ return getSemesterWeeks({
219
+ period,
220
+ year: this.getSemesterYear(period),
221
+ weekCount,
222
+ });
189
223
  }
190
224
  getSemesterStart() {
191
- return getSemesterStart({ period: this.period });
225
+ const period = isSessionPeriod(this.period)
226
+ ? getAdjacentSemester(this.period)
227
+ : this.period;
228
+ return getSemesterStart({
229
+ period,
230
+ year: this.getSemesterYear(period),
231
+ });
192
232
  }
193
233
  }
@@ -14,7 +14,7 @@ export declare function getSemesterStart(opts: {
14
14
  }): Date;
15
15
  /**
16
16
  * All weeks in a semester with their start/end dates.
17
- * Week 0 starts from the semester start date.
17
+ * Week 1 is the calendar week containing the semester start date.
18
18
  */
19
19
  export declare function getSemesterWeeks(opts: {
20
20
  period: Period;
@@ -36,16 +36,16 @@ export function getSemesterStart(opts) {
36
36
  }
37
37
  /**
38
38
  * All weeks in a semester with their start/end dates.
39
- * Week 0 starts from the semester start date.
39
+ * Week 1 is the calendar week containing the semester start date.
40
40
  */
41
41
  export function getSemesterWeeks(opts) {
42
42
  const weekCount = opts.weekCount ?? 17;
43
43
  const semesterStart = getSemesterStart(opts);
44
44
  const startMonday = getMonday(semesterStart);
45
45
  const weeks = [];
46
- for (let i = 0; i <= weekCount; i++) {
46
+ for (let i = 1; i <= weekCount; i++) {
47
47
  const start = new Date(startMonday);
48
- start.setDate(startMonday.getDate() + i * 7);
48
+ start.setDate(startMonday.getDate() + (i - 1) * 7);
49
49
  const end = new Date(start);
50
50
  end.setDate(start.getDate() + 6);
51
51
  end.setHours(23, 59, 59, 999);
@@ -60,5 +60,5 @@ export function getWeekNumber(opts) {
60
60
  const startMonday = getMonday(semesterStart);
61
61
  const targetMonday = getMonday(date);
62
62
  const diff = targetMonday.getTime() - startMonday.getTime();
63
- return Math.floor(diff / (7 * 24 * 60 * 60 * 1000));
63
+ return Math.floor(diff / (7 * 24 * 60 * 60 * 1000)) + 1;
64
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "4.1.2",
3
+ "version": "4.1.5",
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",
@@ -43,6 +43,7 @@
43
43
  "scripts": {
44
44
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
45
45
  "build": "pnpm clean && tsc",
46
- "test": "pnpm build && node --test"
46
+ "test": "pnpm build && node --test",
47
+ "test:live:schedules": "pnpm build && node --env-file=.env examples/testSchedules.mjs"
47
48
  }
48
49
  }