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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/auth.js +40 -18
  3. package/src/tui.js +174 -168
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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,235 +1,241 @@
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
 
51
- const courseChoices = config.courses.map((url, i) => {
52
- const match = url.match(/\/course\/(\d+)\//);
53
- const id = match ? match[1] : '?';
54
- return { name: `[${id}] ${url}`, value: i, checked: true };
55
- });
56
-
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
- }
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(selectedCourses.map(i => config.courses[i]), headless);
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(config) {
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
- 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
- }
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
- 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);
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
- { type: 'confirm', name: 'confirm', message: '確定要清空所有課程?', default: false }
133
- ]);
160
+ const { confirm } = await inquirer.prompt([{
161
+ type: 'confirm', name: 'confirm', message: '確定要清空所有課程?', default: false
162
+ }]);
134
163
  if (confirm) {
135
- config.courses = [];
136
- saveConfig(config);
164
+ const config2 = loadConfig();
165
+ config2.courses = [];
166
+ saveConfig(config2);
137
167
  console.log(chalk.green(' 已清空'));
138
168
  }
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
- }
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(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
- ]);
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
- 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
- ]);
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
- type: 'confirm',
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
- type: 'number',
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 (input.match(/^\d+$/)) {
227
- return `https://eclass.yuntech.edu.tw/course/${input}/content#/`;
228
+ if (/^\d+$/.test(input)) {
229
+ return `${BASE_URL}/course/${input}/content#/`;
228
230
  }
229
231
  if (!input.startsWith('http')) {
230
- return `https://eclass.yuntech.edu.tw/course/${input}/content#/`;
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 };