channel-worker 2.5.29 → 2.5.31
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_facebook.js +51 -8
- package/scripts/warmup_youtube.js +30 -0
package/package.json
CHANGED
|
@@ -231,8 +231,25 @@ async function clickResultByHref(page, href) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Read the open reel/video's playback state — used to advance early when it
|
|
235
|
+
// ENDS or STALLS instead of idling until watch_time elapses.
|
|
236
|
+
async function readVideoState(page) {
|
|
237
|
+
return page.evaluate(() => {
|
|
238
|
+
const v = document.querySelector('video');
|
|
239
|
+
if (!v) return null;
|
|
240
|
+
if (v.paused && !v.ended) { try { v.play(); } catch {} }
|
|
241
|
+
return {
|
|
242
|
+
ended: !!v.ended,
|
|
243
|
+
currentTime: Number(v.currentTime) || 0,
|
|
244
|
+
duration: (isFinite(v.duration) && v.duration > 0) ? v.duration : 0,
|
|
245
|
+
loop: !!v.loop,
|
|
246
|
+
};
|
|
247
|
+
}).catch(() => null);
|
|
248
|
+
}
|
|
249
|
+
|
|
234
250
|
// Watch whatever reel/video is currently open (after clicking a result): ensure
|
|
235
|
-
// the <video> plays, watch a random span
|
|
251
|
+
// the <video> plays, watch a random span — but ADVANCE EARLY if a non-looping
|
|
252
|
+
// video ends/stalls (don't sit on a finished video). Mid-view scroll. Best-effort.
|
|
236
253
|
async function watchCurrent(page, minSec, maxSec, log) {
|
|
237
254
|
try {
|
|
238
255
|
await page.waitForTimeout(randInt(2500, 4500)); // let the reel viewer mount
|
|
@@ -244,12 +261,27 @@ async function watchCurrent(page, minSec, maxSec, log) {
|
|
|
244
261
|
return true;
|
|
245
262
|
}).catch(() => false);
|
|
246
263
|
if (!hasVideo) { log('info', '[warmup-fb] no <video> after click — skipping'); return false; }
|
|
264
|
+
|
|
247
265
|
const watchMs = randInt(minSec, maxSec) * 1000;
|
|
248
266
|
log('info', `[warmup-fb] watching reel for ~${Math.round(watchMs / 1000)}s (url=${page.url().slice(-28)})`);
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
const TICK = 1000;
|
|
268
|
+
let watched = 0, scrolled = false, lastT = -1, stallTicks = 0;
|
|
269
|
+
while (watched < watchMs) {
|
|
270
|
+
const st = await readVideoState(page);
|
|
271
|
+
// A non-looping video that ended/stalled → stop waiting, go to next.
|
|
272
|
+
if (st && !st.loop) {
|
|
273
|
+
if (st.ended || (st.duration > 0 && st.currentTime >= st.duration - 0.6)) {
|
|
274
|
+
log('info', `[warmup-fb] reel ended at ${Math.round(st.currentTime)}s — advancing`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (Math.abs(st.currentTime - lastT) < 0.15) { stallTicks++; if (stallTicks >= 6) { log('info', '[warmup-fb] reel stalled — advancing'); break; } }
|
|
278
|
+
else stallTicks = 0;
|
|
279
|
+
lastT = st.currentTime;
|
|
280
|
+
}
|
|
281
|
+
if (!scrolled && watched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
|
|
282
|
+
await page.waitForTimeout(TICK);
|
|
283
|
+
watched += TICK;
|
|
284
|
+
}
|
|
253
285
|
return true;
|
|
254
286
|
} catch (e) {
|
|
255
287
|
log('info', `[warmup-fb] watch failed: ${String(e.message || e).slice(0, 90)}`);
|
|
@@ -289,6 +321,7 @@ async function run({ page, payload, log }) {
|
|
|
289
321
|
|
|
290
322
|
const keywordsSearched = [];
|
|
291
323
|
let videosWatched = 0;
|
|
324
|
+
const watchedGlobal = new Set(); // dedupe reels across ALL keywords this session
|
|
292
325
|
|
|
293
326
|
for (const kw of sessionKeywords) {
|
|
294
327
|
// 3) Search the keyword via the top bar (from the Reels surface).
|
|
@@ -313,15 +346,25 @@ async function run({ page, payload, log }) {
|
|
|
313
346
|
continue; // top-bar search persists; next keyword searches from here
|
|
314
347
|
}
|
|
315
348
|
log('info', `[warmup-fb] "${kw}" → ${urls.length} results, opening up to ${videosPerKeyword}`);
|
|
316
|
-
const watched = new Set();
|
|
317
349
|
for (let n = 0; n < videosPerKeyword; n++) {
|
|
318
350
|
// Re-collect each iteration — clicking + goBack mutates the DOM.
|
|
319
351
|
const current = await collectResultUrls(page);
|
|
320
|
-
const next = current.find(u => !
|
|
352
|
+
const next = current.find(u => !watchedGlobal.has(u.split('?')[0]));
|
|
321
353
|
if (!next) break;
|
|
322
|
-
|
|
354
|
+
watchedGlobal.add(next.split('?')[0]);
|
|
355
|
+
const urlBefore = page.url();
|
|
323
356
|
const clicked = await clickResultByHref(page, next);
|
|
324
357
|
if (!clicked) { log('info', `[warmup-fb] couldn't click result ${next.slice(-24)} — skipping`); continue; }
|
|
358
|
+
// Verify the click actually navigated into a reel/video — otherwise we'd
|
|
359
|
+
// re-watch whatever was already open (saw the same reel id reused across
|
|
360
|
+
// keywords). Wait up to 8s for the URL to change to /reel/ or /watch.
|
|
361
|
+
let navigated = false;
|
|
362
|
+
for (let w = 0; w < 8; w++) {
|
|
363
|
+
await page.waitForTimeout(1000);
|
|
364
|
+
const u = page.url();
|
|
365
|
+
if (u !== urlBefore && /\/(reel|watch|videos)\//.test(u)) { navigated = true; break; }
|
|
366
|
+
}
|
|
367
|
+
if (!navigated) { log('info', `[warmup-fb] click didn't navigate (still ${page.url().slice(-30)}) — skipping`); continue; }
|
|
325
368
|
const ok = await watchCurrent(page, watchMin, watchMax, log);
|
|
326
369
|
if (ok) videosWatched++;
|
|
327
370
|
// Back to the results list for the next pick (Escape closes a reel overlay
|
|
@@ -145,6 +145,23 @@ async function isAdShowing(page) {
|
|
|
145
145
|
}).catch(() => false);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Read the main <video> playback state. Used to advance early when a video
|
|
149
|
+
// ENDS or STALLS instead of sitting idle on a finished video until watch_time
|
|
150
|
+
// elapses (the "video chạy hết rồi không nhảy" report). Returns null if no video.
|
|
151
|
+
async function readVideoState(page) {
|
|
152
|
+
return page.evaluate(() => {
|
|
153
|
+
const v = document.querySelector('video');
|
|
154
|
+
if (!v) return null;
|
|
155
|
+
if (v.paused && !v.ended) { try { v.play(); } catch {} } // nudge autoplay
|
|
156
|
+
return {
|
|
157
|
+
ended: !!v.ended,
|
|
158
|
+
currentTime: Number(v.currentTime) || 0,
|
|
159
|
+
duration: (isFinite(v.duration) && v.duration > 0) ? v.duration : 0,
|
|
160
|
+
loop: !!v.loop,
|
|
161
|
+
};
|
|
162
|
+
}).catch(() => null);
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
// Watch one video: navigate, SKIP any ads (so we watch the real video, not the
|
|
149
166
|
// ad), then let the real content play for a random span — counting only
|
|
150
167
|
// non-ad time toward the budget — with a small mid-view scroll like a real
|
|
@@ -168,6 +185,7 @@ async function watchVideo(page, url, minSec, maxSec, log) {
|
|
|
168
185
|
let adWaited = 0; // ms spent sitting through/again skipping ads
|
|
169
186
|
let scrolled = false;
|
|
170
187
|
let skips = 0;
|
|
188
|
+
let lastT = -1, stallTicks = 0;
|
|
171
189
|
|
|
172
190
|
while (realWatched < watchMs) {
|
|
173
191
|
const adOn = await isAdShowing(page);
|
|
@@ -177,6 +195,18 @@ async function watchVideo(page, url, minSec, maxSec, log) {
|
|
|
177
195
|
if (adWaited >= MAX_AD_WAIT_MS) { log('warn', `[warmup] ${url.slice(-11)} ads exceeded ${MAX_AD_WAIT_MS/1000}s — moving on`); break; }
|
|
178
196
|
} else {
|
|
179
197
|
realWatched += TICK;
|
|
198
|
+
// Advance early if the (non-looping) video ENDED or STALLED, instead of
|
|
199
|
+
// idling on a finished video until watch_time runs out.
|
|
200
|
+
const st = await readVideoState(page);
|
|
201
|
+
if (st && !st.loop) {
|
|
202
|
+
if (st.ended || (st.duration > 0 && st.currentTime >= st.duration - 0.6)) {
|
|
203
|
+
log('info', `[warmup] ${url.slice(-11)} ended at ${Math.round(st.currentTime)}s — advancing`);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
if (Math.abs(st.currentTime - lastT) < 0.15) { stallTicks++; if (stallTicks >= 6) { log('info', `[warmup] ${url.slice(-11)} stalled — advancing`); break; } }
|
|
207
|
+
else stallTicks = 0;
|
|
208
|
+
lastT = st.currentTime;
|
|
209
|
+
}
|
|
180
210
|
// Mid-view scroll once, roughly halfway through real watch time.
|
|
181
211
|
if (!scrolled && realWatched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
|
|
182
212
|
}
|