@aitofy/youtube 0.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.
@@ -0,0 +1,847 @@
1
+ // src/channel/get-videos.ts
2
+ import { XMLParser } from "fast-xml-parser";
3
+
4
+ // src/types.ts
5
+ var YouTubeToolsError = class extends Error {
6
+ constructor(message, code, statusCode) {
7
+ super(message);
8
+ this.code = code;
9
+ this.statusCode = statusCode;
10
+ this.name = "YouTubeToolsError";
11
+ }
12
+ };
13
+ var ErrorCodes = {
14
+ CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
15
+ VIDEO_NOT_FOUND: "VIDEO_NOT_FOUND",
16
+ TRANSCRIPT_NOT_AVAILABLE: "TRANSCRIPT_NOT_AVAILABLE",
17
+ RATE_LIMITED: "RATE_LIMITED",
18
+ PARSING_ERROR: "PARSING_ERROR",
19
+ NETWORK_ERROR: "NETWORK_ERROR"
20
+ };
21
+
22
+ // src/utils/fetcher.ts
23
+ var CACHE_TTL = 5 * 60 * 1e3;
24
+ function parseChannelId(input) {
25
+ if (/^UC[a-zA-Z0-9_-]{22}$/.test(input)) {
26
+ return { type: "id", value: input };
27
+ }
28
+ if (input.startsWith("@")) {
29
+ return { type: "handle", value: input };
30
+ }
31
+ const patterns = [
32
+ { regex: /youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})/, type: "id" },
33
+ { regex: /youtube\.com\/@([a-zA-Z0-9_-]+)/, type: "handle" },
34
+ { regex: /youtube\.com\/c\/([a-zA-Z0-9_-]+)/, type: "custom" },
35
+ { regex: /youtube\.com\/user\/([a-zA-Z0-9_-]+)/, type: "custom" }
36
+ ];
37
+ for (const { regex, type } of patterns) {
38
+ const match = input.match(regex);
39
+ if (match) {
40
+ return { type, value: type === "handle" ? `@${match[1]}` : match[1] };
41
+ }
42
+ }
43
+ return { type: "custom", value: input };
44
+ }
45
+
46
+ // src/channel/get-videos.ts
47
+ var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
48
+ async function getChannelVideos(options) {
49
+ const opts = typeof options === "string" ? { channel: options } : options;
50
+ const { channel, limit = 15, sortBy = "newest", contentType = "videos" } = opts;
51
+ const channelInfo = parseChannelId(channel);
52
+ if (limit <= 15 && sortBy === "newest" && contentType === "videos") {
53
+ try {
54
+ const channelId = await resolveChannelId(channelInfo);
55
+ return getVideosFromRSS(channelId, limit);
56
+ } catch {
57
+ }
58
+ }
59
+ return getVideosFromScraping(channelInfo, limit, sortBy, contentType);
60
+ }
61
+ async function getVideosFromRSS(channelId, limit) {
62
+ const url = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
63
+ const response = await fetch(url, {
64
+ headers: { "User-Agent": USER_AGENT }
65
+ });
66
+ if (!response.ok) {
67
+ throw new YouTubeToolsError(
68
+ `Failed to fetch RSS: ${response.status}`,
69
+ ErrorCodes.NETWORK_ERROR,
70
+ response.status
71
+ );
72
+ }
73
+ const xml = await response.text();
74
+ const parser = new XMLParser({
75
+ ignoreAttributes: false,
76
+ attributeNamePrefix: "@_"
77
+ });
78
+ const data = parser.parse(xml);
79
+ const entries = data?.feed?.entry || [];
80
+ const videos = [];
81
+ for (const entry of entries.slice(0, limit)) {
82
+ videos.push({
83
+ videoId: entry["yt:videoId"],
84
+ title: entry.title,
85
+ description: entry["media:group"]?.["media:description"] || "",
86
+ publishedAt: entry.published,
87
+ url: entry.link?.["@_href"] || `https://youtube.com/watch?v=${entry["yt:videoId"]}`,
88
+ thumbnails: {
89
+ default: entry["media:group"]?.["media:thumbnail"]?.["@_url"],
90
+ medium: entry["media:group"]?.["media:thumbnail"]?.["@_url"]?.replace(
91
+ "default",
92
+ "mqdefault"
93
+ ),
94
+ high: entry["media:group"]?.["media:thumbnail"]?.["@_url"]?.replace(
95
+ "default",
96
+ "hqdefault"
97
+ )
98
+ },
99
+ channelId: entry["yt:channelId"],
100
+ channelTitle: entry.author?.name
101
+ });
102
+ }
103
+ return videos;
104
+ }
105
+ async function getVideosFromScraping(channelInfo, limit, sortBy, contentType) {
106
+ let baseUrl;
107
+ if (channelInfo.type === "id") {
108
+ baseUrl = `https://www.youtube.com/channel/${channelInfo.value}`;
109
+ } else if (channelInfo.type === "handle") {
110
+ baseUrl = `https://www.youtube.com/${channelInfo.value}`;
111
+ } else {
112
+ baseUrl = `https://www.youtube.com/c/${channelInfo.value}`;
113
+ }
114
+ const contentPath = contentType === "shorts" ? "/shorts" : contentType === "streams" ? "/streams" : "/videos";
115
+ const url = baseUrl + contentPath;
116
+ const sortParam = sortBy === "popular" ? "?sort=p" : sortBy === "oldest" ? "?sort=da" : "";
117
+ const fullUrl = url + sortParam;
118
+ const response = await fetch(fullUrl, {
119
+ headers: {
120
+ "User-Agent": USER_AGENT,
121
+ "Accept-Language": "en-US,en;q=0.9"
122
+ }
123
+ });
124
+ if (!response.ok) {
125
+ throw new YouTubeToolsError(
126
+ `Failed to fetch channel page: ${response.status}`,
127
+ ErrorCodes.NETWORK_ERROR,
128
+ response.status
129
+ );
130
+ }
131
+ const html = await response.text();
132
+ return extractVideosFromHTML(html, limit);
133
+ }
134
+ function extractVideosFromHTML(html, limit) {
135
+ const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
136
+ if (!dataMatch) {
137
+ return [];
138
+ }
139
+ try {
140
+ const data = JSON.parse(dataMatch[1]);
141
+ const videos = [];
142
+ const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
143
+ for (const tab of tabs) {
144
+ const tabContent = tab?.tabRenderer?.content;
145
+ if (!tabContent) continue;
146
+ const items = tabContent?.richGridRenderer?.contents || tabContent?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents || [];
147
+ for (const item of items) {
148
+ if (videos.length >= limit) break;
149
+ const videoRenderer = item?.richItemRenderer?.content?.videoRenderer || item?.gridVideoRenderer || item?.videoRenderer;
150
+ if (videoRenderer?.videoId) {
151
+ videos.push(parseVideoRenderer(videoRenderer));
152
+ }
153
+ }
154
+ if (videos.length > 0) break;
155
+ }
156
+ return videos;
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
161
+ function parseVideoRenderer(renderer) {
162
+ const videoId = renderer.videoId;
163
+ return {
164
+ videoId,
165
+ title: renderer.title?.runs?.[0]?.text || renderer.title?.simpleText || "",
166
+ description: renderer.descriptionSnippet?.runs?.map((r) => r.text).join("") || "",
167
+ publishedAt: renderer.publishedTimeText?.simpleText || "",
168
+ duration: renderer.lengthText?.simpleText || "",
169
+ durationSeconds: parseDuration(renderer.lengthText?.simpleText),
170
+ viewCount: parseViewCount(renderer.viewCountText?.simpleText),
171
+ url: `https://youtube.com/watch?v=${videoId}`,
172
+ thumbnails: {
173
+ default: `https://i.ytimg.com/vi/${videoId}/default.jpg`,
174
+ medium: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
175
+ high: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
176
+ maxres: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
177
+ }
178
+ };
179
+ }
180
+ function parseDuration(duration) {
181
+ if (!duration) return void 0;
182
+ const parts = duration.split(":").map(Number);
183
+ if (parts.length === 3) {
184
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
185
+ } else if (parts.length === 2) {
186
+ return parts[0] * 60 + parts[1];
187
+ }
188
+ return parts[0];
189
+ }
190
+ function parseViewCount(viewText) {
191
+ if (!viewText) return void 0;
192
+ const match = viewText.match(/[\d,]+/);
193
+ if (match) {
194
+ return parseInt(match[0].replace(/,/g, ""), 10);
195
+ }
196
+ return void 0;
197
+ }
198
+ async function resolveChannelId(channelInfo) {
199
+ if (channelInfo.type === "id") {
200
+ return channelInfo.value;
201
+ }
202
+ let url;
203
+ if (channelInfo.type === "handle") {
204
+ url = `https://www.youtube.com/${channelInfo.value}`;
205
+ } else {
206
+ url = `https://www.youtube.com/c/${channelInfo.value}`;
207
+ }
208
+ const response = await fetch(url, {
209
+ headers: { "User-Agent": USER_AGENT }
210
+ });
211
+ const html = await response.text();
212
+ const match = html.match(/"channelId":"(UC[a-zA-Z0-9_-]{22})"/);
213
+ if (match) {
214
+ return match[1];
215
+ }
216
+ throw new YouTubeToolsError(
217
+ `Could not resolve channel ID for: ${channelInfo.value}`,
218
+ ErrorCodes.CHANNEL_NOT_FOUND
219
+ );
220
+ }
221
+
222
+ // src/channel/get-info.ts
223
+ var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
224
+ async function getChannelInfo(channel) {
225
+ const channelInfo = parseChannelId(channel);
226
+ let url;
227
+ if (channelInfo.type === "id") {
228
+ url = `https://www.youtube.com/channel/${channelInfo.value}`;
229
+ } else if (channelInfo.type === "handle") {
230
+ url = `https://www.youtube.com/${channelInfo.value}`;
231
+ } else {
232
+ url = `https://www.youtube.com/c/${channelInfo.value}`;
233
+ }
234
+ const response = await fetch(url, {
235
+ headers: {
236
+ "User-Agent": USER_AGENT2,
237
+ "Accept-Language": "en-US,en;q=0.9"
238
+ }
239
+ });
240
+ if (!response.ok) {
241
+ throw new YouTubeToolsError(
242
+ `Channel not found: ${channel}`,
243
+ ErrorCodes.CHANNEL_NOT_FOUND,
244
+ response.status
245
+ );
246
+ }
247
+ const html = await response.text();
248
+ return extractChannelInfo(html);
249
+ }
250
+ function extractChannelInfo(html) {
251
+ const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
252
+ if (!dataMatch) {
253
+ throw new YouTubeToolsError(
254
+ "Could not parse channel data",
255
+ ErrorCodes.PARSING_ERROR
256
+ );
257
+ }
258
+ try {
259
+ const data = JSON.parse(dataMatch[1]);
260
+ const metadata = data?.metadata?.channelMetadataRenderer || {};
261
+ const header = data?.header?.c4TabbedHeaderRenderer || data?.header?.pageHeaderRenderer?.pageHeaderViewModel || {};
262
+ const channelIdMatch = html.match(/"channelId":"(UC[a-zA-Z0-9_-]{22})"/);
263
+ const channelId = channelIdMatch?.[1] || metadata.externalId || "";
264
+ let subscriberCount;
265
+ const subText = header?.subscriberCountText?.simpleText || header?.metadata?.contentMetadataViewModel?.metadataRows?.[1]?.metadataParts?.[0]?.text?.content;
266
+ if (subText) {
267
+ subscriberCount = parseCount(subText);
268
+ }
269
+ let videoCount;
270
+ const videoText = header?.videosCountText?.runs?.[0]?.text;
271
+ if (videoText) {
272
+ videoCount = parseInt(videoText.replace(/,/g, ""), 10);
273
+ }
274
+ return {
275
+ channelId,
276
+ title: metadata.title || header?.title || "",
277
+ description: metadata.description || "",
278
+ customUrl: metadata.vanityChannelUrl?.split("/").pop() || "",
279
+ subscriberCount,
280
+ videoCount,
281
+ url: `https://youtube.com/channel/${channelId}`,
282
+ thumbnails: {
283
+ default: metadata.avatar?.thumbnails?.[0]?.url,
284
+ high: metadata.avatar?.thumbnails?.slice(-1)[0]?.url
285
+ }
286
+ };
287
+ } catch (error) {
288
+ throw new YouTubeToolsError(
289
+ "Failed to parse channel info",
290
+ ErrorCodes.PARSING_ERROR
291
+ );
292
+ }
293
+ }
294
+ function parseCount(text) {
295
+ const match = text.match(/([\d.]+)\s*([KMB])?/i);
296
+ if (!match) return 0;
297
+ let num = parseFloat(match[1]);
298
+ const suffix = match[2]?.toUpperCase();
299
+ if (suffix === "K") num *= 1e3;
300
+ else if (suffix === "M") num *= 1e6;
301
+ else if (suffix === "B") num *= 1e9;
302
+ return Math.round(num);
303
+ }
304
+
305
+ // src/video/get-transcript.ts
306
+ var USER_AGENT3 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
307
+ var WATCH_URL = "https://www.youtube.com/watch?v=";
308
+ var INNERTUBE_API_URL = "https://www.youtube.com/youtubei/v1/player?key=";
309
+ var INNERTUBE_CONTEXT = {
310
+ client: {
311
+ hl: "en",
312
+ gl: "US",
313
+ clientName: "WEB",
314
+ clientVersion: "2.20240101.00.00"
315
+ }
316
+ };
317
+ async function listTranscripts(videoId) {
318
+ const captionsData = await fetchCaptionsData(videoId);
319
+ return captionsData.tracks;
320
+ }
321
+ async function getTranscript(videoId, options = {}) {
322
+ const { languages = ["en"], preferGenerated = false } = options;
323
+ const captionsData = await fetchCaptionsData(videoId);
324
+ const tracks = captionsData.tracks;
325
+ if (tracks.length === 0) {
326
+ throw new YouTubeToolsError(
327
+ `No transcripts available for video: ${videoId}`,
328
+ ErrorCodes.TRANSCRIPT_NOT_AVAILABLE
329
+ );
330
+ }
331
+ const track = findBestTrack(tracks, languages, preferGenerated);
332
+ if (!track) {
333
+ throw new YouTubeToolsError(
334
+ `No transcript found for languages: ${languages.join(", ")}`,
335
+ ErrorCodes.TRANSCRIPT_NOT_AVAILABLE
336
+ );
337
+ }
338
+ const url = track.baseUrl.replace("&fmt=srv3", "");
339
+ return fetchTranscriptXML(url);
340
+ }
341
+ async function getTranscriptText(videoId, options = {}) {
342
+ const segments = await getTranscript(videoId, options);
343
+ return segments.map((s) => s.text).join("\n");
344
+ }
345
+ async function getTranscriptSRT(videoId, options = {}) {
346
+ const segments = await getTranscript(videoId, options);
347
+ return formatAsSRT(segments);
348
+ }
349
+ async function getTranscriptVTT(videoId, options = {}) {
350
+ const segments = await getTranscript(videoId, options);
351
+ return formatAsVTT(segments);
352
+ }
353
+ async function fetchCaptionsData(videoId) {
354
+ const html = await fetchVideoHtml(videoId);
355
+ const apiKey = extractInnertubeApiKey(html);
356
+ const data = await fetchInnertubeData(videoId, apiKey);
357
+ return extractCaptionsData(data, videoId);
358
+ }
359
+ async function fetchVideoHtml(videoId) {
360
+ const url = WATCH_URL + videoId;
361
+ const response = await fetch(url, {
362
+ headers: {
363
+ "User-Agent": USER_AGENT3,
364
+ "Accept-Language": "en-US,en;q=0.9",
365
+ Accept: "text/html,application/xhtml+xml"
366
+ }
367
+ });
368
+ if (!response.ok) {
369
+ throw new YouTubeToolsError(
370
+ `Failed to fetch video page: ${response.status}`,
371
+ ErrorCodes.NETWORK_ERROR,
372
+ response.status
373
+ );
374
+ }
375
+ const html = await response.text();
376
+ return decodeHtmlEntities(html);
377
+ }
378
+ function extractInnertubeApiKey(html) {
379
+ const match = html.match(/"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"/);
380
+ if (match && match[1]) {
381
+ return match[1];
382
+ }
383
+ if (html.includes('class="g-recaptcha"')) {
384
+ throw new YouTubeToolsError(
385
+ "IP blocked by YouTube (reCAPTCHA required)",
386
+ ErrorCodes.RATE_LIMITED
387
+ );
388
+ }
389
+ throw new YouTubeToolsError(
390
+ "Could not extract Innertube API key",
391
+ ErrorCodes.PARSING_ERROR
392
+ );
393
+ }
394
+ async function fetchInnertubeData(videoId, apiKey) {
395
+ const url = INNERTUBE_API_URL + apiKey;
396
+ const response = await fetch(url, {
397
+ method: "POST",
398
+ headers: {
399
+ "User-Agent": USER_AGENT3,
400
+ "Content-Type": "application/json"
401
+ },
402
+ body: JSON.stringify({
403
+ context: INNERTUBE_CONTEXT,
404
+ videoId
405
+ })
406
+ });
407
+ if (!response.ok) {
408
+ throw new YouTubeToolsError(
409
+ `Innertube API failed: ${response.status}`,
410
+ ErrorCodes.NETWORK_ERROR,
411
+ response.status
412
+ );
413
+ }
414
+ return response.json();
415
+ }
416
+ function extractCaptionsData(data, videoId) {
417
+ const playabilityStatus = data?.playabilityStatus?.status;
418
+ if (playabilityStatus && playabilityStatus !== "OK") {
419
+ const reason = data?.playabilityStatus?.reason || "Video unavailable";
420
+ throw new YouTubeToolsError(
421
+ `Video not playable: ${reason}`,
422
+ ErrorCodes.VIDEO_NOT_FOUND
423
+ );
424
+ }
425
+ const captionsData = data?.captions?.playerCaptionsTracklistRenderer;
426
+ if (!captionsData || !captionsData.captionTracks) {
427
+ throw new YouTubeToolsError(
428
+ `No transcripts available for video: ${videoId}`,
429
+ ErrorCodes.TRANSCRIPT_NOT_AVAILABLE
430
+ );
431
+ }
432
+ const tracks = captionsData.captionTracks.map((track) => ({
433
+ languageCode: track.languageCode,
434
+ language: track.name?.runs?.[0]?.text || track.name?.simpleText || track.languageCode,
435
+ baseUrl: track.baseUrl,
436
+ isGenerated: track.kind === "asr",
437
+ isTranslatable: track.isTranslatable || false
438
+ }));
439
+ const translationLanguages = (captionsData.translationLanguages || []).map(
440
+ (lang) => lang.languageCode
441
+ );
442
+ return { tracks, translationLanguages };
443
+ }
444
+ async function fetchTranscriptXML(url) {
445
+ const response = await fetch(url, {
446
+ headers: {
447
+ "User-Agent": USER_AGENT3,
448
+ "Accept-Language": "en-US,en;q=0.9"
449
+ }
450
+ });
451
+ if (!response.ok) {
452
+ throw new YouTubeToolsError(
453
+ `Failed to fetch transcript: ${response.status}`,
454
+ ErrorCodes.NETWORK_ERROR,
455
+ response.status
456
+ );
457
+ }
458
+ const xml = await response.text();
459
+ return parseTranscriptXML(xml);
460
+ }
461
+ function parseTranscriptXML(xml) {
462
+ const segments = [];
463
+ const regex = /<text\s+start="([^"]+)"\s+dur="([^"]*)"[^>]*>([^<]*)<\/text>/g;
464
+ let match;
465
+ while ((match = regex.exec(xml)) !== null) {
466
+ const text = decodeHtmlEntities(match[3]).trim();
467
+ if (text) {
468
+ segments.push({
469
+ start: parseFloat(match[1]),
470
+ duration: parseFloat(match[2] || "0"),
471
+ text
472
+ });
473
+ }
474
+ }
475
+ if (segments.length === 0) {
476
+ const altRegex = /<text\s+start="([^"]+)"[^>]*>([^<]*)<\/text>/g;
477
+ while ((match = altRegex.exec(xml)) !== null) {
478
+ const text = decodeHtmlEntities(match[2]).trim();
479
+ if (text) {
480
+ segments.push({
481
+ start: parseFloat(match[1]),
482
+ duration: 0,
483
+ text
484
+ });
485
+ }
486
+ }
487
+ }
488
+ return segments;
489
+ }
490
+ function findBestTrack(tracks, languages, preferGenerated) {
491
+ const sortedTracks = [...tracks].sort((a, b) => {
492
+ if (a.isGenerated !== b.isGenerated) {
493
+ if (preferGenerated) {
494
+ return a.isGenerated ? -1 : 1;
495
+ }
496
+ return a.isGenerated ? 1 : -1;
497
+ }
498
+ return 0;
499
+ });
500
+ for (const lang of languages) {
501
+ const track = sortedTracks.find(
502
+ (t) => t.languageCode === lang || t.languageCode.startsWith(lang + "-")
503
+ );
504
+ if (track) {
505
+ return track;
506
+ }
507
+ }
508
+ return sortedTracks[0] || null;
509
+ }
510
+ function decodeHtmlEntities(text) {
511
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#39;/g, "'").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&#x27;/g, "'").replace(/&#x2F;/g, "/").replace(/\n/g, " ").replace(/\\n/g, " ");
512
+ }
513
+ function formatTime(seconds) {
514
+ const hours = Math.floor(seconds / 3600);
515
+ const mins = Math.floor(seconds % 3600 / 60);
516
+ const secs = Math.floor(seconds % 60);
517
+ const ms = Math.floor(seconds % 1 * 1e3);
518
+ return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")},${ms.toString().padStart(3, "0")}`;
519
+ }
520
+ function formatTimeVTT(seconds) {
521
+ const hours = Math.floor(seconds / 3600);
522
+ const mins = Math.floor(seconds % 3600 / 60);
523
+ const secs = Math.floor(seconds % 60);
524
+ const ms = Math.floor(seconds % 1 * 1e3);
525
+ return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(3, "0")}`;
526
+ }
527
+ function formatAsSRT(segments) {
528
+ return segments.map((seg, i) => {
529
+ const start = formatTime(seg.start);
530
+ const end = formatTime(seg.start + seg.duration);
531
+ return `${i + 1}
532
+ ${start} --> ${end}
533
+ ${seg.text}
534
+ `;
535
+ }).join("\n");
536
+ }
537
+ function formatAsVTT(segments) {
538
+ const lines = ["WEBVTT\n"];
539
+ for (const seg of segments) {
540
+ const start = formatTimeVTT(seg.start);
541
+ const end = formatTimeVTT(seg.start + seg.duration);
542
+ lines.push(`${start} --> ${end}
543
+ ${seg.text}
544
+ `);
545
+ }
546
+ return lines.join("\n");
547
+ }
548
+
549
+ // src/video/get-info.ts
550
+ var USER_AGENT4 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
551
+ async function getVideoInfo(videoIdOrUrl) {
552
+ const videoId = parseVideoId(videoIdOrUrl);
553
+ const html = await fetchVideoPage(videoId);
554
+ return extractVideoInfo(html, videoId);
555
+ }
556
+ async function getBasicVideoInfo(videoIdOrUrl) {
557
+ const videoId = parseVideoId(videoIdOrUrl);
558
+ const url = `https://www.youtube.com/oembed?url=https://youtube.com/watch?v=${videoId}&format=json`;
559
+ const response = await fetch(url);
560
+ if (!response.ok) {
561
+ throw new YouTubeToolsError(
562
+ `Video not found: ${videoId}`,
563
+ ErrorCodes.VIDEO_NOT_FOUND,
564
+ response.status
565
+ );
566
+ }
567
+ const data = await response.json();
568
+ return {
569
+ videoId,
570
+ title: data.title,
571
+ url: `https://youtube.com/watch?v=${videoId}`,
572
+ channelTitle: data.author_name,
573
+ thumbnails: {
574
+ default: data.thumbnail_url,
575
+ high: data.thumbnail_url.replace("hqdefault", "maxresdefault")
576
+ },
577
+ publishedAt: ""
578
+ };
579
+ }
580
+ function parseVideoId(input) {
581
+ if (/^[a-zA-Z0-9_-]{11}$/.test(input)) {
582
+ return input;
583
+ }
584
+ const patterns = [
585
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
586
+ /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
587
+ /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
588
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/
589
+ ];
590
+ for (const pattern of patterns) {
591
+ const match = input.match(pattern);
592
+ if (match) {
593
+ return match[1];
594
+ }
595
+ }
596
+ throw new YouTubeToolsError(
597
+ `Invalid video ID or URL: ${input}`,
598
+ ErrorCodes.VIDEO_NOT_FOUND
599
+ );
600
+ }
601
+ async function fetchVideoPage(videoId) {
602
+ const url = `https://www.youtube.com/watch?v=${videoId}`;
603
+ const response = await fetch(url, {
604
+ headers: {
605
+ "User-Agent": USER_AGENT4,
606
+ "Accept-Language": "en-US,en;q=0.9",
607
+ Cookie: "CONSENT=YES+"
608
+ }
609
+ });
610
+ if (!response.ok) {
611
+ throw new YouTubeToolsError(
612
+ `Failed to fetch video page: ${response.status}`,
613
+ ErrorCodes.NETWORK_ERROR,
614
+ response.status
615
+ );
616
+ }
617
+ return response.text();
618
+ }
619
+ function extractVideoInfo(html, videoId) {
620
+ const playerMatch = html.match(
621
+ /ytInitialPlayerResponse\s*=\s*({.+?});(?:var|<\/script>)/s
622
+ );
623
+ const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
624
+ let playerData = {};
625
+ let pageData = {};
626
+ try {
627
+ if (playerMatch) {
628
+ playerData = JSON.parse(playerMatch[1]);
629
+ }
630
+ if (dataMatch) {
631
+ pageData = JSON.parse(dataMatch[1]);
632
+ }
633
+ } catch {
634
+ }
635
+ const videoDetails = playerData?.videoDetails || {};
636
+ const microformat = playerData?.microformat?.playerMicroformatRenderer || {};
637
+ return {
638
+ videoId,
639
+ title: videoDetails.title || "",
640
+ description: videoDetails.shortDescription || "",
641
+ publishedAt: microformat.publishDate || "",
642
+ duration: formatDuration(parseInt(videoDetails.lengthSeconds || "0", 10)),
643
+ durationSeconds: parseInt(videoDetails.lengthSeconds || "0", 10),
644
+ viewCount: parseInt(videoDetails.viewCount || "0", 10),
645
+ likeCount: extractLikeCount(pageData),
646
+ commentCount: extractCommentCount(pageData),
647
+ url: `https://youtube.com/watch?v=${videoId}`,
648
+ thumbnails: {
649
+ default: `https://i.ytimg.com/vi/${videoId}/default.jpg`,
650
+ medium: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
651
+ high: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
652
+ maxres: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
653
+ },
654
+ channelId: videoDetails.channelId,
655
+ channelTitle: videoDetails.author,
656
+ category: microformat.category,
657
+ tags: videoDetails.keywords,
658
+ keywords: videoDetails.keywords,
659
+ chapters: extractChapters(pageData),
660
+ isLive: videoDetails.isLiveContent,
661
+ isUpcoming: videoDetails.isUpcoming
662
+ };
663
+ }
664
+ function formatDuration(seconds) {
665
+ const hours = Math.floor(seconds / 3600);
666
+ const mins = Math.floor(seconds % 3600 / 60);
667
+ const secs = seconds % 60;
668
+ if (hours > 0) {
669
+ return `${hours}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
670
+ }
671
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
672
+ }
673
+ function extractLikeCount(pageData) {
674
+ try {
675
+ const contents = pageData?.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
676
+ for (const content of contents) {
677
+ const buttons = content?.videoPrimaryInfoRenderer?.videoActions?.menuRenderer?.topLevelButtons || [];
678
+ for (const button of buttons) {
679
+ const likeButton = button?.segmentedLikeDislikeButtonViewModel?.likeButtonViewModel;
680
+ if (likeButton?.likeButtonViewModel?.toggleButtonViewModel) {
681
+ const countText = likeButton.likeButtonViewModel.toggleButtonViewModel.toggleButtonViewModel?.defaultButtonViewModel?.buttonViewModel?.title;
682
+ if (countText) {
683
+ return parseCount2(countText);
684
+ }
685
+ }
686
+ }
687
+ }
688
+ } catch {
689
+ }
690
+ return void 0;
691
+ }
692
+ function extractCommentCount(pageData) {
693
+ try {
694
+ const contents = pageData?.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
695
+ for (const content of contents) {
696
+ const header = content?.itemSectionRenderer?.contents?.[0]?.commentsEntryPointHeaderRenderer;
697
+ if (header?.commentCount?.simpleText) {
698
+ return parseCount2(header.commentCount.simpleText);
699
+ }
700
+ }
701
+ } catch {
702
+ }
703
+ return void 0;
704
+ }
705
+ function extractChapters(pageData) {
706
+ try {
707
+ const panels = pageData?.playerOverlays?.playerOverlayRenderer?.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer?.playerBar?.multiMarkersPlayerBarRenderer?.markersMap || [];
708
+ for (const panel of panels) {
709
+ if (panel?.key === "AUTO_CHAPTERS" || panel?.key === "DESCRIPTION_CHAPTERS") {
710
+ const markers = panel?.value?.chapters || [];
711
+ return markers.map((m) => ({
712
+ title: m.chapterRenderer?.title?.simpleText || "",
713
+ startTime: m.chapterRenderer?.timeRangeStartMillis / 1e3 || 0
714
+ }));
715
+ }
716
+ }
717
+ } catch {
718
+ }
719
+ return [];
720
+ }
721
+ function parseCount2(text) {
722
+ const match = text.match(/([\d.]+)\s*([KMB])?/i);
723
+ if (!match) return 0;
724
+ let num = parseFloat(match[1]);
725
+ const suffix = match[2]?.toUpperCase();
726
+ if (suffix === "K") num *= 1e3;
727
+ else if (suffix === "M") num *= 1e6;
728
+ else if (suffix === "B") num *= 1e9;
729
+ return Math.round(num);
730
+ }
731
+
732
+ // src/video/search.ts
733
+ var USER_AGENT5 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
734
+ async function searchVideos(queryOrOptions) {
735
+ const options = typeof queryOrOptions === "string" ? { query: queryOrOptions } : queryOrOptions;
736
+ const { query, limit = 20, sortBy = "relevance" } = options;
737
+ const searchUrl = buildSearchUrl(query, options);
738
+ const response = await fetch(searchUrl, {
739
+ headers: {
740
+ "User-Agent": USER_AGENT5,
741
+ "Accept-Language": "en-US,en;q=0.9"
742
+ }
743
+ });
744
+ if (!response.ok) {
745
+ throw new YouTubeToolsError(
746
+ `Search failed: ${response.status}`,
747
+ ErrorCodes.NETWORK_ERROR,
748
+ response.status
749
+ );
750
+ }
751
+ const html = await response.text();
752
+ return extractSearchResults(html, limit);
753
+ }
754
+ function buildSearchUrl(query, options) {
755
+ const params = new URLSearchParams({
756
+ search_query: query
757
+ });
758
+ if (options.sortBy === "date") {
759
+ params.set("sp", "CAI%3D");
760
+ } else if (options.sortBy === "viewCount") {
761
+ params.set("sp", "CAM%3D");
762
+ } else if (options.sortBy === "rating") {
763
+ params.set("sp", "CAE%3D");
764
+ }
765
+ return `https://www.youtube.com/results?${params.toString()}`;
766
+ }
767
+ function extractSearchResults(html, limit) {
768
+ const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
769
+ if (!dataMatch) {
770
+ return [];
771
+ }
772
+ try {
773
+ const data = JSON.parse(dataMatch[1]);
774
+ const videos = [];
775
+ const contents = data?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
776
+ for (const section of contents) {
777
+ const items = section?.itemSectionRenderer?.contents || [];
778
+ for (const item of items) {
779
+ if (videos.length >= limit) break;
780
+ const videoRenderer = item?.videoRenderer;
781
+ if (videoRenderer?.videoId) {
782
+ videos.push(parseVideoRenderer2(videoRenderer));
783
+ }
784
+ }
785
+ }
786
+ return videos;
787
+ } catch {
788
+ return [];
789
+ }
790
+ }
791
+ function parseVideoRenderer2(renderer) {
792
+ const videoId = renderer.videoId;
793
+ return {
794
+ videoId,
795
+ title: renderer.title?.runs?.[0]?.text || "",
796
+ description: renderer.detailedMetadataSnippets?.[0]?.snippetText?.runs?.map((r) => r.text).join("") || "",
797
+ publishedAt: renderer.publishedTimeText?.simpleText || "",
798
+ duration: renderer.lengthText?.simpleText || "",
799
+ durationSeconds: parseDuration2(renderer.lengthText?.simpleText),
800
+ viewCount: parseViewCount2(renderer.viewCountText?.simpleText),
801
+ url: `https://youtube.com/watch?v=${videoId}`,
802
+ thumbnails: {
803
+ default: `https://i.ytimg.com/vi/${videoId}/default.jpg`,
804
+ medium: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
805
+ high: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
806
+ maxres: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
807
+ },
808
+ channelId: renderer.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId,
809
+ channelTitle: renderer.ownerText?.runs?.[0]?.text
810
+ };
811
+ }
812
+ function parseDuration2(duration) {
813
+ if (!duration) return void 0;
814
+ const parts = duration.split(":").map(Number);
815
+ if (parts.length === 3) {
816
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
817
+ } else if (parts.length === 2) {
818
+ return parts[0] * 60 + parts[1];
819
+ }
820
+ return parts[0];
821
+ }
822
+ function parseViewCount2(viewText) {
823
+ if (!viewText) return void 0;
824
+ const match = viewText.match(/([\d,.]+)\s*([KMB])?/i);
825
+ if (!match) return void 0;
826
+ let num = parseFloat(match[1].replace(/,/g, ""));
827
+ const suffix = match[2]?.toUpperCase();
828
+ if (suffix === "K") num *= 1e3;
829
+ else if (suffix === "M") num *= 1e6;
830
+ else if (suffix === "B") num *= 1e9;
831
+ return Math.round(num);
832
+ }
833
+
834
+ export {
835
+ YouTubeToolsError,
836
+ ErrorCodes,
837
+ getChannelVideos,
838
+ getChannelInfo,
839
+ listTranscripts,
840
+ getTranscript,
841
+ getTranscriptText,
842
+ getTranscriptSRT,
843
+ getTranscriptVTT,
844
+ getVideoInfo,
845
+ getBasicVideoInfo,
846
+ searchVideos
847
+ };