utils.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. const miniget = require('miniget');
  2. /**
  3. * Extract string inbetween another.
  4. *
  5. * @param {string} haystack
  6. * @param {string} left
  7. * @param {string} right
  8. * @returns {string}
  9. */
  10. exports.between = (haystack, left, right) => {
  11. let pos;
  12. if (left instanceof RegExp) {
  13. const match = haystack.match(left);
  14. if (!match) { return ''; }
  15. pos = match.index + match[0].length;
  16. } else {
  17. pos = haystack.indexOf(left);
  18. if (pos === -1) { return ''; }
  19. pos += left.length;
  20. }
  21. haystack = haystack.slice(pos);
  22. pos = haystack.indexOf(right);
  23. if (pos === -1) { return ''; }
  24. haystack = haystack.slice(0, pos);
  25. return haystack;
  26. };
  27. /**
  28. * Get a number from an abbreviated number string.
  29. *
  30. * @param {string} string
  31. * @returns {number}
  32. */
  33. exports.parseAbbreviatedNumber = string => {
  34. const match = string
  35. .replace(',', '.')
  36. .replace(' ', '')
  37. .match(/([\d,.]+)([MK]?)/);
  38. if (match) {
  39. let [, num, multi] = match;
  40. num = parseFloat(num);
  41. return Math.round(multi === 'M' ? num * 1000000 :
  42. multi === 'K' ? num * 1000 : num);
  43. }
  44. return null;
  45. };
  46. /**
  47. * Escape sequences for cutAfterJS
  48. * @param {string} start the character string the escape sequence
  49. * @param {string} end the character string to stop the escape seequence
  50. * @param {undefined|Regex} startPrefix a regex to check against the preceding 10 characters
  51. */
  52. const ESCAPING_SEQUENZES = [
  53. // Strings
  54. { start: '"', end: '"' },
  55. { start: "'", end: "'" },
  56. { start: '`', end: '`' },
  57. // RegeEx
  58. { start: '/', end: '/', startPrefix: /(^|[[{:;,/])\s?$/ },
  59. ];
  60. /**
  61. * Match begin and end braces of input JS, return only JS
  62. *
  63. * @param {string} mixedJson
  64. * @returns {string}
  65. */
  66. exports.cutAfterJS = mixedJson => {
  67. // Define the general open and closing tag
  68. let open, close;
  69. if (mixedJson[0] === '[') {
  70. open = '[';
  71. close = ']';
  72. } else if (mixedJson[0] === '{') {
  73. open = '{';
  74. close = '}';
  75. }
  76. if (!open) {
  77. throw new Error(`Can't cut unsupported JSON (need to begin with [ or { ) but got: ${mixedJson[0]}`);
  78. }
  79. // States if the loop is currently inside an escaped js object
  80. let isEscapedObject = null;
  81. // States if the current character is treated as escaped or not
  82. let isEscaped = false;
  83. // Current open brackets to be closed
  84. let counter = 0;
  85. let i;
  86. // Go through all characters from the start
  87. for (i = 0; i < mixedJson.length; i++) {
  88. // End of current escaped object
  89. if (!isEscaped && isEscapedObject !== null && mixedJson[i] === isEscapedObject.end) {
  90. isEscapedObject = null;
  91. continue;
  92. // Might be the start of a new escaped object
  93. } else if (!isEscaped && isEscapedObject === null) {
  94. for (const escaped of ESCAPING_SEQUENZES) {
  95. if (mixedJson[i] !== escaped.start) continue;
  96. // Test startPrefix against last 10 characters
  97. if (!escaped.startPrefix || mixedJson.substring(i - 10, i).match(escaped.startPrefix)) {
  98. isEscapedObject = escaped;
  99. break;
  100. }
  101. }
  102. // Continue if we found a new escaped object
  103. if (isEscapedObject !== null) {
  104. continue;
  105. }
  106. }
  107. // Toggle the isEscaped boolean for every backslash
  108. // Reset for every regular character
  109. isEscaped = mixedJson[i] === '\\' && !isEscaped;
  110. if (isEscapedObject !== null) continue;
  111. if (mixedJson[i] === open) {
  112. counter++;
  113. } else if (mixedJson[i] === close) {
  114. counter--;
  115. }
  116. // All brackets have been closed, thus end of JSON is reached
  117. if (counter === 0) {
  118. // Return the cut JSON
  119. return mixedJson.substring(0, i + 1);
  120. }
  121. }
  122. // We ran through the whole string and ended up with an unclosed bracket
  123. throw Error("Can't cut unsupported JSON (no matching closing bracket found)");
  124. };
  125. /**
  126. * Checks if there is a playability error.
  127. *
  128. * @param {Object} player_response
  129. * @param {Array.<string>} statuses
  130. * @param {Error} ErrorType
  131. * @returns {!Error}
  132. */
  133. exports.playError = (player_response, statuses, ErrorType = Error) => {
  134. let playability = player_response && player_response.playabilityStatus;
  135. if (playability && statuses.includes(playability.status)) {
  136. return new ErrorType(playability.reason || (playability.messages && playability.messages[0]));
  137. }
  138. return null;
  139. };
  140. /**
  141. * Does a miniget request and calls options.requestCallback if present
  142. *
  143. * @param {string} url the request url
  144. * @param {Object} options an object with optional requestOptions and requestCallback parameters
  145. * @param {Object} requestOptionsOverwrite overwrite of options.requestOptions
  146. * @returns {miniget.Stream}
  147. */
  148. exports.exposedMiniget = (url, options = {}, requestOptionsOverwrite) => {
  149. const req = miniget(url, requestOptionsOverwrite || options.requestOptions);
  150. if (typeof options.requestCallback === 'function') options.requestCallback(req);
  151. return req;
  152. };
  153. /**
  154. * Temporary helper to help deprecating a few properties.
  155. *
  156. * @param {Object} obj
  157. * @param {string} prop
  158. * @param {Object} value
  159. * @param {string} oldPath
  160. * @param {string} newPath
  161. */
  162. exports.deprecate = (obj, prop, value, oldPath, newPath) => {
  163. Object.defineProperty(obj, prop, {
  164. get: () => {
  165. console.warn(`\`${oldPath}\` will be removed in a near future release, ` +
  166. `use \`${newPath}\` instead.`);
  167. return value;
  168. },
  169. });
  170. };
  171. // Check for updates.
  172. const pkg = require('../package.json');
  173. const UPDATE_INTERVAL = 1000 * 60 * 60 * 12;
  174. exports.lastUpdateCheck = 0;
  175. exports.checkForUpdates = () => {
  176. if (!process.env.YTDL_NO_UPDATE && !pkg.version.startsWith('0.0.0-') &&
  177. Date.now() - exports.lastUpdateCheck >= UPDATE_INTERVAL) {
  178. exports.lastUpdateCheck = Date.now();
  179. return miniget('https://api.github.com/repos/fent/node-ytdl-core/releases/latest', {
  180. headers: { 'User-Agent': 'ytdl-core' },
  181. }).text().then(response => {
  182. if (JSON.parse(response).tag_name !== `v${pkg.version}`) {
  183. console.warn('\x1b[33mWARNING:\x1B[0m ytdl-core is out of date! Update with "npm install ytdl-core@latest".');
  184. }
  185. }, err => {
  186. console.warn('Error checking for updates:', err.message);
  187. console.warn('You can disable this check by setting the `YTDL_NO_UPDATE` env variable.');
  188. });
  189. }
  190. return null;
  191. };
  192. /**
  193. * Gets random IPv6 Address from a block
  194. *
  195. * @param {string} ip the IPv6 block in CIDR-Notation
  196. * @returns {string}
  197. */
  198. exports.getRandomIPv6 = ip => {
  199. // Start with a fast Regex-Check
  200. if (!isIPv6(ip)) throw Error('Invalid IPv6 format');
  201. // Start by splitting and normalizing addr and mask
  202. const [rawAddr, rawMask] = ip.split('/');
  203. let base10Mask = parseInt(rawMask);
  204. if (!base10Mask || base10Mask > 128 || base10Mask < 24) throw Error('Invalid IPv6 subnet');
  205. const base10addr = normalizeIP(rawAddr);
  206. // Get random addr to pad with
  207. // using Math.random since we're not requiring high level of randomness
  208. const randomAddr = new Array(8).fill(1).map(() => Math.floor(Math.random() * 0xffff));
  209. // Merge base10addr with randomAddr
  210. const mergedAddr = randomAddr.map((randomItem, idx) => {
  211. // Calculate the amount of static bits
  212. const staticBits = Math.min(base10Mask, 16);
  213. // Adjust the bitmask with the staticBits
  214. base10Mask -= staticBits;
  215. // Calculate the bitmask
  216. // lsb makes the calculation way more complicated
  217. const mask = 0xffff - ((2 ** (16 - staticBits)) - 1);
  218. // Combine base10addr and random
  219. return (base10addr[idx] & mask) + (randomItem & (mask ^ 0xffff));
  220. });
  221. // Return new addr
  222. return mergedAddr.map(x => x.toString('16')).join(':');
  223. };
  224. // eslint-disable-next-line max-len
  225. const IPV6_REGEX = /^(([0-9a-f]{1,4}:)(:[0-9a-f]{1,4}){1,6}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4})|([0-9a-f]{1,4}:){1,7}(([0-9a-f]{1,4})|:))\/(1[0-1]\d|12[0-8]|\d{1,2})$/;
  226. /**
  227. * Quick check for a valid IPv6
  228. * The Regex only accepts a subset of all IPv6 Addresses
  229. *
  230. * @param {string} ip the IPv6 block in CIDR-Notation to test
  231. * @returns {boolean} true if valid
  232. */
  233. const isIPv6 = exports.isIPv6 = ip => IPV6_REGEX.test(ip);
  234. /**
  235. * Normalise an IP Address
  236. *
  237. * @param {string} ip the IPv6 Addr
  238. * @returns {number[]} the 8 parts of the IPv6 as Integers
  239. */
  240. const normalizeIP = exports.normalizeIP = ip => {
  241. // Split by fill position
  242. const parts = ip.split('::').map(x => x.split(':'));
  243. // Normalize start and end
  244. const partStart = parts[0] || [];
  245. const partEnd = parts[1] || [];
  246. partEnd.reverse();
  247. // Placeholder for full ip
  248. const fullIP = new Array(8).fill(0);
  249. // Fill in start and end parts
  250. for (let i = 0; i < Math.min(partStart.length, 8); i++) {
  251. fullIP[i] = parseInt(partStart[i], 16) || 0;
  252. }
  253. for (let i = 0; i < Math.min(partEnd.length, 8); i++) {
  254. fullIP[7 - i] = parseInt(partEnd[i], 16) || 0;
  255. }
  256. return fullIP;
  257. };