channel-worker 1.0.17 → 1.0.19

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.
Files changed (2) hide show
  1. package/lib/command-poller.js +147 -16
  2. package/package.json +1 -1
@@ -49,6 +49,9 @@ class CommandPoller {
49
49
  case 'save_file':
50
50
  await this.handleSaveFile(command);
51
51
  break;
52
+ case 'set_thumbnail':
53
+ await this.handleSetThumbnail(command);
54
+ break;
52
55
  default:
53
56
  // Other commands (scan_facebook_pages, etc.) handled by extension
54
57
  console.log(`[commands] Skipping ${command.type} — handled by extension`);
@@ -88,32 +91,41 @@ class CommandPoller {
88
91
  const baseExtPath = this.config.extension_path || '';
89
92
  let extensionPath = baseExtPath;
90
93
 
91
- // Create per-profile extension dir with config.json containing profileId
94
+ // Create unique extension dir each launch prevents Chromium bytecode cache
92
95
  if (baseExtPath && profile_id) {
93
96
  const fs = require('fs');
94
97
  const path = require('path');
95
- const profileExtPath = baseExtPath + '-' + profile_id;
98
+ const ts = Date.now();
99
+ const uniqueExtPath = baseExtPath + '-' + profile_id + '-' + ts;
96
100
  try {
97
- // Copy extension files to per-profile dir (if not exists or outdated)
98
- if (!fs.existsSync(profileExtPath)) fs.mkdirSync(profileExtPath, { recursive: true });
101
+ fs.mkdirSync(uniqueExtPath, { recursive: true });
99
102
  const files = fs.readdirSync(baseExtPath);
100
103
  for (const f of files) {
101
104
  const src = path.join(baseExtPath, f);
102
- const dst = path.join(profileExtPath, f);
103
- if (fs.statSync(src).isFile()) {
104
- // Copy if newer
105
- const srcMtime = fs.statSync(src).mtimeMs;
106
- const dstMtime = fs.existsSync(dst) ? fs.statSync(dst).mtimeMs : 0;
107
- if (srcMtime > dstMtime) fs.copyFileSync(src, dst);
108
- }
105
+ if (fs.statSync(src).isFile()) fs.copyFileSync(src, path.join(uniqueExtPath, f));
109
106
  }
110
107
  // Write profile-specific config.json
111
- const config = { channelManagerApi: 'https://api.channel.tunasm.art', profileId: profile_id };
112
- fs.writeFileSync(path.join(profileExtPath, 'config.json'), JSON.stringify(config));
113
- extensionPath = profileExtPath;
114
- console.log(`[commands] Extension dir: ${profileExtPath} (profile: ${profile_id})`);
108
+ fs.writeFileSync(path.join(uniqueExtPath, 'config.json'), JSON.stringify({
109
+ channelManagerApi: 'https://api.channel.tunasm.art',
110
+ profileId: profile_id,
111
+ }));
112
+ extensionPath = uniqueExtPath;
113
+ console.log(`[commands] Extension dir: ${uniqueExtPath}`);
114
+
115
+ // Cleanup old dirs (keep last 3)
116
+ const parent = path.dirname(baseExtPath);
117
+ const baseName = path.basename(baseExtPath);
118
+ const oldDirs = fs.readdirSync(parent)
119
+ .filter(d => d.startsWith(baseName + '-') && d !== path.basename(uniqueExtPath))
120
+ .map(d => path.join(parent, d))
121
+ .filter(d => fs.statSync(d).isDirectory())
122
+ .sort()
123
+ .slice(0, -3);
124
+ for (const d of oldDirs) {
125
+ try { fs.rmSync(d, { recursive: true }); } catch {}
126
+ }
115
127
  } catch (e) {
116
- console.warn(`[commands] Per-profile ext dir failed: ${e.message}, using base`);
128
+ console.warn(`[commands] Unique ext dir failed: ${e.message}, using base`);
117
129
  }
118
130
  }
119
131
 
@@ -203,6 +215,125 @@ class CommandPoller {
203
215
  }
204
216
  }
205
217
 
218
+ async handleSetThumbnail(command) {
219
+ const { thumbnail_url, profile_id, file_path } = command.payload || {};
220
+ console.log(`[commands] Setting thumbnail for profile: ${profile_id}`);
221
+ try {
222
+ const fs = require('fs');
223
+ const path = require('path');
224
+ const WebSocket = require('ws');
225
+
226
+ // 1. Download thumbnail to disk
227
+ const dir = path.dirname(file_path);
228
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
229
+ const res = await fetch(thumbnail_url);
230
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
231
+ const buffer = Buffer.from(await res.arrayBuffer());
232
+ fs.writeFileSync(file_path, buffer);
233
+ console.log(`[commands] Thumbnail saved: ${file_path} (${buffer.length} bytes)`);
234
+
235
+ // 2. Find running browser for this profile
236
+ if (!this.nst) {
237
+ const apiKey = await this.api.getSetting('nst_api_key');
238
+ if (apiKey) this.nst = new NstManager(apiKey);
239
+ }
240
+ const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
241
+ headers: { 'x-api-key': this.nst?.apiKey || '' },
242
+ });
243
+ const browsersData = await browsersRes.json();
244
+ const browsers = browsersData?.data || [];
245
+ const browser = browsers.find(b => b.name === profile_id) || browsers[0];
246
+ if (!browser) throw new Error('No running browser found');
247
+
248
+ const debugPort = browser.remoteDebuggingPort;
249
+ console.log(`[commands] Browser debug port: ${debugPort}`);
250
+
251
+ // 3. Get WebSocket URL for YouTube Studio tab
252
+ const pagesRes = await fetch(`http://localhost:${debugPort}/json/list`);
253
+ const pages = await pagesRes.json();
254
+ const studioPage = pages.find(p => p.url?.includes('studio.youtube.com') && p.type === 'page');
255
+ if (!studioPage) throw new Error('YouTube Studio tab not found');
256
+
257
+ const wsUrl = studioPage.webSocketDebuggerUrl;
258
+ console.log(`[commands] Connecting CDP: ${wsUrl}`);
259
+
260
+ // 4. Use CDP to set thumbnail file
261
+ const result = await new Promise((resolve, reject) => {
262
+ const ws = new WebSocket(wsUrl);
263
+ let msgId = 1;
264
+
265
+ function send(method, params = {}) {
266
+ const id = msgId++;
267
+ return new Promise((res, rej) => {
268
+ const handler = (data) => {
269
+ const msg = JSON.parse(data);
270
+ if (msg.id === id) {
271
+ ws.removeListener('message', handler);
272
+ if (msg.error) rej(new Error(msg.error.message));
273
+ else res(msg.result);
274
+ }
275
+ };
276
+ ws.on('message', handler);
277
+ ws.send(JSON.stringify({ id, method, params }));
278
+ });
279
+ }
280
+
281
+ ws.on('open', async () => {
282
+ try {
283
+ await send('DOM.enable');
284
+ const doc = await send('DOM.getDocument');
285
+ const node = await send('DOM.querySelector', {
286
+ nodeId: doc.root.nodeId,
287
+ selector: '#file-loader',
288
+ });
289
+
290
+ if (!node?.nodeId) {
291
+ ws.close();
292
+ resolve({ success: false, error: '#file-loader not found' });
293
+ return;
294
+ }
295
+
296
+ await send('DOM.setFileInputFiles', {
297
+ nodeId: node.nodeId,
298
+ files: [file_path],
299
+ });
300
+
301
+ // Trigger change event
302
+ await send('Runtime.evaluate', {
303
+ expression: `
304
+ const inp = document.querySelector('#file-loader');
305
+ if (inp) {
306
+ inp.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
307
+ inp.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
308
+ }
309
+ 'triggered'
310
+ `,
311
+ });
312
+
313
+ ws.close();
314
+ resolve({ success: true, file_path });
315
+ } catch (e) {
316
+ ws.close();
317
+ resolve({ success: false, error: e.message });
318
+ }
319
+ });
320
+
321
+ ws.on('error', (e) => reject(e));
322
+ setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 15000);
323
+ });
324
+
325
+ console.log(`[commands] Thumbnail CDP result:`, result);
326
+ await this.api.updateCommand(command._id, {
327
+ status: result.success ? 'done' : 'failed',
328
+ result,
329
+ error: result.error || null,
330
+ });
331
+ } catch (err) {
332
+ console.error(`[commands] Set thumbnail failed: ${err.message}`);
333
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
334
+ }
335
+ }
336
+
206
337
  async handleCloseProfile(command) {
207
338
  const { profile_id } = command.payload || {};
208
339
  console.log(`[commands] Closing profile: ${profile_id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
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": {