channel-worker 2.5.28 → 2.5.30
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/lib/api-client.js +1 -1
- package/package.json +1 -1
- package/scripts/warmup_facebook.js +353 -0
package/lib/api-client.js
CHANGED
|
@@ -68,7 +68,7 @@ class ApiClient {
|
|
|
68
68
|
async getNextCommand(workerId) {
|
|
69
69
|
// Daemon-handled types. `_pw` variants route to the Playwright pipeline
|
|
70
70
|
// (lib/playwright-runner → scripts/<base>.js) instead of the extension.
|
|
71
|
-
const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,warmup_youtube_pw,scrape_affiliate_products,ingest_shopee_product';
|
|
71
|
+
const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,scrape_affiliate_products,ingest_shopee_product';
|
|
72
72
|
return this.request('GET', `/workers/commands?worker_id=${workerId}&types=${encodeURIComponent(workerTypes)}`);
|
|
73
73
|
}
|
|
74
74
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// Facebook page warmup — algorithm seeding for new channels.
|
|
2
|
+
//
|
|
3
|
+
// Runs on the channel's own NST profile (logged-in FB session, own residential
|
|
4
|
+
// proxy → FB sees organic per-account activity, not one IP crawling many).
|
|
5
|
+
// One run = one "session". Natural flow (per requirement — NO deep-linking to
|
|
6
|
+
// search URLs):
|
|
7
|
+
// home (facebook.com) → scroll a bit → click the "Reels" entry → from there
|
|
8
|
+
// use the top search bar to search each niche keyword → open + watch a few
|
|
9
|
+
// result reels/videos → back.
|
|
10
|
+
//
|
|
11
|
+
// Contract: run({ page, payload, log }) → { keywords_searched, videos_watched,
|
|
12
|
+
// duration_sec }. payload = { keywords[], config{...}, platform:'facebook' }.
|
|
13
|
+
//
|
|
14
|
+
// NOT a publish/engagement flow — only search + watch + scroll (no like /
|
|
15
|
+
// follow / comment). Best-effort per keyword/video; the search box + Reels
|
|
16
|
+
// entry are hard requirements — their absence (not logged in / consent wall /
|
|
17
|
+
// layout change) FAILS the session loudly (no silent bypass).
|
|
18
|
+
|
|
19
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
20
|
+
function randInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
|
|
21
|
+
function shuffle(arr) {
|
|
22
|
+
const a = [...arr];
|
|
23
|
+
for (let i = a.length - 1; i > 0; i--) { const j = randInt(0, i); [a[i], a[j]] = [a[j], a[i]]; }
|
|
24
|
+
return a;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function firstVisible(locator, max = 8) {
|
|
28
|
+
const n = Math.min(await locator.count().catch(() => 0), max);
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
if (await locator.nth(i).isVisible().catch(() => false)) return locator.nth(i);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function typeHuman(page, text) {
|
|
36
|
+
for (const ch of text) {
|
|
37
|
+
await page.keyboard.type(ch);
|
|
38
|
+
await page.waitForTimeout(randInt(60, 160));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function organicScroll(page, rounds = null) {
|
|
43
|
+
const n = rounds ?? randInt(2, 4);
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
await page.mouse.wheel(0, randInt(300, 850)).catch(() => {});
|
|
46
|
+
await page.waitForTimeout(randInt(500, 1500));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Dismiss FB cross-sell / cookie / "not now" popups that overlay the page.
|
|
51
|
+
// Best-effort — never throws. Avoids touching the composer or search.
|
|
52
|
+
async function dismissDialogs(page, log) {
|
|
53
|
+
const verbs = ['Cho phép tất cả cookie', 'Allow all cookies', 'Để sau', 'Not now', 'Lúc khác', 'Không phải bây giờ', 'Bỏ qua', 'Skip', 'Đóng', 'Close', 'OK', 'Đã hiểu', 'Got it'];
|
|
54
|
+
for (let round = 0; round < 3; round++) {
|
|
55
|
+
const hit = await page.evaluate((vs) => {
|
|
56
|
+
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
57
|
+
for (const dlg of dlgs) {
|
|
58
|
+
const r = dlg.getBoundingClientRect();
|
|
59
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
60
|
+
const aria = (dlg.getAttribute('aria-label') || '').toLowerCase();
|
|
61
|
+
if (/tạo bài viết|create post|thước phim|reel/i.test(aria)) continue; // never the composer/reels
|
|
62
|
+
for (const v of vs) {
|
|
63
|
+
for (const b of dlg.querySelectorAll("[role='button'], button")) {
|
|
64
|
+
const t = (b.innerText || '').trim();
|
|
65
|
+
if ((t === v || (b.getAttribute('aria-label') || '').trim() === v) && b.offsetParent !== null) {
|
|
66
|
+
b.setAttribute('__warm_dismiss__', '1');
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}, verbs).catch(() => null);
|
|
74
|
+
if (!hit) break;
|
|
75
|
+
try { await page.locator("[__warm_dismiss__='1']").click({ timeout: 2500 }); } catch {}
|
|
76
|
+
await page.evaluate(() => document.querySelectorAll("[__warm_dismiss__]").forEach(e => e.removeAttribute('__warm_dismiss__'))).catch(() => {});
|
|
77
|
+
await page.waitForTimeout(800);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Click the "Reels" entry in the left nav / shortcut bar (organic entry — NOT a
|
|
82
|
+
// direct URL). Matches the nav link to the reels surface (href /reel/ + aria/
|
|
83
|
+
// text Reels|Thước phim), scoped to navigation so we don't hit a feed item.
|
|
84
|
+
async function clickReelsEntry(page, log) {
|
|
85
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
86
|
+
const found = await page.evaluate(() => {
|
|
87
|
+
const isReel = (el) => {
|
|
88
|
+
const href = el.getAttribute('href') || '';
|
|
89
|
+
const aria = (el.getAttribute('aria-label') || '').trim();
|
|
90
|
+
const txt = (el.innerText || '').trim();
|
|
91
|
+
const hrefReel = /\/reel(s)?\b|\/reel\//i.test(href);
|
|
92
|
+
const labelReel = /^reels?$|^thước phim$/i.test(aria) || /^reels?$|^thước phim$/i.test(txt);
|
|
93
|
+
return (hrefReel && (labelReel || aria === '' )) || (labelReel && (hrefReel || /reel/i.test(href)));
|
|
94
|
+
};
|
|
95
|
+
// Prefer links inside navigation regions (left rail / shortcut bar).
|
|
96
|
+
const navs = [...document.querySelectorAll("[role='navigation'] a, [role='banner'] a, a")];
|
|
97
|
+
for (const a of navs) {
|
|
98
|
+
if (!a.getAttribute) continue;
|
|
99
|
+
const r = a.getBoundingClientRect();
|
|
100
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
101
|
+
if (a.offsetParent === null) continue;
|
|
102
|
+
if (isReel(a)) { a.setAttribute('__warm_reels__', '1'); return { href: a.getAttribute('href') || '', aria: (a.getAttribute('aria-label') || a.innerText || '').slice(0, 30) }; }
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}).catch(() => null);
|
|
106
|
+
if (found) {
|
|
107
|
+
try {
|
|
108
|
+
await page.locator("[__warm_reels__='1']").first().click({ timeout: 4000 });
|
|
109
|
+
await page.evaluate(() => document.querySelectorAll("[__warm_reels__]").forEach(e => e.removeAttribute('__warm_reels__'))).catch(() => {});
|
|
110
|
+
await page.waitForTimeout(randInt(3000, 5000));
|
|
111
|
+
log('info', `[warmup-fb] clicked Reels entry (aria="${found.aria}")`);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
log('info', `[warmup-fb] Reels click failed: ${String(e.message || e).slice(0, 80)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await page.waitForTimeout(1500);
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Focus the FB top-bar search input. FB sometimes renders a collapsed button
|
|
123
|
+
// that expands into an input on click — handle both. Returns true if focused.
|
|
124
|
+
async function focusFbSearch(page, log) {
|
|
125
|
+
const inputSels = [
|
|
126
|
+
"input[aria-label*='Tìm kiếm']",
|
|
127
|
+
"input[aria-label*='earch']",
|
|
128
|
+
"input[type='search']",
|
|
129
|
+
"[role='banner'] input[type='text']",
|
|
130
|
+
];
|
|
131
|
+
const triggerSels = [
|
|
132
|
+
"[aria-label='Tìm kiếm trên Facebook']",
|
|
133
|
+
"[aria-label='Search Facebook']",
|
|
134
|
+
"[role='banner'] [role='button'][aria-label*='earch']",
|
|
135
|
+
"[role='banner'] [aria-label*='Tìm kiếm']",
|
|
136
|
+
];
|
|
137
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
138
|
+
for (const sel of inputSels) {
|
|
139
|
+
const box = await firstVisible(page.locator(sel), 3);
|
|
140
|
+
if (box) {
|
|
141
|
+
try {
|
|
142
|
+
await box.click({ timeout: 2500 });
|
|
143
|
+
await page.keyboard.down('Control'); await page.keyboard.press('A'); await page.keyboard.up('Control');
|
|
144
|
+
await page.keyboard.press('Backspace');
|
|
145
|
+
return true;
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// No visible input → click a search trigger to expand it, then retry.
|
|
150
|
+
for (const sel of triggerSels) {
|
|
151
|
+
const t = await firstVisible(page.locator(sel), 3);
|
|
152
|
+
if (t) { try { await t.click({ timeout: 2500 }); await page.waitForTimeout(1200); } catch {} break; }
|
|
153
|
+
}
|
|
154
|
+
await page.waitForTimeout(1000);
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// On a search-results page, click the "Reels" filter tab (fallback "Videos") so
|
|
160
|
+
// results bias toward watchable reels/videos. Best-effort.
|
|
161
|
+
async function clickResultsFilter(page, log) {
|
|
162
|
+
const labels = ['Thước phim', 'Reels', 'Video', 'Videos'];
|
|
163
|
+
const found = await page.evaluate((labs) => {
|
|
164
|
+
const els = document.querySelectorAll("[role='link'], [role='tab'], a, [role='button']");
|
|
165
|
+
for (const lab of labs) {
|
|
166
|
+
for (const el of els) {
|
|
167
|
+
const t = (el.innerText || '').trim();
|
|
168
|
+
if (t === lab && el.offsetParent !== null) {
|
|
169
|
+
const r = el.getBoundingClientRect();
|
|
170
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
171
|
+
el.setAttribute('__warm_filter__', '1');
|
|
172
|
+
return lab;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}, labels).catch(() => null);
|
|
178
|
+
if (found) {
|
|
179
|
+
try {
|
|
180
|
+
await page.locator("[__warm_filter__='1']").first().click({ timeout: 3000 });
|
|
181
|
+
await page.evaluate(() => document.querySelectorAll("[__warm_filter__]").forEach(e => e.removeAttribute('__warm_filter__'))).catch(() => {});
|
|
182
|
+
await page.waitForTimeout(randInt(2000, 3500));
|
|
183
|
+
log('info', `[warmup-fb] results filtered → "${found}"`);
|
|
184
|
+
return found;
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Collect watchable result hrefs (reels/videos) on the current results page.
|
|
191
|
+
async function collectResultUrls(page) {
|
|
192
|
+
return page.evaluate(() => {
|
|
193
|
+
const out = []; const seen = new Set();
|
|
194
|
+
for (const a of document.querySelectorAll("a[href*='/reel/'], a[href*='/watch'], a[href*='/videos/']")) {
|
|
195
|
+
let href = a.getAttribute('href') || '';
|
|
196
|
+
if (!href) continue;
|
|
197
|
+
if (href.startsWith('/')) href = 'https://www.facebook.com' + href;
|
|
198
|
+
if (!/facebook\.com/.test(href)) continue;
|
|
199
|
+
const key = href.split('?')[0];
|
|
200
|
+
if (seen.has(key)) continue;
|
|
201
|
+
const r = a.getBoundingClientRect();
|
|
202
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
203
|
+
seen.add(key); out.push(href);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}).catch(() => []);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// CLICK the result anchor matching `href` (natural — not a URL navigation).
|
|
210
|
+
// Marks the element then clicks via Playwright (trusted). Returns true on click.
|
|
211
|
+
async function clickResultByHref(page, href) {
|
|
212
|
+
const key = href.split('?')[0];
|
|
213
|
+
const found = await page.evaluate((k) => {
|
|
214
|
+
for (const a of document.querySelectorAll("a[href*='/reel/'], a[href*='/watch'], a[href*='/videos/']")) {
|
|
215
|
+
let h = a.getAttribute('href') || '';
|
|
216
|
+
if (h.startsWith('/')) h = 'https://www.facebook.com' + h;
|
|
217
|
+
if (h.split('?')[0] === k && a.offsetParent !== null) { a.setAttribute('__warm_click__', '1'); return true; }
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}, key).catch(() => false);
|
|
221
|
+
if (!found) return false;
|
|
222
|
+
const loc = page.locator("[__warm_click__='1']").first();
|
|
223
|
+
try {
|
|
224
|
+
await loc.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
|
|
225
|
+
await loc.click({ timeout: 4000 });
|
|
226
|
+
await page.evaluate(() => document.querySelectorAll("[__warm_click__]").forEach(e => e.removeAttribute('__warm_click__'))).catch(() => {});
|
|
227
|
+
return true;
|
|
228
|
+
} catch {
|
|
229
|
+
await page.evaluate(() => document.querySelectorAll("[__warm_click__]").forEach(e => e.removeAttribute('__warm_click__'))).catch(() => {});
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Watch whatever reel/video is currently open (after clicking a result): ensure
|
|
235
|
+
// the <video> plays, watch a random span with a mid-view scroll. Best-effort.
|
|
236
|
+
async function watchCurrent(page, minSec, maxSec, log) {
|
|
237
|
+
try {
|
|
238
|
+
await page.waitForTimeout(randInt(2500, 4500)); // let the reel viewer mount
|
|
239
|
+
await dismissDialogs(page, log);
|
|
240
|
+
const hasVideo = await page.evaluate(() => {
|
|
241
|
+
const v = document.querySelector('video');
|
|
242
|
+
if (!v) return false;
|
|
243
|
+
if (v.paused) { try { v.play(); } catch {} }
|
|
244
|
+
return true;
|
|
245
|
+
}).catch(() => false);
|
|
246
|
+
if (!hasVideo) { log('info', '[warmup-fb] no <video> after click — skipping'); return false; }
|
|
247
|
+
const watchMs = randInt(minSec, maxSec) * 1000;
|
|
248
|
+
log('info', `[warmup-fb] watching reel for ~${Math.round(watchMs / 1000)}s (url=${page.url().slice(-28)})`);
|
|
249
|
+
const half = Math.floor(watchMs / 2);
|
|
250
|
+
await page.waitForTimeout(half);
|
|
251
|
+
await organicScroll(page, randInt(1, 2));
|
|
252
|
+
await page.waitForTimeout(watchMs - half);
|
|
253
|
+
return true;
|
|
254
|
+
} catch (e) {
|
|
255
|
+
log('info', `[warmup-fb] watch failed: ${String(e.message || e).slice(0, 90)}`);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── main ─────────────────────────────────────────────────────────────────
|
|
261
|
+
async function run({ page, payload, log }) {
|
|
262
|
+
const t0 = Date.now();
|
|
263
|
+
const allKeywords = Array.isArray(payload?.keywords) ? payload.keywords.filter(Boolean) : [];
|
|
264
|
+
if (!allKeywords.length) throw new Error('warmup-fb: no keywords provided');
|
|
265
|
+
|
|
266
|
+
const cfg = payload.config || {};
|
|
267
|
+
const keywordsPerSession = Math.max(1, cfg.keywords_per_session ?? 5);
|
|
268
|
+
const videosPerKeyword = Math.max(1, cfg.videos_per_keyword ?? 3);
|
|
269
|
+
const watchMin = Math.max(5, cfg.watch_time_min_sec ?? 30);
|
|
270
|
+
const watchMax = Math.max(watchMin, cfg.watch_time_max_sec ?? 90);
|
|
271
|
+
const sessionKeywords = shuffle(allKeywords).slice(0, keywordsPerSession);
|
|
272
|
+
log('info', `[warmup-fb] session start — ${sessionKeywords.length}/${allKeywords.length} keywords, ${videosPerKeyword} videos/keyword, watch ${watchMin}-${watchMax}s`);
|
|
273
|
+
|
|
274
|
+
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
275
|
+
|
|
276
|
+
// 1) Land on the FB home feed (organic entry — not a deep link).
|
|
277
|
+
await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
278
|
+
await page.waitForTimeout(randInt(3000, 5000));
|
|
279
|
+
await dismissDialogs(page, log);
|
|
280
|
+
await organicScroll(page, randInt(1, 3));
|
|
281
|
+
|
|
282
|
+
// 2) Click the "Reels" entry (left nav / shortcut) — natural navigation.
|
|
283
|
+
const reelsOk = await clickReelsEntry(page, log);
|
|
284
|
+
if (!reelsOk) {
|
|
285
|
+
throw new Error('warmup-fb: "Reels" entry not found on FB home — check the profile is logged in to Facebook and the left-nav Reels shortcut is present');
|
|
286
|
+
}
|
|
287
|
+
await dismissDialogs(page, log);
|
|
288
|
+
await organicScroll(page, randInt(1, 2));
|
|
289
|
+
|
|
290
|
+
const keywordsSearched = [];
|
|
291
|
+
let videosWatched = 0;
|
|
292
|
+
const watchedGlobal = new Set(); // dedupe reels across ALL keywords this session
|
|
293
|
+
|
|
294
|
+
for (const kw of sessionKeywords) {
|
|
295
|
+
// 3) Search the keyword via the top bar (from the Reels surface).
|
|
296
|
+
const focused = await focusFbSearch(page, log);
|
|
297
|
+
if (!focused) {
|
|
298
|
+
throw new Error('warmup-fb: search box not found — FB layout/login issue (cannot warm up without searching)');
|
|
299
|
+
}
|
|
300
|
+
log('info', `[warmup-fb] searching: "${kw}"`);
|
|
301
|
+
await typeHuman(page, kw);
|
|
302
|
+
await page.waitForTimeout(randInt(500, 1100));
|
|
303
|
+
await page.keyboard.press('Enter');
|
|
304
|
+
await page.waitForTimeout(randInt(3000, 5000));
|
|
305
|
+
await dismissDialogs(page, log);
|
|
306
|
+
keywordsSearched.push(kw);
|
|
307
|
+
|
|
308
|
+
// 4) Bias results toward reels/videos, then CLICK + watch a few (no URL nav).
|
|
309
|
+
await clickResultsFilter(page, log);
|
|
310
|
+
await organicScroll(page, randInt(1, 3));
|
|
311
|
+
const urls = await collectResultUrls(page);
|
|
312
|
+
if (!urls.length) {
|
|
313
|
+
log('info', `[warmup-fb] no reel/video results for "${kw}" — next keyword`);
|
|
314
|
+
continue; // top-bar search persists; next keyword searches from here
|
|
315
|
+
}
|
|
316
|
+
log('info', `[warmup-fb] "${kw}" → ${urls.length} results, opening up to ${videosPerKeyword}`);
|
|
317
|
+
for (let n = 0; n < videosPerKeyword; n++) {
|
|
318
|
+
// Re-collect each iteration — clicking + goBack mutates the DOM.
|
|
319
|
+
const current = await collectResultUrls(page);
|
|
320
|
+
const next = current.find(u => !watchedGlobal.has(u.split('?')[0]));
|
|
321
|
+
if (!next) break;
|
|
322
|
+
watchedGlobal.add(next.split('?')[0]);
|
|
323
|
+
const urlBefore = page.url();
|
|
324
|
+
const clicked = await clickResultByHref(page, next);
|
|
325
|
+
if (!clicked) { log('info', `[warmup-fb] couldn't click result ${next.slice(-24)} — skipping`); continue; }
|
|
326
|
+
// Verify the click actually navigated into a reel/video — otherwise we'd
|
|
327
|
+
// re-watch whatever was already open (saw the same reel id reused across
|
|
328
|
+
// keywords). Wait up to 8s for the URL to change to /reel/ or /watch.
|
|
329
|
+
let navigated = false;
|
|
330
|
+
for (let w = 0; w < 8; w++) {
|
|
331
|
+
await page.waitForTimeout(1000);
|
|
332
|
+
const u = page.url();
|
|
333
|
+
if (u !== urlBefore && /\/(reel|watch|videos)\//.test(u)) { navigated = true; break; }
|
|
334
|
+
}
|
|
335
|
+
if (!navigated) { log('info', `[warmup-fb] click didn't navigate (still ${page.url().slice(-30)}) — skipping`); continue; }
|
|
336
|
+
const ok = await watchCurrent(page, watchMin, watchMax, log);
|
|
337
|
+
if (ok) videosWatched++;
|
|
338
|
+
// Back to the results list for the next pick (Escape closes a reel overlay
|
|
339
|
+
// if goBack didn't return to results).
|
|
340
|
+
await page.goBack({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
|
|
341
|
+
await page.waitForTimeout(randInt(1500, 2800));
|
|
342
|
+
if (/\/reel\//.test(page.url())) { await page.keyboard.press('Escape').catch(() => {}); await page.waitForTimeout(1200); }
|
|
343
|
+
}
|
|
344
|
+
// Next keyword searches from the persistent top-bar (no home bounce).
|
|
345
|
+
await page.waitForTimeout(randInt(1000, 2000));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const durationSec = Math.round((Date.now() - t0) / 1000);
|
|
349
|
+
log('info', `[warmup-fb] session done — keywords=${keywordsSearched.length} videos=${videosWatched} duration=${durationSec}s`);
|
|
350
|
+
return { keywords_searched: keywordsSearched, videos_watched: videosWatched, duration_sec: durationSec };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = { run };
|