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