channel-worker 1.5.0 → 1.5.2

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/stats-syncer.js +226 -272
  2. package/package.json +1 -1
@@ -10,18 +10,12 @@ class StatsSyncer {
10
10
  this.api = apiClient;
11
11
  }
12
12
 
13
- /**
14
- * Sync stats for a single channel via YouTube Studio
15
- * @param {object} channel - { _id, name, nst_profile_id, proxy }
16
- * @returns {object} { subscribers, total_views, video_count }
17
- */
18
13
  async syncYouTubeStats(channel) {
19
14
  const profileId = channel.nst_profile_id;
20
15
  if (!profileId) throw new Error(`Channel "${channel.name}" has no nst_profile_id`);
21
16
 
22
17
  console.log(`[stats] Syncing YouTube stats for "${channel.name}" (profile: ${profileId})`);
23
18
 
24
- // Launch profile (no extension needed for stats)
25
19
  const { wsEndpoint } = await this.nst.launchProfile(profileId, {
26
20
  proxy: channel.proxy || null,
27
21
  });
@@ -29,10 +23,9 @@ class StatsSyncer {
29
23
  if (!wsEndpoint) throw new Error('No wsEndpoint returned from Nstbrowser');
30
24
 
31
25
  try {
32
- const stats = await this.scrapeYouTubeStudio(wsEndpoint);
26
+ const stats = await this.scrapeViaCDP(wsEndpoint);
33
27
  console.log(`[stats] "${channel.name}" → subs: ${stats.subscribers}, views: ${stats.total_views}, videos: ${stats.video_count}`);
34
28
 
35
- // Send to API
36
29
  await this.api.request('POST', '/analytics/sync', {
37
30
  channel_id: channel._id,
38
31
  platform: 'youtube',
@@ -44,7 +37,6 @@ class StatsSyncer {
44
37
  console.log(`[stats] "${channel.name}" stats saved to API`);
45
38
  return stats;
46
39
  } finally {
47
- // Close browser after scraping
48
40
  try {
49
41
  await this.nst.stopProfile(profileId);
50
42
  } catch (err) {
@@ -53,32 +45,28 @@ class StatsSyncer {
53
45
  }
54
46
  }
55
47
 
56
- /**
57
- * Connect to browser via CDP WebSocket and scrape YouTube Studio
58
- */
59
- async scrapeYouTubeStudio(wsEndpoint) {
48
+ scrapeViaCDP(wsEndpoint) {
60
49
  return new Promise((resolve, reject) => {
61
50
  const ws = new WebSocket(wsEndpoint);
62
51
  let msgId = 1;
63
52
  const pending = new Map();
64
53
 
65
- const send = (method, params = {}) => {
54
+ const send = (method, params = {}, sid) => {
66
55
  return new Promise((res, rej) => {
67
56
  const id = msgId++;
68
57
  pending.set(id, { resolve: res, reject: rej });
69
- ws.send(JSON.stringify({ id, method, params }));
58
+ const msg = { id, method, params };
59
+ if (sid) msg.sessionId = sid;
60
+ ws.send(JSON.stringify(msg));
70
61
  });
71
62
  };
72
63
 
73
64
  const timeout = setTimeout(() => {
74
65
  ws.close();
75
- reject(new Error('YouTube Studio scrape timed out (60s)'));
76
- }, 60000);
66
+ reject(new Error('Stats scrape timed out (90s)'));
67
+ }, 90000);
77
68
 
78
- ws.on('error', (err) => {
79
- clearTimeout(timeout);
80
- reject(err);
81
- });
69
+ ws.on('error', (err) => { clearTimeout(timeout); reject(err); });
82
70
 
83
71
  ws.on('message', (data) => {
84
72
  try {
@@ -94,50 +82,231 @@ class StatsSyncer {
94
82
 
95
83
  ws.on('open', async () => {
96
84
  try {
97
- // Get list of targets (pages)
98
85
  const targets = await send('Target.getTargets');
99
86
  const pages = targets.targetInfos.filter(t => t.type === 'page');
87
+ const targetId = pages.length > 0 ? pages[0].targetId : (await send('Target.createTarget', { url: 'about:blank' })).targetId;
100
88
 
101
- // Find or create a page
102
- let targetId;
103
- if (pages.length > 0) {
104
- targetId = pages[0].targetId;
105
- } else {
106
- const created = await send('Target.createTarget', { url: 'about:blank' });
107
- targetId = created.targetId;
108
- }
89
+ const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
90
+ const s = (method, params = {}) => send(method, params, sessionId);
109
91
 
110
- // Attach to target
111
- const { sessionId } = await send('Target.attachToTarget', {
112
- targetId,
113
- flatten: true,
114
- });
92
+ await s('Page.enable');
93
+ await s('Runtime.enable');
94
+
95
+ let stats = { subscribers: 0, total_views: 0, video_count: 0 };
96
+
97
+ // Step 1: Go to YouTube Studio → auto-redirects to correct channel
98
+ console.log('[stats] Navigating to YouTube Studio...');
99
+ await s('Page.navigate', { url: 'https://studio.youtube.com' });
100
+ await this.waitForLoad(s);
101
+ await this.sleep(5000);
102
+
103
+ // Get channel ID from URL
104
+ const urlResult = await s('Runtime.evaluate', { expression: 'window.location.href', returnByValue: true });
105
+ const studioUrl = urlResult?.result?.value || '';
106
+ console.log(`[stats] Studio URL: ${studioUrl}`);
107
+
108
+ const channelIdMatch = studioUrl.match(/channel\/(UC[\w-]+)/);
109
+ const channelId = channelIdMatch ? channelIdMatch[1] : null;
115
110
 
116
- // Helper to send commands to the attached session
117
- const sessionSend = (method, params = {}) => {
118
- return new Promise((res, rej) => {
119
- const id = msgId++;
120
- pending.set(id, { resolve: res, reject: rej });
121
- ws.send(JSON.stringify({ id, method, params, sessionId }));
111
+ if (!channelId) {
112
+ // Try ytcfg
113
+ const cfgResult = await s('Runtime.evaluate', {
114
+ expression: '(window.ytcfg && window.ytcfg.get) ? window.ytcfg.get("CHANNEL_ID") : ""',
115
+ returnByValue: true,
122
116
  });
123
- };
117
+ console.log(`[stats] ytcfg CHANNEL_ID: ${cfgResult?.result?.value}`);
118
+ }
124
119
 
125
- // Enable page events
126
- await sessionSend('Page.enable');
120
+ const cid = channelId || (await s('Runtime.evaluate', {
121
+ expression: '(window.ytcfg && window.ytcfg.get) ? window.ytcfg.get("CHANNEL_ID") : ""',
122
+ returnByValue: true,
123
+ }))?.result?.value;
124
+
125
+ // Step 2: Navigate to Studio Analytics page (English URL works regardless of UI language)
126
+ if (cid) {
127
+ const analyticsUrl = `https://studio.youtube.com/channel/${cid}/analytics/tab-overview/period-default`;
128
+ console.log(`[stats] Navigating to analytics: ${analyticsUrl}`);
129
+ await s('Page.navigate', { url: analyticsUrl });
130
+ await this.waitForLoad(s);
131
+ await this.sleep(6000);
132
+
133
+ // Scrape analytics page — look for numbers regardless of language
134
+ const analyticsResult = await s('Runtime.evaluate', {
135
+ expression: `
136
+ (function() {
137
+ var result = { subscribers: 0, total_views: 0, video_count: 0, debug_text: '' };
138
+ var text = document.body ? document.body.innerText : '';
139
+ result.debug_text = text.substring(0, 800);
140
+
141
+ // YouTube Studio Analytics shows metric cards with labels and numbers
142
+ // The layout is: Label (in any language) followed by a number
143
+ // We look for large numbers near subscriber/view keywords in ANY language
144
+
145
+ // Strategy: find all metric-like elements by looking for number patterns
146
+ // Studio analytics typically shows: Views, Watch time, Subscribers
147
+ // in cards with the number prominently displayed
148
+
149
+ // Try to find subscriber-related numbers
150
+ // Multi-language patterns: subscribers, người đăng ký, abonnés, Abonnenten, etc.
151
+ var subKeywords = /subscri|đăng ký|abonn|подписч|구독/i;
152
+ var viewKeywords = /views?|lượt xem|vues?|aufrufe|просмотр|조회/i;
153
+
154
+ var lines = text.split('\\n').map(function(l) { return l.trim(); }).filter(function(l) { return l.length > 0; });
155
+
156
+ for (var i = 0; i < lines.length; i++) {
157
+ var line = lines[i];
158
+ var nextLine = lines[i + 1] || '';
159
+ var prevLine = lines[i - 1] || '';
160
+
161
+ // Check if this line or adjacent lines contain keywords
162
+ if (subKeywords.test(line) || subKeywords.test(prevLine)) {
163
+ // Look for a number in this line or nearby
164
+ var numInLine = extractNumber(line);
165
+ var numInNext = extractNumber(nextLine);
166
+ if (numInLine > 0 && result.subscribers === 0) result.subscribers = numInLine;
167
+ else if (numInNext > 0 && result.subscribers === 0) result.subscribers = numInNext;
168
+ }
169
+
170
+ if (viewKeywords.test(line) || viewKeywords.test(prevLine)) {
171
+ var numInLine2 = extractNumber(line);
172
+ var numInNext2 = extractNumber(nextLine);
173
+ if (numInLine2 > 0 && result.total_views === 0) result.total_views = numInLine2;
174
+ else if (numInNext2 > 0 && result.total_views === 0) result.total_views = numInNext2;
175
+ }
176
+ }
177
+
178
+ // Also try: Current subscribers count shown in Studio sidebar
179
+ // Pattern: "Current subscribers\\n123" or "Người đăng ký hiện tại\\n123"
180
+ var currentSubMatch = text.match(/(?:current subscribers|người đăng ký hiện tại|subscriber|đăng ký)[\\s\\n]*([\\d,\\.]+[KMB]?)/i);
181
+ if (currentSubMatch && result.subscribers === 0) {
182
+ result.subscribers = parseHumanNum(currentSubMatch[1]);
183
+ }
184
+
185
+ function extractNumber(str) {
186
+ if (!str) return 0;
187
+ // Match standalone numbers like "1,234" or "5.2K" or "123"
188
+ var m = str.match(/^([\\d,\\.]+[KMB]?)$/i) || str.match(/\\b([\\d,\\.]+[KMB]?)\\b/);
189
+ if (m) return parseHumanNum(m[1]);
190
+ return 0;
191
+ }
192
+
193
+ function parseHumanNum(str) {
194
+ if (!str) return 0;
195
+ str = str.replace(/,/g, '').trim();
196
+ var m = str.match(/([\\d\\.]+)\\s*([KMB])/i);
197
+ if (m) {
198
+ var n = parseFloat(m[1]);
199
+ var s = m[2].toUpperCase();
200
+ if (s === 'K') return Math.round(n * 1000);
201
+ if (s === 'M') return Math.round(n * 1000000);
202
+ if (s === 'B') return Math.round(n * 1000000000);
203
+ }
204
+ return parseInt(str, 10) || 0;
205
+ }
206
+
207
+ return JSON.stringify(result);
208
+ })()
209
+ `,
210
+ returnByValue: true,
211
+ });
127
212
 
128
- // Navigate to YouTube Studio dashboard
129
- await sessionSend('Page.navigate', {
130
- url: 'https://studio.youtube.com',
131
- });
213
+ try {
214
+ var parsed = JSON.parse(analyticsResult?.result?.value || '{}');
215
+ console.log('[stats] Analytics debug text (first 300):', (parsed.debug_text || '').substring(0, 300));
216
+ if (parsed.subscribers > 0) stats.subscribers = parsed.subscribers;
217
+ if (parsed.total_views > 0) stats.total_views = parsed.total_views;
218
+ } catch {}
219
+ }
132
220
 
133
- // Wait for page to load
134
- await this.waitForLoad(sessionSend, 15000);
221
+ // Step 3: Get video count + subscriber from channel page via ytInitialData
222
+ var channelPageUrl = cid
223
+ ? 'https://www.youtube.com/channel/' + cid
224
+ : 'https://www.youtube.com/@me';
225
+
226
+ console.log('[stats] Navigating to channel page:', channelPageUrl);
227
+ await s('Page.navigate', { url: channelPageUrl });
228
+ await this.waitForLoad(s);
229
+ await this.sleep(4000);
230
+
231
+ var ytResult = await s('Runtime.evaluate', {
232
+ expression: `
233
+ (function() {
234
+ var result = { subscribers: 0, total_views: 0, video_count: 0 };
235
+ try {
236
+ var data = window.ytInitialData;
237
+ if (!data) return JSON.stringify({ error: 'no ytInitialData' });
238
+
239
+ // Try pageHeaderRenderer (new layout)
240
+ var pageHeader = data.header && data.header.pageHeaderRenderer;
241
+ if (pageHeader) {
242
+ var content = pageHeader.content && pageHeader.content.pageHeaderViewModel;
243
+ if (content && content.metadata && content.metadata.contentMetadataViewModel) {
244
+ var rows = content.metadata.contentMetadataViewModel.metadataRows || [];
245
+ for (var r = 0; r < rows.length; r++) {
246
+ var parts = rows[r].metadataParts || [];
247
+ for (var p = 0; p < parts.length; p++) {
248
+ var t = (parts[p].text && parts[p].text.content) || '';
249
+ // subscriber text like "123 subscribers" or "0 người đăng ký"
250
+ if (/subscri|đăng ký/i.test(t)) {
251
+ var m = t.match(/([\\d,\\.]+[KMB]?)/);
252
+ if (m) result.subscribers = parseN(m[1]);
253
+ }
254
+ // video count like "1 video" or "15 video"
255
+ if (/video/i.test(t)) {
256
+ var m2 = t.match(/([\\d,]+)/);
257
+ if (m2) result.video_count = parseInt(m2[1].replace(/,/g, ''), 10);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ // Try c4TabbedHeaderRenderer (old layout)
265
+ var c4Header = data.header && data.header.c4TabbedHeaderRenderer;
266
+ if (c4Header) {
267
+ var subText = c4Header.subscriberCountText && c4Header.subscriberCountText.simpleText || '';
268
+ if (subText) {
269
+ var m3 = subText.match(/([\\d,\\.]+[KMB]?)/);
270
+ if (m3 && result.subscribers === 0) result.subscribers = parseN(m3[1]);
271
+ }
272
+ if (c4Header.videosCountText) {
273
+ var vText = c4Header.videosCountText.runs ? c4Header.videosCountText.runs.map(function(r){return r.text}).join('') : (c4Header.videosCountText.simpleText || '');
274
+ var m4 = vText.match(/([\\d,]+)/);
275
+ if (m4) result.video_count = parseInt(m4[1].replace(/,/g, ''), 10);
276
+ }
277
+ }
278
+
279
+ } catch (e) {
280
+ return JSON.stringify({ error: e.message });
281
+ }
135
282
 
136
- // Wait a bit for Studio SPA to render
137
- await this.sleep(5000);
283
+ function parseN(str) {
284
+ if (!str) return 0;
285
+ str = str.replace(/,/g, '').trim();
286
+ var m = str.match(/([\\d\\.]+)\\s*([KMB])/i);
287
+ if (m) {
288
+ var n = parseFloat(m[1]);
289
+ var s = m[2].toUpperCase();
290
+ if (s === 'K') return Math.round(n * 1000);
291
+ if (s === 'M') return Math.round(n * 1000000);
292
+ if (s === 'B') return Math.round(n * 1000000000);
293
+ }
294
+ return parseInt(str, 10) || 0;
295
+ }
296
+
297
+ return JSON.stringify(result);
298
+ })()
299
+ `,
300
+ returnByValue: true,
301
+ });
138
302
 
139
- // Try to scrape stats from YouTube Studio dashboard
140
- const stats = await this.extractStudioStats(sessionSend);
303
+ try {
304
+ var ytData = JSON.parse(ytResult?.result?.value || '{}');
305
+ console.log('[stats] ytInitialData result:', JSON.stringify(ytData));
306
+ if (ytData.subscribers > 0 && stats.subscribers === 0) stats.subscribers = ytData.subscribers;
307
+ if (ytData.video_count > 0) stats.video_count = ytData.video_count;
308
+ if (ytData.total_views > 0 && stats.total_views === 0) stats.total_views = ytData.total_views;
309
+ } catch {}
141
310
 
142
311
  clearTimeout(timeout);
143
312
  ws.close();
@@ -151,232 +320,17 @@ class StatsSyncer {
151
320
  });
152
321
  }
153
322
 
154
- async waitForLoad(sessionSend, timeoutMs = 15000) {
323
+ async waitForLoad(s, timeoutMs = 15000) {
155
324
  const start = Date.now();
156
325
  while (Date.now() - start < timeoutMs) {
157
326
  try {
158
- const result = await sessionSend('Runtime.evaluate', {
159
- expression: 'document.readyState',
160
- returnByValue: true,
161
- });
162
- if (result?.result?.value === 'complete') return;
327
+ const r = await s('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true });
328
+ if (r?.result?.value === 'complete') return;
163
329
  } catch {}
164
330
  await this.sleep(500);
165
331
  }
166
332
  }
167
333
 
168
- async extractStudioStats(sessionSend) {
169
- // YouTube Studio dashboard shows channel-level stats
170
- // We'll try multiple approaches to get the data
171
-
172
- // Approach 1: Navigate to channel analytics overview and extract from the page
173
- await sessionSend('Page.navigate', {
174
- url: 'https://studio.youtube.com/channel/UC/analytics/tab-overview/period-default',
175
- });
176
- await this.sleep(5000);
177
-
178
- // Try to get subscriber count from the Studio header/sidebar
179
- const statsResult = await sessionSend('Runtime.evaluate', {
180
- expression: `
181
- (function() {
182
- const stats = { subscribers: 0, total_views: 0, video_count: 0 };
183
-
184
- // Try to get subscriber count from channel dashboard
185
- // YouTube Studio shows subscriber count in various places
186
- const allText = document.body.innerText || '';
187
-
188
- // Method 1: Look for subscriber count in the top bar
189
- // YouTube Studio typically shows "X subscribers" somewhere
190
- const subMatch = allText.match(/([\\d,\\.]+[KMB]?)\\s*subscribers?/i);
191
- if (subMatch) {
192
- stats.subscribers = parseHumanNumber(subMatch[1]);
193
- }
194
-
195
- // Method 2: Look for specific YouTube Studio elements
196
- // The channel dashboard header often has subscriber count
197
- const headerEl = document.querySelector('#channel-title .subscriber-count, .subscriber-count, [class*="subscriber"]');
198
- if (headerEl) {
199
- const text = headerEl.textContent.trim();
200
- const num = text.match(/([\\d,\\.]+[KMB]?)/);
201
- if (num) stats.subscribers = parseHumanNumber(num[1]);
202
- }
203
-
204
- // Try ytInitialData or ytcfg for exact numbers
205
- try {
206
- if (window.ytcfg) {
207
- const data = window.ytcfg.data_ || {};
208
- // Look through ytcfg for subscriber info
209
- }
210
- } catch {}
211
-
212
- function parseHumanNumber(str) {
213
- if (!str) return 0;
214
- str = str.replace(/,/g, '').trim();
215
- const mult = str.match(/([\\d\\.]+)\\s*([KMB])/i);
216
- if (mult) {
217
- const num = parseFloat(mult[1]);
218
- const suffix = mult[2].toUpperCase();
219
- if (suffix === 'K') return Math.round(num * 1000);
220
- if (suffix === 'M') return Math.round(num * 1000000);
221
- if (suffix === 'B') return Math.round(num * 1000000000);
222
- }
223
- return parseInt(str, 10) || 0;
224
- }
225
-
226
- return JSON.stringify(stats);
227
- })()
228
- `,
229
- returnByValue: true,
230
- });
231
-
232
- let stats = { subscribers: 0, total_views: 0, video_count: 0 };
233
- try {
234
- stats = JSON.parse(statsResult?.result?.value || '{}');
235
- } catch {}
236
-
237
- // Approach 2: Use YouTube Studio's internal API via fetch in page context
238
- // This gives exact numbers from the Studio API
239
- const apiResult = await sessionSend('Runtime.evaluate', {
240
- expression: `
241
- (async function() {
242
- try {
243
- // YouTube Studio uses internal API calls
244
- // We can intercept the channel data from the page's __INITIAL_DATA__ or similar
245
-
246
- // Try getting data from the Studio SPA state
247
- const scripts = document.querySelectorAll('script');
248
- for (const s of scripts) {
249
- const text = s.textContent || '';
250
-
251
- // Look for serialized channel data
252
- if (text.includes('subscriberCount') || text.includes('subscriber_count')) {
253
- const subMatch = text.match(/"subscriberCount"\\s*:\\s*"?(\\d+)"?/);
254
- const viewMatch = text.match(/"viewCount"\\s*:\\s*"?(\\d+)"?/);
255
- const videoMatch = text.match(/"videoCount"\\s*:\\s*"?(\\d+)"?/);
256
-
257
- if (subMatch || viewMatch || videoMatch) {
258
- return JSON.stringify({
259
- subscribers: parseInt(subMatch?.[1] || '0', 10),
260
- total_views: parseInt(viewMatch?.[1] || '0', 10),
261
- video_count: parseInt(videoMatch?.[1] || '0', 10),
262
- });
263
- }
264
- }
265
- }
266
-
267
- // Fallback: try YouTube Data endpoint from within the page (same origin, authenticated)
268
- const res = await fetch('https://studio.youtube.com/youtubei/v1/creator/get_creator_channels?key=AIzaSyBUPetSUmoZL-OhlxA7wSac5XinrGktHmQ', {
269
- method: 'POST',
270
- headers: { 'Content-Type': 'application/json' },
271
- body: JSON.stringify({
272
- context: {
273
- client: { clientName: 62, clientVersion: '1.0' },
274
- },
275
- }),
276
- });
277
-
278
- if (res.ok) {
279
- const data = await res.json();
280
- const ch = data?.channels?.[0] || {};
281
- return JSON.stringify({
282
- subscribers: parseInt(ch.subscriberCount || '0', 10),
283
- total_views: parseInt(ch.viewCount || '0', 10),
284
- video_count: parseInt(ch.videoCount || '0', 10),
285
- });
286
- }
287
- } catch (e) {
288
- return JSON.stringify({ error: e.message });
289
- }
290
- return JSON.stringify({});
291
- })()
292
- `,
293
- awaitPromise: true,
294
- returnByValue: true,
295
- });
296
-
297
- try {
298
- const apiStats = JSON.parse(apiResult?.result?.value || '{}');
299
- if (!apiStats.error) {
300
- // Merge: prefer API data if available
301
- if (apiStats.subscribers > 0) stats.subscribers = apiStats.subscribers;
302
- if (apiStats.total_views > 0) stats.total_views = apiStats.total_views;
303
- if (apiStats.video_count > 0) stats.video_count = apiStats.video_count;
304
- }
305
- } catch {}
306
-
307
- // Approach 3: Navigate to the public channel page as fallback
308
- if (stats.subscribers === 0 && stats.total_views === 0) {
309
- console.log('[stats] Studio scrape returned no data, trying public channel page...');
310
- await sessionSend('Page.navigate', {
311
- url: 'https://www.youtube.com/@me',
312
- });
313
- await this.waitForLoad(sessionSend, 10000);
314
- await this.sleep(3000);
315
-
316
- const publicResult = await sessionSend('Runtime.evaluate', {
317
- expression: `
318
- (function() {
319
- const stats = { subscribers: 0, total_views: 0, video_count: 0 };
320
-
321
- // ytInitialData contains channel info
322
- try {
323
- const data = window.ytInitialData;
324
- const header = data?.header?.c4TabbedHeaderRenderer || data?.header?.pageHeaderRenderer;
325
-
326
- // Subscriber count
327
- const subText = header?.subscriberCountText?.simpleText || '';
328
- const subMatch = subText.match(/([\\d,\\.]+[KMB]?)/);
329
- if (subMatch) {
330
- stats.subscribers = parseHumanNumber(subMatch[1]);
331
- }
332
-
333
- // Video count from tabs
334
- const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
335
- for (const tab of tabs) {
336
- const title = tab?.tabRenderer?.title || '';
337
- if (title === 'Videos' || title === 'Shorts') {
338
- // Video tab might show count
339
- }
340
- }
341
-
342
- // Try metadata
343
- const meta = data?.metadata?.channelMetadataRenderer;
344
- if (meta) {
345
- // Some data available here
346
- }
347
- } catch {}
348
-
349
- function parseHumanNumber(str) {
350
- if (!str) return 0;
351
- str = str.replace(/,/g, '').trim();
352
- const mult = str.match(/([\\d\\.]+)\\s*([KMB])/i);
353
- if (mult) {
354
- const num = parseFloat(mult[1]);
355
- const suffix = mult[2].toUpperCase();
356
- if (suffix === 'K') return Math.round(num * 1000);
357
- if (suffix === 'M') return Math.round(num * 1000000);
358
- if (suffix === 'B') return Math.round(num * 1000000000);
359
- }
360
- return parseInt(str, 10) || 0;
361
- }
362
-
363
- return JSON.stringify(stats);
364
- })()
365
- `,
366
- returnByValue: true,
367
- });
368
-
369
- try {
370
- const publicStats = JSON.parse(publicResult?.result?.value || '{}');
371
- if (publicStats.subscribers > 0) stats.subscribers = publicStats.subscribers;
372
- if (publicStats.total_views > 0) stats.total_views = publicStats.total_views;
373
- if (publicStats.video_count > 0) stats.video_count = publicStats.video_count;
374
- } catch {}
375
- }
376
-
377
- return stats;
378
- }
379
-
380
334
  sleep(ms) {
381
335
  return new Promise(resolve => setTimeout(resolve, ms));
382
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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": {