@mo7yw4ng/openape 1.0.4 → 1.0.5
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/esm/deno.js +1 -1
- package/esm/src/commands/announcements.js +3 -3
- package/esm/src/commands/auth.d.ts.map +1 -1
- package/esm/src/commands/auth.js +24 -14
- package/esm/src/commands/calendar.js +3 -3
- package/esm/src/commands/courses.js +3 -3
- package/esm/src/commands/forums.js +3 -3
- package/esm/src/commands/grades.js +3 -3
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +114 -191
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +33 -22
- package/esm/src/commands/videos.js +5 -5
- package/esm/src/lib/auth.js +2 -2
- package/esm/src/lib/logger.d.ts +1 -1
- package/esm/src/lib/logger.d.ts.map +1 -1
- package/esm/src/lib/logger.js +7 -4
- package/esm/src/lib/moodle.d.ts +4 -0
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +19 -2
- package/package.json +1 -1
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.js +3 -3
- package/script/src/commands/auth.d.ts.map +1 -1
- package/script/src/commands/auth.js +24 -14
- package/script/src/commands/calendar.js +3 -3
- package/script/src/commands/courses.js +3 -3
- package/script/src/commands/forums.js +3 -3
- package/script/src/commands/grades.js +3 -3
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +111 -188
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +32 -21
- package/script/src/commands/videos.js +5 -5
- package/script/src/lib/auth.js +2 -2
- package/script/src/lib/logger.d.ts +1 -1
- package/script/src/lib/logger.d.ts.map +1 -1
- package/script/src/lib/logger.js +7 -4
- package/script/src/lib/moodle.d.ts +4 -0
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +20 -2
- package/skills/openape/SKILL.md +4 -2
package/esm/deno.js
CHANGED
|
@@ -17,19 +17,19 @@ export function registerAnnouncementsCommand(program) {
|
|
|
17
17
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
18
18
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
19
19
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
20
|
-
const log = createLogger(opts.verbose, silent);
|
|
20
|
+
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
21
21
|
const baseDir = getBaseDir();
|
|
22
22
|
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
23
23
|
// Check if session exists
|
|
24
24
|
if (!fs.existsSync(sessionPath)) {
|
|
25
|
-
|
|
25
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
26
26
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
// Try to load WS token
|
|
30
30
|
const wsToken = loadWsToken(sessionPath);
|
|
31
31
|
if (!wsToken) {
|
|
32
|
-
|
|
32
|
+
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkStD"}
|
package/esm/src/commands/auth.js
CHANGED
|
@@ -129,31 +129,41 @@ export function registerCommand(program) {
|
|
|
129
129
|
catch {
|
|
130
130
|
// Ignore sesskey extraction errors
|
|
131
131
|
}
|
|
132
|
-
//
|
|
132
|
+
// Acquire WS token
|
|
133
|
+
let wsToken;
|
|
133
134
|
try {
|
|
134
|
-
|
|
135
|
-
username: "",
|
|
136
|
-
password: "",
|
|
137
|
-
courseUrl: "",
|
|
138
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
139
|
-
headless: false,
|
|
140
|
-
slowMo: 0,
|
|
141
|
-
authStatePath: sessionPath,
|
|
142
|
-
ollamaBaseUrl: "",
|
|
143
|
-
};
|
|
144
|
-
const wsToken = await acquireWsToken(page, config, log);
|
|
135
|
+
wsToken = await acquireWsToken(page, { moodleBaseUrl: "https://ilearning.cycu.edu.tw" }, log);
|
|
145
136
|
saveWsToken(sessionPath, wsToken);
|
|
146
137
|
}
|
|
147
138
|
catch {
|
|
148
139
|
// WS token is optional, ignore errors
|
|
149
140
|
}
|
|
150
141
|
const stats = fs.statSync(sessionPath);
|
|
142
|
+
// Get user info via WS API
|
|
143
|
+
let user;
|
|
144
|
+
try {
|
|
145
|
+
if (wsToken) {
|
|
146
|
+
const siteInfo = await getSiteInfoApi({
|
|
147
|
+
wsToken,
|
|
148
|
+
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
149
|
+
});
|
|
150
|
+
user = {
|
|
151
|
+
userid: siteInfo.userid,
|
|
152
|
+
username: siteInfo.username,
|
|
153
|
+
fullname: siteInfo.fullname,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Ignore
|
|
159
|
+
}
|
|
151
160
|
const result = {
|
|
152
161
|
status: "success",
|
|
153
162
|
message: "Login successful",
|
|
154
163
|
session_path: sessionPath,
|
|
155
164
|
session_size: stats.size,
|
|
156
|
-
updated: true
|
|
165
|
+
updated: true,
|
|
166
|
+
...(user ? { user } : {}),
|
|
157
167
|
};
|
|
158
168
|
console.log(JSON.stringify(result, null, 2));
|
|
159
169
|
}
|
|
@@ -247,7 +257,7 @@ export function registerCommand(program) {
|
|
|
247
257
|
status: "error",
|
|
248
258
|
error: "Session not found",
|
|
249
259
|
session_path: sessionPath,
|
|
250
|
-
hint: "Run 'openape
|
|
260
|
+
hint: "Run 'openape login' first"
|
|
251
261
|
};
|
|
252
262
|
console.log(JSON.stringify(result, null, 2));
|
|
253
263
|
}
|
|
@@ -17,19 +17,19 @@ export function registerCalendarCommand(program) {
|
|
|
17
17
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
18
18
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
19
19
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
20
|
-
const log = createLogger(opts.verbose, silent);
|
|
20
|
+
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
21
21
|
const baseDir = getBaseDir();
|
|
22
22
|
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
23
23
|
// Check if session exists
|
|
24
24
|
if (!fs.existsSync(sessionPath)) {
|
|
25
|
-
|
|
25
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
26
26
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
// Try to load WS token
|
|
30
30
|
const wsToken = loadWsToken(sessionPath);
|
|
31
31
|
if (!wsToken) {
|
|
32
|
-
|
|
32
|
+
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
@@ -18,19 +18,19 @@ export function registerCoursesCommand(program) {
|
|
|
18
18
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
19
19
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
20
20
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
21
|
-
const log = createLogger(opts.verbose, silent);
|
|
21
|
+
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
22
22
|
const baseDir = getBaseDir();
|
|
23
23
|
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
24
24
|
// Check if session exists
|
|
25
25
|
if (!fs.existsSync(sessionPath)) {
|
|
26
|
-
|
|
26
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
27
27
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
// Try to load WS token
|
|
31
31
|
const wsToken = loadWsToken(sessionPath);
|
|
32
32
|
if (!wsToken) {
|
|
33
|
-
|
|
33
|
+
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
return {
|
|
@@ -12,19 +12,19 @@ export function registerForumsCommand(program) {
|
|
|
12
12
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
13
13
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
14
14
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
15
|
-
const log = createLogger(opts.verbose, silent);
|
|
15
|
+
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
16
16
|
const baseDir = getBaseDir();
|
|
17
17
|
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
18
18
|
// Check if session exists
|
|
19
19
|
if (!fs.existsSync(sessionPath)) {
|
|
20
|
-
|
|
20
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
21
21
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
// Try to load WS token
|
|
25
25
|
const wsToken = loadWsToken(sessionPath);
|
|
26
26
|
if (!wsToken) {
|
|
27
|
-
|
|
27
|
+
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
// Try to load sesskey from cache
|
|
@@ -17,19 +17,19 @@ export function registerGradesCommand(program) {
|
|
|
17
17
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
18
18
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
19
19
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
20
|
-
const log = createLogger(opts.verbose, silent);
|
|
20
|
+
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
21
21
|
const baseDir = getBaseDir();
|
|
22
22
|
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
23
23
|
// Check if session exists
|
|
24
24
|
if (!fs.existsSync(sessionPath)) {
|
|
25
|
-
|
|
25
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
26
26
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
// Try to load WS token
|
|
30
30
|
const wsToken = loadWsToken(sessionPath);
|
|
31
31
|
if (!wsToken) {
|
|
32
|
-
|
|
32
|
+
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAid/D"}
|
|
@@ -1,75 +1,24 @@
|
|
|
1
|
-
import { getOutputFormat, sanitizeFilename,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { launchAuthenticated, createApiContext } from "../lib/auth.js";
|
|
5
|
-
import { extractSessionInfo } from "../lib/session.js";
|
|
6
|
-
import { closeBrowserSafely } from "../lib/auth.js";
|
|
1
|
+
import { getOutputFormat, sanitizeFilename, formatFileSize } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getResourcesByCoursesApi, updateActivityCompletionStatusManually, getSiteInfoApi, moodleApiCall } from "../lib/moodle.js";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
7
4
|
import { formatAndOutput } from "../index.js";
|
|
8
5
|
import path from "node:path";
|
|
9
6
|
import fs from "node:fs";
|
|
10
7
|
export function registerMaterialsCommand(program) {
|
|
11
8
|
const materialsCmd = program.command("materials");
|
|
12
9
|
materialsCmd.description("Material/resource operations");
|
|
13
|
-
// Helper
|
|
14
|
-
async function
|
|
15
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
16
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
17
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
18
|
-
const log = createLogger(opts.verbose, silent);
|
|
19
|
-
const sessionPath = getSessionPath();
|
|
20
|
-
if (!fs.existsSync(sessionPath)) {
|
|
21
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
const config = {
|
|
25
|
-
username: "",
|
|
26
|
-
password: "",
|
|
27
|
-
courseUrl: "",
|
|
28
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
29
|
-
headless: !options.headed,
|
|
30
|
-
slowMo: 0,
|
|
31
|
-
authStatePath: sessionPath,
|
|
32
|
-
ollamaBaseUrl: "",
|
|
33
|
-
};
|
|
34
|
-
log.info("啟動瀏覽器...");
|
|
35
|
-
const { browser, context, page } = await launchAuthenticated(config, log);
|
|
36
|
-
try {
|
|
37
|
-
const session = await extractSessionInfo(page, config, log);
|
|
38
|
-
return { log, page, session, browser, context };
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
await context.close();
|
|
42
|
-
await browser.close();
|
|
43
|
-
throw err;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// Helper to download a single resource
|
|
47
|
-
async function downloadResource(page, resource, outputDir, log) {
|
|
10
|
+
// Helper to download a single resource via HTTP (no browser needed)
|
|
11
|
+
async function downloadResourceHttp(resource, outputDir, log, token) {
|
|
48
12
|
try {
|
|
49
13
|
// Only download resource type (skip url)
|
|
50
14
|
if (resource.modType !== "resource") {
|
|
51
15
|
log.debug(` Skipping ${resource.modType}: ${resource.name}`);
|
|
52
16
|
return null;
|
|
53
17
|
}
|
|
54
|
-
// Create course directory
|
|
55
18
|
const courseDir = path.join(outputDir, sanitizeFilename(resource.course_name));
|
|
56
19
|
await fs.promises.mkdir(courseDir, { recursive: true });
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
await page.goto(resource.url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
60
|
-
// Try to find download link on the page
|
|
61
|
-
const downloadLinks = await page.$$eval('a[href*="forcedownload=1"]', (links) => links.map((a) => a.href));
|
|
62
|
-
if (downloadLinks.length === 0) {
|
|
63
|
-
log.warn(` No download link found for: ${resource.name}`);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
// Download the first available file
|
|
67
|
-
const downloadUrl = downloadLinks[0];
|
|
68
|
-
// Extract filename from URL or use resource name
|
|
69
|
-
const urlObj = new URL(downloadUrl);
|
|
70
|
-
const filenameParam = urlObj.searchParams.get("filename");
|
|
71
|
-
let filename = filenameParam || sanitizeFilename(resource.name);
|
|
72
|
-
// Add extension if missing
|
|
20
|
+
// Build filename
|
|
21
|
+
let filename = sanitizeFilename(resource.name);
|
|
73
22
|
if (resource.mimetype && !path.extname(filename)) {
|
|
74
23
|
const extMap = {
|
|
75
24
|
"application/pdf": ".pdf",
|
|
@@ -88,27 +37,54 @@ export function registerMaterialsCommand(program) {
|
|
|
88
37
|
}
|
|
89
38
|
}
|
|
90
39
|
const outputPath = path.join(courseDir, filename);
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
40
|
+
// Skip if already exists
|
|
41
|
+
if (fs.existsSync(outputPath)) {
|
|
42
|
+
log.debug(` Skipping (exists): ${filename}`);
|
|
43
|
+
const stats = await fs.promises.stat(outputPath);
|
|
44
|
+
return { filename, path: outputPath, size: stats.size, course_id: resource.course_id, course_name: resource.course_name };
|
|
45
|
+
}
|
|
46
|
+
// Download via HTTP with WS token
|
|
47
|
+
const separator = resource.url.includes("?") ? "&" : "?";
|
|
48
|
+
const downloadUrl = `${resource.url}${separator}token=${token}`;
|
|
49
|
+
log.debug(` Downloading: ${resource.name}`);
|
|
50
|
+
const response = await fetch(downloadUrl);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
log.warn(` Failed to download ${resource.name}: HTTP ${response.status}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
56
|
+
const data = new Uint8Array(arrayBuffer);
|
|
57
|
+
await fs.promises.writeFile(outputPath, data);
|
|
58
|
+
log.success(` Downloaded: ${filename} (${formatFileSize(data.byteLength, 1)} KB)`);
|
|
59
|
+
return { filename, path: outputPath, size: data.byteLength, course_id: resource.course_id, course_name: resource.course_name };
|
|
106
60
|
}
|
|
107
61
|
catch (err) {
|
|
108
62
|
log.warn(` Failed to download ${resource.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
63
|
return null;
|
|
110
64
|
}
|
|
111
65
|
}
|
|
66
|
+
// Helper to build material list from API resources
|
|
67
|
+
function buildMaterialsList(courses, apiResources) {
|
|
68
|
+
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
69
|
+
const materials = [];
|
|
70
|
+
for (const resource of apiResources) {
|
|
71
|
+
const course = courseMap.get(resource.courseId);
|
|
72
|
+
if (course) {
|
|
73
|
+
materials.push({
|
|
74
|
+
course_id: resource.courseId,
|
|
75
|
+
course_name: course.fullname,
|
|
76
|
+
cmid: resource.cmid,
|
|
77
|
+
name: resource.name,
|
|
78
|
+
url: resource.url,
|
|
79
|
+
modType: resource.modType,
|
|
80
|
+
mimetype: resource.mimetype,
|
|
81
|
+
filesize: resource.filesize,
|
|
82
|
+
modified: resource.modified,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return materials;
|
|
87
|
+
}
|
|
112
88
|
materialsCmd
|
|
113
89
|
.command("list-all")
|
|
114
90
|
.description("List all materials/resources across all courses")
|
|
@@ -174,150 +150,97 @@ export function registerMaterialsCommand(program) {
|
|
|
174
150
|
});
|
|
175
151
|
materialsCmd
|
|
176
152
|
.command("download")
|
|
177
|
-
.description("Download all materials from a specific course
|
|
153
|
+
.description("Download all materials from a specific course")
|
|
178
154
|
.argument("<course-id>", "Course ID")
|
|
179
155
|
.option("--output-dir <path>", "Output directory", "./downloads")
|
|
180
156
|
.action(async (courseId, options, command) => {
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
157
|
+
const apiContext = await createApiContext(options, command);
|
|
158
|
+
if (!apiContext) {
|
|
183
159
|
process.exitCode = 1;
|
|
184
160
|
return;
|
|
185
161
|
}
|
|
186
|
-
const { log,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
name: a.textContent?.trim() || "",
|
|
203
|
-
}));
|
|
204
|
-
});
|
|
205
|
-
for (const link of resourceLinks) {
|
|
206
|
-
const cmidMatch = link.url.match(/id=(\d+)/);
|
|
207
|
-
if (cmidMatch) {
|
|
208
|
-
materials.push({
|
|
209
|
-
course_id: course.id,
|
|
210
|
-
course_name: course.fullname,
|
|
211
|
-
cmid: cmidMatch[1],
|
|
212
|
-
name: link.name,
|
|
213
|
-
url: link.url,
|
|
214
|
-
modType: "resource",
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
log.info(`Found ${materials.length} materials in course: ${course.fullname}`);
|
|
219
|
-
const downloadedFiles = [];
|
|
220
|
-
for (const material of materials) {
|
|
221
|
-
const result = await downloadResource(page, material, options.outputDir, log);
|
|
222
|
-
if (result) {
|
|
223
|
-
downloadedFiles.push(result);
|
|
224
|
-
}
|
|
162
|
+
const { log, session } = apiContext;
|
|
163
|
+
const courses = await getEnrolledCoursesApi(session);
|
|
164
|
+
const course = courses.find((c) => c.id === parseInt(courseId, 10));
|
|
165
|
+
if (!course) {
|
|
166
|
+
log.error(`Course not found: ${courseId}`);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const apiResources = await getResourcesByCoursesApi(session, [course.id]);
|
|
171
|
+
const materials = buildMaterialsList(courses, apiResources);
|
|
172
|
+
log.info(`Found ${materials.length} materials in course: ${course.fullname}`);
|
|
173
|
+
const downloadedFiles = [];
|
|
174
|
+
for (const material of materials) {
|
|
175
|
+
const result = await downloadResourceHttp(material, options.outputDir, log, session.wsToken);
|
|
176
|
+
if (result) {
|
|
177
|
+
downloadedFiles.push(result);
|
|
225
178
|
}
|
|
226
|
-
|
|
179
|
+
}
|
|
180
|
+
const output = {
|
|
181
|
+
status: "success",
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
downloaded_files: downloadedFiles.map(f => ({
|
|
184
|
+
filename: f.filename,
|
|
185
|
+
path: f.path,
|
|
186
|
+
size: f.size,
|
|
187
|
+
course_id: f.course_id,
|
|
188
|
+
course_name: f.course_name,
|
|
189
|
+
})),
|
|
190
|
+
summary: {
|
|
227
191
|
total_materials: materials.length,
|
|
228
192
|
downloaded: downloadedFiles.length,
|
|
229
193
|
skipped: materials.length - downloadedFiles.length,
|
|
230
194
|
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
timestamp: new Date().toISOString(),
|
|
235
|
-
downloaded_files: downloadedFiles.map(f => ({
|
|
236
|
-
filename: f.filename,
|
|
237
|
-
path: f.path,
|
|
238
|
-
size: f.size,
|
|
239
|
-
course_id: f.course_id,
|
|
240
|
-
course_name: f.course_name,
|
|
241
|
-
})),
|
|
242
|
-
summary,
|
|
243
|
-
};
|
|
244
|
-
console.log(JSON.stringify(output));
|
|
245
|
-
}
|
|
246
|
-
finally {
|
|
247
|
-
await closeBrowserSafely(browser, browserContext);
|
|
248
|
-
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
console.log(JSON.stringify(output));
|
|
249
198
|
});
|
|
250
199
|
materialsCmd
|
|
251
200
|
.command("download-all")
|
|
252
|
-
.description("Download all materials from all courses
|
|
201
|
+
.description("Download all materials from all courses")
|
|
253
202
|
.option("--output-dir <path>", "Output directory", "./downloads")
|
|
254
203
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
255
204
|
.action(async (options, command) => {
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
205
|
+
const apiContext = await createApiContext(options, command);
|
|
206
|
+
if (!apiContext) {
|
|
258
207
|
process.exitCode = 1;
|
|
259
208
|
return;
|
|
260
209
|
}
|
|
261
|
-
const { log,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
275
|
-
for (const link of resourceLinks) {
|
|
276
|
-
const cmidMatch = link.url.match(/id=(\d+)/);
|
|
277
|
-
if (cmidMatch) {
|
|
278
|
-
allMaterials.push({
|
|
279
|
-
course_id: course.id,
|
|
280
|
-
course_name: course.fullname,
|
|
281
|
-
cmid: cmidMatch[1],
|
|
282
|
-
name: link.name,
|
|
283
|
-
url: link.url,
|
|
284
|
-
modType: "resource",
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
log.info(`Found ${allMaterials.length} materials across ${courses.length} courses`);
|
|
290
|
-
const downloadedFiles = [];
|
|
291
|
-
for (const material of allMaterials) {
|
|
292
|
-
const result = await downloadResource(page, material, options.outputDir, log);
|
|
293
|
-
if (result) {
|
|
294
|
-
downloadedFiles.push(result);
|
|
295
|
-
}
|
|
210
|
+
const { log, session } = apiContext;
|
|
211
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
212
|
+
const courses = await getEnrolledCoursesApi(session, { classification });
|
|
213
|
+
log.info(`Scanning ${courses.length} courses for materials...`);
|
|
214
|
+
const courseIds = courses.map((c) => c.id);
|
|
215
|
+
const apiResources = await getResourcesByCoursesApi(session, courseIds);
|
|
216
|
+
const allMaterials = buildMaterialsList(courses, apiResources);
|
|
217
|
+
log.info(`Found ${allMaterials.length} materials across ${courses.length} courses`);
|
|
218
|
+
const downloadedFiles = [];
|
|
219
|
+
for (const material of allMaterials) {
|
|
220
|
+
const result = await downloadResourceHttp(material, options.outputDir, log, session.wsToken);
|
|
221
|
+
if (result) {
|
|
222
|
+
downloadedFiles.push(result);
|
|
296
223
|
}
|
|
297
|
-
|
|
224
|
+
}
|
|
225
|
+
const output = {
|
|
226
|
+
status: "success",
|
|
227
|
+
timestamp: new Date().toISOString(),
|
|
228
|
+
downloaded_files: downloadedFiles.map(f => ({
|
|
229
|
+
filename: f.filename,
|
|
230
|
+
path: f.path,
|
|
231
|
+
size: f.size,
|
|
232
|
+
course_id: f.course_id,
|
|
233
|
+
course_name: f.course_name,
|
|
234
|
+
})),
|
|
235
|
+
summary: {
|
|
298
236
|
total_courses: courses.length,
|
|
299
237
|
total_materials: allMaterials.length,
|
|
300
238
|
downloaded: downloadedFiles.length,
|
|
301
239
|
skipped: allMaterials.length - downloadedFiles.length,
|
|
302
240
|
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
timestamp: new Date().toISOString(),
|
|
307
|
-
downloaded_files: downloadedFiles.map(f => ({
|
|
308
|
-
filename: f.filename,
|
|
309
|
-
path: f.path,
|
|
310
|
-
size: f.size,
|
|
311
|
-
course_id: f.course_id,
|
|
312
|
-
course_name: f.course_name,
|
|
313
|
-
})),
|
|
314
|
-
summary,
|
|
315
|
-
};
|
|
316
|
-
console.log(JSON.stringify(output));
|
|
317
|
-
}
|
|
318
|
-
finally {
|
|
319
|
-
await closeBrowserSafely(browser, browserContext);
|
|
320
|
-
}
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
console.log(JSON.stringify(output));
|
|
321
244
|
});
|
|
322
245
|
materialsCmd
|
|
323
246
|
.command("complete")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyEpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoQ7D"}
|