extract-youtube 1.0.538 → 1.0.539

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