tronclass-auto 1.2.1 → 1.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.2.1",
3
+ "version": "1.2.4",
4
4
  "description": "自動觀看 eclass/TronClass 影片、填寫表單的 CLI 工具",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -27,10 +27,28 @@ async function getUncompleted(page, courseId) {
27
27
  let completeness = '';
28
28
  try { completeness = scope.getActivityCompleteness(a); } catch (e) {}
29
29
  if (completeness !== 'full') {
30
+ let durationSec = 0;
31
+ try {
32
+ const durText = actEl.querySelector('.activity-attribute');
33
+ if (durText) {
34
+ const m = durText.textContent.match(/(\d{2}):(\d{2}):(\d{2})/);
35
+ if (m) durationSec = parseInt(m[1]) * 3600 + parseInt(m[2]) * 60 + parseInt(m[3]);
36
+ }
37
+ } catch (e) {}
38
+ if (durationSec === 0) {
39
+ try {
40
+ const durScope = angular.element(actEl).scope();
41
+ if (durScope && durScope.activity) {
42
+ const d = durScope.activity.data;
43
+ if (d && d.duration) durationSec = d.duration;
44
+ }
45
+ } catch (e) {}
46
+ }
30
47
  result.push({
31
48
  id: a.id,
32
49
  title: (a.title || '').substring(0, 60),
33
- type: a.type || ''
50
+ type: a.type || '',
51
+ durationSec
34
52
  });
35
53
  }
36
54
  });
@@ -49,9 +67,13 @@ async function processCourse(page, courseId, stats) {
49
67
 
50
68
  console.log(chalk.cyan(` Uncompleted: ${videos.length} videos, remaining: ${remaining.length}`));
51
69
 
70
+ const totalDuration = remaining.reduce((sum, v) => sum + (v.durationSec || 120), 0);
71
+ const estWatchTime = Math.ceil(totalDuration / 2) + remaining.length * 15;
72
+ console.log(chalk.cyan(` 預估時間: ${formatTime(estWatchTime)}`));
73
+
52
74
  if (stats) {
53
75
  stats.remainingVideos = remaining.length;
54
- stats.estimatedTimeForRemaining = remaining.reduce((sum, v) => sum + 120, 0);
76
+ stats.estimatedTimeForRemaining = estWatchTime;
55
77
  }
56
78
 
57
79
  for (let i = 0; i < remaining.length; i++) {
@@ -71,14 +93,17 @@ async function processCourse(page, courseId, stats) {
71
93
  }
72
94
 
73
95
  try {
96
+ const thisDuration = Math.ceil((act.durationSec || 120) / 2) + 15;
74
97
  if (stats) {
75
98
  stats.remainingVideos = remaining.length - i - 1;
99
+ stats.estimatedTimeForRemaining -= thisDuration;
100
+ if (stats.estimatedTimeForRemaining < 0) stats.estimatedTimeForRemaining = 0;
76
101
  }
77
102
  const success = await watchVideo(page, stats);
78
103
  if (success) {
79
104
  markDone(courseId, act.id);
80
105
  done++;
81
- console.log(chalk.green(' [OK] Saved'));
106
+ console.log(chalk.green(' ✓ Saved'));
82
107
  } else {
83
108
  fail++;
84
109
  }
package/src/video.js CHANGED
@@ -1,9 +1,12 @@
1
1
  const chalk = require('chalk');
2
2
 
3
3
  function formatTime(seconds) {
4
- const h = Math.floor(seconds / 3600);
4
+ seconds = Math.max(0, Math.floor(seconds));
5
+ const d = Math.floor(seconds / 86400);
6
+ const h = Math.floor((seconds % 86400) / 3600);
5
7
  const m = Math.floor((seconds % 3600) / 60);
6
- const s = Math.floor(seconds % 60);
8
+ const s = seconds % 60;
9
+ if (d > 0) return `${d}d ${h}h ${m}m`;
7
10
  if (h > 0) return `${h}h ${m}m ${s}s`;
8
11
  if (m > 0) return `${m}m ${s}s`;
9
12
  return `${s}s`;
@@ -12,11 +15,11 @@ function formatTime(seconds) {
12
15
  async function watchVideo(page, stats) {
13
16
  const hasVideo = await page.evaluate(() => document.querySelector('video') !== null);
14
17
  if (!hasVideo) {
15
- log(chalk.yellow(' [VIDEO] No video element'));
18
+ log(chalk.yellow(' No video element'));
16
19
  return false;
17
20
  }
18
21
 
19
- log(chalk.blue(' [VIDEO] Muted + 2x speed'));
22
+ log(chalk.blue(' Muted + 2x'));
20
23
 
21
24
  await page.evaluate(() => {
22
25
  document.querySelectorAll('video').forEach(v => {
@@ -35,7 +38,7 @@ async function watchVideo(page, stats) {
35
38
  });
36
39
 
37
40
  if (!isPlaying) {
38
- log(chalk.yellow(' [VIDEO] Clicking play...'));
41
+ log(chalk.yellow(' Clicking play...'));
39
42
  await page.evaluate(() => {
40
43
  document.querySelectorAll('[class*="play"], .vjs-big-play-button, button').forEach(btn => {
41
44
  if (btn.offsetParent && (btn.className.includes('play') || btn.className.includes('Play'))) {
@@ -53,14 +56,14 @@ async function watchVideo(page, stats) {
53
56
  });
54
57
 
55
58
  if (!duration || duration <= 0) {
56
- log(chalk.yellow(' [VIDEO] Cannot get duration, waiting 30s...'));
59
+ log(chalk.yellow(' Cannot get duration, waiting 30s...'));
57
60
  await page.waitForTimeout(30000);
58
61
  if (stats) stats.videosWatched++;
59
62
  return true;
60
63
  }
61
64
 
62
65
  const waitTime = (duration / 2) + 15;
63
- log(chalk.blue(` [VIDEO] ${formatTime(duration)} video, waiting ~${formatTime(waitTime)} (2x)`));
66
+ log(chalk.blue(` ${formatTime(duration)} video → ~${formatTime(waitTime)} (2x)`));
64
67
 
65
68
  if (stats) {
66
69
  stats.currentVideoDuration = duration / 2;
@@ -90,37 +93,48 @@ async function watchVideo(page, stats) {
90
93
 
91
94
  if (state.p > lastPct) {
92
95
  const pct = state.p;
96
+ const barWidth = 20;
97
+ const filled = Math.floor(barWidth * pct / 100);
98
+ const empty = barWidth - filled;
99
+ const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
93
100
  const remaining = Math.max(0, waitTime - elapsed);
94
101
  let etaStr = '';
95
102
  if (stats && stats.remainingVideos > 0) {
96
103
  const totalRemaining = remaining + stats.estimatedTimeForRemaining;
97
- etaStr = chalk.gray(` | ETA: ${formatTime(totalRemaining)}`);
104
+ etaStr = chalk.gray(` | ETA ${formatTime(totalRemaining)}`);
98
105
  } else {
99
- etaStr = chalk.gray(` | ~${formatTime(remaining)} left`);
106
+ etaStr = chalk.gray(` | ${formatTime(remaining)} left`);
100
107
  }
101
- log(chalk.blue(` [VIDEO] ${pct}% (${formatTime(elapsed)})${etaStr}`));
108
+ log(chalk.blue(` ${bar} ${String(pct).padStart(3)}% (${formatTime(elapsed)})${etaStr}`));
102
109
  lastPct = pct;
103
110
  }
104
111
 
105
112
  if (state.e) {
106
- log(chalk.green(' [VIDEO] 100% Finished'));
113
+ log(chalk.green(' Done ✓'));
107
114
  break;
108
115
  }
109
116
 
110
117
  if (state.stalled) {
111
118
  if (elapsed % 15 === 0) {
112
- log(chalk.gray(` [VIDEO] Buffering... (${formatTime(elapsed)})`));
119
+ log(chalk.gray(` Buffering... (${formatTime(elapsed)})`));
113
120
  }
114
121
  continue;
115
122
  }
116
123
 
117
- if (state.paused && elapsed > 5) {
124
+ if (state.paused && !state.stalled && elapsed > 10) {
125
+ await page.waitForTimeout(3000);
126
+ const stillPaused = await page.evaluate(() => {
127
+ const v = document.querySelector('video');
128
+ return v ? v.paused : false;
129
+ });
130
+ if (!stillPaused) continue;
131
+
118
132
  restartCount++;
119
133
  if (restartCount > 10) {
120
- log(chalk.red(' [VIDEO] Too many restarts, giving up'));
134
+ log(chalk.red(' Too many restarts, giving up'));
121
135
  return false;
122
136
  }
123
- log(chalk.yellow(` [VIDEO] Paused, restarting (#${restartCount})...`));
137
+ log(chalk.yellow(` Paused, restarting (#${restartCount})...`));
124
138
  await page.evaluate(() => document.querySelector('video').play());
125
139
  await page.waitForTimeout(1000);
126
140
  }