tubezero 1.0.0 → 1.1.0

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/dist/index.js CHANGED
@@ -1,360 +1,3 @@
1
- // src/scraper.ts
2
- async function fetchYtInitialData(url) {
3
- try {
4
- console.log(`[Scraper] Fetching HTML from ${url}`);
5
- const response = await fetch(url, { credentials: "include" });
6
- const text = await response.text();
7
- const patterns = [
8
- /var ytInitialData\s*=\s*(\{.*?\});<\/script>/,
9
- /window\["ytInitialData"\]\s*=\s*(\{.*?\});/
10
- ];
11
- for (const regex of patterns) {
12
- const match = text.match(regex);
13
- if (match && match[1]) {
14
- return JSON.parse(match[1]);
15
- }
16
- }
17
- console.warn(`[Scraper] Could not find ytInitialData in ${url}`);
18
- } catch (e) {
19
- console.error(`[Scraper] Failed to fetch data from ${url}`, e);
20
- }
21
- return null;
22
- }
23
- function getSapisidFromCookie() {
24
- if (typeof document === "undefined") return null;
25
- const match = document.cookie.match(/__Secure-3PAPISID=([^;]+)/) || document.cookie.match(/__Secure-1PAPISID=([^;]+)/) || document.cookie.match(/SAPISID=([^;]+)/);
26
- return match ? match[1] : null;
27
- }
28
- async function getSApiSidHash(sapisid, origin = "https://www.youtube.com") {
29
- if (!sapisid) return null;
30
- try {
31
- const timestamp = Math.floor(Date.now() / 1e3);
32
- const input = `${timestamp} ${sapisid} ${origin}`;
33
- const encoder = new TextEncoder();
34
- const data = encoder.encode(input);
35
- const buffer = await crypto.subtle.digest("SHA-1", data);
36
- const hash = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
37
- return `${timestamp}_${hash}`;
38
- } catch (e) {
39
- console.error("[Scraper] Failed to generate SAPISIDHASH", e);
40
- return null;
41
- }
42
- }
43
- function extractVideoEntries(data) {
44
- const entries = [];
45
- const seenTitles = /* @__PURE__ */ new Set();
46
- function recurse(obj) {
47
- if (!obj || typeof obj !== "object") return;
48
- if (Array.isArray(obj)) {
49
- for (let i = 0; i < obj.length; i++) {
50
- recurse(obj[i]);
51
- }
52
- return;
53
- }
54
- if (obj.videoId && obj.title) {
55
- let title = "";
56
- if (typeof obj.title === "string") {
57
- title = obj.title;
58
- } else if (obj.title.runs && obj.title.runs[0] && obj.title.runs[0].text) {
59
- title = obj.title.runs[0].text;
60
- } else if (obj.title.simpleText) {
61
- title = obj.title.simpleText;
62
- }
63
- title = title.trim();
64
- if (title && title.length > 2 && title !== "Skip navigation") {
65
- if (!seenTitles.has(title)) {
66
- seenTitles.add(title);
67
- let channel = "";
68
- const byline = obj.longBylineText || obj.shortBylineText || obj.ownerText;
69
- if (byline) {
70
- if (typeof byline === "string") {
71
- channel = byline;
72
- } else if (byline.runs && byline.runs[0] && byline.runs[0].text) {
73
- channel = byline.runs[0].text;
74
- } else if (byline.simpleText) {
75
- channel = byline.simpleText;
76
- }
77
- }
78
- entries.push({
79
- title,
80
- channel: channel.split("\n")[0].replace(/•/g, "").trim()
81
- });
82
- }
83
- }
84
- return;
85
- }
86
- if (obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
87
- const model = obj.lockupViewModel;
88
- const meta = model.metadata?.lockupMetadataViewModel;
89
- if (meta && meta.title && meta.title.content) {
90
- const title = meta.title.content.trim();
91
- if (title && !seenTitles.has(title)) {
92
- seenTitles.add(title);
93
- let channel = "";
94
- const rows = meta.metadata?.contentMetadataViewModel?.metadataRows;
95
- if (rows && rows[0] && rows[0].metadataParts && rows[0].metadataParts[0] && rows[0].metadataParts[0].text) {
96
- channel = rows[0].metadataParts[0].text.content || "";
97
- }
98
- entries.push({
99
- title,
100
- channel: channel.split("\n")[0].replace(/•/g, "").trim()
101
- });
102
- }
103
- }
104
- return;
105
- }
106
- for (const key in obj) {
107
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
108
- recurse(obj[key]);
109
- }
110
- }
111
- }
112
- try {
113
- recurse(data);
114
- } catch (e) {
115
- console.warn("[Scraper] Error extracting video entries recursively", e);
116
- }
117
- return entries;
118
- }
119
- function getFallbackClientVersion() {
120
- const d = /* @__PURE__ */ new Date();
121
- d.setDate(d.getDate() - 2);
122
- const yyyymmdd = d.toISOString().split("T")[0].replace(/-/g, "");
123
- return `2.${yyyymmdd}.00.00`;
124
- }
125
- var cachedApiKey = null;
126
- var cachedClientVersion = null;
127
- var cachedIdToken = null;
128
- async function getInnerTubeConfig(injectedConfig) {
129
- if (injectedConfig && injectedConfig.apiKey) {
130
- cachedApiKey = injectedConfig.apiKey;
131
- cachedClientVersion = injectedConfig.clientVersion || getFallbackClientVersion();
132
- cachedIdToken = injectedConfig.idToken ?? null;
133
- return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
134
- }
135
- if (cachedApiKey && cachedClientVersion) {
136
- return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
137
- }
138
- try {
139
- console.log("[Scraper] Fetching YouTube homepage for config...");
140
- const response = await fetch("https://www.youtube.com", { credentials: "include" });
141
- const html = await response.text();
142
- const apiKeyMatch = html.match(/"INNERTUBE_API_KEY":"([^"]+)"/);
143
- const clientVersionMatch = html.match(/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/);
144
- const idTokenMatch = html.match(/"ID_TOKEN":"([^"]+)"/);
145
- if (apiKeyMatch && apiKeyMatch[1]) cachedApiKey = apiKeyMatch[1];
146
- if (clientVersionMatch && clientVersionMatch[1]) cachedClientVersion = clientVersionMatch[1];
147
- if (idTokenMatch && idTokenMatch[1]) cachedIdToken = idTokenMatch[1];
148
- console.log(`[Scraper] Extracted Config \u2014 Key: ${cachedApiKey ? "OK" : "Failed"}, Version: ${cachedClientVersion}`);
149
- } catch (e) {
150
- console.warn("[Scraper] Failed to extract InnerTube config from HTML", e);
151
- }
152
- if (!cachedClientVersion) {
153
- cachedClientVersion = getFallbackClientVersion();
154
- }
155
- return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
156
- }
157
- function findContinuationToken(obj) {
158
- if (!obj || typeof obj !== "object") return null;
159
- if (obj.continuationCommand && obj.continuationCommand.token) {
160
- return obj.continuationCommand.token;
161
- }
162
- for (const key in obj) {
163
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
164
- const token = findContinuationToken(obj[key]);
165
- if (token) return token;
166
- }
167
- }
168
- return null;
169
- }
170
- async function fetchInnerTubeContinuation(apiKey, clientVersion, idToken, initialToken, limit) {
171
- const entries = [];
172
- let continuationToken = initialToken;
173
- try {
174
- while (continuationToken && entries.length < limit) {
175
- const headers = {
176
- "Content-Type": "application/json",
177
- "X-Youtube-Client-Name": "1",
178
- "X-Youtube-Client-Version": clientVersion
179
- };
180
- if (idToken) {
181
- headers["X-Youtube-Identity-Token"] = idToken;
182
- }
183
- const sapisid = getSapisidFromCookie();
184
- if (sapisid) {
185
- const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
186
- if (authHash) {
187
- headers["Authorization"] = `SAPISIDHASH ${authHash}`;
188
- }
189
- }
190
- const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
191
- method: "POST",
192
- headers,
193
- credentials: "include",
194
- body: JSON.stringify({
195
- context: {
196
- client: {
197
- clientName: "WEB",
198
- clientVersion,
199
- hl: "en",
200
- gl: "US"
201
- }
202
- },
203
- continuation: continuationToken
204
- })
205
- });
206
- if (!response.ok) break;
207
- const data = await response.json();
208
- const pageEntries = extractVideoEntries(data);
209
- entries.push(...pageEntries);
210
- continuationToken = findContinuationToken(data);
211
- }
212
- } catch (e) {
213
- console.error("[Scraper] Error in InnerTube pagination", e);
214
- }
215
- return entries;
216
- }
217
- async function fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit = 500) {
218
- const entries = [];
219
- try {
220
- const headers = {
221
- "Content-Type": "application/json",
222
- "X-Youtube-Client-Name": "1",
223
- "X-Youtube-Client-Version": clientVersion
224
- };
225
- if (idToken) {
226
- headers["X-Youtube-Identity-Token"] = idToken;
227
- }
228
- const sapisid = getSapisidFromCookie();
229
- if (sapisid) {
230
- const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
231
- if (authHash) {
232
- headers["Authorization"] = `SAPISIDHASH ${authHash}`;
233
- }
234
- }
235
- const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
236
- method: "POST",
237
- headers,
238
- credentials: "include",
239
- body: JSON.stringify({
240
- context: {
241
- client: {
242
- clientName: "WEB",
243
- clientVersion,
244
- hl: "en",
245
- gl: "US"
246
- }
247
- },
248
- browseId
249
- })
250
- });
251
- if (!response.ok) return [];
252
- const data = await response.json();
253
- const pageEntries = extractVideoEntries(data);
254
- entries.push(...pageEntries);
255
- if (entries.length < limit) {
256
- const continuationToken = findContinuationToken(data);
257
- if (continuationToken) {
258
- const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, continuationToken, limit - entries.length);
259
- entries.push(...more);
260
- }
261
- }
262
- } catch (e) {
263
- console.error(`[Scraper] InnerTube fetch error for ${browseId}`, e);
264
- }
265
- return entries;
266
- }
267
- async function scrapeTasteData(injectedConfig, customPlaylists = [], limit = 500) {
268
- let historyEntries = [];
269
- let likesEntries = [];
270
- let wlEntries = [];
271
- const dislikesEntries = [];
272
- const config = await getInnerTubeConfig(injectedConfig);
273
- const apiKey = config.apiKey;
274
- const clientVersion = config.clientVersion;
275
- const idToken = config.idToken;
276
- if (apiKey) {
277
- historyEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "FEhistory", limit);
278
- likesEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLLL", limit);
279
- wlEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLWL", limit);
280
- }
281
- if (historyEntries.length === 0) {
282
- const historyData = await fetchYtInitialData("https://www.youtube.com/feed/history");
283
- if (historyData) {
284
- historyEntries = extractVideoEntries(historyData);
285
- if (historyEntries.length < limit) {
286
- const token = findContinuationToken(historyData);
287
- if (token && apiKey) {
288
- const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - historyEntries.length);
289
- historyEntries.push(...more);
290
- }
291
- }
292
- }
293
- }
294
- if (likesEntries.length === 0) {
295
- const likesData = await fetchYtInitialData("https://www.youtube.com/playlist?list=LL");
296
- if (likesData) {
297
- likesEntries = extractVideoEntries(likesData);
298
- if (likesEntries.length < limit) {
299
- const token = findContinuationToken(likesData);
300
- if (token && apiKey) {
301
- const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - likesEntries.length);
302
- likesEntries.push(...more);
303
- }
304
- }
305
- }
306
- }
307
- if (wlEntries.length === 0) {
308
- const wlData = await fetchYtInitialData("https://www.youtube.com/playlist?list=WL");
309
- if (wlData) {
310
- wlEntries = extractVideoEntries(wlData);
311
- if (wlEntries.length < limit) {
312
- const token = findContinuationToken(wlData);
313
- if (token && apiKey) {
314
- const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - wlEntries.length);
315
- wlEntries.push(...more);
316
- }
317
- }
318
- }
319
- }
320
- const customPlaylistsData = [];
321
- for (const pl of customPlaylists) {
322
- if (!pl.url) continue;
323
- const match = pl.url.match(/[&?]list=([a-zA-Z0-9_-]+)/);
324
- const playlistId = match ? match[1] : pl.url.trim();
325
- if (!playlistId || !/^[a-zA-Z0-9_-]+$/.test(playlistId)) continue;
326
- const browseId = playlistId.startsWith("VL") ? playlistId : "VL" + playlistId;
327
- let entries = [];
328
- if (apiKey) {
329
- entries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit);
330
- }
331
- if (entries.length === 0) {
332
- const data = await fetchYtInitialData(`https://www.youtube.com/playlist?list=${playlistId}`);
333
- if (data) {
334
- entries = extractVideoEntries(data);
335
- if (entries.length < limit) {
336
- const token = findContinuationToken(data);
337
- if (token && apiKey) {
338
- const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - entries.length);
339
- entries.push(...more);
340
- }
341
- }
342
- }
343
- }
344
- customPlaylistsData.push({
345
- id: playlistId,
346
- entries: entries.slice(0, limit)
347
- });
348
- }
349
- return {
350
- historyEntries: historyEntries.slice(0, limit),
351
- likesEntries: likesEntries.slice(0, limit),
352
- wlEntries: wlEntries.slice(0, limit),
353
- dislikesEntries: dislikesEntries.slice(0, limit),
354
- customPlaylistsData
355
- };
356
- }
357
-
358
1
  // src/base.ts
359
2
  var Base = class {
360
3
  constructor(client) {
@@ -413,26 +56,27 @@ var Continuable = class extends Base {
413
56
  };
414
57
 
415
58
  // src/thumbnails.ts
416
- var Thumbnails = class {
417
- list;
418
- constructor(list) {
419
- this.list = list;
420
- }
421
- getBestResolution() {
422
- if (!this.list || this.list.length === 0) {
423
- return void 0;
424
- }
425
- let best = this.list[0];
426
- let maxResolution = (best.width || 0) * (best.height || 0);
427
- for (let i = 1; i < this.list.length; i++) {
428
- const thumb = this.list[i];
429
- const resolution = (thumb.width || 0) * (thumb.height || 0);
430
- if (resolution > maxResolution) {
431
- maxResolution = resolution;
432
- best = thumb;
433
- }
59
+ var Thumbnails = class extends Array {
60
+ constructor(list = []) {
61
+ if (typeof list === "number") {
62
+ super(list);
63
+ } else {
64
+ super(...list);
434
65
  }
435
- return best;
66
+ }
67
+ get min() {
68
+ return this[0]?.url;
69
+ }
70
+ get best() {
71
+ return this.reduce((prev, curr) => {
72
+ if (!prev) return curr;
73
+ return prev.width * prev.height > curr.width * curr.height ? prev : curr;
74
+ }, void 0)?.url;
75
+ }
76
+ load(thumbnails) {
77
+ this.length = 0;
78
+ this.push(...thumbnails);
79
+ return this;
436
80
  }
437
81
  };
438
82
 
@@ -456,51 +100,142 @@ var VideoCompact = class extends Base {
456
100
  let pubAt = null;
457
101
  let thumbnails = [];
458
102
  let channelObj = void 0;
459
- if (data.videoId) {
460
- videoId = data.videoId;
103
+ if (data.videoId) {
104
+ videoId = data.videoId;
105
+ titleText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
106
+ thumbnails = data.thumbnail?.thumbnails || [];
107
+ if (data.lengthText) {
108
+ const text = data.lengthText.simpleText || data.lengthText.runs?.[0]?.text || "";
109
+ durationSec = this.parseDuration(text);
110
+ }
111
+ if (data.badges?.some((b) => b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW")) {
112
+ isLive = true;
113
+ }
114
+ if (data.viewCountText) {
115
+ const text = data.viewCountText.simpleText || data.viewCountText.runs?.[0]?.text || "";
116
+ viewCountNum = this.parseViewCount(text);
117
+ }
118
+ if (data.publishedTimeText) {
119
+ pubAt = data.publishedTimeText.simpleText || data.publishedTimeText.runs?.[0]?.text || null;
120
+ }
121
+ const byline = data.shortBylineText || data.longBylineText || data.ownerText;
122
+ if (byline && byline.runs && byline.runs[0]) {
123
+ const run = byline.runs[0];
124
+ channelObj = {
125
+ id: run.navigationEndpoint?.browseEndpoint?.browseId || void 0,
126
+ name: run.text,
127
+ thumbnails: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails ? new Thumbnails(data.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails) : void 0
128
+ };
129
+ }
130
+ } else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
131
+ const model = data.lockupViewModel;
132
+ videoId = model.contentId;
133
+ const meta = model.metadata?.lockupMetadataViewModel;
134
+ titleText = meta?.title?.content || "";
135
+ const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
136
+ let cName = "";
137
+ let vCount = null;
138
+ let pAt = null;
139
+ for (const row of rows) {
140
+ for (const part of row.metadataParts || []) {
141
+ const text = part.text?.content || "";
142
+ if (text.includes("views") || text.includes("watching")) {
143
+ if (text.includes("watching")) isLive = true;
144
+ vCount = this.parseViewCount(text);
145
+ } else if (text.includes("ago")) {
146
+ pAt = text;
147
+ } else {
148
+ cName = text;
149
+ }
150
+ }
151
+ }
152
+ if (cName) {
153
+ channelObj = { name: cName.replace(/•/g, "").trim() };
154
+ }
155
+ viewCountNum = vCount;
156
+ pubAt = pAt;
157
+ }
158
+ this.id = videoId;
159
+ this.title = titleText;
160
+ this.thumbnails = new Thumbnails(thumbnails);
161
+ this.duration = durationSec;
162
+ this.isLive = isLive;
163
+ this.channel = channelObj;
164
+ this.viewCount = viewCountNum;
165
+ this.publishedAt = pubAt;
166
+ }
167
+ parseDuration(text) {
168
+ const parts = text.split(":").map(Number);
169
+ if (parts.some(isNaN)) return 0;
170
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
171
+ if (parts.length === 2) return parts[0] * 60 + parts[1];
172
+ return parts[0] || 0;
173
+ }
174
+ parseViewCount(text) {
175
+ const cleaned = text.trim().replace(/,/g, "");
176
+ const match = cleaned.match(/([\d.]+)\s*([KMB])?/i);
177
+ if (!match) return null;
178
+ const num = parseFloat(match[1]);
179
+ if (isNaN(num)) return null;
180
+ const suffix = match[2]?.toUpperCase();
181
+ if (suffix === "K") return Math.round(num * 1e3);
182
+ if (suffix === "M") return Math.round(num * 1e6);
183
+ if (suffix === "B") return Math.round(num * 1e9);
184
+ const result = Math.round(num);
185
+ return isNaN(result) ? null : result;
186
+ }
187
+ };
188
+
189
+ // src/playlist-compact.ts
190
+ var PlaylistCompact = class extends Base {
191
+ id;
192
+ title;
193
+ thumbnails;
194
+ videoCount;
195
+ channel;
196
+ constructor(client, data) {
197
+ super(client);
198
+ let playlistId = "";
199
+ let titleText = "";
200
+ let thumbnails = [];
201
+ let videoCountNum = null;
202
+ let channelObj = void 0;
203
+ if (data.playlistId) {
204
+ playlistId = data.playlistId;
461
205
  titleText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
462
- thumbnails = data.thumbnail?.thumbnails || [];
463
- if (data.lengthText) {
464
- const text = data.lengthText.simpleText || data.lengthText.runs?.[0]?.text || "";
465
- durationSec = this.parseDuration(text);
466
- }
467
- if (data.badges?.some((b) => b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW")) {
468
- isLive = true;
469
- }
470
- if (data.viewCountText) {
471
- const text = data.viewCountText.simpleText || data.viewCountText.runs?.[0]?.text || "";
472
- viewCountNum = this.parseViewCount(text);
473
- }
474
- if (data.publishedTimeText) {
475
- pubAt = data.publishedTimeText.simpleText || data.publishedTimeText.runs?.[0]?.text || null;
206
+ thumbnails = data.thumbnails?.[0]?.thumbnails || data.thumbnail?.thumbnails || [];
207
+ if (data.videoCount) {
208
+ const parsed = parseInt(data.videoCount, 10);
209
+ videoCountNum = isNaN(parsed) ? null : parsed;
210
+ } else if (data.videoCountText) {
211
+ const text = data.videoCountText.runs?.[0]?.text || data.videoCountText.simpleText || "";
212
+ const cleaned = text.replace(/[^0-9]/g, "");
213
+ if (cleaned) videoCountNum = parseInt(cleaned, 10);
476
214
  }
477
215
  const byline = data.shortBylineText || data.longBylineText || data.ownerText;
478
216
  if (byline && byline.runs && byline.runs[0]) {
479
217
  const run = byline.runs[0];
480
218
  channelObj = {
481
219
  id: run.navigationEndpoint?.browseEndpoint?.browseId || void 0,
482
- name: run.text,
483
- thumbnails: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails ? new Thumbnails(data.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails) : void 0
220
+ name: run.text
484
221
  };
485
222
  }
486
- } else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
223
+ } else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_PLAYLIST") {
487
224
  const model = data.lockupViewModel;
488
- videoId = model.contentId;
225
+ playlistId = model.contentId;
489
226
  const meta = model.metadata?.lockupMetadataViewModel;
490
227
  titleText = meta?.title?.content || "";
228
+ const img = model.image?.lockupImageViewModel?.image;
229
+ if (img && img.sources) thumbnails = img.sources;
491
230
  const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
492
231
  let cName = "";
493
- let vCount = null;
494
- let pAt = null;
495
232
  for (const row of rows) {
496
233
  for (const part of row.metadataParts || []) {
497
234
  const text = part.text?.content || "";
498
- if (text.includes("views") || text.includes("watching")) {
499
- if (text.includes("watching")) isLive = true;
500
- vCount = this.parseViewCount(text);
501
- } else if (text.includes("ago")) {
502
- pAt = text;
503
- } else {
235
+ if (text.includes("video")) {
236
+ const cleaned = text.replace(/[^0-9]/g, "");
237
+ if (cleaned) videoCountNum = parseInt(cleaned, 10);
238
+ } else if (!cName) {
504
239
  cName = text;
505
240
  }
506
241
  }
@@ -508,206 +243,761 @@ var VideoCompact = class extends Base {
508
243
  if (cName) {
509
244
  channelObj = { name: cName.replace(/•/g, "").trim() };
510
245
  }
511
- viewCountNum = vCount;
512
- pubAt = pAt;
513
246
  }
514
- this.id = videoId;
247
+ this.id = playlistId;
515
248
  this.title = titleText;
516
249
  this.thumbnails = new Thumbnails(thumbnails);
517
- this.duration = durationSec;
518
- this.isLive = isLive;
250
+ this.videoCount = videoCountNum;
519
251
  this.channel = channelObj;
520
- this.viewCount = viewCountNum;
521
- this.publishedAt = pubAt;
522
252
  }
523
- parseDuration(text) {
524
- const parts = text.split(":").map(Number);
525
- if (parts.some(isNaN)) return 0;
526
- if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
527
- if (parts.length === 2) return parts[0] * 60 + parts[1];
528
- return parts[0] || 0;
253
+ };
254
+
255
+ // src/channel-compact.ts
256
+ var ChannelCompact = class extends Base {
257
+ id;
258
+ name;
259
+ thumbnails;
260
+ subscriberCount;
261
+ constructor(client, data) {
262
+ super(client);
263
+ let channelId = "";
264
+ let nameText = "";
265
+ let thumbnails = [];
266
+ let subCount = null;
267
+ if (data.channelId) {
268
+ channelId = data.channelId;
269
+ nameText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
270
+ thumbnails = data.thumbnail?.thumbnails || [];
271
+ if (data.subscriberCountText) {
272
+ subCount = data.subscriberCountText.simpleText || data.subscriberCountText.runs?.[0]?.text || null;
273
+ } else if (data.videoSubscriberCountText) {
274
+ subCount = data.videoSubscriberCountText.simpleText || data.videoSubscriberCountText.runs?.[0]?.text || null;
275
+ }
276
+ }
277
+ this.id = channelId;
278
+ this.name = nameText;
279
+ this.thumbnails = new Thumbnails(thumbnails);
280
+ this.subscriberCount = subCount;
281
+ }
282
+ };
283
+
284
+ // src/search-result.ts
285
+ var SearchResult = class _SearchResult extends Continuable {
286
+ constructor(client, initialData) {
287
+ super(client);
288
+ const { items, continuation } = _SearchResult.parseData(client, initialData);
289
+ this.items = items;
290
+ this.continuation = continuation;
291
+ }
292
+ async fetch() {
293
+ if (!this.continuation) {
294
+ return { items: [], continuation: null };
295
+ }
296
+ const data = await this.client.request("search", {
297
+ continuation: this.continuation
298
+ });
299
+ return _SearchResult.parseData(this.client, data);
300
+ }
301
+ static parseData(client, data) {
302
+ const items = [];
303
+ let continuation = null;
304
+ function traverse(obj) {
305
+ if (!obj || typeof obj !== "object") return;
306
+ if (Array.isArray(obj)) {
307
+ for (const item of obj) traverse(item);
308
+ return;
309
+ }
310
+ if (obj.videoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
311
+ items.push(new VideoCompact(client, obj.videoRenderer || obj));
312
+ return;
313
+ }
314
+ if (obj.playlistRenderer) {
315
+ items.push(new PlaylistCompact(client, obj.playlistRenderer));
316
+ return;
317
+ }
318
+ if (obj.channelRenderer) {
319
+ items.push(new ChannelCompact(client, obj.channelRenderer));
320
+ return;
321
+ }
322
+ if (obj.continuationCommand && obj.continuationCommand.token) {
323
+ continuation = obj.continuationCommand.token;
324
+ } else if (obj.continuationItemRenderer?.continuationEndpoint) {
325
+ continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
326
+ }
327
+ for (const key of Object.keys(obj)) {
328
+ traverse(obj[key]);
329
+ }
330
+ }
331
+ traverse(data);
332
+ return { items, continuation };
333
+ }
334
+ };
335
+
336
+ // src/base-channel.ts
337
+ var TAB_PARAMS = {
338
+ videos: "EgZ2aWRlb3PyBgQKAjoA",
339
+ shorts: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D",
340
+ live: "EgdzdHJlYW1z8gYECgJ6AA%3D%3D",
341
+ playlists: "EglwbGF5bGlzdHPyBgQKAkIA",
342
+ posts: "EgVwb3N0c_IGBAoCSgA%3D"
343
+ };
344
+ var ChannelVideos = class extends Continuable {
345
+ constructor(client, channel) {
346
+ super(client);
347
+ this.channel = channel;
348
+ }
349
+ channel;
350
+ async fetch() {
351
+ const data = await this.client.request("browse", {
352
+ browseId: this.channel.id,
353
+ params: TAB_PARAMS.videos,
354
+ continuation: this.continuation || void 0
355
+ });
356
+ return parseTabItems(this.client, data, "videos");
357
+ }
358
+ };
359
+ var ChannelShorts = class extends Continuable {
360
+ constructor(client, channel) {
361
+ super(client);
362
+ this.channel = channel;
363
+ }
364
+ channel;
365
+ async fetch() {
366
+ const data = await this.client.request("browse", {
367
+ browseId: this.channel.id,
368
+ params: TAB_PARAMS.shorts,
369
+ continuation: this.continuation || void 0
370
+ });
371
+ return parseTabItems(this.client, data, "shorts");
372
+ }
373
+ };
374
+ var ChannelLive = class extends Continuable {
375
+ constructor(client, channel) {
376
+ super(client);
377
+ this.channel = channel;
378
+ }
379
+ channel;
380
+ async fetch() {
381
+ const data = await this.client.request("browse", {
382
+ browseId: this.channel.id,
383
+ params: TAB_PARAMS.live,
384
+ continuation: this.continuation || void 0
385
+ });
386
+ return parseTabItems(this.client, data, "live");
387
+ }
388
+ };
389
+ var ChannelPlaylists = class extends Continuable {
390
+ constructor(client, channel) {
391
+ super(client);
392
+ this.channel = channel;
393
+ }
394
+ channel;
395
+ async fetch() {
396
+ const data = await this.client.request("browse", {
397
+ browseId: this.channel.id,
398
+ params: TAB_PARAMS.playlists,
399
+ continuation: this.continuation || void 0
400
+ });
401
+ return parseTabItems(this.client, data, "playlists");
402
+ }
403
+ };
404
+ var ChannelPosts = class extends Continuable {
405
+ constructor(client, channel) {
406
+ super(client);
407
+ this.channel = channel;
408
+ }
409
+ channel;
410
+ async fetch() {
411
+ const data = await this.client.request("browse", {
412
+ browseId: this.channel.id,
413
+ params: TAB_PARAMS.posts,
414
+ continuation: this.continuation || void 0
415
+ });
416
+ return parseTabItems(this.client, data, "posts");
417
+ }
418
+ };
419
+ var BaseChannel = class extends Base {
420
+ id;
421
+ name;
422
+ handle;
423
+ description;
424
+ thumbnails;
425
+ subscriberCount;
426
+ videos;
427
+ shorts;
428
+ live;
429
+ playlists;
430
+ posts;
431
+ constructor(client, data) {
432
+ super(client);
433
+ this.id = "";
434
+ this.name = "";
435
+ this.videos = new ChannelVideos(client, this);
436
+ this.shorts = new ChannelShorts(client, this);
437
+ this.live = new ChannelLive(client, this);
438
+ this.playlists = new ChannelPlaylists(client, this);
439
+ this.posts = new ChannelPosts(client, this);
440
+ if (data) {
441
+ this.load(data);
442
+ }
443
+ }
444
+ get url() {
445
+ return `https://www.youtube.com/channel/${this.id}`;
446
+ }
447
+ load(data) {
448
+ const headerObj = data.header?.c4TabbedHeaderRenderer || data.header?.pageHeaderRenderer || data.c4TabbedHeaderRenderer || data;
449
+ if (headerObj.pageTitle) {
450
+ this.name = headerObj.pageTitle;
451
+ }
452
+ const viewModel = headerObj.content?.pageHeaderViewModel;
453
+ if (viewModel) {
454
+ this.name = viewModel.title?.dynamicTextViewModel?.text?.content || this.name;
455
+ const thumbs = viewModel.image?.decoratedAvatarViewModel?.avatar?.avatarViewModel?.image?.sources || [];
456
+ this.thumbnails = new Thumbnails(thumbs);
457
+ const rows = viewModel.metadata?.contentMetadataViewModel?.metadataRows || [];
458
+ for (const row of rows) {
459
+ for (const part of row.metadataParts || []) {
460
+ const txt = part.text?.content || "";
461
+ if (txt.includes("subscriber")) {
462
+ this.subscriberCount = txt;
463
+ }
464
+ }
465
+ }
466
+ }
467
+ if (headerObj.channelId) {
468
+ this.id = headerObj.channelId;
469
+ }
470
+ if (headerObj.title) {
471
+ this.name = headerObj.title.simpleText || headerObj.title.runs?.[0]?.text || this.name;
472
+ }
473
+ if (headerObj.subscriberCountText) {
474
+ this.subscriberCount = headerObj.subscriberCountText.simpleText || headerObj.subscriberCountText.runs?.[0]?.text || this.subscriberCount;
475
+ }
476
+ if (headerObj.thumbnail) {
477
+ const thumbs = headerObj.thumbnail.thumbnails || [];
478
+ this.thumbnails = new Thumbnails(thumbs);
479
+ }
480
+ this.id = this.id || data.id || data.channelId || "";
481
+ this.name = this.name || data.name || "";
482
+ if (!this.thumbnails || this.thumbnails.length === 0) {
483
+ const thumbs = data.thumbnails || data.thumbnail?.thumbnails || [];
484
+ this.thumbnails = new Thumbnails(thumbs);
485
+ }
486
+ if (!this.id) {
487
+ const serviceParams = data.responseContext?.serviceTrackingParams || [];
488
+ for (const sp of serviceParams) {
489
+ const browseIdParam = sp.params?.find((p) => p.key === "browse_id");
490
+ if (browseIdParam) {
491
+ this.id = browseIdParam.value;
492
+ break;
493
+ }
494
+ }
495
+ }
496
+ return this;
497
+ }
498
+ };
499
+ function parseTabItems(client, data, tabName) {
500
+ const items = [];
501
+ let continuation = null;
502
+ function traverse(obj) {
503
+ if (!obj || typeof obj !== "object") return;
504
+ if (Array.isArray(obj)) {
505
+ for (const item of obj) traverse(item);
506
+ return;
507
+ }
508
+ if (obj.videoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
509
+ if (tabName !== "playlists") {
510
+ items.push(new VideoCompact(client, obj.videoRenderer || obj));
511
+ }
512
+ return;
513
+ }
514
+ if (obj.playlistRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_PLAYLIST") {
515
+ if (tabName === "playlists") {
516
+ items.push(new PlaylistCompact(client, obj.playlistRenderer || obj));
517
+ }
518
+ return;
519
+ }
520
+ if (obj.backstagePostRenderer || obj.sharedPostRenderer) {
521
+ if (tabName === "posts") {
522
+ items.push(obj);
523
+ }
524
+ return;
525
+ }
526
+ if (obj.continuationCommand && obj.continuationCommand.token) {
527
+ continuation = obj.continuationCommand.token;
528
+ } else if (obj.continuationItemRenderer?.continuationEndpoint) {
529
+ continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
530
+ }
531
+ for (const key of Object.keys(obj)) {
532
+ traverse(obj[key]);
533
+ }
534
+ }
535
+ traverse(data);
536
+ return { items, continuation };
537
+ }
538
+
539
+ // src/video-related.ts
540
+ var VideoRelated = class extends Continuable {
541
+ constructor(client, video) {
542
+ super(client);
543
+ this.video = video;
544
+ }
545
+ video;
546
+ async fetch() {
547
+ if (!this.continuation) {
548
+ return { items: [], continuation: null };
549
+ }
550
+ const data = await this.client.request("next", {
551
+ continuation: this.continuation
552
+ });
553
+ return parseRelatedData(this.client, data);
554
+ }
555
+ };
556
+ function parseRelatedData(client, data) {
557
+ const items = [];
558
+ let continuation = null;
559
+ function traverse(obj) {
560
+ if (!obj || typeof obj !== "object") return;
561
+ if (Array.isArray(obj)) {
562
+ for (const item of obj) traverse(item);
563
+ return;
564
+ }
565
+ if (obj.compactVideoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
566
+ items.push(new VideoCompact(client, obj.compactVideoRenderer || obj));
567
+ return;
568
+ }
569
+ if (obj.compactRadioRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_PLAYLIST") {
570
+ items.push(new PlaylistCompact(client, obj.compactRadioRenderer || obj));
571
+ return;
572
+ }
573
+ if (obj.continuationCommand && obj.continuationCommand.token) {
574
+ continuation = obj.continuationCommand.token;
575
+ } else if (obj.continuationItemRenderer?.continuationEndpoint) {
576
+ continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
577
+ }
578
+ for (const key of Object.keys(obj)) {
579
+ traverse(obj[key]);
580
+ }
581
+ }
582
+ const secondaryContents = data.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results || data.onResponseReceivedEndpoints?.[0]?.appendContinuationItemsAction?.continuationItems || [];
583
+ if (secondaryContents.length > 0) {
584
+ traverse(secondaryContents);
585
+ } else {
586
+ traverse(data);
587
+ }
588
+ return { items, continuation };
589
+ }
590
+
591
+ // src/caption.ts
592
+ var CaptionLanguage = class {
593
+ captions;
594
+ name;
595
+ code;
596
+ isTranslatable;
597
+ url;
598
+ constructor(attr) {
599
+ this.captions = attr.captions;
600
+ this.name = attr.name;
601
+ this.code = attr.code;
602
+ this.isTranslatable = attr.isTranslatable;
603
+ this.url = attr.url;
604
+ }
605
+ async get(translationLanguageCode) {
606
+ return this.captions.get(this.code, translationLanguageCode);
607
+ }
608
+ };
609
+ var Caption = class {
610
+ duration;
611
+ start;
612
+ text;
613
+ segments;
614
+ constructor(attr) {
615
+ Object.assign(this, attr);
616
+ }
617
+ get end() {
618
+ return this.start + this.duration;
619
+ }
620
+ };
621
+
622
+ // src/video-captions.ts
623
+ var VideoCaptions = class extends Base {
624
+ video;
625
+ languages = [];
626
+ constructor(attr) {
627
+ super(attr.client);
628
+ this.video = attr.video;
629
+ }
630
+ load(data) {
631
+ const captionTracks = data?.captionTracks || [];
632
+ this.languages = captionTracks.map((track) => new CaptionLanguage({
633
+ captions: this,
634
+ name: track.name?.simpleText || track.name?.runs?.[0]?.text || "",
635
+ code: track.languageCode,
636
+ isTranslatable: !!track.isTranslatable,
637
+ url: track.baseUrl
638
+ }));
639
+ return this;
529
640
  }
530
- parseViewCount(text) {
531
- const cleaned = text.trim().replace(/,/g, "");
532
- const match = cleaned.match(/([\d.]+)\s*([KMB])?/i);
533
- if (!match) return null;
534
- const num = parseFloat(match[1]);
535
- if (isNaN(num)) return null;
536
- const suffix = match[2]?.toUpperCase();
537
- if (suffix === "K") return Math.round(num * 1e3);
538
- if (suffix === "M") return Math.round(num * 1e6);
539
- if (suffix === "B") return Math.round(num * 1e9);
540
- const result = Math.round(num);
541
- return isNaN(result) ? null : result;
641
+ async get(languageCode, translationLanguageCode) {
642
+ if (!languageCode) {
643
+ languageCode = "en";
644
+ }
645
+ const lang = this.languages.find((l) => l.code.toUpperCase() === languageCode?.toUpperCase());
646
+ const url = lang?.url;
647
+ if (!url) return void 0;
648
+ let fetchUrl = `${url}&fmt=json3`;
649
+ if (translationLanguageCode) {
650
+ fetchUrl += `&tlang=${translationLanguageCode}`;
651
+ }
652
+ const response = await this.client.fetch(fetchUrl);
653
+ if (!response.ok) {
654
+ throw new Error(`Failed to fetch caption: ${response.status}`);
655
+ }
656
+ const json = await response.json();
657
+ const events = json.events || [];
658
+ const captions = [];
659
+ for (const e of events) {
660
+ if (e.segs === void 0) continue;
661
+ captions.push(new Caption({
662
+ duration: e.dDurationMs || 0,
663
+ start: e.tStartMs || 0,
664
+ text: e.segs.map((s) => s.utf8).join(""),
665
+ segments: e.segs
666
+ }));
667
+ }
668
+ return captions;
542
669
  }
543
670
  };
544
671
 
545
- // src/playlist-compact.ts
546
- var PlaylistCompact = class extends Base {
672
+ // src/base-video.ts
673
+ var BaseVideo = class extends Base {
547
674
  id;
548
675
  title;
676
+ description;
549
677
  thumbnails;
550
- videoCount;
678
+ viewCount;
679
+ uploadDate;
551
680
  channel;
681
+ channels;
682
+ isLiveContent;
683
+ likeCount;
684
+ tags;
685
+ formats;
686
+ adaptiveFormats;
687
+ streamingData;
688
+ related;
689
+ captions;
552
690
  constructor(client, data) {
553
691
  super(client);
554
- let playlistId = "";
555
- let titleText = "";
556
- let thumbnails = [];
557
- let videoCountNum = null;
558
- let channelObj = void 0;
559
- if (data.playlistId) {
560
- playlistId = data.playlistId;
561
- titleText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
562
- thumbnails = data.thumbnails?.[0]?.thumbnails || data.thumbnail?.thumbnails || [];
563
- if (data.videoCount) {
564
- const parsed = parseInt(data.videoCount, 10);
565
- videoCountNum = isNaN(parsed) ? null : parsed;
566
- } else if (data.videoCountText) {
567
- const text = data.videoCountText.runs?.[0]?.text || data.videoCountText.simpleText || "";
568
- const cleaned = text.replace(/[^0-9]/g, "");
569
- if (cleaned) videoCountNum = parseInt(cleaned, 10);
570
- }
571
- const byline = data.shortBylineText || data.longBylineText || data.ownerText;
572
- if (byline && byline.runs && byline.runs[0]) {
573
- const run = byline.runs[0];
574
- channelObj = {
575
- id: run.navigationEndpoint?.browseEndpoint?.browseId || void 0,
576
- name: run.text
577
- };
578
- }
692
+ this.id = "";
693
+ this.title = "";
694
+ this.description = "";
695
+ this.thumbnails = new Thumbnails([]);
696
+ this.viewCount = null;
697
+ this.uploadDate = null;
698
+ this.channel = null;
699
+ this.channels = null;
700
+ this.isLiveContent = false;
701
+ this.likeCount = null;
702
+ this.tags = [];
703
+ this.formats = [];
704
+ this.adaptiveFormats = [];
705
+ this.related = new VideoRelated(client, this);
706
+ this.captions = null;
707
+ if (data) {
708
+ this.parse(data);
579
709
  }
580
- this.id = playlistId;
581
- this.title = titleText;
582
- this.thumbnails = new Thumbnails(thumbnails);
583
- this.videoCount = videoCountNum;
584
- this.channel = channelObj;
585
710
  }
586
- };
587
-
588
- // src/channel-compact.ts
589
- var ChannelCompact = class extends Base {
590
- id;
591
- name;
592
- thumbnails;
593
- subscriberCount;
594
- constructor(client, data) {
595
- super(client);
596
- let channelId = "";
597
- let nameText = "";
598
- let thumbnails = [];
599
- let subCount = null;
600
- if (data.channelId) {
601
- channelId = data.channelId;
602
- nameText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
603
- thumbnails = data.thumbnail?.thumbnails || [];
604
- if (data.subscriberCountText) {
605
- subCount = data.subscriberCountText.simpleText || data.subscriberCountText.runs?.[0]?.text || null;
606
- } else if (data.videoSubscriberCountText) {
607
- subCount = data.videoSubscriberCountText.simpleText || data.videoSubscriberCountText.runs?.[0]?.text || null;
711
+ parse(data) {
712
+ const videoDetails = data.videoDetails || {};
713
+ const playerResponse = data.playerResponse || data;
714
+ const watchResponse = data.response || {};
715
+ this.id = videoDetails.videoId || playerResponse.videoDetails?.videoId || "";
716
+ this.title = videoDetails.title || playerResponse.videoDetails?.title || "";
717
+ this.description = videoDetails.shortDescription || playerResponse.videoDetails?.shortDescription || "";
718
+ const thumbs = videoDetails.thumbnail?.thumbnails || playerResponse.videoDetails?.thumbnail?.thumbnails || [];
719
+ this.thumbnails = new Thumbnails(thumbs);
720
+ this.viewCount = videoDetails.viewCount ? parseInt(videoDetails.viewCount, 10) : playerResponse.videoDetails?.viewCount ? parseInt(playerResponse.videoDetails.viewCount, 10) : null;
721
+ this.isLiveContent = videoDetails.isLiveContent || playerResponse.videoDetails?.isLiveContent || false;
722
+ const authorId = videoDetails.channelId || playerResponse.videoDetails?.channelId || "";
723
+ const authorName = videoDetails.author || playerResponse.videoDetails?.author || "";
724
+ if (authorId || authorName) {
725
+ this.channel = new BaseChannel(this.client, {
726
+ channelId: authorId,
727
+ title: { simpleText: authorName }
728
+ });
729
+ }
730
+ const watchNextResults = watchResponse.contents?.twoColumnWatchNextResults || data.contents?.twoColumnWatchNextResults;
731
+ const primaryInfo = watchNextResults?.results?.results?.contents?.find((c) => c.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer;
732
+ if (primaryInfo) {
733
+ this.uploadDate = primaryInfo.dateText?.simpleText || primaryInfo.dateText?.runs?.[0]?.text || null;
734
+ const topLevelButtons = primaryInfo.videoActions?.menuRenderer?.topLevelButtons || [];
735
+ let likeButtonText = "";
736
+ for (const button of topLevelButtons) {
737
+ const renderer = button.toggleButtonRenderer || button.buttonRenderer;
738
+ if (renderer) {
739
+ const label = renderer.defaultText?.accessibility?.accessibilityData?.accessibilityData || renderer.accessibilityData?.accessibilityData?.label;
740
+ if (label && label.toLowerCase().includes("like")) {
741
+ likeButtonText = label;
742
+ break;
743
+ }
744
+ }
745
+ }
746
+ if (likeButtonText) {
747
+ this.likeCount = parseInt(likeButtonText.replace(/[^0-9]/g, "") || "0", 10) || null;
608
748
  }
749
+ const runs = primaryInfo.superTitleLink?.runs || [];
750
+ this.tags = runs.map((r) => r.text.trim()).filter((t) => t.startsWith("#"));
751
+ }
752
+ const streamingData = data.streamingData || playerResponse.streamingData;
753
+ if (streamingData) {
754
+ this.formats = streamingData.formats || [];
755
+ this.adaptiveFormats = streamingData.adaptiveFormats || [];
756
+ const parseFormat = (f) => ({
757
+ itag: f.itag,
758
+ url: f.url,
759
+ mimeType: f.mimeType,
760
+ bitrate: f.bitrate,
761
+ width: f.width,
762
+ height: f.height,
763
+ hasVideo: !!f.width || f.mimeType?.includes("video/"),
764
+ hasAudio: !!f.audioBitrate || !!f.audioChannels || f.mimeType?.includes("audio/") || f.mimeType?.includes("video/") && (f.mimeType?.includes("mp4a") || f.mimeType?.includes("opus") || f.mimeType?.includes("vorbis") || f.mimeType?.includes("ec-3")),
765
+ isLive: this.isLiveContent,
766
+ contentLength: f.contentLength,
767
+ quality: f.quality,
768
+ qualityLabel: f.qualityLabel,
769
+ audioQuality: f.audioQuality,
770
+ approxDurationMs: f.approxDurationMs
771
+ });
772
+ this.streamingData = {
773
+ expiresInSeconds: streamingData.expiresInSeconds || "0",
774
+ formats: this.formats.map(parseFormat),
775
+ adaptiveFormats: this.adaptiveFormats.map(parseFormat)
776
+ };
777
+ }
778
+ const relatedRes = parseRelatedData(this.client, data);
779
+ this.related.items = relatedRes.items;
780
+ this.related.continuation = relatedRes.continuation;
781
+ const captionTracks = playerResponse.captions?.playerCaptionsTracklistRenderer;
782
+ if (captionTracks) {
783
+ this.captions = new VideoCaptions({ video: this, client: this.client }).load(captionTracks);
609
784
  }
610
- this.id = channelId;
611
- this.name = nameText;
612
- this.thumbnails = new Thumbnails(thumbnails);
613
- this.subscriberCount = subCount;
785
+ }
786
+ get upNext() {
787
+ return this.related.items[0] || null;
614
788
  }
615
789
  };
616
790
 
617
- // src/search-result.ts
618
- var SearchResult = class _SearchResult extends Continuable {
619
- constructor(client, initialData) {
791
+ // src/comment.ts
792
+ var CommentReplies = class extends Continuable {
793
+ constructor(client, comment) {
620
794
  super(client);
621
- const { items, continuation } = _SearchResult.parseData(client, initialData);
622
- this.items = items;
623
- this.continuation = continuation;
795
+ this.comment = comment;
624
796
  }
797
+ comment;
625
798
  async fetch() {
626
799
  if (!this.continuation) {
627
800
  return { items: [], continuation: null };
628
801
  }
629
- const data = await this.client.request("search", {
802
+ const data = await this.client.request("next", {
630
803
  continuation: this.continuation
631
804
  });
632
- return _SearchResult.parseData(this.client, data);
633
- }
634
- static parseData(client, data) {
635
805
  const items = [];
636
- let continuation = null;
806
+ let nextContinuation = null;
807
+ const mutations = data.frameworkUpdates?.entityBatchUpdate?.mutations || [];
808
+ for (const mutation of mutations) {
809
+ const payload = mutation.payload?.commentEntityPayload;
810
+ if (payload) {
811
+ items.push(new Comment(this.client, { payload, video: this.comment.video }));
812
+ }
813
+ }
637
814
  function traverse(obj) {
638
815
  if (!obj || typeof obj !== "object") return;
639
- if (Array.isArray(obj)) {
640
- for (const item of obj) traverse(item);
816
+ if (obj.continuationCommand && obj.continuationCommand.token) {
817
+ nextContinuation = obj.continuationCommand.token;
641
818
  return;
642
- }
643
- if (obj.videoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
644
- items.push(new VideoCompact(client, obj.videoRenderer || obj));
819
+ } else if (obj.continuationItemRenderer?.continuationEndpoint) {
820
+ nextContinuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
645
821
  return;
646
822
  }
647
- if (obj.playlistRenderer) {
648
- items.push(new PlaylistCompact(client, obj.playlistRenderer));
649
- return;
823
+ for (const key of Object.keys(obj)) {
824
+ traverse(obj[key]);
650
825
  }
651
- if (obj.channelRenderer) {
652
- items.push(new ChannelCompact(client, obj.channelRenderer));
653
- return;
826
+ }
827
+ traverse(data);
828
+ return { items, continuation: nextContinuation };
829
+ }
830
+ };
831
+ var VideoComments = class extends Continuable {
832
+ constructor(client, video) {
833
+ super(client);
834
+ this.video = video;
835
+ }
836
+ video;
837
+ async fetch() {
838
+ if (!this.continuation) {
839
+ return { items: [], continuation: null };
840
+ }
841
+ const data = await this.client.request("next", {
842
+ continuation: this.continuation
843
+ });
844
+ const items = [];
845
+ let nextContinuation = null;
846
+ const mutations = data.frameworkUpdates?.entityBatchUpdate?.mutations || [];
847
+ for (const mutation of mutations) {
848
+ const payload = mutation.payload?.commentEntityPayload;
849
+ if (payload) {
850
+ items.push(new Comment(this.client, { payload, video: this.video }));
654
851
  }
852
+ }
853
+ function traverse(obj) {
854
+ if (!obj || typeof obj !== "object") return;
655
855
  if (obj.continuationCommand && obj.continuationCommand.token) {
656
- continuation = obj.continuationCommand.token;
856
+ nextContinuation = obj.continuationCommand.token;
857
+ return;
657
858
  } else if (obj.continuationItemRenderer?.continuationEndpoint) {
658
- continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
859
+ nextContinuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
860
+ return;
659
861
  }
660
862
  for (const key of Object.keys(obj)) {
661
863
  traverse(obj[key]);
662
864
  }
663
865
  }
664
866
  traverse(data);
665
- return { items, continuation };
867
+ return { items, continuation: nextContinuation };
666
868
  }
667
869
  };
668
-
669
- // src/base-video.ts
670
- var BaseVideo = class extends Base {
870
+ var Comment = class extends Base {
671
871
  id;
672
- title;
673
- description;
674
- thumbnails;
675
- viewCount;
872
+ video;
873
+ author;
874
+ content;
676
875
  publishDate;
677
- channel;
678
- isLive;
679
- constructor(client, data) {
876
+ likeCount;
877
+ isAuthorChannelOwner;
878
+ isPinned;
879
+ replyCount;
880
+ replies;
881
+ constructor(client, options) {
680
882
  super(client);
883
+ this.video = options.video;
681
884
  this.id = "";
682
- this.title = "";
683
- this.description = "";
684
- this.thumbnails = new Thumbnails([]);
685
- this.viewCount = null;
686
- this.publishDate = null;
687
- this.isLive = false;
688
- this.parse(data);
885
+ this.content = "";
886
+ this.publishDate = "";
887
+ this.likeCount = 0;
888
+ this.isAuthorChannelOwner = false;
889
+ this.isPinned = false;
890
+ this.replyCount = 0;
891
+ this.author = new BaseChannel(client);
892
+ this.replies = new CommentReplies(client, this);
893
+ this.load(options.payload);
689
894
  }
690
- parse(data) {
691
- const videoDetails = data.videoDetails || data.microformat?.playerMicroformatRenderer || {};
692
- this.id = videoDetails.videoId || "";
693
- this.title = typeof videoDetails.title === "string" ? videoDetails.title : videoDetails.title?.simpleText || videoDetails.title?.runs?.[0]?.text || "";
694
- this.description = videoDetails.shortDescription || videoDetails.description?.simpleText || videoDetails.description?.runs?.map((r) => r.text).join("") || "";
695
- this.thumbnails = new Thumbnails(videoDetails.thumbnail?.thumbnails || []);
696
- this.viewCount = videoDetails.viewCount ? parseInt(videoDetails.viewCount, 10) : null;
697
- this.isLive = videoDetails.isLiveContent || false;
698
- this.publishDate = videoDetails.publishDate || null;
699
- this.channel = {
700
- name: videoDetails.author || videoDetails.ownerChannelName || "",
701
- id: videoDetails.channelId || videoDetails.externalChannelId || ""
702
- };
895
+ load(payload) {
896
+ const props = payload.properties || {};
897
+ const toolbar = payload.toolbar || {};
898
+ const authorInfo = payload.author || {};
899
+ const avatar = payload.avatar || {};
900
+ this.id = props.commentId || payload.commentId || "";
901
+ this.content = props.content?.content || payload.contentText?.simpleText || payload.contentText?.runs?.map((r) => r.text).join("") || "";
902
+ this.publishDate = props.publishedTime || payload.publishedTimeText?.simpleText || "";
903
+ this.isAuthorChannelOwner = !!authorInfo.isCreator || !!payload.authorIsCreator;
904
+ this.isPinned = !!payload.pinned;
905
+ const likeStr = toolbar.likeCountLiked || toolbar.likeCountNotliked || payload.voteCount?.simpleText || "0";
906
+ this.likeCount = parseInt(String(likeStr).replace(/[^0-9]/g, "") || "0", 10);
907
+ const replyStr = toolbar.replyCount || payload.replyCount || "0";
908
+ this.replyCount = parseInt(String(replyStr).replace(/[^0-9]/g, "") || "0", 10);
909
+ const authorId = authorInfo.id || payload.authorEndpoint?.browseEndpoint?.browseId || "";
910
+ const authorName = authorInfo.displayName || payload.authorText?.simpleText || payload.authorText?.runs?.[0]?.text || "";
911
+ const thumbs = avatar.image?.sources || payload.authorThumbnail?.thumbnails || [];
912
+ this.author = new BaseChannel(this.client, {
913
+ channelId: authorId,
914
+ title: { simpleText: authorName },
915
+ thumbnail: { thumbnails: thumbs }
916
+ });
917
+ return this;
918
+ }
919
+ get url() {
920
+ return `https://www.youtube.com/watch?v=${this.video.id}&lc=${this.id}`;
703
921
  }
704
922
  };
705
923
 
706
924
  // src/video.ts
707
925
  var Video = class extends BaseVideo {
708
- // We will expand this with related videos, comments pagination, and chapters in Sprints 3 & 4.
926
+ duration;
927
+ chapters;
928
+ comments;
929
+ music;
709
930
  constructor(client, data) {
710
- super(client, data);
931
+ super(client);
932
+ this.duration = 0;
933
+ this.chapters = [];
934
+ this.music = null;
935
+ this.comments = new VideoComments(client, this);
936
+ this.load(data);
937
+ }
938
+ load(data) {
939
+ super.parse(data);
940
+ const videoDetails = data.videoDetails || data.playerResponse?.videoDetails || {};
941
+ this.duration = parseInt(videoDetails.lengthSeconds || "0", 10);
942
+ this.chapters = [];
943
+ const markersMap = data.response?.playerOverlays?.playerOverlayRenderer?.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer?.playerBar?.multiMarkersPlayerBarRenderer?.markersMap;
944
+ const chaptersList = markersMap?.[0]?.value?.chapters || [];
945
+ for (const chap of chaptersList) {
946
+ const renderer = chap.chapterRenderer;
947
+ if (renderer) {
948
+ this.chapters.push({
949
+ title: renderer.title?.simpleText || renderer.title?.runs?.[0]?.text || "",
950
+ start: renderer.timeRangeStartMillis || 0,
951
+ thumbnails: new Thumbnails(renderer.thumbnail?.thumbnails || [])
952
+ });
953
+ }
954
+ }
955
+ this.music = null;
956
+ const panels = data.response?.engagementPanels || [];
957
+ const structuredPanel = panels.find((p) => p.engagementPanelSectionListRenderer?.content?.structuredDescriptionContentRenderer);
958
+ const descriptionItems = structuredPanel?.engagementPanelSectionListRenderer?.content?.structuredDescriptionContentRenderer?.items || [];
959
+ const musicItem = descriptionItems.find((i) => i.horizontalCardListRenderer?.footerButton?.buttonViewModel?.iconName === "MUSIC" || i.horizontalCardListRenderer?.cards?.some((c) => c.videoAttributeViewModel));
960
+ if (musicItem) {
961
+ const card = musicItem.horizontalCardListRenderer?.cards?.find((c) => c.videoAttributeViewModel)?.videoAttributeViewModel;
962
+ if (card) {
963
+ this.music = {
964
+ imageUrl: card.image?.sources?.[0]?.url || "",
965
+ title: card.title || "",
966
+ artist: card.subtitle || "",
967
+ album: card.secondarySubtitle?.content || null
968
+ };
969
+ }
970
+ }
971
+ const watchNextResults = data.response?.contents?.twoColumnWatchNextResults || data.contents?.twoColumnWatchNextResults;
972
+ const contents = watchNextResults?.results?.results?.contents || [];
973
+ let commentToken = null;
974
+ for (const content of contents) {
975
+ const itemSection = content.itemSectionRenderer;
976
+ if (itemSection?.contents) {
977
+ for (const item of itemSection.contents) {
978
+ if (item.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token) {
979
+ commentToken = item.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
980
+ break;
981
+ }
982
+ if (item.commentsEntryPointHeaderRenderer?.content?.commentsEntryPointHeaderRenderer?.headerText?.runs?.[0]?.navigationEndpoint?.continuationCommand?.token) {
983
+ commentToken = item.commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.headerText.runs[0].navigationEndpoint.continuationCommand.token;
984
+ break;
985
+ }
986
+ }
987
+ }
988
+ if (commentToken) break;
989
+ }
990
+ if (!commentToken) {
991
+ for (const panel of panels) {
992
+ const token = panel.engagementPanelSectionListRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
993
+ if (token) {
994
+ commentToken = token;
995
+ break;
996
+ }
997
+ }
998
+ }
999
+ this.comments.continuation = commentToken;
1000
+ return this;
711
1001
  }
712
1002
  };
713
1003
 
@@ -817,18 +1107,293 @@ var Playlist = class extends Base {
817
1107
  }
818
1108
  };
819
1109
 
1110
+ // src/channel.ts
1111
+ var Channel = class extends BaseChannel {
1112
+ videoCount;
1113
+ banner;
1114
+ mobileBanner;
1115
+ tvBanner;
1116
+ shelves;
1117
+ constructor(client, data) {
1118
+ super(client, data);
1119
+ this.banner = new Thumbnails([]);
1120
+ this.mobileBanner = new Thumbnails([]);
1121
+ this.tvBanner = new Thumbnails([]);
1122
+ this.shelves = [];
1123
+ if (data) {
1124
+ this.load(data);
1125
+ }
1126
+ }
1127
+ load(data) {
1128
+ super.load(data);
1129
+ const headerObj = data.header?.c4TabbedHeaderRenderer || data.header?.pageHeaderRenderer || data.c4TabbedHeaderRenderer || data;
1130
+ const viewModel = headerObj.content?.pageHeaderViewModel;
1131
+ if (viewModel) {
1132
+ this.banner = new Thumbnails(viewModel.banner?.imageBannerViewModel?.image?.sources || []);
1133
+ this.handle = viewModel.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.[0]?.text?.content || this.handle;
1134
+ this.description = viewModel.description?.descriptionPreviewViewModel?.description?.content || this.description;
1135
+ const rows = viewModel.metadata?.contentMetadataViewModel?.metadataRows || [];
1136
+ for (const row of rows) {
1137
+ for (const part of row.metadataParts || []) {
1138
+ const txt = part.text?.content || "";
1139
+ if (txt.includes("video")) {
1140
+ this.videoCount = txt;
1141
+ }
1142
+ }
1143
+ }
1144
+ }
1145
+ if (headerObj.channelId) {
1146
+ this.videoCount = headerObj.videosCountText?.runs?.[0]?.text || headerObj.videosCountText?.simpleText || this.videoCount;
1147
+ this.banner = new Thumbnails(headerObj.banner?.thumbnails || []);
1148
+ this.tvBanner = new Thumbnails(headerObj.tvBanner?.thumbnails || []);
1149
+ this.mobileBanner = new Thumbnails(headerObj.mobileBanner?.thumbnails || []);
1150
+ }
1151
+ this.shelves = [];
1152
+ const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
1153
+ const homeTab = tabs.find((t) => t.tabRenderer?.selected || t.tabRenderer?.title?.toLowerCase() === "home");
1154
+ const sectionList = homeTab?.tabRenderer?.content?.sectionListRenderer?.contents || [];
1155
+ for (const section of sectionList) {
1156
+ const shelf = section.itemSectionRenderer?.contents?.[0]?.shelfRenderer;
1157
+ if (shelf) {
1158
+ const title = shelf.title?.simpleText || shelf.title?.runs?.[0]?.text || "";
1159
+ const subtitle = shelf.subtitle?.simpleText || shelf.subtitle?.runs?.[0]?.text || void 0;
1160
+ const items = [];
1161
+ const gridItems = shelf.content?.horizontalListRenderer?.items || shelf.content?.gridRenderer?.items || [];
1162
+ for (const item of gridItems) {
1163
+ if (item.gridVideoRenderer) {
1164
+ items.push(item.gridVideoRenderer);
1165
+ } else if (item.gridPlaylistRenderer) {
1166
+ items.push(item.gridPlaylistRenderer);
1167
+ } else if (item.gridChannelRenderer) {
1168
+ items.push(item.gridChannelRenderer);
1169
+ }
1170
+ }
1171
+ this.shelves.push({ title, subtitle, items });
1172
+ }
1173
+ }
1174
+ return this;
1175
+ }
1176
+ };
1177
+
1178
+ // src/mix-playlist.ts
1179
+ var MixPlaylist = class extends Base {
1180
+ id;
1181
+ title;
1182
+ videoCount;
1183
+ videos;
1184
+ constructor(client, data) {
1185
+ super(client);
1186
+ this.id = "";
1187
+ this.title = "";
1188
+ this.videoCount = 0;
1189
+ this.videos = [];
1190
+ if (data) {
1191
+ this.load(data);
1192
+ }
1193
+ }
1194
+ load(data) {
1195
+ const playlist = data.contents?.twoColumnWatchNextResults?.playlist?.playlist || {};
1196
+ this.title = playlist.titleText?.simpleText || playlist.title || "";
1197
+ this.id = playlist.playlistId || "";
1198
+ const contents = playlist.contents || [];
1199
+ this.videos = [];
1200
+ for (const item of contents) {
1201
+ const renderer = item.playlistPanelVideoRenderer;
1202
+ if (renderer) {
1203
+ this.videos.push(new VideoCompact(this.client, renderer));
1204
+ }
1205
+ }
1206
+ this.videoCount = this.videos.length;
1207
+ return this;
1208
+ }
1209
+ };
1210
+
1211
+ // src/chat.ts
1212
+ var Chat = class extends Base {
1213
+ id;
1214
+ message;
1215
+ author;
1216
+ timestamp;
1217
+ constructor(client, data) {
1218
+ super(client);
1219
+ if (data) {
1220
+ this.load(data);
1221
+ }
1222
+ }
1223
+ load(data) {
1224
+ const { id, message, authorName, authorPhoto, timestampUsec, authorExternalChannelId } = data;
1225
+ this.id = id;
1226
+ this.message = message?.runs?.map((r) => r.text).join("") || "";
1227
+ this.timestamp = parseInt(timestampUsec || "0", 10);
1228
+ this.author = new BaseChannel(this.client, {
1229
+ channelId: authorExternalChannelId,
1230
+ title: { simpleText: authorName },
1231
+ thumbnail: { thumbnails: authorPhoto }
1232
+ });
1233
+ return this;
1234
+ }
1235
+ };
1236
+
1237
+ // src/live-video.ts
1238
+ var LiveVideo = class extends BaseVideo {
1239
+ watchingCount = 0;
1240
+ chatContinuation = "";
1241
+ delay = 0;
1242
+ chatRequestPoolingTimeout = null;
1243
+ timeoutMs = 0;
1244
+ isChatPlaying = false;
1245
+ chatQueue = /* @__PURE__ */ new Set();
1246
+ pollSessionId = 0;
1247
+ listeners = {};
1248
+ constructor(client, data) {
1249
+ super(client, data);
1250
+ if (data) {
1251
+ this.load(data);
1252
+ }
1253
+ }
1254
+ on(event, listener) {
1255
+ if (!this.listeners[event]) {
1256
+ this.listeners[event] = [];
1257
+ }
1258
+ this.listeners[event].push(listener);
1259
+ return this;
1260
+ }
1261
+ emit(event, ...args) {
1262
+ const list = this.listeners[event];
1263
+ if (!list || list.length === 0) return false;
1264
+ for (const listener of list) {
1265
+ listener(...args);
1266
+ }
1267
+ return true;
1268
+ }
1269
+ load(data) {
1270
+ super.parse(data);
1271
+ const videoDetails = data.videoDetails || {};
1272
+ const runText = data.response?.contents?.twoColumnWatchNextResults?.results?.results?.contents?.[0]?.videoPrimaryInfoRenderer?.viewCount?.videoViewCountRenderer?.viewCount?.runs?.map((r) => r.text).join(" ");
1273
+ const countStr = runText || videoDetails.viewCount || "0";
1274
+ this.watchingCount = parseInt(String(countStr).replace(/[^0-9]/g, "") || "0", 10);
1275
+ this.chatContinuation = data.response?.contents?.twoColumnWatchNextResults?.conversationBar?.liveChatRenderer?.continuations?.[0]?.reloadContinuationData?.continuation || "";
1276
+ return this;
1277
+ }
1278
+ playChat(delay = 0) {
1279
+ if (this.isChatPlaying) return;
1280
+ this.delay = delay;
1281
+ this.isChatPlaying = true;
1282
+ const sessionId = ++this.pollSessionId;
1283
+ this.pollChatContinuation(sessionId);
1284
+ }
1285
+ stopChat() {
1286
+ this.isChatPlaying = false;
1287
+ this.pollSessionId++;
1288
+ if (this.chatRequestPoolingTimeout) {
1289
+ clearTimeout(this.chatRequestPoolingTimeout);
1290
+ this.chatRequestPoolingTimeout = null;
1291
+ }
1292
+ }
1293
+ async pollChatContinuation(sessionId) {
1294
+ if (!this.isChatPlaying || !this.chatContinuation || sessionId !== this.pollSessionId) return;
1295
+ try {
1296
+ const response = await this.client.request("live_chat/get_live_chat", {
1297
+ continuation: this.chatContinuation
1298
+ });
1299
+ if (!response?.continuationContents?.liveChatContinuation || sessionId !== this.pollSessionId) return;
1300
+ const chatCont = response.continuationContents.liveChatContinuation;
1301
+ const actions = chatCont.actions || [];
1302
+ for (const action of actions) {
1303
+ const item = action.addChatItemAction?.item?.liveChatTextMessageRenderer;
1304
+ if (item) {
1305
+ const chat = new Chat(this.client).load(item);
1306
+ if (this.chatQueue.has(chat.id)) continue;
1307
+ this.chatQueue.add(chat.id);
1308
+ if (this.chatQueue.size > 500) {
1309
+ const firstKey = this.chatQueue.keys().next().value;
1310
+ if (firstKey !== void 0) {
1311
+ this.chatQueue.delete(firstKey);
1312
+ }
1313
+ }
1314
+ const timeout = chat.timestamp / 1e3 - (Date.now() - this.delay);
1315
+ setTimeout(() => {
1316
+ if (sessionId === this.pollSessionId) {
1317
+ this.emit("chat", chat);
1318
+ }
1319
+ }, Math.max(0, timeout));
1320
+ }
1321
+ }
1322
+ const continuation = chatCont.continuations?.[0];
1323
+ const continuationData = continuation?.timedContinuationData || continuation?.invalidationContinuationData;
1324
+ if (continuationData && sessionId === this.pollSessionId) {
1325
+ this.timeoutMs = continuationData.timeoutMs || 5e3;
1326
+ this.chatContinuation = continuationData.continuation;
1327
+ this.chatRequestPoolingTimeout = setTimeout(() => this.pollChatContinuation(sessionId), this.timeoutMs);
1328
+ }
1329
+ } catch (e) {
1330
+ console.error("[LiveVideo] Error polling live chat:", e);
1331
+ if (sessionId === this.pollSessionId) {
1332
+ this.chatRequestPoolingTimeout = setTimeout(() => this.pollChatContinuation(sessionId), 5e3);
1333
+ }
1334
+ }
1335
+ }
1336
+ };
1337
+
820
1338
  // src/client.ts
821
- function getFallbackClientVersion2() {
1339
+ var CLIENT_PROFILES = [
1340
+ {
1341
+ name: "ios",
1342
+ clientName: "IOS",
1343
+ clientVersion: "20.10.4",
1344
+ clientNameHeader: "5",
1345
+ userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)",
1346
+ context: {
1347
+ deviceMake: "Apple",
1348
+ deviceModel: "iPhone16,2",
1349
+ platform: "MOBILE",
1350
+ osName: "iOS",
1351
+ osVersion: "18.3.2.22D82"
1352
+ }
1353
+ },
1354
+ {
1355
+ name: "android_vr",
1356
+ clientName: "ANDROID_VR",
1357
+ clientVersion: "1.62.20",
1358
+ clientNameHeader: "28",
1359
+ userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.20 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip",
1360
+ context: {
1361
+ deviceMake: "Oculus",
1362
+ deviceModel: "Quest 3",
1363
+ platform: "MOBILE",
1364
+ osName: "Android",
1365
+ osVersion: "12L",
1366
+ androidSdkVersion: 32
1367
+ }
1368
+ },
1369
+ {
1370
+ name: "mweb",
1371
+ clientName: "MWEB",
1372
+ clientVersion: "2.20251209.01.00",
1373
+ clientNameHeader: "2",
1374
+ userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
1375
+ context: {
1376
+ platform: "MOBILE",
1377
+ osName: "iOS",
1378
+ osVersion: "17.5.1"
1379
+ }
1380
+ }
1381
+ ];
1382
+ function getFallbackClientVersion() {
822
1383
  const d = /* @__PURE__ */ new Date();
823
1384
  d.setDate(d.getDate() - 2);
824
1385
  const yyyymmdd = d.toISOString().split("T")[0].replace(/-/g, "");
825
1386
  return `2.${yyyymmdd}.00.00`;
826
1387
  }
827
1388
  function getSapisidFromCookieString(cookieString) {
1389
+ if (!cookieString && typeof document !== "undefined") {
1390
+ cookieString = document.cookie;
1391
+ }
1392
+ if (!cookieString) return null;
828
1393
  const match = cookieString.match(/__Secure-3PAPISID=([^;]+)/) || cookieString.match(/__Secure-1PAPISID=([^;]+)/) || cookieString.match(/SAPISID=([^;]+)/);
829
1394
  return match ? match[1] : null;
830
1395
  }
831
- async function getSApiSidHash2(sapisid, origin = "https://www.youtube.com") {
1396
+ async function getSApiSidHash(sapisid, origin = "https://www.youtube.com") {
832
1397
  if (!sapisid) return null;
833
1398
  try {
834
1399
  const timestamp = Math.floor(Date.now() / 1e3);
@@ -843,13 +1408,47 @@ async function getSApiSidHash2(sapisid, origin = "https://www.youtube.com") {
843
1408
  return null;
844
1409
  }
845
1410
  }
1411
+ var cachedApiKey = null;
1412
+ var cachedClientVersion = null;
1413
+ var cachedIdToken = null;
1414
+ async function getInnerTubeConfig(injectedConfig, customFetch) {
1415
+ if (injectedConfig && injectedConfig.apiKey) {
1416
+ return {
1417
+ apiKey: injectedConfig.apiKey,
1418
+ clientVersion: injectedConfig.clientVersion || getFallbackClientVersion(),
1419
+ idToken: injectedConfig.idToken ?? null
1420
+ };
1421
+ }
1422
+ if (cachedApiKey && cachedClientVersion) {
1423
+ return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
1424
+ }
1425
+ try {
1426
+ const fetchFn = customFetch || (typeof globalThis !== "undefined" ? globalThis.fetch : fetch);
1427
+ const response = await fetchFn("https://www.youtube.com", { credentials: "include" });
1428
+ const html = await response.text();
1429
+ const apiKeyMatch = html.match(/"INNERTUBE_API_KEY":"([^"]+)"/);
1430
+ const clientVersionMatch = html.match(/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/);
1431
+ const idTokenMatch = html.match(/"ID_TOKEN":"([^"]+)"/);
1432
+ if (apiKeyMatch && apiKeyMatch[1]) cachedApiKey = apiKeyMatch[1];
1433
+ if (clientVersionMatch && clientVersionMatch[1]) cachedClientVersion = clientVersionMatch[1];
1434
+ if (idTokenMatch && idTokenMatch[1]) cachedIdToken = idTokenMatch[1];
1435
+ } catch (e) {
1436
+ console.warn("[Client] Failed to extract InnerTube config from HTML", e);
1437
+ }
1438
+ if (!cachedClientVersion) cachedClientVersion = getFallbackClientVersion();
1439
+ return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
1440
+ }
846
1441
  var Client = class {
847
1442
  apiKey = null;
848
1443
  clientVersion = "";
849
1444
  idToken = null;
850
1445
  cookie = "";
1446
+ fetch;
1447
+ cache;
851
1448
  constructor(options = {}) {
852
1449
  this.cookie = options.cookie !== void 0 ? options.cookie : "";
1450
+ this.fetch = options.fetch || (typeof globalThis !== "undefined" ? globalThis.fetch : fetch);
1451
+ this.cache = options.cache;
853
1452
  let resolvedApiKey = options.apiKey !== void 0 ? options.apiKey : null;
854
1453
  let resolvedClientVersion = options.clientVersion !== void 0 ? options.clientVersion : null;
855
1454
  let resolvedIdToken = options.idToken !== void 0 ? options.idToken : null;
@@ -865,114 +1464,550 @@ var Client = class {
865
1464
  if (resolvedIdToken === null) resolvedIdToken = ytcfg.ID_TOKEN || null;
866
1465
  }
867
1466
  }
868
- this.apiKey = resolvedApiKey;
869
- this.clientVersion = resolvedClientVersion || getFallbackClientVersion2();
870
- this.idToken = resolvedIdToken;
871
- }
872
- /**
873
- * Asynchronously resolves config parameters if apiKey is not set yet.
874
- * Fetches and parses YouTube's home page HTML using the scraper logic.
875
- */
876
- async ensureConfig() {
877
- if (this.apiKey) {
878
- return;
879
- }
880
- const config = await getInnerTubeConfig();
881
- this.apiKey = config.apiKey;
882
- if (config.clientVersion) {
883
- this.clientVersion = config.clientVersion;
884
- }
885
- this.idToken = config.idToken;
1467
+ this.apiKey = resolvedApiKey;
1468
+ this.clientVersion = resolvedClientVersion || getFallbackClientVersion();
1469
+ this.idToken = resolvedIdToken;
1470
+ }
1471
+ async ensureConfig() {
1472
+ if (this.apiKey) return;
1473
+ const config = await getInnerTubeConfig(null, this.fetch);
1474
+ this.apiKey = config.apiKey;
1475
+ if (config.clientVersion) this.clientVersion = config.clientVersion;
1476
+ this.idToken = config.idToken;
1477
+ }
1478
+ /**
1479
+ * Standard InnerTube request using the WEB client (useful for authenticated endpoints).
1480
+ */
1481
+ async request(endpoint, payload) {
1482
+ await this.ensureConfig();
1483
+ if (!this.apiKey) {
1484
+ throw new Error("[Client] InnerTube API key is not configured.");
1485
+ }
1486
+ const cleanEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
1487
+ const url = `https://www.youtube.com/youtubei/v1/${cleanEndpoint}?key=${this.apiKey}&prettyPrint=false`;
1488
+ const headers = {
1489
+ "Content-Type": "application/json",
1490
+ "X-Youtube-Client-Name": "1",
1491
+ "X-Youtube-Client-Version": this.clientVersion
1492
+ };
1493
+ if (this.idToken) headers["X-Youtube-Identity-Token"] = this.idToken;
1494
+ const activeCookie = this.cookie || (typeof document !== "undefined" ? document.cookie : "");
1495
+ if (activeCookie) {
1496
+ headers["Cookie"] = activeCookie;
1497
+ const sapisid = getSapisidFromCookieString(activeCookie);
1498
+ if (sapisid) {
1499
+ const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
1500
+ if (authHash) headers["Authorization"] = `SAPISIDHASH ${authHash}`;
1501
+ }
1502
+ }
1503
+ const bodyPayload = payload && typeof payload === "object" ? payload : {};
1504
+ const requestBody = {
1505
+ ...bodyPayload,
1506
+ context: {
1507
+ ...bodyPayload.context,
1508
+ client: {
1509
+ clientName: "WEB",
1510
+ clientVersion: this.clientVersion,
1511
+ hl: "en",
1512
+ gl: "US",
1513
+ ...bodyPayload.context?.client
1514
+ }
1515
+ }
1516
+ };
1517
+ const authContext = this.idToken ? "_auth" : "_anon";
1518
+ const cacheKey = `yt_request_${cleanEndpoint}_${JSON.stringify(bodyPayload)}${authContext}`;
1519
+ if (this.cache) {
1520
+ const cached = await this.cache.get(cacheKey);
1521
+ if (cached) return cached;
1522
+ }
1523
+ const response = await this.fetch(url, {
1524
+ method: "POST",
1525
+ headers,
1526
+ body: JSON.stringify(requestBody),
1527
+ credentials: "include"
1528
+ });
1529
+ if (!response.ok) {
1530
+ throw new Error(`InnerTube request failed with status: ${response.status}`);
1531
+ }
1532
+ const data = await response.json();
1533
+ if (this.cache) {
1534
+ await this.cache.set(cacheKey, data);
1535
+ }
1536
+ return data;
1537
+ }
1538
+ /**
1539
+ * Specialized request for the /player endpoint that loops through client profiles
1540
+ * to bypass IP blocks or Captcha walls.
1541
+ */
1542
+ async requestPlayerWithFallback(videoId) {
1543
+ await this.ensureConfig();
1544
+ const authContext = this.idToken ? "_auth" : "_anon";
1545
+ const cacheKey = `yt_player_fallback_${videoId}${authContext}`;
1546
+ if (this.cache) {
1547
+ const cached = await this.cache.get(cacheKey);
1548
+ if (cached) return cached;
1549
+ }
1550
+ const url = `https://www.youtube.com/youtubei/v1/player?key=${this.apiKey}&prettyPrint=false`;
1551
+ let firstPlayable = null;
1552
+ const failures = [];
1553
+ for (const profile of CLIENT_PROFILES) {
1554
+ try {
1555
+ const body = {
1556
+ videoId,
1557
+ context: {
1558
+ client: {
1559
+ clientName: profile.clientName,
1560
+ clientVersion: profile.clientVersion,
1561
+ hl: "en",
1562
+ gl: "US",
1563
+ ...profile.context
1564
+ },
1565
+ user: { lockedSafetyMode: false },
1566
+ request: { useSsl: true }
1567
+ },
1568
+ contentCheckOk: true,
1569
+ racyCheckOk: true
1570
+ };
1571
+ const headers = {
1572
+ "Content-Type": "application/json",
1573
+ "User-Agent": profile.userAgent,
1574
+ "X-YouTube-Client-Name": profile.clientNameHeader,
1575
+ "X-YouTube-Client-Version": profile.clientVersion,
1576
+ "Origin": "https://www.youtube.com"
1577
+ };
1578
+ if (this.idToken) headers["X-Youtube-Identity-Token"] = this.idToken;
1579
+ const activeCookie = this.cookie || (typeof document !== "undefined" ? document.cookie : "");
1580
+ if (activeCookie) {
1581
+ headers["Cookie"] = activeCookie;
1582
+ const sapisid = getSapisidFromCookieString(activeCookie);
1583
+ if (sapisid) {
1584
+ const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
1585
+ if (authHash) headers["Authorization"] = `SAPISIDHASH ${authHash}`;
1586
+ }
1587
+ }
1588
+ const response = await this.fetch(url, {
1589
+ method: "POST",
1590
+ headers,
1591
+ body: JSON.stringify(body),
1592
+ credentials: "include"
1593
+ });
1594
+ if (!response.ok) {
1595
+ failures.push(`${profile.name}: ${response.status}`);
1596
+ continue;
1597
+ }
1598
+ const data = await response.json();
1599
+ const status = data.playabilityStatus?.status;
1600
+ if (status && status !== "OK") {
1601
+ const reason = data.playabilityStatus?.reason;
1602
+ failures.push(`${profile.name}: ${status}${reason ? ` - ${reason}` : ""}`);
1603
+ continue;
1604
+ }
1605
+ if (!firstPlayable) firstPlayable = data;
1606
+ const tracks = data.captions?.playerCaptionsTracklistRenderer?.captionTracks;
1607
+ if (tracks && tracks.length > 0) {
1608
+ if (this.cache) await this.cache.set(cacheKey, data);
1609
+ return data;
1610
+ }
1611
+ failures.push(`${profile.name}: OK but no caption tracks`);
1612
+ } catch (err) {
1613
+ failures.push(`${profile.name}: ${err.message}`);
1614
+ }
1615
+ }
1616
+ if (firstPlayable) {
1617
+ if (this.cache) await this.cache.set(cacheKey, firstPlayable);
1618
+ return firstPlayable;
1619
+ }
1620
+ console.warn(`[Client] All mobile profiles failed, falling back to WEB profile. Attempts:
1621
+ ${failures.join("\n")}`);
1622
+ return this.request("player", { videoId, contentCheckOk: true, racyCheckOk: true });
1623
+ }
1624
+ async search(query, options) {
1625
+ const payload = { query };
1626
+ if (options?.type === "video") payload.params = "EgIQAQ%3D%3D";
1627
+ else if (options?.type === "playlist") payload.params = "EgIQAw%3D%3D";
1628
+ else if (options?.type === "channel") payload.params = "EgIQAg%3D%3D";
1629
+ const data = await this.request("search", payload);
1630
+ return new SearchResult(this, data);
1631
+ }
1632
+ async findOne(query, options) {
1633
+ const result = await this.search(query, options);
1634
+ return result.items[0] || void 0;
1635
+ }
1636
+ async getVideo(videoId) {
1637
+ const [playerData, nextData] = await Promise.all([
1638
+ this.requestPlayerWithFallback(videoId),
1639
+ this.request("next", { videoId })
1640
+ ]);
1641
+ const merged = { ...playerData, ...nextData };
1642
+ if (playerData.playabilityStatus?.liveStreamability) {
1643
+ return new LiveVideo(this, merged);
1644
+ }
1645
+ return new Video(this, merged);
1646
+ }
1647
+ async getPlaylist(playlistId) {
1648
+ if (playlistId.startsWith("RD")) {
1649
+ const data2 = await this.request("next", { playlistId });
1650
+ return new MixPlaylist(this, data2);
1651
+ }
1652
+ const browseId = playlistId.startsWith("VL") ? playlistId : `VL${playlistId}`;
1653
+ const data = await this.request("browse", { browseId });
1654
+ return new Playlist(this, data);
1655
+ }
1656
+ async getChannel(channelId) {
1657
+ const data = await this.request("browse", { browseId: channelId });
1658
+ if (data.error) return void 0;
1659
+ return new Channel(this, data);
1660
+ }
1661
+ async getVideoTranscript(videoId, languageCode) {
1662
+ const video = await this.getVideo(videoId);
1663
+ return video.captions?.get(languageCode);
1664
+ }
1665
+ };
1666
+
1667
+ // src/scraper.ts
1668
+ async function fetchYtInitialData(url) {
1669
+ try {
1670
+ console.log(`[Scraper] Fetching HTML from ${url}`);
1671
+ const response = await fetch(url, { credentials: "include" });
1672
+ const text = await response.text();
1673
+ const patterns = [
1674
+ /var ytInitialData\s*=\s*(\{.*?\});<\/script>/,
1675
+ /window\["ytInitialData"\]\s*=\s*(\{.*?\});/
1676
+ ];
1677
+ for (const regex of patterns) {
1678
+ const match = text.match(regex);
1679
+ if (match && match[1]) {
1680
+ return JSON.parse(match[1]);
1681
+ }
1682
+ }
1683
+ console.warn(`[Scraper] Could not find ytInitialData in ${url}`);
1684
+ } catch (e) {
1685
+ console.error(`[Scraper] Failed to fetch data from ${url}`, e);
1686
+ }
1687
+ return null;
1688
+ }
1689
+ function getSapisidFromCookie() {
1690
+ return getSapisidFromCookieString("");
1691
+ }
1692
+ function extractVideoEntries(data) {
1693
+ const entries = [];
1694
+ const seenTitles = /* @__PURE__ */ new Set();
1695
+ function recurse(obj) {
1696
+ if (!obj || typeof obj !== "object") return;
1697
+ if (Array.isArray(obj)) {
1698
+ for (let i = 0; i < obj.length; i++) {
1699
+ recurse(obj[i]);
1700
+ }
1701
+ return;
1702
+ }
1703
+ if (obj.videoId && obj.title) {
1704
+ let title = "";
1705
+ if (typeof obj.title === "string") {
1706
+ title = obj.title;
1707
+ } else if (obj.title.runs && obj.title.runs[0] && obj.title.runs[0].text) {
1708
+ title = obj.title.runs[0].text;
1709
+ } else if (obj.title.simpleText) {
1710
+ title = obj.title.simpleText;
1711
+ }
1712
+ title = title.trim();
1713
+ if (title && title.length > 2 && title !== "Skip navigation") {
1714
+ if (!seenTitles.has(title)) {
1715
+ seenTitles.add(title);
1716
+ let channel = "";
1717
+ const byline = obj.longBylineText || obj.shortBylineText || obj.ownerText;
1718
+ if (byline) {
1719
+ if (typeof byline === "string") {
1720
+ channel = byline;
1721
+ } else if (byline.runs && byline.runs[0] && byline.runs[0].text) {
1722
+ channel = byline.runs[0].text;
1723
+ } else if (byline.simpleText) {
1724
+ channel = byline.simpleText;
1725
+ }
1726
+ }
1727
+ entries.push({
1728
+ title,
1729
+ channel: channel.split("\n")[0].replace(/•/g, "").trim()
1730
+ });
1731
+ }
1732
+ }
1733
+ return;
1734
+ }
1735
+ if (obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
1736
+ const model = obj.lockupViewModel;
1737
+ const meta = model.metadata?.lockupMetadataViewModel;
1738
+ if (meta && meta.title && meta.title.content) {
1739
+ const title = meta.title.content.trim();
1740
+ if (title && !seenTitles.has(title)) {
1741
+ seenTitles.add(title);
1742
+ let channel = "";
1743
+ const rows = meta.metadata?.contentMetadataViewModel?.metadataRows;
1744
+ if (rows && rows[0] && rows[0].metadataParts && rows[0].metadataParts[0] && rows[0].metadataParts[0].text) {
1745
+ channel = rows[0].metadataParts[0].text.content || "";
1746
+ }
1747
+ entries.push({
1748
+ title,
1749
+ channel: channel.split("\n")[0].replace(/•/g, "").trim()
1750
+ });
1751
+ }
1752
+ }
1753
+ return;
1754
+ }
1755
+ for (const key in obj) {
1756
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1757
+ recurse(obj[key]);
1758
+ }
1759
+ }
1760
+ }
1761
+ try {
1762
+ recurse(data);
1763
+ } catch (e) {
1764
+ console.warn("[Scraper] Error extracting video entries recursively", e);
1765
+ }
1766
+ return entries;
1767
+ }
1768
+ function findContinuationToken(obj) {
1769
+ if (!obj || typeof obj !== "object") return null;
1770
+ if (obj.continuationCommand && obj.continuationCommand.token) {
1771
+ return obj.continuationCommand.token;
1772
+ }
1773
+ for (const key in obj) {
1774
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1775
+ const token = findContinuationToken(obj[key]);
1776
+ if (token) return token;
1777
+ }
1778
+ }
1779
+ return null;
1780
+ }
1781
+ async function fetchInnerTubeContinuation(apiKey, clientVersion, idToken, initialToken, limit) {
1782
+ const entries = [];
1783
+ let continuationToken = initialToken;
1784
+ try {
1785
+ while (continuationToken && entries.length < limit) {
1786
+ const headers = {
1787
+ "Content-Type": "application/json",
1788
+ "X-Youtube-Client-Name": "1",
1789
+ "X-Youtube-Client-Version": clientVersion
1790
+ };
1791
+ if (idToken) {
1792
+ headers["X-Youtube-Identity-Token"] = idToken;
1793
+ }
1794
+ const sapisid = getSapisidFromCookie();
1795
+ if (sapisid) {
1796
+ const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
1797
+ if (authHash) {
1798
+ headers["Authorization"] = `SAPISIDHASH ${authHash}`;
1799
+ }
1800
+ }
1801
+ const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
1802
+ method: "POST",
1803
+ headers,
1804
+ credentials: "include",
1805
+ body: JSON.stringify({
1806
+ context: {
1807
+ client: {
1808
+ clientName: "WEB",
1809
+ clientVersion,
1810
+ hl: "en",
1811
+ gl: "US"
1812
+ }
1813
+ },
1814
+ continuation: continuationToken
1815
+ })
1816
+ });
1817
+ if (!response.ok) break;
1818
+ const data = await response.json();
1819
+ const pageEntries = extractVideoEntries(data);
1820
+ entries.push(...pageEntries);
1821
+ continuationToken = findContinuationToken(data);
1822
+ }
1823
+ } catch (e) {
1824
+ console.error("[Scraper] Error in InnerTube pagination", e);
886
1825
  }
887
- /**
888
- * Dispatches an authenticated or unauthenticated POST request to the specified InnerTube endpoint.
889
- */
890
- async request(endpoint, payload) {
891
- await this.ensureConfig();
892
- if (!this.apiKey) {
893
- throw new Error("[Client] InnerTube API key is not configured.");
894
- }
895
- const cleanEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
896
- const url = `https://www.youtube.com/youtubei/v1/${cleanEndpoint}?key=${this.apiKey}&prettyPrint=false`;
1826
+ return entries;
1827
+ }
1828
+ async function fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit = 500) {
1829
+ const entries = [];
1830
+ try {
897
1831
  const headers = {
898
1832
  "Content-Type": "application/json",
899
1833
  "X-Youtube-Client-Name": "1",
900
- "X-Youtube-Client-Version": this.clientVersion
1834
+ "X-Youtube-Client-Version": clientVersion
901
1835
  };
902
- if (this.idToken) {
903
- headers["X-Youtube-Identity-Token"] = this.idToken;
1836
+ if (idToken) {
1837
+ headers["X-Youtube-Identity-Token"] = idToken;
904
1838
  }
905
- const activeCookie = this.cookie || (typeof document !== "undefined" ? document.cookie : "");
906
- if (activeCookie) {
907
- headers["Cookie"] = activeCookie;
908
- const sapisid = getSapisidFromCookieString(activeCookie);
909
- if (sapisid) {
910
- const authHash = await getSApiSidHash2(sapisid, "https://www.youtube.com");
911
- if (authHash) {
912
- headers["Authorization"] = `SAPISIDHASH ${authHash}`;
913
- }
1839
+ const sapisid = getSapisidFromCookie();
1840
+ if (sapisid) {
1841
+ const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
1842
+ if (authHash) {
1843
+ headers["Authorization"] = `SAPISIDHASH ${authHash}`;
914
1844
  }
915
1845
  }
916
- const bodyPayload = payload && typeof payload === "object" ? payload : {};
917
- const requestBody = {
918
- ...bodyPayload,
919
- context: {
920
- ...bodyPayload.context,
921
- client: {
922
- clientName: "WEB",
923
- clientVersion: this.clientVersion,
924
- hl: "en",
925
- gl: "US",
926
- ...bodyPayload.context?.client
927
- }
928
- }
929
- };
930
- const response = await fetch(url, {
1846
+ const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
931
1847
  method: "POST",
932
1848
  headers,
933
- body: JSON.stringify(requestBody),
934
- credentials: "include"
1849
+ credentials: "include",
1850
+ body: JSON.stringify({
1851
+ context: {
1852
+ client: {
1853
+ clientName: "WEB",
1854
+ clientVersion,
1855
+ hl: "en",
1856
+ gl: "US"
1857
+ }
1858
+ },
1859
+ browseId
1860
+ })
935
1861
  });
936
- if (!response.ok) {
937
- throw new Error(`InnerTube request failed with status: ${response.status}`);
1862
+ if (!response.ok) return [];
1863
+ const data = await response.json();
1864
+ const pageEntries = extractVideoEntries(data);
1865
+ entries.push(...pageEntries);
1866
+ if (entries.length < limit) {
1867
+ const continuationToken = findContinuationToken(data);
1868
+ if (continuationToken) {
1869
+ const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, continuationToken, limit - entries.length);
1870
+ entries.push(...more);
1871
+ }
938
1872
  }
939
- return response.json();
1873
+ } catch (e) {
1874
+ console.error(`[Scraper] InnerTube fetch error for ${browseId}`, e);
940
1875
  }
941
- /**
942
- * Searches YouTube for the given query.
943
- */
944
- async search(query, options) {
945
- const payload = { query };
946
- if (options?.type === "video") {
947
- payload.params = "EgIQAQ%3D%3D";
948
- } else if (options?.type === "playlist") {
949
- payload.params = "EgIQAw%3D%3D";
950
- } else if (options?.type === "channel") {
951
- payload.params = "EgIQAg%3D%3D";
1876
+ return entries;
1877
+ }
1878
+ async function scrapeTasteData(injectedConfig, customPlaylists = [], limit = 500) {
1879
+ let historyEntries = [];
1880
+ let likesEntries = [];
1881
+ let wlEntries = [];
1882
+ const dislikesEntries = [];
1883
+ const config = await getInnerTubeConfig(injectedConfig);
1884
+ const apiKey = config.apiKey;
1885
+ const clientVersion = config.clientVersion;
1886
+ const idToken = config.idToken;
1887
+ if (apiKey) {
1888
+ historyEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "FEhistory", limit);
1889
+ likesEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLLL", limit);
1890
+ wlEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLWL", limit);
1891
+ }
1892
+ if (historyEntries.length === 0) {
1893
+ const historyData = await fetchYtInitialData("https://www.youtube.com/feed/history");
1894
+ if (historyData) {
1895
+ historyEntries = extractVideoEntries(historyData);
1896
+ if (historyEntries.length < limit) {
1897
+ const token = findContinuationToken(historyData);
1898
+ if (token && apiKey) {
1899
+ const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - historyEntries.length);
1900
+ historyEntries.push(...more);
1901
+ }
1902
+ }
952
1903
  }
953
- const data = await this.request("search", payload);
954
- return new SearchResult(this, data);
955
1904
  }
956
- /**
957
- * Gets metadata for a specific video.
958
- */
959
- async getVideo(videoId) {
960
- const [playerData, nextData] = await Promise.all([
961
- this.request("player", { videoId }),
962
- this.request("next", { videoId })
963
- ]);
964
- const merged = { ...playerData, ...nextData };
965
- return new Video(this, merged);
1905
+ if (likesEntries.length === 0) {
1906
+ const likesData = await fetchYtInitialData("https://www.youtube.com/playlist?list=LL");
1907
+ if (likesData) {
1908
+ likesEntries = extractVideoEntries(likesData);
1909
+ if (likesEntries.length < limit) {
1910
+ const token = findContinuationToken(likesData);
1911
+ if (token && apiKey) {
1912
+ const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - likesEntries.length);
1913
+ likesEntries.push(...more);
1914
+ }
1915
+ }
1916
+ }
966
1917
  }
967
- /**
968
- * Gets metadata and videos for a specific playlist.
969
- */
970
- async getPlaylist(playlistId) {
971
- const browseId = playlistId.startsWith("VL") ? playlistId : `VL${playlistId}`;
972
- const data = await this.request("browse", { browseId });
973
- return new Playlist(this, data);
1918
+ if (wlEntries.length === 0) {
1919
+ const wlData = await fetchYtInitialData("https://www.youtube.com/playlist?list=WL");
1920
+ if (wlData) {
1921
+ wlEntries = extractVideoEntries(wlData);
1922
+ if (wlEntries.length < limit) {
1923
+ const token = findContinuationToken(wlData);
1924
+ if (token && apiKey) {
1925
+ const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - wlEntries.length);
1926
+ wlEntries.push(...more);
1927
+ }
1928
+ }
1929
+ }
974
1930
  }
975
- };
1931
+ const customPlaylistsData = [];
1932
+ for (const pl of customPlaylists) {
1933
+ if (!pl.url) continue;
1934
+ const match = pl.url.match(/[&?]list=([a-zA-Z0-9_-]+)/);
1935
+ const playlistId = match ? match[1] : pl.url.trim();
1936
+ if (!playlistId || !/^[a-zA-Z0-9_-]+$/.test(playlistId)) continue;
1937
+ const browseId = playlistId.startsWith("VL") ? playlistId : "VL" + playlistId;
1938
+ let entries = [];
1939
+ if (apiKey) {
1940
+ entries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit);
1941
+ }
1942
+ if (entries.length === 0) {
1943
+ const data = await fetchYtInitialData(`https://www.youtube.com/playlist?list=${playlistId}`);
1944
+ if (data) {
1945
+ entries = extractVideoEntries(data);
1946
+ if (entries.length < limit) {
1947
+ const token = findContinuationToken(data);
1948
+ if (token && apiKey) {
1949
+ const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - entries.length);
1950
+ entries.push(...more);
1951
+ }
1952
+ }
1953
+ }
1954
+ }
1955
+ customPlaylistsData.push({
1956
+ id: playlistId,
1957
+ entries: entries.slice(0, limit)
1958
+ });
1959
+ }
1960
+ return {
1961
+ historyEntries: historyEntries.slice(0, limit),
1962
+ likesEntries: likesEntries.slice(0, limit),
1963
+ wlEntries: wlEntries.slice(0, limit),
1964
+ dislikesEntries: dislikesEntries.slice(0, limit),
1965
+ customPlaylistsData
1966
+ };
1967
+ }
1968
+
1969
+ // src/formatters.ts
1970
+ function formatSrtTimestamp(seconds) {
1971
+ const totalMs = Math.round(seconds * 1e3);
1972
+ const ms = totalMs % 1e3;
1973
+ const totalSeconds = Math.floor(totalMs / 1e3);
1974
+ const s = totalSeconds % 60;
1975
+ const m = Math.floor(totalSeconds / 60) % 60;
1976
+ const h = Math.floor(totalSeconds / 3600);
1977
+ return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") + "," + String(ms).padStart(3, "0");
1978
+ }
1979
+ function formatVttTimestamp(seconds) {
1980
+ const totalMs = Math.round(seconds * 1e3);
1981
+ const ms = totalMs % 1e3;
1982
+ const totalSeconds = Math.floor(totalMs / 1e3);
1983
+ const s = totalSeconds % 60;
1984
+ const m = Math.floor(totalSeconds / 60) % 60;
1985
+ const h = Math.floor(totalSeconds / 3600);
1986
+ return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") + "." + String(ms).padStart(3, "0");
1987
+ }
1988
+ function toSRT(segments) {
1989
+ return segments.map((segment, index) => {
1990
+ const start = formatSrtTimestamp(segment.start);
1991
+ const end = formatSrtTimestamp(segment.start + segment.duration);
1992
+ return `${index + 1}
1993
+ ${start} --> ${end}
1994
+ ${segment.text}`;
1995
+ }).join("\n\n");
1996
+ }
1997
+ function toVTT(segments) {
1998
+ const cues = segments.map((segment) => {
1999
+ const start = formatVttTimestamp(segment.start);
2000
+ const end = formatVttTimestamp(segment.start + segment.duration);
2001
+ return `${start} --> ${end}
2002
+ ${segment.text}`;
2003
+ }).join("\n\n");
2004
+ return `WEBVTT
2005
+
2006
+ ${cues}`;
2007
+ }
2008
+ function toPlainText(segments, separator = "\n") {
2009
+ return segments.map((segment) => segment.text).join(separator);
2010
+ }
976
2011
 
977
2012
  // src/comments.ts
978
2013
  function findValue(obj, path, defaultValue = void 0) {
@@ -1096,7 +2131,7 @@ async function fetchCommentsFromYouTube(videoId, count = 50, injectedConfig = nu
1096
2131
  console.log(`[Comments] Fetching comment batch, total comments so far: ${fetchedCount}`);
1097
2132
  const options = createCommentsApiRequestOptions(continuationToken, clientVersion);
1098
2133
  const headers = { ...options.headers };
1099
- const sapisid = getSapisidFromCookie();
2134
+ const sapisid = getSapisidFromCookieString();
1100
2135
  if (sapisid) {
1101
2136
  const authHash = await getSApiSidHash(sapisid);
1102
2137
  if (authHash) {
@@ -1235,6 +2270,15 @@ function extractPlayerResponse(html) {
1235
2270
  }
1236
2271
  return null;
1237
2272
  }
2273
+ function decodeHtmlEntities(text) {
2274
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
2275
+ const code = parseInt(hex, 16);
2276
+ return code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : "";
2277
+ }).replace(/&#(\d+);/g, (_, dec) => {
2278
+ const code = parseInt(dec, 10);
2279
+ return code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : "";
2280
+ });
2281
+ }
1238
2282
  function parseXmlTranscriptRegex(xmlText) {
1239
2283
  const segments = [];
1240
2284
  const regex = /<text start="([^"]+)" dur="([^"]+)"[^>]*>(.*?)<\/text>/g;
@@ -1242,26 +2286,45 @@ function parseXmlTranscriptRegex(xmlText) {
1242
2286
  while ((match = regex.exec(xmlText)) !== null) {
1243
2287
  const start = parseFloat(match[1]);
1244
2288
  const duration = parseFloat(match[2]);
1245
- const text = match[3].replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\n/g, " ").trim();
2289
+ let text = decodeHtmlEntities(match[3]).replace(/\n/g, " ").trim();
1246
2290
  if (text && !isNaN(start)) {
1247
2291
  segments.push({ start, duration, text });
1248
2292
  }
1249
2293
  }
1250
2294
  return segments;
1251
2295
  }
1252
- async function fetchSubtitlesFromYouTube(videoId, language = "en") {
2296
+ async function fetchSubtitlesFromYouTube(videoId, language = "en", clientOrOptions) {
1253
2297
  try {
1254
- console.log(`[Subtitles] Fetching video watch page for video: ${videoId}`);
1255
- const videoPageUrl = `https://www.youtube.com/watch?v=${videoId}&bpctr=9999999999`;
1256
- const response = await fetch(videoPageUrl, {
1257
- headers: {
1258
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
1259
- "Accept-Language": "en-US"
1260
- }
1261
- });
1262
- const pageHtml = await response.text();
1263
- console.log("[Subtitles] Extracting caption tracks...");
1264
- const playerResponse = extractPlayerResponse(pageHtml);
2298
+ let client;
2299
+ if (clientOrOptions instanceof Client) {
2300
+ client = clientOrOptions;
2301
+ } else {
2302
+ client = new Client(clientOrOptions);
2303
+ }
2304
+ const cacheKey = `yt_subtitles_${videoId}_${language}`;
2305
+ if (client.cache) {
2306
+ const cached = await client.cache.get(cacheKey);
2307
+ if (cached) return cached;
2308
+ }
2309
+ console.log(`[Subtitles] Fetching player data for video: ${videoId}`);
2310
+ let playerResponse = null;
2311
+ try {
2312
+ playerResponse = await client.requestPlayerWithFallback(videoId);
2313
+ } catch (apiError) {
2314
+ console.warn(`[Subtitles] InnerTube API failed for ${videoId}. Falling back to HTML scraping.`, apiError);
2315
+ }
2316
+ if (!playerResponse || !playerResponse.captions || !playerResponse.captions.playerCaptionsTracklistRenderer) {
2317
+ console.log(`[Subtitles] No captions in API response, trying HTML scraping fallback for ${videoId}`);
2318
+ const videoPageUrl = `https://www.youtube.com/watch?v=${videoId}&bpctr=9999999999`;
2319
+ const response = await client.fetch(videoPageUrl, {
2320
+ headers: {
2321
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
2322
+ "Accept-Language": "en-US"
2323
+ }
2324
+ });
2325
+ const pageHtml = await response.text();
2326
+ playerResponse = extractPlayerResponse(pageHtml);
2327
+ }
1265
2328
  if (!playerResponse || !playerResponse.captions || !playerResponse.captions.playerCaptionsTracklistRenderer) {
1266
2329
  console.warn(`[Subtitles] No captions tracklist found for video: ${videoId}`);
1267
2330
  return [];
@@ -1273,6 +2336,7 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
1273
2336
  }
1274
2337
  const tracks = captionTracks.map((t) => ({
1275
2338
  baseUrl: t.baseUrl,
2339
+ vssId: t.vssId,
1276
2340
  languageCode: t.languageCode,
1277
2341
  name: t.name?.simpleText || t.name,
1278
2342
  isTranslatable: t.kind === "asr"
@@ -1280,9 +2344,7 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
1280
2344
  let selectedTrack;
1281
2345
  if (language && language !== "auto") {
1282
2346
  const lowerLang = language.toLowerCase();
1283
- selectedTrack = tracks.find((t) => t.languageCode.toLowerCase() === lowerLang && !t.isTranslatable);
1284
- if (!selectedTrack) selectedTrack = tracks.find((t) => t.languageCode.toLowerCase() === lowerLang);
1285
- if (!selectedTrack) selectedTrack = tracks.find((t) => t.languageCode.toLowerCase().includes(lowerLang));
2347
+ selectedTrack = tracks.find((t) => t.vssId === `.${lowerLang}`) || tracks.find((t) => t.vssId === `a.${lowerLang}`) || tracks.find((t) => t.languageCode.toLowerCase() === lowerLang && !t.isTranslatable) || tracks.find((t) => t.languageCode.toLowerCase() === lowerLang) || tracks.find((t) => t.languageCode.toLowerCase().includes(lowerLang));
1286
2348
  }
1287
2349
  if (!selectedTrack) {
1288
2350
  selectedTrack = tracks.find((t) => t.languageCode.toLowerCase().startsWith("en") && !t.isTranslatable);
@@ -1296,9 +2358,10 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
1296
2358
  return [];
1297
2359
  }
1298
2360
  console.log(`[Subtitles] Selected track: ${selectedTrack.languageCode} (${selectedTrack.isTranslatable ? "ASR/Auto" : "Manual"})`);
1299
- const urlWithFormat = selectedTrack.baseUrl.includes("&fmt=") ? selectedTrack.baseUrl : `${selectedTrack.baseUrl}&fmt=srv3`;
2361
+ let urlWithFormat = selectedTrack.baseUrl.replace(/&fmt=[^&]+/, "");
2362
+ urlWithFormat += "&fmt=json3";
1300
2363
  console.log(`[Subtitles] Fetching transcript from: ${urlWithFormat.substring(0, 100)}...`);
1301
- const transcriptResponse = await fetch(urlWithFormat, {
2364
+ const transcriptResponse = await client.fetch(urlWithFormat, {
1302
2365
  headers: {
1303
2366
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
1304
2367
  "Referer": "https://www.youtube.com/",
@@ -1307,21 +2370,31 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
1307
2370
  });
1308
2371
  const transcriptData = await transcriptResponse.text();
1309
2372
  let transcripts = [];
1310
- if (transcriptData.trim().startsWith("<?xml") || transcriptData.includes("<transcript>") || transcriptData.includes("<text start=")) {
1311
- transcripts = parseXmlTranscriptRegex(transcriptData);
1312
- } else {
1313
- try {
1314
- const json = JSON.parse(transcriptData);
1315
- if (json.events) {
1316
- transcripts = json.events.map((e) => ({
1317
- start: e.tStartMs / 1e3,
1318
- duration: e.dDurationMs / 1e3,
1319
- text: (e.segs ? e.segs.map((s) => s.utf8).join("") : "").replace(/\n/g, " ").trim()
1320
- })).filter((s) => s.text);
1321
- }
1322
- } catch (e) {
1323
- console.warn("[Subtitles] Failed to parse transcript string as JSON/XML.");
2373
+ try {
2374
+ const json = JSON.parse(transcriptData);
2375
+ if (json.events) {
2376
+ transcripts = json.events.map((e) => {
2377
+ if (!e) return null;
2378
+ const rawText = e.segs ? e.segs.map((s) => s?.utf8 || "").join("") : "";
2379
+ const cleanText = decodeHtmlEntities(rawText.replace(/<[^>]+>/g, "")).replace(/\n/g, " ").trim();
2380
+ return {
2381
+ start: (e.tStartMs || 0) / 1e3,
2382
+ duration: (e.dDurationMs || 0) / 1e3,
2383
+ text: cleanText
2384
+ };
2385
+ }).filter((s) => s && s.text);
1324
2386
  }
2387
+ } catch (jsonError) {
2388
+ console.warn("[Subtitles] Failed to parse transcript string as JSON3. Falling back to XML regex.", jsonError);
2389
+ const xmlUrl = selectedTrack.baseUrl.replace(/&fmt=[^&]+/, "") + "&fmt=srv3";
2390
+ const xmlResponse = await client.fetch(xmlUrl, {
2391
+ headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" }
2392
+ });
2393
+ const xmlData = await xmlResponse.text();
2394
+ transcripts = parseXmlTranscriptRegex(xmlData);
2395
+ }
2396
+ if (client.cache && transcripts.length > 0) {
2397
+ await client.cache.set(cacheKey, transcripts);
1325
2398
  }
1326
2399
  console.log(`[Subtitles] Retrieved ${transcripts.length} segments.`);
1327
2400
  return transcripts;
@@ -1330,19 +2403,46 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
1330
2403
  return [];
1331
2404
  }
1332
2405
  }
2406
+
2407
+ // src/index.ts
2408
+ async function searchYouTube(query, options) {
2409
+ const client = new Client(options);
2410
+ return client.search(query, options);
2411
+ }
2412
+ async function getVideoPlayback(videoId, options) {
2413
+ const client = new Client(options);
2414
+ return client.getVideo(videoId);
2415
+ }
1333
2416
  export {
1334
2417
  Base,
2418
+ BaseChannel,
1335
2419
  BaseVideo,
2420
+ Caption,
2421
+ CaptionLanguage,
2422
+ Channel,
1336
2423
  ChannelCompact,
2424
+ ChannelLive,
2425
+ ChannelPlaylists,
2426
+ ChannelPosts,
2427
+ ChannelShorts,
2428
+ ChannelVideos,
2429
+ Chat,
1337
2430
  Client,
2431
+ Comment,
2432
+ CommentReplies,
1338
2433
  Continuable,
2434
+ LiveVideo,
2435
+ MixPlaylist,
1339
2436
  Playlist,
1340
2437
  PlaylistCompact,
1341
2438
  PlaylistVideos,
1342
2439
  SearchResult,
1343
2440
  Thumbnails,
1344
2441
  Video,
2442
+ VideoCaptions,
2443
+ VideoComments,
1345
2444
  VideoCompact,
2445
+ VideoRelated,
1346
2446
  createCommentsApiRequestOptions,
1347
2447
  extractPlayerResponse,
1348
2448
  extractVideoEntries,
@@ -1353,7 +2453,12 @@ export {
1353
2453
  findContinuationToken,
1354
2454
  getInnerTubeConfig,
1355
2455
  getSApiSidHash,
1356
- getSapisidFromCookie,
2456
+ getSapisidFromCookieString,
2457
+ getVideoPlayback,
1357
2458
  parseXmlTranscriptRegex,
1358
- scrapeTasteData
2459
+ scrapeTasteData,
2460
+ searchYouTube,
2461
+ toPlainText,
2462
+ toSRT,
2463
+ toVTT
1359
2464
  };