@mks2508/bundlp 0.1.1 → 0.1.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.
Files changed (86) hide show
  1. package/dist/index.d.mts +614 -0
  2. package/dist/index.d.mts.map +1 -0
  3. package/dist/index.mjs +3072 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/package.json +18 -8
  6. package/dist/core/extractor.d.ts +0 -30
  7. package/dist/core/extractor.d.ts.map +0 -1
  8. package/dist/http/client.d.ts +0 -50
  9. package/dist/http/client.d.ts.map +0 -1
  10. package/dist/http/retry.d.ts +0 -22
  11. package/dist/http/retry.d.ts.map +0 -1
  12. package/dist/index.d.ts +0 -20
  13. package/dist/index.d.ts.map +0 -1
  14. package/dist/index.js +0 -19021
  15. package/dist/innertube/client.d.ts +0 -62
  16. package/dist/innertube/client.d.ts.map +0 -1
  17. package/dist/player/ast/analyzer.d.ts +0 -16
  18. package/dist/player/ast/analyzer.d.ts.map +0 -1
  19. package/dist/player/ast/extractor.d.ts +0 -35
  20. package/dist/player/ast/extractor.d.ts.map +0 -1
  21. package/dist/player/ast/matchers.d.ts +0 -40
  22. package/dist/player/ast/matchers.d.ts.map +0 -1
  23. package/dist/player/cache.d.ts +0 -60
  24. package/dist/player/cache.d.ts.map +0 -1
  25. package/dist/player/player.d.ts +0 -49
  26. package/dist/player/player.d.ts.map +0 -1
  27. package/dist/po-token/botguard/challenge.d.ts +0 -22
  28. package/dist/po-token/botguard/challenge.d.ts.map +0 -1
  29. package/dist/po-token/botguard/client.d.ts +0 -25
  30. package/dist/po-token/botguard/client.d.ts.map +0 -1
  31. package/dist/po-token/cache/token-cache.d.ts +0 -24
  32. package/dist/po-token/cache/token-cache.d.ts.map +0 -1
  33. package/dist/po-token/index.d.ts +0 -14
  34. package/dist/po-token/index.d.ts.map +0 -1
  35. package/dist/po-token/manager.d.ts +0 -34
  36. package/dist/po-token/manager.d.ts.map +0 -1
  37. package/dist/po-token/minter/web-minter.d.ts +0 -20
  38. package/dist/po-token/minter/web-minter.d.ts.map +0 -1
  39. package/dist/po-token/policies.d.ts +0 -18
  40. package/dist/po-token/policies.d.ts.map +0 -1
  41. package/dist/po-token/providers/local.provider.d.ts +0 -26
  42. package/dist/po-token/providers/local.provider.d.ts.map +0 -1
  43. package/dist/po-token/providers/provider.interface.d.ts +0 -15
  44. package/dist/po-token/providers/provider.interface.d.ts.map +0 -1
  45. package/dist/po-token/types.d.ts +0 -160
  46. package/dist/po-token/types.d.ts.map +0 -1
  47. package/dist/result/index.d.ts +0 -6
  48. package/dist/result/index.d.ts.map +0 -1
  49. package/dist/result/result.types.d.ts +0 -14
  50. package/dist/result/result.types.d.ts.map +0 -1
  51. package/dist/result/result.utils.d.ts +0 -32
  52. package/dist/result/result.utils.d.ts.map +0 -1
  53. package/dist/streaming/dash/parser.d.ts +0 -37
  54. package/dist/streaming/dash/parser.d.ts.map +0 -1
  55. package/dist/streaming/dash/segments.d.ts +0 -58
  56. package/dist/streaming/dash/segments.d.ts.map +0 -1
  57. package/dist/streaming/decipher.d.ts +0 -24
  58. package/dist/streaming/decipher.d.ts.map +0 -1
  59. package/dist/streaming/drm.d.ts +0 -26
  60. package/dist/streaming/drm.d.ts.map +0 -1
  61. package/dist/streaming/formats.d.ts +0 -20
  62. package/dist/streaming/formats.d.ts.map +0 -1
  63. package/dist/streaming/hls/parser.d.ts +0 -10
  64. package/dist/streaming/hls/parser.d.ts.map +0 -1
  65. package/dist/streaming/hls/segments.d.ts +0 -37
  66. package/dist/streaming/hls/segments.d.ts.map +0 -1
  67. package/dist/streaming/processor.d.ts +0 -20
  68. package/dist/streaming/processor.d.ts.map +0 -1
  69. package/dist/types/error.types.d.ts +0 -12
  70. package/dist/types/error.types.d.ts.map +0 -1
  71. package/dist/types/index.d.ts +0 -8
  72. package/dist/types/index.d.ts.map +0 -1
  73. package/dist/types/innertube.types.d.ts +0 -155
  74. package/dist/types/innertube.types.d.ts.map +0 -1
  75. package/dist/types/player.types.d.ts +0 -30
  76. package/dist/types/player.types.d.ts.map +0 -1
  77. package/dist/types/video.types.d.ts +0 -112
  78. package/dist/types/video.types.d.ts.map +0 -1
  79. package/dist/utils/constants.d.ts +0 -129
  80. package/dist/utils/constants.d.ts.map +0 -1
  81. package/dist/utils/m3u8.d.ts +0 -31
  82. package/dist/utils/m3u8.d.ts.map +0 -1
  83. package/dist/utils/xml.d.ts +0 -41
  84. package/dist/utils/xml.d.ts.map +0 -1
  85. package/dist/validation/schemas.d.ts +0 -290
  86. package/dist/validation/schemas.d.ts.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,3072 @@
1
+ import { type } from "arktype";
2
+ import { parse } from "meriyah";
3
+ import { Database } from "bun:sqlite";
4
+ import { mkdirSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { JSDOM } from "jsdom";
7
+ import logger from "@mks2508/better-logger";
8
+
9
+ //#region src/result/result.utils.ts
10
+ /** Create a successful result */
11
+ const ok = (value) => ({
12
+ ok: true,
13
+ value
14
+ });
15
+ /** Create an error result */
16
+ const err = (error) => ({
17
+ ok: false,
18
+ error
19
+ });
20
+ /** Type guard for successful result */
21
+ const isOk = (result) => result.ok;
22
+ /** Type guard for error result */
23
+ const isErr = (result) => !result.ok;
24
+ /** Pattern match on a Result */
25
+ function match(result, handlers) {
26
+ return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
27
+ }
28
+ /** Map over a successful result */
29
+ function map(result, fn) {
30
+ return result.ok ? ok(fn(result.value)) : result;
31
+ }
32
+ /** Map over an error result */
33
+ function mapErr(result, fn) {
34
+ return result.ok ? result : err(fn(result.error));
35
+ }
36
+ /** Chain Results (flatMap) */
37
+ function andThen(result, fn) {
38
+ return result.ok ? fn(result.value) : result;
39
+ }
40
+ /** Unwrap with default value */
41
+ function unwrapOr(result, defaultValue) {
42
+ return result.ok ? result.value : defaultValue;
43
+ }
44
+ /** Unwrap or throw */
45
+ function unwrap(result) {
46
+ if (result.ok) return result.value;
47
+ throw new Error(`Unwrap called on Err: ${JSON.stringify(result.error)}`);
48
+ }
49
+ /** Try to execute a function and wrap in Result */
50
+ function tryCatch(fn) {
51
+ try {
52
+ return ok(fn());
53
+ } catch (e) {
54
+ return err(e);
55
+ }
56
+ }
57
+ /** Try to execute an async function and wrap in Result */
58
+ async function tryCatchAsync(fn) {
59
+ try {
60
+ return ok(await fn());
61
+ } catch (e) {
62
+ return err(e);
63
+ }
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/utils/constants.ts
68
+ const YOUTUBE_BASE_URL = "https://www.youtube.com";
69
+ const INNER_TUBE_API_URL = `${YOUTUBE_BASE_URL}/youtubei/v1`;
70
+ const VIDEO_ID_LENGTH = 11;
71
+ const RETRY_DEFAULTS = {
72
+ RETRIES: 3,
73
+ DELAY: 1e3,
74
+ BACKOFF: 2,
75
+ MAX_DELAY: 3e4
76
+ };
77
+ const INNERTUBE_CLIENTS = {
78
+ WEB: {
79
+ NAME: "WEB",
80
+ VERSION: "2.20250222.10.00",
81
+ CLIENT_ID: 1,
82
+ API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
83
+ INNERTUBE_CONTEXT: { client: {
84
+ clientName: "WEB",
85
+ clientVersion: "2.20250222.10.00",
86
+ platform: "DESKTOP"
87
+ } },
88
+ HEADERS: {
89
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
90
+ "X-YouTube-Client-Name": "1",
91
+ "X-YouTube-Client-Version": "2.20250222.10.00"
92
+ }
93
+ },
94
+ ANDROID: {
95
+ NAME: "ANDROID",
96
+ VERSION: "19.35.36",
97
+ CLIENT_ID: 3,
98
+ API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
99
+ SDK_VERSION: 33,
100
+ INNERTUBE_CONTEXT: { client: {
101
+ clientName: "ANDROID",
102
+ clientVersion: "19.35.36",
103
+ androidSdkVersion: 33
104
+ } },
105
+ HEADERS: {
106
+ "User-Agent": "com.google.android.youtube/19.35.36(Linux; U; Android 13; en_US; SM-S908E Build/TP1A.220624.014) gzip",
107
+ "X-YouTube-Client-Name": "3",
108
+ "X-YouTube-Client-Version": "19.35.36"
109
+ }
110
+ },
111
+ TV: {
112
+ NAME: "TVHTML5",
113
+ VERSION: "7.20250923.13.00",
114
+ CLIENT_ID: 7,
115
+ API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
116
+ INNERTUBE_CONTEXT: { client: {
117
+ clientName: "TVHTML5",
118
+ clientVersion: "7.20250923.13.00",
119
+ userAgent: "Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version"
120
+ } },
121
+ HEADERS: {
122
+ "User-Agent": "Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version",
123
+ "X-YouTube-Client-Name": "7",
124
+ "X-YouTube-Client-Version": "7.20250923.13.00"
125
+ }
126
+ },
127
+ IOS: {
128
+ NAME: "IOS",
129
+ VERSION: "20.10.4",
130
+ CLIENT_ID: 5,
131
+ API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
132
+ INNERTUBE_CONTEXT: { client: {
133
+ clientName: "IOS",
134
+ clientVersion: "20.10.4",
135
+ deviceMake: "Apple",
136
+ deviceModel: "iPhone16,2",
137
+ osName: "iPhone",
138
+ osVersion: "18.3.2.22D82"
139
+ } },
140
+ HEADERS: {
141
+ "User-Agent": "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)",
142
+ "X-YouTube-Client-Name": "5",
143
+ "X-YouTube-Client-Version": "20.10.4"
144
+ }
145
+ },
146
+ ANDROID_SDKLESS: {
147
+ NAME: "ANDROID",
148
+ VERSION: "20.10.38",
149
+ CLIENT_ID: 3,
150
+ API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
151
+ INNERTUBE_CONTEXT: { client: {
152
+ clientName: "ANDROID",
153
+ clientVersion: "20.10.38",
154
+ osName: "Android",
155
+ osVersion: "11"
156
+ } },
157
+ HEADERS: {
158
+ "User-Agent": "com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip",
159
+ "X-YouTube-Client-Name": "3",
160
+ "X-YouTube-Client-Version": "20.10.38"
161
+ }
162
+ },
163
+ TV_EMBEDDED: {
164
+ NAME: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
165
+ VERSION: "2.0",
166
+ CLIENT_ID: 85,
167
+ API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
168
+ INNERTUBE_CONTEXT: {
169
+ client: {
170
+ clientName: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
171
+ clientVersion: "2.0",
172
+ clientScreen: "EMBED",
173
+ platform: "TV"
174
+ },
175
+ thirdParty: { embedUrl: "https://www.youtube.com/" }
176
+ },
177
+ HEADERS: {
178
+ "User-Agent": "Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version",
179
+ "X-YouTube-Client-Name": "85",
180
+ "X-YouTube-Client-Version": "2.0"
181
+ }
182
+ }
183
+ };
184
+
185
+ //#endregion
186
+ //#region src/types/error.types.ts
187
+ function createError(code, message, cause) {
188
+ return {
189
+ code,
190
+ message,
191
+ cause,
192
+ recoverable: isRecoverable(code)
193
+ };
194
+ }
195
+ function isRecoverable(code) {
196
+ return [
197
+ "RATE_LIMITED",
198
+ "NETWORK_ERROR",
199
+ "PO_TOKEN_REQUIRED",
200
+ "PO_TOKEN_EXPIRED",
201
+ "BOTGUARD_INIT_FAILED",
202
+ "ALL_PROVIDERS_FAILED"
203
+ ].includes(code);
204
+ }
205
+
206
+ //#endregion
207
+ //#region src/http/client.ts
208
+ function parseCookies(setCookieHeaders) {
209
+ return setCookieHeaders.map((header) => {
210
+ const [nameValue, ...attributes] = header.split(";").map((p) => p.trim());
211
+ const [name, value] = nameValue.split("=");
212
+ const cookie = {
213
+ name,
214
+ value
215
+ };
216
+ for (const attr of attributes) {
217
+ const [key, val] = attr.split("=");
218
+ const keyLower = key.toLowerCase();
219
+ if (keyLower === "domain") cookie.domain = val;
220
+ else if (keyLower === "path") cookie.path = val;
221
+ else if (keyLower === "secure") cookie.secure = true;
222
+ else if (keyLower === "httponly") cookie.httpOnly = true;
223
+ else if (keyLower === "expires") cookie.expires = new Date(val);
224
+ }
225
+ return cookie;
226
+ });
227
+ }
228
+ function cookiesToHeader(cookies) {
229
+ return Array.from(cookies.values()).map((c) => `${c.name}=${c.value}`).join("; ");
230
+ }
231
+ /**
232
+ * HTTP client with cookie jar and controlled redirect handling.
233
+ * Superior to YouTube.js: tracks cookies across redirects, prevents loops.
234
+ */
235
+ var HttpClient = class {
236
+ cookies = /* @__PURE__ */ new Map();
237
+ /**
238
+ * Performs a fetch request with automatic cookie and redirect handling.
239
+ * @param url - The URL to fetch
240
+ * @param options - Fetch options. Use manualRedirects=true for controlled redirects
241
+ * @returns Result containing the Response or a BundlpError
242
+ */
243
+ async request(url, options = {}) {
244
+ const { maxRedirects = 10, manualRedirects = false, timeout, ...fetchOptions } = options;
245
+ if (!manualRedirects) try {
246
+ return ok(await fetch(url, {
247
+ ...fetchOptions,
248
+ signal: timeout ? AbortSignal.timeout(timeout) : void 0
249
+ }));
250
+ } catch (error) {
251
+ return err(createError("NETWORK_ERROR", `Request to ${url} failed: ${error.message}`, error));
252
+ }
253
+ let currentUrl = url;
254
+ let redirectCount = 0;
255
+ const visitedUrls = /* @__PURE__ */ new Set();
256
+ while (redirectCount <= maxRedirects) {
257
+ if (visitedUrls.has(currentUrl)) return err(createError("NETWORK_ERROR", `Redirect loop detected at ${currentUrl}`));
258
+ visitedUrls.add(currentUrl);
259
+ try {
260
+ const headers = new Headers(fetchOptions.headers);
261
+ const cookieHeader = cookiesToHeader(this.cookies);
262
+ if (cookieHeader) headers.set("Cookie", cookieHeader);
263
+ const response = await fetch(currentUrl, {
264
+ ...fetchOptions,
265
+ headers,
266
+ redirect: "manual",
267
+ signal: timeout ? AbortSignal.timeout(timeout) : void 0
268
+ });
269
+ const setCookies = response.headers.getSetCookie?.() || [];
270
+ for (const cookie of parseCookies(setCookies)) this.cookies.set(cookie.name, cookie);
271
+ if (response.status >= 300 && response.status < 400) {
272
+ const location = response.headers.get("location");
273
+ if (!location) return ok(response);
274
+ currentUrl = new URL(location, currentUrl).href;
275
+ redirectCount++;
276
+ continue;
277
+ }
278
+ return ok(response);
279
+ } catch (error) {
280
+ return err(createError("NETWORK_ERROR", `Request to ${currentUrl} failed: ${error.message}`, error));
281
+ }
282
+ }
283
+ return err(createError("NETWORK_ERROR", `Too many redirects (${redirectCount}) for ${url}`));
284
+ }
285
+ clearCookies() {
286
+ this.cookies.clear();
287
+ }
288
+ getCookies() {
289
+ return new Map(this.cookies);
290
+ }
291
+ /**
292
+ * Performs a GET request.
293
+ * @param url - The URL to fetch
294
+ * @param options - Optional fetch options
295
+ * @returns Result containing the Response or a BundlpError
296
+ */
297
+ async get(url, options = {}) {
298
+ return this.request(url, {
299
+ ...options,
300
+ method: "GET"
301
+ });
302
+ }
303
+ /**
304
+ * Performs a POST request with JSON body.
305
+ * @param url - The URL to post to
306
+ * @param body - The request body (will be JSON stringified)
307
+ * @param options - Optional fetch options
308
+ * @returns Result containing the Response or a BundlpError
309
+ */
310
+ async post(url, body, options = {}) {
311
+ return this.request(url, {
312
+ ...options,
313
+ method: "POST",
314
+ body: JSON.stringify(body),
315
+ headers: {
316
+ "Content-Type": "application/json",
317
+ ...options.headers
318
+ }
319
+ });
320
+ }
321
+ };
322
+ const httpClient = new HttpClient();
323
+
324
+ //#endregion
325
+ //#region src/validation/schemas.ts
326
+ /**
327
+ * Schema for playability status returned by InnerTube API.
328
+ */
329
+ const PlayabilityStatusSchema = type({
330
+ status: "'OK' | 'UNPLAYABLE' | 'LOGIN_REQUIRED' | 'AGE_CHECK_REQUIRED' | 'ERROR'",
331
+ "reason?": "string",
332
+ "playableInEmbed?": "boolean",
333
+ "liveStreamability?": "unknown"
334
+ });
335
+ /**
336
+ * Schema for thumbnail objects.
337
+ */
338
+ const ThumbnailSchema = type({
339
+ url: "string",
340
+ width: "number",
341
+ height: "number"
342
+ });
343
+ /**
344
+ * Schema for video details from InnerTube response.
345
+ * Some fields are optional as TV and other clients may omit them.
346
+ */
347
+ const VideoDetailsSchema = type({
348
+ videoId: "string",
349
+ "title?": "string",
350
+ lengthSeconds: "string",
351
+ channelId: "string",
352
+ "shortDescription?": "string",
353
+ "thumbnail?": { thumbnails: ThumbnailSchema.array() },
354
+ "viewCount?": "string",
355
+ "author?": "string",
356
+ isLiveContent: "boolean",
357
+ "isPrivate?": "boolean",
358
+ "keywords?": "string[]"
359
+ });
360
+ /**
361
+ * Schema for raw format objects from streaming data.
362
+ */
363
+ const RawFormatSchema = type({
364
+ itag: "number",
365
+ "url?": "string",
366
+ "signatureCipher?": "string",
367
+ "cipher?": "string",
368
+ mimeType: "string",
369
+ bitrate: "number",
370
+ "width?": "number",
371
+ "height?": "number",
372
+ "fps?": "number",
373
+ "qualityLabel?": "string",
374
+ quality: "string",
375
+ "audioQuality?": "string",
376
+ "audioSampleRate?": "string",
377
+ "audioChannels?": "number",
378
+ "contentLength?": "string",
379
+ "approxDurationMs?": "string",
380
+ "initRange?": {
381
+ start: "string",
382
+ end: "string"
383
+ },
384
+ "indexRange?": {
385
+ start: "string",
386
+ end: "string"
387
+ },
388
+ "lastModified?": "string",
389
+ "drmFamilies?": "string[]"
390
+ });
391
+ /**
392
+ * Schema for streaming data section of player response.
393
+ */
394
+ const StreamingDataSchema = type({
395
+ expiresInSeconds: "string",
396
+ "formats?": RawFormatSchema.array(),
397
+ "adaptiveFormats?": RawFormatSchema.array(),
398
+ "hlsManifestUrl?": "string",
399
+ "dashManifestUrl?": "string"
400
+ });
401
+ /**
402
+ * Schema for caption track information.
403
+ * Note: name can be either { simpleText } or { runs } format.
404
+ */
405
+ const CaptionTrackSchema = type({
406
+ baseUrl: "string",
407
+ languageCode: "string",
408
+ "name?": "unknown",
409
+ "kind?": "string",
410
+ "isTranslatable?": "boolean"
411
+ });
412
+ /**
413
+ * Schema for captions data section.
414
+ */
415
+ const CaptionsDataSchema = type({ "playerCaptionsTracklistRenderer?": { "captionTracks?": CaptionTrackSchema.array() } });
416
+ /**
417
+ * Schema for microformat data section.
418
+ */
419
+ const MicroformatDataSchema = type({ "playerMicroformatRenderer?": {
420
+ "uploadDate?": "string",
421
+ "publishDate?": "string",
422
+ "category?": "string",
423
+ "isFamilySafe?": "boolean",
424
+ "lengthSeconds?": "string"
425
+ } });
426
+ /**
427
+ * Schema for the complete InnerTube player response.
428
+ */
429
+ const PlayerResponseSchema = type({
430
+ playabilityStatus: PlayabilityStatusSchema,
431
+ "videoDetails?": VideoDetailsSchema,
432
+ "streamingData?": StreamingDataSchema,
433
+ "captions?": CaptionsDataSchema,
434
+ "microformat?": MicroformatDataSchema,
435
+ "storyboards?": "unknown"
436
+ });
437
+
438
+ //#endregion
439
+ //#region src/streaming/drm.ts
440
+ /**
441
+ * Checks if a format is DRM-protected.
442
+ * @param format - Format to check
443
+ * @returns Whether format has DRM
444
+ */
445
+ function hasDrm(format) {
446
+ return !!(format.drmFamilies && format.drmFamilies.length > 0);
447
+ }
448
+ /**
449
+ * Checks if all formats are DRM-only.
450
+ * @param formats - Array of formats
451
+ * @returns Whether all formats have DRM
452
+ */
453
+ function isDrmOnly(formats) {
454
+ if (formats.length === 0) return false;
455
+ return formats.every(hasDrm);
456
+ }
457
+
458
+ //#endregion
459
+ //#region src/innertube/client.ts
460
+ /**
461
+ * Client for interacting with YouTube's InnerTube API.
462
+ */
463
+ var InnerTubeClient = class {
464
+ currentClient;
465
+ FALLBACK_ORDER = [
466
+ "ANDROID_SDKLESS",
467
+ "TV",
468
+ "WEB"
469
+ ];
470
+ constructor(config = {}) {
471
+ this.currentClient = config.initialClient || "WEB";
472
+ }
473
+ /**
474
+ * Sets the client type to use for API requests.
475
+ * @param client - The client name to use
476
+ * @returns Result indicating success or error if client is unknown
477
+ */
478
+ setClient(client) {
479
+ if (!INNERTUBE_CLIENTS[client]) return err(createError("INVALID_CLIENT", `Unknown client: ${client}`));
480
+ this.currentClient = client;
481
+ return ok(void 0);
482
+ }
483
+ /**
484
+ * Gets the current client name being used.
485
+ * @returns The current client name
486
+ */
487
+ getClientName() {
488
+ return this.currentClient;
489
+ }
490
+ getHeaders() {
491
+ const config = INNERTUBE_CLIENTS[this.currentClient];
492
+ return {
493
+ "Content-Type": "application/json",
494
+ Accept: "*/*",
495
+ Origin: YOUTUBE_BASE_URL,
496
+ Referer: `${YOUTUBE_BASE_URL}/`,
497
+ ...config.HEADERS
498
+ };
499
+ }
500
+ buildContext() {
501
+ const config = INNERTUBE_CLIENTS[this.currentClient];
502
+ const context = {
503
+ client: {
504
+ hl: "en",
505
+ gl: "US",
506
+ ...config.INNERTUBE_CONTEXT?.client
507
+ },
508
+ user: { lockedSafetyMode: false },
509
+ request: { useSsl: true }
510
+ };
511
+ const innertubeContext = config.INNERTUBE_CONTEXT;
512
+ if (innertubeContext?.thirdParty) context.thirdParty = innertubeContext.thirdParty;
513
+ return context;
514
+ }
515
+ /**
516
+ * Fetches video player data from the InnerTube API.
517
+ * @param videoId - The YouTube video ID
518
+ * @param options - Optional parameters for the request
519
+ * @returns Result containing PlayerResponse or BundlpError
520
+ */
521
+ async fetchPlayer(videoId, options = {}) {
522
+ const config = INNERTUBE_CLIENTS[this.currentClient];
523
+ const payload = {
524
+ context: this.buildContext(),
525
+ videoId,
526
+ contentCheckOk: true,
527
+ racyCheckOk: true
528
+ };
529
+ if (options.signatureTimestamp) payload.playbackContext = { contentPlaybackContext: { signatureTimestamp: options.signatureTimestamp } };
530
+ if (options.poToken) {
531
+ if (!payload.serviceIntegrityDimensions) payload.serviceIntegrityDimensions = {};
532
+ payload.serviceIntegrityDimensions.poToken = options.poToken;
533
+ }
534
+ const url = `${INNER_TUBE_API_URL}/player?key=${config.API_KEY || ""}`;
535
+ const responseResult = await httpClient.post(url, payload, { headers: this.getHeaders() });
536
+ if (isErr(responseResult)) return responseResult;
537
+ const response = responseResult.value;
538
+ if (!response.ok) return err(createError("INNERTUBE_ERROR", `Player request failed with status ${response.status}`));
539
+ try {
540
+ const validated = PlayerResponseSchema(await response.json());
541
+ if (validated instanceof type.errors) return err(createError("PARSE_ERROR", `Invalid player response: ${validated.summary}`));
542
+ if (validated.playabilityStatus.status === "ERROR") return err(createError("VIDEO_UNAVAILABLE", validated.playabilityStatus.reason || "Video unavailable"));
543
+ return ok(validated);
544
+ } catch (error) {
545
+ return err(createError("PARSE_ERROR", "Failed to parse InnerTube response", error));
546
+ }
547
+ }
548
+ /**
549
+ * Fetches player data with automatic client fallback.
550
+ * Tries clients in order: ANDROID_SDKLESS → TV → IOS → WEB
551
+ * @param videoId - Video ID to fetch
552
+ * @param options - Fetch options
553
+ * @returns Result containing player response or error
554
+ */
555
+ async fetchPlayerWithFallback(videoId, options = {}) {
556
+ const errors = [];
557
+ for (const clientName of this.FALLBACK_ORDER) {
558
+ if (isErr(this.setClient(clientName))) continue;
559
+ const result = await this.fetchPlayer(videoId, options);
560
+ if (isOk(result)) {
561
+ if (this.isValidResponse(result.value)) return result;
562
+ errors.push({
563
+ client: clientName,
564
+ error: createError("VIDEO_UNAVAILABLE", "Invalid or DRM-only response")
565
+ });
566
+ } else errors.push({
567
+ client: clientName,
568
+ error: result.error
569
+ });
570
+ }
571
+ return err(createError("VIDEO_UNAVAILABLE", "All clients failed", errors));
572
+ }
573
+ /**
574
+ * Validates if a player response is usable.
575
+ * @param response - Player response to validate
576
+ * @returns Whether response is valid
577
+ */
578
+ isValidResponse(response) {
579
+ if (response.playabilityStatus.status !== "OK") return false;
580
+ if (!response.streamingData) return false;
581
+ if (this.hasDrmOnly(response.streamingData)) return false;
582
+ return true;
583
+ }
584
+ /**
585
+ * Checks if streaming data contains only DRM-protected formats.
586
+ * @param streamingData - Streaming data to check
587
+ * @returns Whether all formats are DRM-protected
588
+ */
589
+ hasDrmOnly(streamingData) {
590
+ const allFormats = [...streamingData.formats || [], ...streamingData.adaptiveFormats || []];
591
+ if (allFormats.length === 0) return true;
592
+ return isDrmOnly(allFormats);
593
+ }
594
+ };
595
+
596
+ //#endregion
597
+ //#region src/player/ast/matchers.ts
598
+ const WALK_STOP = Symbol("stop");
599
+ /**
600
+ * Recursively walks an AST tree and applies a callback to each node.
601
+ * Uses iterative approach with explicit stack for better performance on large ASTs.
602
+ */
603
+ function walkAst(node, callback) {
604
+ const stack = [node];
605
+ while (stack.length > 0) {
606
+ const current = stack.pop();
607
+ const result = callback(current);
608
+ if (result === WALK_STOP) return void 0;
609
+ if (result) return result;
610
+ for (const key of Object.keys(current)) {
611
+ const child = current[key];
612
+ if (child && typeof child === "object") {
613
+ if (Array.isArray(child)) for (let i = child.length - 1; i >= 0; i--) {
614
+ const item = child[i];
615
+ if (item && typeof item === "object" && "type" in item) stack.push(item);
616
+ }
617
+ else if ("type" in child) stack.push(child);
618
+ }
619
+ }
620
+ }
621
+ }
622
+ /**
623
+ * Extracts FunctionExpression from either VariableDeclarator or AssignmentExpression
624
+ */
625
+ function getFunctionExpression(node) {
626
+ if (node.type === "VariableDeclarator") {
627
+ const init = node.init;
628
+ if (init?.type === "FunctionExpression") return init;
629
+ }
630
+ if (node.type === "AssignmentExpression") {
631
+ const right = node.right;
632
+ if (right?.type === "FunctionExpression") return right;
633
+ }
634
+ return null;
635
+ }
636
+ /**
637
+ * Finds the signature decipher function call in YouTube's player.js.
638
+ * Supports multiple patterns:
639
+ * - Pattern 1: LogicalExpression with SequenceExpression (YouTube.js style)
640
+ * - Pattern 2: Direct CallExpression with decodeURIComponent argument
641
+ * - Pattern 3: AssignmentExpression with cipher function call
642
+ */
643
+ function sigMatcher(node) {
644
+ const funcExpr = getFunctionExpression(node);
645
+ if (!funcExpr) return false;
646
+ const params = funcExpr.params;
647
+ if (!params || params.length < 1 || params.length > 5) return false;
648
+ const body = funcExpr.body;
649
+ if (body?.type !== "BlockStatement") return false;
650
+ let foundCallExpr = null;
651
+ walkAst(body, (innerNode) => {
652
+ if (innerNode.type === "LogicalExpression" && innerNode.operator === "&&") {
653
+ const right = innerNode.right;
654
+ if (right?.type === "SequenceExpression") {
655
+ const expressions = right.expressions;
656
+ if (expressions?.[0]?.type === "AssignmentExpression") {
657
+ const assignRight = expressions[0].right;
658
+ if (assignRight?.type === "CallExpression") {
659
+ if (assignRight.arguments?.some((arg) => {
660
+ if (arg.type !== "CallExpression") return false;
661
+ const callee = arg.callee;
662
+ return callee?.type === "Identifier" && callee.name === "decodeURIComponent";
663
+ })) {
664
+ foundCallExpr = assignRight;
665
+ return WALK_STOP;
666
+ }
667
+ }
668
+ }
669
+ }
670
+ }
671
+ if (innerNode.type === "CallExpression") {
672
+ const args = innerNode.arguments;
673
+ if (!args || args.length !== 2) return void 0;
674
+ const firstArg = args[0];
675
+ if (firstArg?.type !== "Literal" || typeof firstArg.value !== "number") return;
676
+ const secondArg = args[1];
677
+ if (secondArg?.type !== "CallExpression") return void 0;
678
+ const secondCallee = secondArg.callee;
679
+ if (secondCallee?.type !== "Identifier" || secondCallee.name !== "decodeURIComponent") return;
680
+ if (innerNode.callee?.type === "Identifier") {
681
+ foundCallExpr = innerNode;
682
+ return WALK_STOP;
683
+ }
684
+ }
685
+ if (innerNode.type === "AssignmentExpression") {
686
+ const right = innerNode.right;
687
+ if (right?.type === "CallExpression") {
688
+ const args = right.arguments;
689
+ if (args?.length === 2) {
690
+ const firstArg = args[0];
691
+ const secondArg = args[1];
692
+ if (firstArg?.type === "Literal" && typeof firstArg.value === "number" && secondArg?.type === "CallExpression") {
693
+ const secondCallee = secondArg.callee;
694
+ if (secondCallee?.type === "Identifier" && secondCallee.name === "decodeURIComponent") {
695
+ foundCallExpr = right;
696
+ return WALK_STOP;
697
+ }
698
+ }
699
+ }
700
+ }
701
+ }
702
+ });
703
+ return foundCallExpr || false;
704
+ }
705
+ /**
706
+ * Finds all potential 'n' parameter transformation function references.
707
+ * Searches for patterns like: var XX = [functionName] or var XX = [functionName, "string"]
708
+ * YouTube uses both single-element and multi-element arrays.
709
+ * @param ast - Full AST to search
710
+ * @returns Array of candidate function names
711
+ */
712
+ function findNFunctionCandidates(ast) {
713
+ const candidates = [];
714
+ walkAst(ast, (node) => {
715
+ if (node.type !== "VariableDeclarator") return void 0;
716
+ const id = node.id;
717
+ const init = node.init;
718
+ if (id?.type !== "Identifier") return void 0;
719
+ if (init?.type !== "ArrayExpression") return void 0;
720
+ const elements = init.elements;
721
+ if (!elements || elements.length === 0) return void 0;
722
+ const firstElement = elements[0];
723
+ if (firstElement?.type !== "Identifier") return void 0;
724
+ candidates.push(firstElement.name);
725
+ });
726
+ return candidates;
727
+ }
728
+ /**
729
+ * Finds the signatureTimestamp value in player.js.
730
+ * Pattern 1: Direct ObjectExpression property (fastest)
731
+ * Pattern 2: Inside FunctionExpression body (fallback)
732
+ */
733
+ function timestampMatcher(node) {
734
+ if (node.type === "ObjectExpression") {
735
+ const properties = node.properties;
736
+ if (!properties) return false;
737
+ for (const prop of properties) {
738
+ if (prop.type !== "Property") continue;
739
+ const key = prop.key;
740
+ if ((key?.type === "Identifier" ? key.name : key?.type === "Literal" ? String(key.value) : null) === "signatureTimestamp") return prop;
741
+ }
742
+ return false;
743
+ }
744
+ const funcExpr = getFunctionExpression(node);
745
+ if (!funcExpr) return false;
746
+ const funcBody = funcExpr.body;
747
+ if (!funcBody) return false;
748
+ let foundProperty = null;
749
+ walkAst(funcBody, (innerNode) => {
750
+ if (innerNode.type !== "ObjectExpression") return void 0;
751
+ const properties = innerNode.properties;
752
+ if (!properties) return void 0;
753
+ for (const prop of properties) {
754
+ if (prop.type !== "Property") continue;
755
+ const key = prop.key;
756
+ if ((key?.type === "Identifier" ? key.name : key?.type === "Literal" ? String(key.value) : null) === "signatureTimestamp") {
757
+ foundProperty = prop;
758
+ return WALK_STOP;
759
+ }
760
+ }
761
+ });
762
+ return foundProperty || false;
763
+ }
764
+
765
+ //#endregion
766
+ //#region src/player/ast/analyzer.ts
767
+ const JS_BUILTINS = new Set([
768
+ "Array",
769
+ "Object",
770
+ "String",
771
+ "Number",
772
+ "Boolean",
773
+ "Math",
774
+ "JSON",
775
+ "Date",
776
+ "RegExp",
777
+ "Error",
778
+ "TypeError",
779
+ "RangeError",
780
+ "SyntaxError",
781
+ "parseInt",
782
+ "parseFloat",
783
+ "isNaN",
784
+ "isFinite",
785
+ "eval",
786
+ "encodeURIComponent",
787
+ "decodeURIComponent",
788
+ "encodeURI",
789
+ "decodeURI",
790
+ "undefined",
791
+ "null",
792
+ "NaN",
793
+ "Infinity",
794
+ "console",
795
+ "window",
796
+ "document",
797
+ "navigator",
798
+ "location",
799
+ "setTimeout",
800
+ "setInterval",
801
+ "clearTimeout",
802
+ "clearInterval",
803
+ "Promise",
804
+ "Map",
805
+ "Set",
806
+ "WeakMap",
807
+ "WeakSet",
808
+ "Symbol",
809
+ "Uint8Array",
810
+ "Int8Array",
811
+ "Uint16Array",
812
+ "Int16Array",
813
+ "Uint32Array",
814
+ "Int32Array",
815
+ "Float32Array",
816
+ "Float64Array",
817
+ "ArrayBuffer",
818
+ "DataView",
819
+ "Blob",
820
+ "URL",
821
+ "URLSearchParams",
822
+ "fetch",
823
+ "Request",
824
+ "Response",
825
+ "Headers",
826
+ "atob",
827
+ "btoa",
828
+ "escape",
829
+ "unescape",
830
+ "Proxy",
831
+ "Reflect",
832
+ "Function",
833
+ "this",
834
+ "arguments",
835
+ "self"
836
+ ]);
837
+ function isBuiltin(name) {
838
+ return JS_BUILTINS.has(name);
839
+ }
840
+ /**
841
+ * Analyzes a function node to find all external dependencies it references.
842
+ * Recursively discovers nested dependencies.
843
+ * Filters out JavaScript built-ins.
844
+ */
845
+ const MAX_DEPTH = 4;
846
+ function isLocalParameterVar(name) {
847
+ if (name.length !== 1) return false;
848
+ if (name === "z" || name === "g") return false;
849
+ return true;
850
+ }
851
+ function analyzeDependencies(funcNode, ast, visited = /* @__PURE__ */ new Set(), depth = 0) {
852
+ const dependencies = /* @__PURE__ */ new Set();
853
+ if (depth > MAX_DEPTH) return dependencies;
854
+ const identifiers = extractReferencedIdentifiers(funcNode);
855
+ for (const identifier of identifiers) {
856
+ if (isBuiltin(identifier)) continue;
857
+ if (visited.has(identifier)) continue;
858
+ if (isLocalParameterVar(identifier)) continue;
859
+ visited.add(identifier);
860
+ const definition = findDefinition(identifier, ast);
861
+ if (definition) {
862
+ dependencies.add(identifier);
863
+ const nestedDeps = analyzeDependencies(definition, ast, visited, depth + 1);
864
+ for (const dep of nestedDeps) dependencies.add(dep);
865
+ }
866
+ }
867
+ return dependencies;
868
+ }
869
+ /**
870
+ * Extracts all identifiers referenced within a node that are not locally declared.
871
+ * @param node - The AST node to scan
872
+ * @returns Set of external identifier names
873
+ */
874
+ function extractReferencedIdentifiers(node) {
875
+ const identifiers = /* @__PURE__ */ new Set();
876
+ const declared = /* @__PURE__ */ new Set();
877
+ walkAst(node, (innerNode) => {
878
+ if (innerNode.type === "VariableDeclarator") {
879
+ const id = innerNode.id;
880
+ if (id?.type === "Identifier") declared.add(id.name);
881
+ }
882
+ if (innerNode.type === "FunctionDeclaration" || innerNode.type === "FunctionExpression") {
883
+ const params = innerNode.params;
884
+ if (params) {
885
+ for (const param of params) if (param.type === "Identifier") declared.add(param.name);
886
+ }
887
+ }
888
+ if (innerNode.type === "MemberExpression") {
889
+ const object = innerNode.object;
890
+ if (object?.type === "Identifier") {
891
+ const name = object.name;
892
+ if (!declared.has(name)) identifiers.add(name);
893
+ }
894
+ }
895
+ if (innerNode.type === "Identifier") {
896
+ const parent = getParent(node, innerNode);
897
+ if (parent?.type === "CallExpression" && parent.callee === innerNode) {
898
+ const name = innerNode.name;
899
+ if (!declared.has(name)) identifiers.add(name);
900
+ }
901
+ if (parent?.type === "ArrayExpression") {
902
+ if (parent.elements?.includes(innerNode)) {
903
+ const name = innerNode.name;
904
+ if (!declared.has(name)) identifiers.add(name);
905
+ }
906
+ }
907
+ if (parent?.type === "UnaryExpression" && parent.operator === "typeof") {
908
+ const name = innerNode.name;
909
+ if (!declared.has(name)) identifiers.add(name);
910
+ }
911
+ }
912
+ });
913
+ return identifiers;
914
+ }
915
+ /**
916
+ * Finds a variable or function definition by name in the AST.
917
+ * @param name - The identifier name to find
918
+ * @param ast - The AST to search
919
+ * @returns The definition node if found, null otherwise
920
+ */
921
+ function findDefinition(name, ast) {
922
+ let found = null;
923
+ walkAst(ast, (node) => {
924
+ if (node.type === "VariableDeclarator") {
925
+ const id = node.id;
926
+ if (id?.type === "Identifier" && id.name === name) {
927
+ const init = node.init;
928
+ if (init) {
929
+ found = init;
930
+ return node;
931
+ }
932
+ }
933
+ }
934
+ if (node.type === "FunctionDeclaration") {
935
+ const id = node.id;
936
+ if (id?.type === "Identifier" && id.name === name) {
937
+ found = node;
938
+ return node;
939
+ }
940
+ }
941
+ if (node.type === "AssignmentExpression") {
942
+ const left = node.left;
943
+ if (left?.type === "Identifier" && left.name === name) {
944
+ const right = node.right;
945
+ if (right) {
946
+ found = right;
947
+ return node;
948
+ }
949
+ }
950
+ }
951
+ });
952
+ return found;
953
+ }
954
+ /**
955
+ * Finds the parent node of a target node within an AST.
956
+ * @param root - The root node to search from
957
+ * @param target - The target node to find the parent of
958
+ * @returns The parent node if found, null otherwise
959
+ */
960
+ function getParent(root, target) {
961
+ let parent = null;
962
+ walkAst(root, (node) => {
963
+ for (const key of Object.keys(node)) {
964
+ const child = node[key];
965
+ if (child === target) {
966
+ parent = node;
967
+ return node;
968
+ }
969
+ if (Array.isArray(child) && child.includes(target)) {
970
+ parent = node;
971
+ return node;
972
+ }
973
+ }
974
+ });
975
+ return parent;
976
+ }
977
+
978
+ //#endregion
979
+ //#region src/player/ast/extractor.ts
980
+ /**
981
+ * Finds a function definition by name in the AST.
982
+ * Supports function declarations, variable declarations with function expressions,
983
+ * and assignment expressions.
984
+ * @param ast - The AST root node to search
985
+ * @param functionName - The name of the function to find
986
+ * @returns The function node if found, null otherwise
987
+ */
988
+ function findFunctionDefinition(ast, functionName) {
989
+ let definition = null;
990
+ walkAst(ast, (node) => {
991
+ const id = node.id;
992
+ if (node.type === "FunctionDeclaration" && id?.name === functionName) {
993
+ definition = node;
994
+ return WALK_STOP;
995
+ }
996
+ if (node.type === "VariableDeclarator" && id?.type === "Identifier" && id.name === functionName) {
997
+ const init = node.init;
998
+ if (init?.type === "FunctionExpression" || init?.type === "ArrowFunctionExpression") {
999
+ definition = init;
1000
+ return WALK_STOP;
1001
+ }
1002
+ }
1003
+ const left = node.left;
1004
+ if (node.type === "AssignmentExpression" && left?.type === "Identifier" && left.name === functionName) {
1005
+ const right = node.right;
1006
+ if (right?.type === "FunctionExpression" || right?.type === "ArrowFunctionExpression") {
1007
+ definition = right;
1008
+ return WALK_STOP;
1009
+ }
1010
+ }
1011
+ });
1012
+ return definition;
1013
+ }
1014
+ /**
1015
+ * Extracts the source code for a given AST node.
1016
+ * @param code - The full source code string
1017
+ * @param node - The AST node with start and end positions
1018
+ * @returns The extracted code slice
1019
+ */
1020
+ function extractCode(code, node) {
1021
+ const start = node.start;
1022
+ const end = node.end;
1023
+ if (typeof start !== "number" || typeof end !== "number") return "";
1024
+ return code.slice(start, end);
1025
+ }
1026
+ function isSafeInitializer(node) {
1027
+ if (!node) return true;
1028
+ return true;
1029
+ }
1030
+ /**
1031
+ * Extracts a full declaration (var X = ...) for a given name from the AST.
1032
+ * Handles both VariableDeclaration and AssignmentExpression patterns.
1033
+ * Only extracts if initializer is "safe" (no side-effects).
1034
+ * @param code - The full source code string
1035
+ * @param name - The identifier name to find
1036
+ * @param ast - The AST root node
1037
+ * @returns The complete declaration code, or null if not found
1038
+ */
1039
+ function extractFullDeclaration(code, name, ast) {
1040
+ let result = null;
1041
+ walkAst(ast, (node) => {
1042
+ if (node.type === "FunctionDeclaration") {
1043
+ if (node.id?.name === name) {
1044
+ result = extractCode(code, node);
1045
+ return WALK_STOP;
1046
+ }
1047
+ }
1048
+ if (node.type === "VariableDeclaration") {
1049
+ const decls = node.declarations;
1050
+ if (decls) for (const decl of decls) {
1051
+ const id = decl.id;
1052
+ const init = decl.init;
1053
+ if (id?.name === name && init && isSafeInitializer(init)) {
1054
+ result = extractCode(code, node);
1055
+ return WALK_STOP;
1056
+ }
1057
+ }
1058
+ }
1059
+ if (node.type === "ExpressionStatement") {
1060
+ const expr = node.expression;
1061
+ if (expr?.type === "AssignmentExpression") {
1062
+ const left = expr.left;
1063
+ const right = expr.right;
1064
+ if (left?.type === "Identifier" && left.name === name && isSafeInitializer(right)) {
1065
+ const rightCode = extractCode(code, right);
1066
+ if (rightCode) {
1067
+ result = `var ${name} = ${rightCode}`;
1068
+ return WALK_STOP;
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ if (node.type === "AssignmentExpression") {
1074
+ const left = node.left;
1075
+ if (left?.type === "Identifier" && left.name === name) {
1076
+ const right = node.right;
1077
+ if (isSafeInitializer(right)) {
1078
+ const rightCode = extractCode(code, right);
1079
+ if (rightCode) {
1080
+ result = `var ${name} = ${rightCode}`;
1081
+ return WALK_STOP;
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ });
1087
+ return result;
1088
+ }
1089
+ /**
1090
+ * Extracts code for a function with a simplified approach.
1091
+ * Creates a self-contained IIFE with stubs for globals.
1092
+ * @param code - The full source code string
1093
+ * @param funcNode - The function AST node
1094
+ * @param ast - The full AST for dependency lookup
1095
+ * @param funcName - The name of the main function
1096
+ * @returns Combined code with dependencies wrapped in IIFE
1097
+ */
1098
+ function extractCodeWithDependencies(code, funcNode, ast, funcName) {
1099
+ const dependencies = analyzeDependencies(funcNode, ast);
1100
+ const parts = [];
1101
+ const extracted = /* @__PURE__ */ new Set();
1102
+ const predeclared = /* @__PURE__ */ new Set();
1103
+ for (const depName of dependencies) {
1104
+ if (extracted.has(depName)) continue;
1105
+ const depCode = extractFullDeclaration(code, depName, ast);
1106
+ if (depCode) {
1107
+ parts.push(depCode);
1108
+ extracted.add(depName);
1109
+ } else predeclared.add(depName);
1110
+ }
1111
+ if (!extracted.has(funcName)) {
1112
+ const mainCode = extractFullDeclaration(code, funcName, ast);
1113
+ if (mainCode) parts.push(mainCode);
1114
+ }
1115
+ const stubs = ["var g = { cF: function(a, b) { return b.indexOf(a) >= 0; }, C$: function(a) { return /\\s/.test(a); } };"];
1116
+ const predeclareStmt = predeclared.size > 0 ? `var ${Array.from(predeclared).join(", ")};` : "";
1117
+ const postProcess = "if (typeof z === 'string' && z.indexOf(';') !== -1) z = z.split(';');";
1118
+ return [
1119
+ ...stubs,
1120
+ predeclareStmt,
1121
+ parts.join(";\n"),
1122
+ postProcess
1123
+ ].filter(Boolean).join("\n");
1124
+ }
1125
+
1126
+ //#endregion
1127
+ //#region src/player/cache.ts
1128
+ /**
1129
+ * SQLite-based cache for YouTube player.js cipher functions.
1130
+ * Stores extracted signature and n-transform functions to avoid re-parsing.
1131
+ */
1132
+ var PlayerCache = class {
1133
+ db;
1134
+ ttlMs;
1135
+ /**
1136
+ * Creates a new PlayerCache instance.
1137
+ * @param cachePath - Path to the SQLite database file
1138
+ * @param ttlDays - Time-to-live for cached entries in days
1139
+ */
1140
+ constructor(cachePath, ttlDays = 7) {
1141
+ mkdirSync(dirname(cachePath), { recursive: true });
1142
+ this.db = new Database(cachePath);
1143
+ this.ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
1144
+ this.initializeSchema();
1145
+ }
1146
+ initializeSchema() {
1147
+ this.db.run(`
1148
+ CREATE TABLE IF NOT EXISTS players (
1149
+ player_id TEXT PRIMARY KEY,
1150
+ code TEXT NOT NULL,
1151
+ sig_function TEXT,
1152
+ n_function TEXT,
1153
+ signature_timestamp INTEGER,
1154
+ cached_at INTEGER NOT NULL
1155
+ )
1156
+ `);
1157
+ }
1158
+ /**
1159
+ * Retrieves a cached player entry.
1160
+ * @param playerId - The player ID to look up
1161
+ * @returns Result containing the cached player or null if not found/expired
1162
+ */
1163
+ get(playerId) {
1164
+ try {
1165
+ const row = this.db.prepare(`
1166
+ SELECT player_id, code, sig_function, n_function, signature_timestamp, cached_at
1167
+ FROM players
1168
+ WHERE player_id = ?
1169
+ `).get(playerId);
1170
+ if (!row) return ok(null);
1171
+ if (this.isExpired(row.cached_at)) {
1172
+ this.delete(playerId);
1173
+ return ok(null);
1174
+ }
1175
+ return ok({
1176
+ playerId: row.player_id,
1177
+ code: row.code,
1178
+ sigFunction: row.sig_function || "",
1179
+ nFunction: row.n_function || "",
1180
+ signatureTimestamp: row.signature_timestamp || 0,
1181
+ cachedAt: row.cached_at
1182
+ });
1183
+ } catch (error) {
1184
+ return err(createError("CACHE_ERROR", "Failed to read from cache", error));
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Stores a player entry in the cache.
1189
+ * @param data - The player data to cache
1190
+ * @returns Result indicating success or failure
1191
+ */
1192
+ set(data) {
1193
+ try {
1194
+ this.db.prepare(`
1195
+ INSERT OR REPLACE INTO players (
1196
+ player_id, code, sig_function, n_function, signature_timestamp, cached_at
1197
+ ) VALUES (?, ?, ?, ?, ?, ?)
1198
+ `).run(data.playerId, data.code, data.sigFunction, data.nFunction, data.signatureTimestamp, data.cachedAt);
1199
+ return ok(void 0);
1200
+ } catch (error) {
1201
+ return err(createError("CACHE_ERROR", "Failed to write to cache", error));
1202
+ }
1203
+ }
1204
+ /**
1205
+ * Deletes a specific player entry from the cache.
1206
+ * @param playerId - The player ID to delete
1207
+ * @returns Result indicating success or failure
1208
+ */
1209
+ delete(playerId) {
1210
+ try {
1211
+ this.db.prepare("DELETE FROM players WHERE player_id = ?").run(playerId);
1212
+ return ok(void 0);
1213
+ } catch (error) {
1214
+ return err(createError("CACHE_ERROR", "Failed to delete from cache", error));
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Clears all entries from the cache.
1219
+ * @returns Result indicating success or failure
1220
+ */
1221
+ clear() {
1222
+ try {
1223
+ this.db.run("DELETE FROM players");
1224
+ return ok(void 0);
1225
+ } catch (error) {
1226
+ return err(createError("CACHE_ERROR", "Failed to clear cache", error));
1227
+ }
1228
+ }
1229
+ /**
1230
+ * Retrieves the most recent valid cached player entry.
1231
+ * Used to avoid re-downloading player.js when we have valid cache.
1232
+ * @returns Result containing the cached player or null if none valid
1233
+ */
1234
+ getLatest() {
1235
+ try {
1236
+ const row = this.db.prepare(`
1237
+ SELECT player_id, code, sig_function, n_function, signature_timestamp, cached_at
1238
+ FROM players
1239
+ ORDER BY cached_at DESC
1240
+ LIMIT 1
1241
+ `).get();
1242
+ if (!row) return ok(null);
1243
+ if (this.isExpired(row.cached_at)) {
1244
+ this.delete(row.player_id);
1245
+ return ok(null);
1246
+ }
1247
+ return ok({
1248
+ playerId: row.player_id,
1249
+ code: row.code,
1250
+ sigFunction: row.sig_function || "",
1251
+ nFunction: row.n_function || "",
1252
+ signatureTimestamp: row.signature_timestamp || 0,
1253
+ cachedAt: row.cached_at
1254
+ });
1255
+ } catch (error) {
1256
+ return err(createError("CACHE_ERROR", "Failed to read latest from cache", error));
1257
+ }
1258
+ }
1259
+ /**
1260
+ * Closes the database connection.
1261
+ */
1262
+ close() {
1263
+ this.db.close();
1264
+ }
1265
+ isExpired(cachedAt) {
1266
+ return Date.now() - cachedAt > this.ttlMs;
1267
+ }
1268
+ };
1269
+
1270
+ //#endregion
1271
+ //#region src/player/player.ts
1272
+ /**
1273
+ * Handles YouTube player.js downloading, parsing, and cipher function extraction.
1274
+ * Uses AST-based extraction for robustness against YouTube's obfuscation changes.
1275
+ */
1276
+ var Player = class {
1277
+ cache = null;
1278
+ playerCode = "";
1279
+ sigFunctionCode = "";
1280
+ nFunctionCode = "";
1281
+ decipherFunc = null;
1282
+ nFunc = null;
1283
+ signatureTimestamp = 0;
1284
+ initialized = false;
1285
+ constructor(cacheDir) {
1286
+ if (cacheDir) this.cache = new PlayerCache(join(cacheDir, "player.db"));
1287
+ }
1288
+ /**
1289
+ * Downloads and parses the YouTube player.js to extract cipher functions.
1290
+ * Uses cache-first strategy to minimize network requests.
1291
+ * @param playerUrl - Optional direct URL to player.js, auto-detected if not provided
1292
+ * @returns Result indicating success or failure
1293
+ */
1294
+ async initialize(playerUrl) {
1295
+ if (this.initialized) return ok(void 0);
1296
+ if (this.cache && !playerUrl) {
1297
+ const latestResult = this.cache.getLatest();
1298
+ if (isOk(latestResult) && latestResult.value) return this.restoreFromCache(latestResult.value);
1299
+ }
1300
+ if (!playerUrl) {
1301
+ const htmlResult = await httpClient.get(YOUTUBE_BASE_URL);
1302
+ if (!isOk(htmlResult)) return err(htmlResult.error);
1303
+ const match$1 = (await htmlResult.value.text()).match(/\/s\/player\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/base\.js/);
1304
+ if (!match$1) return err(createError("PLAYER_FETCH_FAILED", "Could not determine player URL"));
1305
+ playerUrl = YOUTUBE_BASE_URL + match$1[0];
1306
+ }
1307
+ const playerId = this.extractPlayerId(playerUrl);
1308
+ if (this.cache && playerId) {
1309
+ const cacheResult = this.cache.get(playerId);
1310
+ if (isOk(cacheResult) && cacheResult.value) return this.restoreFromCache(cacheResult.value);
1311
+ }
1312
+ const codeResult = await httpClient.get(playerUrl);
1313
+ if (!isOk(codeResult)) return err(codeResult.error);
1314
+ this.playerCode = await codeResult.value.text();
1315
+ const processResult = this.processCode(this.playerCode);
1316
+ if (isOk(processResult)) {
1317
+ this.initialized = true;
1318
+ if (this.cache && playerId) this.cache.set({
1319
+ playerId,
1320
+ code: this.playerCode,
1321
+ sigFunction: this.sigFunctionCode,
1322
+ nFunction: this.nFunctionCode,
1323
+ signatureTimestamp: this.signatureTimestamp,
1324
+ cachedAt: Date.now()
1325
+ });
1326
+ }
1327
+ return processResult;
1328
+ }
1329
+ extractPlayerId(playerUrl) {
1330
+ const match$1 = playerUrl.match(/\/s\/player\/([a-zA-Z0-9_-]+)\//);
1331
+ return match$1 ? match$1[1] : null;
1332
+ }
1333
+ restoreFromCache(cached) {
1334
+ try {
1335
+ this.playerCode = cached.code;
1336
+ this.sigFunctionCode = cached.sigFunction;
1337
+ this.nFunctionCode = cached.nFunction;
1338
+ this.signatureTimestamp = cached.signatureTimestamp;
1339
+ if (this.sigFunctionCode) this.decipherFunc = new Function("sig", this.sigFunctionCode);
1340
+ if (this.nFunctionCode) this.nFunc = new Function("n", this.nFunctionCode);
1341
+ this.initialized = true;
1342
+ return ok(void 0);
1343
+ } catch (error) {
1344
+ return err(createError("CACHE_ERROR", "Failed to restore from cache", error));
1345
+ }
1346
+ }
1347
+ processCode(code) {
1348
+ try {
1349
+ const ast = parse(code, {
1350
+ ranges: true,
1351
+ next: true
1352
+ });
1353
+ this.extractSignatureTimestamp(ast);
1354
+ this.extractDecipherFunction(ast, code);
1355
+ this.extractNFunction(ast, code);
1356
+ return ok(void 0);
1357
+ } catch (error) {
1358
+ return err(createError("PARSE_ERROR", "Failed to parse player.js", error));
1359
+ }
1360
+ }
1361
+ extractSignatureTimestamp(ast) {
1362
+ const stsNode = walkAst(ast, timestampMatcher);
1363
+ if (stsNode) {
1364
+ const value = stsNode.value;
1365
+ if (value?.type === "Literal" && typeof value.value === "number") this.signatureTimestamp = value.value;
1366
+ }
1367
+ }
1368
+ extractDecipherFunction(ast, code) {
1369
+ const sigCall = walkAst(ast, sigMatcher);
1370
+ if (!sigCall) return;
1371
+ const callee = sigCall.callee;
1372
+ if (callee?.type !== "Identifier") return;
1373
+ const funcName = callee.name;
1374
+ const funcDef = findFunctionDefinition(ast, funcName);
1375
+ if (!funcDef) return;
1376
+ const modeArg = sigCall.arguments?.[0];
1377
+ const modeValue = modeArg?.type === "Literal" && typeof modeArg.value === "number" ? modeArg.value : 83;
1378
+ const funcCode = extractCodeWithDependencies(code, funcDef, ast, funcName);
1379
+ if (!funcCode) return;
1380
+ this.sigFunctionCode = `
1381
+ ${funcCode}
1382
+ function __decipher_wrapper__(input) {
1383
+ var result = ${funcName}(${modeValue}, input);
1384
+ return Array.isArray(result) ? result.join('') : result;
1385
+ }
1386
+ return __decipher_wrapper__(sig);
1387
+ `;
1388
+ try {
1389
+ this.decipherFunc = new Function("sig", this.sigFunctionCode);
1390
+ } catch {
1391
+ this.decipherFunc = null;
1392
+ }
1393
+ }
1394
+ extractNFunction(ast, code) {
1395
+ const candidates = findNFunctionCandidates(ast);
1396
+ let bestCandidate = null;
1397
+ for (const nFuncName of candidates) {
1398
+ const nFuncDef = findFunctionDefinition(ast, nFuncName);
1399
+ if (!nFuncDef) continue;
1400
+ if (nFuncDef.type !== "FunctionExpression" && nFuncDef.type !== "ArrowFunctionExpression" && nFuncDef.type !== "FunctionDeclaration") continue;
1401
+ const nFuncCode = extractCodeWithDependencies(code, nFuncDef, ast, nFuncName);
1402
+ if (!bestCandidate || nFuncCode.length > bestCandidate.size) bestCandidate = {
1403
+ name: nFuncName,
1404
+ code: nFuncCode,
1405
+ size: nFuncCode.length
1406
+ };
1407
+ }
1408
+ if (!bestCandidate) return;
1409
+ this.nFunctionCode = `
1410
+ ${bestCandidate.code}
1411
+ function __n_wrapper__(input) {
1412
+ return ${bestCandidate.name}(input);
1413
+ }
1414
+ return __n_wrapper__(n);
1415
+ `;
1416
+ try {
1417
+ this.nFunc = new Function("n", this.nFunctionCode);
1418
+ } catch {
1419
+ this.nFunc = null;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Returns the signature timestamp extracted from player.js.
1424
+ * Required for some InnerTube API calls.
1425
+ */
1426
+ getSignatureTimestamp() {
1427
+ return this.signatureTimestamp;
1428
+ }
1429
+ /**
1430
+ * Deciphers a signature cipher string and constructs the final playback URL.
1431
+ * @param baseUrl - The base URL from the format
1432
+ * @param signatureCipher - The encoded signature cipher string
1433
+ * @returns The deciphered playback URL
1434
+ */
1435
+ decipher(baseUrl, signatureCipher) {
1436
+ if (!this.decipherFunc) return baseUrl;
1437
+ const params = new URLSearchParams(signatureCipher);
1438
+ const urlParam = params.get("url");
1439
+ const sig = params.get("s");
1440
+ const sp = params.get("sp") || "sig";
1441
+ if (!sig || !urlParam) return baseUrl;
1442
+ try {
1443
+ const decryptedSig = this.decipherFunc(decodeURIComponent(sig));
1444
+ return `${decodeURIComponent(urlParam)}&${sp}=${encodeURIComponent(decryptedSig)}`;
1445
+ } catch {
1446
+ return baseUrl;
1447
+ }
1448
+ }
1449
+ /**
1450
+ * Deobfuscates the 'n' parameter to prevent throttling.
1451
+ * @param nParam - The obfuscated n parameter value
1452
+ * @returns The deobfuscated n parameter value
1453
+ */
1454
+ deobfuscateN(nParam) {
1455
+ if (!this.nFunc) return nParam;
1456
+ try {
1457
+ return this.nFunc(nParam);
1458
+ } catch {
1459
+ return nParam;
1460
+ }
1461
+ }
1462
+ };
1463
+
1464
+ //#endregion
1465
+ //#region src/http/retry.ts
1466
+ /**
1467
+ * Retry utilities with exponential backoff.
1468
+ * Provides automatic retry logic for transient failures.
1469
+ * @module http/retry
1470
+ */
1471
+ const NON_RETRYABLE_PATTERNS = [
1472
+ "invalid",
1473
+ "not found",
1474
+ "drm",
1475
+ "private"
1476
+ ];
1477
+ /**
1478
+ * Executes a function with automatic retry on failure.
1479
+ * @param fn - The async function to execute
1480
+ * @param options - Retry configuration options
1481
+ * @returns The result of the function if successful
1482
+ * @throws The last error if all retries are exhausted
1483
+ */
1484
+ async function withRetry(fn, options = {}) {
1485
+ const { retries = RETRY_DEFAULTS.RETRIES, delay = RETRY_DEFAULTS.DELAY, backoff = RETRY_DEFAULTS.BACKOFF, maxDelay = RETRY_DEFAULTS.MAX_DELAY, shouldRetry = defaultShouldRetry, onRetry } = options;
1486
+ let lastError;
1487
+ for (let attempt = 0; attempt <= retries; attempt++) try {
1488
+ return await fn();
1489
+ } catch (error) {
1490
+ lastError = error;
1491
+ if (attempt < retries && shouldRetry(lastError)) {
1492
+ const waitTime = Math.min(delay * Math.pow(backoff, attempt), maxDelay);
1493
+ onRetry?.(lastError, attempt + 1);
1494
+ await sleep(waitTime);
1495
+ }
1496
+ }
1497
+ throw lastError;
1498
+ }
1499
+ function defaultShouldRetry(error) {
1500
+ const message = error.message.toLowerCase();
1501
+ return !NON_RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern));
1502
+ }
1503
+ function sleep(ms) {
1504
+ return new Promise((resolve) => setTimeout(resolve, ms));
1505
+ }
1506
+
1507
+ //#endregion
1508
+ //#region src/streaming/formats.ts
1509
+ /**
1510
+ * Parses a raw format object from InnerTube response into a typed Format.
1511
+ * @param raw - The raw format object from streaming data
1512
+ * @param isAdaptive - Whether this is an adaptive format (video-only or audio-only)
1513
+ * @returns Parsed Format object
1514
+ */
1515
+ function parseFormat(raw, isAdaptive) {
1516
+ return {
1517
+ itag: raw.itag,
1518
+ url: raw.url || "",
1519
+ mimeType: raw.mimeType,
1520
+ codecs: extractCodecs(raw.mimeType),
1521
+ bitrate: raw.bitrate,
1522
+ width: raw.width,
1523
+ height: raw.height,
1524
+ fps: raw.fps,
1525
+ qualityLabel: raw.qualityLabel,
1526
+ audioSampleRate: raw.audioSampleRate ? parseInt(raw.audioSampleRate, 10) : void 0,
1527
+ audioChannels: raw.audioChannels,
1528
+ contentLength: raw.contentLength ? parseInt(raw.contentLength, 10) : void 0,
1529
+ hasDrm: !!raw.drmFamilies,
1530
+ isAdaptive
1531
+ };
1532
+ }
1533
+ /**
1534
+ * Extracts codec strings from a mimeType.
1535
+ * @param mimeType - The mimeType string containing codec info
1536
+ * @returns Array of codec strings
1537
+ */
1538
+ function extractCodecs(mimeType) {
1539
+ const match$1 = mimeType.match(/codecs="([^"]+)"/);
1540
+ if (!match$1) return [];
1541
+ return match$1[1].split(", ");
1542
+ }
1543
+ /**
1544
+ * Categorizes formats into combined (progressive), video-only, and audio-only.
1545
+ * @param formats - Array of all formats
1546
+ * @returns Object with categorized formats
1547
+ */
1548
+ function categorizeFormats(formats) {
1549
+ const combined = [];
1550
+ const video = [];
1551
+ const audio = [];
1552
+ for (const format of formats) if (!format.isAdaptive) combined.push(format);
1553
+ else if (format.mimeType.includes("video")) video.push(format);
1554
+ else if (format.mimeType.includes("audio")) audio.push(format);
1555
+ return {
1556
+ combined,
1557
+ video,
1558
+ audio
1559
+ };
1560
+ }
1561
+
1562
+ //#endregion
1563
+ //#region src/streaming/decipher.ts
1564
+ /**
1565
+ * Handles deciphering of YouTube format URLs.
1566
+ * Processes signature ciphers and n-parameter obfuscation.
1567
+ */
1568
+ var FormatDecipher = class {
1569
+ constructor(player) {
1570
+ this.player = player;
1571
+ }
1572
+ /**
1573
+ * Deciphers a single format's URL.
1574
+ * @param format - The raw format with potentially ciphered URL
1575
+ * @returns Format with deciphered URL
1576
+ */
1577
+ decipher(format) {
1578
+ if (format.url && !format.signatureCipher && !format.cipher) return format;
1579
+ let url = format.url || "";
1580
+ if (format.signatureCipher || format.cipher) url = this.player.decipher(url, format.signatureCipher || format.cipher || "");
1581
+ if (url && url.includes("n=")) url = this.deobfuscateNParam(url);
1582
+ return {
1583
+ ...format,
1584
+ url
1585
+ };
1586
+ }
1587
+ /**
1588
+ * Deciphers a batch of formats.
1589
+ * @param formats - Array of raw formats
1590
+ * @returns Array of formats with deciphered URLs
1591
+ */
1592
+ decipherBatch(formats) {
1593
+ return formats.map((format) => this.decipher(format));
1594
+ }
1595
+ deobfuscateNParam(url) {
1596
+ try {
1597
+ const urlObj = new URL(url);
1598
+ const nParam = urlObj.searchParams.get("n");
1599
+ if (nParam) {
1600
+ const deobfuscatedN = this.player.deobfuscateN(nParam);
1601
+ urlObj.searchParams.set("n", deobfuscatedN);
1602
+ return urlObj.toString();
1603
+ }
1604
+ } catch {
1605
+ return url;
1606
+ }
1607
+ return url;
1608
+ }
1609
+ };
1610
+
1611
+ //#endregion
1612
+ //#region src/utils/m3u8.ts
1613
+ /**
1614
+ * Parses an M3U8 playlist.
1615
+ * @param content - M3U8 file content
1616
+ * @returns Result containing parsed playlist or error
1617
+ */
1618
+ function parseM3u8(content) {
1619
+ try {
1620
+ const lines = content.split("\n").map((line) => line.trim()).filter((line) => line);
1621
+ if (!lines[0]?.startsWith("#EXTM3U")) return err(createError("PARSE_ERROR", "Invalid M3U8 file: missing #EXTM3U header"));
1622
+ const version = extractVersion(lines);
1623
+ if (lines.some((line) => line.includes("#EXT-X-STREAM-INF"))) return ok({
1624
+ type: "master",
1625
+ version,
1626
+ variants: parseMasterPlaylist(lines)
1627
+ });
1628
+ return ok({
1629
+ type: "media",
1630
+ version,
1631
+ segments: parseMediaPlaylist(lines),
1632
+ targetDuration: extractTargetDuration(lines)
1633
+ });
1634
+ } catch (error) {
1635
+ return err(createError("PARSE_ERROR", "Failed to parse M3U8", error));
1636
+ }
1637
+ }
1638
+ /**
1639
+ * Extracts version from M3U8 playlist.
1640
+ * @param lines - Playlist lines
1641
+ * @returns Version number
1642
+ */
1643
+ function extractVersion(lines) {
1644
+ const versionLine = lines.find((line) => line.startsWith("#EXT-X-VERSION:"));
1645
+ if (!versionLine) return 3;
1646
+ const version = parseInt(versionLine.split(":")[1], 10);
1647
+ return isNaN(version) ? 3 : version;
1648
+ }
1649
+ /**
1650
+ * Extracts target duration from media playlist.
1651
+ * @param lines - Playlist lines
1652
+ * @returns Target duration in seconds
1653
+ */
1654
+ function extractTargetDuration(lines) {
1655
+ const targetLine = lines.find((line) => line.startsWith("#EXT-X-TARGETDURATION:"));
1656
+ if (!targetLine) return void 0;
1657
+ const duration = parseInt(targetLine.split(":")[1], 10);
1658
+ return isNaN(duration) ? void 0 : duration;
1659
+ }
1660
+ /**
1661
+ * Parses master playlist variants.
1662
+ * @param lines - Playlist lines
1663
+ * @returns Array of HLS variants
1664
+ */
1665
+ function parseMasterPlaylist(lines) {
1666
+ const variants = [];
1667
+ for (let i = 0; i < lines.length; i++) {
1668
+ const line = lines[i];
1669
+ if (line.startsWith("#EXT-X-STREAM-INF:")) {
1670
+ const attributes = parseStreamInf(line);
1671
+ const url = lines[i + 1];
1672
+ if (url && !url.startsWith("#")) variants.push({
1673
+ url,
1674
+ bandwidth: attributes.bandwidth,
1675
+ resolution: attributes.resolution,
1676
+ codecs: attributes.codecs,
1677
+ frameRate: attributes.frameRate
1678
+ });
1679
+ }
1680
+ }
1681
+ return variants;
1682
+ }
1683
+ /**
1684
+ * Parses #EXT-X-STREAM-INF attributes.
1685
+ * @param line - Stream info line
1686
+ * @returns Parsed attributes
1687
+ */
1688
+ function parseStreamInf(line) {
1689
+ const attrs = line.split(":")[1];
1690
+ const bandwidth = extractAttribute(attrs, "BANDWIDTH");
1691
+ const resolution = extractAttribute(attrs, "RESOLUTION");
1692
+ const codecs = extractAttribute(attrs, "CODECS");
1693
+ const frameRateStr = extractAttribute(attrs, "FRAME-RATE");
1694
+ return {
1695
+ bandwidth: parseInt(bandwidth || "0", 10),
1696
+ resolution: resolution || void 0,
1697
+ codecs: codecs || void 0,
1698
+ frameRate: frameRateStr ? parseFloat(frameRateStr) : void 0
1699
+ };
1700
+ }
1701
+ /**
1702
+ * Parses media playlist segments.
1703
+ * @param lines - Playlist lines
1704
+ * @returns Array of HLS segments
1705
+ */
1706
+ function parseMediaPlaylist(lines) {
1707
+ const segments = [];
1708
+ for (let i = 0; i < lines.length; i++) {
1709
+ const line = lines[i];
1710
+ if (line.startsWith("#EXTINF:")) {
1711
+ const duration = parseFloat(line.split(":")[1].split(",")[0]);
1712
+ const url = lines[i + 1];
1713
+ if (url && !url.startsWith("#")) {
1714
+ const byteRange = parseByteRange(lines[i - 1]);
1715
+ segments.push({
1716
+ url,
1717
+ duration,
1718
+ byteRange
1719
+ });
1720
+ }
1721
+ }
1722
+ }
1723
+ return segments;
1724
+ }
1725
+ /**
1726
+ * Parses #EXT-X-BYTERANGE directive.
1727
+ * @param line - Byte range line
1728
+ * @returns Byte range object or undefined
1729
+ */
1730
+ function parseByteRange(line) {
1731
+ if (!line?.startsWith("#EXT-X-BYTERANGE:")) return void 0;
1732
+ const parts = line.split(":")[1].split("@");
1733
+ if (parts.length !== 2) return void 0;
1734
+ const length = parseInt(parts[0], 10);
1735
+ const start = parseInt(parts[1], 10);
1736
+ if (isNaN(length) || isNaN(start)) return void 0;
1737
+ return {
1738
+ start,
1739
+ length
1740
+ };
1741
+ }
1742
+ /**
1743
+ * Extracts attribute value from M3U8 attribute string.
1744
+ * @param attrs - Attribute string
1745
+ * @param name - Attribute name
1746
+ * @returns Attribute value or undefined
1747
+ */
1748
+ function extractAttribute(attrs, name) {
1749
+ const regex = /* @__PURE__ */ new RegExp(`${name}=([^,]+)`);
1750
+ const match$1 = attrs.match(regex);
1751
+ if (!match$1) return void 0;
1752
+ let value = match$1[1].trim();
1753
+ if (value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
1754
+ return value;
1755
+ }
1756
+
1757
+ //#endregion
1758
+ //#region src/streaming/hls/parser.ts
1759
+ /**
1760
+ * Fetches and parses an HLS manifest.
1761
+ * @param manifestUrl - URL to HLS manifest
1762
+ * @returns Result containing parsed playlist or error
1763
+ */
1764
+ async function fetchHlsManifest(manifestUrl) {
1765
+ const response = await httpClient.get(manifestUrl);
1766
+ if (!isOk(response)) return err(response.error);
1767
+ try {
1768
+ return parseM3u8(await response.value.text());
1769
+ } catch (error) {
1770
+ return err(createError("PARSE_ERROR", "Failed to fetch HLS manifest", error));
1771
+ }
1772
+ }
1773
+
1774
+ //#endregion
1775
+ //#region src/utils/xml.ts
1776
+ /**
1777
+ * Parses XML string into a simple node tree.
1778
+ * @param xml - XML string to parse
1779
+ * @returns Root XML node
1780
+ */
1781
+ function parseXml(xml) {
1782
+ const root = {
1783
+ type: "element",
1784
+ name: "root",
1785
+ children: []
1786
+ };
1787
+ const stack = [root];
1788
+ const tagRegex = /<\/?([a-zA-Z0-9_-]+)([^>]*)>/g;
1789
+ let lastIndex = 0;
1790
+ let match$1;
1791
+ while ((match$1 = tagRegex.exec(xml)) !== null) {
1792
+ const textBefore = xml.slice(lastIndex, match$1.index).trim();
1793
+ if (textBefore && stack.length > 0) {
1794
+ const parent = stack[stack.length - 1];
1795
+ if (parent.children) parent.children.push({
1796
+ type: "text",
1797
+ text: textBefore
1798
+ });
1799
+ }
1800
+ const fullTag = match$1[0];
1801
+ const tagName = match$1[1];
1802
+ const attributes = match$1[2];
1803
+ if (fullTag.startsWith("</")) stack.pop();
1804
+ else {
1805
+ const node = {
1806
+ type: "element",
1807
+ name: tagName,
1808
+ attributes: parseAttributes(attributes),
1809
+ children: []
1810
+ };
1811
+ const parent = stack[stack.length - 1];
1812
+ if (parent.children) parent.children.push(node);
1813
+ if (!fullTag.endsWith("/>")) stack.push(node);
1814
+ }
1815
+ lastIndex = tagRegex.lastIndex;
1816
+ }
1817
+ return root;
1818
+ }
1819
+ /**
1820
+ * Parses XML attributes from a string.
1821
+ * @param attrString - Attribute string from XML tag
1822
+ * @returns Record of attribute name-value pairs
1823
+ */
1824
+ function parseAttributes(attrString) {
1825
+ const attrs = {};
1826
+ const attrRegex = /([a-zA-Z0-9_-]+)="([^"]*)"/g;
1827
+ let match$1;
1828
+ while ((match$1 = attrRegex.exec(attrString)) !== null) attrs[match$1[1]] = match$1[2];
1829
+ return attrs;
1830
+ }
1831
+ /**
1832
+ * Finds all nodes matching a tag name.
1833
+ * @param node - Root node to search
1834
+ * @param tagName - Tag name to match
1835
+ * @returns Array of matching nodes
1836
+ */
1837
+ function findAll(node, tagName) {
1838
+ const results = [];
1839
+ if (node.type === "element" && node.name === tagName) results.push(node);
1840
+ if (node.children) for (const child of node.children) results.push(...findAll(child, tagName));
1841
+ return results;
1842
+ }
1843
+ /**
1844
+ * Finds the first node matching a tag name.
1845
+ * @param node - Root node to search
1846
+ * @param tagName - Tag name to match
1847
+ * @returns First matching node or undefined
1848
+ */
1849
+ function findFirst(node, tagName) {
1850
+ if (node.type === "element" && node.name === tagName) return node;
1851
+ if (node.children) for (const child of node.children) {
1852
+ const found = findFirst(child, tagName);
1853
+ if (found) return found;
1854
+ }
1855
+ }
1856
+ /**
1857
+ * Gets attribute value from a node.
1858
+ * @param node - XML node
1859
+ * @param attrName - Attribute name
1860
+ * @returns Attribute value or undefined
1861
+ */
1862
+ function getAttribute(node, attrName) {
1863
+ return node.attributes?.[attrName];
1864
+ }
1865
+ /**
1866
+ * Gets text content from a node.
1867
+ * @param node - XML node
1868
+ * @returns Text content or empty string
1869
+ */
1870
+ function getTextContent(node) {
1871
+ if (node.type === "text") return node.text || "";
1872
+ if (node.children) return node.children.map((child) => getTextContent(child)).join("");
1873
+ return "";
1874
+ }
1875
+
1876
+ //#endregion
1877
+ //#region src/streaming/dash/parser.ts
1878
+ /**
1879
+ * Parses a DASH manifest XML string.
1880
+ * @param xml - DASH manifest XML
1881
+ * @returns Result containing parsed manifest or error
1882
+ */
1883
+ function parseDashManifest(xml) {
1884
+ try {
1885
+ const mpd = findFirst(parseXml(xml), "MPD");
1886
+ if (!mpd) return err(createError("PARSE_ERROR", "Invalid DASH manifest: missing MPD element"));
1887
+ const duration = parseDuration(getAttribute(mpd, "mediaPresentationDuration"));
1888
+ const periods = findAll(mpd, "Period");
1889
+ const adaptationSets = [];
1890
+ for (const period of periods) {
1891
+ const sets = findAll(period, "AdaptationSet");
1892
+ for (const set of sets) {
1893
+ const parsed = parseAdaptationSet(set);
1894
+ if (parsed) adaptationSets.push(parsed);
1895
+ }
1896
+ }
1897
+ return ok({
1898
+ duration,
1899
+ adaptationSets
1900
+ });
1901
+ } catch (error) {
1902
+ return err(createError("PARSE_ERROR", "Failed to parse DASH manifest", error));
1903
+ }
1904
+ }
1905
+ /**
1906
+ * Parses an AdaptationSet element.
1907
+ * @param node - AdaptationSet XML node
1908
+ * @returns Parsed adaptation set or undefined
1909
+ */
1910
+ function parseAdaptationSet(node) {
1911
+ const id = getAttribute(node, "id") || "";
1912
+ const mimeType = getAttribute(node, "mimeType") || "";
1913
+ const contentType = mimeType.startsWith("video") ? "video" : "audio";
1914
+ const representations = findAll(node, "Representation");
1915
+ const parsedReps = [];
1916
+ for (const rep of representations) {
1917
+ const parsed = parseRepresentation(rep);
1918
+ if (parsed) parsedReps.push(parsed);
1919
+ }
1920
+ if (parsedReps.length === 0) return void 0;
1921
+ return {
1922
+ id,
1923
+ mimeType,
1924
+ contentType,
1925
+ representations: parsedReps
1926
+ };
1927
+ }
1928
+ /**
1929
+ * Parses a Representation element.
1930
+ * @param node - Representation XML node
1931
+ * @returns Parsed representation or undefined
1932
+ */
1933
+ function parseRepresentation(node) {
1934
+ const id = getAttribute(node, "id") || "";
1935
+ const bandwidthStr = getAttribute(node, "bandwidth");
1936
+ const codecs = getAttribute(node, "codecs") || "";
1937
+ if (!bandwidthStr) return void 0;
1938
+ const bandwidth = parseInt(bandwidthStr, 10);
1939
+ if (isNaN(bandwidth)) return void 0;
1940
+ const widthStr = getAttribute(node, "width");
1941
+ const heightStr = getAttribute(node, "height");
1942
+ const frameRateStr = getAttribute(node, "frameRate");
1943
+ const audioSampleRateStr = getAttribute(node, "audioSampleRate");
1944
+ const baseUrlNode = findFirst(node, "BaseURL");
1945
+ const baseUrl = baseUrlNode ? getTextContent(baseUrlNode) : "";
1946
+ const initRange = parseRange(findFirst(node, "Initialization"));
1947
+ const indexRange = parseRange(findFirst(node, "SegmentBase"));
1948
+ return {
1949
+ id,
1950
+ bandwidth,
1951
+ codecs,
1952
+ width: widthStr ? parseInt(widthStr, 10) : void 0,
1953
+ height: heightStr ? parseInt(heightStr, 10) : void 0,
1954
+ frameRate: frameRateStr ? parseFloat(frameRateStr) : void 0,
1955
+ audioSampleRate: audioSampleRateStr ? parseInt(audioSampleRateStr, 10) : void 0,
1956
+ baseUrl,
1957
+ initRange,
1958
+ indexRange
1959
+ };
1960
+ }
1961
+ /**
1962
+ * Parses range attribute from a node.
1963
+ * @param node - XML node with range attribute
1964
+ * @returns Range object or undefined
1965
+ */
1966
+ function parseRange(node) {
1967
+ if (!node) return void 0;
1968
+ const range = getAttribute(node, "range");
1969
+ if (!range) return void 0;
1970
+ const parts = range.split("-");
1971
+ if (parts.length !== 2) return void 0;
1972
+ const start = parseInt(parts[0], 10);
1973
+ const end = parseInt(parts[1], 10);
1974
+ if (isNaN(start) || isNaN(end)) return void 0;
1975
+ return {
1976
+ start,
1977
+ end
1978
+ };
1979
+ }
1980
+ /**
1981
+ * Parses ISO 8601 duration string.
1982
+ * @param duration - ISO 8601 duration string (e.g., "PT1H2M3S")
1983
+ * @returns Duration in seconds
1984
+ */
1985
+ function parseDuration(duration) {
1986
+ if (!duration) return 0;
1987
+ const match$1 = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?/);
1988
+ if (!match$1) return 0;
1989
+ const hours = parseInt(match$1[1] || "0", 10);
1990
+ const minutes = parseInt(match$1[2] || "0", 10);
1991
+ const seconds = parseFloat(match$1[3] || "0");
1992
+ return hours * 3600 + minutes * 60 + seconds;
1993
+ }
1994
+
1995
+ //#endregion
1996
+ //#region src/streaming/hls/segments.ts
1997
+ /**
1998
+ * Resolves a potentially relative URL against a base URL.
1999
+ * @param baseUrl - Base URL from manifest location
2000
+ * @param path - Relative or absolute path
2001
+ * @returns Resolved absolute URL
2002
+ */
2003
+ function resolveUrl(baseUrl, path) {
2004
+ if (path.startsWith("http://") || path.startsWith("https://")) return path;
2005
+ const url = new URL(baseUrl);
2006
+ if (path.startsWith("/")) return `${url.protocol}//${url.host}${path}`;
2007
+ const basePath = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
2008
+ return `${url.protocol}//${url.host}${basePath}${path}`;
2009
+ }
2010
+ /**
2011
+ * Resolves all variant URLs in a master playlist.
2012
+ * @param playlist - Parsed HLS master playlist
2013
+ * @param manifestUrl - Original manifest URL
2014
+ * @returns Playlist with resolved variant URLs
2015
+ */
2016
+ function resolveVariantUrls(playlist, manifestUrl) {
2017
+ if (playlist.type !== "master" || !playlist.variants) return playlist;
2018
+ return {
2019
+ ...playlist,
2020
+ variants: playlist.variants.map((variant) => ({
2021
+ ...variant,
2022
+ url: resolveUrl(manifestUrl, variant.url)
2023
+ }))
2024
+ };
2025
+ }
2026
+
2027
+ //#endregion
2028
+ //#region src/streaming/dash/segments.ts
2029
+ /**
2030
+ * Extracts stream information from a DASH manifest.
2031
+ * @param manifest - Parsed DASH manifest
2032
+ * @returns Extracted stream information
2033
+ */
2034
+ function extractDashStreams(manifest) {
2035
+ const videoStreams = [];
2036
+ const audioStreams = [];
2037
+ for (const adaptationSet of manifest.adaptationSets) {
2038
+ const streams = adaptationSet.contentType === "video" ? videoStreams : audioStreams;
2039
+ for (const rep of adaptationSet.representations) streams.push(representationToStreamInfo(rep, adaptationSet));
2040
+ }
2041
+ videoStreams.sort((a, b) => b.bandwidth - a.bandwidth);
2042
+ audioStreams.sort((a, b) => b.bandwidth - a.bandwidth);
2043
+ return {
2044
+ duration: manifest.duration,
2045
+ videoStreams,
2046
+ audioStreams
2047
+ };
2048
+ }
2049
+ /**
2050
+ * Converts a DASH representation to stream info.
2051
+ * @param rep - DASH representation
2052
+ * @param adaptationSet - Parent adaptation set
2053
+ * @returns Stream info
2054
+ */
2055
+ function representationToStreamInfo(rep, adaptationSet) {
2056
+ return {
2057
+ representationId: rep.id,
2058
+ bandwidth: rep.bandwidth,
2059
+ mimeType: adaptationSet.mimeType,
2060
+ codecs: rep.codecs,
2061
+ width: rep.width,
2062
+ height: rep.height,
2063
+ baseUrl: rep.baseUrl,
2064
+ initRange: rep.initRange,
2065
+ indexRange: rep.indexRange
2066
+ };
2067
+ }
2068
+
2069
+ //#endregion
2070
+ //#region src/streaming/processor.ts
2071
+ /**
2072
+ * Processes streaming data from InnerTube API responses.
2073
+ * Handles format parsing, deciphering, and categorization.
2074
+ */
2075
+ var StreamingDataProcessor = class {
2076
+ decipher;
2077
+ constructor(player) {
2078
+ this.decipher = new FormatDecipher(player);
2079
+ }
2080
+ /**
2081
+ * Processes streaming data into a categorized FormatCollection.
2082
+ * @param streamingData - Raw streaming data from player response
2083
+ * @returns Categorized format collection with deciphered URLs
2084
+ */
2085
+ async process(streamingData) {
2086
+ const rawFormats = streamingData.formats || [];
2087
+ const rawAdaptive = streamingData.adaptiveFormats || [];
2088
+ const decipheredFormats = this.decipher.decipherBatch(rawFormats);
2089
+ const decipheredAdaptive = this.decipher.decipherBatch(rawAdaptive);
2090
+ const formats = decipheredFormats.map((f) => parseFormat(f, false));
2091
+ const adaptiveFormats = decipheredAdaptive.map((f) => parseFormat(f, true));
2092
+ const categorized = categorizeFormats([...formats, ...adaptiveFormats]);
2093
+ const [hls, dash] = await Promise.all([this.extractHlsInfo(streamingData), this.extractDashInfo(streamingData)]);
2094
+ return {
2095
+ combined: categorized.combined,
2096
+ video: categorized.video,
2097
+ audio: categorized.audio,
2098
+ hls,
2099
+ dash
2100
+ };
2101
+ }
2102
+ async extractHlsInfo(streamingData) {
2103
+ if (!streamingData.hlsManifestUrl) return void 0;
2104
+ const manifestUrl = streamingData.hlsManifestUrl;
2105
+ const result = await fetchHlsManifest(manifestUrl);
2106
+ if (!isOk(result)) return {
2107
+ manifestUrl,
2108
+ variants: []
2109
+ };
2110
+ const playlist = result.value;
2111
+ if (playlist.type !== "master" || !playlist.variants) return {
2112
+ manifestUrl,
2113
+ variants: []
2114
+ };
2115
+ return {
2116
+ manifestUrl,
2117
+ variants: (resolveVariantUrls(playlist, manifestUrl).variants || []).map((v) => ({
2118
+ url: v.url,
2119
+ bandwidth: v.bandwidth,
2120
+ resolution: v.resolution,
2121
+ codecs: v.codecs
2122
+ }))
2123
+ };
2124
+ }
2125
+ async extractDashInfo(streamingData) {
2126
+ if (!streamingData.dashManifestUrl) return void 0;
2127
+ const manifestUrl = streamingData.dashManifestUrl;
2128
+ const response = await httpClient.get(manifestUrl);
2129
+ if (!isOk(response)) return {
2130
+ manifestUrl,
2131
+ duration: 0
2132
+ };
2133
+ try {
2134
+ const parseResult = parseDashManifest(await response.value.text());
2135
+ if (!isOk(parseResult)) return {
2136
+ manifestUrl,
2137
+ duration: 0
2138
+ };
2139
+ return {
2140
+ manifestUrl,
2141
+ duration: extractDashStreams(parseResult.value).duration
2142
+ };
2143
+ } catch {
2144
+ return {
2145
+ manifestUrl,
2146
+ duration: 0
2147
+ };
2148
+ }
2149
+ }
2150
+ };
2151
+
2152
+ //#endregion
2153
+ //#region src/core/extractor.ts
2154
+ const VIDEO_ID_PATTERNS = [
2155
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
2156
+ /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
2157
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
2158
+ /youtube\.com\/live\/([a-zA-Z0-9_-]{11})/,
2159
+ /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
2160
+ /youtube-nocookie\.com\/embed\/([a-zA-Z0-9_-]{11})/,
2161
+ /m\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/,
2162
+ /music\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/
2163
+ ];
2164
+ /**
2165
+ * Main YouTube video extractor class.
2166
+ * Handles video information extraction using InnerTube API.
2167
+ */
2168
+ var YouTubeExtractor = class {
2169
+ client;
2170
+ player;
2171
+ config;
2172
+ constructor(config = {}) {
2173
+ this.config = config;
2174
+ this.client = new InnerTubeClient({ initialClient: config.preferredClient });
2175
+ this.player = new Player(config.cacheDir);
2176
+ }
2177
+ /**
2178
+ * Extracts video information from a YouTube URL or video ID.
2179
+ * @param url - YouTube URL or video ID
2180
+ * @returns Result containing VideoInfo or BundlpError
2181
+ */
2182
+ async extract(url) {
2183
+ const videoIdResult = this.parseVideoId(url);
2184
+ if (isErr(videoIdResult)) return videoIdResult;
2185
+ return withRetry(() => this.realExtract(videoIdResult.value), {
2186
+ retries: RETRY_DEFAULTS.RETRIES,
2187
+ delay: RETRY_DEFAULTS.DELAY,
2188
+ backoff: RETRY_DEFAULTS.BACKOFF
2189
+ });
2190
+ }
2191
+ parseVideoId(url) {
2192
+ for (const pattern of VIDEO_ID_PATTERNS) {
2193
+ const match$1 = url.match(pattern);
2194
+ if (match$1) return ok(match$1[1]);
2195
+ }
2196
+ if ((/* @__PURE__ */ new RegExp(`^[a-zA-Z0-9_-]{${VIDEO_ID_LENGTH}}$`)).test(url)) return ok(url);
2197
+ return err(createError("INVALID_URL", `Could not extract video ID from: ${url}`));
2198
+ }
2199
+ async realExtract(videoId) {
2200
+ const playerResult = await this.player.initialize();
2201
+ if (isErr(playerResult)) return err(playerResult.error);
2202
+ const responseResult = await this.client.fetchPlayerWithFallback(videoId, {
2203
+ signatureTimestamp: this.player.getSignatureTimestamp(),
2204
+ poToken: this.config.poToken
2205
+ });
2206
+ if (isErr(responseResult)) return err(responseResult.error);
2207
+ const playerResponse = responseResult.value;
2208
+ if (playerResponse.playabilityStatus.status === "ERROR") return err(createError("VIDEO_UNAVAILABLE", playerResponse.playabilityStatus.reason || "Video unavailable"));
2209
+ if (!playerResponse.streamingData) return err(createError("VIDEO_UNAVAILABLE", "No streaming data found"));
2210
+ const formats = await this.processFormats(playerResponse.streamingData);
2211
+ return ok(this.assembleVideoInfo(videoId, playerResponse, formats));
2212
+ }
2213
+ async processFormats(streamingData) {
2214
+ return new StreamingDataProcessor(this.player).process(streamingData);
2215
+ }
2216
+ assembleVideoInfo(videoId, playerResponse, formats) {
2217
+ const details = playerResponse.videoDetails;
2218
+ const microformat = playerResponse.microformat?.playerMicroformatRenderer;
2219
+ return {
2220
+ id: videoId,
2221
+ title: details.title,
2222
+ description: details.shortDescription,
2223
+ duration: parseInt(details.lengthSeconds, 10),
2224
+ uploadDate: microformat?.uploadDate,
2225
+ channel: {
2226
+ id: details.channelId,
2227
+ name: details.author,
2228
+ url: `${YOUTUBE_BASE_URL}/channel/${details.channelId}`
2229
+ },
2230
+ viewCount: parseInt(details.viewCount, 10),
2231
+ thumbnails: details.thumbnail.thumbnails,
2232
+ formats,
2233
+ subtitles: /* @__PURE__ */ new Map(),
2234
+ isLive: details.isLiveContent,
2235
+ isPrivate: details.isPrivate
2236
+ };
2237
+ }
2238
+ };
2239
+
2240
+ //#endregion
2241
+ //#region src/utils/headers.ts
2242
+ /**
2243
+ * HTTP Headers Utilities for YouTube Downloads
2244
+ *
2245
+ * Provides headers needed for downloading from deciphered YouTube URLs.
2246
+ * These headers are extracted from bundlp's InnerTube client configurations.
2247
+ */
2248
+ /**
2249
+ * Get HTTP headers for downloading from deciphered YouTube URLs
2250
+ * Based on the client type used for extraction
2251
+ *
2252
+ * @param clientType - The client type to use (WEB, ANDROID, TV, IOS)
2253
+ * @returns Object with required HTTP headers for YouTube CDN requests
2254
+ */
2255
+ function getDownloadHeaders(clientType = "ANDROID") {
2256
+ const client = INNERTUBE_CLIENTS[clientType];
2257
+ return {
2258
+ "User-Agent": client.HEADERS["User-Agent"],
2259
+ "Accept": "*/*",
2260
+ "Accept-Encoding": "identity",
2261
+ "Referer": "https://www.youtube.com/",
2262
+ "Origin": "https://www.youtube.com",
2263
+ "Sec-Fetch-Mode": "no-cors"
2264
+ };
2265
+ }
2266
+ /**
2267
+ * Get minimal headers that work for most YouTube CDN requests
2268
+ * Use this as a fallback if full headers cause issues
2269
+ *
2270
+ * @returns Minimal set of headers needed for YouTube downloads
2271
+ */
2272
+ function getMinimalDownloadHeaders() {
2273
+ return {
2274
+ "User-Agent": "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
2275
+ "Referer": "https://www.youtube.com/"
2276
+ };
2277
+ }
2278
+
2279
+ //#endregion
2280
+ //#region src/po-token/policies.ts
2281
+ /**
2282
+ * PO Token policies for each InnerTube client.
2283
+ * Defines when PO tokens are required for different endpoints.
2284
+ */
2285
+ const PO_TOKEN_POLICIES = {
2286
+ WEB: {
2287
+ GVS: {
2288
+ HTTPS: "required",
2289
+ DASH: "required",
2290
+ HLS: "recommended"
2291
+ },
2292
+ PLAYER: "not_required",
2293
+ SUBS: "not_required",
2294
+ notRequiredForPremium: true
2295
+ },
2296
+ ANDROID_SDKLESS: {
2297
+ GVS: "not_required",
2298
+ PLAYER: "not_required",
2299
+ SUBS: "not_required"
2300
+ },
2301
+ ANDROID: {
2302
+ GVS: "not_required",
2303
+ PLAYER: "not_required",
2304
+ SUBS: "not_required"
2305
+ },
2306
+ TV: {
2307
+ GVS: "not_required",
2308
+ PLAYER: "not_required",
2309
+ SUBS: "not_required"
2310
+ },
2311
+ TV_EMBEDDED: {
2312
+ GVS: "not_required",
2313
+ PLAYER: "not_required",
2314
+ SUBS: "not_required"
2315
+ },
2316
+ IOS: {
2317
+ GVS: {
2318
+ HTTPS: "required",
2319
+ DASH: "required",
2320
+ HLS: "recommended"
2321
+ },
2322
+ PLAYER: "recommended",
2323
+ SUBS: "not_required"
2324
+ }
2325
+ };
2326
+
2327
+ //#endregion
2328
+ //#region src/po-token/botguard/challenge.ts
2329
+ const WAA_CREATE_ENDPOINT = "https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/Create";
2330
+ const INNERTUBE_ATT_ENDPOINT = "https://www.youtube.com/youtubei/v1/att/get";
2331
+ const GOOG_API_KEY$1 = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw";
2332
+ const REQUEST_KEY$1 = "O43z0dpjhgX20SCx4KAo";
2333
+ var ChallengeFetcher = class {
2334
+ timeout;
2335
+ constructor(options = {}) {
2336
+ this.timeout = options.timeout ?? 15e3;
2337
+ }
2338
+ async fetchAttestation() {
2339
+ const payload = { context: { client: {
2340
+ clientName: "WEB",
2341
+ clientVersion: "2.20250222.10.00",
2342
+ hl: "en",
2343
+ gl: "US"
2344
+ } } };
2345
+ try {
2346
+ const response = await fetch(INNERTUBE_ATT_ENDPOINT, {
2347
+ method: "POST",
2348
+ headers: {
2349
+ "Content-Type": "application/json",
2350
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
2351
+ "Origin": "https://www.youtube.com",
2352
+ "Referer": "https://www.youtube.com/"
2353
+ },
2354
+ body: JSON.stringify(payload),
2355
+ signal: AbortSignal.timeout(this.timeout)
2356
+ });
2357
+ if (!response.ok) return err(createError("BOTGUARD_INIT_FAILED", `Attestation request failed with status ${response.status}`));
2358
+ const data = await response.json();
2359
+ return this.parseAttestationResponse(data);
2360
+ } catch (error) {
2361
+ if (error instanceof Error && error.name === "TimeoutError") return err(createError("BOTGUARD_INIT_FAILED", "Attestation fetch timeout"));
2362
+ return err(createError("BOTGUARD_INIT_FAILED", "Failed to fetch attestation", error));
2363
+ }
2364
+ }
2365
+ parseAttestationResponse(data) {
2366
+ if (!data || typeof data !== "object") return err(createError("PARSE_ERROR", "Invalid attestation response"));
2367
+ const response = data;
2368
+ const responseContext = response.responseContext;
2369
+ const botguardData = response.botguardData;
2370
+ const challenge = response.challenge;
2371
+ if (!responseContext?.visitorData) return err(createError("PARSE_ERROR", "Missing visitorData in response"));
2372
+ if (!challenge || typeof challenge !== "string") return err(createError("PARSE_ERROR", "Missing challenge in response"));
2373
+ if (!botguardData?.program) return err(createError("PARSE_ERROR", "Missing botguardData.program in response"));
2374
+ return ok({
2375
+ visitorData: responseContext.visitorData,
2376
+ challenge,
2377
+ program: botguardData.program
2378
+ });
2379
+ }
2380
+ async fetchWaaChallenge() {
2381
+ try {
2382
+ const response = await fetch(WAA_CREATE_ENDPOINT, {
2383
+ method: "POST",
2384
+ headers: {
2385
+ "Content-Type": "application/json+protobuf",
2386
+ "x-goog-api-key": GOOG_API_KEY$1
2387
+ },
2388
+ body: JSON.stringify([REQUEST_KEY$1]),
2389
+ signal: AbortSignal.timeout(this.timeout)
2390
+ });
2391
+ if (!response.ok) return err(createError("BOTGUARD_INIT_FAILED", `WAA Create request failed with status ${response.status}`));
2392
+ const encoded = (await response.json())[1];
2393
+ if (!encoded || typeof encoded !== "string") return err(createError("PARSE_ERROR", "Invalid WAA response format"));
2394
+ return this.descrambleWaaResponse(encoded);
2395
+ } catch (error) {
2396
+ if (error instanceof Error && error.name === "TimeoutError") return err(createError("BOTGUARD_INIT_FAILED", "WAA Create fetch timeout"));
2397
+ return err(createError("BOTGUARD_INIT_FAILED", "Failed to fetch WAA challenge", error));
2398
+ }
2399
+ }
2400
+ descrambleWaaResponse(encoded) {
2401
+ try {
2402
+ const decoded = atob(encoded);
2403
+ const bytes = new Uint8Array(decoded.length);
2404
+ for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i) + 97 & 255;
2405
+ const descrambled = new TextDecoder("utf-8").decode(bytes);
2406
+ const parsed = JSON.parse(descrambled);
2407
+ const messageId = parsed[0];
2408
+ const interpreterArray = parsed[1];
2409
+ const interpreterHash = parsed[3];
2410
+ const program = parsed[4];
2411
+ const globalName = parsed[5];
2412
+ if (!program || !globalName) return err(createError("PARSE_ERROR", "Missing required fields in WAA response"));
2413
+ let interpreterScript = "";
2414
+ if (Array.isArray(interpreterArray) && interpreterArray.length >= 6) interpreterScript = interpreterArray[5];
2415
+ if (!interpreterScript || typeof interpreterScript !== "string") return err(createError("PARSE_ERROR", "Failed to extract interpreter script"));
2416
+ return ok({
2417
+ messageId: messageId || "",
2418
+ interpreterScript,
2419
+ interpreterHash: interpreterHash || "",
2420
+ program,
2421
+ globalName
2422
+ });
2423
+ } catch (error) {
2424
+ return err(createError("PARSE_ERROR", "Failed to descramble WAA response", error));
2425
+ }
2426
+ }
2427
+ async fetchFullChallenge() {
2428
+ return this.fetchWaaChallenge();
2429
+ }
2430
+ async fetch() {
2431
+ return this.fetchAttestation();
2432
+ }
2433
+ };
2434
+
2435
+ //#endregion
2436
+ //#region src/po-token/botguard/client.ts
2437
+ var DeferredPromise = class {
2438
+ promise;
2439
+ resolve;
2440
+ reject;
2441
+ constructor() {
2442
+ this.promise = new Promise((resolve, reject) => {
2443
+ this.resolve = resolve;
2444
+ this.reject = reject;
2445
+ });
2446
+ }
2447
+ };
2448
+ var BotGuardClient = class BotGuardClient {
2449
+ dom = null;
2450
+ vm = null;
2451
+ program;
2452
+ globalName;
2453
+ deferredVmFunctions = new DeferredPromise();
2454
+ syncSnapshotFunction;
2455
+ initTimeout;
2456
+ snapshotTimeout;
2457
+ constructor(challenge, options = {}) {
2458
+ this.program = challenge.program;
2459
+ this.globalName = challenge.globalName;
2460
+ this.initTimeout = options.timeout ?? 1e4;
2461
+ this.snapshotTimeout = 5e3;
2462
+ }
2463
+ static async create(challenge, options = {}) {
2464
+ const client = new BotGuardClient(challenge, options);
2465
+ const loadResult = await client.load(challenge);
2466
+ if (!isOk(loadResult)) return loadResult;
2467
+ return ok(client);
2468
+ }
2469
+ async load(challenge) {
2470
+ try {
2471
+ this.dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
2472
+ url: "https://www.youtube.com/",
2473
+ runScripts: "dangerously",
2474
+ pretendToBeVisual: true
2475
+ });
2476
+ const window = this.dom.window;
2477
+ window.trustedTypes = { createPolicy: (name, rules) => ({
2478
+ name,
2479
+ createHTML: rules?.createHTML || ((s) => s),
2480
+ createScript: rules?.createScript || ((s) => s),
2481
+ createScriptURL: rules?.createScriptURL || ((s) => s)
2482
+ }) };
2483
+ window.fetch = globalThis.fetch;
2484
+ window.Request = globalThis.Request;
2485
+ window.Response = globalThis.Response;
2486
+ window.Headers = globalThis.Headers;
2487
+ const evalFn = window.eval;
2488
+ evalFn(challenge.interpreterScript);
2489
+ this.vm = window[this.globalName];
2490
+ if (!this.vm) return err(createError("BOTGUARD_INIT_FAILED", `VM not found at globalName: ${this.globalName}`));
2491
+ if (typeof this.vm.a !== "function") return err(createError("BOTGUARD_INIT_FAILED", "VM init function (a) not found"));
2492
+ const loadResult = await this.loadProgram();
2493
+ if (!isOk(loadResult)) return loadResult;
2494
+ return ok(void 0);
2495
+ } catch (error) {
2496
+ return err(createError("BOTGUARD_INIT_FAILED", `Failed to load BotGuard: ${error.message}`, error));
2497
+ }
2498
+ }
2499
+ async loadProgram() {
2500
+ return new Promise((resolve) => {
2501
+ const timeoutId = setTimeout(() => {
2502
+ resolve(err(createError("BOTGUARD_INIT_FAILED", "Program load timeout")));
2503
+ }, this.initTimeout);
2504
+ try {
2505
+ const vmFunctionsCallback = (asyncSnapshotFunction, shutdownFunction, passEventFunction, checkCameraFunction) => {
2506
+ this.deferredVmFunctions.resolve({
2507
+ asyncSnapshotFunction,
2508
+ shutdownFunction,
2509
+ passEventFunction,
2510
+ checkCameraFunction
2511
+ });
2512
+ };
2513
+ const initFn = this.vm.a;
2514
+ const result = initFn(this.program, vmFunctionsCallback, true, void 0, () => {}, [[], []]);
2515
+ clearTimeout(timeoutId);
2516
+ if (Array.isArray(result) && result[0]) this.syncSnapshotFunction = result[0];
2517
+ resolve(ok(void 0));
2518
+ } catch (error) {
2519
+ clearTimeout(timeoutId);
2520
+ resolve(err(createError("BOTGUARD_INIT_FAILED", `Program load error: ${error.message}`, error)));
2521
+ }
2522
+ });
2523
+ }
2524
+ async snapshot(binding) {
2525
+ const snapshotArgs = [
2526
+ binding,
2527
+ Date.now(),
2528
+ [],
2529
+ false
2530
+ ];
2531
+ try {
2532
+ let result;
2533
+ if (this.syncSnapshotFunction) result = await this.syncSnapshotFunction(snapshotArgs);
2534
+ else {
2535
+ const vmFunctions = await Promise.race([this.deferredVmFunctions.promise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Timeout waiting for VM functions")), this.snapshotTimeout))]);
2536
+ if (!vmFunctions.asyncSnapshotFunction) return err(createError("BOTGUARD_SNAPSHOT_FAILED", "No snapshot function available"));
2537
+ result = await new Promise((resolve, reject) => {
2538
+ const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Snapshot timeout")), this.snapshotTimeout);
2539
+ vmFunctions.asyncSnapshotFunction((response) => {
2540
+ clearTimeout(timeout);
2541
+ resolve(response);
2542
+ }, snapshotArgs);
2543
+ });
2544
+ }
2545
+ return ok(result);
2546
+ } catch (error) {
2547
+ return err(createError("BOTGUARD_SNAPSHOT_FAILED", `Snapshot failed: ${error.message}`, error));
2548
+ }
2549
+ }
2550
+ async snapshotWithSignalOutput(binding) {
2551
+ const webPoSignalOutput = [];
2552
+ const snapshotArgs = [
2553
+ binding,
2554
+ Date.now(),
2555
+ webPoSignalOutput,
2556
+ false
2557
+ ];
2558
+ try {
2559
+ let snapshot;
2560
+ if (this.syncSnapshotFunction) snapshot = await this.syncSnapshotFunction(snapshotArgs);
2561
+ else {
2562
+ const vmFunctions = await Promise.race([this.deferredVmFunctions.promise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Timeout waiting for VM functions")), this.snapshotTimeout))]);
2563
+ if (!vmFunctions.asyncSnapshotFunction) return err(createError("BOTGUARD_SNAPSHOT_FAILED", "No snapshot function available"));
2564
+ snapshot = await new Promise((resolve, reject) => {
2565
+ const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Snapshot timeout")), this.snapshotTimeout);
2566
+ vmFunctions.asyncSnapshotFunction((response) => {
2567
+ clearTimeout(timeout);
2568
+ resolve(response);
2569
+ }, snapshotArgs);
2570
+ });
2571
+ }
2572
+ return ok({
2573
+ snapshot,
2574
+ webPoSignalOutput
2575
+ });
2576
+ } catch (error) {
2577
+ return err(createError("BOTGUARD_SNAPSHOT_FAILED", `Snapshot failed: ${error.message}`, error));
2578
+ }
2579
+ }
2580
+ shutdown() {
2581
+ this.deferredVmFunctions.promise.then((vmFunctions) => {
2582
+ if (vmFunctions.shutdownFunction) try {
2583
+ vmFunctions.shutdownFunction();
2584
+ } catch {}
2585
+ }).catch(() => {});
2586
+ if (this.dom) {
2587
+ this.dom.window.close();
2588
+ this.dom = null;
2589
+ }
2590
+ }
2591
+ };
2592
+ function generateColdStartToken(identifier, clientState = 1) {
2593
+ const encodedIdentifier = new TextEncoder().encode(identifier);
2594
+ if (encodedIdentifier.length > 118) throw new Error("Content binding is too long");
2595
+ const timestamp = Math.floor(Date.now() / 1e3);
2596
+ const randomKeys = [Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)];
2597
+ const header = new Uint8Array([
2598
+ randomKeys[0],
2599
+ randomKeys[1],
2600
+ 0,
2601
+ clientState,
2602
+ timestamp >> 24 & 255,
2603
+ timestamp >> 16 & 255,
2604
+ timestamp >> 8 & 255,
2605
+ timestamp & 255
2606
+ ]);
2607
+ const packet = new Uint8Array(2 + header.length + encodedIdentifier.length);
2608
+ packet[0] = 34;
2609
+ packet[1] = header.length + encodedIdentifier.length;
2610
+ packet.set(header, 2);
2611
+ packet.set(encodedIdentifier, 2 + header.length);
2612
+ const payload = packet.subarray(2);
2613
+ const keyLength = randomKeys.length;
2614
+ for (let i = keyLength; i < payload.length; i++) payload[i] ^= payload[i % keyLength];
2615
+ return u8ToBase64Url$1(packet);
2616
+ }
2617
+ function u8ToBase64Url$1(u8) {
2618
+ return btoa(String.fromCharCode(...u8)).replace(/\+/g, "-").replace(/\//g, "_");
2619
+ }
2620
+
2621
+ //#endregion
2622
+ //#region src/po-token/minter/web-minter.ts
2623
+ const WAA_GENERATE_IT_ENDPOINT = "https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/GenerateIT";
2624
+ const GOOG_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw";
2625
+ const REQUEST_KEY = "O43z0dpjhgX20SCx4KAo";
2626
+ var WebPoMinter = class WebPoMinter {
2627
+ mintCallback;
2628
+ constructor(mintCallback) {
2629
+ this.mintCallback = mintCallback;
2630
+ }
2631
+ static async create(integrityTokenData, webPoSignalOutput) {
2632
+ const getMinter = webPoSignalOutput[0];
2633
+ if (!getMinter || typeof getMinter !== "function") return err(createError("INTEGRITY_TOKEN_FAILED", "Minter function not found in webPoSignalOutput"));
2634
+ if (!integrityTokenData.integrityToken) return err(createError("INTEGRITY_TOKEN_FAILED", "No integrity token provided"));
2635
+ try {
2636
+ const mintCallback = await getMinter(base64ToU8(integrityTokenData.integrityToken));
2637
+ if (typeof mintCallback !== "function") return err(createError("INTEGRITY_TOKEN_FAILED", "Invalid mint callback returned"));
2638
+ return ok(new WebPoMinter(mintCallback));
2639
+ } catch (error) {
2640
+ return err(createError("INTEGRITY_TOKEN_FAILED", `Failed to create minter: ${error.message}`, error));
2641
+ }
2642
+ }
2643
+ async mint(identifier) {
2644
+ try {
2645
+ const identifierBytes = new TextEncoder().encode(identifier);
2646
+ const result = await this.mintCallback(identifierBytes);
2647
+ if (!result) return err(createError("INTEGRITY_TOKEN_FAILED", "Mint returned undefined"));
2648
+ if (result instanceof Uint8Array) return ok(result);
2649
+ if (ArrayBuffer.isView(result)) return ok(new Uint8Array(result.buffer));
2650
+ if (Array.isArray(result)) return ok(new Uint8Array(result));
2651
+ return err(createError("INTEGRITY_TOKEN_FAILED", `Mint returned invalid type: ${typeof result}`));
2652
+ } catch (error) {
2653
+ return err(createError("INTEGRITY_TOKEN_FAILED", `Mint failed: ${error.message}`, error));
2654
+ }
2655
+ }
2656
+ async mintAsWebsafeString(identifier) {
2657
+ const mintResult = await this.mint(identifier);
2658
+ if (!isOk(mintResult)) return mintResult;
2659
+ return ok(u8ToBase64Url(mintResult.value));
2660
+ }
2661
+ };
2662
+ async function fetchIntegrityToken(botguardResponse, options = {}) {
2663
+ const timeout = options.timeout ?? 1e4;
2664
+ try {
2665
+ const response = await fetch(WAA_GENERATE_IT_ENDPOINT, {
2666
+ method: "POST",
2667
+ headers: {
2668
+ "Content-Type": "application/json+protobuf",
2669
+ "x-goog-api-key": GOOG_API_KEY
2670
+ },
2671
+ body: JSON.stringify([REQUEST_KEY, botguardResponse]),
2672
+ signal: AbortSignal.timeout(timeout)
2673
+ });
2674
+ if (!response.ok) return err(createError("INTEGRITY_TOKEN_FAILED", `GenerateIT request failed with status ${response.status}`));
2675
+ const json = await response.json();
2676
+ const integrityToken = json[0];
2677
+ const estimatedTtlSecs = json[1];
2678
+ const mintRefreshThreshold = json[2];
2679
+ const websafeFallbackToken = json[3];
2680
+ if (!integrityToken) return err(createError("INTEGRITY_TOKEN_FAILED", "No integrity token in response"));
2681
+ return ok({
2682
+ integrityToken,
2683
+ estimatedTtlSecs: estimatedTtlSecs ?? 3600,
2684
+ mintRefreshThreshold: mintRefreshThreshold ?? 300,
2685
+ websafeFallbackToken
2686
+ });
2687
+ } catch (error) {
2688
+ if (error instanceof Error && error.name === "TimeoutError") return err(createError("INTEGRITY_TOKEN_FAILED", "GenerateIT request timeout"));
2689
+ return err(createError("INTEGRITY_TOKEN_FAILED", "Failed to fetch integrity token", error));
2690
+ }
2691
+ }
2692
+ function base64ToU8(base64) {
2693
+ const base64Normalized = base64.replace(/-/g, "+").replace(/_/g, "/").replace(/\./g, "=");
2694
+ const binary = atob(base64Normalized);
2695
+ const bytes = new Uint8Array(binary.length);
2696
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2697
+ return bytes;
2698
+ }
2699
+ function u8ToBase64Url(u8) {
2700
+ return btoa(String.fromCharCode(...u8)).replace(/\+/g, "-").replace(/\//g, "_");
2701
+ }
2702
+
2703
+ //#endregion
2704
+ //#region src/po-token/providers/local.provider.ts
2705
+ const REFRESH_MARGIN_MS = 600 * 1e3;
2706
+ var LocalBotGuardProvider = class {
2707
+ name = "local-botguard";
2708
+ priority = 100;
2709
+ options;
2710
+ challengeFetcher;
2711
+ client = null;
2712
+ challenge = null;
2713
+ attestation = null;
2714
+ integrityData = null;
2715
+ minter = null;
2716
+ initPromise = null;
2717
+ createdAt = 0;
2718
+ constructor(options = {}) {
2719
+ this.options = options;
2720
+ this.challengeFetcher = new ChallengeFetcher({ timeout: options.timeout });
2721
+ }
2722
+ async isAvailable() {
2723
+ try {
2724
+ await import("jsdom");
2725
+ return true;
2726
+ } catch {
2727
+ return false;
2728
+ }
2729
+ }
2730
+ async generate(identifier, _binding) {
2731
+ const initResult = await this.ensureInitialized();
2732
+ if (!isOk(initResult)) return initResult;
2733
+ if (!this.minter || !this.integrityData) return err(createError("BOTGUARD_INIT_FAILED", "Minter not initialized"));
2734
+ const tokenResult = await this.minter.mintAsWebsafeString(identifier);
2735
+ if (!isOk(tokenResult)) return tokenResult;
2736
+ const now = Date.now();
2737
+ const ttlMs = (this.integrityData.estimatedTtlSecs ?? 43200) * 1e3;
2738
+ return ok({
2739
+ token: tokenResult.value,
2740
+ createdAt: now,
2741
+ expiresAt: now + ttlMs,
2742
+ refreshAt: now + ttlMs - REFRESH_MARGIN_MS,
2743
+ bindingType: "session",
2744
+ bindingValue: identifier,
2745
+ client: "WEB",
2746
+ context: "GVS"
2747
+ });
2748
+ }
2749
+ async ensureInitialized() {
2750
+ if (this.minter && this.integrityData) {
2751
+ const now = Date.now();
2752
+ const refreshThresholdMs = (this.integrityData.mintRefreshThreshold ?? 100) * 1e3;
2753
+ if (now - this.createdAt < refreshThresholdMs * 1e3) return ok(void 0);
2754
+ this.reset();
2755
+ }
2756
+ if (this.initPromise) return this.initPromise;
2757
+ this.initPromise = this.initialize();
2758
+ const result = await this.initPromise;
2759
+ this.initPromise = null;
2760
+ return result;
2761
+ }
2762
+ async initialize() {
2763
+ const challengeResult = await this.challengeFetcher.fetchFullChallenge();
2764
+ if (!isOk(challengeResult)) return challengeResult;
2765
+ this.challenge = challengeResult.value;
2766
+ const attResult = await this.challengeFetcher.fetchAttestation();
2767
+ if (!isOk(attResult)) return attResult;
2768
+ this.attestation = attResult.value;
2769
+ const clientResult = await BotGuardClient.create(this.challenge, { timeout: this.options.timeout });
2770
+ if (!isOk(clientResult)) return clientResult;
2771
+ this.client = clientResult.value;
2772
+ const snapshotResult = await this.client.snapshotWithSignalOutput();
2773
+ if (!isOk(snapshotResult)) return snapshotResult;
2774
+ const { snapshot, webPoSignalOutput } = snapshotResult.value;
2775
+ const integrityResult = await fetchIntegrityToken(snapshot, { timeout: this.options.timeout });
2776
+ if (!isOk(integrityResult)) return integrityResult;
2777
+ this.integrityData = integrityResult.value;
2778
+ this.createdAt = Date.now();
2779
+ const minterResult = await WebPoMinter.create(this.integrityData, webPoSignalOutput);
2780
+ if (!isOk(minterResult)) return minterResult;
2781
+ this.minter = minterResult.value;
2782
+ return ok(void 0);
2783
+ }
2784
+ reset() {
2785
+ if (this.client) {
2786
+ this.client.shutdown();
2787
+ this.client = null;
2788
+ }
2789
+ this.challenge = null;
2790
+ this.attestation = null;
2791
+ this.integrityData = null;
2792
+ this.minter = null;
2793
+ }
2794
+ shutdown() {
2795
+ this.reset();
2796
+ }
2797
+ getVisitorData() {
2798
+ return this.attestation?.visitorData;
2799
+ }
2800
+ };
2801
+
2802
+ //#endregion
2803
+ //#region src/po-token/cache/token-cache.ts
2804
+ var TokenCache = class {
2805
+ db;
2806
+ constructor(cachePath) {
2807
+ mkdirSync(dirname(cachePath), { recursive: true });
2808
+ this.db = new Database(cachePath);
2809
+ this.initializeSchema();
2810
+ }
2811
+ initializeSchema() {
2812
+ this.db.run(`
2813
+ CREATE TABLE IF NOT EXISTS tokens (
2814
+ id TEXT PRIMARY KEY,
2815
+ token TEXT NOT NULL,
2816
+ integrity_token TEXT,
2817
+ binding_type TEXT NOT NULL,
2818
+ binding_value TEXT NOT NULL,
2819
+ client TEXT NOT NULL,
2820
+ context TEXT NOT NULL,
2821
+ created_at INTEGER NOT NULL,
2822
+ expires_at INTEGER NOT NULL,
2823
+ refresh_at INTEGER NOT NULL
2824
+ )
2825
+ `);
2826
+ this.db.run(`
2827
+ CREATE INDEX IF NOT EXISTS idx_tokens_lookup
2828
+ ON tokens(binding_value, client, context)
2829
+ `);
2830
+ this.db.run(`
2831
+ CREATE INDEX IF NOT EXISTS idx_tokens_expires
2832
+ ON tokens(expires_at)
2833
+ `);
2834
+ this.db.run(`
2835
+ CREATE INDEX IF NOT EXISTS idx_tokens_refresh
2836
+ ON tokens(refresh_at)
2837
+ `);
2838
+ }
2839
+ get(bindingValue, client, context) {
2840
+ try {
2841
+ const row = this.db.prepare(`
2842
+ SELECT * FROM tokens
2843
+ WHERE binding_value = ? AND client = ? AND context = ?
2844
+ AND expires_at > ?
2845
+ ORDER BY created_at DESC
2846
+ LIMIT 1
2847
+ `).get(bindingValue, client, context, Date.now());
2848
+ if (!row) return ok(null);
2849
+ return ok(this.rowToTokenData(row));
2850
+ } catch (error) {
2851
+ return err(createError("CACHE_ERROR", "Failed to read token from cache", error));
2852
+ }
2853
+ }
2854
+ getByClient(client) {
2855
+ try {
2856
+ return ok(this.db.prepare(`
2857
+ SELECT * FROM tokens
2858
+ WHERE client = ? AND expires_at > ?
2859
+ ORDER BY created_at DESC
2860
+ `).all(client, Date.now()).map(this.rowToTokenData));
2861
+ } catch (error) {
2862
+ return err(createError("CACHE_ERROR", "Failed to get tokens by client", error));
2863
+ }
2864
+ }
2865
+ getExpiringSoon(marginMs) {
2866
+ try {
2867
+ const refreshThreshold = Date.now() + marginMs;
2868
+ return ok(this.db.prepare(`
2869
+ SELECT * FROM tokens
2870
+ WHERE refresh_at <= ? AND expires_at > ?
2871
+ ORDER BY refresh_at ASC
2872
+ `).all(refreshThreshold, Date.now()).map(this.rowToTokenData));
2873
+ } catch (error) {
2874
+ return err(createError("CACHE_ERROR", "Failed to get expiring tokens", error));
2875
+ }
2876
+ }
2877
+ set(data) {
2878
+ try {
2879
+ const id = `${data.bindingValue}:${data.client}:${data.context}`;
2880
+ this.db.prepare(`
2881
+ INSERT OR REPLACE INTO tokens (
2882
+ id, token, integrity_token, binding_type, binding_value,
2883
+ client, context, created_at, expires_at, refresh_at
2884
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2885
+ `).run(id, data.token, data.integrityToken ?? null, data.bindingType, data.bindingValue, data.client, data.context, data.createdAt, data.expiresAt, data.refreshAt);
2886
+ return ok(void 0);
2887
+ } catch (error) {
2888
+ return err(createError("CACHE_ERROR", "Failed to write token to cache", error));
2889
+ }
2890
+ }
2891
+ delete(bindingValue, client, context) {
2892
+ try {
2893
+ const id = `${bindingValue}:${client}:${context}`;
2894
+ this.db.prepare("DELETE FROM tokens WHERE id = ?").run(id);
2895
+ return ok(void 0);
2896
+ } catch (error) {
2897
+ return err(createError("CACHE_ERROR", "Failed to delete token", error));
2898
+ }
2899
+ }
2900
+ deleteExpired() {
2901
+ try {
2902
+ return ok(this.db.prepare("DELETE FROM tokens WHERE expires_at <= ?").run(Date.now()).changes);
2903
+ } catch (error) {
2904
+ return err(createError("CACHE_ERROR", "Failed to delete expired tokens", error));
2905
+ }
2906
+ }
2907
+ clear() {
2908
+ try {
2909
+ this.db.run("DELETE FROM tokens");
2910
+ return ok(void 0);
2911
+ } catch (error) {
2912
+ return err(createError("CACHE_ERROR", "Failed to clear token cache", error));
2913
+ }
2914
+ }
2915
+ getStats() {
2916
+ try {
2917
+ const now = Date.now();
2918
+ const total = this.db.prepare("SELECT COUNT(*) as count FROM tokens").get().count;
2919
+ const valid = this.db.prepare("SELECT COUNT(*) as count FROM tokens WHERE expires_at > ?").get(now).count;
2920
+ return ok({
2921
+ total,
2922
+ valid,
2923
+ expired: total - valid
2924
+ });
2925
+ } catch (error) {
2926
+ return err(createError("CACHE_ERROR", "Failed to get cache stats", error));
2927
+ }
2928
+ }
2929
+ close() {
2930
+ this.db.close();
2931
+ }
2932
+ rowToTokenData(row) {
2933
+ return {
2934
+ token: row.token,
2935
+ integrityToken: row.integrity_token ?? void 0,
2936
+ bindingType: row.binding_type,
2937
+ bindingValue: row.binding_value,
2938
+ client: row.client,
2939
+ context: row.context,
2940
+ createdAt: row.created_at,
2941
+ expiresAt: row.expires_at,
2942
+ refreshAt: row.refresh_at
2943
+ };
2944
+ }
2945
+ };
2946
+
2947
+ //#endregion
2948
+ //#region src/po-token/manager.ts
2949
+ var PoTokenManager = class {
2950
+ staticToken;
2951
+ policies;
2952
+ providers;
2953
+ cache = null;
2954
+ constructor(config = {}) {
2955
+ this.staticToken = config.token;
2956
+ this.policies = PO_TOKEN_POLICIES;
2957
+ if (config.providers) this.providers = [...config.providers].sort((a, b) => b.priority - a.priority);
2958
+ else this.providers = [new LocalBotGuardProvider({ timeout: config.timeout })];
2959
+ if (config.cacheEnabled !== false && config.cachePath) this.cache = new TokenCache(config.cachePath);
2960
+ }
2961
+ async getToken(identifier, client = "WEB", binding) {
2962
+ if (this.staticToken) return ok(this.staticToken);
2963
+ if (!this.isRequired(client)) return ok("");
2964
+ const context = binding ? "GVS" : "GVS";
2965
+ if (this.cache) {
2966
+ const cachedResult = this.cache.get(identifier, client, context);
2967
+ if (isOk(cachedResult) && cachedResult.value && cachedResult.value.expiresAt > Date.now()) return ok(cachedResult.value.token);
2968
+ }
2969
+ for (const provider of this.providers) {
2970
+ if (!await provider.isAvailable()) continue;
2971
+ const result = await provider.generate(identifier, binding);
2972
+ if (isOk(result)) {
2973
+ if (this.cache) this.cache.set(result.value);
2974
+ return ok(result.value.token);
2975
+ }
2976
+ }
2977
+ return ok(generateColdStartToken(identifier));
2978
+ }
2979
+ async getTokenData(identifier, client = "WEB", binding) {
2980
+ if (this.staticToken) return ok({
2981
+ token: this.staticToken,
2982
+ createdAt: Date.now(),
2983
+ expiresAt: Date.now() + 36e5,
2984
+ refreshAt: Date.now() + 3e6,
2985
+ bindingType: "session",
2986
+ bindingValue: identifier,
2987
+ client,
2988
+ context: "GVS"
2989
+ });
2990
+ if (!this.isRequired(client)) return err(createError("PO_TOKEN_EXPIRED", `PO Token not required for client ${client}`));
2991
+ const context = binding ? "GVS" : "GVS";
2992
+ if (this.cache) {
2993
+ const cachedResult = this.cache.get(identifier, client, context);
2994
+ if (isOk(cachedResult) && cachedResult.value && cachedResult.value.expiresAt > Date.now()) return ok(cachedResult.value);
2995
+ }
2996
+ for (const provider of this.providers) {
2997
+ if (!await provider.isAvailable()) continue;
2998
+ const result = await provider.generate(identifier, binding);
2999
+ if (isOk(result)) {
3000
+ if (this.cache) this.cache.set(result.value);
3001
+ return result;
3002
+ }
3003
+ }
3004
+ return err(createError("ALL_PROVIDERS_FAILED", "All PO Token providers failed"));
3005
+ }
3006
+ getColdStartToken(identifier) {
3007
+ return generateColdStartToken(identifier);
3008
+ }
3009
+ isRequired(client) {
3010
+ const policy = this.policies[client];
3011
+ if (!policy) return false;
3012
+ if (typeof policy.GVS === "string") return policy.GVS === "required";
3013
+ return policy.GVS.HTTPS === "required" || policy.GVS.DASH === "required";
3014
+ }
3015
+ needsTokenForGvs(client) {
3016
+ const policy = this.policies[client];
3017
+ if (!policy) return false;
3018
+ if (typeof policy.GVS === "string") return policy.GVS !== "not_required";
3019
+ return policy.GVS.HTTPS !== "not_required" || policy.GVS.DASH !== "not_required" || policy.GVS.HLS !== "not_required";
3020
+ }
3021
+ getPolicy(client) {
3022
+ return this.policies[client];
3023
+ }
3024
+ getVisitorData() {
3025
+ for (const provider of this.providers) if (provider instanceof LocalBotGuardProvider) return provider.getVisitorData();
3026
+ }
3027
+ shutdown() {
3028
+ for (const provider of this.providers) provider.shutdown();
3029
+ if (this.cache) this.cache.deleteExpired();
3030
+ }
3031
+ getCacheStats() {
3032
+ if (!this.cache) return null;
3033
+ const result = this.cache.getStats();
3034
+ if (isOk(result)) return result.value;
3035
+ return null;
3036
+ }
3037
+ };
3038
+
3039
+ //#endregion
3040
+ //#region src/utils/logger.ts
3041
+ logger.preset("cyberpunk");
3042
+ logger.showTimestamp();
3043
+ logger.showLocation();
3044
+ logger.addSerializer(Error, (err$1) => ({
3045
+ name: err$1.name,
3046
+ message: err$1.message,
3047
+ stack: err$1.stack?.split("\n").slice(0, 5).join("\n")
3048
+ }));
3049
+ const bundlpLogger = logger.scope("Bundlp");
3050
+ const extractorLogger = logger.component("Extractor");
3051
+ const cipherLogger = logger.component("Cipher");
3052
+ const decoderLogger = logger.component("Decoder");
3053
+ const innertubeLogger = logger.api("InnerTube");
3054
+ const httpLogger = logger.api("HTTP");
3055
+ const playerLogger = logger.scope("Bundlp:Player");
3056
+ const streamingLogger = logger.scope("Bundlp:Streaming");
3057
+ const poTokenLogger = logger.scope("Bundlp:PoToken");
3058
+ const cacheLogger = logger.scope("Bundlp:Cache");
3059
+
3060
+ //#endregion
3061
+ //#region src/index.ts
3062
+ /**
3063
+ * Appends po_token to a download URL
3064
+ */
3065
+ function appendPoToken(url, poToken) {
3066
+ if (!url || !poToken) return url;
3067
+ return `${url}${url.includes("?") ? "&" : "?"}pot=${encodeURIComponent(poToken)}`;
3068
+ }
3069
+
3070
+ //#endregion
3071
+ export { LocalBotGuardProvider, PoTokenManager, YouTubeExtractor, andThen, appendPoToken, bundlpLogger, err, extractorLogger, getDownloadHeaders, getMinimalDownloadHeaders, isErr, isOk, map, mapErr, match, ok, tryCatch, tryCatchAsync, unwrap, unwrapOr };
3072
+ //# sourceMappingURL=index.mjs.map