tronclass-auto 1.1.0 → 1.2.0

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/auth.js +40 -18
  3. package/src/tui.js +173 -161
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "自動觀看 eclass/TronClass 影片、填寫表單的 CLI 工具",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 (!url.includes('login') && !url.includes('auth') && !url.includes('cas')) {
108
- if (!saved) {
109
- saved = true;
110
- const cookies = await context.cookies();
111
- const eclassCookies = cookies.filter(c =>
112
- c.domain.includes('yuntech.edu.tw') || c.domain.includes('eclass')
113
- );
114
-
115
- if (eclassCookies.length > 0) {
116
- saveCookies(eclassCookies);
117
- console.log(chalk.green(`\n[AUTH] 登入成功!自動取得 ${eclassCookies.length} 個 Cookie`));
118
- console.log(chalk.green('[AUTH] 現在可以執行 tronclass run 開始自動化\n'));
119
- } else {
120
- console.log(chalk.yellow('\n[AUTH] 找不到 eclass Cookie,請確認已成功登入'));
121
- }
122
-
123
- clearInterval(checkInterval);
124
- await browser.close();
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,50 +1,72 @@
1
1
  const inquirer = require('inquirer');
2
2
  const chalk = require('chalk');
3
- const { loadConfig, saveConfig, manageCourses } = require('./config');
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
- let running = true;
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
- const { action } = await inquirer.prompt([
11
- {
12
- type: 'list',
13
- name: 'action',
14
- message: 'tronclass-auto',
15
- choices: [
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
- await runMenu(config);
29
- break;
30
- case 'courses':
31
- await courseMenu(config);
32
- break;
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 還沒有設定課程,請先新增課程\n'));
68
+ console.log(chalk.yellow('\n 還沒有設定課程,請先新增\n'));
69
+ await pressAnyKey();
48
70
  return;
49
71
  }
50
72
 
@@ -54,182 +76,172 @@ async function runMenu(config) {
54
76
  return { name: `[${id}] ${url}`, value: i, checked: true };
55
77
  });
56
78
 
57
- const { selectedCourses } = await inquirer.prompt([
58
- {
59
- type: 'checkbox',
60
- name: 'selectedCourses',
61
- message: '選擇要觀看的課程:',
62
- choices: courseChoices,
63
- validate: (ans) => ans.length > 0 ? true : '至少選一個課程'
64
- }
79
+ const { selectedCourses } = await inquirer.prompt([{
80
+ type: 'checkbox',
81
+ name: 'selectedCourses',
82
+ message: '選擇要觀看的課程:',
83
+ choices: courseChoices,
84
+ validate: (ans) => ans.length > 0 ? true : '至少選一個課程'
85
+ }]);
86
+
87
+ const { headless } = await inquirer.prompt([{
88
+ type: 'confirm',
89
+ name: 'headless',
90
+ message: '無頭模式(背景執行)?',
91
+ default: false
92
+ }]);
93
+
94
+ console.log('');
95
+ box([
96
+ chalk.green('開始自動觀看'),
97
+ `課程: ${chalk.cyan(selectedCourses.length)} 個`,
98
+ `模式: ${headless ? chalk.gray('無頭') : chalk.white('有頭')}`,
99
+ `倍速: ${chalk.yellow(config.playbackRate || 2)}x`,
65
100
  ]);
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'));
101
+ console.log('');
79
102
 
80
103
  const { runWithSelection } = require('./index');
81
104
  await runWithSelection(selectedCourses.map(i => config.courses[i]), headless);
82
-
83
- console.log(chalk.green('\n 按任意鍵回到選單'));
84
- await inquirer.prompt([{ type: 'input', name: '_press', message: '' }]);
105
+ await pressAnyKey();
85
106
  }
86
107
 
87
- async function courseMenu(config) {
108
+ async function courseMenu() {
88
109
  let editing = true;
89
-
90
110
  while (editing) {
111
+ const config = loadConfig();
91
112
  const courses = config.courses || [];
92
- const choices = courses.map((url, i) => {
93
- const match = url.match(/\/course\/(\d+)\//);
94
- const id = match ? match[1] : '?';
95
- return { name: `${chalk.white(i + 1 + '.')} [${id}] ${chalk.gray(url)}`, value: i };
96
- });
97
-
98
- const { action } = await inquirer.prompt([
99
- {
100
- type: 'list',
101
- name: 'action',
102
- message: `課程列表 (${courses.length})`,
103
- choices: [
104
- ...choices,
105
- new inquirer.Separator(),
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
- }
113
+
114
+ console.clear();
115
+ box([
116
+ chalk.bold.white('📋 課程列表'),
117
+ ...(courses.length === 0
118
+ ? [chalk.gray(' 還沒有課程')]
119
+ : courses.map((url, i) => {
120
+ const match = url.match(/\/course\/(\d+)\//);
121
+ const id = match ? match[1] : '?';
122
+ return ` ${chalk.white(i + 1 + '.')} [${chalk.cyan(id)}] ${chalk.gray(url.substring(0, 45))}`;
123
+ })),
124
+ '',
125
+ chalk.gray(' 新增: tronclass course --add <ID或URL>'),
126
+ chalk.gray(' 移除: tronclass course --remove <編號>'),
113
127
  ]);
128
+ console.log('');
129
+
130
+ const { action } = await inquirer.prompt([{
131
+ type: 'list',
132
+ name: 'action',
133
+ message: `課程管理 (${courses.length} 個)`,
134
+ choices: [
135
+ { name: chalk.green('➕ 新增課程(互動式)'), value: 'add' },
136
+ ...(courses.length > 0 ? [
137
+ ...courses.map((url, i) => {
138
+ const match = url.match(/\/course\/(\d+)\//);
139
+ const id = match ? match[1] : '?';
140
+ return { name: `${chalk.red('✕')} [${id}] 移除`, value: `remove_${i}` };
141
+ }),
142
+ new inquirer.Separator(),
143
+ { name: chalk.red('🗑 清空所有'), value: 'clear' }
144
+ ] : []),
145
+ new inquirer.Separator(),
146
+ { name: chalk.gray('⬅ 返回'), value: 'back' }
147
+ ],
148
+ pageSize: 20
149
+ }]);
114
150
 
115
151
  if (action === 'back') {
116
152
  editing = false;
117
153
  } else if (action === 'add') {
118
- const { input } = await inquirer.prompt([
119
- {
120
- type: 'input',
121
- name: 'input',
122
- message: '輸入課程 ID URL:',
123
- validate: (v) => v.trim().length > 0 ? true : '不可為空'
124
- }
125
- ]);
126
- config.courses = config.courses || [];
127
- config.courses.push(normalizeUrl(input.trim()));
128
- saveConfig(config);
154
+ const { input } = await inquirer.prompt([{
155
+ type: 'input',
156
+ name: 'input',
157
+ message: '輸入課程 ID(如 127331)或完整 URL:',
158
+ validate: (v) => v.trim().length > 0 ? true : '不可為空'
159
+ }]);
160
+ const config2 = loadConfig();
161
+ config2.courses = config2.courses || [];
162
+ config2.courses.push(normalizeUrl(input.trim()));
163
+ saveConfig(config2);
129
164
  console.log(chalk.green(' 已新增!'));
130
165
  } else if (action === 'clear') {
131
- const { confirm } = await inquirer.prompt([
132
- { type: 'confirm', name: 'confirm', message: '確定要清空所有課程?', default: false }
133
- ]);
166
+ const { confirm } = await inquirer.prompt([{
167
+ type: 'confirm', name: 'confirm', message: '確定要清空所有課程?', default: false
168
+ }]);
134
169
  if (confirm) {
135
- config.courses = [];
136
- saveConfig(config);
170
+ const config2 = loadConfig();
171
+ config2.courses = [];
172
+ saveConfig(config2);
137
173
  console.log(chalk.green(' 已清空'));
138
174
  }
139
- } else {
140
- const { subAction } = await inquirer.prompt([
141
- {
142
- type: 'list',
143
- name: 'subAction',
144
- message: courses[action],
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
- }
175
+ } else if (action.startsWith('remove_')) {
176
+ const idx = parseInt(action.split('_')[1]);
177
+ const config2 = loadConfig();
178
+ const removed = config2.courses.splice(idx, 1)[0];
179
+ saveConfig(config2);
180
+ console.log(chalk.green(` 已移除: ${removed}`));
156
181
  }
157
182
  }
158
183
  }
159
184
 
160
- async function settingsMenu(config) {
161
- const { setting } = await inquirer.prompt([
162
- {
163
- type: 'list',
164
- name: 'setting',
165
- message: '設定',
166
- choices: [
167
- { name: `倍速: ${chalk.cyan(config.playbackRate || 2)}x`, value: 'speed' },
168
- { name: `無頭模式: ${chalk.cyan(config.headless ? '是' : '否')}`, value: 'headless' },
169
- { name: `SlowMo: ${chalk.cyan(config.slowMo || 50)}ms`, value: 'slowmo' },
170
- new inquirer.Separator(),
171
- { name: chalk.gray('⬅ 返回'), value: 'back' }
172
- ]
173
- }
174
- ]);
185
+ async function settingsMenu() {
186
+ const config = loadConfig();
187
+
188
+ const { setting } = await inquirer.prompt([{
189
+ type: 'list',
190
+ name: 'setting',
191
+ message: '設定',
192
+ choices: [
193
+ { name: `倍速: ${chalk.cyan(config.playbackRate || 2)}x`, value: 'speed' },
194
+ { name: `無頭模式: ${chalk.cyan(config.headless ? '是' : '否')}`, value: 'headless' },
195
+ { name: `SlowMo: ${chalk.cyan(config.slowMo || 50)}ms`, value: 'slowmo' },
196
+ new inquirer.Separator(),
197
+ { name: chalk.gray('⬅ 返回'), value: 'back' }
198
+ ]
199
+ }]);
175
200
 
176
201
  if (setting === 'back') return;
177
202
 
178
203
  if (setting === 'speed') {
179
- const { speed } = await inquirer.prompt([
180
- {
181
- type: 'list',
182
- name: 'speed',
183
- message: '選擇播放倍速:',
184
- choices: [
185
- { name: '1x(正常)', value: 1 },
186
- { name: '1.5x', value: 1.5 },
187
- { name: '2x(推薦)', value: 2 },
188
- { name: '4x', value: 4 },
189
- { name: '8x', value: 8 }
190
- ],
191
- default: config.playbackRate || 2
192
- }
193
- ]);
204
+ const { speed } = await inquirer.prompt([{
205
+ type: 'list', name: 'speed', message: '選擇播放倍速:',
206
+ choices: [
207
+ { name: '1x(正常)', value: 1 },
208
+ { name: '1.5x', value: 1.5 },
209
+ { name: '2x(推薦)', value: 2 },
210
+ { name: '4x', value: 4 },
211
+ { name: '8x', value: 8 }
212
+ ],
213
+ default: config.playbackRate || 2
214
+ }]);
194
215
  config.playbackRate = speed;
195
216
  saveConfig(config);
196
217
  console.log(chalk.green(` 已設定為 ${speed}x`));
197
218
  } else if (setting === 'headless') {
198
- const { hl } = await inquirer.prompt([
199
- {
200
- type: 'confirm',
201
- name: 'hl',
202
- message: '啟用無頭模式?',
203
- default: config.headless || false
204
- }
205
- ]);
219
+ const { hl } = await inquirer.prompt([{
220
+ type: 'confirm', name: 'hl', message: '啟用無頭模式?', default: config.headless || false
221
+ }]);
206
222
  config.headless = hl;
207
223
  saveConfig(config);
208
- console.log(chalk.green(` 無頭模式: ${hl ? '是' : '否'}`));
209
224
  } else if (setting === 'slowmo') {
210
- const { ms } = await inquirer.prompt([
211
- {
212
- type: 'number',
213
- name: 'ms',
214
- message: 'SlowMo 毫秒數:',
215
- default: config.slowMo || 50,
216
- validate: (v) => v >= 0 ? true : '不可小於 0'
217
- }
218
- ]);
225
+ const { ms } = await inquirer.prompt([{
226
+ type: 'number', name: 'ms', message: 'SlowMo 毫秒數:', default: config.slowMo || 50
227
+ }]);
219
228
  config.slowMo = ms;
220
229
  saveConfig(config);
221
- console.log(chalk.green(` SlowMo: ${ms}ms`));
222
230
  }
223
231
  }
224
232
 
225
233
  function normalizeUrl(input) {
226
- if (input.match(/^\d+$/)) {
227
- return `https://eclass.yuntech.edu.tw/course/${input}/content#/`;
234
+ if (/^\d+$/.test(input)) {
235
+ return `${BASE_URL}/course/${input}/content#/`;
228
236
  }
229
237
  if (!input.startsWith('http')) {
230
- return `https://eclass.yuntech.edu.tw/course/${input}/content#/`;
238
+ return `${BASE_URL}/course/${input}/content#/`;
231
239
  }
232
240
  return input;
233
241
  }
234
242
 
243
+ async function pressAnyKey() {
244
+ await inquirer.prompt([{ type: 'input', name: '_', message: chalk.gray('按 Enter 返回...') }]);
245
+ }
246
+
235
247
  module.exports = { mainMenu };