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.
@@ -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.0",
3
+ "version": "1.0.2",
4
4
  "description": "自動觀看 eclass/TronClass 影片、填寫表單的 CLI 工具",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "tronclass": "./bin/eclass.js"
7
+ "tronclass": "bin/tronclass.js"
8
8
  },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
9
14
  "scripts": {
10
- "start": "node bin/eclass.js run"
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({ headless: false, slowMo: 50 });
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] 現在可以執行 eclass run 開始自動化\n'));
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,請先執行 eclass login 或 eclass import-cookies'));
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 browser = await chromium.launch({ headless: true, slowMo: 50 });
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(' 請執行: eclass login (自動取得 Cookie)'));
139
- console.log(chalk.yellow(' 或: eclass import-cookies "session=xxx"'));
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 browser = await chromium.launch({
148
- headless: config.headless,
149
- slowMo: config.slowMo || 50
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'