extract-youtube 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1852 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ //#region \0rolldown/runtime.js
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
11
+ var __exportAll = (all, no_symbols) => {
12
+ let target = {};
13
+ for (var name in all) __defProp(target, name, {
14
+ get: all[name],
15
+ enumerable: true
16
+ });
17
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
18
+ return target;
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
22
+ key = keys[i];
23
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
24
+ get: ((k) => from[k]).bind(null, key),
25
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
26
+ });
27
+ }
28
+ return to;
29
+ };
30
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
31
+ value: mod,
32
+ enumerable: true
33
+ }) : target, mod));
34
+ var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
+ //#endregion
36
+ let html_entities = require("html-entities");
37
+ let fast_xml_parser = require("fast-xml-parser");
38
+ let node_fetch = require("node-fetch");
39
+ node_fetch = __toESM(node_fetch);
40
+ let https_proxy_agent = require("https-proxy-agent");
41
+ //#region src/errors.ts
42
+ var _CouldNotRetrieveTranscript, _RequestBlocked;
43
+ var YOUTUBE_WATCH_URL$1 = "https://www.youtube.com/watch?v={video_id}";
44
+ /**
45
+ * Base exception class for all YouTube Transcript API errors.
46
+ */
47
+ var YouTubeTranscriptApiException = class YouTubeTranscriptApiException extends Error {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = "YouTubeTranscriptApiException";
51
+ Object.setPrototypeOf(this, YouTubeTranscriptApiException.prototype);
52
+ }
53
+ };
54
+ /**
55
+ * Base exception class for transcript retrieval errors.
56
+ */
57
+ var CouldNotRetrieveTranscript = class CouldNotRetrieveTranscript extends YouTubeTranscriptApiException {
58
+ constructor(videoId) {
59
+ super("");
60
+ this.causeMessage = "";
61
+ this.videoId = videoId;
62
+ Object.setPrototypeOf(this, CouldNotRetrieveTranscript.prototype);
63
+ this.message = this.buildErrorMessage();
64
+ }
65
+ buildErrorMessage() {
66
+ const videoUrl = YOUTUBE_WATCH_URL$1.replace("{video_id}", this.videoId);
67
+ let errorMessage = CouldNotRetrieveTranscript.ERROR_MESSAGE.replace("{video_url}", videoUrl);
68
+ const cause = this.getCause();
69
+ if (cause) {
70
+ errorMessage += CouldNotRetrieveTranscript.CAUSE_MESSAGE_INTRO.replace("{cause}", cause);
71
+ errorMessage += CouldNotRetrieveTranscript.GITHUB_REFERRAL;
72
+ }
73
+ return errorMessage;
74
+ }
75
+ getCause() {
76
+ return this.causeMessage;
77
+ }
78
+ };
79
+ _CouldNotRetrieveTranscript = CouldNotRetrieveTranscript;
80
+ _CouldNotRetrieveTranscript.ERROR_MESSAGE = "\nCould not retrieve a transcript for the video {video_url}!";
81
+ _CouldNotRetrieveTranscript.CAUSE_MESSAGE_INTRO = " This is most likely caused by:\n\n{cause}";
82
+ _CouldNotRetrieveTranscript.GITHUB_REFERRAL = "\n\nIf you are sure that the described cause is not responsible for this error and that a transcript should be retrievable, please create an issue at https://github.com/jdepoix/youtube-transcript-api/issues. Please add which version of youtube_transcript_api you are using and provide the information needed to replicate the error. Also make sure that there are no open issues which already describe your problem!";
83
+ var YouTubeDataUnparsable = class YouTubeDataUnparsable extends CouldNotRetrieveTranscript {
84
+ constructor(videoId) {
85
+ super(videoId);
86
+ this.causeMessage = "The data required to fetch the transcript is not parsable. This should not happen, please open an issue (make sure to include the video ID)!";
87
+ this.name = "YouTubeDataUnparsable";
88
+ Object.setPrototypeOf(this, YouTubeDataUnparsable.prototype);
89
+ this.message = this.buildErrorMessage();
90
+ }
91
+ };
92
+ var YouTubeRequestFailed = class YouTubeRequestFailed extends CouldNotRetrieveTranscript {
93
+ constructor(videoId, httpError) {
94
+ super(videoId);
95
+ this.httpErrorReason = httpError.message;
96
+ this.name = "YouTubeRequestFailed";
97
+ this.message = this.buildErrorMessage();
98
+ Object.setPrototypeOf(this, YouTubeRequestFailed.prototype);
99
+ }
100
+ getCause() {
101
+ return `Request to YouTube failed: ${this.httpErrorReason}`;
102
+ }
103
+ };
104
+ var VideoUnplayable = class VideoUnplayable extends CouldNotRetrieveTranscript {
105
+ constructor(videoId, unplayableReason, additionalDetails) {
106
+ super(videoId);
107
+ this.unplayableReason = unplayableReason;
108
+ this.additionalDetails = additionalDetails;
109
+ this.name = "VideoUnplayable";
110
+ this.message = this.buildErrorMessage();
111
+ Object.setPrototypeOf(this, VideoUnplayable.prototype);
112
+ }
113
+ getCause() {
114
+ let reason = this.unplayableReason ?? "No reason specified!";
115
+ if (this.additionalDetails.length > 0) {
116
+ const formattedDetails = this.additionalDetails.map((detail) => ` - ${detail}`).join("\n");
117
+ reason = `${reason}\n\nAdditional Details:\n${formattedDetails}`;
118
+ }
119
+ return `The video is unplayable for the following reason: ${reason}`;
120
+ }
121
+ };
122
+ var VideoUnavailable = class VideoUnavailable extends CouldNotRetrieveTranscript {
123
+ constructor(videoId) {
124
+ super(videoId);
125
+ this.causeMessage = "The video is no longer available";
126
+ this.name = "VideoUnavailable";
127
+ Object.setPrototypeOf(this, VideoUnavailable.prototype);
128
+ this.message = this.buildErrorMessage();
129
+ }
130
+ };
131
+ var InvalidVideoId = class InvalidVideoId extends CouldNotRetrieveTranscript {
132
+ constructor(videoId) {
133
+ super(videoId);
134
+ this.causeMessage = "You provided an invalid video id. Make sure you are using the video id and NOT the url!\n\nDo NOT run: `new YouTubeTranscriptApi().fetch(\"https://www.youtube.com/watch?v=1234\")`\nInstead run: `new YouTubeTranscriptApi().fetch(\"1234\")`";
135
+ this.name = "InvalidVideoId";
136
+ Object.setPrototypeOf(this, InvalidVideoId.prototype);
137
+ this.message = this.buildErrorMessage();
138
+ }
139
+ };
140
+ var AgeRestricted = class AgeRestricted extends CouldNotRetrieveTranscript {
141
+ constructor(videoId) {
142
+ super(videoId);
143
+ this.causeMessage = "This video is age-restricted. Therefore, you are unable to retrieve transcripts for it without authenticating yourself.\n\nUnfortunately, Cookie Authentication is temporarily unsupported in youtube-transcript-api, as recent changes in YouTube's API broke the previous implementation. I will do my best to re-implement it as soon as possible.";
144
+ this.name = "AgeRestricted";
145
+ Object.setPrototypeOf(this, AgeRestricted.prototype);
146
+ this.message = this.buildErrorMessage();
147
+ }
148
+ };
149
+ var FailedToCreateConsentCookie = class FailedToCreateConsentCookie extends CouldNotRetrieveTranscript {
150
+ constructor(videoId) {
151
+ super(videoId);
152
+ this.causeMessage = "Failed to automatically give consent to saving cookies";
153
+ this.name = "FailedToCreateConsentCookie";
154
+ Object.setPrototypeOf(this, FailedToCreateConsentCookie.prototype);
155
+ this.message = this.buildErrorMessage();
156
+ }
157
+ };
158
+ var TranscriptsDisabled = class TranscriptsDisabled extends CouldNotRetrieveTranscript {
159
+ constructor(videoId) {
160
+ super(videoId);
161
+ this.causeMessage = "Subtitles are disabled for this video";
162
+ this.name = "TranscriptsDisabled";
163
+ Object.setPrototypeOf(this, TranscriptsDisabled.prototype);
164
+ this.message = this.buildErrorMessage();
165
+ }
166
+ };
167
+ var NotTranslatable = class NotTranslatable extends CouldNotRetrieveTranscript {
168
+ constructor(videoId) {
169
+ super(videoId);
170
+ this.causeMessage = "The requested language is not translatable";
171
+ this.name = "NotTranslatable";
172
+ Object.setPrototypeOf(this, NotTranslatable.prototype);
173
+ this.message = this.buildErrorMessage();
174
+ }
175
+ };
176
+ var TranslationLanguageNotAvailable = class TranslationLanguageNotAvailable extends CouldNotRetrieveTranscript {
177
+ constructor(videoId) {
178
+ super(videoId);
179
+ this.causeMessage = "The requested translation language is not available";
180
+ this.name = "TranslationLanguageNotAvailable";
181
+ Object.setPrototypeOf(this, TranslationLanguageNotAvailable.prototype);
182
+ this.message = this.buildErrorMessage();
183
+ }
184
+ };
185
+ var NoTranscriptFound = class NoTranscriptFound extends CouldNotRetrieveTranscript {
186
+ constructor(videoId, requestedLanguageCodes, availableTranscriptData) {
187
+ super(videoId);
188
+ this.requestedLanguageCodes = requestedLanguageCodes;
189
+ this.availableTranscriptData = availableTranscriptData;
190
+ this.name = "NoTranscriptFound";
191
+ this.message = this.buildErrorMessage();
192
+ Object.setPrototypeOf(this, NoTranscriptFound.prototype);
193
+ }
194
+ getCause() {
195
+ return `No transcripts were found for any of the requested language codes: ${this.requestedLanguageCodes.join(", ")}\n\n` + this.availableTranscriptData.toString();
196
+ }
197
+ };
198
+ var PoTokenRequired = class PoTokenRequired extends CouldNotRetrieveTranscript {
199
+ constructor(videoId) {
200
+ super(videoId);
201
+ this.causeMessage = "The requested video cannot be retrieved without a PO Token. If this happens, please open a GitHub issue!";
202
+ this.name = "PoTokenRequired";
203
+ Object.setPrototypeOf(this, PoTokenRequired.prototype);
204
+ this.message = this.buildErrorMessage();
205
+ }
206
+ };
207
+ var RequestBlocked = class RequestBlocked extends CouldNotRetrieveTranscript {
208
+ constructor(videoId) {
209
+ super(videoId);
210
+ this.proxyConfig = null;
211
+ this.name = "RequestBlocked";
212
+ Object.setPrototypeOf(this, RequestBlocked.prototype);
213
+ this.message = this.buildErrorMessage();
214
+ }
215
+ withProxyConfig(proxyConfig) {
216
+ this.proxyConfig = proxyConfig;
217
+ this.message = this.buildErrorMessage();
218
+ return this;
219
+ }
220
+ getCause() {
221
+ if (this.proxyConfig && this.proxyConfig.constructor.name === "WebshareProxyConfig") return RequestBlocked.WITH_WEBSHARE_PROXY_CAUSE_MESSAGE;
222
+ if (this.proxyConfig && this.proxyConfig.constructor.name === "GenericProxyConfig") return RequestBlocked.WITH_GENERIC_PROXY_CAUSE_MESSAGE;
223
+ return RequestBlocked.DEFAULT_CAUSE_MESSAGE;
224
+ }
225
+ };
226
+ _RequestBlocked = RequestBlocked;
227
+ _RequestBlocked.BASE_CAUSE_MESSAGE = "YouTube is blocking requests from your IP. This usually is due to one of the following reasons:\n- You have done too many requests and your IP has been blocked by YouTube\n- You are doing requests from an IP belonging to a cloud provider (like AWS, Google Cloud Platform, Azure, etc.). Unfortunately, most IPs from cloud providers are blocked by YouTube.\n\n";
228
+ _RequestBlocked.DEFAULT_CAUSE_MESSAGE = _RequestBlocked.BASE_CAUSE_MESSAGE + "There are two things you can do to work around this:\n1. Use proxies to hide your IP address, as explained in the \"Working around IP bans\" section of the README (https://github.com/jdepoix/youtube-transcript-api?tab=readme-ov-file#working-around-ip-bans-requestblocked-or-ipblocked-exception).\n2. (NOT RECOMMENDED) If you authenticate your requests using cookies, you will be able to continue doing requests for a while. However, YouTube will eventually permanently ban the account that you have used to authenticate with! So only do this if you don't mind your account being banned!";
229
+ _RequestBlocked.WITH_GENERIC_PROXY_CAUSE_MESSAGE = "YouTube is blocking your requests, despite you using proxies. Keep in mind that a proxy is just a way to hide your real IP behind the IP of that proxy, but there is no guarantee that the IP of that proxy won't be blocked as well.\n\nThe only truly reliable way to prevent IP blocks is rotating through a large pool of residential IPs, by using a provider like Webshare (https://www.webshare.io/?referral_code=w0xno53eb50g), which provides you with a pool of >30M residential IPs (make sure to purchase \"Residential\" proxies, NOT \"Proxy Server\" or \"Static Residential\"!).\n\nYou will find more information on how to easily integrate Webshare here: https://github.com/jdepoix/youtube-transcript-api?tab=readme-ov-file#using-webshare";
230
+ _RequestBlocked.WITH_WEBSHARE_PROXY_CAUSE_MESSAGE = "YouTube is blocking your requests, despite you using Webshare proxies. Please make sure that you have purchased \"Residential\" proxies and NOT \"Proxy Server\" or \"Static Residential\", as those won't work as reliably! The free tier also uses \"Proxy Server\" and will NOT work!\n\nThe only reliable option is using \"Residential\" proxies (not \"Static Residential\"), as this allows you to rotate through a pool of over 30M IPs, which means you will always find an IP that hasn't been blocked by YouTube yet!\n\nYou can support the development of this open source project by making your Webshare purchases through this affiliate link: https://www.webshare.io/?referral_code=w0xno53eb50g \n\nThank you for your support! <3";
231
+ var IpBlocked = class IpBlocked extends RequestBlocked {
232
+ constructor(videoId) {
233
+ super(videoId);
234
+ this.causeMessage = "YouTube is blocking requests from your IP. This usually is due to one of the following reasons:\n- You have done too many requests and your IP has been blocked by YouTube\n- You are doing requests from an IP belonging to a cloud provider (like AWS, Google Cloud Platform, Azure, etc.). Unfortunately, most IPs from cloud providers are blocked by YouTube.\n\nWays to work around this are explained in the \"Working around IP bans\" section of the README (https://github.com/jdepoix/youtube-transcript-api?tab=readme-ov-file#working-around-ip-bans-requestblocked-or-ipblocked-exception).\n";
235
+ this.name = "IpBlocked";
236
+ Object.setPrototypeOf(this, IpBlocked.prototype);
237
+ this.message = this.buildErrorMessage();
238
+ }
239
+ getCause() {
240
+ return this.causeMessage;
241
+ }
242
+ };
243
+ //#endregion
244
+ //#region src/models/fetched-transcript.ts
245
+ /**
246
+ * Represents a fetched transcript with all its snippets and metadata.
247
+ * This class is iterable, allowing you to iterate over transcript snippets.
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const transcript = await api.fetch('video_id');
252
+ * for (const snippet of transcript) {
253
+ * console.log(snippet.text);
254
+ * }
255
+ * ```
256
+ */
257
+ var FetchedTranscript = class {
258
+ /**
259
+ * Creates a new FetchedTranscript instance.
260
+ *
261
+ * @param {FetchedTranscriptSnippet[]} snippets - Array of transcript snippets
262
+ * @param {string} videoId - The video ID
263
+ * @param {string} language - The language name
264
+ * @param {string} languageCode - The language code
265
+ * @param {boolean} isGenerated - Whether the transcript is auto-generated
266
+ */
267
+ constructor(snippets, videoId, language, languageCode, isGenerated) {
268
+ this.snippets = snippets;
269
+ this.videoId = videoId;
270
+ this.language = language;
271
+ this.languageCode = languageCode;
272
+ this.isGenerated = isGenerated;
273
+ }
274
+ /**
275
+ * Makes the transcript iterable, allowing for...of loops.
276
+ *
277
+ * @returns {Iterator<FetchedTranscriptSnippet>} Iterator over the snippets
278
+ */
279
+ [Symbol.iterator]() {
280
+ return this.snippets[Symbol.iterator]();
281
+ }
282
+ /**
283
+ * Gets a snippet by index.
284
+ *
285
+ * @param {number} index - The index of the snippet
286
+ * @returns {FetchedTranscriptSnippet} The snippet at the given index
287
+ */
288
+ getSnippetAtIndex(index) {
289
+ return this.snippets[index];
290
+ }
291
+ /**
292
+ * Alias for getSnippetAtIndex() for backward compatibility.
293
+ *
294
+ * @deprecated Use getSnippetAtIndex() instead
295
+ * @param {number} index - The index of the snippet
296
+ * @returns {FetchedTranscriptSnippet} The snippet at the given index
297
+ */
298
+ get(index) {
299
+ return this.getSnippetAtIndex(index);
300
+ }
301
+ /**
302
+ * Gets the total number of snippets in the transcript.
303
+ *
304
+ * @returns {number} The number of snippets
305
+ */
306
+ get length() {
307
+ return this.snippets.length;
308
+ }
309
+ /**
310
+ * Converts the transcript to raw data format (array of objects).
311
+ * This is useful for serialization or when you need plain data objects.
312
+ *
313
+ * @returns {Array<{text: string, start: number, duration: number}>} Raw transcript data
314
+ */
315
+ toRawData() {
316
+ return this.snippets.map((snippet) => ({
317
+ text: snippet.text,
318
+ start: snippet.start,
319
+ duration: snippet.duration
320
+ }));
321
+ }
322
+ };
323
+ //#endregion
324
+ //#region src/parsers/transcript-parser.ts
325
+ /**
326
+ * @fileoverview Parser for converting YouTube transcript XML data into structured snippets
327
+ */
328
+ var _TranscriptParser;
329
+ /**
330
+ * Parses transcript XML data from YouTube into FetchedTranscriptSnippet objects.
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const parser = new TranscriptParser(false);
335
+ * const snippets = parser.parseTranscriptXml(xmlData);
336
+ * ```
337
+ */
338
+ var TranscriptParser = class TranscriptParser {
339
+ /**
340
+ * Creates a new TranscriptParser instance.
341
+ *
342
+ * @param {boolean} [preserveFormatting=false] - Whether to preserve HTML formatting tags
343
+ */
344
+ constructor(preserveFormatting = false) {
345
+ this.htmlRemovalRegex = this.buildHtmlRemovalRegex(preserveFormatting);
346
+ }
347
+ /**
348
+ * Builds a regex pattern for removing HTML tags based on formatting preferences.
349
+ *
350
+ * @private
351
+ * @param {boolean} preserveFormatting - Whether to preserve formatting tags
352
+ * @returns {RegExp} The HTML removal regex
353
+ */
354
+ buildHtmlRemovalRegex(preserveFormatting) {
355
+ if (preserveFormatting) {
356
+ const pattern = `<\\/?(?!\\/?(?:${TranscriptParser.FORMATTING_TAGS.join("|")})\\b).*?\\b>`;
357
+ return new RegExp(pattern, "gi");
358
+ } else return /<[^>]*>/gi;
359
+ }
360
+ /**
361
+ * Parses raw XML transcript data into an array of transcript snippets.
362
+ *
363
+ * @param {string} xmlData - The raw XML data from YouTube's transcript API
364
+ * @returns {FetchedTranscriptSnippet[]} Array of parsed transcript snippets
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * const parser = new TranscriptParser();
369
+ * const snippets = parser.parseTranscriptXml(xmlData);
370
+ * for (const snippet of snippets) {
371
+ * console.log(`${snippet.start}s: ${snippet.text}`);
372
+ * }
373
+ * ```
374
+ */
375
+ parseTranscriptXml(xmlData) {
376
+ const textElements = new fast_xml_parser.XMLParser({
377
+ ignoreAttributes: false,
378
+ attributeNamePrefix: "@_"
379
+ }).parse(xmlData).transcript?.text || [];
380
+ return (Array.isArray(textElements) ? textElements : [textElements]).filter((element) => element["#text"]).map((element) => this.parseTranscriptSnippet(element));
381
+ }
382
+ /**
383
+ * Parses a single XML element into a FetchedTranscriptSnippet.
384
+ *
385
+ * @private
386
+ * @param {any} element - The XML element to parse
387
+ * @returns {FetchedTranscriptSnippet} The parsed snippet
388
+ */
389
+ parseTranscriptSnippet(element) {
390
+ return {
391
+ text: this.cleanTranscriptText(element["#text"]),
392
+ start: this.parseFloatAttribute(element["@_start"]),
393
+ duration: this.parseFloatAttribute(element["@_dur"])
394
+ };
395
+ }
396
+ /**
397
+ * Parses a string attribute as a float, defaulting to 0 if invalid.
398
+ *
399
+ * @private
400
+ * @param {string | undefined} value - The attribute value to parse
401
+ * @returns {number} The parsed float value
402
+ */
403
+ parseFloatAttribute(value) {
404
+ return parseFloat(value || "0");
405
+ }
406
+ /**
407
+ * Cleans transcript text by decoding HTML entities and removing HTML tags.
408
+ *
409
+ * @private
410
+ * @param {string} text - The raw transcript text
411
+ * @returns {string} The cleaned text
412
+ */
413
+ cleanTranscriptText(text) {
414
+ return (0, html_entities.decode)(text).replace(this.htmlRemovalRegex, "");
415
+ }
416
+ };
417
+ _TranscriptParser = TranscriptParser;
418
+ _TranscriptParser.FORMATTING_TAGS = [
419
+ "strong",
420
+ "em",
421
+ "b",
422
+ "i",
423
+ "mark",
424
+ "small",
425
+ "del",
426
+ "ins",
427
+ "sub",
428
+ "sup"
429
+ ];
430
+ //#endregion
431
+ //#region src/utils/http-error-handler.ts
432
+ /**
433
+ * Checks an HTTP response and throws appropriate errors if the request failed.
434
+ *
435
+ * @param {Response} response - The HTTP response to check
436
+ * @param {string} videoId - The video ID associated with the request
437
+ * @throws {IpBlocked} If status is 429 (Too Many Requests)
438
+ * @throws {YouTubeRequestFailed} If status indicates an error
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * const response = await httpClient.get(url);
443
+ * handleHttpErrors(response, videoId);
444
+ * // If we reach here, the request was successful
445
+ * ```
446
+ */
447
+ function handleHttpErrors(response, videoId) {
448
+ if (response.status === 429) throw new IpBlocked(videoId);
449
+ if (!response.ok) {
450
+ const errorMessage = `HTTP ${response.status}: ${response.statusText}`;
451
+ throw new YouTubeRequestFailed(videoId, new Error(errorMessage));
452
+ }
453
+ }
454
+ //#endregion
455
+ //#region src/models/transcript.ts
456
+ /**
457
+ * Represents a transcript that can be fetched or translated.
458
+ * You typically obtain Transcript objects from a TranscriptList.
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * const transcriptList = await api.list('video_id');
463
+ * const transcript = transcriptList.findTranscript(['en']);
464
+ * const fetched = await transcript.fetch();
465
+ * ```
466
+ */
467
+ var Transcript = class Transcript {
468
+ /**
469
+ * Creates a new Transcript instance.
470
+ * You probably don't want to initialize this directly.
471
+ * Usually you'll access Transcript objects using a TranscriptList.
472
+ *
473
+ * @param {HttpClient} httpClient - HTTP client for making requests
474
+ * @param {string} videoId - The video ID
475
+ * @param {string} transcriptUrl - The transcript URL
476
+ * @param {string} language - The language name
477
+ * @param {string} languageCode - The language code
478
+ * @param {boolean} isGenerated - Whether the transcript is auto-generated
479
+ * @param {TranslationLanguage[]} translationLanguages - Available translation languages
480
+ */
481
+ constructor(httpClient, videoId, transcriptUrl, language, languageCode, isGenerated, translationLanguages) {
482
+ this.httpClient = httpClient;
483
+ this.videoId = videoId;
484
+ this.transcriptUrl = transcriptUrl;
485
+ this.language = language;
486
+ this.languageCode = languageCode;
487
+ this.isGenerated = isGenerated;
488
+ this.translationLanguages = translationLanguages;
489
+ this.translationLanguagesMap = this.buildTranslationLanguagesMap(translationLanguages);
490
+ }
491
+ /**
492
+ * Builds a map of language codes to language names for quick lookup.
493
+ *
494
+ * @private
495
+ * @param {TranslationLanguage[]} languages - Array of translation languages
496
+ * @returns {Record<string, string>} Map of language code to language name
497
+ */
498
+ buildTranslationLanguagesMap(languages) {
499
+ const map = {};
500
+ for (const lang of languages) map[lang.language_code] = lang.language;
501
+ return map;
502
+ }
503
+ /**
504
+ * Loads the actual transcript data from YouTube.
505
+ *
506
+ * @param {boolean} [preserveFormatting=false] - Whether to keep select HTML text formatting
507
+ * @returns {Promise<FetchedTranscript>} The fetched transcript with all snippets
508
+ * @throws {PoTokenRequired} If a PO Token is required for this video
509
+ * @throws {YouTubeRequestFailed} If the HTTP request fails
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * const transcript = transcriptList.findTranscript(['en']);
514
+ * const fetched = await transcript.fetch();
515
+ *
516
+ * // With formatting preserved
517
+ * const fetchedWithFormatting = await transcript.fetch(true);
518
+ * ```
519
+ */
520
+ async fetch(preserveFormatting = false) {
521
+ if (this.transcriptUrl.includes("&exp=xpe")) throw new PoTokenRequired(this.videoId);
522
+ const response = await this.httpClient.get(this.transcriptUrl);
523
+ handleHttpErrors(response, this.videoId);
524
+ const xmlData = await response.text();
525
+ return new FetchedTranscript(new TranscriptParser(preserveFormatting).parseTranscriptXml(xmlData), this.videoId, this.language, this.languageCode, this.isGenerated);
526
+ }
527
+ /**
528
+ * Checks whether this transcript can be translated to other languages.
529
+ *
530
+ * @returns {boolean} True if the transcript supports translation
531
+ */
532
+ get isTranslatable() {
533
+ return this.translationLanguages.length > 0;
534
+ }
535
+ /**
536
+ * Creates a translated version of this transcript.
537
+ *
538
+ * @param {string} targetLanguageCode - The target language code (e.g., 'de', 'fr')
539
+ * @returns {Transcript} A new Transcript object for the translated transcript
540
+ * @throws {NotTranslatable} If the transcript cannot be translated
541
+ * @throws {TranslationLanguageNotAvailable} If the requested language is not available
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * const englishTranscript = transcriptList.findTranscript(['en']);
546
+ * if (englishTranscript.isTranslatable) {
547
+ * const germanTranscript = englishTranscript.translate('de');
548
+ * const fetched = await germanTranscript.fetch();
549
+ * }
550
+ * ```
551
+ */
552
+ translate(targetLanguageCode) {
553
+ if (!this.isTranslatable) throw new NotTranslatable(this.videoId);
554
+ if (!(targetLanguageCode in this.translationLanguagesMap)) throw new TranslationLanguageNotAvailable(this.videoId);
555
+ const translatedUrl = `${this.transcriptUrl}&tlang=${targetLanguageCode}`;
556
+ const translatedLanguage = this.translationLanguagesMap[targetLanguageCode];
557
+ return new Transcript(this.httpClient, this.videoId, translatedUrl, translatedLanguage, targetLanguageCode, true, []);
558
+ }
559
+ /**
560
+ * Returns a string representation of the transcript.
561
+ *
562
+ * @returns {string} A formatted string representation
563
+ */
564
+ toString() {
565
+ const translatableFlag = this.isTranslatable ? "[TRANSLATABLE]" : "";
566
+ return `${this.languageCode} ("${this.language}")${translatableFlag}`;
567
+ }
568
+ };
569
+ //#endregion
570
+ //#region src/models/transcript-list.ts
571
+ /**
572
+ * Represents a list of available transcripts for a video.
573
+ * This object is iterable and provides methods to search for specific transcripts.
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * const transcriptList = await api.list('video_id');
578
+ *
579
+ * // Iterate over all transcripts
580
+ * for (const transcript of transcriptList) {
581
+ * console.log(transcript.language, transcript.languageCode);
582
+ * }
583
+ *
584
+ * // Find a specific transcript
585
+ * const transcript = transcriptList.findTranscript(['de', 'en']);
586
+ * ```
587
+ */
588
+ var TranscriptList = class TranscriptList {
589
+ /**
590
+ * Creates a new TranscriptList instance.
591
+ * The constructor is only for internal use. Use the static buildFromCaptionsJson method instead.
592
+ *
593
+ * @param {string} videoId - The video ID
594
+ * @param {Record<string, Transcript>} manuallyCreatedTranscripts - Map of manually created transcripts
595
+ * @param {Record<string, Transcript>} autoGeneratedTranscripts - Map of auto-generated transcripts
596
+ * @param {TranslationLanguage[]} availableTranslationLanguages - Available translation languages
597
+ */
598
+ constructor(videoId, manuallyCreatedTranscripts, autoGeneratedTranscripts, availableTranslationLanguages) {
599
+ this.videoId = videoId;
600
+ this.manuallyCreatedTranscripts = manuallyCreatedTranscripts;
601
+ this.autoGeneratedTranscripts = autoGeneratedTranscripts;
602
+ this.availableTranslationLanguages = availableTranslationLanguages;
603
+ }
604
+ /**
605
+ * Factory method to build a TranscriptList from YouTube's captions JSON data.
606
+ *
607
+ * @param {HttpClient} httpClient - HTTP client for making requests
608
+ * @param {string} videoId - The video ID
609
+ * @param {CaptionsJson} captionsJson - The JSON parsed from YouTube's response
610
+ * @returns {TranscriptList} The created TranscriptList
611
+ */
612
+ static buildFromCaptionsJson(httpClient, videoId, captionsJson) {
613
+ const translationLanguages = TranscriptList.extractTranslationLanguages(captionsJson);
614
+ const { manualTranscripts, generatedTranscripts } = TranscriptList.categorizeTranscripts(httpClient, videoId, captionsJson, translationLanguages);
615
+ return new TranscriptList(videoId, manualTranscripts, generatedTranscripts, translationLanguages);
616
+ }
617
+ /**
618
+ * Extracts translation languages from the captions JSON.
619
+ *
620
+ * @private
621
+ * @param {CaptionsJson} captionsJson - The captions JSON data
622
+ * @returns {TranslationLanguage[]} Array of translation languages
623
+ */
624
+ static extractTranslationLanguages(captionsJson) {
625
+ return (captionsJson.translationLanguages || []).map((lang) => ({
626
+ language: lang.languageName.runs[0].text,
627
+ language_code: lang.languageCode
628
+ }));
629
+ }
630
+ /**
631
+ * Categorizes transcripts into manually created and auto-generated.
632
+ *
633
+ * @private
634
+ * @param {HttpClient} httpClient - HTTP client for making requests
635
+ * @param {string} videoId - The video ID
636
+ * @param {CaptionsJson} captionsJson - The captions JSON data
637
+ * @param {TranslationLanguage[]} translationLanguages - Available translation languages
638
+ * @returns {{manualTranscripts: Record<string, Transcript>, generatedTranscripts: Record<string, Transcript>}}
639
+ */
640
+ static categorizeTranscripts(httpClient, videoId, captionsJson, translationLanguages) {
641
+ const manualTranscripts = {};
642
+ const generatedTranscripts = {};
643
+ for (const captionTrack of captionsJson.captionTracks) {
644
+ const isAutoGenerated = captionTrack.kind === "asr";
645
+ const targetMap = isAutoGenerated ? generatedTranscripts : manualTranscripts;
646
+ const transcript = new Transcript(httpClient, videoId, captionTrack.baseUrl.replace("&fmt=srv3", ""), captionTrack.name.runs[0].text, captionTrack.languageCode, isAutoGenerated, captionTrack.isTranslatable ? translationLanguages : []);
647
+ targetMap[captionTrack.languageCode] = transcript;
648
+ }
649
+ return {
650
+ manualTranscripts,
651
+ generatedTranscripts
652
+ };
653
+ }
654
+ /**
655
+ * Makes the transcript list iterable, allowing for...of loops.
656
+ * Manual transcripts are yielded first, followed by auto-generated transcripts.
657
+ *
658
+ * @returns {Iterator<Transcript>} Iterator over all transcripts
659
+ */
660
+ [Symbol.iterator]() {
661
+ const manualTranscripts = Object.values(this.manuallyCreatedTranscripts);
662
+ const generatedTranscripts = Object.values(this.autoGeneratedTranscripts);
663
+ return [...manualTranscripts, ...generatedTranscripts][Symbol.iterator]();
664
+ }
665
+ /**
666
+ * Finds a transcript for given language codes. Manually created transcripts are
667
+ * returned first and only if none are found, auto-generated transcripts are used.
668
+ *
669
+ * @param {string[]} languageCodes - A list of language codes in descending priority
670
+ * @returns {Transcript} The found transcript
671
+ * @throws {NoTranscriptFound} If no transcript is found for any of the language codes
672
+ *
673
+ * @example
674
+ * ```typescript
675
+ * // Try German first, then English
676
+ * const transcript = transcriptList.findTranscript(['de', 'en']);
677
+ * ```
678
+ */
679
+ findTranscript(languageCodes) {
680
+ return this.findTranscriptInMaps(languageCodes, [this.manuallyCreatedTranscripts, this.autoGeneratedTranscripts]);
681
+ }
682
+ /**
683
+ * Finds an automatically generated transcript for given language codes.
684
+ *
685
+ * @param {string[]} languageCodes - A list of language codes in descending priority
686
+ * @returns {Transcript} The found auto-generated transcript
687
+ * @throws {NoTranscriptFound} If no auto-generated transcript is found
688
+ *
689
+ * @example
690
+ * ```typescript
691
+ * const transcript = transcriptList.findGeneratedTranscript(['en']);
692
+ * ```
693
+ */
694
+ findGeneratedTranscript(languageCodes) {
695
+ return this.findTranscriptInMaps(languageCodes, [this.autoGeneratedTranscripts]);
696
+ }
697
+ /**
698
+ * Finds a manually created transcript for given language codes.
699
+ *
700
+ * @param {string[]} languageCodes - A list of language codes in descending priority
701
+ * @returns {Transcript} The found manually created transcript
702
+ * @throws {NoTranscriptFound} If no manually created transcript is found
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * const transcript = transcriptList.findManuallyCreatedTranscript(['en']);
707
+ * ```
708
+ */
709
+ findManuallyCreatedTranscript(languageCodes) {
710
+ return this.findTranscriptInMaps(languageCodes, [this.manuallyCreatedTranscripts]);
711
+ }
712
+ /**
713
+ * Internal method to find a transcript by searching through provided maps.
714
+ *
715
+ * @private
716
+ * @param {string[]} languageCodes - Language codes to search for
717
+ * @param {Record<string, Transcript>[]} transcriptMaps - Maps to search through
718
+ * @returns {Transcript} The found transcript
719
+ * @throws {NoTranscriptFound} If no transcript is found
720
+ */
721
+ findTranscriptInMaps(languageCodes, transcriptMaps) {
722
+ for (const languageCode of languageCodes) for (const transcriptMap of transcriptMaps) if (languageCode in transcriptMap) return transcriptMap[languageCode];
723
+ throw new NoTranscriptFound(this.videoId, languageCodes, this);
724
+ }
725
+ /**
726
+ * Returns a formatted string representation of all available transcripts.
727
+ *
728
+ * @returns {string} A formatted string showing all available transcripts
729
+ */
730
+ toString() {
731
+ const formatTranscriptMap = (transcripts) => {
732
+ const descriptions = Object.values(transcripts).map((t) => ` - ${t.toString()}`);
733
+ return descriptions.length > 0 ? descriptions.join("\n") : "None";
734
+ };
735
+ const formatTranslationLanguages = () => {
736
+ const descriptions = this.availableTranslationLanguages.map((lang) => ` - ${lang.language_code} ("${lang.language}")`);
737
+ return descriptions.length > 0 ? descriptions.join("\n") : "None";
738
+ };
739
+ return `For this video (${this.videoId}) transcripts are available in the following languages:\n\n(MANUALLY CREATED)\n${formatTranscriptMap(this.manuallyCreatedTranscripts)}\n\n(GENERATED)\n${formatTranscriptMap(this.autoGeneratedTranscripts)}\n\n(TRANSLATION LANGUAGES)\n${formatTranslationLanguages()}`;
740
+ }
741
+ };
742
+ //#endregion
743
+ //#region src/types.ts
744
+ /**
745
+ * Playability status enum values
746
+ */
747
+ var PlayabilityStatus = /* @__PURE__ */ function(PlayabilityStatus) {
748
+ PlayabilityStatus["OK"] = "OK";
749
+ PlayabilityStatus["ERROR"] = "ERROR";
750
+ PlayabilityStatus["LOGIN_REQUIRED"] = "LOGIN_REQUIRED";
751
+ return PlayabilityStatus;
752
+ }({});
753
+ /**
754
+ * Playability failed reason enum values
755
+ */
756
+ var PlayabilityFailedReason = /* @__PURE__ */ function(PlayabilityFailedReason) {
757
+ PlayabilityFailedReason["BOT_DETECTED"] = "Sign in to confirm you're not a bot";
758
+ PlayabilityFailedReason["AGE_RESTRICTED"] = "This video may be inappropriate for some users.";
759
+ PlayabilityFailedReason["VIDEO_UNAVAILABLE"] = "This video is unavailable";
760
+ return PlayabilityFailedReason;
761
+ }({});
762
+ //#endregion
763
+ //#region src/fetchers/youtube-data-extractor.ts
764
+ /**
765
+ * @fileoverview Utilities for extracting data from YouTube HTML and InnerTube API responses
766
+ */
767
+ /**
768
+ * Extracts the InnerTube API key from YouTube's HTML page.
769
+ *
770
+ * @param {string} htmlContent - The HTML content from YouTube's watch page
771
+ * @param {string} videoId - The video ID being processed
772
+ * @returns {string} The extracted API key
773
+ * @throws {IpBlocked} If the page contains a reCAPTCHA (IP is blocked)
774
+ * @throws {YouTubeDataUnparsable} If the API key cannot be found
775
+ *
776
+ * @example
777
+ * ```typescript
778
+ * const html = await fetchVideoHtml(videoId);
779
+ * const apiKey = extractInnertubeApiKey(html, videoId);
780
+ * ```
781
+ */
782
+ function extractInnertubeApiKey(htmlContent, videoId) {
783
+ const match = htmlContent.match(/"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"/);
784
+ if (match && match.length === 2) return match[1];
785
+ if (htmlContent.includes("class=\"g-recaptcha\"")) throw new IpBlocked(videoId);
786
+ throw new YouTubeDataUnparsable(videoId);
787
+ }
788
+ /**
789
+ * Extracts captions JSON from YouTube's InnerTube API response.
790
+ *
791
+ * @param {InnerTubeData} innertubeData - The InnerTube API response data
792
+ * @param {string} videoId - The video ID being processed
793
+ * @returns {CaptionsJson} The extracted captions data
794
+ * @throws {TranscriptsDisabled} If captions are disabled for the video
795
+ *
796
+ * @example
797
+ * ```typescript
798
+ * const innertubeData = await fetchInnertubeData(videoId, apiKey);
799
+ * const captionsJson = extractCaptionsJson(innertubeData, videoId);
800
+ * ```
801
+ */
802
+ function extractCaptionsJson(innertubeData, videoId) {
803
+ validatePlayabilityStatus(innertubeData.playabilityStatus, videoId);
804
+ const captionsJson = innertubeData.captions?.playerCaptionsTracklistRenderer;
805
+ if (!captionsJson || !captionsJson.captionTracks) throw new TranscriptsDisabled(videoId);
806
+ return captionsJson;
807
+ }
808
+ /**
809
+ * Validates the playability status of a video and throws appropriate errors.
810
+ *
811
+ * @param {any} playabilityStatusData - The playability status data from InnerTube
812
+ * @param {string} videoId - The video ID being validated
813
+ * @throws {RequestBlocked} If YouTube detected a bot
814
+ * @throws {AgeRestricted} If the video is age-restricted
815
+ * @throws {VideoUnavailable} If the video is unavailable
816
+ * @throws {InvalidVideoId} If the video ID is invalid (URL was provided instead)
817
+ * @throws {VideoUnplayable} If the video is unplayable for any other reason
818
+ *
819
+ * @example
820
+ * ```typescript
821
+ * validatePlayabilityStatus(innertubeData.playabilityStatus, videoId);
822
+ * ```
823
+ */
824
+ function validatePlayabilityStatus(playabilityStatusData, videoId) {
825
+ if (!playabilityStatusData) return;
826
+ const playabilityStatus = playabilityStatusData.status;
827
+ if (playabilityStatus === PlayabilityStatus.OK || !playabilityStatus) return;
828
+ const reason = playabilityStatusData.reason;
829
+ if (playabilityStatus === PlayabilityStatus.LOGIN_REQUIRED) {
830
+ if (reason === PlayabilityFailedReason.BOT_DETECTED) throw new RequestBlocked(videoId);
831
+ if (reason === PlayabilityFailedReason.AGE_RESTRICTED) throw new AgeRestricted(videoId);
832
+ }
833
+ if (playabilityStatus === PlayabilityStatus.ERROR && reason === PlayabilityFailedReason.VIDEO_UNAVAILABLE) {
834
+ if (videoId.startsWith("http://") || videoId.startsWith("https://")) throw new InvalidVideoId(videoId);
835
+ throw new VideoUnavailable(videoId);
836
+ }
837
+ throw new VideoUnplayable(videoId, reason, extractPlayabilitySubreasons(playabilityStatusData));
838
+ }
839
+ /**
840
+ * Extracts sub-reasons from the playability status error screen.
841
+ *
842
+ * @private
843
+ * @param {any} playabilityStatusData - The playability status data
844
+ * @returns {string[]} Array of sub-reason messages
845
+ */
846
+ function extractPlayabilitySubreasons(playabilityStatusData) {
847
+ const runs = playabilityStatusData.errorScreen?.playerErrorMessageRenderer?.subreason?.runs;
848
+ if (!runs || !Array.isArray(runs)) return [];
849
+ return runs.map((run) => run.text || "").filter((text) => text.length > 0);
850
+ }
851
+ //#endregion
852
+ //#region src/fetchers/transcript-list-fetcher.ts
853
+ /**
854
+ * @fileoverview Fetcher for retrieving transcript lists from YouTube
855
+ */
856
+ /** YouTube watch page URL template */
857
+ var YOUTUBE_WATCH_URL = "https://www.youtube.com/watch?v={video_id}";
858
+ /** YouTube InnerTube API URL template */
859
+ var INNERTUBE_API_URL = "https://www.youtube.com/youtubei/v1/player?key={api_key}";
860
+ /** InnerTube API context configuration for requests */
861
+ var INNERTUBE_CONTEXT = { client: {
862
+ clientName: "ANDROID",
863
+ clientVersion: "20.10.38"
864
+ } };
865
+ /**
866
+ * Fetches transcript lists from YouTube for given video IDs.
867
+ * Handles consent cookies, retries on blocks, and extracts caption data.
868
+ *
869
+ * @example
870
+ * ```typescript
871
+ * const fetcher = new TranscriptListFetcher(httpClient, proxyConfig);
872
+ * const transcriptList = await fetcher.fetchTranscriptList('dQw4w9WgXcQ');
873
+ * ```
874
+ */
875
+ var TranscriptListFetcher = class {
876
+ /**
877
+ * Creates a new TranscriptListFetcher instance.
878
+ *
879
+ * @param {HttpClient} httpClient - The HTTP client to use for requests
880
+ * @param {ProxyConfig | null} proxyConfig - Optional proxy configuration
881
+ */
882
+ constructor(httpClient, proxyConfig) {
883
+ this.httpClient = httpClient;
884
+ this.proxyConfig = proxyConfig;
885
+ }
886
+ /**
887
+ * Fetches the list of available transcripts for a video.
888
+ *
889
+ * @param {string} videoId - The YouTube video ID
890
+ * @returns {Promise<TranscriptList>} The transcript list
891
+ * @throws {RequestBlocked} If requests are blocked and retries exhausted
892
+ * @throws {IpBlocked} If IP is blocked by YouTube
893
+ * @throws {YouTubeDataUnparsable} If YouTube data cannot be parsed
894
+ * @throws {TranscriptsDisabled} If transcripts are disabled for the video
895
+ *
896
+ * @example
897
+ * ```typescript
898
+ * const transcriptList = await fetcher.fetchTranscriptList('dQw4w9WgXcQ');
899
+ * const transcript = transcriptList.findTranscript(['en']);
900
+ * ```
901
+ */
902
+ async fetchTranscriptList(videoId) {
903
+ const captionsJson = await this.fetchCaptionsJsonWithRetry(videoId);
904
+ return TranscriptList.buildFromCaptionsJson(this.httpClient, videoId, captionsJson);
905
+ }
906
+ /**
907
+ * Fetches captions JSON with retry logic for blocked requests.
908
+ *
909
+ * @private
910
+ * @param {string} videoId - The video ID
911
+ * @param {number} [attemptNumber=0] - Current attempt number (for retry logic)
912
+ * @returns {Promise<CaptionsJson>} The captions JSON data
913
+ */
914
+ async fetchCaptionsJsonWithRetry(videoId, attemptNumber = 0) {
915
+ try {
916
+ const apiKey = extractInnertubeApiKey(await this.fetchVideoPageHtml(videoId), videoId);
917
+ return extractCaptionsJson(await this.fetchInnertubePlayerData(videoId, apiKey), videoId);
918
+ } catch (error) {
919
+ if (error instanceof RequestBlocked) return this.handleBlockedRequest(error, videoId, attemptNumber);
920
+ throw error;
921
+ }
922
+ }
923
+ /**
924
+ * Handles blocked requests with retry logic based on proxy configuration.
925
+ *
926
+ * @private
927
+ * @param {RequestBlocked} error - The blocked request error
928
+ * @param {string} videoId - The video ID
929
+ * @param {number} attemptNumber - Current attempt number
930
+ * @returns {Promise<CaptionsJson>} The captions JSON data from retry
931
+ * @throws {RequestBlocked} If retries are exhausted
932
+ */
933
+ async handleBlockedRequest(error, videoId, attemptNumber) {
934
+ const maxRetries = this.proxyConfig?.retriesWhenBlocked || 0;
935
+ if (attemptNumber + 1 < maxRetries) return this.fetchCaptionsJsonWithRetry(videoId, attemptNumber + 1);
936
+ throw error.withProxyConfig(this.proxyConfig);
937
+ }
938
+ /**
939
+ * Fetches the HTML content of a video's watch page, handling consent cookies.
940
+ *
941
+ * @private
942
+ * @param {string} videoId - The video ID
943
+ * @returns {Promise<string>} The HTML content
944
+ * @throws {FailedToCreateConsentCookie} If consent cookie creation fails
945
+ */
946
+ async fetchVideoPageHtml(videoId) {
947
+ let htmlContent = await this.fetchAndDecodeHtml(videoId);
948
+ if (this.isConsentPageDetected(htmlContent)) {
949
+ this.createConsentCookie(htmlContent, videoId);
950
+ htmlContent = await this.fetchAndDecodeHtml(videoId);
951
+ if (this.isConsentPageDetected(htmlContent)) throw new FailedToCreateConsentCookie(videoId);
952
+ }
953
+ return htmlContent;
954
+ }
955
+ /**
956
+ * Checks if the HTML content is a consent page.
957
+ *
958
+ * @private
959
+ * @param {string} htmlContent - The HTML content to check
960
+ * @returns {boolean} True if consent page detected
961
+ */
962
+ isConsentPageDetected(htmlContent) {
963
+ return htmlContent.includes("action=\"https://consent.youtube.com/s\"");
964
+ }
965
+ /**
966
+ * Fetches HTML from YouTube watch page and decodes HTML entities.
967
+ *
968
+ * @private
969
+ * @param {string} videoId - The video ID
970
+ * @returns {Promise<string>} The decoded HTML content
971
+ */
972
+ async fetchAndDecodeHtml(videoId) {
973
+ const url = this.buildWatchUrl(videoId);
974
+ const response = await this.httpClient.get(url);
975
+ handleHttpErrors(response, videoId);
976
+ return (0, html_entities.decode)(await response.text());
977
+ }
978
+ /**
979
+ * Builds the YouTube watch URL for a video ID.
980
+ *
981
+ * @private
982
+ * @param {string} videoId - The video ID
983
+ * @returns {string} The watch URL
984
+ */
985
+ buildWatchUrl(videoId) {
986
+ return YOUTUBE_WATCH_URL.replace("{video_id}", videoId);
987
+ }
988
+ /**
989
+ * Creates a consent cookie from the HTML content.
990
+ * Note: This is a placeholder - cookie handling should be implemented in the HTTP client.
991
+ *
992
+ * @private
993
+ * @param {string} htmlContent - The HTML content containing consent form
994
+ * @param {string} videoId - The video ID
995
+ * @throws {FailedToCreateConsentCookie} If consent value cannot be extracted
996
+ */
997
+ createConsentCookie(htmlContent, videoId) {
998
+ if (!htmlContent.match(/name="v" value="(.*?)"/)) throw new FailedToCreateConsentCookie(videoId);
999
+ }
1000
+ /**
1001
+ * Fetches player data from YouTube's InnerTube API.
1002
+ *
1003
+ * @private
1004
+ * @param {string} videoId - The video ID
1005
+ * @param {string} apiKey - The InnerTube API key
1006
+ * @returns {Promise<InnerTubeData>} The InnerTube player data
1007
+ */
1008
+ async fetchInnertubePlayerData(videoId, apiKey) {
1009
+ const url = this.buildInnertubeApiUrl(apiKey);
1010
+ const requestBody = this.buildInnertubeRequestBody(videoId);
1011
+ const response = await this.httpClient.post(url, {
1012
+ headers: { "Content-Type": "application/json" },
1013
+ body: JSON.stringify(requestBody)
1014
+ });
1015
+ handleHttpErrors(response, videoId);
1016
+ return await response.json();
1017
+ }
1018
+ /**
1019
+ * Builds the InnerTube API URL with the provided API key.
1020
+ *
1021
+ * @private
1022
+ * @param {string} apiKey - The API key
1023
+ * @returns {string} The complete API URL
1024
+ */
1025
+ buildInnertubeApiUrl(apiKey) {
1026
+ return INNERTUBE_API_URL.replace("{api_key}", apiKey);
1027
+ }
1028
+ /**
1029
+ * Builds the request body for InnerTube API calls.
1030
+ *
1031
+ * @private
1032
+ * @param {string} videoId - The video ID
1033
+ * @returns {object} The request body
1034
+ */
1035
+ buildInnertubeRequestBody(videoId) {
1036
+ return {
1037
+ context: INNERTUBE_CONTEXT,
1038
+ videoId
1039
+ };
1040
+ }
1041
+ };
1042
+ //#endregion
1043
+ //#region src/http/fetch-http-client.ts
1044
+ /**
1045
+ * @fileoverview HTTP client implementation using node-fetch
1046
+ */
1047
+ /**
1048
+ * Simple HTTP client implementation using node-fetch with proxy support.
1049
+ * This client handles HTTP/HTTPS requests with configurable headers and proxy settings.
1050
+ *
1051
+ * @internal
1052
+ */
1053
+ var FetchHttpClient = class {
1054
+ /**
1055
+ * Creates a new FetchHttpClient instance.
1056
+ * Sets default Accept-Language header to English.
1057
+ */
1058
+ constructor() {
1059
+ this.requestHeaders = {};
1060
+ this.proxyConfiguration = {};
1061
+ this.requestHeaders["Accept-Language"] = "en-US";
1062
+ }
1063
+ /**
1064
+ * Sets a custom header for all requests.
1065
+ *
1066
+ * @param {string} headerName - The header name
1067
+ * @param {string} headerValue - The header value
1068
+ */
1069
+ setHeader(headerName, headerValue) {
1070
+ this.requestHeaders[headerName] = headerValue;
1071
+ }
1072
+ /**
1073
+ * Configures proxy settings for all requests.
1074
+ *
1075
+ * @param {RequestsProxyConfigDict} proxies - Proxy configuration dictionary
1076
+ */
1077
+ setProxies(proxies) {
1078
+ this.proxyConfiguration = proxies;
1079
+ }
1080
+ /**
1081
+ * Performs an HTTP GET request.
1082
+ *
1083
+ * @param {string} url - The URL to request
1084
+ * @param {RequestInit} [options] - Additional request options
1085
+ * @returns {Promise<Response>} The HTTP response
1086
+ */
1087
+ async get(url, options) {
1088
+ const proxyAgent = this.getProxyAgentForUrl(url);
1089
+ return (0, node_fetch.default)(url, {
1090
+ ...options,
1091
+ method: "GET",
1092
+ headers: {
1093
+ ...this.requestHeaders,
1094
+ ...options?.headers
1095
+ },
1096
+ agent: proxyAgent
1097
+ });
1098
+ }
1099
+ /**
1100
+ * Performs an HTTP POST request.
1101
+ *
1102
+ * @param {string} url - The URL to request
1103
+ * @param {RequestInit} [options] - Additional request options
1104
+ * @returns {Promise<Response>} The HTTP response
1105
+ */
1106
+ async post(url, options) {
1107
+ const proxyAgent = this.getProxyAgentForUrl(url);
1108
+ return (0, node_fetch.default)(url, {
1109
+ ...options,
1110
+ method: "POST",
1111
+ headers: {
1112
+ ...this.requestHeaders,
1113
+ ...options?.headers
1114
+ },
1115
+ agent: proxyAgent
1116
+ });
1117
+ }
1118
+ /**
1119
+ * Gets the appropriate proxy agent for a given URL.
1120
+ *
1121
+ * @private
1122
+ * @param {string} url - The URL being requested
1123
+ * @returns {any} The proxy agent or undefined if no proxy is configured
1124
+ */
1125
+ getProxyAgentForUrl(url) {
1126
+ if (Object.keys(this.proxyConfiguration).length === 0) return;
1127
+ const proxyUrl = url.startsWith("https://") ? this.proxyConfiguration.https : this.proxyConfiguration.http;
1128
+ if (!proxyUrl) return;
1129
+ return new https_proxy_agent.HttpsProxyAgent(proxyUrl);
1130
+ }
1131
+ };
1132
+ //#endregion
1133
+ //#region src/youtube-transcript-api.ts
1134
+ /**
1135
+ * Main API class for retrieving YouTube transcripts.
1136
+ * This is the primary entry point for the library.
1137
+ *
1138
+ * @example
1139
+ * ```typescript
1140
+ * // Basic usage
1141
+ * const api = new YouTubeTranscriptApi();
1142
+ * const transcript = await api.fetchTranscript('video_id');
1143
+ *
1144
+ * // With language preference
1145
+ * const transcript = await api.fetchTranscript('video_id', { languages: ['de', 'en'] });
1146
+ *
1147
+ * // With proxy
1148
+ * import { GenericProxyConfig } from 'youtube-transcript-api';
1149
+ * const api = new YouTubeTranscriptApi({
1150
+ * proxyConfig: new GenericProxyConfig({
1151
+ * httpUrl: 'http://user:pass@proxy.example.com:8080'
1152
+ * })
1153
+ * });
1154
+ * ```
1155
+ */
1156
+ var YouTubeTranscriptApi = class {
1157
+ /**
1158
+ * Creates a new YouTubeTranscriptApi instance.
1159
+ *
1160
+ * Note on thread-safety: As this class initializes an HTTP client,
1161
+ * it is not thread-safe. Make sure to initialize an instance per
1162
+ * thread if used in a multi-threading scenario!
1163
+ *
1164
+ * @param {Object} [options] - Configuration options
1165
+ * @param {ProxyConfig} [options.proxyConfig] - An optional ProxyConfig object defining proxies
1166
+ * used for all network requests. This can be used to work around your IP being blocked
1167
+ * by YouTube, as described in the "Working around IP bans" section of the README.
1168
+ * @param {HttpClient} [options.httpClient] - You can optionally pass in a custom HTTP client
1169
+ * if you want to share state between different instances of YouTubeTranscriptApi or
1170
+ * customize request behavior.
1171
+ *
1172
+ * @example
1173
+ * ```typescript
1174
+ * // Basic usage
1175
+ * const api = new YouTubeTranscriptApi();
1176
+ *
1177
+ * // With Webshare proxy
1178
+ * import { WebshareProxyConfig } from 'youtube-transcript-api';
1179
+ * const api = new YouTubeTranscriptApi({
1180
+ * proxyConfig: new WebshareProxyConfig({
1181
+ * proxyUsername: 'your-username',
1182
+ * proxyPassword: 'your-password',
1183
+ * filterIpLocations: ['us', 'de']
1184
+ * })
1185
+ * });
1186
+ *
1187
+ * // With generic proxy
1188
+ * import { GenericProxyConfig } from 'youtube-transcript-api';
1189
+ * const api = new YouTubeTranscriptApi({
1190
+ * proxyConfig: new GenericProxyConfig({
1191
+ * httpUrl: 'http://user:pass@proxy.example.com:8080',
1192
+ * httpsUrl: 'https://user:pass@proxy.example.com:8080'
1193
+ * })
1194
+ * });
1195
+ * ```
1196
+ */
1197
+ constructor(options) {
1198
+ const httpClient = this.initializeHttpClient(options);
1199
+ this.configureHttpClientWithProxy(httpClient, options?.proxyConfig);
1200
+ this.transcriptListFetcher = new TranscriptListFetcher(httpClient, options?.proxyConfig || null);
1201
+ }
1202
+ /**
1203
+ * Initializes the HTTP client from options or creates a default one.
1204
+ *
1205
+ * @private
1206
+ * @param {Object} [options] - Configuration options
1207
+ * @returns {HttpClient} The initialized HTTP client
1208
+ */
1209
+ initializeHttpClient(options) {
1210
+ return options?.httpClient || new FetchHttpClient();
1211
+ }
1212
+ /**
1213
+ * Configures the HTTP client with proxy settings if provided.
1214
+ *
1215
+ * @private
1216
+ * @param {HttpClient} httpClient - The HTTP client to configure
1217
+ * @param {ProxyConfig} [proxyConfig] - Optional proxy configuration
1218
+ */
1219
+ configureHttpClientWithProxy(httpClient, proxyConfig) {
1220
+ if (!proxyConfig || !(httpClient instanceof FetchHttpClient)) return;
1221
+ const proxyDictionary = proxyConfig.toRequestsDict();
1222
+ httpClient.setProxies(proxyDictionary);
1223
+ if (proxyConfig.preventKeepingConnectionsAlive) httpClient.setHeader("Connection", "close");
1224
+ }
1225
+ /**
1226
+ * Retrieves the transcript for a single video.
1227
+ * This is a shortcut for calling:
1228
+ * `api.listTranscripts(videoId).then(list => list.findTranscript(languages)).then(t => t.fetch())`
1229
+ *
1230
+ * @param {string} videoId - The ID of the video you want to retrieve the transcript for.
1231
+ * Make sure that this is the actual ID, NOT the full URL to the video!
1232
+ * @param {Object} [options] - Fetch options
1233
+ * @param {string[]} [options.languages=['en']] - A list of language codes in descending priority.
1234
+ * For example, if this is set to ['de', 'en'] it will first try to fetch the german
1235
+ * transcript (de) and then fetch the english transcript (en) if it fails to do so.
1236
+ * @param {boolean} [options.preserveFormatting=false] - Whether to keep select HTML text formatting
1237
+ * @returns {Promise<FetchedTranscript>} The fetched transcript
1238
+ *
1239
+ * @example
1240
+ * ```typescript
1241
+ * const api = new YouTubeTranscriptApi();
1242
+ *
1243
+ * // Fetch English transcript
1244
+ * const transcript = await api.fetchTranscript('dQw4w9WgXcQ');
1245
+ *
1246
+ * // Fetch with language preference
1247
+ * const transcript = await api.fetchTranscript('dQw4w9WgXcQ', {
1248
+ * languages: ['de', 'en']
1249
+ * });
1250
+ *
1251
+ * // Preserve HTML formatting
1252
+ * const transcript = await api.fetchTranscript('dQw4w9WgXcQ', {
1253
+ * preserveFormatting: true
1254
+ * });
1255
+ *
1256
+ * // Iterate over snippets
1257
+ * for (const snippet of transcript) {
1258
+ * console.log(`${snippet.start}s: ${snippet.text}`);
1259
+ * }
1260
+ * ```
1261
+ */
1262
+ async fetchTranscript(videoId, options) {
1263
+ const preferredLanguages = options?.languages || ["en"];
1264
+ const preserveFormatting = options?.preserveFormatting || false;
1265
+ return (await this.listTranscripts(videoId)).findTranscript(preferredLanguages).fetch(preserveFormatting);
1266
+ }
1267
+ /**
1268
+ * Retrieves the list of transcripts which are available for a given video.
1269
+ * It returns a TranscriptList object which is iterable and provides methods to
1270
+ * filter the list of transcripts for specific languages.
1271
+ *
1272
+ * While iterating over the TranscriptList, the individual transcripts are
1273
+ * represented by Transcript objects, which provide metadata and can either
1274
+ * be fetched by calling transcript.fetch() or translated by calling
1275
+ * transcript.translate('en').
1276
+ *
1277
+ * @param {string} videoId - The ID of the video you want to retrieve the transcript for.
1278
+ * Make sure that this is the actual ID, NOT the full URL to the video!
1279
+ * @returns {Promise<TranscriptList>} A list of available transcripts
1280
+ *
1281
+ * @example
1282
+ * ```typescript
1283
+ * const api = new YouTubeTranscriptApi();
1284
+ *
1285
+ * // Retrieve available transcripts
1286
+ * const transcriptList = await api.listTranscripts('video_id');
1287
+ *
1288
+ * // Iterate over all available transcripts
1289
+ * for (const transcript of transcriptList) {
1290
+ * console.log(
1291
+ * transcript.videoId,
1292
+ * transcript.language,
1293
+ * transcript.languageCode,
1294
+ * transcript.isGenerated,
1295
+ * transcript.isTranslatable,
1296
+ * transcript.translationLanguages
1297
+ * );
1298
+ *
1299
+ * // Fetch the actual transcript data
1300
+ * const fetched = await transcript.fetch();
1301
+ * console.log(fetched);
1302
+ *
1303
+ * // Translate the transcript
1304
+ * if (transcript.isTranslatable) {
1305
+ * const translated = transcript.translate('de');
1306
+ * const fetchedTranslated = await translated.fetch();
1307
+ * console.log(fetchedTranslated);
1308
+ * }
1309
+ * }
1310
+ *
1311
+ * // Filter for specific language
1312
+ * const transcript = transcriptList.findTranscript(['de', 'en']);
1313
+ *
1314
+ * // Filter for manually created transcripts
1315
+ * const manual = transcriptList.findManuallyCreatedTranscript(['de', 'en']);
1316
+ *
1317
+ * // Filter for automatically generated transcripts
1318
+ * const generated = transcriptList.findGeneratedTranscript(['de', 'en']);
1319
+ * ```
1320
+ */
1321
+ async listTranscripts(videoId) {
1322
+ return this.transcriptListFetcher.fetchTranscriptList(videoId);
1323
+ }
1324
+ /**
1325
+ * Alias for fetchTranscript() for backward compatibility.
1326
+ *
1327
+ * @deprecated Use fetchTranscript() instead
1328
+ * @param {string} videoId - The video ID
1329
+ * @param {Object} [options] - Fetch options
1330
+ * @returns {Promise<FetchedTranscript>} The fetched transcript
1331
+ */
1332
+ async fetch(videoId, options) {
1333
+ return this.fetchTranscript(videoId, options);
1334
+ }
1335
+ /**
1336
+ * Alias for listTranscripts() for backward compatibility.
1337
+ *
1338
+ * @deprecated Use listTranscripts() instead
1339
+ * @param {string} videoId - The video ID
1340
+ * @returns {Promise<TranscriptList>} A list of available transcripts
1341
+ */
1342
+ async list(videoId) {
1343
+ return this.listTranscripts(videoId);
1344
+ }
1345
+ };
1346
+ //#endregion
1347
+ //#region src/proxies.ts
1348
+ var _WebshareProxyConfig;
1349
+ /**
1350
+ * Exception for invalid proxy configurations
1351
+ */
1352
+ var InvalidProxyConfig = class InvalidProxyConfig extends Error {
1353
+ constructor(message) {
1354
+ super(message);
1355
+ this.name = "InvalidProxyConfig";
1356
+ Object.setPrototypeOf(this, InvalidProxyConfig.prototype);
1357
+ }
1358
+ };
1359
+ /**
1360
+ * Base class for all proxy configurations.
1361
+ * Any proxy config can be used as long as it can be converted to a RequestsProxyConfigDict.
1362
+ */
1363
+ var ProxyConfig = class {
1364
+ /**
1365
+ * If you are using rotating proxies, it can be useful to prevent the HTTP
1366
+ * client from keeping TCP connections alive, as your IP won't be rotated on
1367
+ * every request if your connection stays open.
1368
+ * @returns {boolean} Whether to prevent keeping connections alive
1369
+ */
1370
+ get preventKeepingConnectionsAlive() {
1371
+ return false;
1372
+ }
1373
+ /**
1374
+ * Defines how many times we should retry if a request is blocked. When using
1375
+ * rotating residential proxies with a large IP pool it can make sense to retry a
1376
+ * couple of times when a blocked IP is encountered, since a retry will trigger
1377
+ * an IP rotation and the next IP might not be blocked.
1378
+ * @returns {number} Number of retries when blocked
1379
+ */
1380
+ get retriesWhenBlocked() {
1381
+ return 0;
1382
+ }
1383
+ };
1384
+ /**
1385
+ * Generic proxy configuration that can be used to set up any HTTP/HTTPS/SOCKS proxy.
1386
+ *
1387
+ * If only an HTTP or an HTTPS proxy is provided, it will be used for both types of
1388
+ * connections. However, you will have to provide at least one of the two.
1389
+ *
1390
+ * @example
1391
+ * ```typescript
1392
+ * const proxyConfig = new GenericProxyConfig({
1393
+ * httpUrl: 'http://user:pass@proxy.example.com:8080',
1394
+ * httpsUrl: 'https://user:pass@proxy.example.com:8080'
1395
+ * });
1396
+ * ```
1397
+ */
1398
+ var GenericProxyConfig = class extends ProxyConfig {
1399
+ /**
1400
+ * Creates a generic proxy configuration.
1401
+ * If only an HTTP or an HTTPS proxy is provided, it will be used for both types of
1402
+ * connections. However, you will have to provide at least one of the two.
1403
+ *
1404
+ * @param {Object} options - Proxy configuration options
1405
+ * @param {string} [options.httpUrl] - The proxy URL used for HTTP requests. Defaults to httpsUrl if not provided.
1406
+ * @param {string} [options.httpsUrl] - The proxy URL used for HTTPS requests. Defaults to httpUrl if not provided.
1407
+ * @throws {InvalidProxyConfig} If neither httpUrl nor httpsUrl is provided
1408
+ */
1409
+ constructor(options) {
1410
+ super();
1411
+ const { httpUrl, httpsUrl } = options;
1412
+ if (!httpUrl && !httpsUrl) throw new InvalidProxyConfig("GenericProxyConfig requires you to define at least one of the two: http or https");
1413
+ this._httpUrl = httpUrl || null;
1414
+ this._httpsUrl = httpsUrl || null;
1415
+ }
1416
+ get httpUrl() {
1417
+ return this._httpUrl;
1418
+ }
1419
+ get httpsUrl() {
1420
+ return this._httpsUrl;
1421
+ }
1422
+ toRequestsDict() {
1423
+ return {
1424
+ http: this.httpUrl || this.httpsUrl,
1425
+ https: this.httpsUrl || this.httpUrl
1426
+ };
1427
+ }
1428
+ };
1429
+ /**
1430
+ * Webshare proxy configuration for rotating residential proxies.
1431
+ *
1432
+ * Webshare is a provider offering rotating residential proxies, which is the
1433
+ * most reliable way to work around being blocked by YouTube.
1434
+ *
1435
+ * If you don't have a Webshare account yet, you will have to create one
1436
+ * at https://www.webshare.io/?referral_code=w0xno53eb50g and purchase a "Residential"
1437
+ * proxy package that suits your workload (make sure NOT to purchase "Proxy Server"
1438
+ * or "Static Residential"!).
1439
+ *
1440
+ * Once you have created an account you only need the "Proxy Username" and
1441
+ * "Proxy Password" that you can find in your Webshare settings to set up this config.
1442
+ *
1443
+ * @example
1444
+ * ```typescript
1445
+ * const proxyConfig = new WebshareProxyConfig({
1446
+ * proxyUsername: 'your-username',
1447
+ * proxyPassword: 'your-password',
1448
+ * filterIpLocations: ['us', 'de']
1449
+ * });
1450
+ * ```
1451
+ */
1452
+ var WebshareProxyConfig = class WebshareProxyConfig extends GenericProxyConfig {
1453
+ /**
1454
+ * Creates a Webshare proxy configuration.
1455
+ *
1456
+ * Once you have created a Webshare account and purchased a "Residential" package
1457
+ * (make sure NOT to purchase "Proxy Server" or "Static Residential"!), this config
1458
+ * class allows you to easily use it by defaulting to the most reliable proxy settings
1459
+ * (rotating residential proxies).
1460
+ *
1461
+ * @param {Object} options - Webshare configuration options
1462
+ * @param {string} options.proxyUsername - "Proxy Username" found at https://dashboard.webshare.io/proxy/settings
1463
+ * @param {string} options.proxyPassword - "Proxy Password" found at https://dashboard.webshare.io/proxy/settings
1464
+ * @param {string[]} [options.filterIpLocations] - If you want to limit the pool of IPs to specific countries,
1465
+ * provide a list of location codes (e.g., ['us', 'de']). This can reduce latency and work around location-based restrictions.
1466
+ * @param {number} [options.retriesWhenBlocked=10] - How many times to retry if a request is blocked
1467
+ * @param {string} [options.domainName] - Custom domain name (defaults to p.webshare.io)
1468
+ * @param {number} [options.proxyPort] - Custom proxy port (defaults to 80)
1469
+ */
1470
+ constructor(options) {
1471
+ super({ httpUrl: "placeholder" });
1472
+ this.proxyUsername = options.proxyUsername;
1473
+ this.proxyPassword = options.proxyPassword;
1474
+ this.domainName = options.domainName || WebshareProxyConfig.DEFAULT_DOMAIN_NAME;
1475
+ this.proxyPort = options.proxyPort || WebshareProxyConfig.DEFAULT_PORT;
1476
+ this.filterIpLocations = options.filterIpLocations || [];
1477
+ this._retriesWhenBlocked = options.retriesWhenBlocked ?? 10;
1478
+ }
1479
+ /**
1480
+ * Gets the full proxy URL with location filtering and rotation settings.
1481
+ * @returns {string} The complete proxy URL
1482
+ */
1483
+ get url() {
1484
+ const locationCodes = this.filterIpLocations.map((code) => `-${code.toUpperCase()}`).join("");
1485
+ let username = this.proxyUsername;
1486
+ const suffix = "-rotate";
1487
+ if (username.endsWith(suffix)) username = username.slice(0, -7);
1488
+ return `http://${username}${locationCodes}${suffix}:${this.proxyPassword}@${this.domainName}:${this.proxyPort}/`;
1489
+ }
1490
+ get httpUrl() {
1491
+ return this.url;
1492
+ }
1493
+ get httpsUrl() {
1494
+ return this.url;
1495
+ }
1496
+ toRequestsDict() {
1497
+ return {
1498
+ http: this.url,
1499
+ https: this.url
1500
+ };
1501
+ }
1502
+ get preventKeepingConnectionsAlive() {
1503
+ return true;
1504
+ }
1505
+ get retriesWhenBlocked() {
1506
+ return this._retriesWhenBlocked;
1507
+ }
1508
+ };
1509
+ _WebshareProxyConfig = WebshareProxyConfig;
1510
+ _WebshareProxyConfig.DEFAULT_DOMAIN_NAME = "p.webshare.io";
1511
+ _WebshareProxyConfig.DEFAULT_PORT = 80;
1512
+ //#endregion
1513
+ //#region src/formatters.ts
1514
+ /**
1515
+ * Base abstract class for all transcript formatters.
1516
+ */
1517
+ var Formatter = class {};
1518
+ /**
1519
+ * Formatter that converts transcripts to pretty-printed JSON format.
1520
+ */
1521
+ var PrettyPrintFormatter = class extends Formatter {
1522
+ formatTranscript(transcript) {
1523
+ return JSON.stringify(transcript.toRawData(), null, 2);
1524
+ }
1525
+ formatTranscripts(transcripts) {
1526
+ return JSON.stringify(transcripts.map((t) => t.toRawData()), null, 2);
1527
+ }
1528
+ };
1529
+ /**
1530
+ * Formatter that converts transcripts to JSON format.
1531
+ */
1532
+ var JSONFormatter = class extends Formatter {
1533
+ formatTranscript(transcript, options) {
1534
+ return JSON.stringify(transcript.toRawData(), null, options?.indent);
1535
+ }
1536
+ formatTranscripts(transcripts, options) {
1537
+ return JSON.stringify(transcripts.map((t) => t.toRawData()), null, options?.indent);
1538
+ }
1539
+ };
1540
+ /**
1541
+ * Formatter that converts transcripts to plain text format with no timestamps.
1542
+ */
1543
+ var TextFormatter = class extends Formatter {
1544
+ formatTranscript(transcript) {
1545
+ return transcript.snippets.map((snippet) => snippet.text).join("\n");
1546
+ }
1547
+ formatTranscripts(transcripts) {
1548
+ return transcripts.map((t) => this.formatTranscript(t)).join("\n\n\n");
1549
+ }
1550
+ };
1551
+ /**
1552
+ * Base class for subtitle formatters (SRT, WebVTT).
1553
+ */
1554
+ var SubtitleFormatterBase = class extends TextFormatter {
1555
+ secondsToTimestamp(timeInSeconds) {
1556
+ const hours = Math.floor(timeInSeconds / 3600);
1557
+ const remainder = timeInSeconds % 3600;
1558
+ const minutes = Math.floor(remainder / 60);
1559
+ const seconds = Math.floor(remainder % 60);
1560
+ const milliseconds = Math.round((timeInSeconds - Math.floor(timeInSeconds)) * 1e3);
1561
+ return this.formatTimestamp(hours, minutes, seconds, milliseconds);
1562
+ }
1563
+ formatTranscript(transcript) {
1564
+ const formattedLines = [];
1565
+ for (let i = 0; i < transcript.snippets.length; i++) {
1566
+ const snippet = transcript.snippets[i];
1567
+ const snippetEndTime = snippet.start + snippet.duration;
1568
+ const actualEndTime = i < transcript.snippets.length - 1 && transcript.snippets[i + 1].start < snippetEndTime ? transcript.snippets[i + 1].start : snippetEndTime;
1569
+ const timeRangeText = `${this.secondsToTimestamp(snippet.start)} --> ${this.secondsToTimestamp(actualEndTime)}`;
1570
+ formattedLines.push(this.formatSubtitleEntry(i, timeRangeText, snippet));
1571
+ }
1572
+ return this.formatTranscriptHeader(formattedLines);
1573
+ }
1574
+ };
1575
+ /**
1576
+ * Formatter that converts transcripts to SRT (SubRip) format.
1577
+ */
1578
+ var SRTFormatter = class extends SubtitleFormatterBase {
1579
+ formatTimestamp(h, m, s, ms) {
1580
+ const pad = (n, w) => n.toString().padStart(w, "0");
1581
+ return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)},${pad(ms, 3)}`;
1582
+ }
1583
+ formatTranscriptHeader(lines) {
1584
+ return lines.join("\n\n") + "\n";
1585
+ }
1586
+ formatSubtitleEntry(index, timeRange, snippet) {
1587
+ return `${index + 1}\n${timeRange}\n${snippet.text}`;
1588
+ }
1589
+ };
1590
+ /**
1591
+ * Formatter that converts transcripts to WebVTT format.
1592
+ */
1593
+ var WebVTTFormatter = class extends SubtitleFormatterBase {
1594
+ formatTimestamp(h, m, s, ms) {
1595
+ const pad = (n, w) => n.toString().padStart(w, "0");
1596
+ return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)}.${pad(ms, 3)}`;
1597
+ }
1598
+ formatTranscriptHeader(lines) {
1599
+ return "WEBVTT\n\n" + lines.join("\n\n") + "\n";
1600
+ }
1601
+ formatSubtitleEntry(index, timeRange, snippet) {
1602
+ return `${timeRange}\n${snippet.text}`;
1603
+ }
1604
+ };
1605
+ //#endregion
1606
+ //#region package.json
1607
+ var package_exports = /* @__PURE__ */ __exportAll({
1608
+ author: () => "",
1609
+ bin: () => bin,
1610
+ default: () => package_default,
1611
+ dependencies: () => dependencies,
1612
+ description: () => description,
1613
+ devDependencies: () => devDependencies,
1614
+ engines: () => engines,
1615
+ exports: () => exports$1,
1616
+ keywords: () => keywords,
1617
+ license: () => "MIT",
1618
+ main: () => main$1,
1619
+ module: () => module$1,
1620
+ name: () => name,
1621
+ scripts: () => scripts,
1622
+ types: () => types,
1623
+ version: () => version
1624
+ }), name, version, description, main$1, module$1, types, exports$1, bin, scripts, keywords, dependencies, devDependencies, engines, package_default;
1625
+ var init_package = __esmMin((() => {
1626
+ name = "extract-youtube";
1627
+ version = "1.0.2";
1628
+ description = "Fast, serverless-optimized YouTube transcript extractor. TypeScript port of the popular Python youtube-transcript-api (100k+ monthly downloads). Works in AWS Lambda, Vercel, Cloudflare Workers. ~7KB gzipped.";
1629
+ main$1 = "dist/index.cjs";
1630
+ module$1 = "dist/index.mjs";
1631
+ types = "dist/index.d.ts";
1632
+ exports$1 = { ".": {
1633
+ "import": "./dist/index.mjs",
1634
+ "require": "./dist/index.cjs",
1635
+ "types": "./dist/index.d.ts"
1636
+ } };
1637
+ bin = { "extract-youtube": "dist/cli.js" };
1638
+ scripts = {
1639
+ "build": "npm run build:types && npm run build:lib && npm run build:cli",
1640
+ "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap",
1641
+ "build:lib": "vite build",
1642
+ "build:cli": "BUILD_CLI=true vite build",
1643
+ "build:bundle": "npm run build:lib && npm run build:cli",
1644
+ "build:legacy": "tsc",
1645
+ "prepare": "npm run build",
1646
+ "ship": "npx standard-version --release-as patch; npm publish",
1647
+ "test": "jest",
1648
+ "lint": "eslint src --ext .ts"
1649
+ };
1650
+ keywords = [
1651
+ "youtube",
1652
+ "transcript",
1653
+ "subtitles",
1654
+ "captions",
1655
+ "api",
1656
+ "serverless",
1657
+ "lambda",
1658
+ "vercel",
1659
+ "cloudflare",
1660
+ "edge",
1661
+ "typescript",
1662
+ "youtube-transcript",
1663
+ "youtube-transcript-api",
1664
+ "srt",
1665
+ "webvtt",
1666
+ "video",
1667
+ "parsing",
1668
+ "extraction",
1669
+ "optimized"
1670
+ ];
1671
+ dependencies = {
1672
+ "fast-xml-parser": "^4.2.5",
1673
+ "html-entities": "^2.3.3",
1674
+ "https-proxy-agent": "^5.0.1",
1675
+ "node-fetch": "^2.6.7",
1676
+ "youtube-transcript-api": "^3.0.6"
1677
+ };
1678
+ devDependencies = {
1679
+ "@types/jest": "^29.5.14",
1680
+ "@types/node": "^18.0.0",
1681
+ "@types/node-fetch": "^2.6.2",
1682
+ "jest": "^29.5.0",
1683
+ "terser": "^5.46.0",
1684
+ "ts-jest": "^29.1.0",
1685
+ "typescript": "^5.0.0",
1686
+ "vite": "^8.0.0"
1687
+ };
1688
+ engines = { "node": ">=14.0.0" };
1689
+ package_default = {
1690
+ name,
1691
+ version,
1692
+ description,
1693
+ main: main$1,
1694
+ module: module$1,
1695
+ types,
1696
+ exports: exports$1,
1697
+ bin,
1698
+ scripts,
1699
+ keywords,
1700
+ author: "",
1701
+ license: "MIT",
1702
+ dependencies,
1703
+ devDependencies,
1704
+ engines
1705
+ };
1706
+ }));
1707
+ //#endregion
1708
+ //#region src/cli.ts
1709
+ /**
1710
+ * @fileoverview CLI for extracting YouTube transcripts
1711
+ */
1712
+ function parseArgs() {
1713
+ const args = process.argv.slice(2);
1714
+ const options = { videoId: "" };
1715
+ for (let i = 0; i < args.length; i++) {
1716
+ const arg = args[i];
1717
+ switch (arg) {
1718
+ case "-h":
1719
+ case "--help":
1720
+ options.help = true;
1721
+ break;
1722
+ case "-v":
1723
+ case "--version":
1724
+ options.version = true;
1725
+ break;
1726
+ case "-l":
1727
+ case "--languages":
1728
+ options.languages = args[++i]?.split(",").map((l) => l.trim());
1729
+ break;
1730
+ case "-f":
1731
+ case "--format":
1732
+ options.format = args[++i];
1733
+ break;
1734
+ case "-p":
1735
+ case "--preserve-formatting":
1736
+ options.preserveFormatting = true;
1737
+ break;
1738
+ case "--proxy":
1739
+ options.proxy = args[++i];
1740
+ break;
1741
+ case "--webshare-user":
1742
+ options.webshareUser = args[++i];
1743
+ break;
1744
+ case "--webshare-pass":
1745
+ options.websharePass = args[++i];
1746
+ break;
1747
+ default:
1748
+ if (!arg.startsWith("-") && !options.videoId) options.videoId = arg;
1749
+ break;
1750
+ }
1751
+ }
1752
+ return options;
1753
+ }
1754
+ function showHelp() {
1755
+ console.log(`
1756
+ extract-youtube - Extract YouTube video transcripts
1757
+
1758
+ USAGE:
1759
+ extract-youtube <video-id> [options]
1760
+
1761
+ OPTIONS:
1762
+ -h, --help Show this help message
1763
+ -v, --version Show version number
1764
+ -l, --languages <codes> Comma-separated language codes (e.g., en,de,fr)
1765
+ -f, --format <type> Output format: json, text, srt, webvtt, pretty (default: json)
1766
+ -p, --preserve-formatting Preserve text formatting (line breaks, etc.)
1767
+ --proxy <url> HTTP/HTTPS proxy URL
1768
+ --webshare-user <username> Webshare proxy username
1769
+ --webshare-pass <password> Webshare proxy password
1770
+
1771
+ EXAMPLES:
1772
+ # Extract transcript in JSON format
1773
+ extract-youtube jNQXAC9IVRw
1774
+
1775
+ # Extract transcript in SRT format
1776
+ extract-youtube jNQXAC9IVRw -f srt
1777
+
1778
+ # Extract transcript with specific languages
1779
+ extract-youtube jNQXAC9IVRw -l en,de
1780
+
1781
+ # Extract transcript with proxy
1782
+ extract-youtube jNQXAC9IVRw --proxy http://proxy.example.com:8080
1783
+
1784
+ # Extract transcript with Webshare proxy
1785
+ extract-youtube jNQXAC9IVRw --webshare-user myuser --webshare-pass mypass
1786
+
1787
+ # Extract transcript as plain text with preserved formatting
1788
+ extract-youtube jNQXAC9IVRw -f text -p
1789
+ `);
1790
+ }
1791
+ function showVersion() {
1792
+ const pkg = (init_package(), __toCommonJS(package_exports).default);
1793
+ console.log(`extract-youtube v${pkg.version}`);
1794
+ }
1795
+ async function main() {
1796
+ const options = parseArgs();
1797
+ if (options.help) {
1798
+ showHelp();
1799
+ process.exit(0);
1800
+ }
1801
+ if (options.version) {
1802
+ showVersion();
1803
+ process.exit(0);
1804
+ }
1805
+ if (!options.videoId) {
1806
+ console.error("Error: Video ID is required\n");
1807
+ showHelp();
1808
+ process.exit(1);
1809
+ }
1810
+ try {
1811
+ let proxyConfig;
1812
+ if (options.webshareUser && options.websharePass) proxyConfig = new WebshareProxyConfig({
1813
+ proxyUsername: options.webshareUser,
1814
+ proxyPassword: options.websharePass
1815
+ });
1816
+ else if (options.proxy) proxyConfig = new GenericProxyConfig({ httpUrl: options.proxy });
1817
+ const transcript = await new YouTubeTranscriptApi({ proxyConfig }).fetchTranscript(options.videoId, {
1818
+ languages: options.languages,
1819
+ preserveFormatting: options.preserveFormatting
1820
+ });
1821
+ const format = options.format || "json";
1822
+ let output;
1823
+ switch (format) {
1824
+ case "json":
1825
+ output = new JSONFormatter().formatTranscript(transcript);
1826
+ break;
1827
+ case "text":
1828
+ output = new TextFormatter().formatTranscript(transcript);
1829
+ break;
1830
+ case "srt":
1831
+ output = new SRTFormatter().formatTranscript(transcript);
1832
+ break;
1833
+ case "webvtt":
1834
+ output = new WebVTTFormatter().formatTranscript(transcript);
1835
+ break;
1836
+ case "pretty":
1837
+ output = new PrettyPrintFormatter().formatTranscript(transcript);
1838
+ break;
1839
+ default: throw new Error(`Unknown format: ${format}`);
1840
+ }
1841
+ console.log(output);
1842
+ process.exit(0);
1843
+ } catch (error) {
1844
+ if (error instanceof Error) console.error(`Error: ${error.message}`);
1845
+ else console.error(`Error: ${String(error)}`);
1846
+ process.exit(1);
1847
+ }
1848
+ }
1849
+ main();
1850
+ //#endregion
1851
+
1852
+ //# sourceMappingURL=cli.js.map