channel-worker 1.6.1 → 1.6.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.
@@ -899,8 +899,8 @@ class CommandPoller {
899
899
  }
900
900
 
901
901
  async handleSyncYoutubeStats(command) {
902
- const { channel_id } = command.payload || {};
903
- console.log(`[commands] Syncing YouTube stats for channel: ${channel_id}`);
902
+ const { channel_id, youtube_handle } = command.payload || {};
903
+ console.log(`[commands] Syncing YouTube stats for channel: ${channel_id} (handle: ${youtube_handle || 'none'})`);
904
904
 
905
905
  try {
906
906
  if (!this.nst) {
@@ -917,6 +917,9 @@ class CommandPoller {
917
917
  if (!channel) throw new Error(`Channel ${channel_id} not found`);
918
918
  if (!channel.nst_profile_id) throw new Error(`Channel "${channel.name}" has no Nstbrowser profile`);
919
919
 
920
+ // Pass youtube_handle from command payload
921
+ channel.youtube_handle = youtube_handle || null;
922
+
920
923
  const syncer = new StatsSyncer(this.nst, this.api);
921
924
  const stats = await syncer.syncYouTubeStats(channel);
922
925
 
@@ -2,8 +2,7 @@ const WebSocket = require('ws');
2
2
 
3
3
  /**
4
4
  * Sync YouTube stats via Nstbrowser CDP.
5
- * Flow: Launch profile → connect CDPget channel ID from Studio →
6
- * navigate to channel About page → read #additional-info-container table
5
+ * Flow: Launch profile → navigate to youtube.com/@handle/about read stats table
7
6
  */
8
7
  class StatsSyncer {
9
8
  constructor(nstManager, apiClient) {
@@ -23,8 +22,10 @@ class StatsSyncer {
23
22
 
24
23
  if (!wsEndpoint) throw new Error('No wsEndpoint returned from Nstbrowser');
25
24
 
25
+ const youtubeHandle = channel.youtube_handle;
26
+
26
27
  try {
27
- const stats = await this.scrapeViaCDP(wsEndpoint);
28
+ const stats = await this.scrapeViaCDP(wsEndpoint, youtubeHandle);
28
29
  console.log(`[stats] "${channel.name}" → subs: ${stats.subscribers}, views: ${stats.total_views}, videos: ${stats.video_count}`);
29
30
 
30
31
  await this.api.request('POST', '/analytics/sync', {
@@ -46,7 +47,7 @@ class StatsSyncer {
46
47
  }
47
48
  }
48
49
 
49
- scrapeViaCDP(wsEndpoint) {
50
+ scrapeViaCDP(wsEndpoint, youtubeHandle) {
50
51
  return new Promise((resolve, reject) => {
51
52
  const ws = new WebSocket(wsEndpoint);
52
53
  let msgId = 1;
@@ -93,86 +94,77 @@ class StatsSyncer {
93
94
  await s('Page.enable');
94
95
  await s('Runtime.enable');
95
96
 
96
- // Step 1: Go to YouTube Studio to get channel ID
97
- console.log('[stats] Navigating to YouTube Studio...');
98
- await s('Page.navigate', { url: 'https://studio.youtube.com' });
99
- await this.waitForLoad(s);
100
- await this.sleep(4000);
101
-
102
- const urlResult = await s('Runtime.evaluate', { expression: 'window.location.href', returnByValue: true });
103
- const studioUrl = urlResult?.result?.value || '';
104
- console.log(`[stats] Studio URL: ${studioUrl}`);
105
-
106
- // Extract channel ID
107
- let cid = null;
108
- const cidMatch = studioUrl.match(/channel\/(UC[\w-]+)/);
109
- if (cidMatch) {
110
- cid = cidMatch[1];
97
+ // Determine the about URL
98
+ let aboutUrl;
99
+ if (youtubeHandle) {
100
+ // Direct: youtube.com/@handle/about
101
+ const handle = youtubeHandle.startsWith('@') ? youtubeHandle : '@' + youtubeHandle;
102
+ aboutUrl = `https://www.youtube.com/${handle}/about`;
111
103
  } else {
112
- const cfgResult = await s('Runtime.evaluate', {
113
- expression: '(window.ytcfg && window.ytcfg.get) ? window.ytcfg.get("CHANNEL_ID") : ""',
114
- returnByValue: true,
115
- });
116
- cid = cfgResult?.result?.value || null;
104
+ // Fallback: go to Studio first to get channel ID
105
+ console.log('[stats] No youtube_handle, falling back to Studio...');
106
+ await s('Page.navigate', { url: 'https://studio.youtube.com' });
107
+ await this.waitForLoad(s);
108
+ await this.sleep(4000);
109
+
110
+ const urlResult = await s('Runtime.evaluate', { expression: 'window.location.href', returnByValue: true });
111
+ const studioUrl = urlResult?.result?.value || '';
112
+ const cidMatch = studioUrl.match(/channel\/(UC[\w-]+)/);
113
+ let cid = cidMatch ? cidMatch[1] : null;
114
+
115
+ if (!cid) {
116
+ const cfgResult = await s('Runtime.evaluate', {
117
+ expression: '(window.ytcfg && window.ytcfg.get) ? window.ytcfg.get("CHANNEL_ID") : ""',
118
+ returnByValue: true,
119
+ });
120
+ cid = cfgResult?.result?.value || null;
121
+ }
122
+
123
+ if (!cid) throw new Error('Could not find channel ID from YouTube Studio');
124
+ aboutUrl = `https://www.youtube.com/channel/${cid}/about`;
117
125
  }
118
126
 
119
- if (!cid) {
120
- throw new Error('Could not find channel ID from YouTube Studio');
121
- }
122
- console.log(`[stats] Channel ID: ${cid}`);
123
-
124
- // Step 2: Navigate to channel About page
125
- const aboutUrl = `https://www.youtube.com/channel/${cid}/about`;
127
+ // Navigate directly to about page
126
128
  console.log(`[stats] Navigating to: ${aboutUrl}`);
127
129
  await s('Page.navigate', { url: aboutUrl });
128
130
  await this.waitForLoad(s);
129
131
  await this.sleep(4000);
130
132
 
131
- // Step 3: Read #additional-info-container table
133
+ // Read #additional-info-container table
132
134
  const result = await s('Runtime.evaluate', {
133
135
  expression: `
134
136
  (function() {
135
137
  var stats = { subscribers: -1, total_views: -1, video_count: -1, debug: '' };
136
138
 
137
- // Find the table inside #additional-info-container
138
139
  var container = document.querySelector('#additional-info-container');
139
140
  if (!container) {
140
- stats.debug = 'no #additional-info-container found';
141
- // Fallback: try to find stats from page text
142
- var text = document.body ? document.body.innerText : '';
143
- stats.debug = text.substring(0, 500);
141
+ stats.debug = 'no #additional-info-container';
144
142
  return JSON.stringify(stats);
145
143
  }
146
144
 
147
145
  var table = container.querySelector('table');
148
146
  if (!table) {
149
- stats.debug = 'no table in container, text: ' + container.innerText.substring(0, 300);
147
+ stats.debug = 'no table, text: ' + container.innerText.substring(0, 300);
150
148
  return JSON.stringify(stats);
151
149
  }
152
150
 
153
- // Each row has: icon cell + text cell (e.g. "1 subscriber", "3 videos", "743 views")
154
151
  var rows = table.querySelectorAll('tr');
155
152
  var texts = [];
156
153
  rows.forEach(function(row) {
157
154
  var text = row.innerText.trim();
158
155
  texts.push(text);
159
156
 
160
- // Match patterns: "X subscriber(s)", "X video(s)", "X view(s)"
161
- // Also Vietnamese: "X người đăng ký", "X video", "X lượt xem"
162
157
  var num = 0;
163
- var numMatch = text.match(/([\\d,\\.]+)/);
164
- if (numMatch) {
165
- num = parseInt(numMatch[1].replace(/,/g, ''), 10);
166
- }
167
-
168
- // Check for K/M/B suffix
169
158
  var suffixMatch = text.match(/([\\d,\\.]+)\\s*([KMB])/i);
170
159
  if (suffixMatch) {
171
160
  var n = parseFloat(suffixMatch[1].replace(/,/g, ''));
172
- var suffix = suffixMatch[2].toUpperCase();
173
- if (suffix === 'K') num = Math.round(n * 1000);
174
- else if (suffix === 'M') num = Math.round(n * 1000000);
175
- else if (suffix === 'B') num = Math.round(n * 1000000000);
161
+ var sx = suffixMatch[2].toUpperCase();
162
+ if (sx === 'K') num = Math.round(n * 1000);
163
+ else if (sx === 'M') num = Math.round(n * 1000000);
164
+ else if (sx === 'B') num = Math.round(n * 1000000000);
165
+ } else {
166
+ var numMatch = text.match(/([\\d,]+)/);
167
+ if (numMatch) num = parseInt(numMatch[1].replace(/,/g, ''), 10);
176
168
  }
177
169
 
178
170
  if (/subscriber|đăng ký/i.test(text)) {
@@ -194,12 +186,12 @@ class StatsSyncer {
194
186
  var stats = { subscribers: 0, total_views: 0, video_count: 0 };
195
187
  try {
196
188
  var parsed = JSON.parse(result?.result?.value || '{}');
197
- console.log(`[stats] About page data: ${parsed.debug}`);
189
+ console.log('[stats] About page data:', parsed.debug);
198
190
  if (parsed.subscribers >= 0) stats.subscribers = parsed.subscribers;
199
191
  if (parsed.total_views >= 0) stats.total_views = parsed.total_views;
200
192
  if (parsed.video_count >= 0) stats.video_count = parsed.video_count;
201
193
  } catch (e) {
202
- console.error(`[stats] Parse error: ${e.message}`);
194
+ console.error('[stats] Parse error:', e.message);
203
195
  }
204
196
 
205
197
  clearTimeout(timeout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.6.1",
3
+ "version": "1.6.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": {