@oreohq/ytdl-core 4.15.1

Sign up to get free protection for your applications and to get access to all the features.
package/lib/utils.js ADDED
@@ -0,0 +1,437 @@
1
+ const { request } = require("undici");
2
+ const { writeFileSync } = require("fs");
3
+ const AGENT = require("./agent");
4
+
5
+ /**
6
+ * Extract string inbetween another.
7
+ *
8
+ * @param {string} haystack
9
+ * @param {string} left
10
+ * @param {string} right
11
+ * @returns {string}
12
+ */
13
+ const between = (exports.between = (haystack, left, right) => {
14
+ let pos;
15
+ if (left instanceof RegExp) {
16
+ const match = haystack.match(left);
17
+ if (!match) {
18
+ return "";
19
+ }
20
+ pos = match.index + match[0].length;
21
+ } else {
22
+ pos = haystack.indexOf(left);
23
+ if (pos === -1) {
24
+ return "";
25
+ }
26
+ pos += left.length;
27
+ }
28
+ haystack = haystack.slice(pos);
29
+ pos = haystack.indexOf(right);
30
+ if (pos === -1) {
31
+ return "";
32
+ }
33
+ haystack = haystack.slice(0, pos);
34
+ return haystack;
35
+ });
36
+
37
+ exports.tryParseBetween = (body, left, right, prepend = "", append = "") => {
38
+ try {
39
+ let data = between(body, left, right);
40
+ if (!data) return null;
41
+ return JSON.parse(`${prepend}${data}${append}`);
42
+ } catch (e) {
43
+ return null;
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Get a number from an abbreviated number string.
49
+ *
50
+ * @param {string} string
51
+ * @returns {number}
52
+ */
53
+ exports.parseAbbreviatedNumber = string => {
54
+ const match = string
55
+ .replace(",", ".")
56
+ .replace(" ", "")
57
+ .match(/([\d,.]+)([MK]?)/);
58
+ if (match) {
59
+ let [, num, multi] = match;
60
+ num = parseFloat(num);
61
+ return Math.round(multi === "M" ? num * 1000000 : multi === "K" ? num * 1000 : num);
62
+ }
63
+ return null;
64
+ };
65
+
66
+ /**
67
+ * Escape sequences for cutAfterJS
68
+ * @param {string} start the character string the escape sequence
69
+ * @param {string} end the character string to stop the escape seequence
70
+ * @param {undefined|Regex} startPrefix a regex to check against the preceding 10 characters
71
+ */
72
+ const ESCAPING_SEQUENZES = [
73
+ // Strings
74
+ { start: '"', end: '"' },
75
+ { start: "'", end: "'" },
76
+ { start: "`", end: "`" },
77
+ // RegeEx
78
+ { start: "/", end: "/", startPrefix: /(^|[[{:;,/])\s?$/ },
79
+ ];
80
+
81
+ /**
82
+ * Match begin and end braces of input JS, return only JS
83
+ *
84
+ * @param {string} mixedJson
85
+ * @returns {string}
86
+ */
87
+ exports.cutAfterJS = mixedJson => {
88
+ // Define the general open and closing tag
89
+ let open, close;
90
+ if (mixedJson[0] === "[") {
91
+ open = "[";
92
+ close = "]";
93
+ } else if (mixedJson[0] === "{") {
94
+ open = "{";
95
+ close = "}";
96
+ }
97
+
98
+ if (!open) {
99
+ throw new Error(`Can't cut unsupported JSON (need to begin with [ or { ) but got: ${mixedJson[0]}`);
100
+ }
101
+
102
+ // States if the loop is currently inside an escaped js object
103
+ let isEscapedObject = null;
104
+
105
+ // States if the current character is treated as escaped or not
106
+ let isEscaped = false;
107
+
108
+ // Current open brackets to be closed
109
+ let counter = 0;
110
+
111
+ let i;
112
+ // Go through all characters from the start
113
+ for (i = 0; i < mixedJson.length; i++) {
114
+ // End of current escaped object
115
+ if (!isEscaped && isEscapedObject !== null && mixedJson[i] === isEscapedObject.end) {
116
+ isEscapedObject = null;
117
+ continue;
118
+ // Might be the start of a new escaped object
119
+ } else if (!isEscaped && isEscapedObject === null) {
120
+ for (const escaped of ESCAPING_SEQUENZES) {
121
+ if (mixedJson[i] !== escaped.start) continue;
122
+ // Test startPrefix against last 10 characters
123
+ if (!escaped.startPrefix || mixedJson.substring(i - 10, i).match(escaped.startPrefix)) {
124
+ isEscapedObject = escaped;
125
+ break;
126
+ }
127
+ }
128
+ // Continue if we found a new escaped object
129
+ if (isEscapedObject !== null) {
130
+ continue;
131
+ }
132
+ }
133
+
134
+ // Toggle the isEscaped boolean for every backslash
135
+ // Reset for every regular character
136
+ isEscaped = mixedJson[i] === "\\" && !isEscaped;
137
+
138
+ if (isEscapedObject !== null) continue;
139
+
140
+ if (mixedJson[i] === open) {
141
+ counter++;
142
+ } else if (mixedJson[i] === close) {
143
+ counter--;
144
+ }
145
+
146
+ // All brackets have been closed, thus end of JSON is reached
147
+ if (counter === 0) {
148
+ // Return the cut JSON
149
+ return mixedJson.substring(0, i + 1);
150
+ }
151
+ }
152
+
153
+ // We ran through the whole string and ended up with an unclosed bracket
154
+ throw Error("Can't cut unsupported JSON (no matching closing bracket found)");
155
+ };
156
+
157
+ class UnrecoverableError extends Error {}
158
+ /**
159
+ * Checks if there is a playability error.
160
+ *
161
+ * @param {Object} player_response
162
+ * @returns {!Error}
163
+ */
164
+ exports.playError = player_response => {
165
+ const playability = player_response && player_response.playabilityStatus;
166
+ if (!playability) return null;
167
+ if (["ERROR", "LOGIN_REQUIRED"].includes(playability.status)) {
168
+ return new UnrecoverableError(playability.reason || (playability.messages && playability.messages[0]));
169
+ }
170
+ if (playability.status === "LIVE_STREAM_OFFLINE") {
171
+ return new UnrecoverableError(playability.reason || "The live stream is offline.");
172
+ }
173
+ if (playability.status === "UNPLAYABLE") {
174
+ return new UnrecoverableError(playability.reason || "This video is unavailable.");
175
+ }
176
+ return null;
177
+ };
178
+
179
+ // Undici request
180
+ exports.request = async (url, options = {}) => {
181
+ let { requestOptions, rewriteRequest } = options;
182
+
183
+ if (typeof rewriteRequest === "function") {
184
+ const request = rewriteRequest(url, requestOptions);
185
+ requestOptions = request.requestOptions;
186
+ url = request.url;
187
+ }
188
+
189
+ const req = await request(url, requestOptions);
190
+ const code = req.statusCode.toString();
191
+ if (code.startsWith("2")) {
192
+ if (req.headers["content-type"].includes("application/json")) return req.body.json();
193
+ return req.body.text();
194
+ }
195
+ if (code.startsWith("3")) return exports.request(req.headers.location, options);
196
+ const e = new Error(`Status code: ${code}`);
197
+ e.statusCode = req.statusCode;
198
+ throw e;
199
+ };
200
+
201
+ /**
202
+ * Temporary helper to help deprecating a few properties.
203
+ *
204
+ * @param {Object} obj
205
+ * @param {string} prop
206
+ * @param {Object} value
207
+ * @param {string} oldPath
208
+ * @param {string} newPath
209
+ */
210
+ exports.deprecate = (obj, prop, value, oldPath, newPath) => {
211
+ Object.defineProperty(obj, prop, {
212
+ get: () => {
213
+ console.warn(`\`${oldPath}\` will be removed in a near future release, ` + `use \`${newPath}\` instead.`);
214
+ return value;
215
+ },
216
+ });
217
+ };
218
+
219
+ // Check for updates.
220
+ const pkg = require("../package.json");
221
+ const UPDATE_INTERVAL = 1000 * 60 * 60 * 12;
222
+ let updateWarnTimes = 0;
223
+ exports.lastUpdateCheck = 0;
224
+ exports.checkForUpdates = () => {
225
+ if (
226
+ !process.env.YTDL_NO_UPDATE &&
227
+ !pkg.version.startsWith("0.0.0-") &&
228
+ Date.now() - exports.lastUpdateCheck >= UPDATE_INTERVAL
229
+ ) {
230
+ exports.lastUpdateCheck = Date.now();
231
+ return exports
232
+ .request("https://api.github.com/repos/distubejs/ytdl-core/contents/package.json", {
233
+ requestOptions: {
234
+ headers: {
235
+ "User-Agent":
236
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.3",
237
+ },
238
+ },
239
+ })
240
+ .then(
241
+ response => {
242
+ const buf = Buffer.from(response.content, response.encoding);
243
+ const pkgFile = JSON.parse(buf.toString("ascii"));
244
+ if (pkgFile.version !== pkg.version && updateWarnTimes++ < 5) {
245
+ // eslint-disable-next-line max-len
246
+ console.warn(
247
+ '\x1b[33mWARNING:\x1B[0m @distube/ytdl-core is out of date! Update with "npm install @distube/ytdl-core@latest".',
248
+ );
249
+ }
250
+ },
251
+ err => {
252
+ console.warn("Error checking for updates:", err.message);
253
+ console.warn("You can disable this check by setting the `YTDL_NO_UPDATE` env variable.");
254
+ },
255
+ );
256
+ }
257
+ return null;
258
+ };
259
+
260
+ /**
261
+ * Gets random IPv6 Address from a block
262
+ *
263
+ * @param {string} ip the IPv6 block in CIDR-Notation
264
+ * @returns {string}
265
+ */
266
+ const getRandomIPv6 = (exports.getRandomIPv6 = ip => {
267
+ // Start with a fast Regex-Check
268
+ if (!isIPv6(ip)) throw Error("Invalid IPv6 format");
269
+ // Start by splitting and normalizing addr and mask
270
+ const [rawAddr, rawMask] = ip.split("/");
271
+ let base10Mask = parseInt(rawMask);
272
+ if (!base10Mask || base10Mask > 128 || base10Mask < 24) throw Error("Invalid IPv6 subnet");
273
+ const base10addr = normalizeIP(rawAddr);
274
+ // Get random addr to pad with
275
+ // using Math.random since we're not requiring high level of randomness
276
+ const randomAddr = new Array(8).fill(1).map(() => Math.floor(Math.random() * 0xffff));
277
+
278
+ // Merge base10addr with randomAddr
279
+ const mergedAddr = randomAddr.map((randomItem, idx) => {
280
+ // Calculate the amount of static bits
281
+ const staticBits = Math.min(base10Mask, 16);
282
+ // Adjust the bitmask with the staticBits
283
+ base10Mask -= staticBits;
284
+ // Calculate the bitmask
285
+ // lsb makes the calculation way more complicated
286
+ const mask = 0xffff - (2 ** (16 - staticBits) - 1);
287
+ // Combine base10addr and random
288
+ return (base10addr[idx] & mask) + (randomItem & (mask ^ 0xffff));
289
+ });
290
+ // Return new addr
291
+ return mergedAddr.map(x => x.toString("16")).join(":");
292
+ });
293
+
294
+ // eslint-disable-next-line max-len
295
+ const IPV6_REGEX =
296
+ /^(([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})$/;
297
+ /**
298
+ * Quick check for a valid IPv6
299
+ * The Regex only accepts a subset of all IPv6 Addresses
300
+ *
301
+ * @param {string} ip the IPv6 block in CIDR-Notation to test
302
+ * @returns {boolean} true if valid
303
+ */
304
+ const isIPv6 = (exports.isIPv6 = ip => IPV6_REGEX.test(ip));
305
+
306
+ /**
307
+ * Normalise an IP Address
308
+ *
309
+ * @param {string} ip the IPv6 Addr
310
+ * @returns {number[]} the 8 parts of the IPv6 as Integers
311
+ */
312
+ const normalizeIP = (exports.normalizeIP = ip => {
313
+ // Split by fill position
314
+ const parts = ip.split("::").map(x => x.split(":"));
315
+ // Normalize start and end
316
+ const partStart = parts[0] || [];
317
+ const partEnd = parts[1] || [];
318
+ partEnd.reverse();
319
+ // Placeholder for full ip
320
+ const fullIP = new Array(8).fill(0);
321
+ // Fill in start and end parts
322
+ for (let i = 0; i < Math.min(partStart.length, 8); i++) {
323
+ fullIP[i] = parseInt(partStart[i], 16) || 0;
324
+ }
325
+ for (let i = 0; i < Math.min(partEnd.length, 8); i++) {
326
+ fullIP[7 - i] = parseInt(partEnd[i], 16) || 0;
327
+ }
328
+ return fullIP;
329
+ });
330
+
331
+ exports.saveDebugFile = (name, body) => {
332
+ const filename = `${+new Date()}-${name}`;
333
+ writeFileSync(filename, body);
334
+ return filename;
335
+ };
336
+
337
+ const findPropKeyInsensitive = (obj, prop) =>
338
+ Object.keys(obj).find(p => p.toLowerCase() === prop.toLowerCase()) || null;
339
+
340
+ exports.getPropInsensitive = (obj, prop) => {
341
+ const key = findPropKeyInsensitive(obj, prop);
342
+ return key && obj[key];
343
+ };
344
+
345
+ exports.setPropInsensitive = (obj, prop, value) => {
346
+ const key = findPropKeyInsensitive(obj, prop);
347
+ obj[key || prop] = value;
348
+ return key;
349
+ };
350
+
351
+ let oldCookieWarning = true;
352
+ let oldDispatcherWarning = true;
353
+ exports.applyDefaultAgent = options => {
354
+ if (!options.agent) {
355
+ const { jar } = AGENT.defaultAgent;
356
+ const c = exports.getPropInsensitive(options.requestOptions.headers, "cookie");
357
+ if (c) {
358
+ jar.removeAllCookiesSync();
359
+ AGENT.addCookiesFromString(jar, c);
360
+ if (oldCookieWarning) {
361
+ oldCookieWarning = false;
362
+ console.warn(
363
+ "\x1b[33mWARNING:\x1B[0m Using old cookie format, " +
364
+ "please use the new one instead. (https://github.com/distubejs/ytdl-core#cookies-support)",
365
+ );
366
+ }
367
+ }
368
+ if (options.requestOptions.dispatcher && oldDispatcherWarning) {
369
+ oldDispatcherWarning = false;
370
+ console.warn(
371
+ "\x1b[33mWARNING:\x1B[0m Your dispatcher is overridden by `ytdl.Agent`. " +
372
+ "To implement your own, check out the documentation. " +
373
+ "(https://github.com/distubejs/ytdl-core#how-to-implement-ytdlagent-with-your-own-dispatcher)",
374
+ );
375
+ }
376
+ options.agent = AGENT.defaultAgent;
377
+ }
378
+ };
379
+
380
+ let oldLocalAddressWarning = true;
381
+ exports.applyOldLocalAddress = options => {
382
+ if (
383
+ !options.requestOptions ||
384
+ !options.requestOptions.localAddress ||
385
+ options.requestOptions.localAddress === options.agent.localAddress
386
+ )
387
+ return;
388
+ options.agent = AGENT.createAgent(undefined, { localAddress: options.requestOptions.localAddress });
389
+ if (oldLocalAddressWarning) {
390
+ oldLocalAddressWarning = false;
391
+ console.warn(
392
+ "\x1b[33mWARNING:\x1B[0m Using old localAddress option, " +
393
+ "please add it to the agent options instead. (https://github.com/distubejs/ytdl-core#ip-rotation)",
394
+ );
395
+ }
396
+ };
397
+
398
+ let oldIpRotationsWarning = true;
399
+ exports.applyIPv6Rotations = options => {
400
+ if (options.IPv6Block) {
401
+ options.requestOptions = Object.assign({}, options.requestOptions, {
402
+ localAddress: getRandomIPv6(options.IPv6Block),
403
+ });
404
+ if (oldIpRotationsWarning) {
405
+ oldIpRotationsWarning = false;
406
+ oldLocalAddressWarning = false;
407
+ console.warn(
408
+ "\x1b[33mWARNING:\x1B[0m IPv6Block option is deprecated, " +
409
+ "please create your own ip rotation instead. (https://github.com/distubejs/ytdl-core#ip-rotation)",
410
+ );
411
+ }
412
+ }
413
+ };
414
+
415
+ exports.applyDefaultHeaders = options => {
416
+ options.requestOptions = Object.assign({}, options.requestOptions);
417
+ options.requestOptions.headers = Object.assign(
418
+ {},
419
+ {
420
+ // eslint-disable-next-line max-len
421
+ "User-Agent":
422
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36",
423
+ },
424
+ options.requestOptions.headers,
425
+ );
426
+ };
427
+
428
+ exports.generateClientPlaybackNonce = length => {
429
+ const CPN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
430
+ return Array.from({ length }, () => CPN_CHARS[Math.floor(Math.random() * CPN_CHARS.length)]).join("");
431
+ };
432
+
433
+ exports.applyPlayerClients = options => {
434
+ if (!options.playerClients || options.playerClients.length === 0) {
435
+ options.playerClients = ["WEB_CREATOR", "IOS"];
436
+ }
437
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@oreohq/ytdl-core",
3
+ "description": "Oreo HQ fork of ytdl-core. YouTube video downloader in pure javascript.",
4
+ "version": "4.15.1",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git://github.com/oreohq/ytdl-core.git"
8
+ },
9
+ "author": "Aniket (https://github.com/aniket091)",
10
+ "contributors": [
11
+ "fent <fentbox@gmail.com> (https://github.com/fent)",
12
+ "Tobias Kutscha (https://github.com/TimeForANinja)",
13
+ "Andrew Kelley (https://github.com/andrewrk)",
14
+ "Mauricio Allende (https://github.com/mallendeo)",
15
+ "Rodrigo Altamirano (https://github.com/raltamirano)",
16
+ "Jim Buck (https://github.com/JimmyBoh)",
17
+ "Pawel Rucinski (https://github.com/Roki100)",
18
+ "Alexander Paolini (https://github.com/Million900o)"
19
+ ],
20
+ "main": "./lib/index.js",
21
+ "types": "./typings/index.d.ts",
22
+ "files": [
23
+ "lib",
24
+ "typings"
25
+ ],
26
+ "dependencies": {
27
+ "http-cookie-agent": "^6.0.6",
28
+ "m3u8stream": "^0.8.6",
29
+ "miniget": "^4.2.3",
30
+ "sax": "^1.4.1",
31
+ "tough-cookie": "^4.1.4",
32
+ "undici": "five"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.8.1",
36
+ "prettier": "^3.3.3",
37
+ "typescript": "^5.6.3"
38
+ },
39
+ "engines": {
40
+ "node": ">=14.0"
41
+ },
42
+ "license": "MIT",
43
+ "scripts": {
44
+ "prettier": "prettier --write \"**/*.{js,json,yml,md,ts}\""
45
+ }
46
+ }