reddy-api-srm 1.0.7 → 1.0.9

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.
@@ -62,7 +62,7 @@ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
62
62
  ? { "x-zcsrf-token": `iamcsrcoo=${csrfToken}` }
63
63
  : {}),
64
64
  Referer:
65
- "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
65
+ "https://academia.srmist.edu.in/accounts/p/40-10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
66
66
  "Referrer-Policy": "strict-origin-when-cross-origin",
67
67
  },
68
68
  body: body.toString(),
@@ -100,7 +100,7 @@ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
100
100
  },
101
101
  });
102
102
 
103
- if (res2.ok || res2.status === 200 || res2.status === 204) {
103
+ if (res2.ok || res2.status === 200 || res2.status === 202 || res2.status === 204) {
104
104
  let data = null;
105
105
  try { data = await res2.json(); } catch { /* 204 No Content is fine */ }
106
106
  return { success: true, status: res2.status, strategy: "webclient-delete", data };
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validatePassword = validatePassword;
4
4
  async function validatePassword({ identifier, digest, password, }) {
5
+ let res;
5
6
  try {
6
- const res = await fetch(`https://academia.srmist.edu.in/accounts/p/40-10002227248/signin/v2/primary/${identifier}/password?digest=${digest}&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`, {
7
+ res = await fetch(`https://academia.srmist.edu.in/accounts/p/40-10002227248/signin/v2/primary/${identifier}/password?digest=${digest}&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`, {
7
8
  headers: {
8
9
  accept: "*/*",
9
10
  "accept-language": "en-US,en;q=0.9",
@@ -11,22 +12,39 @@ async function validatePassword({ identifier, digest, password, }) {
11
12
  "sec-fetch-dest": "empty",
12
13
  "sec-fetch-mode": "cors",
13
14
  "sec-fetch-site": "same-origin",
14
- "x-zcsrf-token": "iamcsrcoo=fae2d8fa-e5a1-4cb0-a5ee-cc40af87e89f",
15
- cookie: "zalb_74c3a1eecc=4cad43ac9848cc7edd20d2313fcde774; zccpn=a6fa7bc8-11c7-44ad-8be8-0aa6b04fad8a; JSESSIONID=3BD0053672AF3D628D983A15AA469D07; cli_rgn=IN; _ga=GA1.3.2061081340.1748689001; _gid=GA1.3.1677956689.1748689001; _ga_HQWPLLNMKY=GS2.3.s1748689001$o1$g0$t1748689001$j60$l0$h0; zalb_f0e8db9d3d=7ad3232c36fdd9cc324fb86c2c0a58ad; iamcsr=fae2d8fa-e5a1-4cb0-a5ee-cc40af87e89f; _zcsr_tmp=fae2d8fa-e5a1-4cb0-a5ee-cc40af87e89f; stk=d6559e9a58e77dbea9e24adf3bb57941",
16
- Referer: "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&css_url=/49910842/academia-academic-services/downloadPortalCustomCss/login&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
15
+ Referer: "https://academia.srmist.edu.in/accounts/p/40-10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
17
16
  "Referrer-Policy": "strict-origin-when-cross-origin",
18
17
  },
19
18
  body: `{"passwordauth":{"password":"${password}"}}`,
20
19
  method: "POST",
21
20
  });
22
- const response = (await res.json());
21
+ // ── Detect redirect to Zoho sessions-reminder page ───────────────────────
22
+ if (res.redirected && res.url && res.url.includes("sessions-reminder")) {
23
+ let flowId = null;
24
+ try { const u = new URL(res.url); flowId = u.searchParams.get("flowId") ?? u.searchParams.get("flow_id") ?? null; } catch (_) {}
25
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached. Please terminate existing sessions to continue.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId }, isAuthenticated: false };
26
+ }
27
+ // Read body as text first — safely handles non-JSON responses
28
+ const bodyText = await res.text();
29
+ let response;
30
+ try {
31
+ response = JSON.parse(bodyText);
32
+ } catch (_) {
33
+ const lower = bodyText.toLowerCase();
34
+ if (lower.includes("sessions-reminder") || (lower.includes("concurrent") && lower.includes("session"))) {
35
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached. Please terminate existing sessions to continue.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId: null }, isAuthenticated: false };
36
+ }
37
+ return { error: "Internal Server Error", errorReason: new Error("Non-JSON response from login endpoint") };
38
+ }
23
39
  if (response.status_code === 201) {
24
- // Use filterCookies to process set-cookie header
25
- const setCookieHeader = res.headers.get("set-cookie");
26
- if (!setCookieHeader)
40
+ const setCookieHeaders = (typeof res.headers.getSetCookie === "function"
41
+ ? res.headers.getSetCookie()
42
+ : [res.headers.get("set-cookie")].filter(Boolean));
43
+ if (!setCookieHeaders.length)
27
44
  throw new Error("Couldn't able to get cookie from response header ");
45
+ const combinedCookieHeader = setCookieHeaders.join("; ");
28
46
  const matches = [
29
- ...setCookieHeader.matchAll(/(_(?:iamadt|iambdt)_client_\d+|_z_identity)=[^;]+/g),
47
+ ...combinedCookieHeader.matchAll(/(_(?:iamadt|iambdt)_client_\d+|_z_identity)=[^;]+/g),
30
48
  ];
31
49
  const extractedCookies = matches.map((m) => m[0]).join("; ") + ";";
32
50
  const data = {
@@ -36,9 +54,6 @@ async function validatePassword({ identifier, digest, password, }) {
36
54
  return { data, isAuthenticated: true };
37
55
  }
38
56
  // ── Concurrent / device-limit detection ──────────────────────────────────
39
- // Zoho returns status_code 435 when the concurrent session limit is hit.
40
- // The response also embeds a flowId used to call the terminate endpoint.
41
- // Field names seen in Zoho responses: flowId, flow_id, concurrent_flow_id
42
57
  const msg = (response.localized_message ?? response.message ?? "").toLowerCase();
43
58
  const isConcurrentLimit =
44
59
  response.status_code === 435 ||
@@ -46,17 +61,16 @@ async function validatePassword({ identifier, digest, password, }) {
46
61
  msg.includes("maximum number") ||
47
62
  msg.includes("device limit") ||
48
63
  msg.includes("already logged") ||
64
+ msg.includes("sessions") ||
49
65
  !!response.flowId ||
50
66
  !!response.flow_id ||
51
67
  !!response.concurrent_flow_id;
52
- // Extract the flow identifier — try all known field names
53
68
  const flowId =
54
69
  response.flowId ??
55
70
  response.flow_id ??
56
71
  response.concurrent_flow_id ??
57
72
  response.login_flow_id ??
58
73
  null;
59
- // ─────────────────────────────────────────────────────────────────────────
60
74
  const captchaRequired = msg.includes("captcha");
61
75
  const data = {
62
76
  statusCode: response.status_code,
@@ -64,14 +78,16 @@ async function validatePassword({ identifier, digest, password, }) {
64
78
  captcha: captchaRequired
65
79
  ? { required: true, digest: response.cdigest }
66
80
  : { required: false, digest: null },
67
- // NEW: concurrent session fields
68
81
  isConcurrentLimit,
69
82
  flowId,
70
83
  };
71
84
  return { data, isAuthenticated: false };
72
85
  }
73
86
  catch (e) {
87
+ if (res && res.redirected && res.url && res.url.includes("sessions-reminder")) {
88
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId: null }, isAuthenticated: false };
89
+ }
74
90
  return { error: "Internal Server Error", errorReason: e };
75
91
  }
76
92
  }
77
- //# sourceMappingURL=validatePassword.js.map
93
+ //# sourceMappingURL=validatePassword.js.map
@@ -8,15 +8,10 @@ async function validateUser(username) {
8
8
  accept: "*/*",
9
9
  "accept-language": "en-US,en;q=0.9",
10
10
  "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
11
- "sec-ch-ua": '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
12
- "sec-ch-ua-mobile": "?1",
13
- "sec-ch-ua-platform": '"Android"',
14
11
  "sec-fetch-dest": "empty",
15
12
  "sec-fetch-mode": "cors",
16
13
  "sec-fetch-site": "same-origin",
17
- "x-zcsrf-token": "iamcsrcoo=3dea6395-0540-44ea-8de7-544256dd7549",
18
- cookie: "zalb_74c3a1eecc=50830239914cba1225506e915a665a91; zccpn=68703e7d-ccf0-42ba-92b2-9c87c7a0c8ae; JSESSIONID=739937C3826C1F58C37186170B4F4B36; cli_rgn=IN; _ga=GA1.3.1846200817.1748679237; _gid=GA1.3.734795940.1748679237; _gat=1; _ga_HQWPLLNMKY=GS2.3.s1748679237$o1$g0$t1748679237$j60$l0$h0; zalb_f0e8db9d3d=983d6a65b2f29022f18db52385bfc639; iamcsr=3dea6395-0540-44ea-8de7-544256dd7549; _zcsr_tmp=3dea6395-0540-44ea-8de7-544256dd7549; stk=4ec13d42454007681bd4337cf126baec",
19
- Referer: "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&css_url=/49910842/academia-academic-services/downloadPortalCustomCss/login&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
14
+ Referer: "https://academia.srmist.edu.in/accounts/p/40-10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
20
15
  "Referrer-Policy": "strict-origin-when-cross-origin",
21
16
  },
22
17
  body: `mode=primary&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`,
@@ -39,4 +34,4 @@ async function validateUser(username) {
39
34
  };
40
35
  }
41
36
  }
42
- //# sourceMappingURL=validateUser.js.map
37
+ //# sourceMappingURL=validateUser.js.map
@@ -25,18 +25,14 @@ async function fetchAttendance(cookie) {
25
25
  return request.data;
26
26
  }
27
27
  catch (error) {
28
- if (error &&
29
- typeof error === "object" &&
30
- "status" in error &&
31
- error.status === 500) {
28
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
29
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
32
30
  return { error: "Unauthorized", status: 401 };
33
31
  }
34
32
  return {
35
33
  error: error instanceof Error ? error.message : "Failed to fetch attendance",
36
- status: error && typeof error === "object" && "status" in error
37
- ? error.status
38
- : 500,
34
+ status: statusCode ?? 500,
39
35
  };
40
36
  }
41
37
  }
42
- //# sourceMappingURL=fetchAttendance.js.map
38
+ //# sourceMappingURL=fetchAttendance.js.map
@@ -10,6 +10,7 @@ async function fetchCalendar(cookie) {
10
10
  const url = await (0, dynamicUrl_1.calendarDynamicUrl)();
11
11
  try {
12
12
  const response = await (0, axios_1.default)(url, {
13
+ responseType: "text",
13
14
  headers: {
14
15
  accept: "*/*",
15
16
  "accept-language": "en-US,en;q=0.9",
@@ -29,18 +30,14 @@ async function fetchCalendar(cookie) {
29
30
  return response.data;
30
31
  }
31
32
  catch (error) {
32
- if (error &&
33
- typeof error === "object" &&
34
- "status" in error &&
35
- error.status === 500) {
33
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
34
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
36
35
  return { error: "Unauthorized", status: 401 };
37
36
  }
38
37
  return {
39
38
  error: error instanceof Error ? error.message : "Failed to fetch calendar",
40
- status: error && typeof error === "object" && "status" in error
41
- ? error.status
42
- : 500,
39
+ status: statusCode ?? 500,
43
40
  };
44
41
  }
45
42
  }
46
- //# sourceMappingURL=fetchCalender.js.map
43
+ //# sourceMappingURL=fetchCalender.js.map
@@ -30,20 +30,16 @@ async function fetchCourseDetails(cookie) {
30
30
  return request.data;
31
31
  }
32
32
  catch (error) {
33
- if (error &&
34
- typeof error === "object" &&
35
- "status" in error &&
36
- error.status === 500) {
33
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
34
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
37
35
  return { error: "Unauthorized", status: 401 };
38
36
  }
39
37
  return {
40
38
  error: error instanceof Error
41
39
  ? error.message
42
40
  : "Failed to fetch course details",
43
- status: error && typeof error === "object" && "status" in error
44
- ? error.status
45
- : 500,
41
+ status: statusCode ?? 500,
46
42
  };
47
43
  }
48
44
  }
49
- //# sourceMappingURL=fetchCourseDetails.js.map
45
+ //# sourceMappingURL=fetchCourseDetails.js.map
@@ -25,18 +25,14 @@ async function fetchDayOrder(cookie) {
25
25
  return request.data;
26
26
  }
27
27
  catch (error) {
28
- if (error &&
29
- typeof error === "object" &&
30
- "status" in error &&
31
- error.status === 500) {
28
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
29
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
32
30
  return { error: "Unauthorized", status: 401 };
33
31
  }
34
32
  return {
35
33
  error: error instanceof Error ? error.message : "Failed to fetch attendance",
36
- status: error && typeof error === "object" && "status" in error
37
- ? error.status
38
- : 500,
34
+ status: statusCode ?? 500,
39
35
  };
40
36
  }
41
37
  }
42
- //# sourceMappingURL=fetchDayOrder.js.map
38
+ //# sourceMappingURL=fetchDayOrder.js.map
@@ -25,18 +25,14 @@ async function fetchMarks(cookie) {
25
25
  return request.data;
26
26
  }
27
27
  catch (error) {
28
- if (error &&
29
- typeof error === "object" &&
30
- "status" in error &&
31
- error.status === 500) {
28
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
29
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
32
30
  return { error: "Unauthorized", status: 401 };
33
31
  }
34
32
  return {
35
33
  error: error instanceof Error ? error.message : "Failed to fetch attendance",
36
- status: error && typeof error === "object" && "status" in error
37
- ? error.status
38
- : 500,
34
+ status: statusCode ?? 500,
39
35
  };
40
36
  }
41
37
  }
42
- //# sourceMappingURL=fetchMarks.js.map
38
+ //# sourceMappingURL=fetchMarks.js.map
@@ -30,20 +30,16 @@ async function fetchTimetable(cookie) {
30
30
  return request.data;
31
31
  }
32
32
  catch (error) {
33
- if (error &&
34
- typeof error === "object" &&
35
- "status" in error &&
36
- error.status === 500) {
33
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
34
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
37
35
  return { error: "Unauthorized", status: 401 };
38
36
  }
39
37
  return {
40
38
  error: error instanceof Error
41
39
  ? error.message
42
40
  : "Failed to fetch course details",
43
- status: error && typeof error === "object" && "status" in error
44
- ? error.status
45
- : 500,
41
+ status: statusCode ?? 500,
46
42
  };
47
43
  }
48
44
  }
49
- //# sourceMappingURL=fetchTimetable.js.map
45
+ //# sourceMappingURL=fetchTimetable.js.map
@@ -5,9 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.fetchUserInfo = fetchUserInfo;
7
7
  const axios_1 = __importDefault(require("axios"));
8
+ const dynamicUrl_1 = require("../../utils/dynamicUrl");
8
9
  async function fetchUserInfo(cookie) {
10
+ const url = await (0, dynamicUrl_1.courseDynamicUrl)();
9
11
  try {
10
- const request = await (0, axios_1.default)("https://academia.srmist.edu.in/srm_university/academia-academic-services/page/My_Time_Table_2023_24", {
12
+ const request = await (0, axios_1.default)(url, {
11
13
  headers: {
12
14
  accept: "*/*",
13
15
  "accept-language": "en-US,en;q=0.9",
@@ -25,19 +27,14 @@ async function fetchUserInfo(cookie) {
25
27
  return request.data;
26
28
  }
27
29
  catch (error) {
28
- if (error &&
29
- typeof error === "object" &&
30
- "status" in error &&
31
- error.status === 500) {
30
+ const statusCode = axios_1.default.isAxiosError(error) ? error.response?.status : undefined;
31
+ if (statusCode === 401 || statusCode === 403 || statusCode === 500) {
32
32
  return { error: "Unauthorized", status: 401 };
33
33
  }
34
- // Handle other errors
35
34
  return {
36
35
  error: error instanceof Error ? error.message : "Failed to fetch user info",
37
- status: error && typeof error === "object" && "status" in error
38
- ? error.status
39
- : 500,
36
+ status: statusCode ?? 500,
40
37
  };
41
38
  }
42
39
  }
43
- //# sourceMappingURL=fetchUserInfo.js.map
40
+ //# sourceMappingURL=fetchUserInfo.js.map
package/dist/src/index.js CHANGED
@@ -42,8 +42,8 @@ async function logoutUser(cookie) {
42
42
  return await (0, fetchLogout_1.fetchLogout)(cookie);
43
43
  }
44
44
  // Terminate concurrent sessions (2-device limit bypass)
45
- async function terminateSessions({ flowId, identifier, digest }) {
46
- return await (0, terminateSessions_1.terminateSessions)({ flowId, identifier, digest });
45
+ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
46
+ return await (0, terminateSessions_1.terminateSessions)({ flowId, identifier, digest, csrfToken });
47
47
  }
48
48
  // Get TimeTable
49
49
  async function getTimetable(cookie) {
@@ -115,4 +115,4 @@ async function getCourse(cookie) {
115
115
  return { error: parse.error, status: parse.status };
116
116
  return parse;
117
117
  }
118
- //# sourceMappingURL=index.js.map
118
+ //# sourceMappingURL=index.js.map
@@ -37,92 +37,128 @@ exports.parseCalendar = parseCalendar;
37
37
  const cheerio = __importStar(require("cheerio"));
38
38
  async function parseCalendar(response) {
39
39
  try {
40
- let htmlContent = null;
41
- // Method 1: zmlvalue approach (Zoho Page Builder pages)
42
- const $outer = cheerio.load(response);
43
- const zmlValue = $outer("div.zc-pb-embed-placeholder-content").attr("zmlvalue");
44
- if (zmlValue) {
45
- htmlContent = zmlValue;
40
+ // ── Step 1: Normalise response to a string ─────────────────────────────
41
+ let rawResponse;
42
+ if (typeof response === "string") {
43
+ rawResponse = response;
44
+ } else if (response !== null && response !== undefined && typeof response === "object") {
45
+ // Axios may auto-parse JSON — try common field names that could hold HTML
46
+ const candidate = response.data ?? response.html ?? response.content ??
47
+ response.body ?? response.result ?? response.page ?? response.pageContent;
48
+ rawResponse = typeof candidate === "string" ? candidate : JSON.stringify(response);
49
+ } else {
50
+ rawResponse = String(response ?? "");
46
51
  }
47
- // Method 2: pageSanitizer.sanitize approach (Zoho Report pages)
52
+ // ── Step 2: Extract the HTML content ───────────────────────────────────
53
+ let htmlContent = null;
54
+ // Method 1: Zoho Page Builder embed (zmlvalue attribute)
55
+ try {
56
+ const $outer = cheerio.load(rawResponse);
57
+ const zmlEl = $outer("div.zc-pb-embed-placeholder-content");
58
+ if (zmlEl.length > 0) {
59
+ const zmlValue = zmlEl.attr("zmlvalue");
60
+ if (zmlValue && zmlValue.length > 50) htmlContent = zmlValue;
61
+ }
62
+ } catch (_) { /* ignore */ }
63
+ // Method 2: pageSanitizer.sanitize('...') — Zoho XHR page/report response
48
64
  if (!htmlContent) {
49
- const match = response.match(/pageSanitizer\.sanitize\('(.*)'\);/s);
65
+ const match = rawResponse.match(/pageSanitizer\.sanitize\s*\(\s*['"](.+?)['"]\s*\)/s);
50
66
  if (match && match[1]) {
51
67
  htmlContent = match[1]
52
68
  .replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
53
- .replace(/\\\\/g, "")
54
- .replace(/\\'/g, "'");
69
+ .replace(/\\\\/g, "\\")
70
+ .replace(/\\'/g, "'")
71
+ .replace(/\\"/g, '"')
72
+ .replace(/\\n/g, "\n")
73
+ .replace(/\\t/g, "\t");
55
74
  }
56
75
  }
76
+ // Method 3: Response already contains table HTML
77
+ if (!htmlContent && rawResponse.includes("<table") && rawResponse.includes("<tr")) {
78
+ htmlContent = rawResponse;
79
+ }
80
+ // Method 4: JSON body with embedded HTML
81
+ if (!htmlContent && rawResponse.trim().startsWith("{")) {
82
+ try {
83
+ const parsed = JSON.parse(rawResponse);
84
+ const c = parsed.data ?? parsed.html ?? parsed.content ?? parsed.body ?? parsed.result;
85
+ if (typeof c === "string" && c.includes("<table")) htmlContent = c;
86
+ } catch (_) { /* not JSON */ }
87
+ }
57
88
  if (!htmlContent) {
58
89
  return { error: "Failed to extract calendar details", status: 404 };
59
90
  }
60
- const $inner = cheerio.load(htmlContent);
61
- // Try multiple selectors for the calendar table
62
- let $mainTable = $inner("table[bgcolor='#FAFCFE']");
63
- if ($mainTable.length === 0) $mainTable = $inner("table[bgcolor='#fafcfe']");
64
- if ($mainTable.length === 0) {
65
- // Find the largest table that looks like a calendar
91
+ // ── Step 3: Find the calendar table ────────────────────────────────────
92
+ const $ = cheerio.load(htmlContent);
93
+ let $mainTable = $("table[bgcolor='#FAFCFE']");
94
+ if (!$mainTable.length) $mainTable = $("table[bgcolor='#fafcfe']");
95
+ if (!$mainTable.length) {
96
+ // Fall back to the table with the most data
66
97
  let bestTable = null;
67
- let bestThCount = 0;
68
- $inner("table").each((_, el) => {
69
- const thCount = $inner(el).find("th").length;
70
- if (thCount > bestThCount) {
71
- bestThCount = thCount;
72
- bestTable = $inner(el);
73
- }
98
+ let bestScore = 0;
99
+ $("table").each((_, el) => {
100
+ const $el = $(el);
101
+ const score = $el.find("tr").length * ($el.find("tr").first().find("td, th").length || 1);
102
+ if (score > bestScore) { bestScore = score; bestTable = $el; }
74
103
  });
75
- if (bestTable && bestThCount > 3) $mainTable = bestTable;
104
+ if (bestTable && bestScore > 10) $mainTable = bestTable;
76
105
  }
77
- if ($mainTable.length === 0) {
78
- return { error: "Could not find the main calendar table.", status: 500 };
106
+ if (!$mainTable.length) {
107
+ return { error: "Could not find calendar table", status: 500 };
79
108
  }
80
- const $headerRow = $mainTable.find("tr").first();
81
- const $ths = $headerRow.find("th");
82
- const monthsData = [];
83
- // Auto-detect columns per month by trying common values
84
- let colsPerMonth = 5;
85
- const MONTH_NAMES = ["january","february","march","april","may","june","july","august","september","october","november","december"];
86
- for (const cols of [5, 4, 6, 3]) {
87
- const candidate = [];
88
- for (let i = 0;; i++) {
89
- const idx = i * cols + Math.floor(cols / 2);
90
- if (idx >= $ths.length) break;
91
- let name = $ths.eq(idx).find("strong").text().trim();
92
- if (!name) name = $ths.eq(idx).text().trim();
93
- if (name && MONTH_NAMES.includes(name.toLowerCase())) {
94
- candidate.push(name);
95
- } else {
96
- break;
97
- }
98
- }
99
- if (candidate.length > 0) {
100
- colsPerMonth = cols;
101
- candidate.forEach(m => monthsData.push({ month: m, days: [] }));
102
- break;
109
+ // ── Step 4: Locate the header row and month names ──────────────────────
110
+ const MONTH_NAMES = ["january","february","march","april","may","june",
111
+ "july","august","september","october","november","december"];
112
+ const allRows = $mainTable.find("tr").toArray();
113
+ let headerRowIndex = -1;
114
+ let monthNames = [];
115
+ for (let ri = 0; ri < Math.min(4, allRows.length); ri++) {
116
+ const cells = $(allRows[ri]).find("td, th").toArray();
117
+ const found = [];
118
+ for (const cell of cells) {
119
+ const text = ($(cell).find("strong").text().trim() || $(cell).text().trim());
120
+ if (text && MONTH_NAMES.includes(text.toLowerCase())) found.push(text);
103
121
  }
122
+ if (found.length > 0) { headerRowIndex = ri; monthNames = found; break; }
123
+ }
124
+ if (monthNames.length === 0) {
125
+ return { error: "Could not parse month headers from calendar", status: 500 };
126
+ }
127
+ // ── Step 5: Find first data row (starts with a number = date) ──────────
128
+ let firstDataRowIndex = headerRowIndex + 1;
129
+ while (firstDataRowIndex < allRows.length) {
130
+ const firstCell = $(allRows[firstDataRowIndex]).find("td, th").first().text().trim();
131
+ if (/^\d+$/.test(firstCell)) break;
132
+ firstDataRowIndex++;
104
133
  }
105
- if (monthsData.length === 0) {
106
- return { error: "Could not parse month headers from calendar.", status: 500 };
134
+ if (firstDataRowIndex >= allRows.length) {
135
+ return { error: "No data rows found in calendar table", status: 500 };
107
136
  }
108
- const $dataRows = $mainTable.find("tr").slice(1).toArray();
109
- $dataRows.forEach((rowElement) => {
110
- const $tds = $inner(rowElement).find("td");
111
- monthsData.forEach((month, monthIndex) => {
112
- const offset = monthIndex * colsPerMonth;
113
- if (offset + 3 >= $tds.length)
114
- return;
115
- const date = $tds.eq(offset).text().trim();
116
- if (!date)
117
- return;
118
- const day = $tds.eq(offset + 1).text().trim();
119
- const event = $tds.eq(offset + 2).find("strong").text().trim()
120
- || $tds.eq(offset + 2).text().trim();
121
- const dayOrder = $tds.eq(offset + 3).text().trim();
137
+ // ── Step 6: Calculate columns per month from the first data row ─────────
138
+ const dataRowCellCount = $(allRows[firstDataRowIndex]).find("td, th").length;
139
+ let colsPerMonth = monthNames.length > 0 ? Math.round(dataRowCellCount / monthNames.length) : 5;
140
+ if (colsPerMonth < 3) colsPerMonth = 5;
141
+ // ── Step 7: Parse all data rows ────────────────────────────────────────
142
+ const monthsData = monthNames.map(m => ({ month: m, days: [] }));
143
+ for (let ri = firstDataRowIndex; ri < allRows.length; ri++) {
144
+ const $cells = $(allRows[ri]).find("td, th");
145
+ monthsData.forEach((month, mi) => {
146
+ const offset = mi * colsPerMonth;
147
+ if (offset + 3 >= $cells.length) return;
148
+ const date = $cells.eq(offset).text().trim();
149
+ if (!date || !/^\d+$/.test(date)) return;
150
+ const day = $cells.eq(offset + 1).text().trim();
151
+ const event = ($cells.eq(offset + 2).find("strong").text().trim()
152
+ || $cells.eq(offset + 2).text().trim()).trim();
153
+ const dayOrder = $cells.eq(offset + 3).text().trim();
122
154
  month.days.push({ date, day, event, dayOrder });
123
155
  });
124
- });
125
- return { calendar: monthsData, status: 200 };
156
+ }
157
+ const populated = monthsData.filter(m => m.days.length > 0);
158
+ if (populated.length === 0) {
159
+ return { error: "Calendar table found but no data could be parsed", status: 500 };
160
+ }
161
+ return { calendar: populated, status: 200 };
126
162
  }
127
163
  catch (error) {
128
164
  console.error("Error parsing calendar:", error);
@@ -25,7 +25,12 @@ async function calendarDynamicUrl() {
25
25
  return dynamicUrl;
26
26
  }
27
27
  async function courseDynamicUrl() {
28
- const baseUrl = "https://academia.srmist.edu.in/srm_university/academia-academic-services/page/My_Time_Table_2023_24";
29
- return baseUrl;
28
+ const currentDate = new Date();
29
+ const currentYear = currentDate.getFullYear();
30
+ const currentMonth = currentDate.getMonth() + 1;
31
+ const academicYearString = currentMonth >= 1 && currentMonth <= 6
32
+ ? `${currentYear - 1}_${currentYear.toString().slice(-2)}`
33
+ : `${currentYear}_${(currentYear + 1).toString().slice(-2)}`;
34
+ return `https://academia.srmist.edu.in/srm_university/academia-academic-services/page/My_Time_Table_${academicYearString}`;
30
35
  }
31
- //# sourceMappingURL=dynamicUrl.js.map
36
+ //# sourceMappingURL=dynamicUrl.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reddy-api-srm",
3
3
  "description": "SRMIST KTR Academia portal",
4
- "version": "1.0.7",
4
+ "version": "1.0.9",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
7
7
  "exports": {