tronclass-auto 1.4.6 → 1.5.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/index.js +46 -47
  3. package/src/video.js +77 -60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.4.6",
3
+ "version": "1.5.1",
4
4
  "description": "自動觀看 eclass/TronClass 影片、填寫表單的 CLI 工具",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -81,15 +81,14 @@ async function processCourse(page, courseId, stats) {
81
81
  let skip = 0;
82
82
  let fail = 0;
83
83
  let totalProcessed = 0;
84
- let serverRejects = {};
85
- let consecutiveLocked = 0;
84
+ let triedIds = new Set();
86
85
 
87
86
  console.log(chalk.cyan(` [fetching course activities...]`));
88
87
 
89
88
  while (true) {
90
89
  const uncompleted = await getUncompleted(page, courseId);
91
90
  const videos = uncompleted.filter(u => u.type === 'online_video');
92
- const remaining = videos.filter(v => !isDone(courseId, v.id));
91
+ const remaining = videos.filter(v => !isDone(courseId, v.id) && !triedIds.has(v.id));
93
92
 
94
93
  if (remaining.length === 0) break;
95
94
 
@@ -115,8 +114,9 @@ async function processCourse(page, courseId, stats) {
115
114
 
116
115
  if (!page.url().includes('learning-activity')) {
117
116
  console.log(chalk.yellow(' Page redirect, skipping'));
118
- markLocked(courseId, act.id);
119
- break;
117
+ triedIds.add(act.id);
118
+ totalProcessed++;
119
+ continue;
120
120
  }
121
121
 
122
122
  const playerType = await page.evaluate(() => {
@@ -128,25 +128,22 @@ async function processCourse(page, courseId, stats) {
128
128
  }
129
129
  return 'none';
130
130
  });
131
+
131
132
  if (playerType === 'none') {
132
- console.log(chalk.yellow(' No video, waiting more...'));
133
+ console.log(chalk.yellow(' No player, waiting more...'));
133
134
  await page.waitForTimeout(10000);
134
135
  const hasVideoNow = await page.evaluate(() => {
135
- if (document.querySelector('video')) return true;
136
+ if (document.querySelector('video')) return 'html5';
136
137
  const yt = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="vimeo"]');
137
- if (yt) return true;
138
- for (const f of document.querySelectorAll('iframe')) {
139
- try { if (f.contentDocument && f.contentDocument.querySelector('video')) return true; } catch (e) {}
140
- }
141
- return false;
138
+ if (yt) return 'iframe';
139
+ return 'none';
142
140
  });
143
- if (!hasVideoNow) {
144
- console.log(chalk.yellow(' Still no video, skipping (not a video activity)'));
145
- markLocked(courseId, act.id);
146
- break;
141
+ if (hasVideoNow === 'none') {
142
+ console.log(chalk.yellow(' No player found, skipping'));
143
+ triedIds.add(act.id);
144
+ totalProcessed++;
145
+ continue;
147
146
  }
148
- } else {
149
- consecutiveLocked = 0;
150
147
  }
151
148
 
152
149
  try {
@@ -158,48 +155,50 @@ async function processCourse(page, courseId, stats) {
158
155
  }
159
156
  const success = await watchVideo(page, stats);
160
157
  if (success) {
161
- await page.waitForTimeout(5000);
162
- await page.goto(`${BASE_URL}/course/${courseId}/content#/`, {
163
- waitUntil: 'domcontentloaded', timeout: 15000
164
- }).catch(() => {});
165
- await page.waitForTimeout(3000);
166
-
167
- const serverComplete = await page.evaluate((actId) => {
168
- const el = document.querySelector('.learning-activities');
169
- if (!el) return false;
170
- const scope = angular.element(el).scope();
171
- if (!scope) return false;
172
- const activities = document.querySelectorAll('.learning-activity.sortable');
173
- for (const actEl of activities) {
174
- const actScope = angular.element(actEl).scope();
175
- if (!actScope || !actScope.activity) continue;
176
- if (actScope.activity.id == actId) {
177
- 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
+ }
178
178
  }
179
- }
180
- return false;
181
- }, act.id);
179
+ return false;
180
+ }, act.id);
181
+
182
+ if (serverComplete) break;
183
+ console.log(chalk.gray(` Checking server... (${attempt + 1}/5)`));
184
+ }
182
185
 
183
186
  if (serverComplete) {
184
187
  markDone(courseId, act.id);
185
188
  done++;
186
189
  console.log(chalk.green(' ✓ Saved (server confirmed)'));
187
190
  } else {
188
- serverRejects[act.id] = (serverRejects[act.id] || 0) + 1;
189
- if (serverRejects[act.id] >= 2) {
190
- console.log(chalk.yellow(' Server rejected twice, marking done locally'));
191
- markDone(courseId, act.id);
192
- done++;
193
- } else {
194
- console.log(chalk.yellow(' Server not confirmed, will retry...'));
195
- fail++;
196
- }
191
+ console.log(chalk.yellow(' Server not confirmed after retries, skipping'));
192
+ triedIds.add(act.id);
193
+ fail++;
197
194
  }
198
195
  } else {
196
+ triedIds.add(act.id);
199
197
  fail++;
200
198
  }
201
199
  } catch (e) {
202
200
  console.log(chalk.red(` [ERROR] ${e.message}`));
201
+ triedIds.add(act.id);
203
202
  fail++;
204
203
  }
205
204
 
package/src/video.js CHANGED
@@ -60,32 +60,64 @@ async function watchYouTube(page, stats) {
60
60
  let rate = 1;
61
61
 
62
62
  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);
68
+
63
69
  const ytFrame = page.locator('iframe[src*="youtube"], iframe[src*="youtu.be"]');
64
70
  await ytFrame.click({ timeout: 5000 }).catch(() => {});
65
71
  await page.waitForTimeout(5000);
66
72
 
67
- await page.evaluate(() => {
68
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
69
- if (!f) return;
70
- f.contentWindow.postMessage('{"event":"listening"}', '*');
71
- });
72
- await page.waitForTimeout(3000);
73
+ const postMsg = async (msg) => {
74
+ await page.evaluate((m) => {
75
+ const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
76
+ if (f) f.contentWindow.postMessage(m, '*');
77
+ }, typeof msg === 'string' ? msg : JSON.stringify(msg));
78
+ };
73
79
 
74
- await page.evaluate(() => {
75
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
76
- if (!f) return;
77
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
80
+ await postMsg('{"event":"listening"}');
81
+ await page.waitForTimeout(2000);
82
+ await postMsg({ event: 'command', func: 'mute', args: [] });
83
+ await page.waitForTimeout(1000);
84
+ await postMsg({ event: 'command', func: 'setPlaybackRate', args: [2] });
85
+ await page.waitForTimeout(2000);
86
+ await postMsg({ event: 'command', func: 'playVideo', args: [] });
87
+ await page.waitForTimeout(5000);
88
+
89
+ rate = await page.evaluate(() => {
90
+ return new Promise((resolve) => {
91
+ const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
92
+ if (!f) { resolve(1); return; }
93
+ let resolved = false;
94
+ const handler = (e) => {
95
+ try {
96
+ const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
97
+ if (d.event === 'infoDelivery' && d.info && d.info.playbackRate !== undefined && !resolved) {
98
+ resolved = true;
99
+ window.removeEventListener('message', handler);
100
+ resolve(d.info.playbackRate);
101
+ }
102
+ } catch(e) {}
103
+ };
104
+ window.addEventListener('message', handler);
105
+ f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getPlaybackRate', args: [] }), '*');
106
+ setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(1); } }, 5000);
107
+ });
78
108
  });
79
- await page.waitForTimeout(3000);
109
+ log(chalk.blue(` Speed: ${rate}x`));
80
110
 
81
111
  const isPlaying = await page.evaluate(() => {
82
112
  return new Promise((resolve) => {
83
113
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
84
114
  if (!f) { resolve(false); return; }
115
+ let resolved = false;
85
116
  const handler = (e) => {
86
117
  try {
87
118
  const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
88
- if (d.event === 'infoDelivery' && d.info) {
119
+ if (d.event === 'infoDelivery' && d.info && !resolved) {
120
+ resolved = true;
89
121
  window.removeEventListener('message', handler);
90
122
  resolve(d.info.currentTime > 0);
91
123
  }
@@ -93,67 +125,52 @@ async function watchYouTube(page, stats) {
93
125
  };
94
126
  window.addEventListener('message', handler);
95
127
  f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getCurrentTime', args: [] }), '*');
96
- setTimeout(() => { window.removeEventListener('message', handler); resolve(false); }, 5000);
128
+ setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(false); } }, 5000);
97
129
  });
98
130
  });
99
131
 
100
- if (!isPlaying) {
101
- log(chalk.yellow(' Player not responding, retrying click...'));
102
- try {
103
- const frame = page.frameLocator('iframe[src*="youtube"], iframe[src*="youtu.be"]');
104
- await frame.locator('.ytp-large-play-button, .ytp-play-button').first().click({ timeout: 5000 }).catch(() => {});
105
- } catch(e) {}
106
- await page.waitForTimeout(3000);
107
-
132
+ if (isPlaying) {
133
+ log(chalk.green(' Playing ✓'));
134
+ } else {
135
+ log(chalk.yellow(' Not playing, retrying...'));
108
136
  await page.evaluate(() => {
109
137
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
110
138
  if (!f) return;
111
139
  f.contentWindow.postMessage('{"event":"listening"}', '*');
112
140
  });
113
- await page.waitForTimeout(2000);
141
+ await page.waitForTimeout(3000);
114
142
  await page.evaluate(() => {
115
143
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
116
144
  if (!f) return;
117
145
  f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
118
146
  });
119
- await page.waitForTimeout(3000);
120
- }
121
-
122
- await page.evaluate(() => {
123
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
124
- if (!f) return;
125
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'mute', args: [] }), '*');
126
- });
127
- await page.waitForTimeout(1000);
128
-
129
- await page.evaluate(() => {
130
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
131
- if (!f) return;
132
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'setPlaybackRate', args: [2] }), '*');
133
- });
134
- await page.waitForTimeout(2000);
135
-
136
- rate = await page.evaluate(() => {
137
- return new Promise((resolve) => {
138
- const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
139
- if (!f) { resolve(1); return; }
140
- const handler = (e) => {
141
- try {
142
- const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
143
- if (d.event === 'infoDelivery' && d.info && d.info.playbackRate) {
144
- window.removeEventListener('message', handler);
145
- resolve(d.info.playbackRate);
146
- }
147
- } catch(e) {}
148
- };
149
- window.addEventListener('message', handler);
150
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getPlaybackRate', args: [] }), '*');
151
- setTimeout(() => { window.removeEventListener('message', handler); resolve(1); }, 3000);
147
+ await page.waitForTimeout(5000);
148
+
149
+ const retry = await page.evaluate(() => {
150
+ return new Promise((resolve) => {
151
+ const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
152
+ if (!f) { resolve(false); return; }
153
+ let resolved = false;
154
+ const handler = (e) => {
155
+ try {
156
+ const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
157
+ if (d.event === 'infoDelivery' && d.info && !resolved) {
158
+ resolved = true;
159
+ window.removeEventListener('message', handler);
160
+ resolve(d.info.currentTime > 0);
161
+ }
162
+ } catch(e) {}
163
+ };
164
+ window.addEventListener('message', handler);
165
+ f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getCurrentTime', args: [] }), '*');
166
+ setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(false); } }, 5000);
167
+ });
152
168
  });
153
- });
154
- log(chalk.blue(` Speed: ${rate}x`));
169
+ if (retry) log(chalk.green(' Playing ✓'));
170
+ else log(chalk.yellow(' Could not verify playback'));
171
+ }
155
172
  } catch (e) {
156
- log(chalk.yellow(` YouTube setup error: ${e.message}`));
173
+ log(chalk.yellow(` YouTube error: ${e.message}`));
157
174
  }
158
175
 
159
176
  await page.waitForTimeout(2000);
@@ -167,8 +184,8 @@ async function watchYouTube(page, stats) {
167
184
  return 180;
168
185
  });
169
186
 
170
- const waitTime = ytDuration + 30;
171
- log(chalk.blue(` ~${formatTime(ytDuration)} video → ~${formatTime(waitTime)} (2x playback, full wait)`));
187
+ const waitTime = (rate >= 2 ? Math.ceil(ytDuration / 2) : ytDuration) + 30;
188
+ log(chalk.blue(` ~${formatTime(ytDuration)} video → ~${formatTime(waitTime)} (${rate}x)`));
172
189
 
173
190
  if (stats) {
174
191
  stats.currentVideoDuration = ytDuration;