tronclass-auto 1.5.0 → 1.5.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +27 -21
  3. package/src/video.js +43 -111
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "自動觀看 eclass/TronClass 影片、填寫表單的 CLI 工具",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -155,34 +155,40 @@ async function processCourse(page, courseId, stats) {
155
155
  }
156
156
  const success = await watchVideo(page, stats);
157
157
  if (success) {
158
- await page.waitForTimeout(10000);
159
- await page.goto(`${BASE_URL}/course/${courseId}/content#/`, {
160
- waitUntil: 'domcontentloaded', timeout: 15000
161
- }).catch(() => {});
162
- await page.waitForTimeout(5000);
163
-
164
- const serverComplete = await page.evaluate((actId) => {
165
- const el = document.querySelector('.learning-activities');
166
- if (!el) return false;
167
- const scope = angular.element(el).scope();
168
- if (!scope) return false;
169
- const activities = document.querySelectorAll('.learning-activity.sortable');
170
- for (const actEl of activities) {
171
- const actScope = angular.element(actEl).scope();
172
- if (!actScope || !actScope.activity) continue;
173
- if (actScope.activity.id == actId) {
174
- try { return scope.getActivityCompleteness(actScope.activity) === 'full'; } catch (e) {}
158
+ let serverComplete = false;
159
+ for (let attempt = 0; attempt < 5; attempt++) {
160
+ await page.waitForTimeout(10000);
161
+ await page.goto(`${BASE_URL}/course/${courseId}/content#/`, {
162
+ waitUntil: 'domcontentloaded', timeout: 15000
163
+ }).catch(() => {});
164
+ await page.waitForTimeout(5000);
165
+
166
+ serverComplete = await page.evaluate((actId) => {
167
+ const el = document.querySelector('.learning-activities');
168
+ if (!el) return false;
169
+ const scope = angular.element(el).scope();
170
+ if (!scope) return false;
171
+ const activities = document.querySelectorAll('.learning-activity.sortable');
172
+ for (const actEl of activities) {
173
+ const actScope = angular.element(actEl).scope();
174
+ if (!actScope || !actScope.activity) continue;
175
+ if (actScope.activity.id == actId) {
176
+ try { return scope.getActivityCompleteness(actScope.activity) === 'full'; } catch (e) {}
177
+ }
175
178
  }
176
- }
177
- return false;
178
- }, act.id);
179
+ return false;
180
+ }, act.id);
181
+
182
+ if (serverComplete) break;
183
+ console.log(chalk.gray(` Checking server... (${attempt + 1}/5)`));
184
+ }
179
185
 
180
186
  if (serverComplete) {
181
187
  markDone(courseId, act.id);
182
188
  done++;
183
189
  console.log(chalk.green(' ✓ Saved (server confirmed)'));
184
190
  } else {
185
- console.log(chalk.yellow(' Server not confirmed, skipping'));
191
+ console.log(chalk.yellow(' Server not confirmed after retries, skipping'));
186
192
  triedIds.add(act.id);
187
193
  fail++;
188
194
  }
package/src/video.js CHANGED
@@ -25,18 +25,22 @@ async function detectPlayerType(page) {
25
25
  const v = document.querySelector('video');
26
26
  if (v) return 'html5';
27
27
 
28
+ const yt = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="vimeo"]');
29
+ if (yt) return 'youtube';
30
+
28
31
  for (const f of document.querySelectorAll('iframe')) {
29
- const src = (f.src || f.getAttribute('data-src') || '').toLowerCase();
30
- if (src.includes('youtube') || src.includes('youtu.be')) return 'youtube';
31
- if (src.includes('vimeo')) return 'vimeo';
32
32
  try {
33
- if (f.contentDocument) {
34
- const fv = f.contentDocument.querySelector('video');
35
- if (fv) return 'html5';
36
- }
33
+ if (f.contentDocument && f.contentDocument.querySelector('video')) return 'html5';
37
34
  } catch (e) {}
38
35
  }
39
36
  return 'none';
37
+ }).then(async (type) => {
38
+ if (type === 'none') {
39
+ for (const f of page.frames) {
40
+ if (f.url && (f.url.includes('youtube') || f.url.includes('youtu.be'))) return 'youtube';
41
+ }
42
+ }
43
+ return type;
40
44
  });
41
45
  }
42
46
 
@@ -60,117 +64,45 @@ async function watchYouTube(page, stats) {
60
64
  let rate = 1;
61
65
 
62
66
  try {
63
- await page.evaluate(() => {
64
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
65
- if (f) f.scrollIntoView({ block: 'center' });
66
- });
67
- await page.waitForTimeout(2000);
67
+ let ytFrame = null;
68
+ for (const f of page.frames) {
69
+ if (f.url && (f.url.includes('youtube') || f.url.includes('youtu.be'))) {
70
+ ytFrame = f;
71
+ break;
72
+ }
73
+ }
68
74
 
69
- const ytFrame = page.locator('iframe[src*="youtube"], iframe[src*="youtu.be"]');
70
- await ytFrame.click({ timeout: 5000 }).catch(() => {});
71
- await page.waitForTimeout(5000);
75
+ if (!ytFrame) {
76
+ log(chalk.yellow(' YouTube frame not found'));
77
+ return false;
78
+ }
72
79
 
73
- await page.evaluate(() => {
74
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
75
- if (!f) return;
76
- f.contentWindow.postMessage('{"event":"listening"}', '*');
77
- });
80
+ const evalYT = async (code) => {
81
+ return await ytFrame.evaluate(code).catch(() => null);
82
+ };
83
+
84
+ log(chalk.blue(' Starting video...'));
85
+ await evalYT("() => { const p = document.querySelector('#movie_player'); if (p) p.playVideo(); }");
78
86
  await page.waitForTimeout(3000);
79
87
 
80
- await page.evaluate(() => {
81
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
82
- if (!f) return;
83
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
84
- });
85
- await page.waitForTimeout(5000);
88
+ const state1 = await evalYT("() => { const p = document.querySelector('#movie_player'); return p ? p.getPlayerState() : -1; }");
89
+ if (state1 !== 1) {
90
+ log(chalk.yellow(' Retrying play...'));
91
+ await evalYT("() => { const p = document.querySelector('#movie_player'); if (p) p.playVideo(); }");
92
+ await page.waitForTimeout(3000);
93
+ }
86
94
 
87
- rate = await page.evaluate(() => {
88
- return new Promise((resolve) => {
89
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
90
- if (!f) { resolve(1); return; }
91
-
92
- let resolved = false;
93
- const handler = (e) => {
94
- try {
95
- const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
96
- if (d.event === 'infoDelivery' && d.info && !resolved) {
97
- resolved = true;
98
- window.removeEventListener('message', handler);
99
- resolve(d.info.playbackRate || 1);
100
- }
101
- } catch(e) {}
102
- };
103
- window.addEventListener('message', handler);
104
-
105
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'mute', args: [] }), '*');
106
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'setPlaybackRate', args: [2] }), '*');
107
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getPlaybackRate', args: [] }), '*');
108
-
109
- setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(1); } }, 5000);
110
- });
111
- });
112
- log(chalk.blue(` Speed: ${rate}x`));
95
+ await evalYT("() => { const p = document.querySelector('#movie_player'); if (p) { p.mute(); p.setPlaybackRate(2); } }");
96
+ await page.waitForTimeout(2000);
113
97
 
114
- const isPlaying = await page.evaluate(() => {
115
- return new Promise((resolve) => {
116
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
117
- if (!f) { resolve(false); return; }
118
- let resolved = false;
119
- const handler = (e) => {
120
- try {
121
- const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
122
- if (d.event === 'infoDelivery' && d.info && !resolved) {
123
- resolved = true;
124
- window.removeEventListener('message', handler);
125
- resolve(d.info.currentTime > 0);
126
- }
127
- } catch(e) {}
128
- };
129
- window.addEventListener('message', handler);
130
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getCurrentTime', args: [] }), '*');
131
- setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(false); } }, 5000);
132
- });
133
- });
98
+ rate = await evalYT("() => { const p = document.querySelector('#movie_player'); return p ? p.getPlaybackRate() : 1; }") || 1;
99
+ log(chalk.blue(` Speed: ${rate}x`));
134
100
 
135
- if (isPlaying) {
101
+ const state2 = await evalYT("() => { const p = document.querySelector('#movie_player'); return p ? p.getPlayerState() : -1; }");
102
+ if (state2 === 1) {
136
103
  log(chalk.green(' Playing ✓'));
137
104
  } else {
138
- log(chalk.yellow(' Not playing, retrying...'));
139
- await page.evaluate(() => {
140
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
141
- if (!f) return;
142
- f.contentWindow.postMessage('{"event":"listening"}', '*');
143
- });
144
- await page.waitForTimeout(3000);
145
- await page.evaluate(() => {
146
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
147
- if (!f) return;
148
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
149
- });
150
- await page.waitForTimeout(5000);
151
-
152
- const retry = await page.evaluate(() => {
153
- return new Promise((resolve) => {
154
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
155
- if (!f) { resolve(false); return; }
156
- let resolved = false;
157
- const handler = (e) => {
158
- try {
159
- const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
160
- if (d.event === 'infoDelivery' && d.info && !resolved) {
161
- resolved = true;
162
- window.removeEventListener('message', handler);
163
- resolve(d.info.currentTime > 0);
164
- }
165
- } catch(e) {}
166
- };
167
- window.addEventListener('message', handler);
168
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getCurrentTime', args: [] }), '*');
169
- setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(false); } }, 5000);
170
- });
171
- });
172
- if (retry) log(chalk.green(' Playing ✓'));
173
- else log(chalk.yellow(' Could not verify playback'));
105
+ log(chalk.yellow(` State: ${state2} (may still be loading)`));
174
106
  }
175
107
  } catch (e) {
176
108
  log(chalk.yellow(` YouTube error: ${e.message}`));
@@ -187,8 +119,8 @@ async function watchYouTube(page, stats) {
187
119
  return 180;
188
120
  });
189
121
 
190
- const waitTime = ytDuration + 30;
191
- log(chalk.blue(` ~${formatTime(ytDuration)} video → ~${formatTime(waitTime)} (2x playback, full wait)`));
122
+ const waitTime = (rate >= 2 ? Math.ceil(ytDuration / 2) : ytDuration) + 30;
123
+ log(chalk.blue(` ~${formatTime(ytDuration)} video → ~${formatTime(waitTime)} (${rate}x)`));
192
124
 
193
125
  if (stats) {
194
126
  stats.currentVideoDuration = ytDuration;