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