@oreohq/ytdl-core 4.15.1
Sign up to get free protection for your applications and to get access to all the features.
- 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;
|