channel-worker 2.5.25 → 2.5.26

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.26",
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,48 @@ 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
+ const sels = [
119
+ '.ytp-ad-skip-button-modern',
120
+ '.ytp-ad-skip-button',
121
+ '.ytp-skip-ad-button',
122
+ 'button.ytp-ad-skip-button-modern',
123
+ '.ytp-ad-skip-button-container button',
124
+ ];
125
+ for (const s of sels) {
126
+ const btn = document.querySelector(s);
127
+ if (btn && btn.offsetParent !== null) { btn.click(); return true; }
128
+ }
129
+ // Text-based fallback (locale-proof) — any visible button whose label is a
130
+ // skip-ad phrase.
131
+ const phrases = /bỏ qua quảng cáo|bỏ qua|skip ad|skip ads|skip/i;
132
+ for (const b of document.querySelectorAll('button, .ytp-ad-skip-button-text')) {
133
+ const t = (b.innerText || b.textContent || '').trim();
134
+ if (t && phrases.test(t) && b.offsetParent !== null) { b.click(); return true; }
135
+ }
136
+ return false;
137
+ }).catch(() => false);
138
+ }
139
+
140
+ // True while an ad is playing on the watch page (pre/mid-roll). Used so the
141
+ // watch budget only counts REAL video time, not ad time.
142
+ async function isAdShowing(page) {
143
+ return page.evaluate(() => {
144
+ const player = document.querySelector('#movie_player, .html5-video-player');
145
+ if (player && player.classList.contains('ad-showing')) return true;
146
+ return !!document.querySelector('.ytp-ad-player-overlay, .ytp-ad-module, .ad-showing');
147
+ }).catch(() => false);
148
+ }
149
+
150
+ // Watch one video: navigate, SKIP any ads (so we watch the real video, not the
151
+ // ad), then let the real content play for a random span — counting only
152
+ // non-ad time toward the budget — with a small mid-view scroll like a real
153
+ // viewer. Best-effort — a single failed video never aborts the session.
115
154
  async function watchVideo(page, url, minSec, maxSec, log) {
116
155
  try {
117
156
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
@@ -121,13 +160,31 @@ async function watchVideo(page, url, minSec, maxSec, log) {
121
160
  const v = document.querySelector('video');
122
161
  if (v && v.paused) { try { v.play(); } catch {} }
123
162
  }).catch(() => {});
163
+
124
164
  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);
165
+ log('info', `[warmup] watching ${url.slice(-11)} for ~${Math.round(watchMs / 1000)}s of REAL video (skipping ads)`);
166
+
167
+ const TICK = 1000;
168
+ const MAX_AD_WAIT_MS = 90_000; // cap total ad-waiting so a non-skippable ad reel can't hang the session
169
+ let realWatched = 0; // ms of actual (non-ad) playback counted
170
+ let adWaited = 0; // ms spent sitting through/again skipping ads
171
+ let scrolled = false;
172
+ let skips = 0;
173
+
174
+ while (realWatched < watchMs) {
175
+ const adOn = await isAdShowing(page);
176
+ if (adOn) {
177
+ if (await trySkipAd(page)) skips++;
178
+ adWaited += TICK;
179
+ if (adWaited >= MAX_AD_WAIT_MS) { log('warn', `[warmup] ${url.slice(-11)} ads exceeded ${MAX_AD_WAIT_MS/1000}s — moving on`); break; }
180
+ } else {
181
+ realWatched += TICK;
182
+ // Mid-view scroll once, roughly halfway through real watch time.
183
+ if (!scrolled && realWatched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
184
+ }
185
+ await page.waitForTimeout(TICK);
186
+ }
187
+ if (skips) log('info', `[warmup] ${url.slice(-11)} — skipped ${skips} ad tick(s), real watch ${Math.round(realWatched/1000)}s`);
131
188
  return true;
132
189
  } catch (e) {
133
190
  log('warn', `[warmup] watch failed (${url.slice(-11)}): ${String(e.message || e).slice(0, 100)}`);