@oreohq/ytdl-core 4.15.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 +21 -0
- package/README.md +209 -0
- package/lib/agent.js +100 -0
- package/lib/cache.js +54 -0
- package/lib/format-utils.js +218 -0
- package/lib/formats.js +564 -0
- package/lib/index.js +228 -0
- package/lib/info-extras.js +362 -0
- package/lib/info.js +580 -0
- package/lib/sig.js +280 -0
- package/lib/url-utils.js +87 -0
- package/lib/utils.js +437 -0
- package/package.json +46 -0
- package/typings/index.d.ts +1016 -0
package/lib/info.js
ADDED
@@ -0,0 +1,580 @@
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
2
|
+
const sax = require("sax");
|
3
|
+
const utils = require("./utils");
|
4
|
+
// Forces Node JS version of setTimeout for Electron based applications
|
5
|
+
const { setTimeout } = require("timers");
|
6
|
+
const formatUtils = require("./format-utils");
|
7
|
+
const urlUtils = require("./url-utils");
|
8
|
+
const extras = require("./info-extras");
|
9
|
+
const Cache = require("./cache");
|
10
|
+
const sig = require("./sig");
|
11
|
+
|
12
|
+
const BASE_URL = "https://www.youtube.com/watch?v=";
|
13
|
+
|
14
|
+
// Cached for storing basic/full info.
|
15
|
+
exports.cache = new Cache();
|
16
|
+
exports.watchPageCache = new Cache();
|
17
|
+
|
18
|
+
// List of URLs that show up in `notice_url` for age restricted videos.
|
19
|
+
const AGE_RESTRICTED_URLS = ["support.google.com/youtube/?p=age_restrictions", "youtube.com/t/community_guidelines"];
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Gets info from a video without getting additional formats.
|
23
|
+
*
|
24
|
+
* @param {string} id
|
25
|
+
* @param {Object} options
|
26
|
+
* @returns {Promise<Object>}
|
27
|
+
*/
|
28
|
+
exports.getBasicInfo = async (id, options) => {
|
29
|
+
utils.applyIPv6Rotations(options);
|
30
|
+
utils.applyDefaultHeaders(options);
|
31
|
+
utils.applyDefaultAgent(options);
|
32
|
+
utils.applyOldLocalAddress(options);
|
33
|
+
const retryOptions = Object.assign({}, options.requestOptions);
|
34
|
+
const { jar, dispatcher } = options.agent;
|
35
|
+
utils.setPropInsensitive(
|
36
|
+
options.requestOptions.headers,
|
37
|
+
"cookie",
|
38
|
+
jar.getCookieStringSync("https://www.youtube.com"),
|
39
|
+
);
|
40
|
+
options.requestOptions.dispatcher = dispatcher;
|
41
|
+
const info = await retryFunc(getWatchHTMLPage, [id, options], retryOptions);
|
42
|
+
|
43
|
+
const playErr = utils.playError(info.player_response);
|
44
|
+
if (playErr) throw playErr;
|
45
|
+
|
46
|
+
Object.assign(info, {
|
47
|
+
// Replace with formats from iosPlayerResponse
|
48
|
+
// formats: parseFormats(info.player_response),
|
49
|
+
related_videos: extras.getRelatedVideos(info),
|
50
|
+
});
|
51
|
+
|
52
|
+
// Add additional properties to info.
|
53
|
+
const media = extras.getMedia(info);
|
54
|
+
const additional = {
|
55
|
+
author: extras.getAuthor(info),
|
56
|
+
media,
|
57
|
+
likes: extras.getLikes(info),
|
58
|
+
age_restricted: !!(
|
59
|
+
media && AGE_RESTRICTED_URLS.some(url => Object.values(media).some(v => typeof v === "string" && v.includes(url)))
|
60
|
+
),
|
61
|
+
|
62
|
+
// Give the standard link to the video.
|
63
|
+
video_url: BASE_URL + id,
|
64
|
+
storyboards: extras.getStoryboards(info),
|
65
|
+
chapters: extras.getChapters(info),
|
66
|
+
};
|
67
|
+
|
68
|
+
info.videoDetails = extras.cleanVideoDetails(
|
69
|
+
Object.assign(
|
70
|
+
{},
|
71
|
+
info.player_response &&
|
72
|
+
info.player_response.microformat &&
|
73
|
+
info.player_response.microformat.playerMicroformatRenderer,
|
74
|
+
info.player_response && info.player_response.videoDetails,
|
75
|
+
additional,
|
76
|
+
),
|
77
|
+
info,
|
78
|
+
);
|
79
|
+
|
80
|
+
return info;
|
81
|
+
};
|
82
|
+
|
83
|
+
const getWatchHTMLURL = (id, options) =>
|
84
|
+
`${BASE_URL + id}&hl=${options.lang || "en"}&bpctr=${Math.ceil(Date.now() / 1000)}&has_verified=1`;
|
85
|
+
const getWatchHTMLPageBody = (id, options) => {
|
86
|
+
const url = getWatchHTMLURL(id, options);
|
87
|
+
return exports.watchPageCache.getOrSet(url, () => utils.request(url, options));
|
88
|
+
};
|
89
|
+
|
90
|
+
const EMBED_URL = "https://www.youtube.com/embed/";
|
91
|
+
const getEmbedPageBody = (id, options) => {
|
92
|
+
const embedUrl = `${EMBED_URL + id}?hl=${options.lang || "en"}`;
|
93
|
+
return utils.request(embedUrl, options);
|
94
|
+
};
|
95
|
+
|
96
|
+
const getHTML5player = body => {
|
97
|
+
let html5playerRes =
|
98
|
+
/<script\s+src="([^"]+)"(?:\s+type="text\/javascript")?\s+name="player_ias\/base"\s*>|"jsUrl":"([^"]+)"/.exec(body);
|
99
|
+
return html5playerRes ? html5playerRes[1] || html5playerRes[2] : null;
|
100
|
+
};
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Given a function, calls it with `args` until it's successful,
|
104
|
+
* or until it encounters an unrecoverable error.
|
105
|
+
* Currently, any error from miniget is considered unrecoverable. Errors such as
|
106
|
+
* too many redirects, invalid URL, status code 404, status code 502.
|
107
|
+
*
|
108
|
+
* @param {Function} func
|
109
|
+
* @param {Array.<Object>} args
|
110
|
+
* @param {Object} options
|
111
|
+
* @param {number} options.maxRetries
|
112
|
+
* @param {Object} options.backoff
|
113
|
+
* @param {number} options.backoff.inc
|
114
|
+
*/
|
115
|
+
const retryFunc = async (func, args, options) => {
|
116
|
+
let currentTry = 0,
|
117
|
+
result;
|
118
|
+
if (!options.maxRetries) options.maxRetries = 3;
|
119
|
+
if (!options.backoff) options.backoff = { inc: 500, max: 5000 };
|
120
|
+
while (currentTry <= options.maxRetries) {
|
121
|
+
try {
|
122
|
+
result = await func(...args);
|
123
|
+
break;
|
124
|
+
} catch (err) {
|
125
|
+
if ((err && err.statusCode < 500) || currentTry >= options.maxRetries) throw err;
|
126
|
+
let wait = Math.min(++currentTry * options.backoff.inc, options.backoff.max);
|
127
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
128
|
+
}
|
129
|
+
}
|
130
|
+
return result;
|
131
|
+
};
|
132
|
+
|
133
|
+
const jsonClosingChars = /^[)\]}'\s]+/;
|
134
|
+
const parseJSON = (source, varName, json) => {
|
135
|
+
if (!json || typeof json === "object") {
|
136
|
+
return json;
|
137
|
+
} else {
|
138
|
+
try {
|
139
|
+
json = json.replace(jsonClosingChars, "");
|
140
|
+
return JSON.parse(json);
|
141
|
+
} catch (err) {
|
142
|
+
throw Error(`Error parsing ${varName} in ${source}: ${err.message}`);
|
143
|
+
}
|
144
|
+
}
|
145
|
+
};
|
146
|
+
|
147
|
+
const findJSON = (source, varName, body, left, right, prependJSON) => {
|
148
|
+
let jsonStr = utils.between(body, left, right);
|
149
|
+
if (!jsonStr) {
|
150
|
+
throw Error(`Could not find ${varName} in ${source}`);
|
151
|
+
}
|
152
|
+
return parseJSON(source, varName, utils.cutAfterJS(`${prependJSON}${jsonStr}`));
|
153
|
+
};
|
154
|
+
|
155
|
+
const findPlayerResponse = (source, info) => {
|
156
|
+
const player_response =
|
157
|
+
info &&
|
158
|
+
((info.args && info.args.player_response) ||
|
159
|
+
info.player_response ||
|
160
|
+
info.playerResponse ||
|
161
|
+
info.embedded_player_response);
|
162
|
+
return parseJSON(source, "player_response", player_response);
|
163
|
+
};
|
164
|
+
|
165
|
+
const getWatchHTMLPage = async (id, options) => {
|
166
|
+
let body = await getWatchHTMLPageBody(id, options);
|
167
|
+
let info = { page: "watch" };
|
168
|
+
try {
|
169
|
+
try {
|
170
|
+
info.player_response =
|
171
|
+
utils.tryParseBetween(body, "var ytInitialPlayerResponse = ", "}};", "", "}}") ||
|
172
|
+
utils.tryParseBetween(body, "var ytInitialPlayerResponse = ", ";var") ||
|
173
|
+
utils.tryParseBetween(body, "var ytInitialPlayerResponse = ", ";</script>") ||
|
174
|
+
findJSON("watch.html", "player_response", body, /\bytInitialPlayerResponse\s*=\s*\{/i, "</script>", "{");
|
175
|
+
} catch (_e) {
|
176
|
+
let args = findJSON("watch.html", "player_response", body, /\bytplayer\.config\s*=\s*{/, "</script>", "{");
|
177
|
+
info.player_response = findPlayerResponse("watch.html", args);
|
178
|
+
}
|
179
|
+
|
180
|
+
info.response =
|
181
|
+
utils.tryParseBetween(body, "var ytInitialData = ", "}};", "", "}}") ||
|
182
|
+
utils.tryParseBetween(body, "var ytInitialData = ", ";</script>") ||
|
183
|
+
utils.tryParseBetween(body, 'window["ytInitialData"] = ', "}};", "", "}}") ||
|
184
|
+
utils.tryParseBetween(body, 'window["ytInitialData"] = ', ";</script>") ||
|
185
|
+
findJSON("watch.html", "response", body, /\bytInitialData("\])?\s*=\s*\{/i, "</script>", "{");
|
186
|
+
info.html5player = getHTML5player(body);
|
187
|
+
} catch (_) {
|
188
|
+
throw Error(
|
189
|
+
"Error when parsing watch.html, maybe YouTube made a change.\n" +
|
190
|
+
`Please report this issue with the "${utils.saveDebugFile(
|
191
|
+
"watch.html",
|
192
|
+
body,
|
193
|
+
)}" file on https://github.com/distubejs/ytdl-core/issues.`,
|
194
|
+
);
|
195
|
+
}
|
196
|
+
return info;
|
197
|
+
};
|
198
|
+
|
199
|
+
/**
|
200
|
+
* @param {Object} player_response
|
201
|
+
* @returns {Array.<Object>}
|
202
|
+
*/
|
203
|
+
const parseFormats = player_response => {
|
204
|
+
let formats = [];
|
205
|
+
if (player_response && player_response.streamingData) {
|
206
|
+
formats = formats
|
207
|
+
.concat(player_response.streamingData.formats || [])
|
208
|
+
.concat(player_response.streamingData.adaptiveFormats || []);
|
209
|
+
}
|
210
|
+
return formats;
|
211
|
+
};
|
212
|
+
|
213
|
+
const parseAdditionalManifests = (player_response, options) => {
|
214
|
+
let streamingData = player_response && player_response.streamingData,
|
215
|
+
manifests = [];
|
216
|
+
if (streamingData) {
|
217
|
+
if (streamingData.dashManifestUrl) {
|
218
|
+
manifests.push(getDashManifest(streamingData.dashManifestUrl, options));
|
219
|
+
}
|
220
|
+
if (streamingData.hlsManifestUrl) {
|
221
|
+
manifests.push(getM3U8(streamingData.hlsManifestUrl, options));
|
222
|
+
}
|
223
|
+
}
|
224
|
+
return manifests;
|
225
|
+
};
|
226
|
+
|
227
|
+
// TODO: Clean up this function for readability and support more clients
|
228
|
+
/**
|
229
|
+
* Gets info from a video additional formats and deciphered URLs.
|
230
|
+
*
|
231
|
+
* @param {string} id
|
232
|
+
* @param {Object} options
|
233
|
+
* @returns {Promise<Object>}
|
234
|
+
*/
|
235
|
+
exports.getInfo = async (id, options) => {
|
236
|
+
utils.applyIPv6Rotations(options);
|
237
|
+
utils.applyDefaultHeaders(options);
|
238
|
+
utils.applyDefaultAgent(options);
|
239
|
+
utils.applyOldLocalAddress(options);
|
240
|
+
utils.applyPlayerClients(options);
|
241
|
+
const info = await exports.getBasicInfo(id, options);
|
242
|
+
let funcs = [];
|
243
|
+
|
244
|
+
// Fill in HTML5 player URL
|
245
|
+
info.html5player =
|
246
|
+
info.html5player ||
|
247
|
+
getHTML5player(await getWatchHTMLPageBody(id, options)) ||
|
248
|
+
getHTML5player(await getEmbedPageBody(id, options));
|
249
|
+
|
250
|
+
if (!info.html5player) {
|
251
|
+
throw Error("Unable to find html5player file");
|
252
|
+
}
|
253
|
+
|
254
|
+
const html5player = new URL(info.html5player, BASE_URL).toString();
|
255
|
+
|
256
|
+
try {
|
257
|
+
if (info.videoDetails.age_restricted) throw Error("Cannot download age restricted videos with mobile clients");
|
258
|
+
const promises = [];
|
259
|
+
if (options.playerClients.includes("WEB_CREATOR")) promises.push(fetchWebCreatorPlayer(id, html5player, options));
|
260
|
+
if (options.playerClients.includes("IOS")) promises.push(fetchIosJsonPlayer(id, options));
|
261
|
+
if (options.playerClients.includes("ANDROID")) promises.push(fetchAndroidJsonPlayer(id, options));
|
262
|
+
const responses = await Promise.allSettled(promises);
|
263
|
+
info.formats = [].concat(...responses.map(r => parseFormats(r.value)));
|
264
|
+
if (info.formats.length === 0) throw new Error("Player JSON API failed");
|
265
|
+
|
266
|
+
funcs.push(sig.decipherFormats(info.formats, html5player, options));
|
267
|
+
|
268
|
+
for (let resp of responses) {
|
269
|
+
if (resp.value) {
|
270
|
+
funcs.push(...parseAdditionalManifests(resp.value, options));
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
if (options.playerClients.includes("WEB")) {
|
275
|
+
funcs.push(sig.decipherFormats(parseFormats(info.player_response), html5player, options));
|
276
|
+
funcs.push(...parseAdditionalManifests(info.player_response));
|
277
|
+
}
|
278
|
+
} catch (_) {
|
279
|
+
funcs.push(sig.decipherFormats(parseFormats(info.player_response), html5player, options));
|
280
|
+
funcs.push(...parseAdditionalManifests(info.player_response));
|
281
|
+
}
|
282
|
+
|
283
|
+
let results = await Promise.all(funcs);
|
284
|
+
info.formats = Object.values(Object.assign({}, ...results));
|
285
|
+
info.formats = info.formats.map(formatUtils.addFormatMeta);
|
286
|
+
info.formats.sort(formatUtils.sortFormats);
|
287
|
+
|
288
|
+
info.full = true;
|
289
|
+
return info;
|
290
|
+
};
|
291
|
+
|
292
|
+
const getPlaybackContext = async (html5player, options) => {
|
293
|
+
const body = await utils.request(html5player, options);
|
294
|
+
let mo = body.match(/signatureTimestamp:(\d+)/);
|
295
|
+
|
296
|
+
return {
|
297
|
+
contentPlaybackContext: {
|
298
|
+
html5Preference: "HTML5_PREF_WANTS",
|
299
|
+
signatureTimestamp: mo ? mo[1] : undefined,
|
300
|
+
},
|
301
|
+
};
|
302
|
+
};
|
303
|
+
|
304
|
+
const LOCALE = { hl: "en", timeZone: "UTC", utcOffsetMinutes: 0 },
|
305
|
+
CHECK_FLAGS = { contentCheckOk: true, racyCheckOk: true };
|
306
|
+
|
307
|
+
const WEB_CREATOR_CONTEXT = {
|
308
|
+
client: {
|
309
|
+
clientName: "WEB_CREATOR",
|
310
|
+
clientVersion: "1.20241023.00.01",
|
311
|
+
...LOCALE,
|
312
|
+
},
|
313
|
+
};
|
314
|
+
|
315
|
+
const fetchWebCreatorPlayer = async (videoId, html5player, options) => {
|
316
|
+
const payload = {
|
317
|
+
context: WEB_CREATOR_CONTEXT,
|
318
|
+
videoId,
|
319
|
+
playbackContext: await getPlaybackContext(html5player, options),
|
320
|
+
...CHECK_FLAGS,
|
321
|
+
};
|
322
|
+
|
323
|
+
return await playerAPI(videoId, payload, undefined, options);
|
324
|
+
};
|
325
|
+
|
326
|
+
const playerAPI = async (videoId, payload, userAgent, options) => {
|
327
|
+
const { jar, dispatcher } = options.agent;
|
328
|
+
const opts = {
|
329
|
+
requestOptions: {
|
330
|
+
method: "POST",
|
331
|
+
dispatcher,
|
332
|
+
query: {
|
333
|
+
prettyPrint: false,
|
334
|
+
t: utils.generateClientPlaybackNonce(12),
|
335
|
+
id: videoId,
|
336
|
+
},
|
337
|
+
headers: {
|
338
|
+
"Content-Type": "application/json",
|
339
|
+
cookie: jar.getCookieStringSync("https://www.youtube.com"),
|
340
|
+
"User-Agent": userAgent,
|
341
|
+
"X-Goog-Api-Format-Version": "2",
|
342
|
+
},
|
343
|
+
body: JSON.stringify(payload),
|
344
|
+
},
|
345
|
+
};
|
346
|
+
const response = await utils.request("https://youtubei.googleapis.com/youtubei/v1/player", opts);
|
347
|
+
const playErr = utils.playError(response);
|
348
|
+
if (playErr) throw playErr;
|
349
|
+
if (!response.videoDetails || videoId !== response.videoDetails.videoId) {
|
350
|
+
const err = new Error("Malformed response from YouTube");
|
351
|
+
err.response = response;
|
352
|
+
throw err;
|
353
|
+
}
|
354
|
+
return response;
|
355
|
+
};
|
356
|
+
|
357
|
+
const IOS_CLIENT_VERSION = "19.42.1",
|
358
|
+
IOS_DEVICE_MODEL = "iPhone16,2",
|
359
|
+
IOS_USER_AGENT_VERSION = "17_5_1",
|
360
|
+
IOS_OS_VERSION = "17.5.1.21F90";
|
361
|
+
|
362
|
+
const fetchIosJsonPlayer = async (videoId, options) => {
|
363
|
+
const payload = {
|
364
|
+
videoId,
|
365
|
+
cpn: utils.generateClientPlaybackNonce(16),
|
366
|
+
contentCheckOk: true,
|
367
|
+
racyCheckOk: true,
|
368
|
+
context: {
|
369
|
+
client: {
|
370
|
+
clientName: "IOS",
|
371
|
+
clientVersion: IOS_CLIENT_VERSION,
|
372
|
+
deviceMake: "Apple",
|
373
|
+
deviceModel: IOS_DEVICE_MODEL,
|
374
|
+
platform: "MOBILE",
|
375
|
+
osName: "iOS",
|
376
|
+
osVersion: IOS_OS_VERSION,
|
377
|
+
hl: "en",
|
378
|
+
gl: "US",
|
379
|
+
utcOffsetMinutes: -240,
|
380
|
+
},
|
381
|
+
request: {
|
382
|
+
internalExperimentFlags: [],
|
383
|
+
useSsl: true,
|
384
|
+
},
|
385
|
+
user: {
|
386
|
+
lockedSafetyMode: false,
|
387
|
+
},
|
388
|
+
},
|
389
|
+
};
|
390
|
+
|
391
|
+
const { jar, dispatcher } = options.agent;
|
392
|
+
const opts = {
|
393
|
+
requestOptions: {
|
394
|
+
method: "POST",
|
395
|
+
dispatcher,
|
396
|
+
query: {
|
397
|
+
prettyPrint: false,
|
398
|
+
t: utils.generateClientPlaybackNonce(12),
|
399
|
+
id: videoId,
|
400
|
+
},
|
401
|
+
headers: {
|
402
|
+
"Content-Type": "application/json",
|
403
|
+
cookie: jar.getCookieStringSync("https://www.youtube.com"),
|
404
|
+
"User-Agent": `com.google.ios.youtube/${IOS_CLIENT_VERSION}(${
|
405
|
+
IOS_DEVICE_MODEL
|
406
|
+
}; U; CPU iOS ${IOS_USER_AGENT_VERSION} like Mac OS X; en_US)`,
|
407
|
+
"X-Goog-Api-Format-Version": "2",
|
408
|
+
},
|
409
|
+
body: JSON.stringify(payload),
|
410
|
+
},
|
411
|
+
};
|
412
|
+
const response = await utils.request("https://youtubei.googleapis.com/youtubei/v1/player", opts);
|
413
|
+
const playErr = utils.playError(response);
|
414
|
+
if (playErr) throw playErr;
|
415
|
+
if (!response.videoDetails || videoId !== response.videoDetails.videoId) {
|
416
|
+
const err = new Error("Malformed response from YouTube");
|
417
|
+
err.response = response;
|
418
|
+
throw err;
|
419
|
+
}
|
420
|
+
return response;
|
421
|
+
};
|
422
|
+
|
423
|
+
const ANDROID_CLIENT_VERSION = "19.30.36",
|
424
|
+
ANDROID_OS_VERSION = "14",
|
425
|
+
ANDROID_SDK_VERSION = "34";
|
426
|
+
|
427
|
+
const fetchAndroidJsonPlayer = async (videoId, options) => {
|
428
|
+
const payload = {
|
429
|
+
videoId,
|
430
|
+
cpn: utils.generateClientPlaybackNonce(16),
|
431
|
+
contentCheckOk: true,
|
432
|
+
racyCheckOk: true,
|
433
|
+
context: {
|
434
|
+
client: {
|
435
|
+
clientName: "ANDROID",
|
436
|
+
clientVersion: ANDROID_CLIENT_VERSION,
|
437
|
+
platform: "MOBILE",
|
438
|
+
osName: "Android",
|
439
|
+
osVersion: ANDROID_OS_VERSION,
|
440
|
+
androidSdkVersion: ANDROID_SDK_VERSION,
|
441
|
+
hl: "en",
|
442
|
+
gl: "US",
|
443
|
+
utcOffsetMinutes: -240,
|
444
|
+
},
|
445
|
+
request: {
|
446
|
+
internalExperimentFlags: [],
|
447
|
+
useSsl: true,
|
448
|
+
},
|
449
|
+
user: {
|
450
|
+
lockedSafetyMode: false,
|
451
|
+
},
|
452
|
+
},
|
453
|
+
};
|
454
|
+
|
455
|
+
const { jar, dispatcher } = options.agent;
|
456
|
+
const opts = {
|
457
|
+
requestOptions: {
|
458
|
+
method: "POST",
|
459
|
+
dispatcher,
|
460
|
+
query: {
|
461
|
+
prettyPrint: false,
|
462
|
+
t: utils.generateClientPlaybackNonce(12),
|
463
|
+
id: videoId,
|
464
|
+
},
|
465
|
+
headers: {
|
466
|
+
"Content-Type": "application/json",
|
467
|
+
cookie: jar.getCookieStringSync("https://www.youtube.com"),
|
468
|
+
"User-Agent": `com.google.android.youtube/${
|
469
|
+
ANDROID_CLIENT_VERSION
|
470
|
+
} (Linux; U; Android ${ANDROID_OS_VERSION}; en_US) gzip`,
|
471
|
+
"X-Goog-Api-Format-Version": "2",
|
472
|
+
},
|
473
|
+
body: JSON.stringify(payload),
|
474
|
+
},
|
475
|
+
};
|
476
|
+
const response = await utils.request("https://youtubei.googleapis.com/youtubei/v1/player", opts);
|
477
|
+
const playErr = utils.playError(response);
|
478
|
+
if (playErr) throw playErr;
|
479
|
+
if (!response.videoDetails || videoId !== response.videoDetails.videoId) {
|
480
|
+
const err = new Error("Malformed response from YouTube");
|
481
|
+
err.response = response;
|
482
|
+
throw err;
|
483
|
+
}
|
484
|
+
return response;
|
485
|
+
};
|
486
|
+
|
487
|
+
/**
|
488
|
+
* Gets additional DASH formats.
|
489
|
+
*
|
490
|
+
* @param {string} url
|
491
|
+
* @param {Object} options
|
492
|
+
* @returns {Promise<Array.<Object>>}
|
493
|
+
*/
|
494
|
+
const getDashManifest = (url, options) =>
|
495
|
+
new Promise((resolve, reject) => {
|
496
|
+
let formats = {};
|
497
|
+
const parser = sax.parser(false);
|
498
|
+
parser.onerror = reject;
|
499
|
+
let adaptationSet;
|
500
|
+
parser.onopentag = node => {
|
501
|
+
if (node.name === "ADAPTATIONSET") {
|
502
|
+
adaptationSet = node.attributes;
|
503
|
+
} else if (node.name === "REPRESENTATION") {
|
504
|
+
const itag = parseInt(node.attributes.ID);
|
505
|
+
if (!isNaN(itag)) {
|
506
|
+
formats[url] = Object.assign(
|
507
|
+
{
|
508
|
+
itag,
|
509
|
+
url,
|
510
|
+
bitrate: parseInt(node.attributes.BANDWIDTH),
|
511
|
+
mimeType: `${adaptationSet.MIMETYPE}; codecs="${node.attributes.CODECS}"`,
|
512
|
+
},
|
513
|
+
node.attributes.HEIGHT
|
514
|
+
? {
|
515
|
+
width: parseInt(node.attributes.WIDTH),
|
516
|
+
height: parseInt(node.attributes.HEIGHT),
|
517
|
+
fps: parseInt(node.attributes.FRAMERATE),
|
518
|
+
}
|
519
|
+
: {
|
520
|
+
audioSampleRate: node.attributes.AUDIOSAMPLINGRATE,
|
521
|
+
},
|
522
|
+
);
|
523
|
+
}
|
524
|
+
}
|
525
|
+
};
|
526
|
+
parser.onend = () => {
|
527
|
+
resolve(formats);
|
528
|
+
};
|
529
|
+
utils
|
530
|
+
.request(new URL(url, BASE_URL).toString(), options)
|
531
|
+
.then(res => {
|
532
|
+
parser.write(res);
|
533
|
+
parser.close();
|
534
|
+
})
|
535
|
+
.catch(reject);
|
536
|
+
});
|
537
|
+
|
538
|
+
/**
|
539
|
+
* Gets additional formats.
|
540
|
+
*
|
541
|
+
* @param {string} url
|
542
|
+
* @param {Object} options
|
543
|
+
* @returns {Promise<Array.<Object>>}
|
544
|
+
*/
|
545
|
+
const getM3U8 = async (url, options) => {
|
546
|
+
url = new URL(url, BASE_URL);
|
547
|
+
const body = await utils.request(url.toString(), options);
|
548
|
+
let formats = {};
|
549
|
+
body
|
550
|
+
.split("\n")
|
551
|
+
.filter(line => /^https?:\/\//.test(line))
|
552
|
+
.forEach(line => {
|
553
|
+
const itag = parseInt(line.match(/\/itag\/(\d+)\//)[1]);
|
554
|
+
formats[line] = { itag, url: line };
|
555
|
+
});
|
556
|
+
return formats;
|
557
|
+
};
|
558
|
+
|
559
|
+
// Cache get info functions.
|
560
|
+
// In case a user wants to get a video's info before downloading.
|
561
|
+
for (let funcName of ["getBasicInfo", "getInfo"]) {
|
562
|
+
/**
|
563
|
+
* @param {string} link
|
564
|
+
* @param {Object} options
|
565
|
+
* @returns {Promise<Object>}
|
566
|
+
*/
|
567
|
+
const func = exports[funcName];
|
568
|
+
exports[funcName] = async (link, options = {}) => {
|
569
|
+
utils.checkForUpdates();
|
570
|
+
let id = await urlUtils.getVideoID(link);
|
571
|
+
const key = [funcName, id, options.lang].join("-");
|
572
|
+
return exports.cache.getOrSet(key, () => func(id, options));
|
573
|
+
};
|
574
|
+
}
|
575
|
+
|
576
|
+
// Export a few helpers.
|
577
|
+
exports.validateID = urlUtils.validateID;
|
578
|
+
exports.validateURL = urlUtils.validateURL;
|
579
|
+
exports.getURLVideoID = urlUtils.getURLVideoID;
|
580
|
+
exports.getVideoID = urlUtils.getVideoID;
|