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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.14",
3
+ "version": "2.5.16",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -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.12a-thumb-miss-soft');
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.setFiles(videoPath);
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.setInputFiles(videoPath);
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
- for (let step = 0; step < 6 && !published; step++) {
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`);