tronclass-auto 1.0.0 → 1.0.2
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/bin/{eclass.js → tronclass.js} +12 -0
- package/package.json +8 -3
- package/src/auth.js +7 -2
- package/src/browser.js +42 -0
- package/src/config.js +77 -1
- package/src/index.js +13 -9
|
@@ -46,6 +46,18 @@ program
|
|
|
46
46
|
await showStatus();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
program
|
|
50
|
+
.command('course')
|
|
51
|
+
.description('管理課程列表')
|
|
52
|
+
.option('-a, --add <url>', '新增課程 URL')
|
|
53
|
+
.option('-r, --remove <index>', '移除課程(輸入編號)')
|
|
54
|
+
.option('-l, --list', '列出所有課程')
|
|
55
|
+
.option('--clear', '清空所有課程')
|
|
56
|
+
.action((opts) => {
|
|
57
|
+
const { manageCourses } = require('../src/config');
|
|
58
|
+
manageCourses(opts);
|
|
59
|
+
});
|
|
60
|
+
|
|
49
61
|
program
|
|
50
62
|
.command('config')
|
|
51
63
|
.description('顯示目前設定')
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tronclass-auto",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "自動觀看 eclass/TronClass 影片、填寫表單的 CLI 工具",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tronclass": "
|
|
7
|
+
"tronclass": "bin/tronclass.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
9
14
|
"scripts": {
|
|
10
|
-
"start": "node bin/
|
|
15
|
+
"start": "node bin/tronclass.js run"
|
|
11
16
|
},
|
|
12
17
|
"keywords": ["eclass", "tronclass", "yuntech", "automation", "video"],
|
|
13
18
|
"author": "",
|
package/src/auth.js
CHANGED
|
@@ -81,9 +81,14 @@ async function autoLogin(baseUrl) {
|
|
|
81
81
|
console.log(chalk.yellow('將開啟瀏覽器,請手動登入'));
|
|
82
82
|
console.log(chalk.yellow('登入成功後,Cookie 會自動儲存\n'));
|
|
83
83
|
|
|
84
|
+
const { ensureBrowser } = require('./browser');
|
|
85
|
+
const exePath = await ensureBrowser();
|
|
86
|
+
|
|
84
87
|
const { chromium } = require('playwright');
|
|
88
|
+
const launchOpts = { headless: false, slowMo: 50 };
|
|
89
|
+
if (exePath) launchOpts.executablePath = exePath;
|
|
85
90
|
|
|
86
|
-
const browser = await chromium.launch(
|
|
91
|
+
const browser = await chromium.launch(launchOpts);
|
|
87
92
|
const context = await browser.newContext();
|
|
88
93
|
const page = await context.newPage();
|
|
89
94
|
|
|
@@ -110,7 +115,7 @@ async function autoLogin(baseUrl) {
|
|
|
110
115
|
if (eclassCookies.length > 0) {
|
|
111
116
|
saveCookies(eclassCookies);
|
|
112
117
|
console.log(chalk.green(`\n[AUTH] 登入成功!自動取得 ${eclassCookies.length} 個 Cookie`));
|
|
113
|
-
console.log(chalk.green('[AUTH] 現在可以執行
|
|
118
|
+
console.log(chalk.green('[AUTH] 現在可以執行 tronclass run 開始自動化\n'));
|
|
114
119
|
} else {
|
|
115
120
|
console.log(chalk.yellow('\n[AUTH] 找不到 eclass Cookie,請確認已成功登入'));
|
|
116
121
|
}
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
function getChromiumPath() {
|
|
7
|
+
const playwrightPath = path.join(
|
|
8
|
+
require('os').homedir(),
|
|
9
|
+
'AppData', 'Local', 'ms-playwright'
|
|
10
|
+
);
|
|
11
|
+
if (!fs.existsSync(playwrightPath)) return null;
|
|
12
|
+
|
|
13
|
+
const dirs = fs.readdirSync(playwrightPath).filter(d => d.startsWith('chromium-'));
|
|
14
|
+
if (dirs.length === 0) return null;
|
|
15
|
+
|
|
16
|
+
const chromiumDir = dirs.sort().reverse()[0];
|
|
17
|
+
const exePath = path.join(playwrightPath, chromiumDir, 'chrome-win64', 'chrome.exe');
|
|
18
|
+
return fs.existsSync(exePath) ? exePath : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function ensureBrowser() {
|
|
22
|
+
const exe = getChromiumPath();
|
|
23
|
+
if (exe) return exe;
|
|
24
|
+
|
|
25
|
+
console.log(chalk.yellow('[SETUP] Chromium 未安裝,正在下載...'));
|
|
26
|
+
console.log(chalk.gray(' 首次安裝需要一些時間,之後不需要再裝\n'));
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
execSync('npx playwright install chromium', {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
cwd: path.join(__dirname, '..')
|
|
32
|
+
});
|
|
33
|
+
console.log(chalk.green('\n[SETUP] Chromium 安裝完成\n'));
|
|
34
|
+
return getChromiumPath();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.log(chalk.red('\n[ERROR] Chromium 安裝失敗'));
|
|
37
|
+
console.log(chalk.yellow(' 請手動執行: npx playwright install chromium'));
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { ensureBrowser, getChromiumPath };
|
package/src/config.js
CHANGED
|
@@ -74,9 +74,85 @@ function showConfig() {
|
|
|
74
74
|
console.log(` progress: ${PROGRESS_FILE}\n`);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function normalizeUrl(url) {
|
|
78
|
+
url = url.trim();
|
|
79
|
+
if (url.match(/^\d+$/)) {
|
|
80
|
+
return `${BASE_URL}/course/${url}/content#/`;
|
|
81
|
+
}
|
|
82
|
+
if (!url.startsWith('http')) {
|
|
83
|
+
return `${BASE_URL}/course/${url}/content#/`;
|
|
84
|
+
}
|
|
85
|
+
if (!url.endsWith('#/') && !url.endsWith('#')) {
|
|
86
|
+
if (url.includes('content')) {
|
|
87
|
+
url = url.replace(/\/?$/, '/#/');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return url;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function manageCourses(opts) {
|
|
94
|
+
const config = loadConfig();
|
|
95
|
+
|
|
96
|
+
if (opts.add) {
|
|
97
|
+
const url = normalizeUrl(opts.add);
|
|
98
|
+
config.courses.push(url);
|
|
99
|
+
saveConfig(config);
|
|
100
|
+
console.log(chalk.green(`[OK] 已新增課程`));
|
|
101
|
+
console.log(chalk.white(` ${url}`));
|
|
102
|
+
console.log(chalk.gray(` 目前共 ${config.courses.length} 個課程`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (opts.remove) {
|
|
107
|
+
const idx = parseInt(opts.remove, 10) - 1;
|
|
108
|
+
if (idx < 0 || idx >= config.courses.length) {
|
|
109
|
+
console.log(chalk.red(`[ERROR] 編號 ${opts.remove} 不存在`));
|
|
110
|
+
console.log(chalk.gray(` 目前共 ${config.courses.length} 個課程,請輸入 1~${config.courses.length}`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const removed = config.courses.splice(idx, 1)[0];
|
|
114
|
+
saveConfig(config);
|
|
115
|
+
console.log(chalk.green(`[OK] 已移除課程`));
|
|
116
|
+
console.log(chalk.gray(` ${removed}`));
|
|
117
|
+
console.log(chalk.gray(` 目前共 ${config.courses.length} 個課程`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (opts.clear) {
|
|
122
|
+
config.courses = [];
|
|
123
|
+
saveConfig(config);
|
|
124
|
+
console.log(chalk.green('[OK] 已清空所有課程'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (opts.list || (!opts.add && !opts.remove && !opts.clear)) {
|
|
129
|
+
if (config.courses.length === 0) {
|
|
130
|
+
console.log(chalk.yellow('\n 目前沒有設定任何課程'));
|
|
131
|
+
console.log(chalk.gray(' 使用方法:'));
|
|
132
|
+
console.log(chalk.gray(' tronclass course --add <課程ID或URL>'));
|
|
133
|
+
console.log(chalk.gray(' tronclass course --add 127331'));
|
|
134
|
+
console.log(chalk.gray(' tronclass course --add https://eclass.yuntech.edu.tw/course/127331/content#/'));
|
|
135
|
+
console.log('');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log(chalk.cyan('\n=== 課程列表 ==='));
|
|
139
|
+
config.courses.forEach((c, i) => {
|
|
140
|
+
const match = c.match(/\/course\/(\d+)\//);
|
|
141
|
+
const id = match ? match[1] : '?';
|
|
142
|
+
console.log(chalk.white(` ${i + 1}. [${id}] ${c}`));
|
|
143
|
+
});
|
|
144
|
+
console.log(chalk.gray(`\n 共 ${config.courses.length} 個課程\n`));
|
|
145
|
+
console.log(chalk.gray(' 操作:'));
|
|
146
|
+
console.log(chalk.gray(' tronclass course --add <ID或URL> 新增'));
|
|
147
|
+
console.log(chalk.gray(' tronclass course --remove <編號> 移除'));
|
|
148
|
+
console.log(chalk.gray(' tronclass course --clear 清空'));
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
77
153
|
module.exports = {
|
|
78
154
|
CONFIG_DIR, CONFIG_FILE, PROGRESS_FILE, COOKIE_FILE, SALT_FILE,
|
|
79
155
|
BASE_URL, DEFAULT_CONFIG,
|
|
80
156
|
ensureDir, loadConfig, saveConfig,
|
|
81
|
-
loadProgress, saveProgress, markDone, isDone, showConfig
|
|
157
|
+
loadProgress, saveProgress, markDone, isDone, showConfig, manageCourses
|
|
82
158
|
};
|
package/src/index.js
CHANGED
|
@@ -88,12 +88,16 @@ async function showStatus() {
|
|
|
88
88
|
const config = loadConfig();
|
|
89
89
|
const cookies = loadCookies();
|
|
90
90
|
if (!cookies.length) {
|
|
91
|
-
console.log(chalk.red('[ERROR] 沒有 Cookie,請先執行
|
|
91
|
+
console.log(chalk.red('[ERROR] 沒有 Cookie,請先執行 tronclass login 或 tronclass import-cookies'));
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const { chromium } = re('playwright');
|
|
96
|
-
const
|
|
96
|
+
const { ensureBrowser } = re('./browser');
|
|
97
|
+
const exePath = await ensureBrowser();
|
|
98
|
+
const launchOpts = { headless: true, slowMo: 50 };
|
|
99
|
+
if (exePath) launchOpts.executablePath = exePath;
|
|
100
|
+
const browser = await chromium.launch(launchOpts);
|
|
97
101
|
const context = await browser.newContext();
|
|
98
102
|
await context.addCookies(cookies);
|
|
99
103
|
const page = await context.newPage();
|
|
@@ -135,19 +139,19 @@ async function run(opts = {}) {
|
|
|
135
139
|
|
|
136
140
|
if (!cookies.length) {
|
|
137
141
|
console.log(chalk.red('[ERROR] 沒有 Cookie'));
|
|
138
|
-
console.log(chalk.yellow(' 請執行:
|
|
139
|
-
console.log(chalk.yellow(' 或:
|
|
142
|
+
console.log(chalk.yellow(' 請執行: tronclass login (自動取得 Cookie)'));
|
|
143
|
+
console.log(chalk.yellow(' 或: tronclass import-cookies "session=xxx"'));
|
|
140
144
|
return;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
if (opts.headless) config.headless = true;
|
|
144
148
|
|
|
145
149
|
const { chromium } = re('playwright');
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
const { ensureBrowser } = re('./browser');
|
|
151
|
+
const exePath = await ensureBrowser();
|
|
152
|
+
const launchOpts = { headless: config.headless, slowMo: config.slowMo || 50 };
|
|
153
|
+
if (exePath) launchOpts.executablePath = exePath;
|
|
154
|
+
const browser = await chromium.launch(launchOpts);
|
|
151
155
|
const context = await browser.newContext({
|
|
152
156
|
viewport: { width: 1280, height: 900 },
|
|
153
157
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|