@oreohq/ytdl-core 4.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|