zdu-student-api 1.1.9 → 1.1.11

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/audience.js CHANGED
@@ -74,6 +74,7 @@ export class Audience {
74
74
  this.dops.forEach((dop) => {
75
75
  dops += `&dop_${dop}=yes`;
76
76
  });
77
+ // TODO FIX MIN AND MAX. IF MIN = true & !MAX, MAX=999999999
77
78
  const response = await fetch(`https://dekanat.zu.edu.ua/cgi-bin/timetable_export.cgi?req_type=free_rooms_list&&block_name=${this.blockName !== undefined ? this.encodeCP1251(this.blockName) : ''}&size_min=${this.size_min !== undefined ? this.size_min : ''}&size_max=${this.size_max !== undefined ? this.size_max : ''}&room_type=${this.roomType !== undefined ? this.encodeCP1251(this.roomType) : ''}&begin_date=${date}&lesson=${this.lesson}${dops}&req_format=json&coding_mode=UTF8&bs=%D1%F4%EE%F0%EC%F3%E2%E0%F2%E8+%E7%E0%EF%E8%F2`);
78
79
  if (!response.ok)
79
80
  throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
@@ -2,6 +2,7 @@ import fetch from 'cross-fetch';
2
2
  import iconv from 'iconv-lite';
3
3
  import { generateCookieString, isLoginPage } from './session.js';
4
4
  import { parseDataPageN3, parseDataPageN31, parseTeacherData } from './parsers.js';
5
+ import { Buffer } from 'buffer';
5
6
  /**
6
7
  * Отримати анкетні дані студента
7
8
  * @category CabinetTeacher
@@ -1,5 +1,6 @@
1
1
  import fetch from 'cross-fetch';
2
2
  import iconv from 'iconv-lite';
3
+ import { Buffer } from 'buffer';
3
4
  /**
4
5
  * Отримати sesID та sessGUID користувача
5
6
  * @category Cabinet
@@ -51,9 +51,10 @@ export declare class CabinetStudent {
51
51
  */
52
52
  auth(login?: string, password?: string): Promise<boolean>;
53
53
  /**
54
- * Базове значення семестру
54
+ * Встановити семестр
55
+ * @param password - Семестр в форматі рядка або числа, якщо не передавати згенерується ймовірний семестр
55
56
  */
56
- private setSemester;
57
+ setSemester(semester?: 1 | 2 | '1' | '2'): void;
57
58
  /**
58
59
  * Отримання всіх данних
59
60
  */
@@ -67,10 +67,18 @@ export class CabinetStudent {
67
67
  return false;
68
68
  }
69
69
  /**
70
- * Базове значення семестру
71
- */
72
- setSemester() {
73
- this.semester = getSemester();
70
+ * Встановити семестр
71
+ * @param password - Семестр в форматі рядка або числа, якщо не передавати згенерується ймовірний семестр
72
+ */
73
+ setSemester(semester) {
74
+ if (!semester)
75
+ this.semester = getSemester();
76
+ else {
77
+ if (semester === '1' || semester === 1)
78
+ this.semester = 1;
79
+ if (semester === '2' || semester === 2)
80
+ this.semester = 2;
81
+ }
74
82
  }
75
83
  /**
76
84
  * Отримання всіх данних
@@ -2,6 +2,7 @@ import fetch from 'cross-fetch';
2
2
  import iconv from 'iconv-lite';
3
3
  import { parseDisciplinesPageN6, parseDisciplinesPageN7 } from '../cabinet/parsers.js';
4
4
  import { generateCookieString, isLoginPage } from '../cabinet/session.js';
5
+ import { Buffer } from 'buffer';
5
6
  /**
6
7
  * Отримати всі дисципліни студента
7
8
  * @category CabinetStudent
@@ -1,6 +1,7 @@
1
1
  import fetch from 'cross-fetch';
2
2
  import iconv from 'iconv-lite';
3
3
  import { generateCookieString, isLoginPage } from '../cabinet/session.js';
4
+ import { Buffer } from 'buffer';
4
5
  /**
5
6
  * Отримати оцінки вибраного предмета студента
6
7
  * @category CabinetStudent
@@ -34,7 +35,7 @@ export async function getScores(sesId, sessGUID, prId, semester) {
34
35
  if (isLoginPage(html1))
35
36
  return result;
36
37
  result.scheduleItem = parseSchedule(html1);
37
- result.studentScores = parseScores(html1);
38
+ result.studentScores = parseScores(html1, result.scheduleItem.length);
38
39
  result.studentId = result.studentScores[0]?.id;
39
40
  if (result.studentScores.length > 1) {
40
41
  result.studentScores.sort((a, b) => a.id.localeCompare(b.id));
@@ -51,38 +52,140 @@ export async function getScores(sesId, sessGUID, prId, semester) {
51
52
  * Витягує список пар з HTML
52
53
  */
53
54
  function parseSchedule(html) {
54
- let idx = html.indexOf('<th></th>');
55
- html = idx === -1 ? html : html.slice(idx + '<th></th>'.length);
56
- idx = html.indexOf('<th class="dh">');
57
- html = idx === -1 ? html : html.slice(0, idx);
58
- const regex = /<th[^>]*data-hth="([^"]+)"[^>]*>[\s\S]*?<a[^>]*data-ind="([^"]+)"[^>]*>[^<]+<\/a>[\s\S]*?<br>([^<]+)<\/th>/g;
55
+ let idx = html.indexOf('<thead>');
56
+ if (idx === -1)
57
+ return [];
58
+ let theadEnd = html.indexOf('</thead>', idx);
59
+ if (theadEnd === -1)
60
+ return [];
61
+ let theadContent = html.slice(idx, theadEnd);
62
+ let firstTrStart = theadContent.indexOf('<tr>');
63
+ let firstTrEnd = theadContent.indexOf('</tr>', firstTrStart);
64
+ if (firstTrStart === -1 || firstTrEnd === -1)
65
+ return [];
66
+ let firstRow = theadContent.slice(firstTrStart, firstTrEnd);
67
+ let secondTrStart = theadContent.indexOf('<tr>', firstTrEnd);
68
+ let secondTrEnd = theadContent.indexOf('</tr>', secondTrStart);
69
+ let thirdTrStart = theadContent.indexOf('<tr>', secondTrEnd);
70
+ let thirdTrEnd = theadContent.indexOf('</tr>', thirdTrStart);
71
+ if (thirdTrStart === -1 || thirdTrEnd === -1)
72
+ return [];
73
+ let thirdRow = theadContent.slice(thirdTrStart, thirdTrEnd);
59
74
  const result = [];
75
+ const thStartRegex = /<th\b/g;
76
+ const positions = [];
60
77
  let match;
61
- while ((match = regex.exec(html)) !== null) {
62
- const [_, dataHth, index, time] = match;
63
- const parts = dataHth.split(',');
64
- const teacher = parts[0].trim();
65
- const dateType = parts[1]?.trim().split(' ') || [];
66
- const date = dateType[0] || '';
67
- const type = dateType[1] || '';
68
- result.push({ teacher, date, type, time: time.trim(), index });
78
+ while ((match = thStartRegex.exec(firstRow)) !== null) {
79
+ positions.push(match.index);
80
+ }
81
+ positions.push(firstRow.length);
82
+ const thirdPositions = [];
83
+ const thirdThStartRegex = /<th\b/g;
84
+ while ((match = thirdThStartRegex.exec(thirdRow)) !== null) {
85
+ thirdPositions.push(match.index);
86
+ }
87
+ thirdPositions.push(thirdRow.length);
88
+ for (let i = 0; i < positions.length - 1; i++) {
89
+ const start = positions[i];
90
+ const end = positions[i + 1];
91
+ const thFull = firstRow.slice(start, end);
92
+ const closeIdx = thFull.indexOf('</th>');
93
+ if (closeIdx === -1)
94
+ continue;
95
+ const thTag = thFull.slice(0, closeIdx + 5);
96
+ const thContent = thTag.replace(/<th[^>]*>/, '').replace(/<\/th>/, '');
97
+ if (!thContent.trim())
98
+ continue;
99
+ let moduleMatch = thContent.match(/^(М\d+)<br>/);
100
+ if (moduleMatch) {
101
+ const moduleNum = moduleMatch[1];
102
+ const timeMatch = thContent.match(/<br[^>]*>\s*(\d{2}:\d{2}-\d{2}:\d{2})/);
103
+ const time = timeMatch ? timeMatch[1].trim() : '';
104
+ result.push({
105
+ teacher: undefined,
106
+ date: undefined,
107
+ type: 'МД',
108
+ time: time,
109
+ index: `module_${moduleNum}`,
110
+ description: `Модуль ${moduleNum}`,
111
+ });
112
+ continue;
113
+ }
114
+ if (thContent.match(/Підсум\.|Всього|Невиправд\./)) {
115
+ continue;
116
+ }
117
+ const dataHthMatch = thTag.match(/data-hth="([^"]+)"/);
118
+ if (!dataHthMatch)
119
+ continue;
120
+ const dataHth = dataHthMatch[1];
121
+ const aMatch = thContent.match(/data-ind="([^"]+)"/);
122
+ if (!aMatch)
123
+ continue;
124
+ const index = aMatch[1];
125
+ const dateMatch = thContent.match(/>(\d{2}\.\d{2}\.\d{4})</);
126
+ const date = dateMatch ? dateMatch[1] : '';
127
+ const timeMatch = thContent.match(/<br[^>]*>\s*(\d{2}:\d{2}-\d{2}:\d{2})/);
128
+ const time = timeMatch ? timeMatch[1].trim() : '';
129
+ const parts = dataHth.split(',').map((p) => p.trim());
130
+ const teacher = parts[0] || '';
131
+ let type = '';
132
+ if (parts[1]) {
133
+ const dateTypeParts = parts[1].split(/\s+/);
134
+ type = dateTypeParts[1] || '';
135
+ }
136
+ result.push({ teacher, date, type, time, index });
137
+ }
138
+ const descriptions = parseDescriptions(html);
139
+ for (const item of result) {
140
+ if (item.index && !item.index.startsWith('module_') && descriptions.has(item.index)) {
141
+ item.description = descriptions.get(item.index);
142
+ }
69
143
  }
70
144
  return result;
71
145
  }
146
+ /**
147
+ * Витягує описи занять з HTML
148
+ */
149
+ function parseDescriptions(html) {
150
+ const descriptions = new Map();
151
+ const divRegex = /<div\s+id="r(\d+)"\s+class="hidden">\s*([\s\S]*?)\s*<br>/g;
152
+ let match;
153
+ while ((match = divRegex.exec(html)) !== null) {
154
+ const index = match[1];
155
+ const content = match[2].trim();
156
+ if (content && !content.startsWith('<')) {
157
+ descriptions.set(index, content);
158
+ }
159
+ }
160
+ return descriptions;
161
+ }
72
162
  /**
73
163
  * Витягує список оцінок з HTML
74
164
  */
75
- function parseScores(html) {
165
+ function parseScores(html, expectedScoresCount) {
76
166
  const studentRows = html.match(/<tr\s+[^>]*id="s\d+"[\s\S]*?<\/tr>/g) || [];
77
167
  const result = [];
78
168
  for (const row of studentRows) {
79
169
  const idMatch = row.match(/id="(s\d+)"/);
80
170
  const id = idMatch ? idMatch[1] : '';
81
- const tdMatches = [...row.matchAll(/<td[^>]*data-item="(\d+)"[^>]*>([\s\S]*?)<\/td>/g)];
171
+ const allTds = [...row.matchAll(/<td\b[^>]*>([\s\S]*?)<\/td>/g)];
82
172
  const scoresMap = new Map();
83
- for (const td of tdMatches) {
84
- const dataItem = Number(td[1]);
85
- const score = td[2].trim() || '';
173
+ for (const tdMatch of allTds) {
174
+ const fullTd = tdMatch[0];
175
+ const tdContent = tdMatch[1].trim();
176
+ const dataItemMatch = fullTd.match(/data-item="(\d+)"/);
177
+ if (!dataItemMatch)
178
+ continue;
179
+ const dataItem = Number(dataItemMatch[1]);
180
+ if (fullTd.match(/class="[^"]*\bf\s+f1\b[^"]*"/) ||
181
+ fullTd.match(/class="[^"]*\bf1\b[^"]*"/)) {
182
+ continue;
183
+ }
184
+ if (fullTd.match(/class="[^"]*\bf\s+f2\b[^"]*"/) ||
185
+ fullTd.match(/class="[^"]*\bf2\b[^"]*"/)) {
186
+ continue;
187
+ }
188
+ const score = tdContent || '';
86
189
  if (!scoresMap.has(dataItem)) {
87
190
  scoresMap.set(dataItem, [score]);
88
191
  }
@@ -90,11 +193,13 @@ function parseScores(html) {
90
193
  scoresMap.get(dataItem).push(score);
91
194
  }
92
195
  }
93
- const scores = Array.from(scoresMap.values());
94
- scores.shift();
95
- const finalMatch = row.match(/<td[^>]*class="f f1"[^>]*>([\s\S]*?)<\/td>/);
196
+ const sortedItems = Array.from(scoresMap.entries()).sort((a, b) => a[0] - b[0]);
197
+ const scores = sortedItems.map(([_, values]) => values);
198
+ const finalMatch = row.match(/<td[^>]*class="[^"]*\bf\s+f1\b[^"]*"[^>]*>([\s\S]*?)<\/td>/);
96
199
  const finalScore = finalMatch ? finalMatch[1].trim() : '';
97
- const absenceMatches = [...row.matchAll(/<td[^>]*class="f f2"[^>]*>([\s\S]*?)<\/td>/g)];
200
+ const absenceMatches = [
201
+ ...row.matchAll(/<td[^>]*class="[^"]*\bf\s+f2\b[^"]*"[^>]*>([\s\S]*?)<\/td>/g),
202
+ ];
98
203
  const absences = absenceMatches[0] ? Number(absenceMatches[0][1].trim() || 0) : 0;
99
204
  const uabsences = absenceMatches[1] ? Number(absenceMatches[1][1].trim() || 0) : 0;
100
205
  result.push({ id, scores, absences, uabsences, finalScore });
@@ -90,18 +90,20 @@ export interface Disciplines {
90
90
  * Опис заняття у кабінеті студента
91
91
  * @category CabinetStudent
92
92
  * @remarks
93
- * - `teacher` — Прізвище та ініціали викладача
94
- * - `date` — Дата заняття у форматі "dd.mm.yyyy"
95
- * - `type` — Тип заняття: "Лек", "ПрСем", "Лаб", "МК", "Екз"
93
+ * - `teacher` — Прізвище та ініціали викладача (undefined для модульних днів)
94
+ * - `date` — Дата заняття у форматі "dd.mm.yyyy" (undefined для модульних днів)
95
+ * - `type` — Тип заняття: "Лек", "ПрСем", "Лаб", "МК", "Екз", "МД"
96
96
  * - `time` — Час заняття у форматі "HH:MM-HH:MM"
97
97
  * - `index` — Значення атрибута `data-ind`, унікальний ідентифікатор
98
+ * - `description` — Опис заняття (може бути undefined) ("Модуль ##" для модульних днів, де ## - номер модуля, наприклад М0, М1)
98
99
  */
99
100
  export interface ScheduleItem {
100
- teacher: string;
101
- date: string;
101
+ teacher?: string;
102
+ date?: string;
102
103
  type: string;
103
104
  time: string;
104
105
  index: string;
106
+ description?: string;
105
107
  }
106
108
  /**
107
109
  * Список оцінок студента.
@@ -2,6 +2,7 @@ import fetch from 'cross-fetch';
2
2
  import iconv from 'iconv-lite';
3
3
  import { generateCookieString, isLoginPage } from '../cabinet/session.js';
4
4
  import { parseGroupsPage } from '../cabinet/parsers.js';
5
+ import { Buffer } from 'buffer';
5
6
  /**
6
7
  * Отримати всі академічні групи
7
8
  * @category CabinetTeacher
package/dist/examples.js CHANGED
@@ -1,4 +1,3 @@
1
- import { CabinetStudent, } from './index.js';
2
1
  import 'dotenv/config';
3
2
  // const schedule = new Schedule();
4
3
  // schedule.group = '23Бд-СОінф123'
@@ -28,10 +27,16 @@ import 'dotenv/config';
28
27
  // console.log(await getTeachers('Кривонос Олександр'));
29
28
  // console.log(await getRooms('319'));
30
29
  // const audience = new Audience();
31
- // audience.blockName = "гуртож №3"
30
+ // // audience.blockName = 'гуртож №3';
32
31
  // try {
32
+ // audience.roomType = 'Комп. клас';
33
+ // audience.blockName = '№1';
34
+ // const [day, month, year] = '09.02.2026'.split('.');
35
+ // audience.roomsDate = new Date(+year, +month - 1, +day + 1);
36
+ // console.log(audience.roomsDate);
37
+ // audience.lesson = 5;
33
38
  // const audiences = await audience.getAudience();
34
- // console.log("Аудиторії:", audiences);
39
+ // console.log('Аудиторії:', audiences);
35
40
  // } catch (err: any) {
36
41
  // console.error(err.message);
37
42
  // }
@@ -53,13 +58,14 @@ import 'dotenv/config';
53
58
  // const me = data2.studentScores.find((s) => s.id === data2.studentId)!;
54
59
  // const sesID = '';
55
60
  // const sessGUID = '';
56
- const cb = new CabinetStudent(process.env.LOGIN, process.env.PASSWORD);
57
- await cb.auth();
58
- // // console.log(cb.sesID, cb.sessGUID);
59
- // console.log(await cb.setSession(sesID, sessGUID));
60
- // // console.log(cb.sesID, cb.sessGUID);
61
- // console.log(await cb.loadData());
62
- console.log(await cb.getData());
61
+ // const cb = new CabinetStudent(process.env.LOGIN!, process.env.PASSWORD!);
62
+ // await cb.auth();
63
+ // // // console.log(cb.sesID, cb.sessGUID);
64
+ // // console.log(await cb.setSession(sesID, sessGUID));
65
+ // // // console.log(cb.sesID, cb.sessGUID);
66
+ // await cb.loadData();
67
+ // const scores = cb.allScores;
68
+ // await writeFile('scores.json', JSON.stringify(scores, null, 2), 'utf-8');
63
69
  // console.log(cb.data);
64
70
  // await cb.getDisciplines();
65
71
  // console.log(await cb.getId());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdu-student-api",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "API client for ZDU student services",
5
5
  "author": "Nicita3 <ni596157@gmail.com> (https://github.com/Nicita-3)",
6
6
  "main": "dist/index.js",
@@ -22,16 +22,16 @@
22
22
  },
23
23
  "homepage": "https://nicita-3.github.io/zdu-student-api",
24
24
  "devDependencies": {
25
- "@types/node": "^24.10.1",
26
- "prettier": "^3.7.4",
25
+ "@types/node": "^24.12.2",
26
+ "prettier": "^3.8.1",
27
27
  "ts-node": "^10.9.2",
28
- "typedoc": "^0.28.14",
28
+ "typedoc": "^0.28.18",
29
29
  "typescript": "^5.9.3"
30
30
  },
31
31
  "dependencies": {
32
32
  "buffer": "^6.0.3",
33
33
  "cross-fetch": "^4.1.0",
34
- "dotenv": "^17.2.3",
35
- "iconv-lite": "^0.7.0"
34
+ "dotenv": "^17.4.1",
35
+ "iconv-lite": "^0.7.2"
36
36
  }
37
37
  }