tronclass-auto 1.4.5 → 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 +81 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tronclass-auto",
3
- "version": "1.4.5",
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,59 +60,120 @@ async function watchYouTube(page, stats) {
60
60
  let rate = 1;
61
61
 
62
62
  try {
63
- const ytFrame = page.locator('iframe[src*="youtube"], iframe[src*="youtu.be"]');
64
- await ytFrame.click({ timeout: 5000 }).catch(() => {});
65
- await page.waitForTimeout(3000);
66
-
67
63
  await page.evaluate(() => {
68
64
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
69
- if (!f) return;
70
- f.contentWindow.postMessage('{"event":"listening"}', '*');
65
+ if (f) f.scrollIntoView({ block: 'center' });
71
66
  });
72
67
  await page.waitForTimeout(2000);
73
68
 
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: [] }), '*');
78
- });
79
- await page.waitForTimeout(1000);
69
+ const ytFrame = page.locator('iframe[src*="youtube"], iframe[src*="youtu.be"]');
70
+ await ytFrame.click({ timeout: 5000 }).catch(() => {});
71
+ await page.waitForTimeout(5000);
80
72
 
81
73
  await page.evaluate(() => {
82
74
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
83
75
  if (!f) return;
84
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'mute', args: [] }), '*');
76
+ f.contentWindow.postMessage('{"event":"listening"}', '*');
85
77
  });
86
- await page.waitForTimeout(1000);
78
+ await page.waitForTimeout(3000);
87
79
 
88
80
  await page.evaluate(() => {
89
81
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
90
82
  if (!f) return;
91
- f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'setPlaybackRate', args: [2] }), '*');
83
+ f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
92
84
  });
93
- await page.waitForTimeout(2000);
85
+ await page.waitForTimeout(5000);
94
86
 
95
87
  rate = await page.evaluate(() => {
96
88
  return new Promise((resolve) => {
97
89
  const f = document.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"]');
98
90
  if (!f) { resolve(1); return; }
91
+
92
+ let resolved = false;
99
93
  const handler = (e) => {
100
94
  try {
101
95
  const d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
102
- if (d.event === 'infoDelivery' && d.info && d.info.playbackRate) {
96
+ if (d.event === 'infoDelivery' && d.info && !resolved) {
97
+ resolved = true;
103
98
  window.removeEventListener('message', handler);
104
- resolve(d.info.playbackRate);
99
+ resolve(d.info.playbackRate || 1);
105
100
  }
106
101
  } catch(e) {}
107
102
  };
108
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] }), '*');
109
107
  f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'getPlaybackRate', args: [] }), '*');
110
- setTimeout(() => { window.removeEventListener('message', handler); resolve(1); }, 3000);
108
+
109
+ setTimeout(() => { if (!resolved) { resolved = true; window.removeEventListener('message', handler); resolve(1); } }, 5000);
111
110
  });
112
111
  });
113
112
  log(chalk.blue(` Speed: ${rate}x`));
113
+
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
+ });
134
+
135
+ if (isPlaying) {
136
+ log(chalk.green(' Playing ✓'));
137
+ } 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'));
174
+ }
114
175
  } catch (e) {
115
- log(chalk.yellow(` YouTube setup error: ${e.message}`));
176
+ log(chalk.yellow(` YouTube error: ${e.message}`));
116
177
  }
117
178
 
118
179
  await page.waitForTimeout(2000);