tronclass-auto 1.4.6 → 1.5.0

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 +22 -29
  3. package/src/video.js +68 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.4.6",
3
+ "version": "1.5.0",
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,11 +155,11 @@ 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);
158
+ await page.waitForTimeout(10000);
162
159
  await page.goto(`${BASE_URL}/course/${courseId}/content#/`, {
163
160
  waitUntil: 'domcontentloaded', timeout: 15000
164
161
  }).catch(() => {});
165
- await page.waitForTimeout(3000);
162
+ await page.waitForTimeout(5000);
166
163
 
167
164
  const serverComplete = await page.evaluate((actId) => {
168
165
  const el = document.querySelector('.learning-activities');
@@ -185,21 +182,17 @@ async function processCourse(page, courseId, stats) {
185
182
  done++;
186
183
  console.log(chalk.green(' ✓ Saved (server confirmed)'));
187
184
  } 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
- }
185
+ console.log(chalk.yellow(' Server not confirmed, skipping'));
186
+ triedIds.add(act.id);
187
+ fail++;
197
188
  }
198
189
  } else {
190
+ triedIds.add(act.id);
199
191
  fail++;
200
192
  }
201
193
  } catch (e) {
202
194
  console.log(chalk.red(` [ERROR] ${e.message}`));
195
+ triedIds.add(act.id);
203
196
  fail++;
204
197
  }
205
198
 
package/src/video.js CHANGED
@@ -60,6 +60,12 @@ 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);
@@ -76,16 +82,45 @@ async function watchYouTube(page, stats) {
76
82
  if (!f) return;
77
83
  f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
78
84
  });
79
- await page.waitForTimeout(3000);
85
+ await page.waitForTimeout(5000);
86
+
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`));
80
113
 
81
114
  const isPlaying = await page.evaluate(() => {
82
115
  return new Promise((resolve) => {
83
116
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
84
117
  if (!f) { resolve(false); return; }
118
+ let resolved = false;
85
119
  const handler = (e) => {
86
120
  try {
87
121
  const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
88
- if (d.event === 'infoDelivery' && d.info) {
122
+ if (d.event === 'infoDelivery' && d.info && !resolved) {
123
+ resolved = true;
89
124
  window.removeEventListener('message', handler);
90
125
  resolve(d.info.currentTime > 0);
91
126
  }
@@ -93,67 +128,52 @@ async function watchYouTube(page, stats) {
93
128
  };
94
129
  window.addEventListener('message', handler);
95
130
  f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getCurrentTime', args: [] }), '*');
96
- setTimeout(() => { window.removeEventListener('message', handler); resolve(false); }, 5000);
131
+ setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(false); } }, 5000);
97
132
  });
98
133
  });
99
134
 
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
-
135
+ if (isPlaying) {
136
+ log(chalk.green(' Playing ✓'));
137
+ } else {
138
+ log(chalk.yellow(' Not playing, retrying...'));
108
139
  await page.evaluate(() => {
109
140
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
110
141
  if (!f) return;
111
142
  f.contentWindow.postMessage('{"event":"listening"}', '*');
112
143
  });
113
- await page.waitForTimeout(2000);
144
+ await page.waitForTimeout(3000);
114
145
  await page.evaluate(() => {
115
146
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
116
147
  if (!f) return;
117
148
  f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
118
149
  });
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);
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
+ });
152
171
  });
153
- });
154
- log(chalk.blue(` Speed: ${rate}x`));
172
+ if (retry) log(chalk.green(' Playing ✓'));
173
+ else log(chalk.yellow(' Could not verify playback'));
174
+ }
155
175
  } catch (e) {
156
- log(chalk.yellow(` YouTube setup error: ${e.message}`));
176
+ log(chalk.yellow(` YouTube error: ${e.message}`));
157
177
  }
158
178
 
159
179
  await page.waitForTimeout(2000);