tronclass-auto 1.1.0 → 1.2.1
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/package.json +1 -1
- package/src/auth.js +40 -18
- package/src/tui.js +174 -168
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -97,32 +97,54 @@ async function autoLogin(baseUrl) {
|
|
|
97
97
|
console.log(chalk.cyan('等待您完成登入...'));
|
|
98
98
|
console.log(chalk.cyan('(登入後頁面會自動跳轉,Cookie 會自動儲存)'));
|
|
99
99
|
|
|
100
|
+
let sawLogin = false;
|
|
100
101
|
let saved = false;
|
|
101
102
|
let checkCount = 0;
|
|
102
103
|
|
|
103
104
|
const checkInterval = setInterval(async () => {
|
|
104
105
|
checkCount++;
|
|
105
106
|
const url = page.url();
|
|
107
|
+
const isLoginPage = url.includes('login') || url.includes('auth') || url.includes('cas') || url.includes('signin');
|
|
106
108
|
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
if (isLoginPage) {
|
|
110
|
+
sawLogin = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sawLogin && !isLoginPage && !saved) {
|
|
114
|
+
saved = true;
|
|
115
|
+
console.log(chalk.cyan('偵測到頁面跳轉,驗證登入狀態...'));
|
|
116
|
+
await page.waitForTimeout(3000);
|
|
117
|
+
|
|
118
|
+
const isLoggedIn = await page.evaluate(() => {
|
|
119
|
+
return document.querySelector('[class*="profile"]') !== null ||
|
|
120
|
+
document.querySelector('[class*="avatar"]') !== null ||
|
|
121
|
+
document.querySelector('[class*="user-name"]') !== null ||
|
|
122
|
+
document.querySelector('[ng-click*="showUserOperationList"]') !== null ||
|
|
123
|
+
document.querySelector('a[href*="logout"]') !== null ||
|
|
124
|
+
document.querySelector('a[href*="settings"]') !== null;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!isLoggedIn) {
|
|
128
|
+
console.log(chalk.yellow(' 頁面已跳轉但未偵測到登入狀態,可能未完成登入'));
|
|
129
|
+
saved = false;
|
|
130
|
+
return;
|
|
125
131
|
}
|
|
132
|
+
|
|
133
|
+
const cookies = await context.cookies();
|
|
134
|
+
const eclassCookies = cookies.filter(c =>
|
|
135
|
+
c.domain.includes('yuntech.edu.tw') || c.domain.includes('eclass')
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (eclassCookies.length > 0) {
|
|
139
|
+
saveCookies(eclassCookies);
|
|
140
|
+
console.log(chalk.green(`\n[AUTH] 登入成功!自動取得 ${eclassCookies.length} 個 Cookie`));
|
|
141
|
+
console.log(chalk.green('[AUTH] 現在可以執行 tronclass run 開始自動化\n'));
|
|
142
|
+
} else {
|
|
143
|
+
console.log(chalk.yellow('\n[AUTH] 找不到 eclass Cookie,請確認已成功登入'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
clearInterval(checkInterval);
|
|
147
|
+
await browser.close();
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
if (checkCount > 300) {
|
package/src/tui.js
CHANGED
|
@@ -1,235 +1,241 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
|
-
const { loadConfig, saveConfig,
|
|
3
|
+
const { loadConfig, saveConfig, BASE_URL } = require('./config');
|
|
4
|
+
const { loadCookies } = require('./auth');
|
|
5
|
+
|
|
6
|
+
function box(lines, width = 52) {
|
|
7
|
+
const top = '╔' + '═'.repeat(width - 2) + '╗';
|
|
8
|
+
const bot = '╚' + '═'.repeat(width - 2) + '╝';
|
|
9
|
+
const pad = (s) => '║ ' + s.padEnd(width - 4) + ' ║';
|
|
10
|
+
console.log(chalk.cyan(top));
|
|
11
|
+
lines.forEach(l => console.log(chalk.cyan(pad(l))));
|
|
12
|
+
console.log(chalk.cyan(bot));
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
async function mainMenu() {
|
|
6
|
-
|
|
16
|
+
const cookies = loadCookies();
|
|
17
|
+
if (!cookies.length) {
|
|
18
|
+
console.log(chalk.red('\n 沒有 Cookie,請先執行 tronclass login\n'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
7
21
|
|
|
22
|
+
let running = true;
|
|
8
23
|
while (running) {
|
|
24
|
+
console.clear();
|
|
9
25
|
const config = loadConfig();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ name: chalk.green('▶ 開始自動觀看'), value: 'run' },
|
|
17
|
-
{ name: chalk.cyan('📋 管理課程列表'), value: 'courses' },
|
|
18
|
-
{ name: chalk.yellow('⚙ 設定'), value: 'settings' },
|
|
19
|
-
new inquirer.Separator(),
|
|
20
|
-
{ name: chalk.gray('🚪 離開'), value: 'exit' }
|
|
21
|
-
],
|
|
22
|
-
pageSize: 10
|
|
23
|
-
}
|
|
26
|
+
|
|
27
|
+
box([
|
|
28
|
+
chalk.bold.white('tronclass-auto v1.1.0'),
|
|
29
|
+
chalk.gray('自動觀看 eclass/TronClass 影片'),
|
|
30
|
+
'',
|
|
31
|
+
`課程: ${chalk.cyan(config.courses.length + ' 個')} | Cookie: ${chalk.green('✓')} | 倍速: ${chalk.yellow(config.playbackRate || 2)}x`,
|
|
24
32
|
]);
|
|
33
|
+
console.log('');
|
|
34
|
+
|
|
35
|
+
const { action } = await inquirer.prompt([{
|
|
36
|
+
type: 'list',
|
|
37
|
+
name: 'action',
|
|
38
|
+
message: '選擇操作:',
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: chalk.green('▶ 開始自動觀看'), value: 'run' },
|
|
41
|
+
{ name: chalk.cyan('📋 管理課程列表'), value: 'courses' },
|
|
42
|
+
{ name: chalk.yellow('📊 查看進度統計'), value: 'status' },
|
|
43
|
+
{ name: chalk.magenta('⚙ 設定'), value: 'settings' },
|
|
44
|
+
new inquirer.Separator(),
|
|
45
|
+
{ name: chalk.gray('🚪 離開'), value: 'exit' }
|
|
46
|
+
],
|
|
47
|
+
pageSize: 10
|
|
48
|
+
}]);
|
|
25
49
|
|
|
26
50
|
switch (action) {
|
|
27
|
-
case 'run':
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await
|
|
32
|
-
|
|
33
|
-
case 'settings':
|
|
34
|
-
await settingsMenu(config);
|
|
35
|
-
break;
|
|
36
|
-
case 'exit':
|
|
37
|
-
running = false;
|
|
51
|
+
case 'run': await runMenu(config); break;
|
|
52
|
+
case 'courses': await courseMenu(); break;
|
|
53
|
+
case 'status': {
|
|
54
|
+
const { showStatus } = require('./index');
|
|
55
|
+
await showStatus();
|
|
56
|
+
await pressAnyKey();
|
|
38
57
|
break;
|
|
58
|
+
}
|
|
59
|
+
case 'settings': await settingsMenu(); break;
|
|
60
|
+
case 'exit': running = false; break;
|
|
39
61
|
}
|
|
40
62
|
}
|
|
41
|
-
|
|
42
63
|
console.log(chalk.gray('\n再見!\n'));
|
|
43
64
|
}
|
|
44
65
|
|
|
45
66
|
async function runMenu(config) {
|
|
46
67
|
if (!config.courses.length) {
|
|
47
|
-
console.log(chalk.yellow('\n
|
|
68
|
+
console.log(chalk.yellow('\n 還沒有設定課程,請先新增\n'));
|
|
69
|
+
await pressAnyKey();
|
|
48
70
|
return;
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
const { confirm } = await inquirer.prompt([{
|
|
74
|
+
type: 'confirm',
|
|
75
|
+
name: 'confirm',
|
|
76
|
+
message: `開始觀看全部 ${config.courses.length} 個課程?`,
|
|
77
|
+
default: true
|
|
78
|
+
}]);
|
|
79
|
+
if (!confirm) return;
|
|
80
|
+
|
|
81
|
+
const { headless } = await inquirer.prompt([{
|
|
82
|
+
type: 'confirm',
|
|
83
|
+
name: 'headless',
|
|
84
|
+
message: '無頭模式(背景執行)?',
|
|
85
|
+
default: false
|
|
86
|
+
}]);
|
|
87
|
+
|
|
88
|
+
console.log('');
|
|
89
|
+
box([
|
|
90
|
+
chalk.green('開始自動觀看'),
|
|
91
|
+
`課程: ${chalk.cyan(config.courses.length)} 個`,
|
|
92
|
+
`模式: ${headless ? chalk.gray('無頭') : chalk.white('有頭')}`,
|
|
93
|
+
`倍速: ${chalk.yellow(config.playbackRate || 2)}x`,
|
|
65
94
|
]);
|
|
66
|
-
|
|
67
|
-
const { headless } = await inquirer.prompt([
|
|
68
|
-
{
|
|
69
|
-
type: 'confirm',
|
|
70
|
-
name: 'headless',
|
|
71
|
-
message: '無頭模式(背景執行)?',
|
|
72
|
-
default: false
|
|
73
|
-
}
|
|
74
|
-
]);
|
|
75
|
-
|
|
76
|
-
console.log(chalk.cyan('\n' + '='.repeat(50)));
|
|
77
|
-
console.log(chalk.cyan(' 開始自動觀看'));
|
|
78
|
-
console.log(chalk.cyan('='.repeat(50) + '\n'));
|
|
95
|
+
console.log('');
|
|
79
96
|
|
|
80
97
|
const { runWithSelection } = require('./index');
|
|
81
|
-
await runWithSelection(
|
|
82
|
-
|
|
83
|
-
console.log(chalk.green('\n 按任意鍵回到選單'));
|
|
84
|
-
await inquirer.prompt([{ type: 'input', name: '_press', message: '' }]);
|
|
98
|
+
await runWithSelection(config.courses, headless);
|
|
99
|
+
await pressAnyKey();
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
async function courseMenu(
|
|
102
|
+
async function courseMenu() {
|
|
88
103
|
let editing = true;
|
|
89
|
-
|
|
90
104
|
while (editing) {
|
|
105
|
+
const config = loadConfig();
|
|
91
106
|
const courses = config.courses || [];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ name: chalk.green('➕ 新增課程'), value: 'add' },
|
|
107
|
-
{ name: chalk.red('🗑 清空所有'), value: 'clear' },
|
|
108
|
-
new inquirer.Separator(),
|
|
109
|
-
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
110
|
-
],
|
|
111
|
-
pageSize: 20
|
|
112
|
-
}
|
|
107
|
+
|
|
108
|
+
console.clear();
|
|
109
|
+
box([
|
|
110
|
+
chalk.bold.white('📋 課程列表'),
|
|
111
|
+
...(courses.length === 0
|
|
112
|
+
? [chalk.gray(' 還沒有課程')]
|
|
113
|
+
: courses.map((url, i) => {
|
|
114
|
+
const match = url.match(/\/course\/(\d+)\//);
|
|
115
|
+
const id = match ? match[1] : '?';
|
|
116
|
+
return ` ${chalk.white(i + 1 + '.')} [${chalk.cyan(id)}] ${chalk.gray(url.substring(0, 45))}`;
|
|
117
|
+
})),
|
|
118
|
+
'',
|
|
119
|
+
chalk.gray(' 新增: tronclass course --add <ID或URL>'),
|
|
120
|
+
chalk.gray(' 移除: tronclass course --remove <編號>'),
|
|
113
121
|
]);
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
const { action } = await inquirer.prompt([{
|
|
125
|
+
type: 'list',
|
|
126
|
+
name: 'action',
|
|
127
|
+
message: `課程管理 (${courses.length} 個)`,
|
|
128
|
+
choices: [
|
|
129
|
+
{ name: chalk.green('➕ 新增課程(互動式)'), value: 'add' },
|
|
130
|
+
...(courses.length > 0 ? [
|
|
131
|
+
...courses.map((url, i) => {
|
|
132
|
+
const match = url.match(/\/course\/(\d+)\//);
|
|
133
|
+
const id = match ? match[1] : '?';
|
|
134
|
+
return { name: `${chalk.red('✕')} [${id}] 移除`, value: `remove_${i}` };
|
|
135
|
+
}),
|
|
136
|
+
new inquirer.Separator(),
|
|
137
|
+
{ name: chalk.red('🗑 清空所有'), value: 'clear' }
|
|
138
|
+
] : []),
|
|
139
|
+
new inquirer.Separator(),
|
|
140
|
+
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
141
|
+
],
|
|
142
|
+
pageSize: 20
|
|
143
|
+
}]);
|
|
114
144
|
|
|
115
145
|
if (action === 'back') {
|
|
116
146
|
editing = false;
|
|
117
147
|
} else if (action === 'add') {
|
|
118
|
-
const { input } = await inquirer.prompt([
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
saveConfig(config);
|
|
148
|
+
const { input } = await inquirer.prompt([{
|
|
149
|
+
type: 'input',
|
|
150
|
+
name: 'input',
|
|
151
|
+
message: '輸入課程 ID(如 127331)或完整 URL:',
|
|
152
|
+
validate: (v) => v.trim().length > 0 ? true : '不可為空'
|
|
153
|
+
}]);
|
|
154
|
+
const config2 = loadConfig();
|
|
155
|
+
config2.courses = config2.courses || [];
|
|
156
|
+
config2.courses.push(normalizeUrl(input.trim()));
|
|
157
|
+
saveConfig(config2);
|
|
129
158
|
console.log(chalk.green(' 已新增!'));
|
|
130
159
|
} else if (action === 'clear') {
|
|
131
|
-
const { confirm } = await inquirer.prompt([
|
|
132
|
-
|
|
133
|
-
]);
|
|
160
|
+
const { confirm } = await inquirer.prompt([{
|
|
161
|
+
type: 'confirm', name: 'confirm', message: '確定要清空所有課程?', default: false
|
|
162
|
+
}]);
|
|
134
163
|
if (confirm) {
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
const config2 = loadConfig();
|
|
165
|
+
config2.courses = [];
|
|
166
|
+
saveConfig(config2);
|
|
137
167
|
console.log(chalk.green(' 已清空'));
|
|
138
168
|
}
|
|
139
|
-
} else {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
choices: [
|
|
146
|
-
{ name: chalk.red('🗑 移除此課程'), value: 'remove' },
|
|
147
|
-
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
148
|
-
]
|
|
149
|
-
}
|
|
150
|
-
]);
|
|
151
|
-
if (subAction === 'remove') {
|
|
152
|
-
const removed = config.courses.splice(action, 1)[0];
|
|
153
|
-
saveConfig(config);
|
|
154
|
-
console.log(chalk.green(` 已移除: ${removed}`));
|
|
155
|
-
}
|
|
169
|
+
} else if (action.startsWith('remove_')) {
|
|
170
|
+
const idx = parseInt(action.split('_')[1]);
|
|
171
|
+
const config2 = loadConfig();
|
|
172
|
+
const removed = config2.courses.splice(idx, 1)[0];
|
|
173
|
+
saveConfig(config2);
|
|
174
|
+
console.log(chalk.green(` 已移除: ${removed}`));
|
|
156
175
|
}
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
|
|
160
|
-
async function settingsMenu(
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
]);
|
|
179
|
+
async function settingsMenu() {
|
|
180
|
+
const config = loadConfig();
|
|
181
|
+
|
|
182
|
+
const { setting } = await inquirer.prompt([{
|
|
183
|
+
type: 'list',
|
|
184
|
+
name: 'setting',
|
|
185
|
+
message: '設定',
|
|
186
|
+
choices: [
|
|
187
|
+
{ name: `倍速: ${chalk.cyan(config.playbackRate || 2)}x`, value: 'speed' },
|
|
188
|
+
{ name: `無頭模式: ${chalk.cyan(config.headless ? '是' : '否')}`, value: 'headless' },
|
|
189
|
+
{ name: `SlowMo: ${chalk.cyan(config.slowMo || 50)}ms`, value: 'slowmo' },
|
|
190
|
+
new inquirer.Separator(),
|
|
191
|
+
{ name: chalk.gray('⬅ 返回'), value: 'back' }
|
|
192
|
+
]
|
|
193
|
+
}]);
|
|
175
194
|
|
|
176
195
|
if (setting === 'back') return;
|
|
177
196
|
|
|
178
197
|
if (setting === 'speed') {
|
|
179
|
-
const { speed } = await inquirer.prompt([
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
name: '
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
],
|
|
191
|
-
default: config.playbackRate || 2
|
|
192
|
-
}
|
|
193
|
-
]);
|
|
198
|
+
const { speed } = await inquirer.prompt([{
|
|
199
|
+
type: 'list', name: 'speed', message: '選擇播放倍速:',
|
|
200
|
+
choices: [
|
|
201
|
+
{ name: '1x(正常)', value: 1 },
|
|
202
|
+
{ name: '1.5x', value: 1.5 },
|
|
203
|
+
{ name: '2x(推薦)', value: 2 },
|
|
204
|
+
{ name: '4x', value: 4 },
|
|
205
|
+
{ name: '8x', value: 8 }
|
|
206
|
+
],
|
|
207
|
+
default: config.playbackRate || 2
|
|
208
|
+
}]);
|
|
194
209
|
config.playbackRate = speed;
|
|
195
210
|
saveConfig(config);
|
|
196
211
|
console.log(chalk.green(` 已設定為 ${speed}x`));
|
|
197
212
|
} else if (setting === 'headless') {
|
|
198
|
-
const { hl } = await inquirer.prompt([
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
name: 'hl',
|
|
202
|
-
message: '啟用無頭模式?',
|
|
203
|
-
default: config.headless || false
|
|
204
|
-
}
|
|
205
|
-
]);
|
|
213
|
+
const { hl } = await inquirer.prompt([{
|
|
214
|
+
type: 'confirm', name: 'hl', message: '啟用無頭模式?', default: config.headless || false
|
|
215
|
+
}]);
|
|
206
216
|
config.headless = hl;
|
|
207
217
|
saveConfig(config);
|
|
208
|
-
console.log(chalk.green(` 無頭模式: ${hl ? '是' : '否'}`));
|
|
209
218
|
} else if (setting === 'slowmo') {
|
|
210
|
-
const { ms } = await inquirer.prompt([
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
name: 'ms',
|
|
214
|
-
message: 'SlowMo 毫秒數:',
|
|
215
|
-
default: config.slowMo || 50,
|
|
216
|
-
validate: (v) => v >= 0 ? true : '不可小於 0'
|
|
217
|
-
}
|
|
218
|
-
]);
|
|
219
|
+
const { ms } = await inquirer.prompt([{
|
|
220
|
+
type: 'number', name: 'ms', message: 'SlowMo 毫秒數:', default: config.slowMo || 50
|
|
221
|
+
}]);
|
|
219
222
|
config.slowMo = ms;
|
|
220
223
|
saveConfig(config);
|
|
221
|
-
console.log(chalk.green(` SlowMo: ${ms}ms`));
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
function normalizeUrl(input) {
|
|
226
|
-
if (
|
|
227
|
-
return
|
|
228
|
+
if (/^\d+$/.test(input)) {
|
|
229
|
+
return `${BASE_URL}/course/${input}/content#/`;
|
|
228
230
|
}
|
|
229
231
|
if (!input.startsWith('http')) {
|
|
230
|
-
return
|
|
232
|
+
return `${BASE_URL}/course/${input}/content#/`;
|
|
231
233
|
}
|
|
232
234
|
return input;
|
|
233
235
|
}
|
|
234
236
|
|
|
237
|
+
async function pressAnyKey() {
|
|
238
|
+
await inquirer.prompt([{ type: 'input', name: '_', message: chalk.gray('按 Enter 返回...') }]);
|
|
239
|
+
}
|
|
240
|
+
|
|
235
241
|
module.exports = { mainMenu };
|