gaber-elsayed

info.js بندريتا بوت

Sep 14th, 2021
93
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.37 KB | None | 0 0
  1. const querystring = require('querystring');
  2. const sax = require('sax');
  3. const miniget = require('miniget');
  4. const utils = require('./utils');
  5. // Forces Node JS version of setTimeout for Electron based applications
  6. const { setTimeout } = require('timers');
  7. const formatUtils = require('./format-utils');
  8. const urlUtils = require('./url-utils');
  9. const extras = require('./info-extras');
  10. const sig = require('./sig');
  11. const Cache = require('./cache');
  12.  
  13.  
  14. const BASE_URL = 'https://www.youtube.com/watch?v=';
  15.  
  16.  
  17. // Cached for storing basic/full info.
  18. exports.cache = new Cache();
  19. exports.cookieCache = new Cache(1000 * 60 * 60 * 24);
  20. exports.watchPageCache = new Cache();
  21.  
  22.  
  23. // Special error class used to determine if an error is unrecoverable,
  24. // as in, ytdl-core should not try again to fetch the video metadata.
  25. // In this case, the video is usually unavailable in some way.
  26. class UnrecoverableError extends Error {}
  27.  
  28.  
  29. // List of URLs that show up in `notice_url` for age restricted videos.
  30. const AGE_RESTRICTED_URLS = [
  31. 'support.google.com/youtube/?p=age_restrictions',
  32. 'youtube.com/t/community_guidelines',
  33. ];
  34.  
  35.  
  36. /**
  37. * Gets info from a video without getting additional formats.
  38. *
  39. * @param {string} id
  40. * @param {Object} options
  41. * @returns {Promise<Object>}
  42. */
  43. exports.getBasicInfo = async(id, options) => {
  44. const retryOptions = Object.assign({}, miniget.defaultOptions, options.requestOptions);
  45. options.requestOptions = Object.assign({}, options.requestOptions, {});
  46. options.requestOptions.headers = Object.assign({},
  47. {
  48. // eslint-disable-next-line max-len
  49. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36',
  50. }, options.requestOptions.headers);
  51. const validate = info => {
  52. let playErr = utils.playError(info.player_response, ['ERROR'], UnrecoverableError);
  53. let privateErr = privateVideoError(info.player_response);
  54. if (playErr || privateErr) {
  55. throw playErr || privateErr;
  56. }
  57. return info && info.player_response && (
  58. info.player_response.streamingData || isRental(info.player_response) || isNotYetBroadcasted(info.player_response)
  59. );
  60. };
  61. let info = await pipeline([id, options], validate, retryOptions, [
  62. getWatchHTMLPage,
  63. //getWatchJSONPage,
  64. //getVideoInfoPage,
  65. ]);
  66.  
  67. Object.assign(info, {
  68. formats: parseFormats(info.player_response),
  69. related_videos: extras.getRelatedVideos(info),
  70. });
  71.  
  72. // Add additional properties to info.
  73. const media = extras.getMedia(info);
  74. const additional = {
  75. author: extras.getAuthor(info),
  76. media,
  77. likes: extras.getLikes(info),
  78. dislikes: extras.getDislikes(info),
  79. age_restricted: !!(media && media.notice_url && AGE_RESTRICTED_URLS.some(url => media.notice_url.includes(url))),
  80.  
  81. // Give the standard link to the video.
  82. video_url: BASE_URL + id,
  83. storyboards: extras.getStoryboards(info),
  84. chapters: extras.getChapters(info),
  85. };
  86.  
  87. info.videoDetails = extras.cleanVideoDetails(Object.assign({},
  88. info.player_response && info.player_response.microformat &&
  89. info.player_response.microformat.playerMicroformatRenderer,
  90. info.player_response && info.player_response.videoDetails, additional), info);
  91.  
  92. return info;
  93. };
  94.  
  95. const privateVideoError = player_response => {
  96. let playability = player_response && player_response.playabilityStatus;
  97. if (playability && playability.status === 'LOGIN_REQUIRED' && playability.messages &&
  98. playability.messages.filter(m => /This is a private video/.test(m)).length) {
  99. return new UnrecoverableError(playability.reason || (playability.messages && playability.messages[0]));
  100. } else {
  101. return null;
  102. }
  103. };
  104.  
  105.  
  106. const isRental = player_response => {
  107. let playability = player_response.playabilityStatus;
  108. return playability && playability.status === 'UNPLAYABLE' &&
  109. playability.errorScreen && playability.errorScreen.playerLegacyDesktopYpcOfferRenderer;
  110. };
  111.  
  112.  
  113. const isNotYetBroadcasted = player_response => {
  114. let playability = player_response.playabilityStatus;
  115. return playability && playability.status === 'LIVE_STREAM_OFFLINE';
  116. };
  117.  
  118.  
  119. const getWatchHTMLURL = (id, options) => `${BASE_URL + id}&hl=${options.lang || 'en'}`;
  120. const getWatchHTMLPageBody = (id, options) => {
  121. const url = getWatchHTMLURL(id, options);
  122. return exports.watchPageCache.getOrSet(url, () => utils.exposedMiniget(url, options).text());
  123. };
  124.  
  125.  
  126. const EMBED_URL = 'https://www.youtube.com/embed/';
  127. const getEmbedPageBody = (id, options) => {
  128. const embedUrl = `${EMBED_URL + id}?hl=${options.lang || 'en'}`;
  129. return utils.exposedMiniget(embedUrl, options).text();
  130. };
  131.  
  132.  
  133. const getHTML5player = body => {
  134. let html5playerRes =
  135. /<script\s+src="([^"]+)"(?:\s+type="text\/javascript")?\s+name="player_ias\/base"\s*>|"jsUrl":"([^"]+)"/
  136. .exec(body);
  137. return html5playerRes ? html5playerRes[1] || html5playerRes[2] : null;
  138. };
  139.  
  140.  
  141. const getIdentityToken = (id, options, key, throwIfNotFound) =>
  142. exports.cookieCache.getOrSet(key, async() => {
  143. let page = await getWatchHTMLPageBody(id, options);
  144. let match = page.match(/(["'])ID_TOKEN\1[:,]\s?"([^"]+)"/);
  145. if (!match && throwIfNotFound) {
  146. throw new UnrecoverableError('Cookie header used in request, but unable to find YouTube identity token');
  147. }
  148. return match && match[2];
  149. });
  150.  
  151.  
  152. /**
  153. * Goes through each endpoint in the pipeline, retrying on failure if the error is recoverable.
  154. * If unable to succeed with one endpoint, moves onto the next one.
  155. *
  156. * @param {Array.<Object>} args
  157. * @param {Function} validate
  158. * @param {Object} retryOptions
  159. * @param {Array.<Function>} endpoints
  160. * @returns {[Object, Object, Object]}
  161. */
  162. const pipeline = async(args, validate, retryOptions, endpoints) => {
  163. let info;
  164. for (let func of endpoints) {
  165. try {
  166. const newInfo = await retryFunc(func, args.concat([info]), retryOptions);
  167. if (newInfo.player_response) {
  168. newInfo.player_response.videoDetails = assign(
  169. info && info.player_response && info.player_response.videoDetails,
  170. newInfo.player_response.videoDetails);
  171. newInfo.player_response = assign(info && info.player_response, newInfo.player_response);
  172. }
  173. info = assign(info, newInfo);
  174. if (validate(info, false)) {
  175. break;
  176. }
  177. } catch (err) {
  178. if (err instanceof UnrecoverableError || func === endpoints[endpoints.length - 1]) {
  179. throw err;
  180. }
  181. // Unable to find video metadata... so try next endpoint.
  182. }
  183. }
  184. return info;
  185. };
  186.  
  187.  
  188. /**
  189. * Like Object.assign(), but ignores `null` and `undefined` from `source`.
  190. *
  191. * @param {Object} target
  192. * @param {Object} source
  193. * @returns {Object}
  194. */
  195. const assign = (target, source) => {
  196. if (!target || !source) { return target || source; }
  197. for (let [key, value] of Object.entries(source)) {
  198. if (value !== null && value !== undefined) {
  199. target[key] = value;
  200. }
  201. }
  202. return target;
  203. };
  204.  
  205.  
  206. /**
  207. * Given a function, calls it with `args` until it's successful,
  208. * or until it encounters an unrecoverable error.
  209. * Currently, any error from miniget is considered unrecoverable. Errors such as
  210. * too many redirects, invalid URL, status code 404, status code 502.
  211. *
  212. * @param {Function} func
  213. * @param {Array.<Object>} args
  214. * @param {Object} options
  215. * @param {number} options.maxRetries
  216. * @param {Object} options.backoff
  217. * @param {number} options.backoff.inc
  218. */
  219. const retryFunc = async(func, args, options) => {
  220. let currentTry = 0, result;
  221. while (currentTry <= options.maxRetries) {
  222. try {
  223. result = await func(...args);
  224. break;
  225. } catch (err) {
  226. if (err instanceof UnrecoverableError ||
  227. (err instanceof miniget.MinigetError && err.statusCode < 500) || currentTry >= options.maxRetries) {
  228. throw err;
  229. }
  230. let wait = Math.min(++currentTry * options.backoff.inc, options.backoff.max);
  231. await new Promise(resolve => setTimeout(resolve, wait));
  232. }
  233. }
  234. return result;
  235. };
  236.  
  237.  
  238. const jsonClosingChars = /^[)\]}'\s]+/;
  239. const parseJSON = (source, varName, json) => {
  240. if (!json || typeof json === 'object') {
  241. return json;
  242. } else {
  243. try {
  244. json = json.replace(jsonClosingChars, '');
  245. return JSON.parse(json);
  246. } catch (err) {
  247. throw Error(`Error parsing ${varName} in ${source}: ${err.message}`);
  248. }
  249. }
  250. };
  251.  
  252.  
  253. const findJSON = (source, varName, body, left, right, prependJSON) => {
  254. let jsonStr = body.split("ytInitialPlayerResponse = {")[1].split("}}};")[0] += "}}}";
  255. if (!jsonStr) {
  256. throw Error(`Could not find ${varName} in ${source}`);
  257. }
  258. return parseJSON(source, varName, utils.cutAfterJSON(`${prependJSON}${jsonStr}`));
  259. };
  260.  
  261.  
  262. const findPlayerResponse = (source, info) => {
  263. const player_response = info && (
  264. (info.args && info.args.player_response) ||
  265. info.player_response || info.playerResponse || info.embedded_player_response);
  266. return parseJSON(source, 'player_response', player_response);
  267. };
  268.  
  269.  
  270. const getWatchJSONURL = (id, options) => `${getWatchHTMLURL(id, options)}&pbj=1`;
  271. const getWatchJSONPage = async(id, options) => {
  272. const reqOptions = Object.assign({ headers: {} }, options.requestOptions);
  273. let cookie = reqOptions.headers.Cookie || reqOptions.headers.cookie;
  274. reqOptions.headers = Object.assign({
  275. 'x-youtube-client-name': '1',
  276. 'x-youtube-client-version': '2.20201203.06.00',
  277. 'x-youtube-identity-token': exports.cookieCache.get(cookie || 'browser') || '',
  278. }, reqOptions.headers);
  279.  
  280. const setIdentityToken = async(key, throwIfNotFound) => {
  281. if (reqOptions.headers['x-youtube-identity-token']) { return; }
  282. reqOptions.headers['x-youtube-identity-token'] = await getIdentityToken(id, options, key, throwIfNotFound);
  283. };
  284.  
  285. if (cookie) {
  286. await setIdentityToken(cookie, true);
  287. }
  288.  
  289. const jsonUrl = getWatchJSONURL(id, options);
  290. const body = await utils.exposedMiniget(jsonUrl, options, reqOptions).text();
  291. let parsedBody = parseJSON('watch.json', 'body', body);
  292. if (parsedBody.reload === 'now') {
  293. await setIdentityToken('browser', false);
  294. }
  295. if (parsedBody.reload === 'now' || !Array.isArray(parsedBody)) {
  296. throw Error('Unable to retrieve video metadata in watch.json');
  297. }
  298. let info = parsedBody.reduce((part, curr) => Object.assign(curr, part), {});
  299. info.player_response = findPlayerResponse('watch.json', info);
  300. info.html5player = info.player && info.player.assets && info.player.assets.js;
  301.  
  302. return info;
  303. };
  304.  
  305.  
  306. const getWatchHTMLPage = async(id, options) => {
  307. let body = await getWatchHTMLPageBody(id, options);
  308. let info = { page: 'watch' };
  309. try {
  310. info.player_response = findJSON('watch.html', 'player_response',
  311. body, /\bytInitialPlayerResponse\s*=\s*\{/i, '\n', '{');
  312. } catch (err) {
  313. let args = findJSON('watch.html', 'player_response', body, /\bytplayer\.config\s*=\s*{/, '</script>', '{');
  314. info.player_response = findPlayerResponse('watch.html', args);
  315. }
  316. info.response = findJSON('watch.html', 'response', body, /\bytInitialData("\])?\s*=\s*\{/i, '\n', '{');
  317. info.html5player = getHTML5player(body);
  318. return info;
  319. };
  320.  
  321.  
  322. const INFO_HOST = 'www.youtube.com';
  323. const INFO_PATH = '/get_video_info';
  324. const VIDEO_EURL = 'https://youtube.googleapis.com/v/';
  325. const getVideoInfoPage = async(id, options) => {
  326. const url = new URL(`https://${INFO_HOST}${INFO_PATH}`);
  327. url.searchParams.set('video_id', id);
  328. url.searchParams.set('eurl', VIDEO_EURL + id);
  329. url.searchParams.set('ps', 'default');
  330. url.searchParams.set('gl', 'US');
  331. url.searchParams.set('hl', options.lang || 'en');
  332. url.searchParams.set('html5', '1');
  333. const body = await utils.exposedMiniget(url.toString(), options).text();
  334. let info = querystring.parse(body);
  335. info.player_response = findPlayerResponse('get_video_info', info);
  336. return info;
  337. };
  338.  
  339.  
  340. /**
  341. * @param {Object} player_response
  342. * @returns {Array.<Object>}
  343. */
  344. const parseFormats = player_response => {
  345. let formats = [];
  346. if (player_response && player_response.streamingData) {
  347. formats = formats
  348. .concat(player_response.streamingData.formats || [])
  349. .concat(player_response.streamingData.adaptiveFormats || []);
  350. }
  351. return formats;
  352. };
  353.  
  354.  
  355. /**
  356. * Gets info from a video additional formats and deciphered URLs.
  357. *
  358. * @param {string} id
  359. * @param {Object} options
  360. * @returns {Promise<Object>}
  361. */
  362. exports.getInfo = async(id, options) => {
  363. let info = await exports.getBasicInfo(id, options);
  364. const hasManifest =
  365. info.player_response && info.player_response.streamingData && (
  366. info.player_response.streamingData.dashManifestUrl ||
  367. info.player_response.streamingData.hlsManifestUrl
  368. );
  369. let funcs = [];
  370. if (info.formats.length) {
  371. info.html5player = info.html5player ||
  372. getHTML5player(await getWatchHTMLPageBody(id, options)) || getHTML5player(await getEmbedPageBody(id, options));
  373. if (!info.html5player) {
  374. throw Error('Unable to find html5player file');
  375. }
  376. const html5player = new URL(info.html5player, BASE_URL).toString();
  377. funcs.push(sig.decipherFormats(info.formats, html5player, options));
  378. }
  379. if (hasManifest && info.player_response.streamingData.dashManifestUrl) {
  380. let url = info.player_response.streamingData.dashManifestUrl;
  381. funcs.push(getDashManifest(url, options));
  382. }
  383. if (hasManifest && info.player_response.streamingData.hlsManifestUrl) {
  384. let url = info.player_response.streamingData.hlsManifestUrl;
  385. funcs.push(getM3U8(url, options));
  386. }
  387.  
  388. let results = await Promise.all(funcs);
  389. info.formats = Object.values(Object.assign({}, ...results));
  390. info.formats = info.formats.map(formatUtils.addFormatMeta);
  391. info.formats.sort(formatUtils.sortFormats);
  392. info.full = true;
  393. return info;
  394. };
  395.  
  396.  
  397. /**
  398. * Gets additional DASH formats.
  399. *
  400. * @param {string} url
  401. * @param {Object} options
  402. * @returns {Promise<Array.<Object>>}
  403. */
  404. const getDashManifest = (url, options) => new Promise((resolve, reject) => {
  405. let formats = {};
  406. const parser = sax.parser(false);
  407. parser.onerror = reject;
  408. let adaptationSet;
  409. parser.onopentag = node => {
  410. if (node.name === 'ADAPTATIONSET') {
  411. adaptationSet = node.attributes;
  412. } else if (node.name === 'REPRESENTATION') {
  413. const itag = parseInt(node.attributes.ID);
  414. if (!isNaN(itag)) {
  415. formats[url] = Object.assign({
  416. itag, url,
  417. bitrate: parseInt(node.attributes.BANDWIDTH),
  418. mimeType: `${adaptationSet.MIMETYPE}; codecs="${node.attributes.CODECS}"`,
  419. }, node.attributes.HEIGHT ? {
  420. width: parseInt(node.attributes.WIDTH),
  421. height: parseInt(node.attributes.HEIGHT),
  422. fps: parseInt(node.attributes.FRAMERATE),
  423. } : {
  424. audioSampleRate: node.attributes.AUDIOSAMPLINGRATE,
  425. });
  426. }
  427. }
  428. };
  429. parser.onend = () => { resolve(formats); };
  430. const req = utils.exposedMiniget(new URL(url, BASE_URL).toString(), options);
  431. req.setEncoding('utf8');
  432. req.on('error', reject);
  433. req.on('data', chunk => { parser.write(chunk); });
  434. req.on('end', parser.close.bind(parser));
  435. });
  436.  
  437.  
  438. /**
  439. * Gets additional formats.
  440. *
  441. * @param {string} url
  442. * @param {Object} options
  443. * @returns {Promise<Array.<Object>>}
  444. */
  445. const getM3U8 = async(url, options) => {
  446. url = new URL(url, BASE_URL);
  447. const body = await utils.exposedMiniget(url.toString(), options).text();
  448. let formats = {};
  449. body
  450. .split('\n')
  451. .filter(line => /^https?:\/\//.test(line))
  452. .forEach(line => {
  453. const itag = parseInt(line.match(/\/itag\/(\d+)\//)[1]);
  454. formats[line] = { itag, url: line };
  455. });
  456. return formats;
  457. };
  458.  
  459.  
  460. // Cache get info functions.
  461. // In case a user wants to get a video's info before downloading.
  462. for (let funcName of ['getBasicInfo', 'getInfo']) {
  463. /**
  464. * @param {string} link
  465. * @param {Object} options
  466. * @returns {Promise<Object>}
  467. */
  468. const func = exports[funcName];
  469. exports[funcName] = async(link, options = {}) => {
  470. utils.checkForUpdates();
  471. let id = await urlUtils.getVideoID(link);
  472. const key = [funcName, id, options.lang].join('-');
  473. return exports.cache.getOrSet(key, () => func(id, options));
  474. };
  475. }
  476.  
  477.  
  478. // Export a few helpers.
  479. exports.validateID = urlUtils.validateID;
  480. exports.validateURL = urlUtils.validateURL;
  481. exports.getURLVideoID = urlUtils.getURLVideoID;
  482. exports.getVideoID = urlUtils.getVideoID;
Advertisement
Add Comment
Please, Sign In to add comment