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 +12 -0
- package/dist/tt/client.d.ts +3 -0
- package/dist/tt/client.js +53 -25
- package/dist/tt/parse/index.d.ts +1 -1
- package/dist/tt/parse/index.js +1 -1
- package/dist/tt/parse/lists.d.ts +2 -0
- package/dist/tt/parse/lists.js +22 -0
- package/dist/tt/schedule.d.ts +4 -1
- package/dist/tt/schedule.js +51 -11
- package/dist/tt/utils/semester.d.ts +1 -1
- package/dist/tt/utils/semester.js +4 -4
- package/package.json +3 -2
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
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -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}:${
|
|
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,
|
|
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
|
|
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,
|
|
371
|
+
return new Schedule(audienceId, schedules, context.period, this.educationType, undefined, undefined, undefined, context.academicYearStartYear);
|
|
347
372
|
}
|
|
348
373
|
async getAudienceScheduleForPeriod(opts) {
|
|
349
|
-
const
|
|
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,
|
|
489
|
+
return new Schedule(teacherId, schedules, context.period, this.educationType, undefined, undefined, true, context.academicYearStartYear);
|
|
463
490
|
}
|
|
464
491
|
async getTeacherScheduleForPeriod(opts) {
|
|
465
|
-
const
|
|
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));
|
package/dist/tt/parse/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/tt/parse/index.js
CHANGED
|
@@ -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";
|
package/dist/tt/parse/lists.d.ts
CHANGED
|
@@ -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[];
|
package/dist/tt/parse/lists.js
CHANGED
|
@@ -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 = [];
|
package/dist/tt/schedule.d.ts
CHANGED
|
@@ -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;
|
package/dist/tt/schedule.js
CHANGED
|
@@ -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
|
|
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({
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
}
|