channel-worker 2.5.14 → 2.5.16
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 +111 -4
package/package.json
CHANGED
|
@@ -180,6 +180,102 @@ async function dismissBsOnboarding(page, log) {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ── Large-file upload bypass ───────────────────────────────────────────────
|
|
184
|
+
// Playwright caps file transfers at 50MB when the browser is reached over
|
|
185
|
+
// connectOverCDP (our Nstbrowser case):
|
|
186
|
+
// "Cannot transfer files larger than 50Mb to a browser not connected to the
|
|
187
|
+
// server"
|
|
188
|
+
// Reup videos are routinely 60–200MB, so the native setInputFiles()/
|
|
189
|
+
// fileChooser.setFiles() throws and the upload never starts. Bypass via the raw
|
|
190
|
+
// CDP command DOM.setFileInputFiles: the browser reads the path off its OWN
|
|
191
|
+
// local disk (worker + Nstbrowser are co-located on the same machine), so no
|
|
192
|
+
// bytes cross the wire and there is no size cap. Unlike an in-page fetch() this
|
|
193
|
+
// is also immune to facebook.com's strict connect-src CSP. We tag the exact
|
|
194
|
+
// <input> with a unique attribute so CDP resolves its nodeId unambiguously
|
|
195
|
+
// (the page carries several file inputs).
|
|
196
|
+
async function cdpSetInputFiles(page, inputHandle, filePath, log, tag) {
|
|
197
|
+
const ATTR = 'data-cm-bigfile';
|
|
198
|
+
let cdp;
|
|
199
|
+
try {
|
|
200
|
+
await inputHandle.evaluate((el, a) => el.setAttribute(a, '1'), ATTR);
|
|
201
|
+
cdp = await page.context().newCDPSession(page);
|
|
202
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: 0 });
|
|
203
|
+
const { nodeId } = await cdp.send('DOM.querySelector', {
|
|
204
|
+
nodeId: root.nodeId,
|
|
205
|
+
selector: `input[${ATTR}="1"]`,
|
|
206
|
+
});
|
|
207
|
+
if (!nodeId) throw new Error('CDP querySelector: tagged input not found');
|
|
208
|
+
await cdp.send('DOM.setFileInputFiles', { files: [filePath], nodeId });
|
|
209
|
+
log('info', `[${tag}] file set via CDP DOM.setFileInputFiles (>50MB bypass)`);
|
|
210
|
+
} finally {
|
|
211
|
+
await inputHandle.evaluate((el, a) => el.removeAttribute(a), ATTR).catch(() => {});
|
|
212
|
+
if (cdp) await cdp.detach().catch(() => {});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Set a video file on a file <input>, transparently bypassing the CDP 50MB cap.
|
|
217
|
+
// Tries Playwright's native path first (fine for <50MB / thumbnails), then
|
|
218
|
+
// falls back to the CDP command only on the size error.
|
|
219
|
+
async function setVideoFile(page, inputHandle, filePath, log, tag = 'fb-pw') {
|
|
220
|
+
if (!inputHandle) throw new Error('setVideoFile: no input handle');
|
|
221
|
+
try {
|
|
222
|
+
await inputHandle.setInputFiles(filePath); // native — works for <50MB
|
|
223
|
+
return;
|
|
224
|
+
} catch (e) {
|
|
225
|
+
if (!/larger than 50\s?mb|not connected to the server/i.test(String(e.message || ''))) throw e;
|
|
226
|
+
log('info', `[${tag}] native setInputFiles hit the 50MB CDP cap — switching to DOM.setFileInputFiles…`);
|
|
227
|
+
}
|
|
228
|
+
await cdpSetInputFiles(page, inputHandle, filePath, log, tag);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// On the final "Cài đặt thước phim" (Reel settings) step the only action is
|
|
232
|
+
// "Đăng", which FB renders DISABLED (aria-disabled="true") until it finishes
|
|
233
|
+
// ingesting the uploaded video. For large (>50MB) reup clips that ingest runs
|
|
234
|
+
// well past the 30s upload-wait, so findByVerbs (which skips disabled buttons)
|
|
235
|
+
// finds no publish CTA — and with no "Tiếp" on the last step the loop would
|
|
236
|
+
// throw no-advance. Poll until a bottom-half publish-verb button is present
|
|
237
|
+
// AND enabled. Returns true once enabled; false on timeout, or fast-false if no
|
|
238
|
+
// publish button ever appears (we're not actually on the final step).
|
|
239
|
+
async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw') {
|
|
240
|
+
const deadline = Date.now() + timeoutMs;
|
|
241
|
+
let announced = false, sawPresent = false, absent = 0;
|
|
242
|
+
while (Date.now() < deadline) {
|
|
243
|
+
const st = await page.evaluate((vbs) => {
|
|
244
|
+
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
245
|
+
const roots = dlgs.length ? Array.from(dlgs) : [document];
|
|
246
|
+
const vh = window.innerHeight;
|
|
247
|
+
let present = false, enabled = false;
|
|
248
|
+
for (const root of roots) {
|
|
249
|
+
for (const el of root.querySelectorAll("button, [role='button']")) {
|
|
250
|
+
const t = (el.innerText || el.textContent || '').trim();
|
|
251
|
+
if (!vbs.includes(t)) continue;
|
|
252
|
+
const r = el.getBoundingClientRect();
|
|
253
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
254
|
+
if (r.y < vh * 0.4) continue; // bottom-half action button only
|
|
255
|
+
const cs = getComputedStyle(el);
|
|
256
|
+
if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') continue;
|
|
257
|
+
present = true;
|
|
258
|
+
if (!(el.getAttribute('aria-disabled') === 'true' || el.disabled)) enabled = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { present, enabled };
|
|
262
|
+
}, verbs).catch(() => ({ present: false, enabled: false }));
|
|
263
|
+
if (st.enabled) {
|
|
264
|
+
if (announced) log('info', `[${tag}] "Đăng" is now enabled — video finished processing`);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (st.present) {
|
|
268
|
+
sawPresent = true;
|
|
269
|
+
if (!announced) { log('info', `[${tag}] final step: "Đăng" disabled (video still processing) — waiting up to ${Math.round(timeoutMs / 1000)}s…`); announced = true; }
|
|
270
|
+
} else if (!sawPresent && ++absent >= 4) {
|
|
271
|
+
return false; // no publish CTA after ~10s → not the final step
|
|
272
|
+
}
|
|
273
|
+
await page.waitForTimeout(2500);
|
|
274
|
+
}
|
|
275
|
+
log('warn', `[${tag}] "Đăng" never became enabled within ${Math.round(timeoutMs / 1000)}s`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
183
279
|
async function run({ page, payload, log }) {
|
|
184
280
|
const {
|
|
185
281
|
video_url, title, description = '', tags = [],
|
|
@@ -187,7 +283,7 @@ async function run({ page, payload, log }) {
|
|
|
187
283
|
} = payload || {};
|
|
188
284
|
if (!video_url) throw new Error('No video_url provided');
|
|
189
285
|
|
|
190
|
-
log('info', '[fb-pw] selectors version=2026.06.
|
|
286
|
+
log('info', '[fb-pw] selectors version=2026.06.14b-pub-wait');
|
|
191
287
|
|
|
192
288
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
193
289
|
|
|
@@ -389,7 +485,7 @@ async function run({ page, payload, log }) {
|
|
|
389
485
|
page.waitForEvent('filechooser', { timeout: 8000 }),
|
|
390
486
|
btn.click({ timeout: 3000 }),
|
|
391
487
|
]);
|
|
392
|
-
await chooser.
|
|
488
|
+
await setVideoFile(page, chooser.element(), videoPath, log);
|
|
393
489
|
videoSet = true;
|
|
394
490
|
log('info', `[fb-pw] video file set via modal-scoped "${(await btn.innerText().catch(() => '')).slice(0, 30)}" button`);
|
|
395
491
|
} catch (e) {
|
|
@@ -402,7 +498,7 @@ async function run({ page, payload, log }) {
|
|
|
402
498
|
const fi = reelsDialog.locator("input[type='file']").last();
|
|
403
499
|
if (await fi.count().catch(() => 0) > 0) {
|
|
404
500
|
try {
|
|
405
|
-
await fi.
|
|
501
|
+
await setVideoFile(page, await fi.elementHandle(), videoPath, log);
|
|
406
502
|
videoSet = true;
|
|
407
503
|
log('info', '[fb-pw] video file set via modal-scoped fallback input[type=file]');
|
|
408
504
|
} catch (e) { log('info', `[fb-pw] modal fallback input setInputFiles failed: ${e.message.slice(0, 80)}`); }
|
|
@@ -810,7 +906,8 @@ async function run({ page, payload, log }) {
|
|
|
810
906
|
// iterations, throw with diagnostics.
|
|
811
907
|
let published = false;
|
|
812
908
|
let customThumbDone = false;
|
|
813
|
-
|
|
909
|
+
let pubWaitDone = false; // guard: wait for a disabled "Đăng" at most once
|
|
910
|
+
for (let step = 0; step < 7 && !published; step++) {
|
|
814
911
|
await page.waitForTimeout(3000);
|
|
815
912
|
await fillMetadata();
|
|
816
913
|
|
|
@@ -1442,6 +1539,16 @@ async function run({ page, payload, log }) {
|
|
|
1442
1539
|
// No publish yet — advance via Tiếp.
|
|
1443
1540
|
const next = await findByVerbs(nextVerbs);
|
|
1444
1541
|
if (!next) {
|
|
1542
|
+
// Final "Cài đặt thước phim" step: the only action is "Đăng", which FB
|
|
1543
|
+
// keeps DISABLED while it ingests the video. For >50MB reup clips that
|
|
1544
|
+
// runs past the upload-wait, so the publish branch above found nothing
|
|
1545
|
+
// and there's no "Tiếp". Wait (once) for "Đăng" to enable, then let the
|
|
1546
|
+
// publish branch click it on the next iteration.
|
|
1547
|
+
if (!pubWaitDone) {
|
|
1548
|
+
pubWaitDone = true;
|
|
1549
|
+
log('info', `[fb-pw] no "Tiếp" + no enabled publish at step ${step + 1} — waiting for "Đăng" to enable (large-video processing)…`);
|
|
1550
|
+
if (await waitForPublishEnabled(page, publishVerbs, log, 180_000)) { step--; continue; }
|
|
1551
|
+
}
|
|
1445
1552
|
await dumpInventory(page, log, `no-advance-${step + 1}`);
|
|
1446
1553
|
await dumpFailure(page, `no-advance-${step + 1}`, log);
|
|
1447
1554
|
throw new Error(`FB composer step ${step + 1}: neither publish nor Tiếp button found`);
|