tubezero 1.0.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 ADDED
@@ -0,0 +1,1410 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Base: () => Base,
24
+ BaseVideo: () => BaseVideo,
25
+ ChannelCompact: () => ChannelCompact,
26
+ Client: () => Client,
27
+ Continuable: () => Continuable,
28
+ Playlist: () => Playlist,
29
+ PlaylistCompact: () => PlaylistCompact,
30
+ PlaylistVideos: () => PlaylistVideos,
31
+ SearchResult: () => SearchResult,
32
+ Thumbnails: () => Thumbnails,
33
+ Video: () => Video,
34
+ VideoCompact: () => VideoCompact,
35
+ createCommentsApiRequestOptions: () => createCommentsApiRequestOptions,
36
+ extractPlayerResponse: () => extractPlayerResponse,
37
+ extractVideoEntries: () => extractVideoEntries,
38
+ fetchCommentsFromYouTube: () => fetchCommentsFromYouTube,
39
+ fetchInnerTubeFeed: () => fetchInnerTubeFeed,
40
+ fetchSubtitlesFromYouTube: () => fetchSubtitlesFromYouTube,
41
+ fetchYtInitialData: () => fetchYtInitialData,
42
+ findContinuationToken: () => findContinuationToken,
43
+ getInnerTubeConfig: () => getInnerTubeConfig,
44
+ getSApiSidHash: () => getSApiSidHash,
45
+ getSapisidFromCookie: () => getSapisidFromCookie,
46
+ parseXmlTranscriptRegex: () => parseXmlTranscriptRegex,
47
+ scrapeTasteData: () => scrapeTasteData
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
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
+ // src/base.ts
409
+ var Base = class {
410
+ constructor(client) {
411
+ this.client = client;
412
+ }
413
+ client;
414
+ };
415
+
416
+ // src/continuable.ts
417
+ var Continuable = class extends Base {
418
+ items = [];
419
+ continuation = void 0;
420
+ iteratorIndex = 0;
421
+ async next(count) {
422
+ const newItems = [];
423
+ if (count === void 0) {
424
+ if (this.iteratorIndex < this.items.length) {
425
+ const unread = this.items.slice(this.iteratorIndex);
426
+ this.iteratorIndex = this.items.length;
427
+ return unread;
428
+ }
429
+ if (this.continuation === null) {
430
+ return [];
431
+ }
432
+ const result = await this.fetch();
433
+ this.items.push(...result.items);
434
+ this.continuation = result.continuation ?? null;
435
+ newItems.push(...result.items);
436
+ this.iteratorIndex = this.items.length;
437
+ } else {
438
+ let emptyPageCount = 0;
439
+ while (newItems.length < count) {
440
+ if (this.iteratorIndex < this.items.length) {
441
+ const take = Math.min(count - newItems.length, this.items.length - this.iteratorIndex);
442
+ newItems.push(...this.items.slice(this.iteratorIndex, this.iteratorIndex + take));
443
+ this.iteratorIndex += take;
444
+ }
445
+ if (newItems.length >= count || this.continuation === null) {
446
+ break;
447
+ }
448
+ const result = await this.fetch();
449
+ this.items.push(...result.items);
450
+ this.continuation = result.continuation ?? null;
451
+ if (result.items.length === 0) {
452
+ emptyPageCount++;
453
+ if (this.continuation === null || emptyPageCount >= 3) {
454
+ break;
455
+ }
456
+ } else {
457
+ emptyPageCount = 0;
458
+ }
459
+ }
460
+ }
461
+ return newItems;
462
+ }
463
+ };
464
+
465
+ // 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
+ }
484
+ }
485
+ return best;
486
+ }
487
+ };
488
+
489
+ // src/video-compact.ts
490
+ var VideoCompact = class extends Base {
491
+ id;
492
+ title;
493
+ thumbnails;
494
+ duration;
495
+ isLive;
496
+ channel;
497
+ viewCount;
498
+ publishedAt;
499
+ constructor(client, data) {
500
+ super(client);
501
+ let videoId = "";
502
+ let titleText = "";
503
+ let durationSec = null;
504
+ let isLive = false;
505
+ let viewCountNum = null;
506
+ let pubAt = null;
507
+ let thumbnails = [];
508
+ let channelObj = void 0;
509
+ if (data.videoId) {
510
+ videoId = data.videoId;
511
+ 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;
526
+ }
527
+ const byline = data.shortBylineText || data.longBylineText || data.ownerText;
528
+ if (byline && byline.runs && byline.runs[0]) {
529
+ const run = byline.runs[0];
530
+ channelObj = {
531
+ 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
534
+ };
535
+ }
536
+ } else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
537
+ const model = data.lockupViewModel;
538
+ videoId = model.contentId;
539
+ const meta = model.metadata?.lockupMetadataViewModel;
540
+ titleText = meta?.title?.content || "";
541
+ const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
542
+ let cName = "";
543
+ let vCount = null;
544
+ let pAt = null;
545
+ for (const row of rows) {
546
+ for (const part of row.metadataParts || []) {
547
+ 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 {
554
+ cName = text;
555
+ }
556
+ }
557
+ }
558
+ if (cName) {
559
+ channelObj = { name: cName.replace(/•/g, "").trim() };
560
+ }
561
+ viewCountNum = vCount;
562
+ pubAt = pAt;
563
+ }
564
+ this.id = videoId;
565
+ this.title = titleText;
566
+ this.thumbnails = new Thumbnails(thumbnails);
567
+ this.duration = durationSec;
568
+ this.isLive = isLive;
569
+ this.channel = channelObj;
570
+ this.viewCount = viewCountNum;
571
+ this.publishedAt = pubAt;
572
+ }
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;
579
+ }
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;
592
+ }
593
+ };
594
+
595
+ // src/playlist-compact.ts
596
+ var PlaylistCompact = class extends Base {
597
+ id;
598
+ title;
599
+ thumbnails;
600
+ videoCount;
601
+ channel;
602
+ constructor(client, data) {
603
+ 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
+ }
629
+ }
630
+ this.id = playlistId;
631
+ this.title = titleText;
632
+ this.thumbnails = new Thumbnails(thumbnails);
633
+ this.videoCount = videoCountNum;
634
+ this.channel = channelObj;
635
+ }
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;
658
+ }
659
+ }
660
+ this.id = channelId;
661
+ this.name = nameText;
662
+ this.thumbnails = new Thumbnails(thumbnails);
663
+ this.subscriberCount = subCount;
664
+ }
665
+ };
666
+
667
+ // src/search-result.ts
668
+ var SearchResult = class _SearchResult extends Continuable {
669
+ constructor(client, initialData) {
670
+ super(client);
671
+ const { items, continuation } = _SearchResult.parseData(client, initialData);
672
+ this.items = items;
673
+ this.continuation = continuation;
674
+ }
675
+ async fetch() {
676
+ if (!this.continuation) {
677
+ return { items: [], continuation: null };
678
+ }
679
+ const data = await this.client.request("search", {
680
+ continuation: this.continuation
681
+ });
682
+ return _SearchResult.parseData(this.client, data);
683
+ }
684
+ static parseData(client, data) {
685
+ const items = [];
686
+ let continuation = null;
687
+ function traverse(obj) {
688
+ if (!obj || typeof obj !== "object") return;
689
+ if (Array.isArray(obj)) {
690
+ for (const item of obj) traverse(item);
691
+ 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));
695
+ return;
696
+ }
697
+ if (obj.playlistRenderer) {
698
+ items.push(new PlaylistCompact(client, obj.playlistRenderer));
699
+ return;
700
+ }
701
+ if (obj.channelRenderer) {
702
+ items.push(new ChannelCompact(client, obj.channelRenderer));
703
+ return;
704
+ }
705
+ if (obj.continuationCommand && obj.continuationCommand.token) {
706
+ continuation = obj.continuationCommand.token;
707
+ } else if (obj.continuationItemRenderer?.continuationEndpoint) {
708
+ continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
709
+ }
710
+ for (const key of Object.keys(obj)) {
711
+ traverse(obj[key]);
712
+ }
713
+ }
714
+ traverse(data);
715
+ return { items, continuation };
716
+ }
717
+ };
718
+
719
+ // src/base-video.ts
720
+ var BaseVideo = class extends Base {
721
+ id;
722
+ title;
723
+ description;
724
+ thumbnails;
725
+ viewCount;
726
+ publishDate;
727
+ channel;
728
+ isLive;
729
+ constructor(client, data) {
730
+ super(client);
731
+ 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);
739
+ }
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
+ };
753
+ }
754
+ };
755
+
756
+ // src/video.ts
757
+ var Video = class extends BaseVideo {
758
+ // We will expand this with related videos, comments pagination, and chapters in Sprints 3 & 4.
759
+ constructor(client, data) {
760
+ super(client, data);
761
+ }
762
+ };
763
+
764
+ // src/playlist.ts
765
+ var PlaylistVideos = class _PlaylistVideos extends Continuable {
766
+ constructor(client, initialData) {
767
+ super(client);
768
+ const { items, continuation } = _PlaylistVideos.parseData(client, initialData);
769
+ this.items = items;
770
+ this.continuation = continuation;
771
+ }
772
+ async fetch() {
773
+ if (!this.continuation) {
774
+ return { items: [], continuation: null };
775
+ }
776
+ const data = await this.client.request("browse", {
777
+ continuation: this.continuation
778
+ });
779
+ return _PlaylistVideos.parseData(this.client, data);
780
+ }
781
+ static parseData(client, data) {
782
+ const items = [];
783
+ let continuation = null;
784
+ function traverse(obj) {
785
+ if (!obj || typeof obj !== "object") return;
786
+ if (Array.isArray(obj)) {
787
+ for (const item of obj) traverse(item);
788
+ return;
789
+ }
790
+ if (obj.playlistVideoRenderer) {
791
+ items.push(new VideoCompact(client, obj.playlistVideoRenderer));
792
+ return;
793
+ }
794
+ if (obj.continuationCommand && obj.continuationCommand.token) {
795
+ continuation = obj.continuationCommand.token;
796
+ } else if (obj.continuationItemRenderer && obj.continuationItemRenderer.continuationEndpoint) {
797
+ continuation = obj.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
798
+ }
799
+ for (const key of Object.keys(obj)) {
800
+ traverse(obj[key]);
801
+ }
802
+ }
803
+ traverse(data);
804
+ return { items, continuation };
805
+ }
806
+ };
807
+ var Playlist = class extends Base {
808
+ id;
809
+ title;
810
+ videoCount;
811
+ viewCount;
812
+ lastUpdated;
813
+ channel;
814
+ videos;
815
+ constructor(client, data) {
816
+ super(client);
817
+ this.id = "";
818
+ this.title = "";
819
+ this.videoCount = 0;
820
+ this.viewCount = 0;
821
+ this.lastUpdated = "";
822
+ this.parse(data);
823
+ this.videos = new PlaylistVideos(client, data);
824
+ }
825
+ parse(data) {
826
+ let header = data.header?.playlistHeaderRenderer;
827
+ let pageHeader = data.header?.pageHeaderRenderer?.content?.pageHeaderViewModel;
828
+ if (header) {
829
+ this.id = header.playlistId || "";
830
+ this.title = header.title?.simpleText || header.title?.runs?.[0]?.text || "";
831
+ const numVideosRaw = header.numVideosText?.runs?.[0]?.text || header.numVideosText?.simpleText || "";
832
+ this.videoCount = parseInt(numVideosRaw.replace(/[^0-9]/g, "") || "0", 10);
833
+ const viewCountRaw = header.viewCountText?.simpleText || header.viewCountText?.runs?.[0]?.text || "";
834
+ this.viewCount = parseInt(viewCountRaw.replace(/[^0-9]/g, "") || "0", 10);
835
+ const owner = header.ownerText?.runs?.[0];
836
+ if (owner) {
837
+ this.channel = {
838
+ id: owner.navigationEndpoint?.browseEndpoint?.browseId || void 0,
839
+ name: owner.text
840
+ };
841
+ }
842
+ } else if (pageHeader) {
843
+ this.id = data.microformat?.microformatDataRenderer?.urlCanonical?.split("list=")?.[1]?.split("&")?.[0] || data.responseContext?.serviceTrackingParams?.find((p) => p.params?.find((pp) => pp.key === "browse_id"))?.params?.find((pp) => pp.key === "browse_id")?.value?.replace("VL", "") || "";
844
+ this.title = pageHeader.title?.dynamicTextViewModel?.text?.content || "";
845
+ const rows = pageHeader.metadata?.contentMetadataViewModel?.metadataRows || [];
846
+ for (const row of rows) {
847
+ for (const part of row.metadataParts || []) {
848
+ if (part.avatarStack?.avatarStackViewModel?.text?.content) {
849
+ const rawName = part.avatarStack.avatarStackViewModel.text.content;
850
+ this.channel = { name: rawName.replace(/^by\s+/i, "").trim() };
851
+ const runs = part.avatarStack.avatarStackViewModel.text.commandRuns || [];
852
+ if (runs[0]?.onTap?.innertubeCommand?.browseEndpoint?.browseId) {
853
+ this.channel.id = runs[0].onTap.innertubeCommand.browseEndpoint.browseId;
854
+ }
855
+ }
856
+ if (part.text?.content) {
857
+ const text = part.text.content;
858
+ if (text.includes("video")) {
859
+ this.videoCount = parseInt(text.replace(/[^0-9]/g, "") || "0", 10);
860
+ } else if (text.includes("view")) {
861
+ this.viewCount = parseInt(text.replace(/[^0-9]/g, "") || "0", 10);
862
+ }
863
+ }
864
+ }
865
+ }
866
+ }
867
+ }
868
+ };
869
+
870
+ // src/client.ts
871
+ function getFallbackClientVersion2() {
872
+ const d = /* @__PURE__ */ new Date();
873
+ d.setDate(d.getDate() - 2);
874
+ const yyyymmdd = d.toISOString().split("T")[0].replace(/-/g, "");
875
+ return `2.${yyyymmdd}.00.00`;
876
+ }
877
+ function getSapisidFromCookieString(cookieString) {
878
+ const match = cookieString.match(/__Secure-3PAPISID=([^;]+)/) || cookieString.match(/__Secure-1PAPISID=([^;]+)/) || cookieString.match(/SAPISID=([^;]+)/);
879
+ return match ? match[1] : null;
880
+ }
881
+ async function getSApiSidHash2(sapisid, origin = "https://www.youtube.com") {
882
+ if (!sapisid) return null;
883
+ try {
884
+ const timestamp = Math.floor(Date.now() / 1e3);
885
+ const input = `${timestamp} ${sapisid} ${origin}`;
886
+ const encoder = new TextEncoder();
887
+ const data = encoder.encode(input);
888
+ const buffer = await crypto.subtle.digest("SHA-1", data);
889
+ const hash = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
890
+ return `${timestamp}_${hash}`;
891
+ } catch (e) {
892
+ console.error("[Client] Failed to generate SAPISIDHASH", e);
893
+ return null;
894
+ }
895
+ }
896
+ var Client = class {
897
+ apiKey = null;
898
+ clientVersion = "";
899
+ idToken = null;
900
+ cookie = "";
901
+ constructor(options = {}) {
902
+ this.cookie = options.cookie !== void 0 ? options.cookie : "";
903
+ let resolvedApiKey = options.apiKey !== void 0 ? options.apiKey : null;
904
+ let resolvedClientVersion = options.clientVersion !== void 0 ? options.clientVersion : null;
905
+ let resolvedIdToken = options.idToken !== void 0 ? options.idToken : null;
906
+ if (typeof window !== "undefined" && window.ytcfg) {
907
+ const ytcfg = window.ytcfg;
908
+ if (typeof ytcfg.get === "function") {
909
+ if (resolvedApiKey === null) resolvedApiKey = ytcfg.get("INNERTUBE_API_KEY") || null;
910
+ if (resolvedClientVersion === null) resolvedClientVersion = ytcfg.get("INNERTUBE_CLIENT_VERSION") || null;
911
+ if (resolvedIdToken === null) resolvedIdToken = ytcfg.get("ID_TOKEN") || null;
912
+ } else {
913
+ if (resolvedApiKey === null) resolvedApiKey = ytcfg.INNERTUBE_API_KEY || null;
914
+ if (resolvedClientVersion === null) resolvedClientVersion = ytcfg.INNERTUBE_CLIENT_VERSION || null;
915
+ if (resolvedIdToken === null) resolvedIdToken = ytcfg.ID_TOKEN || null;
916
+ }
917
+ }
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;
936
+ }
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`;
947
+ const headers = {
948
+ "Content-Type": "application/json",
949
+ "X-Youtube-Client-Name": "1",
950
+ "X-Youtube-Client-Version": this.clientVersion
951
+ };
952
+ if (this.idToken) {
953
+ headers["X-Youtube-Identity-Token"] = this.idToken;
954
+ }
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
+ }
964
+ }
965
+ }
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, {
981
+ method: "POST",
982
+ headers,
983
+ body: JSON.stringify(requestBody),
984
+ credentials: "include"
985
+ });
986
+ if (!response.ok) {
987
+ throw new Error(`InnerTube request failed with status: ${response.status}`);
988
+ }
989
+ return response.json();
990
+ }
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";
1002
+ }
1003
+ const data = await this.request("search", payload);
1004
+ return new SearchResult(this, data);
1005
+ }
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);
1016
+ }
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);
1024
+ }
1025
+ };
1026
+
1027
+ // src/comments.ts
1028
+ function findValue(obj, path, defaultValue = void 0) {
1029
+ const parts = path.split(".");
1030
+ let current = obj;
1031
+ for (const part of parts) {
1032
+ if (current === null || typeof current !== "object" || !(part in current)) {
1033
+ return defaultValue;
1034
+ }
1035
+ current = current[part];
1036
+ }
1037
+ return current;
1038
+ }
1039
+ function createCommentsApiRequestOptions(continuationToken, clientVersion = "2.20240703.00.00") {
1040
+ return {
1041
+ headers: {
1042
+ "Content-Type": "application/json",
1043
+ "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"
1044
+ },
1045
+ body: JSON.stringify({
1046
+ context: {
1047
+ client: {
1048
+ clientName: "WEB",
1049
+ clientVersion
1050
+ }
1051
+ },
1052
+ continuation: continuationToken
1053
+ })
1054
+ };
1055
+ }
1056
+ async function fetchCommentsFromYouTube(videoId, count = 50, injectedConfig = null) {
1057
+ let comments = [];
1058
+ let continuationToken = null;
1059
+ let fetchedCount = 0;
1060
+ const config = await getInnerTubeConfig(injectedConfig);
1061
+ const apiKey = config.apiKey;
1062
+ const clientVersion = config.clientVersion || "2.20240703.00.00";
1063
+ try {
1064
+ console.log(`[Comments] Fetching video page for ID: ${videoId}`);
1065
+ const videoPageUrl = `https://www.youtube.com/watch?v=${videoId}`;
1066
+ const pageResponse = await fetch(videoPageUrl, {
1067
+ headers: {
1068
+ "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",
1069
+ "Accept-Language": "en-US,en;q=0.9"
1070
+ }
1071
+ });
1072
+ const pageHtml = await pageResponse.text();
1073
+ const patterns = [
1074
+ /var ytInitialData\s*=\s*(\{.*?\});<\/script>/,
1075
+ /window\["ytInitialData"\]\s*=\s*(\{.*?\});/,
1076
+ /ytInitialData\s*=\s*(\{.*?\});/
1077
+ ];
1078
+ let ytInitialData = null;
1079
+ for (const regex of patterns) {
1080
+ const match = pageHtml.match(regex);
1081
+ if (match && match[1]) {
1082
+ ytInitialData = JSON.parse(match[1]);
1083
+ break;
1084
+ }
1085
+ }
1086
+ if (ytInitialData) {
1087
+ const contents = findValue(ytInitialData, "contents.twoColumnWatchNextResults.results.results.contents");
1088
+ if (Array.isArray(contents)) {
1089
+ for (const item of contents) {
1090
+ if (item.itemSectionRenderer) {
1091
+ const sectionItems = item.itemSectionRenderer.contents || [];
1092
+ for (const sItem of sectionItems) {
1093
+ let token = findValue(sItem, "continuationItemRenderer.continuationEndpoint.continuationCommand.token");
1094
+ if (token) {
1095
+ continuationToken = token;
1096
+ break;
1097
+ }
1098
+ token = findValue(sItem, "commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.simpleText.runs.0.navigationEndpoint.continuationCommand.token");
1099
+ if (token) {
1100
+ continuationToken = token;
1101
+ break;
1102
+ }
1103
+ token = findValue(sItem, "commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.content.runs.0.navigationEndpoint.continuationCommand.token");
1104
+ if (token) {
1105
+ continuationToken = token;
1106
+ break;
1107
+ }
1108
+ }
1109
+ }
1110
+ if (continuationToken) break;
1111
+ }
1112
+ }
1113
+ if (!continuationToken) {
1114
+ const panels = findValue(ytInitialData, "engagementPanels");
1115
+ if (Array.isArray(panels)) {
1116
+ for (const panel of panels) {
1117
+ const token = findValue(panel, "engagementPanelSectionListRenderer.content.sectionListRenderer.contents.0.itemSectionRenderer.contents.0.continuationItemRenderer.continuationEndpoint.continuationCommand.token");
1118
+ if (token) {
1119
+ continuationToken = token;
1120
+ break;
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ if (!continuationToken) {
1126
+ continuationToken = findValue(
1127
+ ytInitialData,
1128
+ "contents.twoColumnWatchNextResults.results.results.contents.2.itemSectionRenderer.contents.0.commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.simpleText.runs.0.navigationEndpoint.continuationCommand.token"
1129
+ );
1130
+ }
1131
+ if (!continuationToken) {
1132
+ continuationToken = findValue(
1133
+ ytInitialData,
1134
+ "contents.twoColumnWatchNextResults.results.results.contents.2.itemSectionRenderer.contents.0.commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.content.runs.0.navigationEndpoint.continuationCommand.token"
1135
+ );
1136
+ }
1137
+ if (!continuationToken) {
1138
+ continuationToken = findValue(ytInitialData, "contents.twoColumnWatchNextResults.results.results.contents.3.itemSectionRenderer.contents.0.commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.simpleText.runs.0.navigationEndpoint.continuationCommand.token");
1139
+ }
1140
+ }
1141
+ if (!continuationToken) {
1142
+ console.error(`[Comments] Failed to extract comments continuation token for video ${videoId}`);
1143
+ return [];
1144
+ }
1145
+ while (continuationToken && fetchedCount < count) {
1146
+ console.log(`[Comments] Fetching comment batch, total comments so far: ${fetchedCount}`);
1147
+ const options = createCommentsApiRequestOptions(continuationToken, clientVersion);
1148
+ const headers = { ...options.headers };
1149
+ const sapisid = getSapisidFromCookie();
1150
+ if (sapisid) {
1151
+ const authHash = await getSApiSidHash(sapisid);
1152
+ if (authHash) {
1153
+ headers["Authorization"] = `SAPISIDHASH ${authHash}`;
1154
+ }
1155
+ }
1156
+ const url = apiKey ? `https://www.youtube.com/youtubei/v1/next?key=${apiKey}&prettyPrint=false` : "https://www.youtube.com/youtubei/v1/next?prettyPrint=false";
1157
+ const response = await fetch(url, {
1158
+ method: "POST",
1159
+ headers,
1160
+ body: options.body,
1161
+ credentials: "include"
1162
+ });
1163
+ if (!response.ok) {
1164
+ console.error(`[Comments] InnerTube request failed with status: ${response.status}`);
1165
+ break;
1166
+ }
1167
+ const apiResponse = await response.json();
1168
+ const newComments = [];
1169
+ const mutations = findValue(apiResponse, "frameworkUpdates.entityBatchUpdate.mutations", []);
1170
+ for (const mutation of mutations) {
1171
+ const payload = findValue(mutation, "payload.commentEntityPayload");
1172
+ if (payload) {
1173
+ let author = findValue(payload, "author.displayName");
1174
+ let text = findValue(payload, "properties.content.content");
1175
+ let publishedTime = findValue(payload, "properties.publishedTime");
1176
+ let commentId = findValue(payload, "properties.commentId");
1177
+ let likesRaw = findValue(payload, "toolbar.likeCountNotliked") || findValue(payload, "toolbar.likeCountLiked");
1178
+ if (!author) author = findValue(payload, "authorText.simpleText") || "Anonymous";
1179
+ if (!text) {
1180
+ const runs = findValue(payload, "contentText.runs", []);
1181
+ if (runs.length > 0) {
1182
+ text = runs.map((r) => r.text).join("");
1183
+ } else {
1184
+ text = findValue(payload, "contentText.simpleText", "");
1185
+ }
1186
+ }
1187
+ if (!publishedTime) publishedTime = findValue(payload, "publishedTimeText.simpleText", "");
1188
+ if (!commentId) commentId = findValue(payload, "commentId", "");
1189
+ if (!likesRaw) likesRaw = findValue(payload, "voteCount.simpleText", "0");
1190
+ let likeCount = 0;
1191
+ if (likesRaw) {
1192
+ const likesString = String(likesRaw).trim();
1193
+ if (likesString.endsWith("K")) {
1194
+ likeCount = Math.round(parseFloat(likesString.slice(0, -1)) * 1e3);
1195
+ } else if (likesString.endsWith("M")) {
1196
+ likeCount = Math.round(parseFloat(likesString.slice(0, -1)) * 1e6);
1197
+ } else {
1198
+ likeCount = parseInt(likesString.replace(/\D/g, ""), 10) || 0;
1199
+ }
1200
+ }
1201
+ if (text) {
1202
+ newComments.push({ author, text, publishedTime, likeCount, commentId });
1203
+ }
1204
+ }
1205
+ }
1206
+ comments = comments.concat(newComments);
1207
+ fetchedCount = comments.length;
1208
+ continuationToken = findValue(
1209
+ apiResponse,
1210
+ "onResponseReceivedEndpoints.0.appendContinuationItemsAction.continuationItems.0.nextContinuationData.continuation"
1211
+ );
1212
+ if (!continuationToken) {
1213
+ continuationToken = findValue(
1214
+ apiResponse,
1215
+ "onResponseReceivedEndpoints.0.reloadContinuationItemsCommand.continuationItems.0.nextContinuationData.continuation"
1216
+ );
1217
+ }
1218
+ if (!continuationToken && fetchedCount < count) {
1219
+ console.log(`[Comments] No more continuation tokens. Finished fetching.`);
1220
+ break;
1221
+ }
1222
+ }
1223
+ console.log(`[Comments] Successfully fetched ${comments.length} comments.`);
1224
+ return comments.slice(0, count);
1225
+ } catch (error) {
1226
+ console.error(`[Comments] Error fetching comments for ${videoId}:`, error);
1227
+ return [];
1228
+ }
1229
+ }
1230
+
1231
+ // src/subtitles.ts
1232
+ function extractYtInitialData(html) {
1233
+ try {
1234
+ const match = html.match(/var ytInitialData = (.*?);<\/script>/);
1235
+ if (!match || !match[1]) {
1236
+ const fallbackMatch = html.match(/ytInitialData\s*=\s*({.+?});/);
1237
+ if (!fallbackMatch || !fallbackMatch[1]) {
1238
+ return null;
1239
+ }
1240
+ return JSON.parse(fallbackMatch[1]);
1241
+ }
1242
+ return JSON.parse(match[1]);
1243
+ } catch (e) {
1244
+ return null;
1245
+ }
1246
+ }
1247
+ function findPlayerResponseInObject(obj) {
1248
+ if (typeof obj !== "object" || obj === null) return null;
1249
+ if (obj.captions?.playerCaptionsTracklistRenderer) return obj;
1250
+ for (const key in obj) {
1251
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1252
+ const value = obj[key];
1253
+ if (typeof value === "object" && value !== null) {
1254
+ const result = findPlayerResponseInObject(value);
1255
+ if (result) return result;
1256
+ }
1257
+ }
1258
+ }
1259
+ return null;
1260
+ }
1261
+ function extractPlayerResponse(html) {
1262
+ let playerResponse = null;
1263
+ try {
1264
+ const playerResponseMatch = html.match(/ytInitialPlayerResponse\s*=\s*({.+?});/);
1265
+ if (playerResponseMatch && playerResponseMatch[1]) {
1266
+ playerResponse = JSON.parse(playerResponseMatch[1]);
1267
+ if (playerResponse && playerResponse.captions?.playerCaptionsTracklistRenderer) {
1268
+ return playerResponse;
1269
+ }
1270
+ }
1271
+ const windowResponseMatch = html.match(/window\["ytInitialPlayerResponse"\]\s*=\s*({.+?});/);
1272
+ if (windowResponseMatch && windowResponseMatch[1]) {
1273
+ playerResponse = JSON.parse(windowResponseMatch[1]);
1274
+ if (playerResponse && playerResponse.captions?.playerCaptionsTracklistRenderer) {
1275
+ return playerResponse;
1276
+ }
1277
+ }
1278
+ const ytInitialData = extractYtInitialData(html);
1279
+ if (ytInitialData) {
1280
+ const foundPlayerResponse = findPlayerResponseInObject(ytInitialData);
1281
+ if (foundPlayerResponse) return foundPlayerResponse;
1282
+ }
1283
+ } catch (e) {
1284
+ console.error("[Subtitles] Error extracting player response:", e);
1285
+ }
1286
+ return null;
1287
+ }
1288
+ function parseXmlTranscriptRegex(xmlText) {
1289
+ const segments = [];
1290
+ const regex = /<text start="([^"]+)" dur="([^"]+)"[^>]*>(.*?)<\/text>/g;
1291
+ let match;
1292
+ while ((match = regex.exec(xmlText)) !== null) {
1293
+ const start = parseFloat(match[1]);
1294
+ 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();
1296
+ if (text && !isNaN(start)) {
1297
+ segments.push({ start, duration, text });
1298
+ }
1299
+ }
1300
+ return segments;
1301
+ }
1302
+ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
1303
+ 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);
1315
+ if (!playerResponse || !playerResponse.captions || !playerResponse.captions.playerCaptionsTracklistRenderer) {
1316
+ console.warn(`[Subtitles] No captions tracklist found for video: ${videoId}`);
1317
+ return [];
1318
+ }
1319
+ const captionTracks = playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks || [];
1320
+ if (captionTracks.length === 0) {
1321
+ console.warn(`[Subtitles] Empty captionTracks array for video: ${videoId}`);
1322
+ return [];
1323
+ }
1324
+ const tracks = captionTracks.map((t) => ({
1325
+ baseUrl: t.baseUrl,
1326
+ languageCode: t.languageCode,
1327
+ name: t.name?.simpleText || t.name,
1328
+ isTranslatable: t.kind === "asr"
1329
+ }));
1330
+ let selectedTrack;
1331
+ if (language && language !== "auto") {
1332
+ 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));
1336
+ }
1337
+ if (!selectedTrack) {
1338
+ selectedTrack = tracks.find((t) => t.languageCode.toLowerCase().startsWith("en") && !t.isTranslatable);
1339
+ if (!selectedTrack) selectedTrack = tracks.find((t) => t.languageCode.toLowerCase().startsWith("en"));
1340
+ }
1341
+ if (!selectedTrack) {
1342
+ selectedTrack = tracks[0];
1343
+ }
1344
+ if (!selectedTrack) {
1345
+ console.warn("[Subtitles] No suitable caption track found");
1346
+ return [];
1347
+ }
1348
+ 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`;
1350
+ console.log(`[Subtitles] Fetching transcript from: ${urlWithFormat.substring(0, 100)}...`);
1351
+ const transcriptResponse = await fetch(urlWithFormat, {
1352
+ headers: {
1353
+ "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
+ "Referer": "https://www.youtube.com/",
1355
+ "Origin": "https://www.youtube.com"
1356
+ }
1357
+ });
1358
+ const transcriptData = await transcriptResponse.text();
1359
+ 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.");
1374
+ }
1375
+ }
1376
+ console.log(`[Subtitles] Retrieved ${transcripts.length} segments.`);
1377
+ return transcripts;
1378
+ } catch (error) {
1379
+ console.error(`[Subtitles] Error fetching subtitles for video ${videoId}:`, error);
1380
+ return [];
1381
+ }
1382
+ }
1383
+ // Annotate the CommonJS export names for ESM import in node:
1384
+ 0 && (module.exports = {
1385
+ Base,
1386
+ BaseVideo,
1387
+ ChannelCompact,
1388
+ Client,
1389
+ Continuable,
1390
+ Playlist,
1391
+ PlaylistCompact,
1392
+ PlaylistVideos,
1393
+ SearchResult,
1394
+ Thumbnails,
1395
+ Video,
1396
+ VideoCompact,
1397
+ createCommentsApiRequestOptions,
1398
+ extractPlayerResponse,
1399
+ extractVideoEntries,
1400
+ fetchCommentsFromYouTube,
1401
+ fetchInnerTubeFeed,
1402
+ fetchSubtitlesFromYouTube,
1403
+ fetchYtInitialData,
1404
+ findContinuationToken,
1405
+ getInnerTubeConfig,
1406
+ getSApiSidHash,
1407
+ getSapisidFromCookie,
1408
+ parseXmlTranscriptRegex,
1409
+ scrapeTasteData
1410
+ });