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 +1 -1
- package/scripts/warmup_youtube.js +66 -9
package/package.json
CHANGED
|
@@ -109,9 +109,48 @@ async function collectResultVideoUrls(page) {
|
|
|
109
109
|
}).catch(() => []);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
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
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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)}`);
|