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 +1 -1
- package/scripts/warmup_youtube.js +68 -9
package/package.json
CHANGED
|
@@ -109,9 +109,50 @@ 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
|
+
// 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
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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)}`);
|