@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/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
|
+
}
|