channel-worker 2.5.25 → 2.5.27

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": "channel-worker",
3
- "version": "2.5.25",
3
+ "version": "2.5.27",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -109,9 +109,50 @@ async function collectResultVideoUrls(page) {
109
109
  }).catch(() => []);
110
110
  }
111
111
 
112
- // Watch one video: navigate, let it play for a random span, do a small scroll
113
- // (into comments/related) like a real viewer. Best-effort a single failed
114
- // video never aborts the session.
112
+ // Click "Bỏ qua quảng cáo" / "Skip Ad" if present + actionable. Returns true if
113
+ // a skip was clicked this tick. The button only becomes clickable ~5s into a
114
+ // skippable ad; non-skippable ads have no button (we wait them out). Multiple
115
+ // pre-rolls can chain, so this is called every tick of the watch loop.
116
+ async function trySkipAd(page) {
117
+ return page.evaluate(() => {
118
+ // ONLY click inside the player's real skip-ad button (stable YT classes).
119
+ // A loose text match like /skip/i clicked random buttons every tick on
120
+ // videos with no ad — keep it strict to the ad-skip control.
121
+ const sels = [
122
+ '.ytp-ad-skip-button-modern',
123
+ '.ytp-ad-skip-button',
124
+ '.ytp-skip-ad-button',
125
+ '.ytp-ad-skip-button-container button',
126
+ ];
127
+ for (const s of sels) {
128
+ const btn = document.querySelector(s);
129
+ if (btn && btn.offsetParent !== null) { btn.click(); return true; }
130
+ }
131
+ // Strict text fallback — require an ad-specific phrase, NOT bare "skip"/"bỏ qua".
132
+ const phrases = /bỏ qua quảng cáo|skip ad\b|skip ads/i;
133
+ for (const b of document.querySelectorAll('.ytp-ad-skip-button-text, .ytp-ad-skip-button button, button')) {
134
+ const t = (b.innerText || b.textContent || '').trim();
135
+ if (t && phrases.test(t) && b.offsetParent !== null) { b.click(); return true; }
136
+ }
137
+ return false;
138
+ }).catch(() => false);
139
+ }
140
+
141
+ // True while an ad is ACTUALLY playing (pre/mid-roll). Relies on the player's
142
+ // `ad-showing` class — the canonical signal YouTube toggles only during ad
143
+ // playback. Do NOT key off .ytp-ad-module (a persistent container that exists
144
+ // even with no ad → false positive that pinned the watch loop at "0s real").
145
+ async function isAdShowing(page) {
146
+ return page.evaluate(() => {
147
+ const player = document.querySelector('#movie_player, .html5-video-player');
148
+ return !!(player && player.classList.contains('ad-showing'));
149
+ }).catch(() => false);
150
+ }
151
+
152
+ // Watch one video: navigate, SKIP any ads (so we watch the real video, not the
153
+ // ad), then let the real content play for a random span — counting only
154
+ // non-ad time toward the budget — with a small mid-view scroll like a real
155
+ // viewer. Best-effort — a single failed video never aborts the session.
115
156
  async function watchVideo(page, url, minSec, maxSec, log) {
116
157
  try {
117
158
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
@@ -121,13 +162,31 @@ async function watchVideo(page, url, minSec, maxSec, log) {
121
162
  const v = document.querySelector('video');
122
163
  if (v && v.paused) { try { v.play(); } catch {} }
123
164
  }).catch(() => {});
165
+
124
166
  const watchMs = randInt(minSec, maxSec) * 1000;
125
- log('info', `[warmup] watching ${url.slice(-11)} for ~${Math.round(watchMs / 1000)}s`);
126
- // Split the watch time so we can scroll mid-view (human behaviour).
127
- const half = Math.floor(watchMs / 2);
128
- await page.waitForTimeout(half);
129
- await organicScroll(page, randInt(1, 2));
130
- await page.waitForTimeout(watchMs - half);
167
+ log('info', `[warmup] watching ${url.slice(-11)} for ~${Math.round(watchMs / 1000)}s of REAL video (skipping ads)`);
168
+
169
+ const TICK = 1000;
170
+ const MAX_AD_WAIT_MS = 60_000; // cap total ad-waiting so a non-skippable ad reel can't hang the session
171
+ let realWatched = 0; // ms of actual (non-ad) playback counted
172
+ let adWaited = 0; // ms spent sitting through/again skipping ads
173
+ let scrolled = false;
174
+ let skips = 0;
175
+
176
+ while (realWatched < watchMs) {
177
+ const adOn = await isAdShowing(page);
178
+ if (adOn) {
179
+ if (await trySkipAd(page)) skips++;
180
+ adWaited += TICK;
181
+ if (adWaited >= MAX_AD_WAIT_MS) { log('warn', `[warmup] ${url.slice(-11)} ads exceeded ${MAX_AD_WAIT_MS/1000}s — moving on`); break; }
182
+ } else {
183
+ realWatched += TICK;
184
+ // Mid-view scroll once, roughly halfway through real watch time.
185
+ if (!scrolled && realWatched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
186
+ }
187
+ await page.waitForTimeout(TICK);
188
+ }
189
+ if (skips) log('info', `[warmup] ${url.slice(-11)} — skipped ${skips} ad tick(s), real watch ${Math.round(realWatched/1000)}s`);
131
190
  return true;
132
191
  } catch (e) {
133
192
  log('warn', `[warmup] watch failed (${url.slice(-11)}): ${String(e.message || e).slice(0, 100)}`);