chuvsu-js 3.0.3 → 4.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.
- package/README.md +14 -2
- package/dist/common/cache.d.ts +30 -0
- package/dist/common/cache.js +52 -0
- package/dist/lk/client.d.ts +4 -1
- package/dist/lk/client.js +64 -3
- package/dist/lk/types.d.ts +11 -0
- package/dist/shared.d.ts +2 -2
- package/dist/tt/client.d.ts +2 -1
- package/dist/tt/client.js +174 -53
- package/dist/tt/parse/audience.js +7 -6
- package/dist/tt/parse/full-schedule.js +5 -4
- package/dist/tt/parse/groups.js +2 -1
- package/dist/tt/parse/overlays.js +26 -13
- package/dist/tt/parse/patterns.d.ts +11 -0
- package/dist/tt/parse/patterns.js +11 -0
- package/dist/tt/parse/teacher.js +6 -5
- package/dist/tt/types.d.ts +5 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ schedule.currentLesson();
|
|
|
61
61
|
```ts
|
|
62
62
|
import { LkClient } from "chuvsu-js";
|
|
63
63
|
|
|
64
|
-
const lk = new LkClient();
|
|
64
|
+
const lk = new LkClient({ cache: 60_000 });
|
|
65
65
|
await lk.login({ email: "student@mail.ru", password: "password" });
|
|
66
66
|
|
|
67
67
|
const data = await lk.getPersonalData();
|
|
@@ -87,6 +87,8 @@ new TtClient(options?: TtClientOptions)
|
|
|
87
87
|
| --------------- | ----------------------- | ----------------- | -------------------------------------------------------------- |
|
|
88
88
|
| `educationType` | `EducationType` | `HigherEducation` | Тип образования: высшее (1) или СПО (2) |
|
|
89
89
|
| `cache` | `number \| CacheConfig` | — | TTL кеша в мс. Число задаёт единый TTL, объект — по категориям |
|
|
90
|
+
| `cacheAdapter` | `CacheAdapter` | — | Внешний L2-кеш для JSON-данных (например Redis/БД) |
|
|
91
|
+
| `blobAdapter` | `BlobAdapter` | — | Хранилище бинарных данных, например S3/R2/MinIO |
|
|
90
92
|
|
|
91
93
|
#### Авторизация
|
|
92
94
|
|
|
@@ -141,7 +143,13 @@ const data = tt.exportCache();
|
|
|
141
143
|
tt.importCache(data);
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
Категории кеша: `schedule`, `faculties`, `groups`.
|
|
146
|
+
Категории кеша: `schedule`, `faculties`, `groups`, `audiences`, `audienceNames`, `teachers`, `teacherInfo`, `teacherPhotos`, `audienceInfo`, `audienceImages`.
|
|
147
|
+
|
|
148
|
+
Если передать `cacheAdapter`, библиотека использует двухуровневый кеш:
|
|
149
|
+
- L1: in-memory кеш внутри процесса
|
|
150
|
+
- L2: внешний адаптер (`cacheAdapter`)
|
|
151
|
+
|
|
152
|
+
Если передать `blobAdapter`, фото преподавателей, аудиторий, корпусов и планов этажей будут храниться во внешнем blob/object storage, а во внешнем JSON-кеше сохранятся только метаданные с ключом blob-объекта.
|
|
145
153
|
|
|
146
154
|
### Schedule
|
|
147
155
|
|
|
@@ -224,11 +232,15 @@ getWeekNumber({ period: Period.SpringSemester });
|
|
|
224
232
|
Клиент для личного кабинета (`lk.chuvsu.ru`).
|
|
225
233
|
|
|
226
234
|
```ts
|
|
235
|
+
const lk = new LkClient({ cache?: number | LkCacheConfig });
|
|
227
236
|
await lk.login({ email, password });
|
|
228
237
|
const data = await lk.getPersonalData();
|
|
229
238
|
const groupId = await lk.getGroupId();
|
|
230
239
|
```
|
|
231
240
|
|
|
241
|
+
`cache` работает так же, как в `TtClient`: число задаёт единый TTL в мс, объект — TTL по категориям (`personalData`, `photo`, `groupId`).
|
|
242
|
+
Также поддерживаются `cacheAdapter` и `blobAdapter`.
|
|
243
|
+
|
|
232
244
|
**PersonalData** содержит: `lastName`, `firstName`, `patronymic`, `sex`, `birthday`, `recordBookNumber`, `faculty`, `specialty`, `profile`, `group`, `course`, `email`, `phone`.
|
|
233
245
|
|
|
234
246
|
## Типы
|
package/dist/common/cache.d.ts
CHANGED
|
@@ -2,6 +2,21 @@ export interface CacheEntry {
|
|
|
2
2
|
data: unknown;
|
|
3
3
|
timestamp: number;
|
|
4
4
|
}
|
|
5
|
+
export interface CacheAdapter {
|
|
6
|
+
get(category: string, key: string): Promise<unknown | null | undefined>;
|
|
7
|
+
set(category: string, key: string, data: unknown, ttl?: number): Promise<void>;
|
|
8
|
+
clear?(category?: string): Promise<void>;
|
|
9
|
+
delete?(category: string, key: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface BlobPutOptions {
|
|
12
|
+
contentType?: string;
|
|
13
|
+
ttl?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface BlobAdapter {
|
|
16
|
+
get(key: string): Promise<Buffer | null>;
|
|
17
|
+
put(key: string, data: Buffer, opts?: BlobPutOptions): Promise<void>;
|
|
18
|
+
delete?(key: string): Promise<void>;
|
|
19
|
+
}
|
|
5
20
|
export declare class Cache {
|
|
6
21
|
private ttls;
|
|
7
22
|
private store;
|
|
@@ -12,3 +27,18 @@ export declare class Cache {
|
|
|
12
27
|
export(): Record<string, CacheEntry>;
|
|
13
28
|
import(data: Record<string, CacheEntry>): void;
|
|
14
29
|
}
|
|
30
|
+
export declare class HybridCache {
|
|
31
|
+
private memory;
|
|
32
|
+
private ttls;
|
|
33
|
+
private adapter?;
|
|
34
|
+
constructor(ttls: Record<string, number | undefined>, adapter?: CacheAdapter);
|
|
35
|
+
ttl(category: string): number | undefined;
|
|
36
|
+
getLocal(category: string, key: string): unknown | null;
|
|
37
|
+
setLocal(category: string, key: string, data: unknown): void;
|
|
38
|
+
get(category: string, key: string): Promise<unknown | null>;
|
|
39
|
+
set(category: string, key: string, data: unknown): Promise<void>;
|
|
40
|
+
setExternal(category: string, key: string, data: unknown): Promise<void>;
|
|
41
|
+
clear(category?: string): Promise<void>;
|
|
42
|
+
export(): Record<string, CacheEntry>;
|
|
43
|
+
import(data: Record<string, CacheEntry>): void;
|
|
44
|
+
}
|
package/dist/common/cache.js
CHANGED
|
@@ -46,3 +46,55 @@ export class Cache {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
export class HybridCache {
|
|
50
|
+
memory;
|
|
51
|
+
ttls;
|
|
52
|
+
adapter;
|
|
53
|
+
constructor(ttls, adapter) {
|
|
54
|
+
this.ttls = ttls;
|
|
55
|
+
this.memory = new Cache(ttls);
|
|
56
|
+
this.adapter = adapter;
|
|
57
|
+
}
|
|
58
|
+
ttl(category) {
|
|
59
|
+
return this.ttls[category];
|
|
60
|
+
}
|
|
61
|
+
getLocal(category, key) {
|
|
62
|
+
return this.memory.get(category, key);
|
|
63
|
+
}
|
|
64
|
+
setLocal(category, key, data) {
|
|
65
|
+
this.memory.set(category, key, data);
|
|
66
|
+
}
|
|
67
|
+
async get(category, key) {
|
|
68
|
+
const local = this.memory.get(category, key);
|
|
69
|
+
if (local !== null)
|
|
70
|
+
return local;
|
|
71
|
+
const ttl = this.ttls[category];
|
|
72
|
+
if (ttl == null || !this.adapter)
|
|
73
|
+
return null;
|
|
74
|
+
const external = await this.adapter.get(category, key);
|
|
75
|
+
if (external === null || external === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
this.memory.set(category, key, external);
|
|
78
|
+
return external;
|
|
79
|
+
}
|
|
80
|
+
async set(category, key, data) {
|
|
81
|
+
this.memory.set(category, key, data);
|
|
82
|
+
await this.setExternal(category, key, data);
|
|
83
|
+
}
|
|
84
|
+
async setExternal(category, key, data) {
|
|
85
|
+
const ttl = this.ttls[category];
|
|
86
|
+
if (ttl == null || !this.adapter)
|
|
87
|
+
return;
|
|
88
|
+
await this.adapter.set(category, key, data, ttl);
|
|
89
|
+
}
|
|
90
|
+
async clear(category) {
|
|
91
|
+
this.memory.clear(category);
|
|
92
|
+
await this.adapter?.clear?.(category);
|
|
93
|
+
}
|
|
94
|
+
export() {
|
|
95
|
+
return this.memory.export();
|
|
96
|
+
}
|
|
97
|
+
import(data) {
|
|
98
|
+
this.memory.import(data);
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/lk/client.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { PersonalData } from "./types.js";
|
|
1
|
+
import type { LkClientOptions, PersonalData } from "./types.js";
|
|
2
2
|
export declare class LkClient {
|
|
3
3
|
private http;
|
|
4
4
|
private credentials;
|
|
5
|
+
private cache;
|
|
6
|
+
private blobAdapter;
|
|
7
|
+
constructor(opts?: LkClientOptions);
|
|
5
8
|
login(opts: {
|
|
6
9
|
email: string;
|
|
7
10
|
password: string;
|
package/dist/lk/client.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import { HttpClient } from "../common/http.js";
|
|
2
|
+
import { HybridCache } from "../common/cache.js";
|
|
2
3
|
import { AuthError } from "../common/types.js";
|
|
3
4
|
import { extractScriptValues } from "./parse.js";
|
|
4
5
|
const BASE = "https://lk.chuvsu.ru";
|
|
5
6
|
const LOGIN_URL = `${BASE}/info/login.php`;
|
|
6
7
|
const STUDENT_BASE = `${BASE}/student`;
|
|
8
|
+
function makeUniformCacheConfig(ttl) {
|
|
9
|
+
return {
|
|
10
|
+
personalData: ttl,
|
|
11
|
+
photo: ttl,
|
|
12
|
+
groupId: ttl,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
7
15
|
export class LkClient {
|
|
8
16
|
http = new HttpClient();
|
|
9
17
|
credentials = null;
|
|
18
|
+
cache;
|
|
19
|
+
blobAdapter = undefined;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.blobAdapter = opts?.blobAdapter;
|
|
22
|
+
if (opts?.cache == null) {
|
|
23
|
+
this.cache = null;
|
|
24
|
+
}
|
|
25
|
+
else if (typeof opts.cache === "number") {
|
|
26
|
+
this.cache = new HybridCache(makeUniformCacheConfig(opts.cache), opts.cacheAdapter);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.cache = new HybridCache(opts.cache, opts.cacheAdapter);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
10
32
|
async login(opts) {
|
|
11
33
|
const res = await this.http.post(LOGIN_URL, { email: opts.email, password: opts.password, role: "1", enter: "" }, false);
|
|
12
34
|
if (!(res.status === 302 && res.location?.includes("student"))) {
|
|
@@ -26,9 +48,12 @@ export class LkClient {
|
|
|
26
48
|
return res;
|
|
27
49
|
}
|
|
28
50
|
async getPersonalData() {
|
|
51
|
+
const cached = await this.cache?.get("personalData", "self");
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
29
54
|
const { body } = await this.authGet(`${STUDENT_BASE}/personal_data.php`);
|
|
30
55
|
const vals = extractScriptValues(body, "form_personal_data");
|
|
31
|
-
|
|
56
|
+
const data = {
|
|
32
57
|
lastName: vals.fam ?? "",
|
|
33
58
|
firstName: vals.nam ?? "",
|
|
34
59
|
patronymic: vals.oth ?? "",
|
|
@@ -43,13 +68,49 @@ export class LkClient {
|
|
|
43
68
|
email: vals.email ?? "",
|
|
44
69
|
phone: vals.phone ?? "",
|
|
45
70
|
};
|
|
71
|
+
await this.cache?.set("personalData", "self", data);
|
|
72
|
+
return data;
|
|
46
73
|
}
|
|
47
74
|
async getPhoto() {
|
|
48
|
-
|
|
75
|
+
const cached = this.cache?.getLocal("photo", "self") ??
|
|
76
|
+
await this.cache?.get("photo", "self");
|
|
77
|
+
if (cached !== null && cached !== undefined) {
|
|
78
|
+
const entry = cached;
|
|
79
|
+
if (typeof entry === "string")
|
|
80
|
+
return Buffer.from(entry, "base64");
|
|
81
|
+
if (entry.data !== undefined) {
|
|
82
|
+
return entry.data ? Buffer.from(entry.data, "base64") : Buffer.alloc(0);
|
|
83
|
+
}
|
|
84
|
+
if (entry.blobKey && this.blobAdapter) {
|
|
85
|
+
const photo = await this.blobAdapter.get(entry.blobKey);
|
|
86
|
+
if (photo) {
|
|
87
|
+
this.cache?.setLocal("photo", "self", { data: photo.toString("base64") });
|
|
88
|
+
return photo;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const photo = await this.http.getBuffer(`${STUDENT_BASE}/face.php`);
|
|
93
|
+
if (this.blobAdapter) {
|
|
94
|
+
const blobKey = "lk/photo/self";
|
|
95
|
+
this.cache?.setLocal("photo", "self", { data: photo.toString("base64") });
|
|
96
|
+
await this.blobAdapter.put(blobKey, photo, {
|
|
97
|
+
ttl: this.cache?.ttl("photo"),
|
|
98
|
+
});
|
|
99
|
+
await this.cache?.setExternal("photo", "self", { blobKey });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await this.cache?.set("photo", "self", photo.toString("base64"));
|
|
103
|
+
}
|
|
104
|
+
return photo;
|
|
49
105
|
}
|
|
50
106
|
async getGroupId() {
|
|
107
|
+
const cached = await this.cache?.get("groupId", "self");
|
|
108
|
+
if (cached !== null && cached !== undefined)
|
|
109
|
+
return cached;
|
|
51
110
|
const { body } = await this.authGet(`${STUDENT_BASE}/tt.php`);
|
|
52
111
|
const match = body.match(/tt\.chuvsu\.ru\/index\/grouptt\/gr\/(\d+)/);
|
|
53
|
-
|
|
112
|
+
const groupId = match ? parseInt(match[1]) : null;
|
|
113
|
+
await this.cache?.set("groupId", "self", groupId);
|
|
114
|
+
return groupId;
|
|
54
115
|
}
|
|
55
116
|
}
|
package/dist/lk/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlobAdapter, CacheAdapter } from "../common/cache.js";
|
|
1
2
|
export interface PersonalData {
|
|
2
3
|
lastName: string;
|
|
3
4
|
firstName: string;
|
|
@@ -13,3 +14,13 @@ export interface PersonalData {
|
|
|
13
14
|
email: string;
|
|
14
15
|
phone: string;
|
|
15
16
|
}
|
|
17
|
+
export interface LkCacheConfig {
|
|
18
|
+
personalData?: number;
|
|
19
|
+
photo?: number;
|
|
20
|
+
groupId?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface LkClientOptions {
|
|
23
|
+
cache?: number | LkCacheConfig;
|
|
24
|
+
cacheAdapter?: CacheAdapter;
|
|
25
|
+
blobAdapter?: BlobAdapter;
|
|
26
|
+
}
|
package/dist/shared.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffe
|
|
|
4
4
|
export type { Holiday, HolidayTransfer } from "./tt/utils/index.js";
|
|
5
5
|
export { AuthError, EducationType, ParseError, Period, } from "./common/types.js";
|
|
6
6
|
export type { Teacher, Time, WeekRange } from "./common/types.js";
|
|
7
|
-
export type { CacheEntry } from "./common/cache.js";
|
|
7
|
+
export type { BlobAdapter, BlobPutOptions, CacheAdapter, CacheEntry, } from "./common/cache.js";
|
|
8
8
|
export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, } from "./tt/types.js";
|
|
9
|
-
export type { PersonalData } from "./lk/types.js";
|
|
9
|
+
export type { LkCacheConfig, LkClientOptions, PersonalData } from "./lk/types.js";
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -6,10 +6,11 @@ export declare class TtClient {
|
|
|
6
6
|
private http;
|
|
7
7
|
private educationType;
|
|
8
8
|
private cache;
|
|
9
|
+
private blobAdapter;
|
|
9
10
|
private loginMode;
|
|
10
11
|
constructor(opts?: TtClientOptions);
|
|
11
12
|
private get pertt();
|
|
12
|
-
clearCache(category?: keyof CacheConfig): void
|
|
13
|
+
clearCache(category?: keyof CacheConfig): Promise<void>;
|
|
13
14
|
exportCache(): Record<string, CacheEntry>;
|
|
14
15
|
importCache(data: Record<string, CacheEntry>): void;
|
|
15
16
|
login(opts: {
|
package/dist/tt/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpClient } from "../common/http.js";
|
|
2
|
-
import {
|
|
2
|
+
import { HybridCache } from "../common/cache.js";
|
|
3
3
|
import { AuthError } from "../common/types.js";
|
|
4
4
|
import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse/index.js";
|
|
5
5
|
import { Schedule } from "./schedule.js";
|
|
@@ -11,33 +11,45 @@ const ALL_PERIODS = [
|
|
|
11
11
|
3 /* Period.SpringSemester */,
|
|
12
12
|
4 /* Period.SummerSession */,
|
|
13
13
|
];
|
|
14
|
+
function makeUniformCacheConfig(ttl) {
|
|
15
|
+
return {
|
|
16
|
+
schedule: ttl,
|
|
17
|
+
faculties: ttl,
|
|
18
|
+
groups: ttl,
|
|
19
|
+
audiences: ttl,
|
|
20
|
+
audienceNames: ttl,
|
|
21
|
+
teachers: ttl,
|
|
22
|
+
teacherInfo: ttl,
|
|
23
|
+
teacherPhotos: ttl,
|
|
24
|
+
audienceInfo: ttl,
|
|
25
|
+
audienceImages: ttl,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
14
28
|
export class TtClient {
|
|
15
29
|
http = new HttpClient();
|
|
16
30
|
educationType;
|
|
17
31
|
cache;
|
|
32
|
+
blobAdapter = undefined;
|
|
18
33
|
loginMode = null;
|
|
19
34
|
constructor(opts) {
|
|
20
35
|
this.educationType = opts?.educationType ?? 1 /* EducationType.HigherEducation */;
|
|
36
|
+
this.blobAdapter = opts?.blobAdapter;
|
|
21
37
|
if (opts?.cache == null) {
|
|
22
38
|
this.cache = null;
|
|
23
39
|
}
|
|
24
40
|
else if (typeof opts.cache === "number") {
|
|
25
|
-
this.cache = new
|
|
26
|
-
schedule: opts.cache,
|
|
27
|
-
faculties: opts.cache,
|
|
28
|
-
groups: opts.cache,
|
|
29
|
-
});
|
|
41
|
+
this.cache = new HybridCache(makeUniformCacheConfig(opts.cache), opts.cacheAdapter);
|
|
30
42
|
}
|
|
31
43
|
else {
|
|
32
|
-
this.cache = new
|
|
44
|
+
this.cache = new HybridCache(opts.cache, opts.cacheAdapter);
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
get pertt() {
|
|
36
48
|
return String(this.educationType);
|
|
37
49
|
}
|
|
38
50
|
// --- Cache ---
|
|
39
|
-
clearCache(category) {
|
|
40
|
-
this.cache?.clear(category);
|
|
51
|
+
async clearCache(category) {
|
|
52
|
+
await this.cache?.clear(category);
|
|
41
53
|
}
|
|
42
54
|
exportCache() {
|
|
43
55
|
return this.cache?.export() ?? {};
|
|
@@ -102,13 +114,13 @@ export class TtClient {
|
|
|
102
114
|
// --- Schedule ---
|
|
103
115
|
async fetchSchedule(groupId, period) {
|
|
104
116
|
const cacheKey = `${groupId}:${period}`;
|
|
105
|
-
const cached = this.cache?.get("schedule", cacheKey);
|
|
117
|
+
const cached = await this.cache?.get("schedule", cacheKey);
|
|
106
118
|
if (cached)
|
|
107
119
|
return cached;
|
|
108
120
|
const url = `${BASE}/index/grouptt/gr/${groupId}`;
|
|
109
121
|
const { body } = await this.authPost(url, { htype: String(period) });
|
|
110
122
|
const days = parseFullSchedule(body, this.educationType);
|
|
111
|
-
this.cache?.set("schedule", cacheKey, days);
|
|
123
|
+
await this.cache?.set("schedule", cacheKey, days);
|
|
112
124
|
return days;
|
|
113
125
|
}
|
|
114
126
|
/**
|
|
@@ -137,17 +149,17 @@ export class TtClient {
|
|
|
137
149
|
}
|
|
138
150
|
// --- Search / Discovery ---
|
|
139
151
|
async getFaculties() {
|
|
140
|
-
const cached = this.cache?.get("faculties", "all");
|
|
152
|
+
const cached = await this.cache?.get("faculties", "all");
|
|
141
153
|
if (cached)
|
|
142
154
|
return cached;
|
|
143
155
|
const { body } = await this.authGet(`${BASE}/`);
|
|
144
156
|
const data = parseFacultyButtons(body);
|
|
145
|
-
this.cache?.set("faculties", "all", data);
|
|
157
|
+
await this.cache?.set("faculties", "all", data);
|
|
146
158
|
return data;
|
|
147
159
|
}
|
|
148
160
|
async getGroupsForFaculty(opts) {
|
|
149
161
|
const cacheKey = String(opts.facultyId);
|
|
150
|
-
const cached = this.cache?.get("groups", cacheKey);
|
|
162
|
+
const cached = await this.cache?.get("groups", cacheKey);
|
|
151
163
|
if (cached)
|
|
152
164
|
return cached;
|
|
153
165
|
const { body } = await this.authPost(`${BASE}/`, {
|
|
@@ -155,30 +167,42 @@ export class TtClient {
|
|
|
155
167
|
pertt: this.pertt,
|
|
156
168
|
});
|
|
157
169
|
const data = parseGroupButtons(body);
|
|
158
|
-
this.cache?.set("groups", cacheKey, data);
|
|
170
|
+
await this.cache?.set("groups", cacheKey, data);
|
|
159
171
|
return data;
|
|
160
172
|
}
|
|
161
173
|
async searchGroup(opts) {
|
|
174
|
+
const cacheKey = `search:${opts.name}:${this.pertt}`;
|
|
175
|
+
const cached = await this.cache?.get("groups", cacheKey);
|
|
176
|
+
if (cached)
|
|
177
|
+
return cached;
|
|
162
178
|
const { body } = await this.authPost(`${BASE}/`, {
|
|
163
179
|
grname: opts.name,
|
|
164
180
|
findgr: "найти",
|
|
165
181
|
hfac: "0",
|
|
166
182
|
pertt: this.pertt,
|
|
167
183
|
});
|
|
168
|
-
|
|
184
|
+
const data = parseGroupButtons(body);
|
|
185
|
+
await this.cache?.set("groups", cacheKey, data);
|
|
186
|
+
return data;
|
|
169
187
|
}
|
|
170
188
|
/**
|
|
171
189
|
* Search audiences by name (substring match). The server requires at
|
|
172
190
|
* least 3 characters in the query.
|
|
173
191
|
*/
|
|
174
192
|
async searchAudience(opts) {
|
|
193
|
+
const cacheKey = `search:${opts.name}:${this.pertt}`;
|
|
194
|
+
const cached = await this.cache?.get("audiences", cacheKey);
|
|
195
|
+
if (cached)
|
|
196
|
+
return cached;
|
|
175
197
|
const { body } = await this.authPost(`${BASE}/`, {
|
|
176
198
|
audname: opts.name,
|
|
177
199
|
findaud: "найти",
|
|
178
200
|
hfac: "0",
|
|
179
201
|
pertt: this.pertt,
|
|
180
202
|
});
|
|
181
|
-
|
|
203
|
+
const data = parseAudienceButtons(body);
|
|
204
|
+
await this.cache?.set("audiences", cacheKey, data);
|
|
205
|
+
return data;
|
|
182
206
|
}
|
|
183
207
|
/**
|
|
184
208
|
* Get every audience known to the system in a single request.
|
|
@@ -189,13 +213,19 @@ export class TtClient {
|
|
|
189
213
|
* the full list of (id, name) pairs.
|
|
190
214
|
*/
|
|
191
215
|
async getAudiences() {
|
|
216
|
+
const cacheKey = `all:${this.pertt}`;
|
|
217
|
+
const cached = await this.cache?.get("audiences", cacheKey);
|
|
218
|
+
if (cached)
|
|
219
|
+
return cached;
|
|
192
220
|
const { body } = await this.authPost(`${BASE}/`, {
|
|
193
221
|
audname: "%%%",
|
|
194
222
|
findaud: "найти",
|
|
195
223
|
hfac: "0",
|
|
196
224
|
pertt: this.pertt,
|
|
197
225
|
});
|
|
198
|
-
|
|
226
|
+
const data = parseAudienceButtons(body);
|
|
227
|
+
await this.cache?.set("audiences", cacheKey, data);
|
|
228
|
+
return data;
|
|
199
229
|
}
|
|
200
230
|
/**
|
|
201
231
|
* Resolve an audience id from its exact name by searching and
|
|
@@ -208,37 +238,50 @@ export class TtClient {
|
|
|
208
238
|
}
|
|
209
239
|
/** Fetch the audience's display name from its schedule page. */
|
|
210
240
|
async getAudienceName(audienceId) {
|
|
241
|
+
const cacheKey = String(audienceId);
|
|
242
|
+
const cached = await this.cache?.get("audienceNames", cacheKey);
|
|
243
|
+
if (cached !== null && cached !== undefined)
|
|
244
|
+
return cached;
|
|
245
|
+
const cachedInfo = await this.cache?.get("audienceInfo", cacheKey);
|
|
246
|
+
if (cachedInfo) {
|
|
247
|
+
return cachedInfo.name ?? null;
|
|
248
|
+
}
|
|
211
249
|
const { body } = await this.authGet(`${BASE}/index/audtt/aud/${audienceId}`);
|
|
212
|
-
|
|
250
|
+
const name = parseAudienceName(body);
|
|
251
|
+
const info = parseAudienceInfo(body);
|
|
252
|
+
await this.cache?.set("audienceNames", cacheKey, name);
|
|
253
|
+
if (info)
|
|
254
|
+
await this.cache?.set("audienceInfo", cacheKey, info);
|
|
255
|
+
return name;
|
|
213
256
|
}
|
|
214
257
|
/**
|
|
215
258
|
* Fetch detailed info about an audience (building, floor, usage,
|
|
216
259
|
* image URLs for the audience photo, building photo and floor plan).
|
|
217
260
|
*/
|
|
218
261
|
async getAudienceInfo(audienceId) {
|
|
219
|
-
const cached = this.cache?.get("audienceInfo", String(audienceId));
|
|
262
|
+
const cached = await this.cache?.get("audienceInfo", String(audienceId));
|
|
220
263
|
if (cached)
|
|
221
264
|
return cached;
|
|
222
265
|
const { body } = await this.authGet(`${BASE}/index/audtt/aud/${audienceId}`);
|
|
223
266
|
const info = parseAudienceInfo(body);
|
|
224
267
|
if (info)
|
|
225
|
-
this.cache?.set("audienceInfo", String(audienceId), info);
|
|
268
|
+
await this.cache?.set("audienceInfo", String(audienceId), info);
|
|
226
269
|
return info;
|
|
227
270
|
}
|
|
228
271
|
async fetchAudienceSchedule(audienceId, period) {
|
|
229
272
|
const cacheKey = `audience:${audienceId}:${period}`;
|
|
230
|
-
const cached = this.cache?.get("schedule", cacheKey);
|
|
273
|
+
const cached = await this.cache?.get("schedule", cacheKey);
|
|
231
274
|
if (cached)
|
|
232
275
|
return cached;
|
|
233
276
|
const url = `${BASE}/index/audtt/aud/${audienceId}`;
|
|
234
277
|
const { body } = await this.authPost(url, { htype: String(period) });
|
|
235
278
|
const days = parseAudienceFullSchedule(body);
|
|
236
|
-
this.cache?.set("schedule", cacheKey, days);
|
|
279
|
+
await this.cache?.set("schedule", cacheKey, days);
|
|
237
280
|
// Cache audience info from the same page to avoid an extra request.
|
|
238
|
-
if (!this.cache?.get("audienceInfo", String(audienceId))) {
|
|
281
|
+
if (!(await this.cache?.get("audienceInfo", String(audienceId)))) {
|
|
239
282
|
const info = parseAudienceInfo(body);
|
|
240
283
|
if (info)
|
|
241
|
-
this.cache?.set("audienceInfo", String(audienceId), info);
|
|
284
|
+
await this.cache?.set("audienceInfo", String(audienceId), info);
|
|
242
285
|
}
|
|
243
286
|
return days;
|
|
244
287
|
}
|
|
@@ -260,24 +303,48 @@ export class TtClient {
|
|
|
260
303
|
return new Schedule(opts.audienceId, schedules, opts.period, this.educationType);
|
|
261
304
|
}
|
|
262
305
|
async getCachedAudienceImage(cacheKey, fetchUrl) {
|
|
263
|
-
const cached = this.cache?.
|
|
306
|
+
const cached = this.cache?.getLocal("audienceImages", cacheKey) ??
|
|
307
|
+
await this.cache?.get("audienceImages", cacheKey);
|
|
264
308
|
if (cached !== null && cached !== undefined) {
|
|
265
309
|
const entry = cached;
|
|
266
|
-
|
|
310
|
+
if (entry.data !== undefined) {
|
|
311
|
+
return entry.data ? Buffer.from(entry.data, "base64") : null;
|
|
312
|
+
}
|
|
313
|
+
if (entry.blobKey && this.blobAdapter) {
|
|
314
|
+
const buf = await this.blobAdapter.get(entry.blobKey);
|
|
315
|
+
if (buf) {
|
|
316
|
+
this.cache?.setLocal("audienceImages", cacheKey, {
|
|
317
|
+
data: buf.toString("base64"),
|
|
318
|
+
});
|
|
319
|
+
return buf;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
267
322
|
}
|
|
268
323
|
const url = await fetchUrl();
|
|
269
324
|
if (!url) {
|
|
270
|
-
this.cache?.set("audienceImages", cacheKey, { data: null });
|
|
325
|
+
await this.cache?.set("audienceImages", cacheKey, { data: null });
|
|
271
326
|
return null;
|
|
272
327
|
}
|
|
273
328
|
const buf = await this.authGetBuffer(`${BASE}${url}`);
|
|
274
329
|
if (buf.length === 0) {
|
|
275
|
-
this.cache?.set("audienceImages", cacheKey, { data: null });
|
|
330
|
+
await this.cache?.set("audienceImages", cacheKey, { data: null });
|
|
276
331
|
return null;
|
|
277
332
|
}
|
|
278
|
-
this.
|
|
279
|
-
|
|
280
|
-
|
|
333
|
+
if (this.blobAdapter) {
|
|
334
|
+
const blobKey = `tt/audience-images/${cacheKey}`;
|
|
335
|
+
this.cache?.setLocal("audienceImages", cacheKey, {
|
|
336
|
+
data: buf.toString("base64"),
|
|
337
|
+
});
|
|
338
|
+
await this.blobAdapter.put(blobKey, buf, {
|
|
339
|
+
ttl: this.cache?.ttl("audienceImages"),
|
|
340
|
+
});
|
|
341
|
+
await this.cache?.setExternal("audienceImages", cacheKey, { blobKey });
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
await this.cache?.set("audienceImages", cacheKey, {
|
|
345
|
+
data: buf.toString("base64"),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
281
348
|
return buf;
|
|
282
349
|
}
|
|
283
350
|
/** Get the audience photo (audimage). Returns null if missing. */
|
|
@@ -293,38 +360,44 @@ export class TtClient {
|
|
|
293
360
|
return this.getCachedAudienceImage(`floor:${audienceId}`, async () => (await this.getAudienceInfo(audienceId))?.floorplanUrl);
|
|
294
361
|
}
|
|
295
362
|
async searchTeacher(opts) {
|
|
363
|
+
const cacheKey = `search:${opts.name}:${this.pertt}`;
|
|
364
|
+
const cached = await this.cache?.get("teachers", cacheKey);
|
|
365
|
+
if (cached)
|
|
366
|
+
return cached;
|
|
296
367
|
const { body } = await this.authPost(`${BASE}/`, {
|
|
297
368
|
techname: opts.name,
|
|
298
369
|
findtech: "найти",
|
|
299
370
|
hfac: "0",
|
|
300
371
|
pertt: this.pertt,
|
|
301
372
|
});
|
|
302
|
-
|
|
373
|
+
const data = parseTeacherButtons(body);
|
|
374
|
+
await this.cache?.set("teachers", cacheKey, data);
|
|
375
|
+
return data;
|
|
303
376
|
}
|
|
304
377
|
// --- Teacher schedule ---
|
|
305
378
|
async getTeachers() {
|
|
306
|
-
const cached = this.cache?.get("teachers", "all");
|
|
379
|
+
const cached = await this.cache?.get("teachers", "all");
|
|
307
380
|
if (cached)
|
|
308
381
|
return cached;
|
|
309
382
|
const { body } = await this.authGet(`${BASE}/index/tech`);
|
|
310
383
|
const data = parseTeacherButtons(body);
|
|
311
|
-
this.cache?.set("teachers", "all", data);
|
|
384
|
+
await this.cache?.set("teachers", "all", data);
|
|
312
385
|
return data;
|
|
313
386
|
}
|
|
314
387
|
async fetchTeacherSchedule(teacherId, period) {
|
|
315
388
|
const cacheKey = `teacher:${teacherId}:${period}`;
|
|
316
|
-
const cached = this.cache?.get("schedule", cacheKey);
|
|
389
|
+
const cached = await this.cache?.get("schedule", cacheKey);
|
|
317
390
|
if (cached)
|
|
318
391
|
return cached;
|
|
319
392
|
const url = `${BASE}/index/techtt/tech/${teacherId}`;
|
|
320
393
|
const { body } = await this.authPost(url, { htype: String(period) });
|
|
321
394
|
const days = parseTeacherFullSchedule(body, this.educationType);
|
|
322
|
-
this.cache?.set("schedule", cacheKey, days);
|
|
395
|
+
await this.cache?.set("schedule", cacheKey, days);
|
|
323
396
|
// Cache teacher info from the same page to avoid extra requests
|
|
324
|
-
if (!this.cache?.get("teacherInfo", String(teacherId))) {
|
|
397
|
+
if (!(await this.cache?.get("teacherInfo", String(teacherId)))) {
|
|
325
398
|
const info = parseTeacherInfo(body);
|
|
326
399
|
if (info)
|
|
327
|
-
this.cache?.set("teacherInfo", String(teacherId), info);
|
|
400
|
+
await this.cache?.set("teacherInfo", String(teacherId), info);
|
|
328
401
|
}
|
|
329
402
|
return days;
|
|
330
403
|
}
|
|
@@ -346,14 +419,14 @@ export class TtClient {
|
|
|
346
419
|
return new Schedule(opts.teacherId, schedules, opts.period, this.educationType, undefined, undefined, true);
|
|
347
420
|
}
|
|
348
421
|
async getTeacherInfo(teacherId) {
|
|
349
|
-
const cached = this.cache?.get("teacherInfo", String(teacherId));
|
|
422
|
+
const cached = await this.cache?.get("teacherInfo", String(teacherId));
|
|
350
423
|
if (cached)
|
|
351
424
|
return cached;
|
|
352
425
|
const url = `${BASE}/index/techtt/tech/${teacherId}`;
|
|
353
426
|
const { body } = await this.authGet(url);
|
|
354
427
|
const info = parseTeacherInfo(body);
|
|
355
428
|
if (info)
|
|
356
|
-
this.cache?.set("teacherInfo", String(teacherId), info);
|
|
429
|
+
await this.cache?.set("teacherInfo", String(teacherId), info);
|
|
357
430
|
return info;
|
|
358
431
|
}
|
|
359
432
|
/**
|
|
@@ -363,21 +436,45 @@ export class TtClient {
|
|
|
363
436
|
*/
|
|
364
437
|
async getTeacherPhoto(teacherId) {
|
|
365
438
|
const photoCacheKey = String(teacherId);
|
|
366
|
-
const cachedPhoto = this.cache?.
|
|
439
|
+
const cachedPhoto = this.cache?.getLocal("teacherPhotos", photoCacheKey) ??
|
|
440
|
+
await this.cache?.get("teacherPhotos", photoCacheKey);
|
|
367
441
|
if (cachedPhoto !== null && cachedPhoto !== undefined) {
|
|
368
442
|
const entry = cachedPhoto;
|
|
369
|
-
|
|
443
|
+
if (entry.data !== undefined) {
|
|
444
|
+
return entry.data ? Buffer.from(entry.data, "base64") : null;
|
|
445
|
+
}
|
|
446
|
+
if (entry.blobKey && this.blobAdapter) {
|
|
447
|
+
const buf = await this.blobAdapter.get(entry.blobKey);
|
|
448
|
+
if (buf) {
|
|
449
|
+
this.cache?.setLocal("teacherPhotos", photoCacheKey, {
|
|
450
|
+
data: buf.toString("base64"),
|
|
451
|
+
});
|
|
452
|
+
return buf;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
370
455
|
}
|
|
371
456
|
// Get teacher info (may already be cached from schedule fetch)
|
|
372
457
|
const info = await this.getTeacherInfo(teacherId);
|
|
373
458
|
if (!info?.photoUrl) {
|
|
374
|
-
this.cache?.set("teacherPhotos", photoCacheKey, { data: null });
|
|
459
|
+
await this.cache?.set("teacherPhotos", photoCacheKey, { data: null });
|
|
375
460
|
return null;
|
|
376
461
|
}
|
|
377
462
|
const photoBuffer = await this.authGetBuffer(`${BASE}${info.photoUrl}`);
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
463
|
+
if (this.blobAdapter) {
|
|
464
|
+
const blobKey = `tt/teacher-photos/${teacherId}`;
|
|
465
|
+
this.cache?.setLocal("teacherPhotos", photoCacheKey, {
|
|
466
|
+
data: photoBuffer.toString("base64"),
|
|
467
|
+
});
|
|
468
|
+
await this.blobAdapter.put(blobKey, photoBuffer, {
|
|
469
|
+
ttl: this.cache?.ttl("teacherPhotos"),
|
|
470
|
+
});
|
|
471
|
+
await this.cache?.setExternal("teacherPhotos", photoCacheKey, { blobKey });
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
await this.cache?.set("teacherPhotos", photoCacheKey, {
|
|
475
|
+
data: photoBuffer.toString("base64"),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
381
478
|
return photoBuffer;
|
|
382
479
|
}
|
|
383
480
|
/**
|
|
@@ -387,20 +484,44 @@ export class TtClient {
|
|
|
387
484
|
*/
|
|
388
485
|
async getTeacherPhotoLazy(teacherId) {
|
|
389
486
|
const photoCacheKey = String(teacherId);
|
|
390
|
-
const cachedPhoto = this.cache?.
|
|
487
|
+
const cachedPhoto = this.cache?.getLocal("teacherPhotos", photoCacheKey) ??
|
|
488
|
+
await this.cache?.get("teacherPhotos", photoCacheKey);
|
|
391
489
|
if (cachedPhoto !== null && cachedPhoto !== undefined) {
|
|
392
490
|
const entry = cachedPhoto;
|
|
393
|
-
|
|
491
|
+
if (entry.data !== undefined) {
|
|
492
|
+
return entry.data ? Buffer.from(entry.data, "base64") : null;
|
|
493
|
+
}
|
|
494
|
+
if (entry.blobKey && this.blobAdapter) {
|
|
495
|
+
const buf = await this.blobAdapter.get(entry.blobKey);
|
|
496
|
+
if (buf) {
|
|
497
|
+
this.cache?.setLocal("teacherPhotos", photoCacheKey, {
|
|
498
|
+
data: buf.toString("base64"),
|
|
499
|
+
});
|
|
500
|
+
return buf;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
394
503
|
}
|
|
395
504
|
const url = `${BASE}/index/photo/tech/${teacherId}/id/${teacherId}`;
|
|
396
505
|
const photoBuffer = await this.authGetBuffer(url);
|
|
397
506
|
if (photoBuffer.length === 0) {
|
|
398
|
-
this.cache?.set("teacherPhotos", photoCacheKey, { data: null });
|
|
507
|
+
await this.cache?.set("teacherPhotos", photoCacheKey, { data: null });
|
|
399
508
|
return null;
|
|
400
509
|
}
|
|
401
|
-
this.
|
|
402
|
-
|
|
403
|
-
|
|
510
|
+
if (this.blobAdapter) {
|
|
511
|
+
const blobKey = `tt/teacher-photos/${teacherId}`;
|
|
512
|
+
this.cache?.setLocal("teacherPhotos", photoCacheKey, {
|
|
513
|
+
data: photoBuffer.toString("base64"),
|
|
514
|
+
});
|
|
515
|
+
await this.blobAdapter.put(blobKey, photoBuffer, {
|
|
516
|
+
ttl: this.cache?.ttl("teacherPhotos"),
|
|
517
|
+
});
|
|
518
|
+
await this.cache?.setExternal("teacherPhotos", photoCacheKey, { blobKey });
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
await this.cache?.set("teacherPhotos", photoCacheKey, {
|
|
522
|
+
data: photoBuffer.toString("base64"),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
404
525
|
return photoBuffer;
|
|
405
526
|
}
|
|
406
527
|
}
|
|
@@ -2,6 +2,7 @@ import { parseHtml, parseTeacher, parseWeekParity, parseWeeks, text, } from "../
|
|
|
2
2
|
import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
3
3
|
import { parseGroupsString } from "./groups.js";
|
|
4
4
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
5
|
+
import { LESSON_TYPE_GLOBAL_RE, LESSON_TYPE_RE, SUBGROUP_ANNOTATION_RE, SUBGROUP_RE, WEEKS_GLOBAL_RE, WEEKS_RE, } from "./patterns.js";
|
|
5
6
|
export function parseAudienceInfo(html) {
|
|
6
7
|
const doc = parseHtml(html);
|
|
7
8
|
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
@@ -105,9 +106,9 @@ function parseAudienceSemesterEntry(el) {
|
|
|
105
106
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
106
107
|
if (!subject)
|
|
107
108
|
return null;
|
|
108
|
-
const typeMatch = cleanText.match(
|
|
109
|
-
const weeksMatch = cleanText.match(
|
|
110
|
-
const subgroupMatch = cleanText.match(
|
|
109
|
+
const typeMatch = cleanText.match(LESSON_TYPE_RE);
|
|
110
|
+
const weeksMatch = cleanText.match(WEEKS_RE);
|
|
111
|
+
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
111
112
|
const weekParity = parseWeekParity(cleanHtml);
|
|
112
113
|
// Audience entries layout:
|
|
113
114
|
// <span blue>SUBJ</span> (TYPE) (WEEKS) <br>TEACHER<br>GROUPS
|
|
@@ -122,9 +123,9 @@ function parseAudienceSemesterEntry(el) {
|
|
|
122
123
|
const textLines = [];
|
|
123
124
|
for (const p of parts) {
|
|
124
125
|
const cleaned = p
|
|
125
|
-
.replace(
|
|
126
|
-
.replace(
|
|
127
|
-
.replace(
|
|
126
|
+
.replace(LESSON_TYPE_GLOBAL_RE, "")
|
|
127
|
+
.replace(WEEKS_GLOBAL_RE, "")
|
|
128
|
+
.replace(SUBGROUP_ANNOTATION_RE, "")
|
|
128
129
|
.trim();
|
|
129
130
|
if (cleaned)
|
|
130
131
|
textLines.push(cleaned);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
2
|
import { getLessonNumber } from "../utils/index.js";
|
|
3
3
|
import { parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
4
|
+
import { FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
4
5
|
export function parseFullSchedule(html, educationType) {
|
|
5
6
|
const doc = parseHtml(html);
|
|
6
7
|
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
@@ -94,11 +95,11 @@ function parseSemesterEntry(el) {
|
|
|
94
95
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
95
96
|
if (!subject)
|
|
96
97
|
return null;
|
|
97
|
-
const typeMatch = cleanText.match(
|
|
98
|
-
const weeksMatch = cleanText.match(
|
|
98
|
+
const typeMatch = cleanText.match(LESSON_TYPE_RE);
|
|
99
|
+
const weeksMatch = cleanText.match(WEEKS_RE);
|
|
99
100
|
const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
|
|
100
101
|
const teacherMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
|
|
101
|
-
const subgroupMatch = cleanText.match(
|
|
102
|
+
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
102
103
|
const weekParity = parseWeekParity(cleanHtml);
|
|
103
104
|
return {
|
|
104
105
|
room: roomMatch?.[1] ?? "",
|
|
@@ -169,7 +170,7 @@ function parseSessionEntry(td) {
|
|
|
169
170
|
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
170
171
|
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
171
172
|
// Type: parenthesized text after </span>, case-insensitive
|
|
172
|
-
const typeMatch = plainText.match(
|
|
173
|
+
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
173
174
|
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
174
175
|
// Time: after <br>, format HH:MM - HH:MM
|
|
175
176
|
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
package/dist/tt/parse/groups.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SUBGROUP_ANNOTATION_RE_I } from "./patterns.js";
|
|
1
2
|
/**
|
|
2
3
|
* Split a raw "groups" string from the schedule HTML into individual group names.
|
|
3
4
|
*
|
|
@@ -16,7 +17,7 @@ export function parseGroupsString(raw) {
|
|
|
16
17
|
if (!raw)
|
|
17
18
|
return [];
|
|
18
19
|
const cleaned = raw
|
|
19
|
-
.replace(
|
|
20
|
+
.replace(SUBGROUP_ANNOTATION_RE_I, " ")
|
|
20
21
|
.trim();
|
|
21
22
|
if (!cleaned)
|
|
22
23
|
return [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseTeacher, text } from "../../common/parse.js";
|
|
2
2
|
import { parseGroupsString } from "./groups.js";
|
|
3
|
+
import { LESSON_TYPE_PATTERN, LESSON_TYPE_RE, LESSON_TYPE_RE_I, SUBGROUP_RE, } from "./patterns.js";
|
|
3
4
|
export function parseDate(dd, mm, yyyy) {
|
|
4
5
|
return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
5
6
|
}
|
|
@@ -17,19 +18,31 @@ export function parseTransferDiv(div) {
|
|
|
17
18
|
if (!subject)
|
|
18
19
|
return null;
|
|
19
20
|
const roomMatch = divHtml.match(/([А-Яа-яA-Za-z]-\d+)/);
|
|
20
|
-
const typeMatch = divText.match(
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const typeMatch = divText.match(LESSON_TYPE_RE);
|
|
22
|
+
const parts = divHtml
|
|
23
|
+
.split(/<br\s*\/?>/i)
|
|
24
|
+
.map((part) => part.replace(/<[^>]*>/g, "").trim())
|
|
25
|
+
.filter((part) => part.length > 0);
|
|
23
26
|
let teacherPart = "";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
let groupsPart = "";
|
|
28
|
+
for (const part of parts.slice(1)) {
|
|
29
|
+
const cleaned = part.trim();
|
|
30
|
+
if (!cleaned)
|
|
31
|
+
continue;
|
|
32
|
+
const groups = parseGroupsString(cleaned);
|
|
33
|
+
if (groups.length > 0) {
|
|
34
|
+
groupsPart = cleaned;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const isLessonMeta = cleaned.includes(subject) ||
|
|
38
|
+
(roomMatch?.[1] != null && cleaned.includes(roomMatch[1])) ||
|
|
39
|
+
LESSON_TYPE_RE_I.test(cleaned);
|
|
40
|
+
if (!isLessonMeta && !teacherPart) {
|
|
41
|
+
teacherPart = cleaned;
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
const transfer = { targetDate, fromDate, fromSlot, subject };
|
|
32
|
-
const subgroupMatch = divText.match(
|
|
45
|
+
const subgroupMatch = divText.match(SUBGROUP_RE);
|
|
33
46
|
return {
|
|
34
47
|
transfer,
|
|
35
48
|
entry: {
|
|
@@ -38,7 +51,7 @@ export function parseTransferDiv(div) {
|
|
|
38
51
|
type: typeMatch?.[1] ?? "",
|
|
39
52
|
weeks: { from: 0, to: 0 },
|
|
40
53
|
teacher: parseTeacher(teacherPart),
|
|
41
|
-
groups:
|
|
54
|
+
groups: parseGroupsString(groupsPart),
|
|
42
55
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
43
56
|
transfer,
|
|
44
57
|
},
|
|
@@ -86,9 +99,9 @@ export function parseSubstituteForDiv(div) {
|
|
|
86
99
|
if (!subject)
|
|
87
100
|
return null;
|
|
88
101
|
const roomMatch = divHtml.match(/(?:<br\s*\/?>)\s*([А-Яа-яA-Za-z]-\d+)/);
|
|
89
|
-
const typeMatch = divText.match(
|
|
90
|
-
const groupsMatch = divHtml.match(
|
|
91
|
-
const subgroupMatch = divText.match(
|
|
102
|
+
const typeMatch = divText.match(LESSON_TYPE_RE);
|
|
103
|
+
const groupsMatch = divHtml.match(new RegExp(`\\((?:${LESSON_TYPE_PATTERN})\\)\\s*(?:<br\\s*\\/?>)\\s*([^<]+?)(?:\\s*<i|$)`));
|
|
104
|
+
const subgroupMatch = divText.match(SUBGROUP_RE);
|
|
92
105
|
return {
|
|
93
106
|
entry: {
|
|
94
107
|
room: roomMatch?.[1] ?? "",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const LESSON_TYPE_PATTERN = "\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441";
|
|
2
|
+
export declare const FLEXIBLE_LESSON_TYPE_PATTERN = "\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441\\.?|\u042D\u043A\u0437";
|
|
3
|
+
export declare const LESSON_TYPE_RE: RegExp;
|
|
4
|
+
export declare const LESSON_TYPE_RE_I: RegExp;
|
|
5
|
+
export declare const FLEXIBLE_LESSON_TYPE_RE_I: RegExp;
|
|
6
|
+
export declare const LESSON_TYPE_GLOBAL_RE: RegExp;
|
|
7
|
+
export declare const WEEKS_RE: RegExp;
|
|
8
|
+
export declare const WEEKS_GLOBAL_RE: RegExp;
|
|
9
|
+
export declare const SUBGROUP_RE: RegExp;
|
|
10
|
+
export declare const SUBGROUP_ANNOTATION_RE: RegExp;
|
|
11
|
+
export declare const SUBGROUP_ANNOTATION_RE_I: RegExp;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const LESSON_TYPE_PATTERN = "лк|пр|лб|зач|экз|конс";
|
|
2
|
+
export const FLEXIBLE_LESSON_TYPE_PATTERN = `${LESSON_TYPE_PATTERN}\\.?|Экз`;
|
|
3
|
+
export const LESSON_TYPE_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`);
|
|
4
|
+
export const LESSON_TYPE_RE_I = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "i");
|
|
5
|
+
export const FLEXIBLE_LESSON_TYPE_RE_I = new RegExp(`\\((${FLEXIBLE_LESSON_TYPE_PATTERN})\\)`, "i");
|
|
6
|
+
export const LESSON_TYPE_GLOBAL_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "g");
|
|
7
|
+
export const WEEKS_RE = /\(([^)]*нед\.?[^)]*)\)/;
|
|
8
|
+
export const WEEKS_GLOBAL_RE = /\([^)]*нед\.?[^)]*\)/g;
|
|
9
|
+
export const SUBGROUP_RE = /(\d+)\s*подгруппа/;
|
|
10
|
+
export const SUBGROUP_ANNOTATION_RE = /\(\d+\s*подгруппа\)/g;
|
|
11
|
+
export const SUBGROUP_ANNOTATION_RE_I = /\s*\(\s*\d+\s*подгруппа\s*\)\s*/gi;
|
package/dist/tt/parse/teacher.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getLessonNumber } from "../utils/index.js";
|
|
|
3
3
|
import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
4
4
|
import { parseGroupsString } from "./groups.js";
|
|
5
5
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
6
|
+
import { FLEXIBLE_LESSON_TYPE_PATTERN, FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
6
7
|
export function parseTeacherFullSchedule(html, educationType) {
|
|
7
8
|
const doc = parseHtml(html);
|
|
8
9
|
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
@@ -52,11 +53,11 @@ function parseTeacherSemesterEntry(el) {
|
|
|
52
53
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
53
54
|
if (!subject)
|
|
54
55
|
return null;
|
|
55
|
-
const typeMatch = cleanText.match(
|
|
56
|
-
const weeksMatch = cleanText.match(
|
|
56
|
+
const typeMatch = cleanText.match(LESSON_TYPE_RE);
|
|
57
|
+
const weeksMatch = cleanText.match(WEEKS_RE);
|
|
57
58
|
const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
|
|
58
59
|
const groupsMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
|
|
59
|
-
const subgroupMatch = cleanText.match(
|
|
60
|
+
const subgroupMatch = cleanText.match(SUBGROUP_RE);
|
|
60
61
|
const weekParity = parseWeekParity(cleanHtml);
|
|
61
62
|
return {
|
|
62
63
|
room: roomMatch?.[1] ?? "",
|
|
@@ -121,10 +122,10 @@ function parseTeacherSessionEntry(td) {
|
|
|
121
122
|
return null;
|
|
122
123
|
const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
|
|
123
124
|
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
124
|
-
const typeMatch = plainText.match(
|
|
125
|
+
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
125
126
|
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
126
127
|
// Groups: text between </span> type and <br>time
|
|
127
|
-
const groupsMatch = fullHtml.match(
|
|
128
|
+
const groupsMatch = fullHtml.match(new RegExp(`\\((?:${FLEXIBLE_LESSON_TYPE_PATTERN})\\)\\s*([^<]+?)\\s*<br`, "i"));
|
|
128
129
|
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
129
130
|
if (!timeMatch)
|
|
130
131
|
return null;
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlobAdapter, CacheAdapter } from "../common/cache.js";
|
|
1
2
|
import type { Time, WeekRange, Teacher, EducationType } from "../common/types.js";
|
|
2
3
|
export interface Faculty {
|
|
3
4
|
id: number;
|
|
@@ -148,6 +149,8 @@ export interface CacheConfig {
|
|
|
148
149
|
schedule?: number;
|
|
149
150
|
faculties?: number;
|
|
150
151
|
groups?: number;
|
|
152
|
+
audiences?: number;
|
|
153
|
+
audienceNames?: number;
|
|
151
154
|
teachers?: number;
|
|
152
155
|
teacherInfo?: number;
|
|
153
156
|
teacherPhotos?: number;
|
|
@@ -157,4 +160,6 @@ export interface CacheConfig {
|
|
|
157
160
|
export interface TtClientOptions {
|
|
158
161
|
educationType?: EducationType;
|
|
159
162
|
cache?: number | CacheConfig;
|
|
163
|
+
cacheAdapter?: CacheAdapter;
|
|
164
|
+
blobAdapter?: BlobAdapter;
|
|
160
165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chuvsu-js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
45
|
-
"build": "pnpm clean && tsc"
|
|
45
|
+
"build": "pnpm clean && tsc",
|
|
46
|
+
"test": "pnpm build && node --test"
|
|
46
47
|
}
|
|
47
48
|
}
|