chuvsu-js 2.6.2 → 2.8.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/dist/index.d.ts +1 -1
- package/dist/tt/client.d.ts +56 -1
- package/dist/tt/client.js +180 -2
- package/dist/tt/parse.d.ts +5 -1
- package/dist/tt/parse.js +150 -1
- package/dist/tt/types.d.ts +24 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export type { Holiday, HolidayTransfer } from "./tt/utils.js";
|
|
|
7
7
|
export { Period, EducationType, AuthError, ParseError, } from "./common/types.js";
|
|
8
8
|
export type { Time, WeekRange, Teacher } from "./common/types.js";
|
|
9
9
|
export type { PersonalData } from "./lk/types.js";
|
|
10
|
-
export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, SubstituteForInfo, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
|
|
10
|
+
export type { Audience, AudienceInfo, Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, SubstituteForInfo, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CacheEntry } from "../common/cache.js";
|
|
2
2
|
import { Period } from "../common/types.js";
|
|
3
3
|
import { Schedule } from "./schedule.js";
|
|
4
|
-
import type { Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig } from "./types.js";
|
|
4
|
+
import type { Audience, AudienceInfo, Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig } from "./types.js";
|
|
5
5
|
export declare class TtClient {
|
|
6
6
|
private http;
|
|
7
7
|
private educationType;
|
|
@@ -20,6 +20,7 @@ export declare class TtClient {
|
|
|
20
20
|
private isSessionExpired;
|
|
21
21
|
private relogin;
|
|
22
22
|
private authGet;
|
|
23
|
+
private authGetBuffer;
|
|
23
24
|
private authPost;
|
|
24
25
|
private fetchSchedule;
|
|
25
26
|
/**
|
|
@@ -41,6 +42,48 @@ export declare class TtClient {
|
|
|
41
42
|
searchGroup(opts: {
|
|
42
43
|
name: string;
|
|
43
44
|
}): Promise<Group[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Search audiences by name (substring match). The server requires at
|
|
47
|
+
* least 3 characters in the query.
|
|
48
|
+
*/
|
|
49
|
+
searchAudience(opts: {
|
|
50
|
+
name: string;
|
|
51
|
+
}): Promise<Audience[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Get every audience known to the system in a single request.
|
|
54
|
+
*
|
|
55
|
+
* The site exposes only a search form ("at least 3 characters") and no
|
|
56
|
+
* listing endpoint. However the query is passed to a SQL LIKE, so the
|
|
57
|
+
* 3-character wildcard `%%%` matches every audience at once and returns
|
|
58
|
+
* the full list of (id, name) pairs.
|
|
59
|
+
*/
|
|
60
|
+
getAudiences(): Promise<Audience[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an audience id from its exact name by searching and
|
|
63
|
+
* selecting the button whose `value` equals the given name.
|
|
64
|
+
*/
|
|
65
|
+
findAudienceByName(opts: {
|
|
66
|
+
name: string;
|
|
67
|
+
}): Promise<Audience | null>;
|
|
68
|
+
/** Fetch the audience's display name from its schedule page. */
|
|
69
|
+
getAudienceName(audienceId: number): Promise<string | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Fetch detailed info about an audience (building, floor, usage,
|
|
72
|
+
* image URLs for the audience photo, building photo and floor plan).
|
|
73
|
+
*/
|
|
74
|
+
getAudienceInfo(audienceId: number): Promise<AudienceInfo | null>;
|
|
75
|
+
private fetchAudienceSchedule;
|
|
76
|
+
getAudienceSchedule(audienceId: number): Promise<Schedule>;
|
|
77
|
+
getAudienceScheduleForPeriod(opts: {
|
|
78
|
+
audienceId: number;
|
|
79
|
+
period: Period;
|
|
80
|
+
}): Promise<Schedule>;
|
|
81
|
+
/** Get the audience photo (audimage). Returns null if missing. */
|
|
82
|
+
getAudienceImage(audienceId: number): Promise<Buffer | null>;
|
|
83
|
+
/** Get the building exterior image (blockimage). Returns null if missing. */
|
|
84
|
+
getAudienceBlockImage(audienceId: number): Promise<Buffer | null>;
|
|
85
|
+
/** Get the floor plan image for the audience. Returns null if missing. */
|
|
86
|
+
getAudienceFloorplan(audienceId: number): Promise<Buffer | null>;
|
|
44
87
|
searchTeacher(opts: {
|
|
45
88
|
name: string;
|
|
46
89
|
}): Promise<{
|
|
@@ -58,4 +101,16 @@ export declare class TtClient {
|
|
|
58
101
|
period: Period;
|
|
59
102
|
}): Promise<Schedule>;
|
|
60
103
|
getTeacherInfo(teacherId: number): Promise<TeacherInfo | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Get the teacher's photo as a Buffer.
|
|
106
|
+
* Returns null if the teacher has no photo.
|
|
107
|
+
* Uses cached teacher info when available to avoid extra requests.
|
|
108
|
+
*/
|
|
109
|
+
getTeacherPhoto(teacherId: number): Promise<Buffer | null>;
|
|
110
|
+
/**
|
|
111
|
+
* Get the teacher's photo without parsing the schedule page.
|
|
112
|
+
* Uses the known URL pattern directly — no extra page fetch needed.
|
|
113
|
+
* Returns null if the teacher has no photo.
|
|
114
|
+
*/
|
|
115
|
+
getTeacherPhotoLazy(teacherId: number): Promise<Buffer | null>;
|
|
61
116
|
}
|
package/dist/tt/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from "../common/http.js";
|
|
2
2
|
import { Cache } from "../common/cache.js";
|
|
3
3
|
import { AuthError } from "../common/types.js";
|
|
4
|
-
import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse.js";
|
|
4
|
+
import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse.js";
|
|
5
5
|
import { Schedule } from "./schedule.js";
|
|
6
6
|
const BASE = "https://tt.chuvsu.ru";
|
|
7
7
|
const AUTH_URL = `${BASE}/auth`;
|
|
@@ -88,6 +88,9 @@ export class TtClient {
|
|
|
88
88
|
}
|
|
89
89
|
return res;
|
|
90
90
|
}
|
|
91
|
+
async authGetBuffer(url) {
|
|
92
|
+
return this.http.getBuffer(url);
|
|
93
|
+
}
|
|
91
94
|
async authPost(url, data) {
|
|
92
95
|
const res = await this.http.post(url, data);
|
|
93
96
|
if (this.loginMode && this.isSessionExpired(res.body)) {
|
|
@@ -164,6 +167,122 @@ export class TtClient {
|
|
|
164
167
|
});
|
|
165
168
|
return parseGroupButtons(body);
|
|
166
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Search audiences by name (substring match). The server requires at
|
|
172
|
+
* least 3 characters in the query.
|
|
173
|
+
*/
|
|
174
|
+
async searchAudience(opts) {
|
|
175
|
+
const { body } = await this.authPost(`${BASE}/`, {
|
|
176
|
+
audname: opts.name,
|
|
177
|
+
findaud: "найти",
|
|
178
|
+
hfac: "0",
|
|
179
|
+
pertt: this.pertt,
|
|
180
|
+
});
|
|
181
|
+
return parseAudienceButtons(body);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get every audience known to the system in a single request.
|
|
185
|
+
*
|
|
186
|
+
* The site exposes only a search form ("at least 3 characters") and no
|
|
187
|
+
* listing endpoint. However the query is passed to a SQL LIKE, so the
|
|
188
|
+
* 3-character wildcard `%%%` matches every audience at once and returns
|
|
189
|
+
* the full list of (id, name) pairs.
|
|
190
|
+
*/
|
|
191
|
+
async getAudiences() {
|
|
192
|
+
const { body } = await this.authPost(`${BASE}/`, {
|
|
193
|
+
audname: "%%%",
|
|
194
|
+
findaud: "найти",
|
|
195
|
+
hfac: "0",
|
|
196
|
+
pertt: this.pertt,
|
|
197
|
+
});
|
|
198
|
+
return parseAudienceButtons(body);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolve an audience id from its exact name by searching and
|
|
202
|
+
* selecting the button whose `value` equals the given name.
|
|
203
|
+
*/
|
|
204
|
+
async findAudienceByName(opts) {
|
|
205
|
+
const q = opts.name.length >= 3 ? opts.name : "%%%";
|
|
206
|
+
const list = await this.searchAudience({ name: q });
|
|
207
|
+
return list.find((a) => a.name === opts.name) ?? null;
|
|
208
|
+
}
|
|
209
|
+
/** Fetch the audience's display name from its schedule page. */
|
|
210
|
+
async getAudienceName(audienceId) {
|
|
211
|
+
const { body } = await this.authGet(`${BASE}/index/audtt/aud/${audienceId}`);
|
|
212
|
+
return parseAudienceName(body);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Fetch detailed info about an audience (building, floor, usage,
|
|
216
|
+
* image URLs for the audience photo, building photo and floor plan).
|
|
217
|
+
*/
|
|
218
|
+
async getAudienceInfo(audienceId) {
|
|
219
|
+
const cached = this.cache?.get("audienceInfo", String(audienceId));
|
|
220
|
+
if (cached)
|
|
221
|
+
return cached;
|
|
222
|
+
const { body } = await this.authGet(`${BASE}/index/audtt/aud/${audienceId}`);
|
|
223
|
+
const info = parseAudienceInfo(body);
|
|
224
|
+
if (info)
|
|
225
|
+
this.cache?.set("audienceInfo", String(audienceId), info);
|
|
226
|
+
return info;
|
|
227
|
+
}
|
|
228
|
+
async fetchAudienceSchedule(audienceId, period) {
|
|
229
|
+
const cacheKey = `audience:${audienceId}:${period}`;
|
|
230
|
+
const cached = this.cache?.get("schedule", cacheKey);
|
|
231
|
+
if (cached)
|
|
232
|
+
return cached;
|
|
233
|
+
const url = `${BASE}/index/audtt/aud/${audienceId}`;
|
|
234
|
+
const { body } = await this.authPost(url, { htype: String(period) });
|
|
235
|
+
const days = parseAudienceFullSchedule(body);
|
|
236
|
+
this.cache?.set("schedule", cacheKey, days);
|
|
237
|
+
// Cache audience info from the same page to avoid an extra request.
|
|
238
|
+
if (!this.cache?.get("audienceInfo", String(audienceId))) {
|
|
239
|
+
const info = parseAudienceInfo(body);
|
|
240
|
+
if (info)
|
|
241
|
+
this.cache?.set("audienceInfo", String(audienceId), info);
|
|
242
|
+
}
|
|
243
|
+
return days;
|
|
244
|
+
}
|
|
245
|
+
async getAudienceSchedule(audienceId) {
|
|
246
|
+
const schedules = new Map();
|
|
247
|
+
const results = await Promise.all(ALL_PERIODS.map(async (period) => {
|
|
248
|
+
const days = await this.fetchAudienceSchedule(audienceId, period);
|
|
249
|
+
return { period, days };
|
|
250
|
+
}));
|
|
251
|
+
for (const { period, days } of results) {
|
|
252
|
+
schedules.set(period, days);
|
|
253
|
+
}
|
|
254
|
+
return new Schedule(audienceId, schedules, undefined, this.educationType);
|
|
255
|
+
}
|
|
256
|
+
async getAudienceScheduleForPeriod(opts) {
|
|
257
|
+
const days = await this.fetchAudienceSchedule(opts.audienceId, opts.period);
|
|
258
|
+
const schedules = new Map();
|
|
259
|
+
schedules.set(opts.period, days);
|
|
260
|
+
return new Schedule(opts.audienceId, schedules, opts.period, this.educationType);
|
|
261
|
+
}
|
|
262
|
+
/** Get the audience photo (audimage). Returns null if missing. */
|
|
263
|
+
async getAudienceImage(audienceId) {
|
|
264
|
+
const info = await this.getAudienceInfo(audienceId);
|
|
265
|
+
if (!info?.audImageUrl)
|
|
266
|
+
return null;
|
|
267
|
+
const buf = await this.authGetBuffer(`${BASE}${info.audImageUrl}`);
|
|
268
|
+
return buf.length > 0 ? buf : null;
|
|
269
|
+
}
|
|
270
|
+
/** Get the building exterior image (blockimage). Returns null if missing. */
|
|
271
|
+
async getAudienceBlockImage(audienceId) {
|
|
272
|
+
const info = await this.getAudienceInfo(audienceId);
|
|
273
|
+
if (!info?.blockImageUrl)
|
|
274
|
+
return null;
|
|
275
|
+
const buf = await this.authGetBuffer(`${BASE}${info.blockImageUrl}`);
|
|
276
|
+
return buf.length > 0 ? buf : null;
|
|
277
|
+
}
|
|
278
|
+
/** Get the floor plan image for the audience. Returns null if missing. */
|
|
279
|
+
async getAudienceFloorplan(audienceId) {
|
|
280
|
+
const info = await this.getAudienceInfo(audienceId);
|
|
281
|
+
if (!info?.floorplanUrl)
|
|
282
|
+
return null;
|
|
283
|
+
const buf = await this.authGetBuffer(`${BASE}${info.floorplanUrl}`);
|
|
284
|
+
return buf.length > 0 ? buf : null;
|
|
285
|
+
}
|
|
167
286
|
async searchTeacher(opts) {
|
|
168
287
|
const { body } = await this.authPost(`${BASE}/`, {
|
|
169
288
|
techname: opts.name,
|
|
@@ -192,6 +311,12 @@ export class TtClient {
|
|
|
192
311
|
const { body } = await this.authPost(url, { htype: String(period) });
|
|
193
312
|
const days = parseTeacherFullSchedule(body, this.educationType);
|
|
194
313
|
this.cache?.set("schedule", cacheKey, days);
|
|
314
|
+
// Cache teacher info from the same page to avoid extra requests
|
|
315
|
+
if (!this.cache?.get("teacherInfo", String(teacherId))) {
|
|
316
|
+
const info = parseTeacherInfo(body);
|
|
317
|
+
if (info)
|
|
318
|
+
this.cache?.set("teacherInfo", String(teacherId), info);
|
|
319
|
+
}
|
|
195
320
|
return days;
|
|
196
321
|
}
|
|
197
322
|
async getTeacherSchedule(teacherId) {
|
|
@@ -212,8 +337,61 @@ export class TtClient {
|
|
|
212
337
|
return new Schedule(opts.teacherId, schedules, opts.period, this.educationType, undefined, undefined, true);
|
|
213
338
|
}
|
|
214
339
|
async getTeacherInfo(teacherId) {
|
|
340
|
+
const cached = this.cache?.get("teacherInfo", String(teacherId));
|
|
341
|
+
if (cached)
|
|
342
|
+
return cached;
|
|
215
343
|
const url = `${BASE}/index/techtt/tech/${teacherId}`;
|
|
216
344
|
const { body } = await this.authGet(url);
|
|
217
|
-
|
|
345
|
+
const info = parseTeacherInfo(body);
|
|
346
|
+
if (info)
|
|
347
|
+
this.cache?.set("teacherInfo", String(teacherId), info);
|
|
348
|
+
return info;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Get the teacher's photo as a Buffer.
|
|
352
|
+
* Returns null if the teacher has no photo.
|
|
353
|
+
* Uses cached teacher info when available to avoid extra requests.
|
|
354
|
+
*/
|
|
355
|
+
async getTeacherPhoto(teacherId) {
|
|
356
|
+
const photoCacheKey = String(teacherId);
|
|
357
|
+
const cachedPhoto = this.cache?.get("teacherPhotos", photoCacheKey);
|
|
358
|
+
if (cachedPhoto !== null && cachedPhoto !== undefined) {
|
|
359
|
+
const entry = cachedPhoto;
|
|
360
|
+
return entry.data ? Buffer.from(entry.data, "base64") : null;
|
|
361
|
+
}
|
|
362
|
+
// Get teacher info (may already be cached from schedule fetch)
|
|
363
|
+
const info = await this.getTeacherInfo(teacherId);
|
|
364
|
+
if (!info?.photoUrl) {
|
|
365
|
+
this.cache?.set("teacherPhotos", photoCacheKey, { data: null });
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const photoBuffer = await this.authGetBuffer(`${BASE}${info.photoUrl}`);
|
|
369
|
+
this.cache?.set("teacherPhotos", photoCacheKey, {
|
|
370
|
+
data: photoBuffer.toString("base64"),
|
|
371
|
+
});
|
|
372
|
+
return photoBuffer;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get the teacher's photo without parsing the schedule page.
|
|
376
|
+
* Uses the known URL pattern directly — no extra page fetch needed.
|
|
377
|
+
* Returns null if the teacher has no photo.
|
|
378
|
+
*/
|
|
379
|
+
async getTeacherPhotoLazy(teacherId) {
|
|
380
|
+
const photoCacheKey = String(teacherId);
|
|
381
|
+
const cachedPhoto = this.cache?.get("teacherPhotos", photoCacheKey);
|
|
382
|
+
if (cachedPhoto !== null && cachedPhoto !== undefined) {
|
|
383
|
+
const entry = cachedPhoto;
|
|
384
|
+
return entry.data ? Buffer.from(entry.data, "base64") : null;
|
|
385
|
+
}
|
|
386
|
+
const url = `${BASE}/index/photo/tech/${teacherId}/id/${teacherId}`;
|
|
387
|
+
const photoBuffer = await this.authGetBuffer(url);
|
|
388
|
+
if (photoBuffer.length === 0) {
|
|
389
|
+
this.cache?.set("teacherPhotos", photoCacheKey, { data: null });
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
this.cache?.set("teacherPhotos", photoCacheKey, {
|
|
393
|
+
data: photoBuffer.toString("base64"),
|
|
394
|
+
});
|
|
395
|
+
return photoBuffer;
|
|
218
396
|
}
|
|
219
397
|
}
|
package/dist/tt/parse.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type Period, EducationType } from "../common/types.js";
|
|
2
|
-
import type { Faculty, Group, FullScheduleDay, TeacherInfo } from "./types.js";
|
|
2
|
+
import type { Audience, AudienceInfo, Faculty, Group, FullScheduleDay, TeacherInfo } from "./types.js";
|
|
3
3
|
export declare function parsePeriodFromPage(html: string): Period | null;
|
|
4
4
|
export declare function parseGroupButtons(html: string): Group[];
|
|
5
5
|
export declare function parseFacultyButtons(html: string): Faculty[];
|
|
6
|
+
export declare function parseAudienceButtons(html: string): Audience[];
|
|
7
|
+
export declare function parseAudienceName(html: string): string | null;
|
|
6
8
|
export declare function parseTeacherButtons(html: string): {
|
|
7
9
|
id: number;
|
|
8
10
|
name: string;
|
|
9
11
|
}[];
|
|
10
12
|
export declare function parseFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
|
|
13
|
+
export declare function parseAudienceInfo(html: string): AudienceInfo | null;
|
|
14
|
+
export declare function parseAudienceFullSchedule(html: string): FullScheduleDay[];
|
|
11
15
|
export declare function parseTeacherFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
|
|
12
16
|
export declare function parseTeacherInfo(html: string): TeacherInfo | null;
|
package/dist/tt/parse.js
CHANGED
|
@@ -40,6 +40,31 @@ export function parseFacultyButtons(html) {
|
|
|
40
40
|
}
|
|
41
41
|
return faculties;
|
|
42
42
|
}
|
|
43
|
+
export function parseAudienceButtons(html) {
|
|
44
|
+
const results = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const re = /<button[^>]*\bname="aud(\d+)"[^>]*\bvalue="([^"]*)"/g;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = re.exec(html)) !== null) {
|
|
49
|
+
const id = parseInt(m[1]);
|
|
50
|
+
if (seen.has(id))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(id);
|
|
53
|
+
results.push({ id, name: m[2] });
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
export function parseAudienceName(html) {
|
|
58
|
+
const m = html.match(/id="path"[\s\S]*?findaud[^>]*>[^<]*<\/a>([\s\S]*?)<\/div>/);
|
|
59
|
+
if (!m)
|
|
60
|
+
return null;
|
|
61
|
+
const tail = m[1]
|
|
62
|
+
.replace(/ /g, " ")
|
|
63
|
+
.replace(/<[^>]*>/g, "")
|
|
64
|
+
.replace(/^[\s/]+/, "")
|
|
65
|
+
.trim();
|
|
66
|
+
return tail || null;
|
|
67
|
+
}
|
|
43
68
|
export function parseTeacherButtons(html) {
|
|
44
69
|
const doc = parseHtml(html);
|
|
45
70
|
const results = [];
|
|
@@ -344,6 +369,128 @@ function parseSessionEntry(td) {
|
|
|
344
369
|
timeEnd: parseTime(timeMatch[2]),
|
|
345
370
|
};
|
|
346
371
|
}
|
|
372
|
+
// --- Audience schedule & info parsing ---
|
|
373
|
+
export function parseAudienceInfo(html) {
|
|
374
|
+
const doc = parseHtml(html);
|
|
375
|
+
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
376
|
+
const nameEl = doc.querySelector('.htext span[style*="color: blue"]');
|
|
377
|
+
const name = nameEl ? text(nameEl).trim() : "";
|
|
378
|
+
if (!name)
|
|
379
|
+
return null;
|
|
380
|
+
// Details: <span class="htextb"> (Корпус Б; 3 этаж - Учебная лаборатория)</span>
|
|
381
|
+
const detailsEl = doc.querySelector(".htextb");
|
|
382
|
+
const details = detailsEl ? text(detailsEl).trim() : "";
|
|
383
|
+
let building;
|
|
384
|
+
let floor;
|
|
385
|
+
let usage;
|
|
386
|
+
if (details) {
|
|
387
|
+
const buildingMatch = details.match(/Корпус\s+([^\s;,)]+)/i);
|
|
388
|
+
if (buildingMatch)
|
|
389
|
+
building = buildingMatch[1];
|
|
390
|
+
const floorMatch = details.match(/(\d+)\s*этаж/i);
|
|
391
|
+
if (floorMatch)
|
|
392
|
+
floor = parseInt(floorMatch[1]);
|
|
393
|
+
const usageMatch = details.match(/этаж\s*-\s*([^)]+?)\s*\)?\s*$/i);
|
|
394
|
+
if (usageMatch)
|
|
395
|
+
usage = usageMatch[1].trim();
|
|
396
|
+
}
|
|
397
|
+
const audImg = doc.querySelector("#audsrc");
|
|
398
|
+
const blockImg = doc.querySelector("#blocksrc");
|
|
399
|
+
const floorImg = doc.querySelector("#floorsrc");
|
|
400
|
+
return {
|
|
401
|
+
name,
|
|
402
|
+
building,
|
|
403
|
+
floor,
|
|
404
|
+
usage,
|
|
405
|
+
audImageUrl: audImg?.getAttribute("src") || undefined,
|
|
406
|
+
blockImageUrl: blockImg?.getAttribute("src") || undefined,
|
|
407
|
+
floorplanUrl: floorImg?.getAttribute("src") || undefined,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function parseAudienceSemesterEntry(el) {
|
|
411
|
+
const td = el.querySelector("td") ?? el;
|
|
412
|
+
const fullHtml = td.innerHTML ?? "";
|
|
413
|
+
const plainText = text(td);
|
|
414
|
+
if (!plainText)
|
|
415
|
+
return null;
|
|
416
|
+
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
417
|
+
const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
|
|
418
|
+
for (const div of redDivs) {
|
|
419
|
+
const result = parseTransferDiv(div);
|
|
420
|
+
if (result) {
|
|
421
|
+
if (possibleChanges)
|
|
422
|
+
result.entry.possibleChanges = true;
|
|
423
|
+
return result.entry;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const div of redDivs) {
|
|
427
|
+
const result = parseSubstituteForDiv(div);
|
|
428
|
+
if (result) {
|
|
429
|
+
if (possibleChanges)
|
|
430
|
+
result.entry.possibleChanges = true;
|
|
431
|
+
return result.entry;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const substitutions = [];
|
|
435
|
+
for (const div of redDivs) {
|
|
436
|
+
const sub = parseSubstitutionDiv(div);
|
|
437
|
+
if (sub)
|
|
438
|
+
substitutions.push(sub);
|
|
439
|
+
}
|
|
440
|
+
let cleanHtml = fullHtml;
|
|
441
|
+
let cleanText = plainText;
|
|
442
|
+
for (const div of redDivs) {
|
|
443
|
+
cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
|
|
444
|
+
cleanText = cleanText.replace(text(div), "");
|
|
445
|
+
}
|
|
446
|
+
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
447
|
+
const subject = subjectEl ? text(subjectEl) : "";
|
|
448
|
+
if (!subject)
|
|
449
|
+
return null;
|
|
450
|
+
const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
|
|
451
|
+
const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
|
|
452
|
+
const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
|
|
453
|
+
const weekParity = parseWeekParity(cleanHtml);
|
|
454
|
+
// Audience entries layout:
|
|
455
|
+
// <span blue>SUBJ</span> (TYPE) (WEEKS) <br>TEACHER<br>GROUPS
|
|
456
|
+
// Teacher is the first line after </span>...<br>, groups is the next line.
|
|
457
|
+
// We split on <br> after the blue subject span.
|
|
458
|
+
const afterSubject = cleanHtml.split(/<\/span>/i).slice(1).join("</span>");
|
|
459
|
+
const parts = afterSubject
|
|
460
|
+
.split(/<br\s*\/?>/i)
|
|
461
|
+
.map((p) => p.replace(/<[^>]*>/g, "").trim())
|
|
462
|
+
.filter((p) => p.length > 0);
|
|
463
|
+
// parts[0] = " (лк) (1 - 16 нед.) " — trailing metadata; drop tokens that
|
|
464
|
+
// look like (type)/(weeks)/(N подгруппа). First real text line = teacher.
|
|
465
|
+
const textLines = [];
|
|
466
|
+
for (const p of parts) {
|
|
467
|
+
const cleaned = p
|
|
468
|
+
.replace(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/g, "")
|
|
469
|
+
.replace(/\([^)]*нед\.?[^)]*\)/g, "")
|
|
470
|
+
.replace(/\(\d+\s*подгруппа\)/g, "")
|
|
471
|
+
.trim();
|
|
472
|
+
if (cleaned)
|
|
473
|
+
textLines.push(cleaned);
|
|
474
|
+
}
|
|
475
|
+
const teacherLine = textLines[0] ?? "";
|
|
476
|
+
const groupsLine = textLines.slice(1).join(" ").trim();
|
|
477
|
+
return {
|
|
478
|
+
room: "",
|
|
479
|
+
subject,
|
|
480
|
+
type: typeMatch?.[1] ?? "",
|
|
481
|
+
weeks: parseWeeks(weeksMatch?.[1] ?? ""),
|
|
482
|
+
teacher: parseTeacher(teacherLine),
|
|
483
|
+
groups: groupsLine || undefined,
|
|
484
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
485
|
+
weekParity,
|
|
486
|
+
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
487
|
+
possibleChanges,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
export function parseAudienceFullSchedule(html) {
|
|
491
|
+
const doc = parseHtml(html);
|
|
492
|
+
return parseSemesterScheduleWith(doc, parseAudienceSemesterEntry);
|
|
493
|
+
}
|
|
347
494
|
// --- Teacher schedule parsing ---
|
|
348
495
|
export function parseTeacherFullSchedule(html, educationType) {
|
|
349
496
|
const doc = parseHtml(html);
|
|
@@ -497,5 +644,7 @@ export function parseTeacherInfo(html) {
|
|
|
497
644
|
const degree = degreeEl ? text(degreeEl).trim() : undefined;
|
|
498
645
|
const deptEl = doc.querySelector(".htext");
|
|
499
646
|
const department = deptEl ? text(deptEl).trim() : undefined;
|
|
500
|
-
|
|
647
|
+
const photoImg = doc.querySelector("#photosrc");
|
|
648
|
+
const photoUrl = photoImg?.getAttribute("src") || undefined;
|
|
649
|
+
return { name, degree: degree || undefined, department: department || undefined, photoUrl };
|
|
501
650
|
}
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -9,6 +9,25 @@ export interface Group {
|
|
|
9
9
|
specialty?: string;
|
|
10
10
|
profile?: string;
|
|
11
11
|
}
|
|
12
|
+
export interface Audience {
|
|
13
|
+
id: number;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
export interface AudienceInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
/** Building letter/name, e.g. "Б". */
|
|
19
|
+
building?: string;
|
|
20
|
+
/** Floor number, e.g. 3. */
|
|
21
|
+
floor?: number;
|
|
22
|
+
/** Free-form usage description, e.g. "Учебная лаборатория". */
|
|
23
|
+
usage?: string;
|
|
24
|
+
/** Relative URL of the audience photo (/index/audimage/...). */
|
|
25
|
+
audImageUrl?: string;
|
|
26
|
+
/** Relative URL of the building image (/index/blockimage/...). */
|
|
27
|
+
blockImageUrl?: string;
|
|
28
|
+
/** Relative URL of the floor plan image (/index/floorplan/...). */
|
|
29
|
+
floorplanUrl?: string;
|
|
30
|
+
}
|
|
12
31
|
/** A date-specific substitution (room and/or teacher change). */
|
|
13
32
|
export interface Substitution {
|
|
14
33
|
/** The date this substitution applies to. */
|
|
@@ -105,6 +124,8 @@ export interface TeacherInfo {
|
|
|
105
124
|
name: string;
|
|
106
125
|
degree?: string;
|
|
107
126
|
department?: string;
|
|
127
|
+
/** Relative photo URL (e.g. "/index/photo/tech/653/id/653"), or undefined if no photo. */
|
|
128
|
+
photoUrl?: string;
|
|
108
129
|
}
|
|
109
130
|
export interface SemesterWeek {
|
|
110
131
|
week: number;
|
|
@@ -116,6 +137,9 @@ export interface CacheConfig {
|
|
|
116
137
|
faculties?: number;
|
|
117
138
|
groups?: number;
|
|
118
139
|
teachers?: number;
|
|
140
|
+
teacherInfo?: number;
|
|
141
|
+
teacherPhotos?: number;
|
|
142
|
+
audienceInfo?: number;
|
|
119
143
|
}
|
|
120
144
|
export interface TtClientOptions {
|
|
121
145
|
educationType?: EducationType;
|