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