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 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";
@@ -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
- return parseTeacherInfo(body);
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
  }
@@ -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(/&nbsp;/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
- return { name, degree: degree || undefined, department: department || undefined };
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
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "2.6.2",
3
+ "version": "2.8.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",