channel-worker 2.5.32 → 2.5.34
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/upload_facebook.js +56 -0
- package/scripts/warmup_facebook.js +49 -16
package/package.json
CHANGED
|
@@ -1743,6 +1743,62 @@ async function run({ page, payload, log }) {
|
|
|
1743
1743
|
throw new Error(`FB rate-limited / spam-blocked this account — publish was NOT accepted. Detected phrase: "${blockHit}". Cool down account before retry.`);
|
|
1744
1744
|
}
|
|
1745
1745
|
|
|
1746
|
+
// 7b) COMMIT VERIFY + RETRY. A Playwright click on an enabled "Đăng" can
|
|
1747
|
+
// register without FB actually publishing (handler no-op / transient
|
|
1748
|
+
// glitch) — the composer modal ("Cài đặt thước phim") just stays open
|
|
1749
|
+
// (this is the "đứng ở nút Đăng" the user reported). We used to still
|
|
1750
|
+
// return ok:true with an empty post_url → FALSE SUCCESS. Detect the
|
|
1751
|
+
// still-open composer, re-click "Đăng" a few times, and fail loudly if
|
|
1752
|
+
// it refuses to commit (so the cmd is marked failed, not done).
|
|
1753
|
+
const composerStillOpen = () => page.evaluate((verbs) => {
|
|
1754
|
+
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
1755
|
+
for (const dlg of dlgs) {
|
|
1756
|
+
const r = dlg.getBoundingClientRect();
|
|
1757
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
1758
|
+
const cs = getComputedStyle(dlg);
|
|
1759
|
+
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
|
|
1760
|
+
const hay = ((dlg.getAttribute('aria-label') || '') + ' ' + (dlg.innerText || '')).toLowerCase();
|
|
1761
|
+
if (!/cài đặt thước phim|reel settings|tạo thước phim|create reel|create a reel|tạo bài viết|create post/.test(hay)) continue;
|
|
1762
|
+
const btns = dlg.querySelectorAll("button, [role='button']");
|
|
1763
|
+
for (const b of btns) {
|
|
1764
|
+
const t = ((b.innerText || '') + ' ' + (b.getAttribute('aria-label') || '')).trim();
|
|
1765
|
+
if (verbs.some((v) => t === v || t.split('\n').includes(v))) return true; // publish CTA still present → not committed
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return false;
|
|
1769
|
+
}, publishVerbs).catch(() => false);
|
|
1770
|
+
|
|
1771
|
+
for (let commitTry = 0; commitTry < 3 && (await composerStillOpen()); commitTry++) {
|
|
1772
|
+
log('warn', `[fb-pw] composer STILL open after publish click — re-clicking "Đăng" (retry ${commitTry + 1}/3)`);
|
|
1773
|
+
const reClicked = await page.evaluate((verbs) => {
|
|
1774
|
+
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
1775
|
+
for (const dlg of dlgs) {
|
|
1776
|
+
const btns = dlg.querySelectorAll("button, [role='button']");
|
|
1777
|
+
for (const v of verbs) {
|
|
1778
|
+
for (const b of btns) {
|
|
1779
|
+
const t = (b.innerText || '').trim();
|
|
1780
|
+
const a = (b.getAttribute('aria-label') || '').trim();
|
|
1781
|
+
if ((t === v || a === v) && b.getAttribute('aria-disabled') !== 'true') {
|
|
1782
|
+
b.setAttribute('__fbpw_recommit__', '1');
|
|
1783
|
+
return true;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return false;
|
|
1789
|
+
}, publishVerbs).catch(() => false);
|
|
1790
|
+
if (reClicked) {
|
|
1791
|
+
await page.locator("[__fbpw_recommit__='1']").click({ timeout: 5000 }).catch(() => {});
|
|
1792
|
+
await page.evaluate(() => document.querySelectorAll("[__fbpw_recommit__]").forEach((el) => el.removeAttribute('__fbpw_recommit__'))).catch(() => {});
|
|
1793
|
+
}
|
|
1794
|
+
await page.waitForTimeout(8000);
|
|
1795
|
+
}
|
|
1796
|
+
if (await composerStillOpen()) {
|
|
1797
|
+
await dumpInventory(page, log, 'composer-stuck-open');
|
|
1798
|
+
await dumpFailure(page, 'composer-stuck-open', log);
|
|
1799
|
+
throw new Error('FB publish KHÔNG commit — composer vẫn mở ở nút "Đăng" sau 3 lần thử (video CHƯA được đăng). Thử lại sau.');
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1746
1802
|
let postUrl = '';
|
|
1747
1803
|
|
|
1748
1804
|
// (a) Title-anchored page-wall scrape — FB renders the just-published
|
|
@@ -259,9 +259,30 @@ async function readVideoState(page) {
|
|
|
259
259
|
}).catch(() => null);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
262
|
+
// Is the currently-open reel/video SPONSORED (an ad)? Looks for a visible
|
|
263
|
+
// "Được tài trợ"/"Sponsored" label in the viewport (the reel viewer is
|
|
264
|
+
// fullscreen, so a visible sponsored tag means THIS reel is the ad). Best-effort.
|
|
265
|
+
async function isSponsoredReel(page) {
|
|
266
|
+
return page.evaluate(() => {
|
|
267
|
+
const re = /^(được tài trợ|sponsored|tài trợ)$/i;
|
|
268
|
+
for (const el of document.querySelectorAll("a, span, div")) {
|
|
269
|
+
const t = (el.innerText || el.textContent || '').trim();
|
|
270
|
+
if (!re.test(t)) continue;
|
|
271
|
+
if (el.offsetParent === null) continue;
|
|
272
|
+
const r = el.getBoundingClientRect();
|
|
273
|
+
if (r.width > 0 && r.height > 0 && r.top >= 0 && r.top < window.innerHeight && r.left < window.innerWidth) return true;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}).catch(() => false);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Watch whatever reel/video is currently open (after clicking a result). Rules:
|
|
280
|
+
// - SKIP sponsored/ad reels entirely (don't count) — user: ads shouldn't count.
|
|
281
|
+
// - Count ONLY seconds the video actually ADVANCES — a frozen/buffering video
|
|
282
|
+
// no longer accrues watch time (user: "video đơ vẫn đếm giây"). Loop wrap
|
|
283
|
+
// (currentTime jumps back to ~0) counts as progress.
|
|
284
|
+
// - Advance early if a non-looping video ends; bail if frozen too long.
|
|
285
|
+
// Returns true only if real progressing playback was counted.
|
|
265
286
|
async function watchCurrent(page, minSec, maxSec, log) {
|
|
266
287
|
try {
|
|
267
288
|
await page.waitForTimeout(randInt(2500, 4500)); // let the reel viewer mount
|
|
@@ -274,27 +295,39 @@ async function watchCurrent(page, minSec, maxSec, log) {
|
|
|
274
295
|
}).catch(() => false);
|
|
275
296
|
if (!hasVideo) { log('info', '[warmup-fb] no <video> after click — skipping'); return false; }
|
|
276
297
|
|
|
298
|
+
// Skip ads — don't watch, don't count.
|
|
299
|
+
if (await isSponsoredReel(page)) { log('info', '[warmup-fb] sponsored/ad reel — skipping (not counted)'); return false; }
|
|
300
|
+
|
|
277
301
|
const watchMs = randInt(minSec, maxSec) * 1000;
|
|
278
302
|
log('info', `[warmup-fb] watching reel for ~${Math.round(watchMs / 1000)}s (url=${page.url().slice(-28)})`);
|
|
279
303
|
const TICK = 1000;
|
|
280
|
-
let watched = 0
|
|
304
|
+
let watched = 0; // counts ONLY progressing seconds
|
|
305
|
+
let scrolled = false, lastT = -1, frozenTicks = 0, adReChecks = 0;
|
|
281
306
|
while (watched < watchMs) {
|
|
282
307
|
const st = await readVideoState(page);
|
|
283
|
-
//
|
|
284
|
-
if (st && !st.loop) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
if (Math.abs(st.currentTime - lastT) < 0.15) { stallTicks++; if (stallTicks >= 6) { log('info', '[warmup-fb] reel stalled — advancing'); break; } }
|
|
290
|
-
else stallTicks = 0;
|
|
291
|
-
lastT = st.currentTime;
|
|
308
|
+
// Non-looping video finished → go to next.
|
|
309
|
+
if (st && !st.loop && (st.ended || (st.duration > 0 && st.currentTime >= st.duration - 0.6))) {
|
|
310
|
+
log('info', `[warmup-fb] reel ended at ${Math.round(st.currentTime)}s — advancing`);
|
|
311
|
+
break;
|
|
292
312
|
}
|
|
293
|
-
|
|
313
|
+
// Progress = currentTime moved forward, OR wrapped back (loop restart).
|
|
314
|
+
const advanced = !!st && (st.currentTime > lastT + 0.2 || st.currentTime < lastT - 0.5);
|
|
315
|
+
if (advanced) {
|
|
316
|
+
watched += TICK;
|
|
317
|
+
frozenTicks = 0;
|
|
318
|
+
if (!scrolled && watched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
|
|
319
|
+
} else {
|
|
320
|
+
// Not progressing (frozen / buffering / paused-and-won't-play) — do NOT
|
|
321
|
+
// count this second; bail after ~8s stuck.
|
|
322
|
+
frozenTicks++;
|
|
323
|
+
if (frozenTicks >= 8) { log('info', `[warmup-fb] video frozen ~${frozenTicks}s (counted ${Math.round(watched/1000)}s) — advancing`); break; }
|
|
324
|
+
}
|
|
325
|
+
lastT = st ? st.currentTime : lastT;
|
|
326
|
+
// Re-check sponsored a couple times (a mid-scroll ad reel can swap in).
|
|
327
|
+
if (adReChecks < 2 && watched > 0 && watched % 8000 === 0) { adReChecks++; if (await isSponsoredReel(page)) { log('info', '[warmup-fb] became sponsored mid-watch — advancing'); break; } }
|
|
294
328
|
await page.waitForTimeout(TICK);
|
|
295
|
-
watched += TICK;
|
|
296
329
|
}
|
|
297
|
-
return
|
|
330
|
+
return watched > 0; // only a video with real progressing playback counts
|
|
298
331
|
} catch (e) {
|
|
299
332
|
log('info', `[warmup-fb] watch failed: ${String(e.message || e).slice(0, 90)}`);
|
|
300
333
|
return false;
|