@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/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (C) 2012-present by fent
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# @oreohq/ytdl-core
|
2
|
+
|
3
|
+
Oreo HQ fork of `ytdl-core`. This fork is dedicated to fixing bugs and adding features that are not merged into the original repo as soon as possible.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
npm install @oreohq/ytdl-core@latest
|
9
|
+
```
|
10
|
+
|
11
|
+
Make sure you're installing the latest version of `@oreohq/ytdl-core` to keep up with the latest fixes.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```js
|
16
|
+
const ytdl = require("@oreohq/ytdl-core");
|
17
|
+
// TypeScript: import ytdl from '@oreohq/ytdl-core'; with --esModuleInterop
|
18
|
+
// TypeScript: import * as ytdl from '@oreohq/ytdl-core'; with --allowSyntheticDefaultImports
|
19
|
+
// TypeScript: import ytdl = require('@oreohq/ytdl-core'); with neither of the above
|
20
|
+
|
21
|
+
// Download a video
|
22
|
+
ytdl("https://www.youtube.com/watch?v=G9KVla9gwbY").pipe(require("fs").createWriteStream("video.mp4"));
|
23
|
+
|
24
|
+
// Get video info
|
25
|
+
ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY").then(info => {
|
26
|
+
console.log(info.videoDetails.title);
|
27
|
+
});
|
28
|
+
|
29
|
+
// Get video info with download formats
|
30
|
+
ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY").then(info => {
|
31
|
+
console.log(info.formats);
|
32
|
+
});
|
33
|
+
```
|
34
|
+
|
35
|
+
### Cookies Support
|
36
|
+
|
37
|
+
```js
|
38
|
+
const ytdl = require("@oreohq/ytdl-core");
|
39
|
+
|
40
|
+
// (Optional) Below are examples, NOT the recommended options
|
41
|
+
const cookies = [
|
42
|
+
{ name: "cookie1", value: "COOKIE1_HERE" },
|
43
|
+
{ name: "cookie2", value: "COOKIE2_HERE" },
|
44
|
+
];
|
45
|
+
|
46
|
+
// (Optional) http-cookie-agent / undici agent options
|
47
|
+
// Below are examples, NOT the recommended options
|
48
|
+
const agentOptions = {
|
49
|
+
pipelining: 5,
|
50
|
+
maxRedirections: 0,
|
51
|
+
localAddress: "127.0.0.1",
|
52
|
+
};
|
53
|
+
|
54
|
+
// agent should be created once if you don't want to change your cookie
|
55
|
+
const agent = ytdl.createAgent(cookies, agentOptions);
|
56
|
+
|
57
|
+
ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
|
58
|
+
ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
|
59
|
+
```
|
60
|
+
|
61
|
+
#### How to get cookies
|
62
|
+
|
63
|
+
- Install [EditThisCookie](http://www.editthiscookie.com/) extension for your browser.
|
64
|
+
- Go to [YouTube](https://www.youtube.com/).
|
65
|
+
- Log in to your account. (You should use a new account for this purpose)
|
66
|
+
- Click on the extension icon and click "Export" icon.
|
67
|
+
- Your cookies will be added to your clipboard and paste it into your code.
|
68
|
+
|
69
|
+
> [!WARNING]
|
70
|
+
> Don't logout it by clicking logout button on youtube/google account manager, it will expire your cookies.
|
71
|
+
> You can delete your browser's cookies to log it out on your browser.
|
72
|
+
> Or use incognito mode to get your cookies then close it.
|
73
|
+
|
74
|
+
> [!WARNING]
|
75
|
+
> Paste all the cookies array from clipboard into `createAgent` function. Don't remove/edit any cookies if you don't know what you're doing.
|
76
|
+
|
77
|
+
> [!WARNING]
|
78
|
+
> Make sure your account, which logged in when you getting your cookies, use 1 IP at the same time only. It will make your cookies alive longer.
|
79
|
+
|
80
|
+
```js
|
81
|
+
const ytdl = require("@oreohq/ytdl-core");
|
82
|
+
const agent = ytdl.createAgent([
|
83
|
+
{
|
84
|
+
domain: ".youtube.com",
|
85
|
+
expirationDate: 1234567890,
|
86
|
+
hostOnly: false,
|
87
|
+
httpOnly: true,
|
88
|
+
name: "---xxx---",
|
89
|
+
path: "/",
|
90
|
+
sameSite: "no_restriction",
|
91
|
+
secure: true,
|
92
|
+
session: false,
|
93
|
+
value: "---xxx---",
|
94
|
+
},
|
95
|
+
{
|
96
|
+
"...": "...",
|
97
|
+
},
|
98
|
+
]);
|
99
|
+
```
|
100
|
+
|
101
|
+
- Or you can paste your cookies array into a file and use `fs.readFileSync` to read it.
|
102
|
+
|
103
|
+
```js
|
104
|
+
const ytdl = require("@oreohq/ytdl-core");
|
105
|
+
const fs = require("fs");
|
106
|
+
const agent = ytdl.createAgent(JSON.parse(fs.readFileSync("cookies.json")));
|
107
|
+
```
|
108
|
+
|
109
|
+
### Proxy Support
|
110
|
+
|
111
|
+
```js
|
112
|
+
const ytdl = require("@oreohq/ytdl-core");
|
113
|
+
|
114
|
+
const agent = ytdl.createProxyAgent({ uri: "my.proxy.server" });
|
115
|
+
|
116
|
+
ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
|
117
|
+
ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
|
118
|
+
```
|
119
|
+
|
120
|
+
Use both proxy and cookies:
|
121
|
+
|
122
|
+
```js
|
123
|
+
const ytdl = require("@oreohq/ytdl-core");
|
124
|
+
|
125
|
+
const agent = ytdl.createProxyAgent({ uri: "my.proxy.server" }, [{ name: "cookie", value: "COOKIE_HERE" }]);
|
126
|
+
|
127
|
+
ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
|
128
|
+
ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
|
129
|
+
```
|
130
|
+
|
131
|
+
### IP Rotation
|
132
|
+
|
133
|
+
_Built-in ip rotation (`getRandomIPv6`) won't be updated and will be removed in the future, create your own ip rotation instead._
|
134
|
+
|
135
|
+
To implement IP rotation, you need to assign the desired IP address to the `localAddress` property within `undici.Agent.Options`.
|
136
|
+
Therefore, you'll need to use a different `ytdl.Agent` for each IP address you want to use.
|
137
|
+
|
138
|
+
```js
|
139
|
+
const ytdl = require("@oreohq/ytdl-core");
|
140
|
+
const { getRandomIPv6 } = require("@oreohq/ytdl-core/lib/utils");
|
141
|
+
|
142
|
+
const agentForARandomIP = ytdl.createAgent(undefined, {
|
143
|
+
localAddress: getRandomIPv6("2001:2::/48"),
|
144
|
+
});
|
145
|
+
|
146
|
+
ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent: agentForARandomIP });
|
147
|
+
|
148
|
+
const agentForAnotherRandomIP = ytdl.createAgent(undefined, {
|
149
|
+
localAddress: getRandomIPv6("2001:2::/48"),
|
150
|
+
});
|
151
|
+
|
152
|
+
ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent: agentForAnotherRandomIP });
|
153
|
+
```
|
154
|
+
|
155
|
+
## API
|
156
|
+
|
157
|
+
You can find the API documentation in the [original repo](https://github.com/fent/node-ytdl-core#api). Except a few changes:
|
158
|
+
|
159
|
+
### `ytdl.getInfoOptions`
|
160
|
+
|
161
|
+
- `requestOptions` is now `undici`'s [`RequestOptions`](https://github.com/nodejs/undici#undicirequesturl-options-promise).
|
162
|
+
- `agent`: [`ytdl.Agent`](https://github.com/oreohq/ytdl-core/blob/master/typings/index.d.ts#L10-L14)
|
163
|
+
- `playerClients`: An array of player clients to use. Accepts `WEB`, `WEB_CREATOR`, `IOS`, and `ANDROID`. Defaults to `["WEB_CREATOR", "IOS"]`.
|
164
|
+
|
165
|
+
### `ytdl.createAgent([cookies]): ytdl.Agent`
|
166
|
+
|
167
|
+
`cookies`: an array of json cookies exported with [EditThisCookie](http://www.editthiscookie.com/).
|
168
|
+
|
169
|
+
### `ytdl.createProxyAgent(proxy[, cookies]): ytdl.Agent`
|
170
|
+
|
171
|
+
`proxy`: [`ProxyAgentOptions`](https://github.com/nodejs/undici/blob/main/docs/api/ProxyAgent.md#parameter-proxyagentoptions) contains your proxy server information.
|
172
|
+
|
173
|
+
#### How to implement `ytdl.Agent` with your own Dispatcher
|
174
|
+
|
175
|
+
You can find the example [here](https://github.com/oreohq/ytdl-core/blob/master/lib/cookie.js#L73-L86)
|
176
|
+
|
177
|
+
## Limitations
|
178
|
+
|
179
|
+
ytdl cannot download videos that fall into the following
|
180
|
+
|
181
|
+
- Regionally restricted (requires a [proxy](#proxy-support))
|
182
|
+
- Private (if you have access, requires [cookies](#cookies-support))
|
183
|
+
- Rentals (if you have access, requires [cookies](#cookies-support))
|
184
|
+
- YouTube Premium content (if you have access, requires [cookies](#cookies-support))
|
185
|
+
- Only [HLS Livestreams](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) are currently supported. Other formats will get filtered out in ytdl.chooseFormats
|
186
|
+
|
187
|
+
Generated download links are valid for 6 hours, and may only be downloadable from the same IP address.
|
188
|
+
|
189
|
+
## Rate Limiting
|
190
|
+
|
191
|
+
When doing too many requests YouTube might block. This will result in your requests getting denied with HTTP-StatusCode 429. The following steps might help you:
|
192
|
+
|
193
|
+
- Update `@oreohq/ytdl-core` to the latest version
|
194
|
+
- Use proxies (you can find an example [here](#proxy-support))
|
195
|
+
- Extend the Proxy Idea by rotating (IPv6-)Addresses
|
196
|
+
- read [this](https://github.com/fent/node-ytdl-core#how-does-using-an-ipv6-block-help) for more information about this
|
197
|
+
- Use cookies (you can find an example [here](#cookies-support))
|
198
|
+
- for this to take effect you have to FIRST wait for the current rate limit to expire
|
199
|
+
- Wait it out (it usually goes away within a few days)
|
200
|
+
|
201
|
+
## Update Checks
|
202
|
+
|
203
|
+
The issue of using an outdated version of ytdl-core became so prevalent, that ytdl-core now checks for updates at run time, and every 12 hours. If it finds an update, it will print a warning to the console advising you to update. Due to the nature of this library, it is important to always use the latest version as YouTube continues to update.
|
204
|
+
|
205
|
+
If you'd like to disable this update check, you can do so by providing the `YTDL_NO_UPDATE` env variable.
|
206
|
+
|
207
|
+
```
|
208
|
+
env YTDL_NO_UPDATE=1 node myapp.js
|
209
|
+
```
|
package/lib/agent.js
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
const { ProxyAgent } = require("undici");
|
2
|
+
const { Cookie, CookieJar, canonicalDomain } = require("tough-cookie");
|
3
|
+
const { CookieAgent, CookieClient } = require("http-cookie-agent/undici");
|
4
|
+
|
5
|
+
const convertSameSite = sameSite => {
|
6
|
+
switch (sameSite) {
|
7
|
+
case "strict":
|
8
|
+
return "strict";
|
9
|
+
case "lax":
|
10
|
+
return "lax";
|
11
|
+
case "no_restriction":
|
12
|
+
case "unspecified":
|
13
|
+
default:
|
14
|
+
return "none";
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
const convertCookie = cookie =>
|
19
|
+
cookie instanceof Cookie
|
20
|
+
? cookie
|
21
|
+
: new Cookie({
|
22
|
+
key: cookie.name,
|
23
|
+
value: cookie.value,
|
24
|
+
expires: typeof cookie.expirationDate === "number" ? new Date(cookie.expirationDate * 1000) : "Infinity",
|
25
|
+
domain: canonicalDomain(cookie.domain),
|
26
|
+
path: cookie.path,
|
27
|
+
secure: cookie.secure,
|
28
|
+
httpOnly: cookie.httpOnly,
|
29
|
+
sameSite: convertSameSite(cookie.sameSite),
|
30
|
+
hostOnly: cookie.hostOnly,
|
31
|
+
});
|
32
|
+
|
33
|
+
const addCookies = (exports.addCookies = (jar, cookies) => {
|
34
|
+
if (!cookies || !Array.isArray(cookies)) {
|
35
|
+
throw new Error("cookies must be an array");
|
36
|
+
}
|
37
|
+
if (!cookies.some(c => c.name === "SOCS")) {
|
38
|
+
cookies.push({
|
39
|
+
domain: ".youtube.com",
|
40
|
+
hostOnly: false,
|
41
|
+
httpOnly: false,
|
42
|
+
name: "SOCS",
|
43
|
+
path: "/",
|
44
|
+
sameSite: "lax",
|
45
|
+
secure: true,
|
46
|
+
session: false,
|
47
|
+
value: "CAI",
|
48
|
+
});
|
49
|
+
}
|
50
|
+
for (const cookie of cookies) {
|
51
|
+
jar.setCookieSync(convertCookie(cookie), "https://www.youtube.com");
|
52
|
+
}
|
53
|
+
});
|
54
|
+
|
55
|
+
exports.addCookiesFromString = (jar, cookies) => {
|
56
|
+
if (!cookies || typeof cookies !== "string") {
|
57
|
+
throw new Error("cookies must be a string");
|
58
|
+
}
|
59
|
+
return addCookies(
|
60
|
+
jar,
|
61
|
+
cookies
|
62
|
+
.split(";")
|
63
|
+
.map(c => Cookie.parse(c))
|
64
|
+
.filter(Boolean),
|
65
|
+
);
|
66
|
+
};
|
67
|
+
|
68
|
+
const createAgent = (exports.createAgent = (cookies = [], opts = {}) => {
|
69
|
+
const options = Object.assign({}, opts);
|
70
|
+
if (!options.cookies) {
|
71
|
+
const jar = new CookieJar();
|
72
|
+
addCookies(jar, cookies);
|
73
|
+
options.cookies = { jar };
|
74
|
+
}
|
75
|
+
return {
|
76
|
+
dispatcher: new CookieAgent(options),
|
77
|
+
localAddress: options.localAddress,
|
78
|
+
jar: options.cookies.jar,
|
79
|
+
};
|
80
|
+
});
|
81
|
+
|
82
|
+
exports.createProxyAgent = (options, cookies = []) => {
|
83
|
+
if (!cookies) cookies = [];
|
84
|
+
if (typeof options === "string") options = { uri: options };
|
85
|
+
if (options.factory) throw new Error("Cannot use factory with createProxyAgent");
|
86
|
+
const jar = new CookieJar();
|
87
|
+
addCookies(jar, cookies);
|
88
|
+
const proxyOptions = Object.assign(
|
89
|
+
{
|
90
|
+
factory: (origin, opts) => {
|
91
|
+
const o = Object.assign({ cookies: { jar } }, opts);
|
92
|
+
return new CookieClient(origin, o);
|
93
|
+
},
|
94
|
+
},
|
95
|
+
options,
|
96
|
+
);
|
97
|
+
return { dispatcher: new ProxyAgent(proxyOptions), jar, localAddress: options.localAddress };
|
98
|
+
};
|
99
|
+
|
100
|
+
exports.defaultAgent = createAgent();
|
package/lib/cache.js
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
const { setTimeout } = require("timers");
|
2
|
+
|
3
|
+
// A cache that expires.
|
4
|
+
module.exports = class Cache extends Map {
|
5
|
+
constructor(timeout = 1000) {
|
6
|
+
super();
|
7
|
+
this.timeout = timeout;
|
8
|
+
}
|
9
|
+
set(key, value) {
|
10
|
+
if (this.has(key)) {
|
11
|
+
clearTimeout(super.get(key).tid);
|
12
|
+
}
|
13
|
+
super.set(key, {
|
14
|
+
tid: setTimeout(this.delete.bind(this, key), this.timeout).unref(),
|
15
|
+
value,
|
16
|
+
});
|
17
|
+
}
|
18
|
+
get(key) {
|
19
|
+
let entry = super.get(key);
|
20
|
+
if (entry) {
|
21
|
+
return entry.value;
|
22
|
+
}
|
23
|
+
return null;
|
24
|
+
}
|
25
|
+
getOrSet(key, fn) {
|
26
|
+
if (this.has(key)) {
|
27
|
+
return this.get(key);
|
28
|
+
} else {
|
29
|
+
let value = fn();
|
30
|
+
this.set(key, value);
|
31
|
+
(async () => {
|
32
|
+
try {
|
33
|
+
await value;
|
34
|
+
} catch (err) {
|
35
|
+
this.delete(key);
|
36
|
+
}
|
37
|
+
})();
|
38
|
+
return value;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
delete(key) {
|
42
|
+
let entry = super.get(key);
|
43
|
+
if (entry) {
|
44
|
+
clearTimeout(entry.tid);
|
45
|
+
super.delete(key);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
clear() {
|
49
|
+
for (let entry of this.values()) {
|
50
|
+
clearTimeout(entry.tid);
|
51
|
+
}
|
52
|
+
super.clear();
|
53
|
+
}
|
54
|
+
};
|
@@ -0,0 +1,218 @@
|
|
1
|
+
const utils = require("./utils");
|
2
|
+
const FORMATS = require("./formats");
|
3
|
+
|
4
|
+
// Use these to help sort formats, higher index is better.
|
5
|
+
const audioEncodingRanks = ["mp4a", "mp3", "vorbis", "aac", "opus", "flac"];
|
6
|
+
const videoEncodingRanks = ["mp4v", "avc1", "Sorenson H.283", "MPEG-4 Visual", "VP8", "VP9", "H.264"];
|
7
|
+
|
8
|
+
const getVideoBitrate = format => format.bitrate || 0;
|
9
|
+
const getVideoEncodingRank = format =>
|
10
|
+
videoEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
|
11
|
+
const getAudioBitrate = format => format.audioBitrate || 0;
|
12
|
+
const getAudioEncodingRank = format =>
|
13
|
+
audioEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Sort formats by a list of functions.
|
17
|
+
*
|
18
|
+
* @param {Object} a
|
19
|
+
* @param {Object} b
|
20
|
+
* @param {Array.<Function>} sortBy
|
21
|
+
* @returns {number}
|
22
|
+
*/
|
23
|
+
const sortFormatsBy = (a, b, sortBy) => {
|
24
|
+
let res = 0;
|
25
|
+
for (let fn of sortBy) {
|
26
|
+
res = fn(b) - fn(a);
|
27
|
+
if (res !== 0) {
|
28
|
+
break;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
return res;
|
32
|
+
};
|
33
|
+
|
34
|
+
const sortFormatsByVideo = (a, b) =>
|
35
|
+
sortFormatsBy(a, b, [format => parseInt(format.qualityLabel), getVideoBitrate, getVideoEncodingRank]);
|
36
|
+
|
37
|
+
const sortFormatsByAudio = (a, b) => sortFormatsBy(a, b, [getAudioBitrate, getAudioEncodingRank]);
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Sort formats from highest quality to lowest.
|
41
|
+
*
|
42
|
+
* @param {Object} a
|
43
|
+
* @param {Object} b
|
44
|
+
* @returns {number}
|
45
|
+
*/
|
46
|
+
exports.sortFormats = (a, b) =>
|
47
|
+
sortFormatsBy(a, b, [
|
48
|
+
// Formats with both video and audio are ranked highest.
|
49
|
+
format => +!!format.isHLS,
|
50
|
+
format => +!!format.isDashMPD,
|
51
|
+
format => +(format.contentLength > 0),
|
52
|
+
format => +(format.hasVideo && format.hasAudio),
|
53
|
+
format => +format.hasVideo,
|
54
|
+
format => parseInt(format.qualityLabel) || 0,
|
55
|
+
getVideoBitrate,
|
56
|
+
getAudioBitrate,
|
57
|
+
getVideoEncodingRank,
|
58
|
+
getAudioEncodingRank,
|
59
|
+
]);
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Choose a format depending on the given options.
|
63
|
+
*
|
64
|
+
* @param {Array.<Object>} formats
|
65
|
+
* @param {Object} options
|
66
|
+
* @returns {Object}
|
67
|
+
* @throws {Error} when no format matches the filter/format rules
|
68
|
+
*/
|
69
|
+
exports.chooseFormat = (formats, options) => {
|
70
|
+
if (typeof options.format === "object") {
|
71
|
+
if (!options.format.url) {
|
72
|
+
throw Error("Invalid format given, did you use `ytdl.getInfo()`?");
|
73
|
+
}
|
74
|
+
return options.format;
|
75
|
+
}
|
76
|
+
|
77
|
+
if (options.filter) {
|
78
|
+
formats = exports.filterFormats(formats, options.filter);
|
79
|
+
}
|
80
|
+
|
81
|
+
// We currently only support HLS-Formats for livestreams
|
82
|
+
// So we (now) remove all non-HLS streams
|
83
|
+
if (formats.some(fmt => fmt.isHLS)) {
|
84
|
+
formats = formats.filter(fmt => fmt.isHLS || !fmt.isLive);
|
85
|
+
}
|
86
|
+
|
87
|
+
let format;
|
88
|
+
const quality = options.quality || "highest";
|
89
|
+
switch (quality) {
|
90
|
+
case "highest":
|
91
|
+
format = formats[0];
|
92
|
+
break;
|
93
|
+
|
94
|
+
case "lowest":
|
95
|
+
format = formats[formats.length - 1];
|
96
|
+
break;
|
97
|
+
|
98
|
+
case "highestaudio": {
|
99
|
+
formats = exports.filterFormats(formats, "audio");
|
100
|
+
formats.sort(sortFormatsByAudio);
|
101
|
+
// Filter for only the best audio format
|
102
|
+
const bestAudioFormat = formats[0];
|
103
|
+
formats = formats.filter(f => sortFormatsByAudio(bestAudioFormat, f) === 0);
|
104
|
+
// Check for the worst video quality for the best audio quality and pick according
|
105
|
+
// This does not loose default sorting of video encoding and bitrate
|
106
|
+
const worstVideoQuality = formats.map(f => parseInt(f.qualityLabel) || 0).sort((a, b) => a - b)[0];
|
107
|
+
format = formats.find(f => (parseInt(f.qualityLabel) || 0) === worstVideoQuality);
|
108
|
+
break;
|
109
|
+
}
|
110
|
+
|
111
|
+
case "lowestaudio":
|
112
|
+
formats = exports.filterFormats(formats, "audio");
|
113
|
+
formats.sort(sortFormatsByAudio);
|
114
|
+
format = formats[formats.length - 1];
|
115
|
+
break;
|
116
|
+
|
117
|
+
case "highestvideo": {
|
118
|
+
formats = exports.filterFormats(formats, "video");
|
119
|
+
formats.sort(sortFormatsByVideo);
|
120
|
+
// Filter for only the best video format
|
121
|
+
const bestVideoFormat = formats[0];
|
122
|
+
formats = formats.filter(f => sortFormatsByVideo(bestVideoFormat, f) === 0);
|
123
|
+
// Check for the worst audio quality for the best video quality and pick according
|
124
|
+
// This does not loose default sorting of audio encoding and bitrate
|
125
|
+
const worstAudioQuality = formats.map(f => f.audioBitrate || 0).sort((a, b) => a - b)[0];
|
126
|
+
format = formats.find(f => (f.audioBitrate || 0) === worstAudioQuality);
|
127
|
+
break;
|
128
|
+
}
|
129
|
+
|
130
|
+
case "lowestvideo":
|
131
|
+
formats = exports.filterFormats(formats, "video");
|
132
|
+
formats.sort(sortFormatsByVideo);
|
133
|
+
format = formats[formats.length - 1];
|
134
|
+
break;
|
135
|
+
|
136
|
+
default:
|
137
|
+
format = getFormatByQuality(quality, formats);
|
138
|
+
break;
|
139
|
+
}
|
140
|
+
|
141
|
+
if (!format) {
|
142
|
+
throw Error(`No such format found: ${quality}`);
|
143
|
+
}
|
144
|
+
return format;
|
145
|
+
};
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Gets a format based on quality or array of quality's
|
149
|
+
*
|
150
|
+
* @param {string|[string]} quality
|
151
|
+
* @param {[Object]} formats
|
152
|
+
* @returns {Object}
|
153
|
+
*/
|
154
|
+
const getFormatByQuality = (quality, formats) => {
|
155
|
+
let getFormat = itag => formats.find(format => `${format.itag}` === `${itag}`);
|
156
|
+
if (Array.isArray(quality)) {
|
157
|
+
return getFormat(quality.find(q => getFormat(q)));
|
158
|
+
} else {
|
159
|
+
return getFormat(quality);
|
160
|
+
}
|
161
|
+
};
|
162
|
+
|
163
|
+
/**
|
164
|
+
* @param {Array.<Object>} formats
|
165
|
+
* @param {Function} filter
|
166
|
+
* @returns {Array.<Object>}
|
167
|
+
*/
|
168
|
+
exports.filterFormats = (formats, filter) => {
|
169
|
+
let fn;
|
170
|
+
switch (filter) {
|
171
|
+
case "videoandaudio":
|
172
|
+
case "audioandvideo":
|
173
|
+
fn = format => format.hasVideo && format.hasAudio;
|
174
|
+
break;
|
175
|
+
|
176
|
+
case "video":
|
177
|
+
fn = format => format.hasVideo;
|
178
|
+
break;
|
179
|
+
|
180
|
+
case "videoonly":
|
181
|
+
fn = format => format.hasVideo && !format.hasAudio;
|
182
|
+
break;
|
183
|
+
|
184
|
+
case "audio":
|
185
|
+
fn = format => format.hasAudio;
|
186
|
+
break;
|
187
|
+
|
188
|
+
case "audioonly":
|
189
|
+
fn = format => !format.hasVideo && format.hasAudio;
|
190
|
+
break;
|
191
|
+
|
192
|
+
default:
|
193
|
+
if (typeof filter === "function") {
|
194
|
+
fn = filter;
|
195
|
+
} else {
|
196
|
+
throw TypeError(`Given filter (${filter}) is not supported`);
|
197
|
+
}
|
198
|
+
}
|
199
|
+
return formats.filter(format => !!format.url && fn(format));
|
200
|
+
};
|
201
|
+
|
202
|
+
/**
|
203
|
+
* @param {Object} format
|
204
|
+
* @returns {Object}
|
205
|
+
*/
|
206
|
+
exports.addFormatMeta = format => {
|
207
|
+
format = Object.assign({}, FORMATS[format.itag], format);
|
208
|
+
format.hasVideo = !!format.qualityLabel;
|
209
|
+
format.hasAudio = !!format.audioBitrate;
|
210
|
+
format.container = format.mimeType ? format.mimeType.split(";")[0].split("/")[1] : null;
|
211
|
+
format.codecs = format.mimeType ? utils.between(format.mimeType, 'codecs="', '"') : null;
|
212
|
+
format.videoCodec = format.hasVideo && format.codecs ? format.codecs.split(", ")[0] : null;
|
213
|
+
format.audioCodec = format.hasAudio && format.codecs ? format.codecs.split(", ").slice(-1)[0] : null;
|
214
|
+
format.isLive = /\bsource[/=]yt_live_broadcast\b/.test(format.url);
|
215
|
+
format.isHLS = /\/manifest\/hls_(variant|playlist)\//.test(format.url);
|
216
|
+
format.isDashMPD = /\/manifest\/dash\//.test(format.url);
|
217
|
+
return format;
|
218
|
+
};
|