@hallelx/tytube 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +189 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2708 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2552 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +488 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +2523 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2523 @@
|
|
|
1
|
+
// src/exceptions.ts
|
|
2
|
+
var PytubeError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PytubeError";
|
|
6
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var MaxRetriesExceeded = class extends PytubeError {
|
|
10
|
+
constructor(message = "Maximum number of retries exceeded") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "MaxRetriesExceeded";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var HTMLParseError = class extends PytubeError {
|
|
16
|
+
constructor(message = "HTML could not be parsed") {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "HTMLParseError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var ExtractError = class extends PytubeError {
|
|
22
|
+
constructor(message = "Data extraction failed") {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "ExtractError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var RegexMatchError = class extends ExtractError {
|
|
28
|
+
caller;
|
|
29
|
+
pattern;
|
|
30
|
+
constructor(caller, pattern) {
|
|
31
|
+
super(`${caller}: could not find match for ${pattern}`);
|
|
32
|
+
this.name = "RegexMatchError";
|
|
33
|
+
this.caller = caller;
|
|
34
|
+
this.pattern = pattern;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var VideoUnavailable = class extends PytubeError {
|
|
38
|
+
videoId;
|
|
39
|
+
constructor(videoId2, message) {
|
|
40
|
+
super(message ?? `${videoId2} is unavailable`);
|
|
41
|
+
this.name = "VideoUnavailable";
|
|
42
|
+
this.videoId = videoId2;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var AgeRestrictedError = class extends VideoUnavailable {
|
|
46
|
+
constructor(videoId2) {
|
|
47
|
+
super(videoId2, `${videoId2} is age restricted, and can't be accessed without logging in.`);
|
|
48
|
+
this.name = "AgeRestrictedError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var LiveStreamError = class extends VideoUnavailable {
|
|
52
|
+
constructor(videoId2) {
|
|
53
|
+
super(videoId2, `${videoId2} is streaming live and cannot be loaded`);
|
|
54
|
+
this.name = "LiveStreamError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var VideoPrivate = class extends VideoUnavailable {
|
|
58
|
+
constructor(videoId2) {
|
|
59
|
+
super(videoId2, `${videoId2} is a private video`);
|
|
60
|
+
this.name = "VideoPrivate";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var RecordingUnavailable = class extends VideoUnavailable {
|
|
64
|
+
constructor(videoId2) {
|
|
65
|
+
super(videoId2, `${videoId2} does not have a live stream recording available`);
|
|
66
|
+
this.name = "RecordingUnavailable";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var MembersOnly = class extends VideoUnavailable {
|
|
70
|
+
constructor(videoId2) {
|
|
71
|
+
super(videoId2, `${videoId2} is a members-only video`);
|
|
72
|
+
this.name = "MembersOnly";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var VideoRegionBlocked = class extends VideoUnavailable {
|
|
76
|
+
constructor(videoId2) {
|
|
77
|
+
super(videoId2, `${videoId2} is not available in your region`);
|
|
78
|
+
this.name = "VideoRegionBlocked";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/helpers.ts
|
|
83
|
+
function regexSearch(pattern, source, group) {
|
|
84
|
+
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
85
|
+
const match = regex.exec(source);
|
|
86
|
+
if (!match) {
|
|
87
|
+
throw new RegexMatchError("regexSearch", pattern);
|
|
88
|
+
}
|
|
89
|
+
const value = match[group];
|
|
90
|
+
if (value === void 0) {
|
|
91
|
+
throw new RegexMatchError("regexSearch", pattern);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
var NTFS_FORBIDDEN_CHARS = (() => {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (let i = 0; i < 32; i++) out.push(String.fromCharCode(i));
|
|
98
|
+
return out;
|
|
99
|
+
})();
|
|
100
|
+
var FILENAME_FORBIDDEN_LITERALS = [
|
|
101
|
+
'"',
|
|
102
|
+
"#",
|
|
103
|
+
"$",
|
|
104
|
+
"%",
|
|
105
|
+
"'",
|
|
106
|
+
"*",
|
|
107
|
+
",",
|
|
108
|
+
".",
|
|
109
|
+
"/",
|
|
110
|
+
":",
|
|
111
|
+
";",
|
|
112
|
+
"<",
|
|
113
|
+
">",
|
|
114
|
+
"?",
|
|
115
|
+
"\\",
|
|
116
|
+
"^",
|
|
117
|
+
"|",
|
|
118
|
+
"~"
|
|
119
|
+
];
|
|
120
|
+
function safeFilename(s, maxLength = 255) {
|
|
121
|
+
const forbidden = /* @__PURE__ */ new Set([...NTFS_FORBIDDEN_CHARS, ...FILENAME_FORBIDDEN_LITERALS]);
|
|
122
|
+
let out = "";
|
|
123
|
+
for (const ch of s) {
|
|
124
|
+
if (!forbidden.has(ch)) out += ch;
|
|
125
|
+
}
|
|
126
|
+
return out.slice(0, maxLength);
|
|
127
|
+
}
|
|
128
|
+
function uniqueify(items) {
|
|
129
|
+
const seen = /* @__PURE__ */ new Set();
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
if (seen.has(item)) continue;
|
|
133
|
+
seen.add(item);
|
|
134
|
+
out.push(item);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/runtime/http.ts
|
|
140
|
+
var DEFAULT_HEADERS = {
|
|
141
|
+
"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",
|
|
142
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
143
|
+
};
|
|
144
|
+
function mergeHeaders(extra) {
|
|
145
|
+
return { ...DEFAULT_HEADERS, ...extra ?? {} };
|
|
146
|
+
}
|
|
147
|
+
function buildAbortSignal(opts) {
|
|
148
|
+
if (opts.signal && !opts.timeoutMs) return opts.signal;
|
|
149
|
+
if (!opts.signal && !opts.timeoutMs) return void 0;
|
|
150
|
+
const ctl = new AbortController();
|
|
151
|
+
if (opts.signal) {
|
|
152
|
+
if (opts.signal.aborted) ctl.abort(opts.signal.reason);
|
|
153
|
+
else opts.signal.addEventListener("abort", () => ctl.abort(opts.signal.reason), { once: true });
|
|
154
|
+
}
|
|
155
|
+
if (opts.timeoutMs) {
|
|
156
|
+
setTimeout(() => ctl.abort(new Error(`Request timed out after ${opts.timeoutMs}ms`)), opts.timeoutMs);
|
|
157
|
+
}
|
|
158
|
+
return ctl.signal;
|
|
159
|
+
}
|
|
160
|
+
async function doFetch(url, opts) {
|
|
161
|
+
const init = {
|
|
162
|
+
method: opts.method ?? "GET",
|
|
163
|
+
headers: mergeHeaders(opts.headers),
|
|
164
|
+
signal: buildAbortSignal(opts)
|
|
165
|
+
};
|
|
166
|
+
if (opts.body !== void 0) init.body = opts.body;
|
|
167
|
+
const res = await fetch(url, init);
|
|
168
|
+
return {
|
|
169
|
+
status: res.status,
|
|
170
|
+
headers: res.headers,
|
|
171
|
+
body: res.body,
|
|
172
|
+
text: () => res.text(),
|
|
173
|
+
arrayBuffer: () => res.arrayBuffer(),
|
|
174
|
+
json: () => res.json()
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function httpGet(url, opts = {}) {
|
|
178
|
+
return doFetch(url, { ...opts, method: "GET" });
|
|
179
|
+
}
|
|
180
|
+
async function httpPost(url, opts = {}) {
|
|
181
|
+
return doFetch(url, { ...opts, method: "POST" });
|
|
182
|
+
}
|
|
183
|
+
async function httpHead(url, opts = {}) {
|
|
184
|
+
return doFetch(url, { ...opts, method: "HEAD" });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/request.ts
|
|
188
|
+
var DEFAULT_RANGE_SIZE = 9437184;
|
|
189
|
+
async function get(url, opts = {}) {
|
|
190
|
+
const res = await httpGet(url, toFetchOpts(opts));
|
|
191
|
+
return res.text();
|
|
192
|
+
}
|
|
193
|
+
async function post(url, data, opts = {}) {
|
|
194
|
+
const headers = { ...opts.headers ?? {}, "Content-Type": "application/json" };
|
|
195
|
+
const body = data === void 0 ? "{}" : JSON.stringify(data);
|
|
196
|
+
const res = await httpPost(url, { ...toFetchOpts(opts), headers, body });
|
|
197
|
+
return res.text();
|
|
198
|
+
}
|
|
199
|
+
async function head(url, opts = {}) {
|
|
200
|
+
const res = await httpHead(url, toFetchOpts(opts));
|
|
201
|
+
const out = {};
|
|
202
|
+
res.headers.forEach((value, key) => {
|
|
203
|
+
out[key.toLowerCase()] = value;
|
|
204
|
+
});
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
async function filesize(url, opts = {}) {
|
|
208
|
+
const headers = await head(url, opts);
|
|
209
|
+
const len = headers["content-length"];
|
|
210
|
+
if (!len) throw new Error("Missing content-length header");
|
|
211
|
+
return parseInt(len, 10);
|
|
212
|
+
}
|
|
213
|
+
async function* stream(url, opts = {}) {
|
|
214
|
+
let fileSize = DEFAULT_RANGE_SIZE;
|
|
215
|
+
let downloaded = 0;
|
|
216
|
+
const maxRetries = opts.maxRetries ?? 0;
|
|
217
|
+
try {
|
|
218
|
+
const probe = await httpGet(`${url}&range=0-99999999999`, toFetchOpts(opts));
|
|
219
|
+
const cl = probe.headers.get("content-length");
|
|
220
|
+
if (cl) fileSize = parseInt(cl, 10);
|
|
221
|
+
if (probe.body) {
|
|
222
|
+
const reader = probe.body.getReader();
|
|
223
|
+
try {
|
|
224
|
+
while (true) {
|
|
225
|
+
const { done } = await reader.read();
|
|
226
|
+
if (done) break;
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
reader.releaseLock();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
while (downloaded < fileSize) {
|
|
235
|
+
const stopPos = Math.min(downloaded + DEFAULT_RANGE_SIZE, fileSize) - 1;
|
|
236
|
+
const rangeUrl = `${url}&range=${downloaded}-${stopPos}`;
|
|
237
|
+
let attempt = 0;
|
|
238
|
+
let body = null;
|
|
239
|
+
while (true) {
|
|
240
|
+
if (attempt > maxRetries) throw new MaxRetriesExceeded();
|
|
241
|
+
try {
|
|
242
|
+
const res = await httpGet(rangeUrl, {
|
|
243
|
+
...toFetchOpts(opts),
|
|
244
|
+
headers: { ...opts.headers ?? {}, Range: `bytes=${downloaded}-${stopPos}` }
|
|
245
|
+
});
|
|
246
|
+
body = res.body;
|
|
247
|
+
break;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (isAbortError(err)) throw err;
|
|
250
|
+
attempt++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!body) throw new MaxRetriesExceeded();
|
|
254
|
+
const reader = body.getReader();
|
|
255
|
+
try {
|
|
256
|
+
while (true) {
|
|
257
|
+
const { value, done } = await reader.read();
|
|
258
|
+
if (done) break;
|
|
259
|
+
if (!value) continue;
|
|
260
|
+
downloaded += value.byteLength;
|
|
261
|
+
yield value;
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
reader.releaseLock();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function* seqStream(url, opts = {}) {
|
|
269
|
+
const parsed = new URL(url);
|
|
270
|
+
parsed.searchParams.set("sq", "0");
|
|
271
|
+
const headerUrl = parsed.toString();
|
|
272
|
+
const collected = [];
|
|
273
|
+
for await (const chunk of stream(headerUrl, opts)) {
|
|
274
|
+
yield chunk;
|
|
275
|
+
collected.push(chunk);
|
|
276
|
+
}
|
|
277
|
+
const segmentData = concatAll(collected);
|
|
278
|
+
const text = new TextDecoder("utf-8").decode(segmentData);
|
|
279
|
+
const segmentCountMatch = /Segment-Count:\s*(\d+)/.exec(text);
|
|
280
|
+
const segmentCountRaw = segmentCountMatch?.[1];
|
|
281
|
+
if (!segmentCountRaw) {
|
|
282
|
+
throw new RegexMatchError("seqStream", /Segment-Count:\s*(\d+)/);
|
|
283
|
+
}
|
|
284
|
+
const segmentCount = parseInt(segmentCountRaw, 10);
|
|
285
|
+
for (let seq = 1; seq <= segmentCount; seq++) {
|
|
286
|
+
parsed.searchParams.set("sq", String(seq));
|
|
287
|
+
const segUrl = parsed.toString();
|
|
288
|
+
yield* stream(segUrl, opts);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function seqFilesize(url, opts = {}) {
|
|
292
|
+
const parsed = new URL(url);
|
|
293
|
+
parsed.searchParams.set("sq", "0");
|
|
294
|
+
const headerRes = await httpGet(parsed.toString(), toFetchOpts(opts));
|
|
295
|
+
const headerBytes = new Uint8Array(await headerRes.arrayBuffer());
|
|
296
|
+
let total = headerBytes.byteLength;
|
|
297
|
+
const text = new TextDecoder("utf-8").decode(headerBytes);
|
|
298
|
+
const match = /Segment-Count:\s*(\d+)/.exec(text);
|
|
299
|
+
const matchRaw = match?.[1];
|
|
300
|
+
if (!matchRaw) {
|
|
301
|
+
throw new RegexMatchError("seqFilesize", /Segment-Count:\s*(\d+)/);
|
|
302
|
+
}
|
|
303
|
+
const segmentCount = parseInt(matchRaw, 10);
|
|
304
|
+
for (let seq = 1; seq <= segmentCount; seq++) {
|
|
305
|
+
parsed.searchParams.set("sq", String(seq));
|
|
306
|
+
total += await filesize(parsed.toString(), opts);
|
|
307
|
+
}
|
|
308
|
+
return total;
|
|
309
|
+
}
|
|
310
|
+
function toFetchOpts(opts) {
|
|
311
|
+
return {
|
|
312
|
+
headers: opts.headers,
|
|
313
|
+
timeoutMs: opts.timeoutMs,
|
|
314
|
+
signal: opts.signal
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function isAbortError(err) {
|
|
318
|
+
return err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"));
|
|
319
|
+
}
|
|
320
|
+
function concatAll(chunks) {
|
|
321
|
+
let total = 0;
|
|
322
|
+
for (const c of chunks) total += c.byteLength;
|
|
323
|
+
const out = new Uint8Array(total);
|
|
324
|
+
let offset = 0;
|
|
325
|
+
for (const c of chunks) {
|
|
326
|
+
out.set(c, offset);
|
|
327
|
+
offset += c.byteLength;
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/runtime/fs.ts
|
|
333
|
+
var fsPromise = null;
|
|
334
|
+
async function getFs() {
|
|
335
|
+
if (!fsPromise) {
|
|
336
|
+
fsPromise = (async () => {
|
|
337
|
+
try {
|
|
338
|
+
return await import('fs/promises');
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
}
|
|
344
|
+
return fsPromise;
|
|
345
|
+
}
|
|
346
|
+
async function ensureDir(dirPath) {
|
|
347
|
+
const fs = await getFs();
|
|
348
|
+
if (!fs) throw new Error("Filesystem unavailable in this runtime");
|
|
349
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
350
|
+
}
|
|
351
|
+
async function fileExists(filePath) {
|
|
352
|
+
const fs = await getFs();
|
|
353
|
+
if (!fs) return false;
|
|
354
|
+
try {
|
|
355
|
+
await fs.access(filePath);
|
|
356
|
+
return true;
|
|
357
|
+
} catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function writeText(filePath, text) {
|
|
362
|
+
const fs = await getFs();
|
|
363
|
+
if (!fs) throw new Error("Filesystem unavailable in this runtime");
|
|
364
|
+
await fs.writeFile(filePath, text, "utf-8");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/runtime/path.ts
|
|
368
|
+
function joinPath(...segments) {
|
|
369
|
+
const parts = [];
|
|
370
|
+
for (const seg of segments) {
|
|
371
|
+
if (!seg) continue;
|
|
372
|
+
parts.push(seg.replace(/^[/\\]+|[/\\]+$/g, ""));
|
|
373
|
+
}
|
|
374
|
+
const joined = parts.filter((p) => p.length > 0).join("/");
|
|
375
|
+
if (segments[0]?.startsWith("/") || segments[0]?.startsWith("\\")) {
|
|
376
|
+
return "/" + joined;
|
|
377
|
+
}
|
|
378
|
+
return joined;
|
|
379
|
+
}
|
|
380
|
+
function isAbsolute(p) {
|
|
381
|
+
if (!p) return false;
|
|
382
|
+
if (p.startsWith("/")) return true;
|
|
383
|
+
if (/^[a-zA-Z]:[\\/]/.test(p)) return true;
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
async function resolveOutputDir(outputPath) {
|
|
387
|
+
const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : ".";
|
|
388
|
+
if (!outputPath) return cwd;
|
|
389
|
+
if (isAbsolute(outputPath)) return outputPath;
|
|
390
|
+
return joinPath(cwd, outputPath);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/captions.ts
|
|
394
|
+
var Caption = class _Caption {
|
|
395
|
+
url;
|
|
396
|
+
name;
|
|
397
|
+
code;
|
|
398
|
+
constructor(track) {
|
|
399
|
+
this.url = track.baseUrl;
|
|
400
|
+
if (track.name?.simpleText) {
|
|
401
|
+
this.name = track.name.simpleText;
|
|
402
|
+
} else if (track.name?.runs) {
|
|
403
|
+
this.name = track.name.runs.find((r) => r.text)?.text ?? "";
|
|
404
|
+
} else {
|
|
405
|
+
this.name = "";
|
|
406
|
+
}
|
|
407
|
+
this.code = (track.vssId ?? track.languageCode ?? "").replace(/^\.+/, "");
|
|
408
|
+
}
|
|
409
|
+
async xmlCaptions() {
|
|
410
|
+
return get(this.url);
|
|
411
|
+
}
|
|
412
|
+
async jsonCaptions() {
|
|
413
|
+
const url = this.url.replace("fmt=srv3", "fmt=json3");
|
|
414
|
+
const text = await get(url);
|
|
415
|
+
return JSON.parse(text);
|
|
416
|
+
}
|
|
417
|
+
async generateSrtCaptions() {
|
|
418
|
+
return _Caption.xmlCaptionToSrt(await this.xmlCaptions());
|
|
419
|
+
}
|
|
420
|
+
static floatToSrtTimeFormat(d) {
|
|
421
|
+
const whole = Math.floor(d);
|
|
422
|
+
const fraction = d - whole;
|
|
423
|
+
const h = Math.floor(whole / 3600);
|
|
424
|
+
const m = Math.floor(whole % 3600 / 60);
|
|
425
|
+
const s = whole % 60;
|
|
426
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
427
|
+
const ms = String(Math.round(fraction * 1e3)).padStart(3, "0");
|
|
428
|
+
return `${pad(h)}:${pad(m)}:${pad(s)},${ms}`;
|
|
429
|
+
}
|
|
430
|
+
static xmlCaptionToSrt(xml) {
|
|
431
|
+
const segments = [];
|
|
432
|
+
let seq = 1;
|
|
433
|
+
const re = /<text\s+([^>]*?)>([\s\S]*?)<\/text>/g;
|
|
434
|
+
let match;
|
|
435
|
+
while ((match = re.exec(xml)) !== null) {
|
|
436
|
+
const attrs = match[1] ?? "";
|
|
437
|
+
const rawText = match[2] ?? "";
|
|
438
|
+
const startMatch = /\bstart\s*=\s*"([^"]+)"/.exec(attrs);
|
|
439
|
+
const durMatch = /\bdur\s*=\s*"([^"]+)"/.exec(attrs);
|
|
440
|
+
if (!startMatch) continue;
|
|
441
|
+
const start = parseFloat(startMatch[1]);
|
|
442
|
+
const dur = durMatch ? parseFloat(durMatch[1]) : 0;
|
|
443
|
+
const end = start + dur;
|
|
444
|
+
const text = decodeXmlEntities(rawText.replace(/\n/g, " ").replace(/ +/g, " "));
|
|
445
|
+
segments.push(
|
|
446
|
+
`${seq}
|
|
447
|
+
${_Caption.floatToSrtTimeFormat(start)} --> ${_Caption.floatToSrtTimeFormat(end)}
|
|
448
|
+
${text}
|
|
449
|
+
`
|
|
450
|
+
);
|
|
451
|
+
seq++;
|
|
452
|
+
}
|
|
453
|
+
return segments.join("\n").trim();
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Write the caption track to disk as SRT or XML.
|
|
457
|
+
* Returns the absolute path of the written file.
|
|
458
|
+
*/
|
|
459
|
+
async download(title, opts = {}) {
|
|
460
|
+
const srt = opts.srt ?? true;
|
|
461
|
+
let stem = title;
|
|
462
|
+
if (stem.endsWith(".srt") || stem.endsWith(".xml")) {
|
|
463
|
+
stem = stem.split(".").slice(0, -1).join(".");
|
|
464
|
+
}
|
|
465
|
+
if (opts.filenamePrefix) stem = `${safeFilename(opts.filenamePrefix)}${stem}`;
|
|
466
|
+
stem = safeFilename(stem);
|
|
467
|
+
stem += ` (${this.code})`;
|
|
468
|
+
stem += srt ? ".srt" : ".xml";
|
|
469
|
+
const dir = await resolveOutputDir(opts.outputPath);
|
|
470
|
+
await ensureDir(dir);
|
|
471
|
+
const filePath = joinPath(dir, stem);
|
|
472
|
+
const contents = srt ? await this.generateSrtCaptions() : await this.xmlCaptions();
|
|
473
|
+
await writeText(filePath, contents);
|
|
474
|
+
return filePath;
|
|
475
|
+
}
|
|
476
|
+
toString() {
|
|
477
|
+
return `<Caption lang="${this.name}" code="${this.code}">`;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
function decodeXmlEntities(input) {
|
|
481
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10))).replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/parser.ts
|
|
485
|
+
var CONTEXT_CLOSERS = {
|
|
486
|
+
"{": "}",
|
|
487
|
+
"[": "]",
|
|
488
|
+
'"': '"',
|
|
489
|
+
"/": "/"
|
|
490
|
+
// javascript regex
|
|
491
|
+
};
|
|
492
|
+
var REGEX_PRECEDING_CHARS = /* @__PURE__ */ new Set([
|
|
493
|
+
"(",
|
|
494
|
+
",",
|
|
495
|
+
"=",
|
|
496
|
+
":",
|
|
497
|
+
"[",
|
|
498
|
+
"!",
|
|
499
|
+
"&",
|
|
500
|
+
"|",
|
|
501
|
+
"?",
|
|
502
|
+
"{",
|
|
503
|
+
"}",
|
|
504
|
+
";"
|
|
505
|
+
]);
|
|
506
|
+
function findObjectFromStartpoint(source, startPoint) {
|
|
507
|
+
const html = source.slice(startPoint);
|
|
508
|
+
const first = html[0];
|
|
509
|
+
if (first !== "{" && first !== "[") {
|
|
510
|
+
throw new HTMLParseError(`Invalid start point. Start of HTML:
|
|
511
|
+
${html.slice(0, 20)}`);
|
|
512
|
+
}
|
|
513
|
+
const stack = [first];
|
|
514
|
+
let lastChar = "{";
|
|
515
|
+
let currChar = null;
|
|
516
|
+
let i = 1;
|
|
517
|
+
while (i < html.length) {
|
|
518
|
+
if (stack.length === 0) break;
|
|
519
|
+
if (currChar !== null && currChar !== " " && currChar !== "\n") {
|
|
520
|
+
lastChar = currChar;
|
|
521
|
+
}
|
|
522
|
+
currChar = html[i] ?? null;
|
|
523
|
+
if (currChar === null) break;
|
|
524
|
+
const currContext = stack[stack.length - 1];
|
|
525
|
+
if (currChar === CONTEXT_CLOSERS[currContext]) {
|
|
526
|
+
stack.pop();
|
|
527
|
+
i += 1;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (currContext === '"' || currContext === "/") {
|
|
531
|
+
if (currChar === "\\") {
|
|
532
|
+
i += 2;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
if (currChar in CONTEXT_CLOSERS) {
|
|
537
|
+
const isRegexLike = currChar === "/" && (lastChar === null || !REGEX_PRECEDING_CHARS.has(lastChar));
|
|
538
|
+
if (!isRegexLike) {
|
|
539
|
+
stack.push(currChar);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
i += 1;
|
|
544
|
+
}
|
|
545
|
+
return html.slice(0, i);
|
|
546
|
+
}
|
|
547
|
+
function parseForObjectString(html, precedingRegex) {
|
|
548
|
+
const match = precedingRegex.exec(html);
|
|
549
|
+
if (!match) throw new HTMLParseError(`No matches for regex ${precedingRegex}`);
|
|
550
|
+
const startIndex = match.index + match[0].length;
|
|
551
|
+
return findObjectFromStartpoint(html, startIndex);
|
|
552
|
+
}
|
|
553
|
+
function parseForObject(html, precedingRegex) {
|
|
554
|
+
const objectString = parseForObjectString(html, precedingRegex);
|
|
555
|
+
try {
|
|
556
|
+
return JSON.parse(objectString);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
throw new HTMLParseError(`Could not parse object: ${err.message}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function parseForAllObjects(html, precedingRegex) {
|
|
562
|
+
const flags = precedingRegex.flags.includes("g") ? precedingRegex.flags : `${precedingRegex.flags}g`;
|
|
563
|
+
const re = new RegExp(precedingRegex.source, flags);
|
|
564
|
+
const out = [];
|
|
565
|
+
let match;
|
|
566
|
+
while ((match = re.exec(html)) !== null) {
|
|
567
|
+
const startIndex = match.index + match[0].length;
|
|
568
|
+
try {
|
|
569
|
+
const objStr = findObjectFromStartpoint(html, startIndex);
|
|
570
|
+
out.push(JSON.parse(objStr));
|
|
571
|
+
} catch {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (re.lastIndex === match.index) re.lastIndex++;
|
|
575
|
+
}
|
|
576
|
+
if (out.length === 0) {
|
|
577
|
+
throw new HTMLParseError(`No matches for regex ${precedingRegex}`);
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/cipher.ts
|
|
583
|
+
function escapeRegex(input) {
|
|
584
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
585
|
+
}
|
|
586
|
+
var SIGNATURE_FUNCTION_PATTERNS = [
|
|
587
|
+
// Most reliable: the function literally splits its arg into a char array.
|
|
588
|
+
// This is the core fingerprint of every YouTube cipher function.
|
|
589
|
+
/(?:\b|[^a-zA-Z0-9$])([a-zA-Z0-9$_]{1,3})\s*=\s*function\(\s*a\s*\)\s*\{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
|
|
590
|
+
/(?:\b|[^a-zA-Z0-9$])([a-zA-Z0-9$_]{1,4})\s*=\s*function\(\s*a\s*\)\s*\{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
|
|
591
|
+
/([a-zA-Z0-9$_]+)\s*=\s*function\(\s*a\s*\)\s*\{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
|
|
592
|
+
// Function-statement form: function NAME(a){a=a.split("")...}
|
|
593
|
+
/function\s+([a-zA-Z0-9$_]+)\s*\(\s*a\s*\)\s*\{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
|
|
594
|
+
// Older / call-site forms (kept as fallbacks).
|
|
595
|
+
/(["'])signature\1\s*,\s*([a-zA-Z0-9$_]+)\(/,
|
|
596
|
+
/\.sig\|\|([a-zA-Z0-9$_]+)\(/,
|
|
597
|
+
/\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*([a-zA-Z0-9$_]+)\(/,
|
|
598
|
+
/\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*([a-zA-Z0-9$_]+)\(/,
|
|
599
|
+
/yt\.akamaized\.net\/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*([a-zA-Z0-9$_]+)\(/,
|
|
600
|
+
/\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*([a-zA-Z0-9$_]+)\(/,
|
|
601
|
+
/\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*([a-zA-Z0-9$_]+)\(/,
|
|
602
|
+
/\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*([a-zA-Z0-9$_]+)\(/,
|
|
603
|
+
/\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*([a-zA-Z0-9$_]+)\(/
|
|
604
|
+
];
|
|
605
|
+
var FORBIDDEN_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
606
|
+
"decodeURIComponent",
|
|
607
|
+
"encodeURIComponent",
|
|
608
|
+
"JSON",
|
|
609
|
+
"Math",
|
|
610
|
+
"Object",
|
|
611
|
+
"String",
|
|
612
|
+
"Number",
|
|
613
|
+
"Array",
|
|
614
|
+
"parseInt",
|
|
615
|
+
"parseFloat",
|
|
616
|
+
"function",
|
|
617
|
+
"return",
|
|
618
|
+
"typeof",
|
|
619
|
+
"undefined",
|
|
620
|
+
"null",
|
|
621
|
+
"true",
|
|
622
|
+
"false"
|
|
623
|
+
]);
|
|
624
|
+
function isPlausibleFunctionName(name) {
|
|
625
|
+
if (!name) return false;
|
|
626
|
+
if (!/^[a-zA-Z0-9$_]+$/.test(name)) return false;
|
|
627
|
+
if (FORBIDDEN_FUNCTION_NAMES.has(name)) return false;
|
|
628
|
+
if (name.length > 8) return false;
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
function getInitialFunctionName(js) {
|
|
632
|
+
for (const pattern of SIGNATURE_FUNCTION_PATTERNS) {
|
|
633
|
+
const globalPattern = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
|
|
634
|
+
let m;
|
|
635
|
+
while ((m = globalPattern.exec(js)) !== null) {
|
|
636
|
+
for (let i = 1; i < m.length; i++) {
|
|
637
|
+
const candidate = m[i];
|
|
638
|
+
if (candidate && isPlausibleFunctionName(candidate)) return candidate;
|
|
639
|
+
}
|
|
640
|
+
if (globalPattern.lastIndex === m.index) globalPattern.lastIndex++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
throw new RegexMatchError("getInitialFunctionName", "multiple");
|
|
644
|
+
}
|
|
645
|
+
function getThrottlingFunctionName(js) {
|
|
646
|
+
const pattern = /a\.[a-zA-Z]\s*&&\s*\([a-z]\s*=\s*a\.get\("n"\)\)\s*&&\s*\([a-z]\s*=\s*([a-zA-Z0-9$]+)(\[\d+\])?\([a-z]\)/;
|
|
647
|
+
const m = pattern.exec(js);
|
|
648
|
+
if (!m) throw new RegexMatchError("getThrottlingFunctionName", pattern);
|
|
649
|
+
const fnName = m[1];
|
|
650
|
+
const idxRaw = m[2];
|
|
651
|
+
if (!idxRaw) return fnName;
|
|
652
|
+
const idx = parseInt(idxRaw.replace(/\[|\]/g, ""), 10);
|
|
653
|
+
const arrRegex = new RegExp(`var\\s+${escapeRegex(fnName)}\\s*=\\s*\\[(.+?)\\];`);
|
|
654
|
+
const arrMatch = arrRegex.exec(js);
|
|
655
|
+
if (!arrMatch) return fnName;
|
|
656
|
+
const elements = arrMatch[1].split(",").map((s) => s.trim());
|
|
657
|
+
return elements[idx] ?? fnName;
|
|
658
|
+
}
|
|
659
|
+
function extractHelperObjectVarName(js, sigFuncName) {
|
|
660
|
+
const fnBody = extractSignatureFunctionBody(js, sigFuncName);
|
|
661
|
+
const m = /(?:^|;|\{)\s*([a-zA-Z0-9$_]+)\s*\.\s*[a-zA-Z0-9$_]+\s*\(/.exec(fnBody);
|
|
662
|
+
if (!m) throw new RegexMatchError("extractHelperObjectVarName", /(\w+)\./);
|
|
663
|
+
return m[1];
|
|
664
|
+
}
|
|
665
|
+
function extractSignatureFunctionBody(js, sigFuncName) {
|
|
666
|
+
const headerRegex = new RegExp(
|
|
667
|
+
`(?:var\\s+)?${escapeRegex(sigFuncName)}\\s*=\\s*function\\s*\\(\\s*([a-zA-Z0-9_$]+)\\s*\\)\\s*`
|
|
668
|
+
);
|
|
669
|
+
const m = headerRegex.exec(js);
|
|
670
|
+
if (!m) {
|
|
671
|
+
const altRegex = new RegExp(
|
|
672
|
+
`function\\s+${escapeRegex(sigFuncName)}\\s*\\(\\s*([a-zA-Z0-9_$]+)\\s*\\)\\s*`
|
|
673
|
+
);
|
|
674
|
+
const m2 = altRegex.exec(js);
|
|
675
|
+
if (!m2) throw new RegexMatchError("extractSignatureFunctionBody", headerRegex);
|
|
676
|
+
return findObjectFromStartpoint(js, m2.index + m2[0].length);
|
|
677
|
+
}
|
|
678
|
+
return findObjectFromStartpoint(js, m.index + m[0].length);
|
|
679
|
+
}
|
|
680
|
+
function extractSignatureFunctionFullSource(js, sigFuncName) {
|
|
681
|
+
const headerRegex = new RegExp(
|
|
682
|
+
`(?:var\\s+)?${escapeRegex(sigFuncName)}\\s*=\\s*function\\s*\\(\\s*([a-zA-Z0-9_$]+)\\s*\\)\\s*`
|
|
683
|
+
);
|
|
684
|
+
let m = headerRegex.exec(js);
|
|
685
|
+
let argName;
|
|
686
|
+
let bodyStart;
|
|
687
|
+
if (m) {
|
|
688
|
+
argName = m[1];
|
|
689
|
+
bodyStart = m.index + m[0].length;
|
|
690
|
+
} else {
|
|
691
|
+
const altRegex = new RegExp(
|
|
692
|
+
`function\\s+${escapeRegex(sigFuncName)}\\s*\\(\\s*([a-zA-Z0-9_$]+)\\s*\\)\\s*`
|
|
693
|
+
);
|
|
694
|
+
const m2 = altRegex.exec(js);
|
|
695
|
+
if (!m2) throw new RegexMatchError("extractSignatureFunctionFullSource", headerRegex);
|
|
696
|
+
argName = m2[1];
|
|
697
|
+
bodyStart = m2.index + m2[0].length;
|
|
698
|
+
}
|
|
699
|
+
const body = findObjectFromStartpoint(js, bodyStart);
|
|
700
|
+
return { argName, body };
|
|
701
|
+
}
|
|
702
|
+
function extractHelperObjectSource(js, varName) {
|
|
703
|
+
const re = new RegExp(`var\\s+${escapeRegex(varName)}\\s*=\\s*`);
|
|
704
|
+
const m = re.exec(js);
|
|
705
|
+
if (!m) throw new RegexMatchError("extractHelperObjectSource", re);
|
|
706
|
+
return findObjectFromStartpoint(js, m.index + m[0].length);
|
|
707
|
+
}
|
|
708
|
+
function extractThrottlingFunctionSource(js, fnName) {
|
|
709
|
+
const headerRegex = new RegExp(
|
|
710
|
+
`(?:var\\s+)?${escapeRegex(fnName)}\\s*=\\s*function\\s*\\(\\s*([a-zA-Z0-9_$]+)\\s*\\)\\s*`
|
|
711
|
+
);
|
|
712
|
+
const m = headerRegex.exec(js);
|
|
713
|
+
if (!m) {
|
|
714
|
+
const altRegex = new RegExp(
|
|
715
|
+
`function\\s+${escapeRegex(fnName)}\\s*\\(\\s*([a-zA-Z0-9_$]+)\\s*\\)\\s*`
|
|
716
|
+
);
|
|
717
|
+
const m2 = altRegex.exec(js);
|
|
718
|
+
if (!m2) throw new RegexMatchError("extractThrottlingFunctionSource", headerRegex);
|
|
719
|
+
return { argName: m2[1], body: findObjectFromStartpoint(js, m2.index + m2[0].length) };
|
|
720
|
+
}
|
|
721
|
+
return { argName: m[1], body: findObjectFromStartpoint(js, m.index + m[0].length) };
|
|
722
|
+
}
|
|
723
|
+
var JsExecCipher = class {
|
|
724
|
+
sigFn;
|
|
725
|
+
nFn;
|
|
726
|
+
cachedN = null;
|
|
727
|
+
constructor(js) {
|
|
728
|
+
const sigName = getInitialFunctionName(js);
|
|
729
|
+
const helperVar = extractHelperObjectVarName(js, sigName);
|
|
730
|
+
const helperSrc = extractHelperObjectSource(js, helperVar);
|
|
731
|
+
const { argName: sigArg, body: sigBody } = extractSignatureFunctionFullSource(js, sigName);
|
|
732
|
+
try {
|
|
733
|
+
this.sigFn = new Function(
|
|
734
|
+
"input",
|
|
735
|
+
`var ${helperVar} = ${helperSrc};
|
|
736
|
+
var ${sigName} = function(${sigArg}) ${sigBody};
|
|
737
|
+
return ${sigName}(input);`
|
|
738
|
+
);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
throw new ExtractError(`Failed to compile signature function: ${err.message}`);
|
|
741
|
+
}
|
|
742
|
+
const nName = getThrottlingFunctionName(js);
|
|
743
|
+
const { argName: nArg, body: nBody } = extractThrottlingFunctionSource(js, nName);
|
|
744
|
+
try {
|
|
745
|
+
this.nFn = new Function(
|
|
746
|
+
"input",
|
|
747
|
+
`var ${nName} = function(${nArg}) ${nBody};
|
|
748
|
+
return ${nName}(input);`
|
|
749
|
+
);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
throw new ExtractError(`Failed to compile throttling function: ${err.message}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
getSignature(cipheredSignature) {
|
|
755
|
+
return this.sigFn(cipheredSignature);
|
|
756
|
+
}
|
|
757
|
+
calculateN(initialN) {
|
|
758
|
+
if (this.cachedN && this.cachedN.input === initialN) return this.cachedN.output;
|
|
759
|
+
const out = this.nFn(initialN);
|
|
760
|
+
this.cachedN = { input: initialN, output: out };
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
var canEval = (() => {
|
|
765
|
+
try {
|
|
766
|
+
new Function("return 1")();
|
|
767
|
+
return true;
|
|
768
|
+
} catch {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
})();
|
|
772
|
+
function createCipher(js) {
|
|
773
|
+
if (canEval) return new JsExecCipher(js);
|
|
774
|
+
throw new ExtractError(
|
|
775
|
+
"eval is not available in this runtime; use the regex-port fallback (cipher-fallback.ts)."
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/metadata.ts
|
|
780
|
+
var YouTubeMetadata = class {
|
|
781
|
+
raw;
|
|
782
|
+
groups;
|
|
783
|
+
constructor(rawRows) {
|
|
784
|
+
this.raw = rawRows;
|
|
785
|
+
const groups = [{}];
|
|
786
|
+
for (const el of rawRows) {
|
|
787
|
+
const title = el.title?.simpleText;
|
|
788
|
+
if (!title) continue;
|
|
789
|
+
const contents = el.contents?.[0];
|
|
790
|
+
if (!contents) continue;
|
|
791
|
+
const value = contents.simpleText ?? contents.runs?.[0]?.text;
|
|
792
|
+
if (value === void 0) continue;
|
|
793
|
+
groups[groups.length - 1][title] = value;
|
|
794
|
+
if (el.hasDividerLine) groups.push({});
|
|
795
|
+
}
|
|
796
|
+
if (groups.length > 0 && Object.keys(groups[groups.length - 1]).length === 0) {
|
|
797
|
+
groups.pop();
|
|
798
|
+
}
|
|
799
|
+
this.groups = groups;
|
|
800
|
+
}
|
|
801
|
+
get rawMetadata() {
|
|
802
|
+
return this.raw;
|
|
803
|
+
}
|
|
804
|
+
get metadata() {
|
|
805
|
+
return this.groups;
|
|
806
|
+
}
|
|
807
|
+
at(index) {
|
|
808
|
+
return this.groups[index];
|
|
809
|
+
}
|
|
810
|
+
[Symbol.iterator]() {
|
|
811
|
+
return this.groups[Symbol.iterator]();
|
|
812
|
+
}
|
|
813
|
+
toJSON() {
|
|
814
|
+
return this.groups;
|
|
815
|
+
}
|
|
816
|
+
toString() {
|
|
817
|
+
return JSON.stringify(this.groups);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/extract.ts
|
|
822
|
+
function videoId(url) {
|
|
823
|
+
return regexSearch(/(?:v=|\/)([0-9A-Za-z_-]{11}).*/, url, 1);
|
|
824
|
+
}
|
|
825
|
+
function playlistId(url) {
|
|
826
|
+
const u = new URL(url);
|
|
827
|
+
const list = u.searchParams.get("list");
|
|
828
|
+
if (!list) throw new RegexMatchError("playlistId", "list query param");
|
|
829
|
+
return list;
|
|
830
|
+
}
|
|
831
|
+
function channelName(url) {
|
|
832
|
+
const patterns = [
|
|
833
|
+
/(?:\/(c)\/([%\d\w_\-]+)(\/.*)?)/,
|
|
834
|
+
/(?:\/(channel)\/([%\w\d_\-]+)(\/.*)?)/,
|
|
835
|
+
/(?:\/(u)\/([%\d\w_\-]+)(\/.*)?)/,
|
|
836
|
+
/(?:\/(user)\/([%\w\d_\-]+)(\/.*)?)/
|
|
837
|
+
];
|
|
838
|
+
for (const pattern of patterns) {
|
|
839
|
+
const m = pattern.exec(url);
|
|
840
|
+
if (m) {
|
|
841
|
+
return `/${m[1]}/${m[2]}`;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
throw new RegexMatchError("channelName", "patterns");
|
|
845
|
+
}
|
|
846
|
+
function publishDate(watchHtml) {
|
|
847
|
+
try {
|
|
848
|
+
const result = regexSearch(
|
|
849
|
+
/(?<=itemprop="datePublished" content=")\d{4}-\d{2}-\d{2}/,
|
|
850
|
+
watchHtml,
|
|
851
|
+
0
|
|
852
|
+
);
|
|
853
|
+
return /* @__PURE__ */ new Date(`${result}T00:00:00Z`);
|
|
854
|
+
} catch {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function isAgeRestricted(watchHtml) {
|
|
859
|
+
return /og:restrictions:age/.test(watchHtml);
|
|
860
|
+
}
|
|
861
|
+
function playabilityStatus(watchHtml) {
|
|
862
|
+
const playerResponse = initialPlayerResponse(watchHtml);
|
|
863
|
+
const statusDict = playerResponse["playabilityStatus"] ?? {};
|
|
864
|
+
if ("liveStreamability" in statusDict) {
|
|
865
|
+
return { status: "LIVE_STREAM", reasons: ["Video is a live stream."] };
|
|
866
|
+
}
|
|
867
|
+
if ("status" in statusDict) {
|
|
868
|
+
if ("reason" in statusDict) {
|
|
869
|
+
return { status: String(statusDict["status"]), reasons: [String(statusDict["reason"])] };
|
|
870
|
+
}
|
|
871
|
+
if ("messages" in statusDict) {
|
|
872
|
+
return {
|
|
873
|
+
status: String(statusDict["status"]),
|
|
874
|
+
reasons: statusDict["messages"].map((m) => m === null ? null : String(m))
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return { status: null, reasons: [null] };
|
|
879
|
+
}
|
|
880
|
+
function jsUrl(html) {
|
|
881
|
+
let baseJs;
|
|
882
|
+
try {
|
|
883
|
+
const config = getYtplayerConfig(html);
|
|
884
|
+
baseJs = config.assets?.js;
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
if (!baseJs) {
|
|
888
|
+
baseJs = getYtplayerJs(html);
|
|
889
|
+
}
|
|
890
|
+
return `https://youtube.com${baseJs}`;
|
|
891
|
+
}
|
|
892
|
+
function getYtplayerJs(html) {
|
|
893
|
+
const patterns = [/(\/s\/player\/[\w\d]+\/[\w\d_/.]+\/base\.js)/];
|
|
894
|
+
for (const pattern of patterns) {
|
|
895
|
+
const m = pattern.exec(html);
|
|
896
|
+
if (m) return m[1];
|
|
897
|
+
}
|
|
898
|
+
throw new RegexMatchError("getYtplayerJs", "jsUrlPatterns");
|
|
899
|
+
}
|
|
900
|
+
function getYtplayerConfig(html) {
|
|
901
|
+
const configPatterns = [
|
|
902
|
+
/ytplayer\.config\s*=\s*/,
|
|
903
|
+
/ytInitialPlayerResponse\s*=\s*/
|
|
904
|
+
];
|
|
905
|
+
for (const pattern of configPatterns) {
|
|
906
|
+
try {
|
|
907
|
+
return parseForObject(html, pattern);
|
|
908
|
+
} catch {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const setConfigPatterns = [/yt\.setConfig\(.*['"]PLAYER_CONFIG['"]:\s*/];
|
|
913
|
+
for (const pattern of setConfigPatterns) {
|
|
914
|
+
try {
|
|
915
|
+
return parseForObject(html, pattern);
|
|
916
|
+
} catch {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
throw new RegexMatchError("getYtplayerConfig", "config_patterns, setconfig_patterns");
|
|
921
|
+
}
|
|
922
|
+
function getYtcfg(html) {
|
|
923
|
+
const merged = {};
|
|
924
|
+
const ytcfgPatterns = [/ytcfg\s=\s/, /ytcfg\.set\(/];
|
|
925
|
+
for (const pattern of ytcfgPatterns) {
|
|
926
|
+
try {
|
|
927
|
+
const found = parseForAllObjects(html, pattern);
|
|
928
|
+
for (const obj of found) Object.assign(merged, obj);
|
|
929
|
+
} catch {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (Object.keys(merged).length > 0) return merged;
|
|
934
|
+
throw new RegexMatchError("getYtcfg", "ytcfgPatterns");
|
|
935
|
+
}
|
|
936
|
+
function initialData(watchHtml) {
|
|
937
|
+
const patterns = [
|
|
938
|
+
/window\[['"]ytInitialData['"]\]\s*=\s*/,
|
|
939
|
+
/ytInitialData\s*=\s*/
|
|
940
|
+
];
|
|
941
|
+
for (const pattern of patterns) {
|
|
942
|
+
try {
|
|
943
|
+
return parseForObject(watchHtml, pattern);
|
|
944
|
+
} catch {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
throw new RegexMatchError("initialData", "initialDataPattern");
|
|
949
|
+
}
|
|
950
|
+
function initialPlayerResponse(watchHtml) {
|
|
951
|
+
const patterns = [
|
|
952
|
+
/window\[['"]ytInitialPlayerResponse['"]\]\s*=\s*/,
|
|
953
|
+
/ytInitialPlayerResponse\s*=\s*/
|
|
954
|
+
];
|
|
955
|
+
for (const pattern of patterns) {
|
|
956
|
+
try {
|
|
957
|
+
return parseForObject(watchHtml, pattern);
|
|
958
|
+
} catch {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
throw new RegexMatchError("initialPlayerResponse", "initialPlayerResponsePattern");
|
|
963
|
+
}
|
|
964
|
+
function mimeTypeCodec(input) {
|
|
965
|
+
const pattern = /(\w+\/\w+);\s*codecs="([a-zA-Z\-0-9.,\s]*)"/;
|
|
966
|
+
const m = pattern.exec(input);
|
|
967
|
+
if (!m) throw new RegexMatchError("mimeTypeCodec", pattern);
|
|
968
|
+
const codecs = m[2].split(",").map((c) => c.trim());
|
|
969
|
+
return { mimeType: m[1], codecs };
|
|
970
|
+
}
|
|
971
|
+
function applyDescrambler(streamData) {
|
|
972
|
+
if ("url" in streamData && streamData.url !== void 0) return null;
|
|
973
|
+
const formats = [];
|
|
974
|
+
if (streamData.formats) formats.push(...streamData.formats);
|
|
975
|
+
if (streamData.adaptiveFormats) formats.push(...streamData.adaptiveFormats);
|
|
976
|
+
for (const data of formats) {
|
|
977
|
+
if (!data.url) {
|
|
978
|
+
const cipher = data.signatureCipher ?? data.cipher;
|
|
979
|
+
if (cipher) {
|
|
980
|
+
const params = new URLSearchParams(cipher);
|
|
981
|
+
const u = params.get("url");
|
|
982
|
+
const s = params.get("s");
|
|
983
|
+
if (u) data.url = u;
|
|
984
|
+
if (s !== null) data.s = s;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
data.is_otf = data.type === "FORMAT_STREAM_TYPE_OTF";
|
|
988
|
+
}
|
|
989
|
+
return formats;
|
|
990
|
+
}
|
|
991
|
+
function manifestNeedsCipher(streamManifest) {
|
|
992
|
+
return streamManifest.some((s) => s.s !== void 0 && s.s !== null);
|
|
993
|
+
}
|
|
994
|
+
function needsThrottlingTransform(url) {
|
|
995
|
+
try {
|
|
996
|
+
const u = new URL(url);
|
|
997
|
+
return u.searchParams.has("n") && !u.searchParams.has("ratebypass");
|
|
998
|
+
} catch {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function applySignature(streamManifest, vidInfo, cipher) {
|
|
1003
|
+
for (let i = 0; i < streamManifest.length; i++) {
|
|
1004
|
+
const stream2 = streamManifest[i];
|
|
1005
|
+
if (!stream2) continue;
|
|
1006
|
+
const url = stream2.url;
|
|
1007
|
+
if (!url) {
|
|
1008
|
+
const liveStream = vidInfo["playabilityStatus"]?.["liveStreamability"];
|
|
1009
|
+
if (liveStream) throw new LiveStreamError("UNKNOWN");
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
url.includes("signature") || !stream2.s && (url.includes("&sig=") || url.includes("&lsig="));
|
|
1013
|
+
if (stream2.s !== void 0 && stream2.s !== null && cipher) {
|
|
1014
|
+
const signature = cipher.getSignature(stream2.s);
|
|
1015
|
+
const parsed = new URL(url);
|
|
1016
|
+
parsed.searchParams.set("sig", signature);
|
|
1017
|
+
if (needsThrottlingTransform(url)) {
|
|
1018
|
+
const initialN = parsed.searchParams.get("n");
|
|
1019
|
+
if (initialN) {
|
|
1020
|
+
try {
|
|
1021
|
+
parsed.searchParams.set("n", cipher.calculateN(initialN));
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
stream2.url = `${parsed.origin}${parsed.pathname}?${parsed.searchParams.toString()}`;
|
|
1027
|
+
} else if (cipher && needsThrottlingTransform(url)) {
|
|
1028
|
+
try {
|
|
1029
|
+
const parsed = new URL(url);
|
|
1030
|
+
const initialN = parsed.searchParams.get("n");
|
|
1031
|
+
if (initialN) {
|
|
1032
|
+
parsed.searchParams.set("n", cipher.calculateN(initialN));
|
|
1033
|
+
stream2.url = `${parsed.origin}${parsed.pathname}?${parsed.searchParams.toString()}`;
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function metadata(initialDataObj) {
|
|
1041
|
+
try {
|
|
1042
|
+
const root = initialDataObj;
|
|
1043
|
+
const rows = root?.contents?.twoColumnWatchNextResults?.results?.results?.contents?.[1]?.videoSecondaryInfoRenderer?.metadataRowContainer?.metadataRowContainerRenderer?.rows ?? [];
|
|
1044
|
+
const filtered = rows.filter((x) => x && "metadataRowRenderer" in x);
|
|
1045
|
+
const mapped = filtered.map((x) => x.metadataRowRenderer);
|
|
1046
|
+
return new YouTubeMetadata(mapped);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return new YouTubeMetadata([]);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/innertube.ts
|
|
1053
|
+
var K = (suffix) => ["AI", "za", "Sy", suffix].join("");
|
|
1054
|
+
var KEYS = {
|
|
1055
|
+
web: K("AO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"),
|
|
1056
|
+
android: K("A8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w"),
|
|
1057
|
+
ios: K("B-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc"),
|
|
1058
|
+
webMusic: K("C9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"),
|
|
1059
|
+
androidMusic: K("AOghZGza2MQSZkY_zfZ370N-PUdXEo8AI"),
|
|
1060
|
+
iosMusic: K("BAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s"),
|
|
1061
|
+
androidCreator: K("D_qjV8zaaUMehtLkrKFgVeSX_Iqbtyws8")
|
|
1062
|
+
};
|
|
1063
|
+
var DEFAULT_CLIENTS = {
|
|
1064
|
+
WEB: {
|
|
1065
|
+
context: {
|
|
1066
|
+
client: {
|
|
1067
|
+
clientName: "WEB",
|
|
1068
|
+
clientVersion: "2.20241126.01.00",
|
|
1069
|
+
hl: "en",
|
|
1070
|
+
gl: "US",
|
|
1071
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36,gzip(gfe)"
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
1074
|
+
header: {
|
|
1075
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
1076
|
+
"X-YouTube-Client-Name": "1",
|
|
1077
|
+
"X-YouTube-Client-Version": "2.20241126.01.00",
|
|
1078
|
+
Origin: "https://www.youtube.com"
|
|
1079
|
+
},
|
|
1080
|
+
apiKey: KEYS.web
|
|
1081
|
+
},
|
|
1082
|
+
ANDROID: {
|
|
1083
|
+
context: {
|
|
1084
|
+
client: {
|
|
1085
|
+
clientName: "ANDROID",
|
|
1086
|
+
clientVersion: "19.44.38",
|
|
1087
|
+
androidSdkVersion: 30,
|
|
1088
|
+
osName: "Android",
|
|
1089
|
+
osVersion: "14",
|
|
1090
|
+
platform: "MOBILE",
|
|
1091
|
+
hl: "en",
|
|
1092
|
+
gl: "US",
|
|
1093
|
+
userAgent: "com.google.android.youtube/19.44.38 (Linux; U; Android 14) gzip"
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
header: {
|
|
1097
|
+
"User-Agent": "com.google.android.youtube/19.44.38 (Linux; U; Android 14) gzip",
|
|
1098
|
+
"X-YouTube-Client-Name": "3",
|
|
1099
|
+
"X-YouTube-Client-Version": "19.44.38"
|
|
1100
|
+
},
|
|
1101
|
+
apiKey: KEYS.android
|
|
1102
|
+
},
|
|
1103
|
+
IOS: {
|
|
1104
|
+
context: {
|
|
1105
|
+
client: {
|
|
1106
|
+
clientName: "IOS",
|
|
1107
|
+
clientVersion: "19.45.4",
|
|
1108
|
+
deviceMake: "Apple",
|
|
1109
|
+
deviceModel: "iPhone16,2",
|
|
1110
|
+
osName: "iPhone",
|
|
1111
|
+
osVersion: "18.1.0.22B83",
|
|
1112
|
+
platform: "MOBILE",
|
|
1113
|
+
hl: "en",
|
|
1114
|
+
gl: "US",
|
|
1115
|
+
userAgent: "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X)"
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
header: {
|
|
1119
|
+
"User-Agent": "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X)",
|
|
1120
|
+
"X-YouTube-Client-Name": "5",
|
|
1121
|
+
"X-YouTube-Client-Version": "19.45.4"
|
|
1122
|
+
},
|
|
1123
|
+
apiKey: KEYS.ios
|
|
1124
|
+
},
|
|
1125
|
+
TV_EMBED: {
|
|
1126
|
+
context: {
|
|
1127
|
+
client: {
|
|
1128
|
+
clientName: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
|
|
1129
|
+
clientVersion: "2.0",
|
|
1130
|
+
hl: "en",
|
|
1131
|
+
gl: "US"
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
header: {
|
|
1135
|
+
"User-Agent": "Mozilla/5.0 (PlayStation; PlayStation 4/12.00) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15",
|
|
1136
|
+
"X-YouTube-Client-Name": "85",
|
|
1137
|
+
"X-YouTube-Client-Version": "2.0"
|
|
1138
|
+
},
|
|
1139
|
+
apiKey: KEYS.web
|
|
1140
|
+
},
|
|
1141
|
+
WEB_EMBED: {
|
|
1142
|
+
context: {
|
|
1143
|
+
client: {
|
|
1144
|
+
clientName: "WEB_EMBEDDED_PLAYER",
|
|
1145
|
+
clientVersion: "1.20241201.00.00",
|
|
1146
|
+
clientScreen: "EMBED",
|
|
1147
|
+
hl: "en",
|
|
1148
|
+
gl: "US"
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
header: {
|
|
1152
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
1153
|
+
"X-YouTube-Client-Name": "56",
|
|
1154
|
+
"X-YouTube-Client-Version": "1.20241201.00.00"
|
|
1155
|
+
},
|
|
1156
|
+
apiKey: KEYS.web
|
|
1157
|
+
},
|
|
1158
|
+
ANDROID_EMBED: {
|
|
1159
|
+
context: {
|
|
1160
|
+
client: {
|
|
1161
|
+
clientName: "ANDROID_EMBEDDED_PLAYER",
|
|
1162
|
+
clientVersion: "19.44.38",
|
|
1163
|
+
clientScreen: "EMBED",
|
|
1164
|
+
androidSdkVersion: 30,
|
|
1165
|
+
osName: "Android",
|
|
1166
|
+
osVersion: "14",
|
|
1167
|
+
platform: "MOBILE",
|
|
1168
|
+
hl: "en",
|
|
1169
|
+
gl: "US"
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
header: {
|
|
1173
|
+
"User-Agent": "com.google.android.youtube/19.44.38 (Linux; U; Android 14) gzip",
|
|
1174
|
+
"X-YouTube-Client-Name": "55",
|
|
1175
|
+
"X-YouTube-Client-Version": "19.44.38"
|
|
1176
|
+
},
|
|
1177
|
+
apiKey: KEYS.android
|
|
1178
|
+
},
|
|
1179
|
+
IOS_EMBED: {
|
|
1180
|
+
context: {
|
|
1181
|
+
client: {
|
|
1182
|
+
clientName: "IOS_MESSAGES_EXTENSION",
|
|
1183
|
+
clientVersion: "19.45.4",
|
|
1184
|
+
deviceMake: "Apple",
|
|
1185
|
+
deviceModel: "iPhone16,2",
|
|
1186
|
+
osName: "iPhone",
|
|
1187
|
+
osVersion: "18.1.0.22B83"
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
header: {
|
|
1191
|
+
"User-Agent": "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X)",
|
|
1192
|
+
"X-YouTube-Client-Name": "66",
|
|
1193
|
+
"X-YouTube-Client-Version": "19.45.4"
|
|
1194
|
+
},
|
|
1195
|
+
apiKey: KEYS.ios
|
|
1196
|
+
},
|
|
1197
|
+
MWEB: {
|
|
1198
|
+
context: {
|
|
1199
|
+
client: {
|
|
1200
|
+
clientName: "MWEB",
|
|
1201
|
+
clientVersion: "2.20241202.07.00",
|
|
1202
|
+
hl: "en",
|
|
1203
|
+
gl: "US"
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
header: {
|
|
1207
|
+
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1",
|
|
1208
|
+
"X-YouTube-Client-Name": "2",
|
|
1209
|
+
"X-YouTube-Client-Version": "2.20241202.07.00"
|
|
1210
|
+
},
|
|
1211
|
+
apiKey: KEYS.web
|
|
1212
|
+
},
|
|
1213
|
+
WEB_MUSIC: {
|
|
1214
|
+
context: { client: { clientName: "WEB_REMIX", clientVersion: "1.20241127.01.00", hl: "en", gl: "US" } },
|
|
1215
|
+
header: {
|
|
1216
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
1217
|
+
"X-YouTube-Client-Name": "67",
|
|
1218
|
+
"X-YouTube-Client-Version": "1.20241127.01.00"
|
|
1219
|
+
},
|
|
1220
|
+
apiKey: KEYS.webMusic
|
|
1221
|
+
},
|
|
1222
|
+
ANDROID_MUSIC: {
|
|
1223
|
+
context: {
|
|
1224
|
+
client: {
|
|
1225
|
+
clientName: "ANDROID_MUSIC",
|
|
1226
|
+
clientVersion: "7.27.52",
|
|
1227
|
+
androidSdkVersion: 30,
|
|
1228
|
+
osName: "Android",
|
|
1229
|
+
osVersion: "14",
|
|
1230
|
+
hl: "en",
|
|
1231
|
+
gl: "US"
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
header: {
|
|
1235
|
+
"User-Agent": "com.google.android.apps.youtube.music/7.27.52 (Linux; U; Android 14) gzip",
|
|
1236
|
+
"X-YouTube-Client-Name": "21",
|
|
1237
|
+
"X-YouTube-Client-Version": "7.27.52"
|
|
1238
|
+
},
|
|
1239
|
+
apiKey: KEYS.androidMusic
|
|
1240
|
+
},
|
|
1241
|
+
IOS_MUSIC: {
|
|
1242
|
+
context: {
|
|
1243
|
+
client: {
|
|
1244
|
+
clientName: "IOS_MUSIC",
|
|
1245
|
+
clientVersion: "7.27.0",
|
|
1246
|
+
deviceMake: "Apple",
|
|
1247
|
+
deviceModel: "iPhone16,2",
|
|
1248
|
+
osName: "iPhone",
|
|
1249
|
+
osVersion: "18.1.0.22B83"
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
header: {
|
|
1253
|
+
"User-Agent": "com.google.ios.youtubemusic/7.27.0 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X)",
|
|
1254
|
+
"X-YouTube-Client-Name": "26",
|
|
1255
|
+
"X-YouTube-Client-Version": "7.27.0"
|
|
1256
|
+
},
|
|
1257
|
+
apiKey: KEYS.iosMusic
|
|
1258
|
+
},
|
|
1259
|
+
WEB_CREATOR: {
|
|
1260
|
+
context: {
|
|
1261
|
+
client: { clientName: "WEB_CREATOR", clientVersion: "1.20241203.01.00", hl: "en", gl: "US" }
|
|
1262
|
+
},
|
|
1263
|
+
header: {
|
|
1264
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
1265
|
+
"X-YouTube-Client-Name": "62",
|
|
1266
|
+
"X-YouTube-Client-Version": "1.20241203.01.00"
|
|
1267
|
+
},
|
|
1268
|
+
apiKey: KEYS.web
|
|
1269
|
+
},
|
|
1270
|
+
ANDROID_CREATOR: {
|
|
1271
|
+
context: {
|
|
1272
|
+
client: {
|
|
1273
|
+
clientName: "ANDROID_CREATOR",
|
|
1274
|
+
clientVersion: "24.45.100",
|
|
1275
|
+
androidSdkVersion: 30,
|
|
1276
|
+
osName: "Android",
|
|
1277
|
+
osVersion: "14"
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
header: {
|
|
1281
|
+
"User-Agent": "com.google.android.apps.youtube.creator/24.45.100 (Linux; U; Android 14) gzip",
|
|
1282
|
+
"X-YouTube-Client-Name": "14",
|
|
1283
|
+
"X-YouTube-Client-Version": "24.45.100"
|
|
1284
|
+
},
|
|
1285
|
+
apiKey: KEYS.androidCreator
|
|
1286
|
+
},
|
|
1287
|
+
IOS_CREATOR: {
|
|
1288
|
+
context: {
|
|
1289
|
+
client: {
|
|
1290
|
+
clientName: "IOS_CREATOR",
|
|
1291
|
+
clientVersion: "24.45.100",
|
|
1292
|
+
deviceMake: "Apple",
|
|
1293
|
+
deviceModel: "iPhone16,2",
|
|
1294
|
+
osName: "iPhone",
|
|
1295
|
+
osVersion: "18.1.0.22B83"
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
header: {
|
|
1299
|
+
"User-Agent": "com.google.ios.ytcreator/24.45.100 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X)",
|
|
1300
|
+
"X-YouTube-Client-Name": "15",
|
|
1301
|
+
"X-YouTube-Client-Version": "24.45.100"
|
|
1302
|
+
},
|
|
1303
|
+
apiKey: KEYS.web
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
var OAUTH_CLIENT_ID = "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com";
|
|
1307
|
+
var OAUTH_CLIENT_SECRET = "SboVhoG9s0rNafixCSGGKXAT";
|
|
1308
|
+
var InnerTube = class {
|
|
1309
|
+
context;
|
|
1310
|
+
header;
|
|
1311
|
+
apiKey;
|
|
1312
|
+
useOauth;
|
|
1313
|
+
onAuthPrompt;
|
|
1314
|
+
accessToken = null;
|
|
1315
|
+
refreshToken = null;
|
|
1316
|
+
expires = null;
|
|
1317
|
+
constructor(opts = {}) {
|
|
1318
|
+
const clientName = opts.client ?? "ANDROID_MUSIC";
|
|
1319
|
+
const cfg = DEFAULT_CLIENTS[clientName];
|
|
1320
|
+
if (!cfg) throw new Error(`Unknown InnerTube client: ${clientName}`);
|
|
1321
|
+
this.context = cfg.context;
|
|
1322
|
+
this.header = cfg.header;
|
|
1323
|
+
this.apiKey = cfg.apiKey;
|
|
1324
|
+
this.useOauth = opts.useOauth ?? false;
|
|
1325
|
+
this.onAuthPrompt = opts.onAuthPrompt;
|
|
1326
|
+
}
|
|
1327
|
+
get baseUrl() {
|
|
1328
|
+
return "https://www.youtube.com/youtubei/v1";
|
|
1329
|
+
}
|
|
1330
|
+
baseData() {
|
|
1331
|
+
return {
|
|
1332
|
+
context: { client: { ...this.context.client } },
|
|
1333
|
+
contentCheckOk: true,
|
|
1334
|
+
racyCheckOk: true
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
endpointUrl(endpoint) {
|
|
1338
|
+
const params = new URLSearchParams({ prettyPrint: "false" });
|
|
1339
|
+
if (!this.useOauth) params.set("key", this.apiKey);
|
|
1340
|
+
return `${endpoint}?${params.toString()}`;
|
|
1341
|
+
}
|
|
1342
|
+
async callApi(endpoint, data) {
|
|
1343
|
+
const url = this.endpointUrl(endpoint);
|
|
1344
|
+
const headers = { "Content-Type": "application/json", ...this.header };
|
|
1345
|
+
if (this.useOauth) {
|
|
1346
|
+
if (!this.accessToken) await this.fetchBearerToken();
|
|
1347
|
+
else await this.refreshBearerToken();
|
|
1348
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
1349
|
+
}
|
|
1350
|
+
const text = await post(url, data, { headers });
|
|
1351
|
+
return JSON.parse(text);
|
|
1352
|
+
}
|
|
1353
|
+
// ---------- Endpoints ----------
|
|
1354
|
+
async player(videoId2) {
|
|
1355
|
+
const endpoint = `${this.baseUrl}/player`;
|
|
1356
|
+
const data = this.baseData();
|
|
1357
|
+
data["videoId"] = videoId2;
|
|
1358
|
+
data["params"] = "CgIQBg==";
|
|
1359
|
+
return await this.callApi(endpoint, data);
|
|
1360
|
+
}
|
|
1361
|
+
async search(searchQuery, continuation) {
|
|
1362
|
+
const endpoint = `${this.baseUrl}/search`;
|
|
1363
|
+
const data = this.baseData();
|
|
1364
|
+
data["query"] = searchQuery;
|
|
1365
|
+
if (continuation) data["continuation"] = continuation;
|
|
1366
|
+
return await this.callApi(endpoint, data);
|
|
1367
|
+
}
|
|
1368
|
+
async browse(continuation) {
|
|
1369
|
+
const endpoint = `${this.baseUrl}/browse`;
|
|
1370
|
+
const data = this.baseData();
|
|
1371
|
+
data["continuation"] = continuation;
|
|
1372
|
+
return await this.callApi(endpoint, data);
|
|
1373
|
+
}
|
|
1374
|
+
async verifyAge(videoId2) {
|
|
1375
|
+
const endpoint = `${this.baseUrl}/verify_age`;
|
|
1376
|
+
const data = this.baseData();
|
|
1377
|
+
data["nextEndpoint"] = { urlEndpoint: { url: `/watch?v=${videoId2}` } };
|
|
1378
|
+
data["setControvercy"] = true;
|
|
1379
|
+
return await this.callApi(endpoint, data);
|
|
1380
|
+
}
|
|
1381
|
+
async getTranscript(videoId2) {
|
|
1382
|
+
const endpoint = `${this.baseUrl}/get_transcript`;
|
|
1383
|
+
const data = this.baseData();
|
|
1384
|
+
data["videoId"] = videoId2;
|
|
1385
|
+
return await this.callApi(endpoint, data);
|
|
1386
|
+
}
|
|
1387
|
+
// ---------- OAuth device flow ----------
|
|
1388
|
+
async refreshBearerToken(force = false) {
|
|
1389
|
+
if (!this.useOauth) return;
|
|
1390
|
+
if (!force && this.expires !== null && this.expires > Date.now() / 1e3) return;
|
|
1391
|
+
if (!this.refreshToken) return;
|
|
1392
|
+
const startTime = Math.floor(Date.now() / 1e3) - 30;
|
|
1393
|
+
const body = JSON.stringify({
|
|
1394
|
+
client_id: OAUTH_CLIENT_ID,
|
|
1395
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
1396
|
+
grant_type: "refresh_token",
|
|
1397
|
+
refresh_token: this.refreshToken
|
|
1398
|
+
});
|
|
1399
|
+
const text = await post("https://oauth2.googleapis.com/token", JSON.parse(body), {
|
|
1400
|
+
headers: { "Content-Type": "application/json" }
|
|
1401
|
+
});
|
|
1402
|
+
const data = JSON.parse(text);
|
|
1403
|
+
this.accessToken = data.access_token;
|
|
1404
|
+
this.expires = startTime + data.expires_in;
|
|
1405
|
+
}
|
|
1406
|
+
async fetchBearerToken() {
|
|
1407
|
+
const startTime = Math.floor(Date.now() / 1e3) - 30;
|
|
1408
|
+
const text1 = await post(
|
|
1409
|
+
"https://oauth2.googleapis.com/device/code",
|
|
1410
|
+
{ client_id: OAUTH_CLIENT_ID, scope: "https://www.googleapis.com/auth/youtube" },
|
|
1411
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
1412
|
+
);
|
|
1413
|
+
const phase1 = JSON.parse(text1);
|
|
1414
|
+
if (this.onAuthPrompt) {
|
|
1415
|
+
await this.onAuthPrompt(phase1.verification_url, phase1.user_code);
|
|
1416
|
+
} else {
|
|
1417
|
+
console.log(
|
|
1418
|
+
`Please open ${phase1.verification_url} and input code ${phase1.user_code}, then continue.`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
const text2 = await post(
|
|
1422
|
+
"https://oauth2.googleapis.com/token",
|
|
1423
|
+
{
|
|
1424
|
+
client_id: OAUTH_CLIENT_ID,
|
|
1425
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
1426
|
+
device_code: phase1.device_code,
|
|
1427
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1428
|
+
},
|
|
1429
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
1430
|
+
);
|
|
1431
|
+
const phase2 = JSON.parse(text2);
|
|
1432
|
+
this.accessToken = phase2.access_token;
|
|
1433
|
+
this.refreshToken = phase2.refresh_token;
|
|
1434
|
+
this.expires = startTime + phase2.expires_in;
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
// src/itags.ts
|
|
1439
|
+
var t = (resolution, abr) => ({ resolution, abr });
|
|
1440
|
+
var PROGRESSIVE_VIDEO = {
|
|
1441
|
+
5: t("240p", "64kbps"),
|
|
1442
|
+
6: t("270p", "64kbps"),
|
|
1443
|
+
13: t("144p", null),
|
|
1444
|
+
17: t("144p", "24kbps"),
|
|
1445
|
+
18: t("360p", "96kbps"),
|
|
1446
|
+
22: t("720p", "192kbps"),
|
|
1447
|
+
34: t("360p", "128kbps"),
|
|
1448
|
+
35: t("480p", "128kbps"),
|
|
1449
|
+
36: t("240p", null),
|
|
1450
|
+
37: t("1080p", "192kbps"),
|
|
1451
|
+
38: t("3072p", "192kbps"),
|
|
1452
|
+
43: t("360p", "128kbps"),
|
|
1453
|
+
44: t("480p", "128kbps"),
|
|
1454
|
+
45: t("720p", "192kbps"),
|
|
1455
|
+
46: t("1080p", "192kbps"),
|
|
1456
|
+
59: t("480p", "128kbps"),
|
|
1457
|
+
78: t("480p", "128kbps"),
|
|
1458
|
+
82: t("360p", "128kbps"),
|
|
1459
|
+
83: t("480p", "128kbps"),
|
|
1460
|
+
84: t("720p", "192kbps"),
|
|
1461
|
+
85: t("1080p", "192kbps"),
|
|
1462
|
+
91: t("144p", "48kbps"),
|
|
1463
|
+
92: t("240p", "48kbps"),
|
|
1464
|
+
93: t("360p", "128kbps"),
|
|
1465
|
+
94: t("480p", "128kbps"),
|
|
1466
|
+
95: t("720p", "256kbps"),
|
|
1467
|
+
96: t("1080p", "256kbps"),
|
|
1468
|
+
100: t("360p", "128kbps"),
|
|
1469
|
+
101: t("480p", "192kbps"),
|
|
1470
|
+
102: t("720p", "192kbps"),
|
|
1471
|
+
132: t("240p", "48kbps"),
|
|
1472
|
+
151: t("720p", "24kbps"),
|
|
1473
|
+
300: t("720p", "128kbps"),
|
|
1474
|
+
301: t("1080p", "128kbps")
|
|
1475
|
+
};
|
|
1476
|
+
var DASH_VIDEO = {
|
|
1477
|
+
133: t("240p", null),
|
|
1478
|
+
134: t("360p", null),
|
|
1479
|
+
135: t("480p", null),
|
|
1480
|
+
136: t("720p", null),
|
|
1481
|
+
137: t("1080p", null),
|
|
1482
|
+
138: t("2160p", null),
|
|
1483
|
+
160: t("144p", null),
|
|
1484
|
+
167: t("360p", null),
|
|
1485
|
+
168: t("480p", null),
|
|
1486
|
+
169: t("720p", null),
|
|
1487
|
+
170: t("1080p", null),
|
|
1488
|
+
212: t("480p", null),
|
|
1489
|
+
218: t("480p", null),
|
|
1490
|
+
219: t("480p", null),
|
|
1491
|
+
242: t("240p", null),
|
|
1492
|
+
243: t("360p", null),
|
|
1493
|
+
244: t("480p", null),
|
|
1494
|
+
245: t("480p", null),
|
|
1495
|
+
246: t("480p", null),
|
|
1496
|
+
247: t("720p", null),
|
|
1497
|
+
248: t("1080p", null),
|
|
1498
|
+
264: t("1440p", null),
|
|
1499
|
+
266: t("2160p", null),
|
|
1500
|
+
271: t("1440p", null),
|
|
1501
|
+
272: t("4320p", null),
|
|
1502
|
+
278: t("144p", null),
|
|
1503
|
+
298: t("720p", null),
|
|
1504
|
+
299: t("1080p", null),
|
|
1505
|
+
302: t("720p", null),
|
|
1506
|
+
303: t("1080p", null),
|
|
1507
|
+
308: t("1440p", null),
|
|
1508
|
+
313: t("2160p", null),
|
|
1509
|
+
315: t("2160p", null),
|
|
1510
|
+
330: t("144p", null),
|
|
1511
|
+
331: t("240p", null),
|
|
1512
|
+
332: t("360p", null),
|
|
1513
|
+
333: t("480p", null),
|
|
1514
|
+
334: t("720p", null),
|
|
1515
|
+
335: t("1080p", null),
|
|
1516
|
+
336: t("1440p", null),
|
|
1517
|
+
337: t("2160p", null),
|
|
1518
|
+
394: t("144p", null),
|
|
1519
|
+
395: t("240p", null),
|
|
1520
|
+
396: t("360p", null),
|
|
1521
|
+
397: t("480p", null),
|
|
1522
|
+
398: t("720p", null),
|
|
1523
|
+
399: t("1080p", null),
|
|
1524
|
+
400: t("1440p", null),
|
|
1525
|
+
401: t("2160p", null),
|
|
1526
|
+
402: t("4320p", null),
|
|
1527
|
+
571: t("4320p", null),
|
|
1528
|
+
694: t("144p", null),
|
|
1529
|
+
695: t("240p", null),
|
|
1530
|
+
696: t("360p", null),
|
|
1531
|
+
697: t("480p", null),
|
|
1532
|
+
698: t("720p", null),
|
|
1533
|
+
699: t("1080p", null),
|
|
1534
|
+
700: t("1440p", null),
|
|
1535
|
+
701: t("2160p", null),
|
|
1536
|
+
702: t("4320p", null)
|
|
1537
|
+
};
|
|
1538
|
+
var DASH_AUDIO = {
|
|
1539
|
+
139: t(null, "48kbps"),
|
|
1540
|
+
140: t(null, "128kbps"),
|
|
1541
|
+
141: t(null, "256kbps"),
|
|
1542
|
+
171: t(null, "128kbps"),
|
|
1543
|
+
172: t(null, "256kbps"),
|
|
1544
|
+
249: t(null, "50kbps"),
|
|
1545
|
+
250: t(null, "70kbps"),
|
|
1546
|
+
251: t(null, "160kbps"),
|
|
1547
|
+
256: t(null, "192kbps"),
|
|
1548
|
+
258: t(null, "384kbps"),
|
|
1549
|
+
325: t(null, null),
|
|
1550
|
+
328: t(null, null)
|
|
1551
|
+
};
|
|
1552
|
+
var ITAGS = {
|
|
1553
|
+
...PROGRESSIVE_VIDEO,
|
|
1554
|
+
...DASH_VIDEO,
|
|
1555
|
+
...DASH_AUDIO
|
|
1556
|
+
};
|
|
1557
|
+
var HDR = /* @__PURE__ */ new Set([330, 331, 332, 333, 334, 335, 336, 337]);
|
|
1558
|
+
var THREE_D = /* @__PURE__ */ new Set([82, 83, 84, 85, 100, 101, 102]);
|
|
1559
|
+
var LIVE = /* @__PURE__ */ new Set([91, 92, 93, 94, 95, 96, 132, 151]);
|
|
1560
|
+
function getFormatProfile(itag) {
|
|
1561
|
+
const code = typeof itag === "string" ? parseInt(itag, 10) : itag;
|
|
1562
|
+
const info = ITAGS[code];
|
|
1563
|
+
return {
|
|
1564
|
+
resolution: info?.resolution ?? null,
|
|
1565
|
+
abr: info?.abr ?? null,
|
|
1566
|
+
isLive: LIVE.has(code),
|
|
1567
|
+
is3d: THREE_D.has(code),
|
|
1568
|
+
isHdr: HDR.has(code),
|
|
1569
|
+
isDash: code in DASH_AUDIO || code in DASH_VIDEO
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/stream.ts
|
|
1574
|
+
var Stream = class {
|
|
1575
|
+
itag;
|
|
1576
|
+
url;
|
|
1577
|
+
mimeType;
|
|
1578
|
+
codecs;
|
|
1579
|
+
type;
|
|
1580
|
+
subtype;
|
|
1581
|
+
videoCodec;
|
|
1582
|
+
audioCodec;
|
|
1583
|
+
isOtf;
|
|
1584
|
+
bitrate;
|
|
1585
|
+
fps;
|
|
1586
|
+
resolution;
|
|
1587
|
+
abr;
|
|
1588
|
+
isDash;
|
|
1589
|
+
is3d;
|
|
1590
|
+
isHdr;
|
|
1591
|
+
isLive;
|
|
1592
|
+
cachedFilesize;
|
|
1593
|
+
state;
|
|
1594
|
+
constructor(raw, state) {
|
|
1595
|
+
this.state = state;
|
|
1596
|
+
if (!raw.url) throw new Error("Stream missing url after descrambling");
|
|
1597
|
+
this.url = raw.url;
|
|
1598
|
+
this.itag = typeof raw.itag === "string" ? parseInt(raw.itag, 10) : raw.itag;
|
|
1599
|
+
const { mimeType, codecs } = mimeTypeCodec(raw.mimeType ?? "");
|
|
1600
|
+
this.mimeType = mimeType;
|
|
1601
|
+
this.codecs = codecs;
|
|
1602
|
+
const [type, subtype] = mimeType.split("/");
|
|
1603
|
+
this.type = type ?? "";
|
|
1604
|
+
this.subtype = subtype ?? "";
|
|
1605
|
+
[this.videoCodec, this.audioCodec] = this.parseCodecs();
|
|
1606
|
+
this.isOtf = raw.is_otf ?? false;
|
|
1607
|
+
this.bitrate = raw.bitrate ?? null;
|
|
1608
|
+
this.cachedFilesize = parseInt(String(raw.contentLength ?? "0"), 10);
|
|
1609
|
+
const profile = getFormatProfile(this.itag);
|
|
1610
|
+
this.isDash = profile.isDash;
|
|
1611
|
+
this.abr = profile.abr;
|
|
1612
|
+
this.fps = raw.fps ?? null;
|
|
1613
|
+
this.resolution = profile.resolution;
|
|
1614
|
+
this.is3d = profile.is3d;
|
|
1615
|
+
this.isHdr = profile.isHdr;
|
|
1616
|
+
this.isLive = profile.isLive;
|
|
1617
|
+
}
|
|
1618
|
+
// Adaptive = single codec list (audio-only OR video-only)
|
|
1619
|
+
get isAdaptive() {
|
|
1620
|
+
return this.codecs.length % 2 === 1;
|
|
1621
|
+
}
|
|
1622
|
+
get isProgressive() {
|
|
1623
|
+
return !this.isAdaptive;
|
|
1624
|
+
}
|
|
1625
|
+
get includesAudioTrack() {
|
|
1626
|
+
return this.isProgressive || this.type === "audio";
|
|
1627
|
+
}
|
|
1628
|
+
get includesVideoTrack() {
|
|
1629
|
+
return this.isProgressive || this.type === "video";
|
|
1630
|
+
}
|
|
1631
|
+
parseCodecs() {
|
|
1632
|
+
let video = null;
|
|
1633
|
+
let audio = null;
|
|
1634
|
+
if (this.codecs.length === 0) return [null, null];
|
|
1635
|
+
if (this.codecs.length === 2) {
|
|
1636
|
+
video = this.codecs[0] ?? null;
|
|
1637
|
+
audio = this.codecs[1] ?? null;
|
|
1638
|
+
} else if (this.codecs.length === 1) {
|
|
1639
|
+
video = this.codecs[0] ?? null;
|
|
1640
|
+
}
|
|
1641
|
+
return [video, audio];
|
|
1642
|
+
}
|
|
1643
|
+
get title() {
|
|
1644
|
+
return this.state.title ?? "Unknown YouTube Video Title";
|
|
1645
|
+
}
|
|
1646
|
+
async filesize() {
|
|
1647
|
+
if (this.cachedFilesize === 0) {
|
|
1648
|
+
try {
|
|
1649
|
+
this.cachedFilesize = await filesize(this.url);
|
|
1650
|
+
} catch {
|
|
1651
|
+
this.cachedFilesize = await seqFilesize(this.url);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return this.cachedFilesize;
|
|
1655
|
+
}
|
|
1656
|
+
async filesizeApprox() {
|
|
1657
|
+
if (this.state.duration && this.bitrate) {
|
|
1658
|
+
return Math.floor(this.state.duration * this.bitrate / 8);
|
|
1659
|
+
}
|
|
1660
|
+
return this.filesize();
|
|
1661
|
+
}
|
|
1662
|
+
get expiration() {
|
|
1663
|
+
try {
|
|
1664
|
+
const u = new URL(this.url);
|
|
1665
|
+
const expire = u.searchParams.get("expire");
|
|
1666
|
+
if (!expire) return null;
|
|
1667
|
+
return new Date(parseInt(expire, 10) * 1e3);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
get defaultFilename() {
|
|
1673
|
+
return `${safeFilename(this.title)}.${this.subtype}`;
|
|
1674
|
+
}
|
|
1675
|
+
/** Compute the absolute output path the download will be written to. */
|
|
1676
|
+
async getFilePath(opts = {}) {
|
|
1677
|
+
const filename = opts.filename ?? this.defaultFilename;
|
|
1678
|
+
const prefixed = opts.filenamePrefix ? `${opts.filenamePrefix}${filename}` : filename;
|
|
1679
|
+
const dir = await resolveOutputDir(opts.outputPath);
|
|
1680
|
+
await ensureDir(dir);
|
|
1681
|
+
return joinPath(dir, prefixed);
|
|
1682
|
+
}
|
|
1683
|
+
async existsAtPath(filePath) {
|
|
1684
|
+
if (!await fileExists(filePath)) return false;
|
|
1685
|
+
const fs = await getFs();
|
|
1686
|
+
if (!fs) return false;
|
|
1687
|
+
const expected = await this.filesize();
|
|
1688
|
+
const stat = await fs.stat(filePath);
|
|
1689
|
+
return stat.size === expected;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Download the stream to the local filesystem and return the absolute path.
|
|
1693
|
+
* Throws if no filesystem is available (e.g., browser); use streamChunks() instead.
|
|
1694
|
+
*/
|
|
1695
|
+
async download(opts = {}) {
|
|
1696
|
+
const fs = await getFs();
|
|
1697
|
+
if (!fs) {
|
|
1698
|
+
throw new Error("Filesystem unavailable in this runtime; use streamChunks() instead.");
|
|
1699
|
+
}
|
|
1700
|
+
const filePath = await this.getFilePath(opts);
|
|
1701
|
+
const skipExisting = opts.skipExisting ?? true;
|
|
1702
|
+
if (skipExisting && await this.existsAtPath(filePath)) {
|
|
1703
|
+
this.fireOnComplete(filePath);
|
|
1704
|
+
return filePath;
|
|
1705
|
+
}
|
|
1706
|
+
let bytesRemaining = await this.filesize();
|
|
1707
|
+
const reqOpts = {
|
|
1708
|
+
timeoutMs: opts.timeoutMs,
|
|
1709
|
+
maxRetries: opts.maxRetries ?? 0,
|
|
1710
|
+
signal: opts.signal
|
|
1711
|
+
};
|
|
1712
|
+
const handle = await fs.open(filePath, "w");
|
|
1713
|
+
try {
|
|
1714
|
+
let usedSeqStream = false;
|
|
1715
|
+
try {
|
|
1716
|
+
for await (const chunk of stream(this.url, reqOpts)) {
|
|
1717
|
+
await handle.write(chunk);
|
|
1718
|
+
bytesRemaining -= chunk.byteLength;
|
|
1719
|
+
this.fireOnProgress(chunk, bytesRemaining);
|
|
1720
|
+
}
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
if (!is404(err)) throw err;
|
|
1723
|
+
usedSeqStream = true;
|
|
1724
|
+
}
|
|
1725
|
+
if (usedSeqStream) {
|
|
1726
|
+
for await (const chunk of seqStream(this.url, reqOpts)) {
|
|
1727
|
+
await handle.write(chunk);
|
|
1728
|
+
bytesRemaining -= chunk.byteLength;
|
|
1729
|
+
this.fireOnProgress(chunk, bytesRemaining);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
} finally {
|
|
1733
|
+
await handle.close();
|
|
1734
|
+
}
|
|
1735
|
+
this.fireOnComplete(filePath);
|
|
1736
|
+
return filePath;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Stream the media as raw chunks. Browser-friendly alternative to download().
|
|
1740
|
+
* Caller is responsible for collecting the chunks (write to a Blob, sink, etc.).
|
|
1741
|
+
*/
|
|
1742
|
+
async *streamChunks(opts = {}) {
|
|
1743
|
+
const reqOpts = {
|
|
1744
|
+
timeoutMs: opts.timeoutMs,
|
|
1745
|
+
maxRetries: opts.maxRetries ?? 0,
|
|
1746
|
+
signal: opts.signal
|
|
1747
|
+
};
|
|
1748
|
+
let bytesRemaining = await this.filesize();
|
|
1749
|
+
try {
|
|
1750
|
+
for await (const chunk of stream(this.url, reqOpts)) {
|
|
1751
|
+
bytesRemaining -= chunk.byteLength;
|
|
1752
|
+
this.fireOnProgress(chunk, bytesRemaining);
|
|
1753
|
+
yield chunk;
|
|
1754
|
+
}
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
if (!is404(err)) throw err;
|
|
1757
|
+
for await (const chunk of seqStream(this.url, reqOpts)) {
|
|
1758
|
+
bytesRemaining -= chunk.byteLength;
|
|
1759
|
+
this.fireOnProgress(chunk, bytesRemaining);
|
|
1760
|
+
yield chunk;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
this.fireOnComplete(null);
|
|
1764
|
+
}
|
|
1765
|
+
fireOnProgress(chunk, bytesRemaining) {
|
|
1766
|
+
this.state.onProgress?.(this, chunk, bytesRemaining);
|
|
1767
|
+
}
|
|
1768
|
+
fireOnComplete(path) {
|
|
1769
|
+
this.state.onComplete?.(this, path);
|
|
1770
|
+
}
|
|
1771
|
+
toString() {
|
|
1772
|
+
const parts = [`itag="${this.itag}"`, `mime_type="${this.mimeType}"`];
|
|
1773
|
+
if (this.includesVideoTrack) {
|
|
1774
|
+
parts.push(`res="${this.resolution}"`, `fps="${this.fps}fps"`);
|
|
1775
|
+
if (!this.isAdaptive) {
|
|
1776
|
+
parts.push(`vcodec="${this.videoCodec}"`, `acodec="${this.audioCodec}"`);
|
|
1777
|
+
} else {
|
|
1778
|
+
parts.push(`vcodec="${this.videoCodec}"`);
|
|
1779
|
+
}
|
|
1780
|
+
} else {
|
|
1781
|
+
parts.push(`abr="${this.abr}"`, `acodec="${this.audioCodec}"`);
|
|
1782
|
+
}
|
|
1783
|
+
parts.push(`progressive="${this.isProgressive}"`, `type="${this.type}"`);
|
|
1784
|
+
return `<Stream: ${parts.join(" ")}>`;
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
function is404(err) {
|
|
1788
|
+
if (!err) return false;
|
|
1789
|
+
const msg = err.message ?? "";
|
|
1790
|
+
return msg.includes("404");
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/query.ts
|
|
1794
|
+
var StreamQuery = class _StreamQuery {
|
|
1795
|
+
fmtStreams;
|
|
1796
|
+
itagIndex;
|
|
1797
|
+
constructor(fmtStreams) {
|
|
1798
|
+
this.fmtStreams = fmtStreams;
|
|
1799
|
+
this.itagIndex = new Map(fmtStreams.map((s) => [s.itag, s]));
|
|
1800
|
+
}
|
|
1801
|
+
filter(criteria = {}) {
|
|
1802
|
+
const filters = [];
|
|
1803
|
+
const resolution = criteria.res ?? criteria.resolution;
|
|
1804
|
+
if (resolution) {
|
|
1805
|
+
if (Array.isArray(resolution)) {
|
|
1806
|
+
filters.push((s) => s.resolution !== null && resolution.includes(s.resolution));
|
|
1807
|
+
} else {
|
|
1808
|
+
filters.push((s) => s.resolution === resolution);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (criteria.fps !== void 0) filters.push((s) => s.fps === criteria.fps);
|
|
1812
|
+
if (criteria.mimeType !== void 0) filters.push((s) => s.mimeType === criteria.mimeType);
|
|
1813
|
+
if (criteria.type !== void 0) filters.push((s) => s.type === criteria.type);
|
|
1814
|
+
const subtype = criteria.subtype ?? criteria.fileExtension;
|
|
1815
|
+
if (subtype !== void 0) filters.push((s) => s.subtype === subtype);
|
|
1816
|
+
const abr = criteria.abr ?? criteria.bitrate;
|
|
1817
|
+
if (abr !== void 0) filters.push((s) => s.abr === abr);
|
|
1818
|
+
if (criteria.videoCodec !== void 0) filters.push((s) => s.videoCodec === criteria.videoCodec);
|
|
1819
|
+
if (criteria.audioCodec !== void 0) filters.push((s) => s.audioCodec === criteria.audioCodec);
|
|
1820
|
+
if (criteria.onlyAudio) filters.push((s) => s.includesAudioTrack && !s.includesVideoTrack);
|
|
1821
|
+
if (criteria.onlyVideo) filters.push((s) => s.includesVideoTrack && !s.includesAudioTrack);
|
|
1822
|
+
if (criteria.progressive) filters.push((s) => s.isProgressive);
|
|
1823
|
+
if (criteria.adaptive) filters.push((s) => s.isAdaptive);
|
|
1824
|
+
if (criteria.isDash !== void 0) filters.push((s) => s.isDash === criteria.isDash);
|
|
1825
|
+
if (criteria.customFilters) filters.push(...criteria.customFilters);
|
|
1826
|
+
return this.applyFilters(filters);
|
|
1827
|
+
}
|
|
1828
|
+
applyFilters(filters) {
|
|
1829
|
+
let result = this.fmtStreams;
|
|
1830
|
+
for (const f of filters) result = result.filter(f);
|
|
1831
|
+
return new _StreamQuery(result);
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Sort by an attribute. Filters out streams that don't have it. For string
|
|
1835
|
+
* attributes (e.g., "720p"), sorts by the embedded integer.
|
|
1836
|
+
*/
|
|
1837
|
+
orderBy(attribute) {
|
|
1838
|
+
const present = this.fmtStreams.filter((s) => s[attribute] !== null && s[attribute] !== void 0);
|
|
1839
|
+
if (present.length === 0) return new _StreamQuery(present);
|
|
1840
|
+
const first = present[0][attribute];
|
|
1841
|
+
if (typeof first === "string") {
|
|
1842
|
+
try {
|
|
1843
|
+
const sorted2 = [...present].sort((a, b) => {
|
|
1844
|
+
const va = parseInt(String(a[attribute]).replace(/\D/g, ""), 10);
|
|
1845
|
+
const vb = parseInt(String(b[attribute]).replace(/\D/g, ""), 10);
|
|
1846
|
+
if (Number.isNaN(va) || Number.isNaN(vb)) throw new Error("non-numeric");
|
|
1847
|
+
return va - vb;
|
|
1848
|
+
});
|
|
1849
|
+
return new _StreamQuery(sorted2);
|
|
1850
|
+
} catch {
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const sorted = [...present].sort((a, b) => {
|
|
1854
|
+
const va = a[attribute];
|
|
1855
|
+
const vb = b[attribute];
|
|
1856
|
+
if (va < vb) return -1;
|
|
1857
|
+
if (va > vb) return 1;
|
|
1858
|
+
return 0;
|
|
1859
|
+
});
|
|
1860
|
+
return new _StreamQuery(sorted);
|
|
1861
|
+
}
|
|
1862
|
+
desc() {
|
|
1863
|
+
return new _StreamQuery([...this.fmtStreams].reverse());
|
|
1864
|
+
}
|
|
1865
|
+
asc() {
|
|
1866
|
+
return this;
|
|
1867
|
+
}
|
|
1868
|
+
getByItag(itag) {
|
|
1869
|
+
return this.itagIndex.get(itag);
|
|
1870
|
+
}
|
|
1871
|
+
getByResolution(resolution) {
|
|
1872
|
+
return this.filter({ progressive: true, subtype: "mp4", resolution }).first();
|
|
1873
|
+
}
|
|
1874
|
+
getLowestResolution() {
|
|
1875
|
+
return this.filter({ progressive: true, subtype: "mp4" }).orderBy("resolution").first();
|
|
1876
|
+
}
|
|
1877
|
+
getHighestResolution() {
|
|
1878
|
+
return this.filter({ progressive: true }).orderBy("resolution").last();
|
|
1879
|
+
}
|
|
1880
|
+
getAudioOnly(subtype = "mp4") {
|
|
1881
|
+
return this.filter({ onlyAudio: true, subtype }).orderBy("abr").last();
|
|
1882
|
+
}
|
|
1883
|
+
otf(isOtf = false) {
|
|
1884
|
+
return this.applyFilters([(s) => s.isOtf === isOtf]);
|
|
1885
|
+
}
|
|
1886
|
+
first() {
|
|
1887
|
+
return this.fmtStreams[0];
|
|
1888
|
+
}
|
|
1889
|
+
last() {
|
|
1890
|
+
return this.fmtStreams[this.fmtStreams.length - 1];
|
|
1891
|
+
}
|
|
1892
|
+
get length() {
|
|
1893
|
+
return this.fmtStreams.length;
|
|
1894
|
+
}
|
|
1895
|
+
at(index) {
|
|
1896
|
+
return this.fmtStreams[index];
|
|
1897
|
+
}
|
|
1898
|
+
toArray() {
|
|
1899
|
+
return [...this.fmtStreams];
|
|
1900
|
+
}
|
|
1901
|
+
[Symbol.iterator]() {
|
|
1902
|
+
return this.fmtStreams[Symbol.iterator]();
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
var CaptionQuery = class {
|
|
1906
|
+
index;
|
|
1907
|
+
constructor(captions) {
|
|
1908
|
+
this.index = new Map(captions.map((c) => [c.code, c]));
|
|
1909
|
+
}
|
|
1910
|
+
get(langCode) {
|
|
1911
|
+
return this.index.get(langCode);
|
|
1912
|
+
}
|
|
1913
|
+
has(langCode) {
|
|
1914
|
+
return this.index.has(langCode);
|
|
1915
|
+
}
|
|
1916
|
+
get length() {
|
|
1917
|
+
return this.index.size;
|
|
1918
|
+
}
|
|
1919
|
+
toArray() {
|
|
1920
|
+
return [...this.index.values()];
|
|
1921
|
+
}
|
|
1922
|
+
[Symbol.iterator]() {
|
|
1923
|
+
return this.index.values();
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
// src/youtube.ts
|
|
1928
|
+
var YouTube = class _YouTube {
|
|
1929
|
+
videoId;
|
|
1930
|
+
watchUrl;
|
|
1931
|
+
embedUrl;
|
|
1932
|
+
useOauth;
|
|
1933
|
+
allowOauthCache;
|
|
1934
|
+
state;
|
|
1935
|
+
cachedJs = null;
|
|
1936
|
+
cachedJsUrl = null;
|
|
1937
|
+
cachedVidInfo = null;
|
|
1938
|
+
cachedWatchHtml = null;
|
|
1939
|
+
cachedEmbedHtml = null;
|
|
1940
|
+
cachedAgeRestricted = null;
|
|
1941
|
+
cachedFmtStreams = null;
|
|
1942
|
+
cachedInitialData = null;
|
|
1943
|
+
cachedMetadata = null;
|
|
1944
|
+
cachedTitle = null;
|
|
1945
|
+
cachedAuthor = null;
|
|
1946
|
+
cachedPublishDate = void 0;
|
|
1947
|
+
constructor(url, opts = {}) {
|
|
1948
|
+
this.videoId = videoId(url);
|
|
1949
|
+
this.watchUrl = `https://youtube.com/watch?v=${this.videoId}`;
|
|
1950
|
+
this.embedUrl = `https://www.youtube.com/embed/${this.videoId}`;
|
|
1951
|
+
this.useOauth = opts.useOauth ?? false;
|
|
1952
|
+
this.allowOauthCache = opts.allowOauthCache ?? true;
|
|
1953
|
+
this.state = {
|
|
1954
|
+
title: null,
|
|
1955
|
+
duration: null,
|
|
1956
|
+
onProgress: opts.onProgress,
|
|
1957
|
+
onComplete: opts.onComplete
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
static fromId(videoId2) {
|
|
1961
|
+
return new _YouTube(`https://www.youtube.com/watch?v=${videoId2}`);
|
|
1962
|
+
}
|
|
1963
|
+
// ---------- Lazy network resources ----------
|
|
1964
|
+
async watchHtml() {
|
|
1965
|
+
if (this.cachedWatchHtml) return this.cachedWatchHtml;
|
|
1966
|
+
this.cachedWatchHtml = await get(this.watchUrl);
|
|
1967
|
+
return this.cachedWatchHtml;
|
|
1968
|
+
}
|
|
1969
|
+
async embedHtml() {
|
|
1970
|
+
if (this.cachedEmbedHtml) return this.cachedEmbedHtml;
|
|
1971
|
+
this.cachedEmbedHtml = await get(this.embedUrl);
|
|
1972
|
+
return this.cachedEmbedHtml;
|
|
1973
|
+
}
|
|
1974
|
+
async ageRestricted() {
|
|
1975
|
+
if (this.cachedAgeRestricted !== null) return this.cachedAgeRestricted;
|
|
1976
|
+
this.cachedAgeRestricted = isAgeRestricted(await this.watchHtml());
|
|
1977
|
+
return this.cachedAgeRestricted;
|
|
1978
|
+
}
|
|
1979
|
+
async jsUrl() {
|
|
1980
|
+
if (this.cachedJsUrl) return this.cachedJsUrl;
|
|
1981
|
+
const html = await this.ageRestricted() ? await this.embedHtml() : await this.watchHtml();
|
|
1982
|
+
this.cachedJsUrl = jsUrl(html);
|
|
1983
|
+
return this.cachedJsUrl;
|
|
1984
|
+
}
|
|
1985
|
+
async js() {
|
|
1986
|
+
if (this.cachedJs) return this.cachedJs;
|
|
1987
|
+
const url = await this.jsUrl();
|
|
1988
|
+
this.cachedJs = await get(url);
|
|
1989
|
+
return this.cachedJs;
|
|
1990
|
+
}
|
|
1991
|
+
async initialData() {
|
|
1992
|
+
if (this.cachedInitialData) return this.cachedInitialData;
|
|
1993
|
+
this.cachedInitialData = initialData(await this.watchHtml());
|
|
1994
|
+
return this.cachedInitialData;
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Resolve the player response containing videoDetails + streamingData.
|
|
1998
|
+
*
|
|
1999
|
+
* Strategy (late-2025 reality):
|
|
2000
|
+
* 1. Primary: scrape `ytInitialPlayerResponse` from the watch HTML. This is
|
|
2001
|
+
* the same payload a browser sees and contains streamingData + captions.
|
|
2002
|
+
* 2. Fallback: InnerTube /player endpoint (useful for age-gate bypass, but
|
|
2003
|
+
* currently rate-limited by YouTube's "Sign in to confirm you're not a
|
|
2004
|
+
* bot" check for server-side IPs).
|
|
2005
|
+
*
|
|
2006
|
+
* If the primary path returns a useful response we never touch InnerTube,
|
|
2007
|
+
* which avoids the bot-detection entirely for normal public videos.
|
|
2008
|
+
*/
|
|
2009
|
+
async vidInfo() {
|
|
2010
|
+
if (this.cachedVidInfo) return this.cachedVidInfo;
|
|
2011
|
+
try {
|
|
2012
|
+
const html = await this.watchHtml();
|
|
2013
|
+
const fromHtml = initialPlayerResponse(html);
|
|
2014
|
+
if (fromHtml?.videoDetails && fromHtml?.streamingData) {
|
|
2015
|
+
this.cachedVidInfo = fromHtml;
|
|
2016
|
+
return fromHtml;
|
|
2017
|
+
}
|
|
2018
|
+
} catch {
|
|
2019
|
+
}
|
|
2020
|
+
const innertube = new InnerTube({ useOauth: this.useOauth });
|
|
2021
|
+
this.cachedVidInfo = await innertube.player(this.videoId);
|
|
2022
|
+
return this.cachedVidInfo;
|
|
2023
|
+
}
|
|
2024
|
+
async streamingData() {
|
|
2025
|
+
let info = await this.vidInfo();
|
|
2026
|
+
if (!info.streamingData) {
|
|
2027
|
+
await this.bypassAgeGate();
|
|
2028
|
+
info = await this.vidInfo();
|
|
2029
|
+
}
|
|
2030
|
+
if (!info.streamingData) {
|
|
2031
|
+
throw new VideoUnavailable(this.videoId);
|
|
2032
|
+
}
|
|
2033
|
+
return info.streamingData;
|
|
2034
|
+
}
|
|
2035
|
+
// ---------- Availability gates ----------
|
|
2036
|
+
async checkAvailability() {
|
|
2037
|
+
const html = await this.watchHtml();
|
|
2038
|
+
const { status, reasons } = playabilityStatus(html);
|
|
2039
|
+
for (const reason of reasons) {
|
|
2040
|
+
if (status === "UNPLAYABLE") {
|
|
2041
|
+
if (reason === "Join this channel to get access to members-only content like this video, and other exclusive perks.") {
|
|
2042
|
+
throw new MembersOnly(this.videoId);
|
|
2043
|
+
}
|
|
2044
|
+
if (reason === "This live stream recording is not available.") {
|
|
2045
|
+
throw new RecordingUnavailable(this.videoId);
|
|
2046
|
+
}
|
|
2047
|
+
throw new VideoUnavailable(this.videoId);
|
|
2048
|
+
} else if (status === "LOGIN_REQUIRED") {
|
|
2049
|
+
if (reason === "This is a private video. Please sign in to verify that you may see it.") {
|
|
2050
|
+
throw new VideoPrivate(this.videoId);
|
|
2051
|
+
}
|
|
2052
|
+
} else if (status === "ERROR") {
|
|
2053
|
+
if (reason === "Video unavailable") {
|
|
2054
|
+
throw new VideoUnavailable(this.videoId);
|
|
2055
|
+
}
|
|
2056
|
+
} else if (status === "LIVE_STREAM") {
|
|
2057
|
+
throw new LiveStreamError(this.videoId);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
async bypassAgeGate() {
|
|
2062
|
+
const innertube = new InnerTube({ client: "ANDROID_EMBED", useOauth: this.useOauth });
|
|
2063
|
+
const response = await innertube.player(this.videoId);
|
|
2064
|
+
const status = response.playabilityStatus?.status;
|
|
2065
|
+
if (status === "UNPLAYABLE") {
|
|
2066
|
+
throw new AgeRestrictedError(this.videoId);
|
|
2067
|
+
}
|
|
2068
|
+
this.cachedVidInfo = response;
|
|
2069
|
+
}
|
|
2070
|
+
// ---------- Streams ----------
|
|
2071
|
+
async fmtStreams() {
|
|
2072
|
+
await this.checkAvailability();
|
|
2073
|
+
if (this.cachedFmtStreams) return this.cachedFmtStreams;
|
|
2074
|
+
const result = [];
|
|
2075
|
+
const streamingData = await this.streamingData();
|
|
2076
|
+
const manifest = applyDescrambler({
|
|
2077
|
+
formats: streamingData.formats,
|
|
2078
|
+
adaptiveFormats: streamingData.adaptiveFormats
|
|
2079
|
+
});
|
|
2080
|
+
if (!manifest) return result;
|
|
2081
|
+
const vidInfo = await this.vidInfo();
|
|
2082
|
+
const needsCipher = manifestNeedsCipher(manifest);
|
|
2083
|
+
let cipher = null;
|
|
2084
|
+
if (needsCipher) {
|
|
2085
|
+
try {
|
|
2086
|
+
const js = await this.js();
|
|
2087
|
+
cipher = createCipher(js);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
if (!(err instanceof ExtractError)) throw err;
|
|
2090
|
+
this.cachedJs = null;
|
|
2091
|
+
this.cachedJsUrl = null;
|
|
2092
|
+
try {
|
|
2093
|
+
const freshJs = await this.js();
|
|
2094
|
+
cipher = createCipher(freshJs);
|
|
2095
|
+
} catch {
|
|
2096
|
+
cipher = null;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
applySignature(manifest, vidInfo, cipher);
|
|
2101
|
+
this.state.title = await this.title();
|
|
2102
|
+
this.state.duration = await this.length();
|
|
2103
|
+
for (const raw of manifest) {
|
|
2104
|
+
if (!raw.url) continue;
|
|
2105
|
+
if (raw.s !== void 0 && raw.s !== null && !cipher) continue;
|
|
2106
|
+
result.push(new Stream(raw, this.state));
|
|
2107
|
+
}
|
|
2108
|
+
this.cachedFmtStreams = result;
|
|
2109
|
+
return result;
|
|
2110
|
+
}
|
|
2111
|
+
async streams() {
|
|
2112
|
+
await this.checkAvailability();
|
|
2113
|
+
return new StreamQuery(await this.fmtStreams());
|
|
2114
|
+
}
|
|
2115
|
+
// ---------- Captions ----------
|
|
2116
|
+
async captionTracks() {
|
|
2117
|
+
const info = await this.vidInfo();
|
|
2118
|
+
const tracks = info.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
2119
|
+
return tracks.map((t2) => new Caption(t2));
|
|
2120
|
+
}
|
|
2121
|
+
async captions() {
|
|
2122
|
+
return new CaptionQuery(await this.captionTracks());
|
|
2123
|
+
}
|
|
2124
|
+
// ---------- Metadata ----------
|
|
2125
|
+
async title() {
|
|
2126
|
+
if (this.cachedTitle) return this.cachedTitle;
|
|
2127
|
+
const info = await this.vidInfo();
|
|
2128
|
+
const t2 = info.videoDetails?.title;
|
|
2129
|
+
if (!t2) {
|
|
2130
|
+
await this.checkAvailability();
|
|
2131
|
+
throw new PytubeError(`Could not extract title for ${this.watchUrl}`);
|
|
2132
|
+
}
|
|
2133
|
+
this.cachedTitle = t2;
|
|
2134
|
+
return t2;
|
|
2135
|
+
}
|
|
2136
|
+
async description() {
|
|
2137
|
+
return (await this.vidInfo()).videoDetails?.shortDescription ?? null;
|
|
2138
|
+
}
|
|
2139
|
+
async length() {
|
|
2140
|
+
const len = (await this.vidInfo()).videoDetails?.lengthSeconds;
|
|
2141
|
+
return len ? parseInt(len, 10) : 0;
|
|
2142
|
+
}
|
|
2143
|
+
async views() {
|
|
2144
|
+
const v = (await this.vidInfo()).videoDetails?.viewCount;
|
|
2145
|
+
return v ? parseInt(v, 10) : 0;
|
|
2146
|
+
}
|
|
2147
|
+
async rating() {
|
|
2148
|
+
return (await this.vidInfo()).videoDetails?.averageRating ?? null;
|
|
2149
|
+
}
|
|
2150
|
+
async author() {
|
|
2151
|
+
if (this.cachedAuthor) return this.cachedAuthor;
|
|
2152
|
+
this.cachedAuthor = (await this.vidInfo()).videoDetails?.author ?? "unknown";
|
|
2153
|
+
return this.cachedAuthor;
|
|
2154
|
+
}
|
|
2155
|
+
async keywords() {
|
|
2156
|
+
return (await this.vidInfo()).videoDetails?.keywords ?? [];
|
|
2157
|
+
}
|
|
2158
|
+
async channelId() {
|
|
2159
|
+
return (await this.vidInfo()).videoDetails?.channelId ?? null;
|
|
2160
|
+
}
|
|
2161
|
+
async channelUrl() {
|
|
2162
|
+
return `https://www.youtube.com/channel/${await this.channelId()}`;
|
|
2163
|
+
}
|
|
2164
|
+
async thumbnailUrl() {
|
|
2165
|
+
const thumbnails = (await this.vidInfo()).videoDetails?.thumbnail?.thumbnails;
|
|
2166
|
+
if (thumbnails && thumbnails.length > 0) {
|
|
2167
|
+
return thumbnails[thumbnails.length - 1].url;
|
|
2168
|
+
}
|
|
2169
|
+
return `https://img.youtube.com/vi/${this.videoId}/maxresdefault.jpg`;
|
|
2170
|
+
}
|
|
2171
|
+
async publishDate() {
|
|
2172
|
+
if (this.cachedPublishDate !== void 0) return this.cachedPublishDate;
|
|
2173
|
+
this.cachedPublishDate = publishDate(await this.watchHtml());
|
|
2174
|
+
return this.cachedPublishDate;
|
|
2175
|
+
}
|
|
2176
|
+
async metadata() {
|
|
2177
|
+
if (this.cachedMetadata) return this.cachedMetadata;
|
|
2178
|
+
this.cachedMetadata = metadata(await this.initialData());
|
|
2179
|
+
return this.cachedMetadata;
|
|
2180
|
+
}
|
|
2181
|
+
// ---------- Prefetch + sync getters ----------
|
|
2182
|
+
/**
|
|
2183
|
+
* Fetch and cache everything needed for synchronous metadata access.
|
|
2184
|
+
* After this resolves, the *Sync getters and the `streams` getter return
|
|
2185
|
+
* results immediately without further network I/O.
|
|
2186
|
+
*/
|
|
2187
|
+
async prefetch() {
|
|
2188
|
+
await this.watchHtml();
|
|
2189
|
+
await this.vidInfo();
|
|
2190
|
+
await this.title();
|
|
2191
|
+
await this.author();
|
|
2192
|
+
await this.length();
|
|
2193
|
+
await this.fmtStreams();
|
|
2194
|
+
}
|
|
2195
|
+
get titleSync() {
|
|
2196
|
+
if (!this.cachedTitle) throw new Error("Call await yt.prefetch() first.");
|
|
2197
|
+
return this.cachedTitle;
|
|
2198
|
+
}
|
|
2199
|
+
get authorSync() {
|
|
2200
|
+
if (!this.cachedAuthor) throw new Error("Call await yt.prefetch() first.");
|
|
2201
|
+
return this.cachedAuthor;
|
|
2202
|
+
}
|
|
2203
|
+
get streamsSync() {
|
|
2204
|
+
if (!this.cachedFmtStreams) throw new Error("Call await yt.prefetch() first.");
|
|
2205
|
+
return new StreamQuery(this.cachedFmtStreams);
|
|
2206
|
+
}
|
|
2207
|
+
// ---------- Misc ----------
|
|
2208
|
+
registerOnProgressCallback(fn) {
|
|
2209
|
+
this.state.onProgress = fn;
|
|
2210
|
+
}
|
|
2211
|
+
registerOnCompleteCallback(fn) {
|
|
2212
|
+
this.state.onComplete = fn;
|
|
2213
|
+
}
|
|
2214
|
+
toString() {
|
|
2215
|
+
return `<YouTube videoId=${this.videoId}>`;
|
|
2216
|
+
}
|
|
2217
|
+
equals(other) {
|
|
2218
|
+
return other instanceof _YouTube && other.watchUrl === this.watchUrl;
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
|
|
2222
|
+
// src/playlist.ts
|
|
2223
|
+
var Playlist = class {
|
|
2224
|
+
inputUrl;
|
|
2225
|
+
cachedHtml = null;
|
|
2226
|
+
cachedYtcfg = null;
|
|
2227
|
+
cachedInitialData = null;
|
|
2228
|
+
cachedSidebar = null;
|
|
2229
|
+
cachedPlaylistId = null;
|
|
2230
|
+
cachedVideoUrls = null;
|
|
2231
|
+
constructor(url) {
|
|
2232
|
+
this.inputUrl = url;
|
|
2233
|
+
}
|
|
2234
|
+
// ---------- Identifiers ----------
|
|
2235
|
+
get playlistId() {
|
|
2236
|
+
if (this.cachedPlaylistId) return this.cachedPlaylistId;
|
|
2237
|
+
this.cachedPlaylistId = playlistId(this.inputUrl);
|
|
2238
|
+
return this.cachedPlaylistId;
|
|
2239
|
+
}
|
|
2240
|
+
get playlistUrl() {
|
|
2241
|
+
return `https://www.youtube.com/playlist?list=${this.playlistId}`;
|
|
2242
|
+
}
|
|
2243
|
+
// ---------- Network resources ----------
|
|
2244
|
+
async html() {
|
|
2245
|
+
if (this.cachedHtml) return this.cachedHtml;
|
|
2246
|
+
this.cachedHtml = await get(this.playlistUrl);
|
|
2247
|
+
return this.cachedHtml;
|
|
2248
|
+
}
|
|
2249
|
+
async ytcfg() {
|
|
2250
|
+
if (this.cachedYtcfg) return this.cachedYtcfg;
|
|
2251
|
+
this.cachedYtcfg = getYtcfg(await this.html());
|
|
2252
|
+
return this.cachedYtcfg;
|
|
2253
|
+
}
|
|
2254
|
+
async initialData() {
|
|
2255
|
+
if (this.cachedInitialData) return this.cachedInitialData;
|
|
2256
|
+
this.cachedInitialData = await initialData(await this.html());
|
|
2257
|
+
return this.cachedInitialData;
|
|
2258
|
+
}
|
|
2259
|
+
async sidebarInfo() {
|
|
2260
|
+
if (this.cachedSidebar) return this.cachedSidebar;
|
|
2261
|
+
const data = await this.initialData();
|
|
2262
|
+
this.cachedSidebar = data.sidebar?.playlistSidebarRenderer?.items ?? [];
|
|
2263
|
+
return this.cachedSidebar;
|
|
2264
|
+
}
|
|
2265
|
+
async ytApiKey() {
|
|
2266
|
+
return String((await this.ytcfg())["INNERTUBE_API_KEY"]);
|
|
2267
|
+
}
|
|
2268
|
+
// ---------- Pagination ----------
|
|
2269
|
+
async *paginate(untilWatchId) {
|
|
2270
|
+
let [videoUrls, continuation] = this.constructor.extractVideos(
|
|
2271
|
+
JSON.stringify(await this.initialData())
|
|
2272
|
+
);
|
|
2273
|
+
if (untilWatchId !== void 0) {
|
|
2274
|
+
const idx = videoUrls.indexOf(`/watch?v=${untilWatchId}`);
|
|
2275
|
+
if (idx !== -1) {
|
|
2276
|
+
yield videoUrls.slice(0, idx);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
yield videoUrls;
|
|
2281
|
+
while (continuation) {
|
|
2282
|
+
const req = await this.buildContinuationRequest(continuation);
|
|
2283
|
+
const text = await post(req.url, req.data, { headers: req.headers });
|
|
2284
|
+
[videoUrls, continuation] = this.constructor.extractVideos(text);
|
|
2285
|
+
if (untilWatchId !== void 0) {
|
|
2286
|
+
const idx = videoUrls.indexOf(`/watch?v=${untilWatchId}`);
|
|
2287
|
+
if (idx !== -1) {
|
|
2288
|
+
yield videoUrls.slice(0, idx);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
yield videoUrls;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
async buildContinuationRequest(continuation) {
|
|
2296
|
+
return {
|
|
2297
|
+
url: `https://www.youtube.com/youtubei/v1/browse?key=${await this.ytApiKey()}`,
|
|
2298
|
+
headers: {
|
|
2299
|
+
"X-YouTube-Client-Name": "1",
|
|
2300
|
+
"X-YouTube-Client-Version": "2.20200720.00.02"
|
|
2301
|
+
},
|
|
2302
|
+
data: {
|
|
2303
|
+
continuation,
|
|
2304
|
+
context: { client: { clientName: "WEB", clientVersion: "2.20200720.00.02" } }
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
static extractVideos(rawJson) {
|
|
2309
|
+
const data = JSON.parse(rawJson);
|
|
2310
|
+
let videos = null;
|
|
2311
|
+
try {
|
|
2312
|
+
const sectionContents = data.contents?.twoColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents;
|
|
2313
|
+
if (sectionContents) {
|
|
2314
|
+
const importantContent = sectionContents[0]?.itemSectionRenderer?.contents?.[0]?.playlistVideoListRenderer ?? sectionContents[1]?.itemSectionRenderer?.contents?.[0]?.playlistVideoListRenderer;
|
|
2315
|
+
videos = importantContent?.contents ?? null;
|
|
2316
|
+
}
|
|
2317
|
+
} catch {
|
|
2318
|
+
}
|
|
2319
|
+
if (!videos) {
|
|
2320
|
+
const continuationItems = data.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems;
|
|
2321
|
+
if (continuationItems) videos = continuationItems;
|
|
2322
|
+
}
|
|
2323
|
+
if (!videos) return [[], null];
|
|
2324
|
+
let continuation = null;
|
|
2325
|
+
const last = videos[videos.length - 1];
|
|
2326
|
+
const continuationToken = last?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
2327
|
+
if (continuationToken) {
|
|
2328
|
+
continuation = String(continuationToken);
|
|
2329
|
+
videos = videos.slice(0, -1);
|
|
2330
|
+
}
|
|
2331
|
+
const watchPaths = videos.map((v) => v?.playlistVideoRenderer?.videoId).filter((id) => typeof id === "string").map((id) => `/watch?v=${id}`);
|
|
2332
|
+
return [uniqueify(watchPaths), continuation];
|
|
2333
|
+
}
|
|
2334
|
+
// ---------- Public iteration ----------
|
|
2335
|
+
async videoUrls() {
|
|
2336
|
+
if (this.cachedVideoUrls) return this.cachedVideoUrls;
|
|
2337
|
+
const out = [];
|
|
2338
|
+
for await (const page of this.paginate()) {
|
|
2339
|
+
for (const path of page) out.push(`https://www.youtube.com${path}`);
|
|
2340
|
+
}
|
|
2341
|
+
this.cachedVideoUrls = out;
|
|
2342
|
+
return out;
|
|
2343
|
+
}
|
|
2344
|
+
async *videos() {
|
|
2345
|
+
for await (const page of this.paginate()) {
|
|
2346
|
+
for (const path of page) yield new YouTube(`https://www.youtube.com${path}`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
async length() {
|
|
2350
|
+
return (await this.videoUrls()).length;
|
|
2351
|
+
}
|
|
2352
|
+
async *[Symbol.asyncIterator]() {
|
|
2353
|
+
yield* this.videos();
|
|
2354
|
+
}
|
|
2355
|
+
// ---------- Sidebar metadata ----------
|
|
2356
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2357
|
+
async title() {
|
|
2358
|
+
const sidebar = await this.sidebarInfo();
|
|
2359
|
+
return sidebar[0]?.playlistSidebarPrimaryInfoRenderer?.title?.runs?.[0]?.text ?? null;
|
|
2360
|
+
}
|
|
2361
|
+
async description() {
|
|
2362
|
+
const sidebar = await this.sidebarInfo();
|
|
2363
|
+
return sidebar[0]?.playlistSidebarPrimaryInfoRenderer?.description?.simpleText ?? null;
|
|
2364
|
+
}
|
|
2365
|
+
async owner() {
|
|
2366
|
+
const sidebar = await this.sidebarInfo();
|
|
2367
|
+
return sidebar[1]?.playlistSidebarSecondaryInfoRenderer?.videoOwner?.videoOwnerRenderer?.title?.runs?.[0]?.text ?? null;
|
|
2368
|
+
}
|
|
2369
|
+
async ownerId() {
|
|
2370
|
+
const sidebar = await this.sidebarInfo();
|
|
2371
|
+
return sidebar[1]?.playlistSidebarSecondaryInfoRenderer?.videoOwner?.videoOwnerRenderer?.title?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId ?? null;
|
|
2372
|
+
}
|
|
2373
|
+
async ownerUrl() {
|
|
2374
|
+
const id = await this.ownerId();
|
|
2375
|
+
return id ? `https://www.youtube.com/channel/${id}` : null;
|
|
2376
|
+
}
|
|
2377
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
// src/channel.ts
|
|
2381
|
+
var Channel = class extends Playlist {
|
|
2382
|
+
channelUri;
|
|
2383
|
+
channelUrl;
|
|
2384
|
+
videosUrl;
|
|
2385
|
+
playlistsUrl;
|
|
2386
|
+
communityUrl;
|
|
2387
|
+
featuredChannelsUrl;
|
|
2388
|
+
aboutUrl;
|
|
2389
|
+
cachedVideosHtml = null;
|
|
2390
|
+
constructor(url) {
|
|
2391
|
+
super(url);
|
|
2392
|
+
this.channelUri = channelName(url);
|
|
2393
|
+
this.channelUrl = `https://www.youtube.com${this.channelUri}`;
|
|
2394
|
+
this.videosUrl = `${this.channelUrl}/videos`;
|
|
2395
|
+
this.playlistsUrl = `${this.channelUrl}/playlists`;
|
|
2396
|
+
this.communityUrl = `${this.channelUrl}/community`;
|
|
2397
|
+
this.featuredChannelsUrl = `${this.channelUrl}/channels`;
|
|
2398
|
+
this.aboutUrl = `${this.channelUrl}/about`;
|
|
2399
|
+
}
|
|
2400
|
+
async html() {
|
|
2401
|
+
if (this.cachedVideosHtml) return this.cachedVideosHtml;
|
|
2402
|
+
this.cachedVideosHtml = await get(this.videosUrl);
|
|
2403
|
+
return this.cachedVideosHtml;
|
|
2404
|
+
}
|
|
2405
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2406
|
+
async channelName() {
|
|
2407
|
+
const data = await this.initialData();
|
|
2408
|
+
return data?.metadata?.channelMetadataRenderer?.title ?? null;
|
|
2409
|
+
}
|
|
2410
|
+
async channelId() {
|
|
2411
|
+
const data = await this.initialData();
|
|
2412
|
+
return data?.metadata?.channelMetadataRenderer?.externalId ?? null;
|
|
2413
|
+
}
|
|
2414
|
+
async vanityUrl() {
|
|
2415
|
+
const data = await this.initialData();
|
|
2416
|
+
return data?.metadata?.channelMetadataRenderer?.vanityChannelUrl ?? null;
|
|
2417
|
+
}
|
|
2418
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
2419
|
+
static extractVideos(rawJson) {
|
|
2420
|
+
const data = JSON.parse(rawJson);
|
|
2421
|
+
let videos = null;
|
|
2422
|
+
try {
|
|
2423
|
+
videos = data.contents?.twoColumnBrowseResultsRenderer?.tabs?.[1]?.tabRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.gridRenderer?.items ?? null;
|
|
2424
|
+
} catch {
|
|
2425
|
+
videos = null;
|
|
2426
|
+
}
|
|
2427
|
+
if (!videos) {
|
|
2428
|
+
videos = data[1]?.response?.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems ?? data.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems ?? null;
|
|
2429
|
+
}
|
|
2430
|
+
if (!videos) return [[], null];
|
|
2431
|
+
let continuation = null;
|
|
2432
|
+
const last = videos[videos.length - 1];
|
|
2433
|
+
const continuationToken = last?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
2434
|
+
if (continuationToken) {
|
|
2435
|
+
continuation = String(continuationToken);
|
|
2436
|
+
videos = videos.slice(0, -1);
|
|
2437
|
+
}
|
|
2438
|
+
const watchPaths = videos.map((v) => v?.gridVideoRenderer?.videoId).filter((id) => typeof id === "string").map((id) => `/watch?v=${id}`);
|
|
2439
|
+
return [uniqueify(watchPaths), continuation];
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
// src/search.ts
|
|
2444
|
+
var Search = class {
|
|
2445
|
+
query;
|
|
2446
|
+
innertube;
|
|
2447
|
+
cachedResults = null;
|
|
2448
|
+
currentContinuation = null;
|
|
2449
|
+
initialResults = null;
|
|
2450
|
+
constructor(query) {
|
|
2451
|
+
this.query = query;
|
|
2452
|
+
this.innertube = new InnerTube({ client: "WEB" });
|
|
2453
|
+
}
|
|
2454
|
+
async results() {
|
|
2455
|
+
if (this.cachedResults) return this.cachedResults;
|
|
2456
|
+
const [videos, continuation] = await this.fetchAndParse();
|
|
2457
|
+
this.cachedResults = videos;
|
|
2458
|
+
this.currentContinuation = continuation;
|
|
2459
|
+
return videos;
|
|
2460
|
+
}
|
|
2461
|
+
/** Fetch the next page of results and append to the cached results array. */
|
|
2462
|
+
async getNextResults() {
|
|
2463
|
+
if (!this.cachedResults) {
|
|
2464
|
+
await this.results();
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (!this.currentContinuation) {
|
|
2468
|
+
throw new Error("No further results available.");
|
|
2469
|
+
}
|
|
2470
|
+
const [videos, continuation] = await this.fetchAndParse(this.currentContinuation);
|
|
2471
|
+
this.cachedResults.push(...videos);
|
|
2472
|
+
this.currentContinuation = continuation;
|
|
2473
|
+
}
|
|
2474
|
+
async completionSuggestions() {
|
|
2475
|
+
if (!this.initialResults) await this.results();
|
|
2476
|
+
return this.initialResults?.refinements ?? null;
|
|
2477
|
+
}
|
|
2478
|
+
async fetchAndParse(continuation) {
|
|
2479
|
+
const raw = await this.innertube.search(this.query, continuation);
|
|
2480
|
+
if (!this.initialResults) this.initialResults = raw;
|
|
2481
|
+
let sections = null;
|
|
2482
|
+
try {
|
|
2483
|
+
sections = raw.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents ?? null;
|
|
2484
|
+
} catch {
|
|
2485
|
+
sections = null;
|
|
2486
|
+
}
|
|
2487
|
+
if (!sections) {
|
|
2488
|
+
sections = raw.onResponseReceivedCommands?.[0]?.appendContinuationItemsAction?.continuationItems ?? null;
|
|
2489
|
+
}
|
|
2490
|
+
if (!sections) return [[], null];
|
|
2491
|
+
let itemRenderer = null;
|
|
2492
|
+
let continuationRenderer = null;
|
|
2493
|
+
for (const s of sections) {
|
|
2494
|
+
if (s?.itemSectionRenderer) itemRenderer = s.itemSectionRenderer;
|
|
2495
|
+
if (s?.continuationItemRenderer) continuationRenderer = s.continuationItemRenderer;
|
|
2496
|
+
}
|
|
2497
|
+
const nextContinuation = continuationRenderer?.continuationEndpoint?.continuationCommand?.token ?? null;
|
|
2498
|
+
if (!itemRenderer) return [[], nextContinuation];
|
|
2499
|
+
const videos = [];
|
|
2500
|
+
for (const detail of itemRenderer.contents ?? []) {
|
|
2501
|
+
if (detail?.searchPyvRenderer?.ads) continue;
|
|
2502
|
+
if (detail?.shelfRenderer) continue;
|
|
2503
|
+
if (detail?.radioRenderer) continue;
|
|
2504
|
+
if (detail?.playlistRenderer) continue;
|
|
2505
|
+
if (detail?.channelRenderer) continue;
|
|
2506
|
+
if (detail?.horizontalCardListRenderer) continue;
|
|
2507
|
+
if (detail?.didYouMeanRenderer) continue;
|
|
2508
|
+
if (detail?.backgroundPromoRenderer) continue;
|
|
2509
|
+
if (!detail?.videoRenderer) continue;
|
|
2510
|
+
const vr = detail.videoRenderer;
|
|
2511
|
+
const vidId = vr.videoId;
|
|
2512
|
+
videos.push(new YouTube(`https://www.youtube.com/watch?v=${vidId}`));
|
|
2513
|
+
}
|
|
2514
|
+
return [videos, nextContinuation];
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2518
|
+
// src/version.ts
|
|
2519
|
+
var VERSION = "0.1.1";
|
|
2520
|
+
|
|
2521
|
+
export { AgeRestrictedError, Caption, CaptionQuery, Channel, DASH_AUDIO, DASH_VIDEO, ExtractError, HTMLParseError, ITAGS, InnerTube, LiveStreamError, MaxRetriesExceeded, MembersOnly, PROGRESSIVE_VIDEO, Playlist, PytubeError, RecordingUnavailable, RegexMatchError, Search, Stream, StreamQuery, VERSION, VideoPrivate, VideoRegionBlocked, VideoUnavailable, YouTube, YouTubeMetadata, getFormatProfile };
|
|
2522
|
+
//# sourceMappingURL=index.js.map
|
|
2523
|
+
//# sourceMappingURL=index.js.map
|