channel-worker 2.5.53 → 2.5.54
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 +62 -31
package/package.json
CHANGED
|
@@ -2232,6 +2232,30 @@ async function runOnce({ page, payload, log }) {
|
|
|
2232
2232
|
// - Page wall display order isn't strictly chronological
|
|
2233
2233
|
// - "Quảng bá thước phim" appears on ALL Page reels, not just new
|
|
2234
2234
|
// - Title match alone can't distinguish same-title duplicates
|
|
2235
|
+
// Fresh-tile scrape, dùng ở lượt đầu VÀ lượt retry (a.2b) — cùng một luật:
|
|
2236
|
+
// chỉ nhận tile có timestamp tươi ("Vừa xong"/"X phút"), tuyệt đối không
|
|
2237
|
+
// nhận tile cũ.
|
|
2238
|
+
const scrapeFreshReelTile = () => page.evaluate(() => {
|
|
2239
|
+
const FRESH_RE = /vừa xong|vài giây|^\s*\d{1,2}\s*giây|^\s*[1-5]\s*phút\b|\b[1-5]\s*phút trước|just now|few seconds ago|\b[1-5] min(ute)?s? ago/i;
|
|
2240
|
+
const anchors = document.querySelectorAll("a[href*='/reel/']");
|
|
2241
|
+
for (const a of anchors) {
|
|
2242
|
+
const href = a.getAttribute('href') || '';
|
|
2243
|
+
const m = href.match(/\/reel\/(\d{8,18})/);
|
|
2244
|
+
if (!m) continue;
|
|
2245
|
+
const r = a.getBoundingClientRect();
|
|
2246
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
2247
|
+
let ctx = a;
|
|
2248
|
+
for (let depth = 0; depth < 5 && ctx; depth++) {
|
|
2249
|
+
const raw = (ctx.innerText || ctx.textContent || '').slice(0, 500);
|
|
2250
|
+
if (FRESH_RE.test(raw)) {
|
|
2251
|
+
return { href, id: m[1], depth, timestamp: (raw.match(FRESH_RE) || [''])[0] };
|
|
2252
|
+
}
|
|
2253
|
+
ctx = ctx.parentElement;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return null;
|
|
2257
|
+
}).catch(() => null);
|
|
2258
|
+
let reelsTabUrl = null; // giữ lại cho lượt retry (a.2b)
|
|
2235
2259
|
try {
|
|
2236
2260
|
// Find a link to the page's reels tab. Multi-strategy:
|
|
2237
2261
|
// 1. Any existing href containing "sk=reels_tab"
|
|
@@ -2272,35 +2296,14 @@ async function runOnce({ page, payload, log }) {
|
|
|
2272
2296
|
return null;
|
|
2273
2297
|
}).catch(() => null);
|
|
2274
2298
|
if (reelsTabHref) {
|
|
2299
|
+
reelsTabUrl = reelsTabHref;
|
|
2275
2300
|
log('info', `[fb-pw] navigating to page reels tab: ${reelsTabHref.slice(0, 100)}`);
|
|
2276
2301
|
await page.goto(reelsTabHref, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
|
|
2277
2302
|
await page.waitForTimeout(5000);
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
// wrapping a thumbnail + meta. Iterate in DOM order (newest first
|
|
2283
|
-
// on reels tab). For each tile, check if its subtree has a fresh
|
|
2284
|
-
// timestamp.
|
|
2285
|
-
const anchors = document.querySelectorAll("a[href*='/reel/']");
|
|
2286
|
-
for (const a of anchors) {
|
|
2287
|
-
const href = a.getAttribute('href') || '';
|
|
2288
|
-
const m = href.match(/\/reel\/(\d{8,18})/);
|
|
2289
|
-
if (!m) continue;
|
|
2290
|
-
const r = a.getBoundingClientRect();
|
|
2291
|
-
if (r.width < 8 || r.height < 8) continue;
|
|
2292
|
-
// Look within the anchor's subtree + closest meaningful ancestor.
|
|
2293
|
-
let ctx = a;
|
|
2294
|
-
for (let depth = 0; depth < 5 && ctx; depth++) {
|
|
2295
|
-
const raw = (ctx.innerText || ctx.textContent || '').slice(0, 500);
|
|
2296
|
-
if (FRESH_RE.test(raw)) {
|
|
2297
|
-
return { href, id: m[1], depth, timestamp: (raw.match(FRESH_RE) || [''])[0] };
|
|
2298
|
-
}
|
|
2299
|
-
ctx = ctx.parentElement;
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
return null;
|
|
2303
|
-
}).catch(() => null);
|
|
2303
|
+
// Reel tiles on the reels tab — each is an <a href="/reel/<id>/">
|
|
2304
|
+
// wrapping a thumbnail + meta, DOM order = newest first. Only a tile
|
|
2305
|
+
// with a FRESH timestamp counts (see scrapeFreshReelTile).
|
|
2306
|
+
const fresh = await scrapeFreshReelTile();
|
|
2304
2307
|
if (fresh) {
|
|
2305
2308
|
const full = fresh.href.startsWith('http') ? fresh.href : `https://www.facebook.com${fresh.href}`;
|
|
2306
2309
|
postUrl = full;
|
|
@@ -2422,24 +2425,52 @@ async function runOnce({ page, payload, log }) {
|
|
|
2422
2425
|
}
|
|
2423
2426
|
}
|
|
2424
2427
|
|
|
2428
|
+
// (a.2b) RETRY reels_tab sau khi chờ — reel mới thường cần 1-3 phút xử lý
|
|
2429
|
+
// phía FB rồi mới hiện lên tab, nên lượt scrape đầu (chạy ngay sau
|
|
2430
|
+
// khi bấm Đăng) hay trượt và mọi thứ rơi xuống last-resort bốc nhầm
|
|
2431
|
+
// reel cũ (4 lần gần nhất 2026-08-19 đều thế). Một lượt chờ 75s +
|
|
2432
|
+
// re-scrape cứu được phần lớn ca đó, vẫn giữ luật fresh-timestamp
|
|
2433
|
+
// nên không thể nhận nhầm tile cũ.
|
|
2434
|
+
if (!postUrl && reelsTabUrl) {
|
|
2435
|
+
log('info', '[fb-pw] no post URL yet — waiting 75s for FB to surface the new reel on reels_tab, then re-scraping…');
|
|
2436
|
+
await page.waitForTimeout(75_000);
|
|
2437
|
+
await page.goto(reelsTabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
|
|
2438
|
+
await page.waitForTimeout(5000);
|
|
2439
|
+
const fresh2 = await scrapeFreshReelTile();
|
|
2440
|
+
if (fresh2) {
|
|
2441
|
+
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
2442
|
+
log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
|
|
2443
|
+
} else {
|
|
2444
|
+
log('warn', '[fb-pw] reels_tab retry: still no fresh tile');
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2425
2448
|
// (a.3) LAST-RESORT: first reel tile on the profile reels tab. UNRELIABLE —
|
|
2426
2449
|
// the just-published reel may not be on the tab yet (still
|
|
2427
2450
|
// processing), so the first tile can be a STALE older reel. Only used
|
|
2428
2451
|
// when every authoritative source above (fresh-timestamp tile,
|
|
2429
|
-
// title-match, inline CTA, network capture) returned
|
|
2452
|
+
// title-match, inline CTA, network capture, timed retry) returned
|
|
2453
|
+
// nothing. Provably-stale filter: mọi reel ID đã bắt được TRƯỚC khi
|
|
2454
|
+
// bấm Đăng là reel có sẵn trên page — reel mới không thể là chúng.
|
|
2455
|
+
// Thà post_url rỗng còn hơn ghi sai (2026-08-19: nhánh này ghi reel
|
|
2456
|
+
// 02/08 cho bài vừa đăng → link trong DB trỏ nhầm bài cũ).
|
|
2430
2457
|
if (!postUrl) {
|
|
2431
|
-
const
|
|
2458
|
+
const preIds = capturedReelIds.slice(0, capturedReelIdsSnapshotLen);
|
|
2459
|
+
const tileHref = await page.evaluate((staleIds) => {
|
|
2460
|
+
const staleSet = new Set(staleIds);
|
|
2432
2461
|
const anchors = document.querySelectorAll("a[role='link']");
|
|
2433
2462
|
for (const a of anchors) {
|
|
2434
2463
|
const aria = (a.getAttribute('aria-label') || '').toLowerCase();
|
|
2435
2464
|
if (!/bản xem trước ô thước phim|reel tile preview|reel preview/.test(aria)) continue;
|
|
2436
2465
|
const href = a.getAttribute('href') || '';
|
|
2437
2466
|
const m = href.match(/\/reel\/(\d{8,20})/);
|
|
2438
|
-
if (m) return { href, aria: aria.slice(0, 60) };
|
|
2467
|
+
if (m) return { href, aria: aria.slice(0, 60), id: m[1], provablyStale: staleSet.has(m[1]) };
|
|
2439
2468
|
}
|
|
2440
2469
|
return null;
|
|
2441
|
-
}).catch(() => null);
|
|
2442
|
-
if (tileHref) {
|
|
2470
|
+
}, preIds).catch(() => null);
|
|
2471
|
+
if (tileHref && tileHref.provablyStale) {
|
|
2472
|
+
log('warn', `[fb-pw] LAST-RESORT tile ${tileHref.id} was already captured PRE-publish — provably an OLD reel; leaving post_url empty instead of recording a wrong one`);
|
|
2473
|
+
} else if (tileHref) {
|
|
2443
2474
|
postUrl = tileHref.href.startsWith('http') ? tileHref.href : `https://www.facebook.com${tileHref.href}`;
|
|
2444
2475
|
log('warn', `[fb-pw] post URL from FIRST reel tile on reels_tab (LAST-RESORT, may be STALE): ${postUrl}`);
|
|
2445
2476
|
}
|