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