@oreohq/ytdl-core 4.15.1 → 4.16.5

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/README.md CHANGED
@@ -1,50 +1,42 @@
1
1
  # @oreohq/ytdl-core
2
+ Oreo 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.
2
3
 
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
+ ![Builders](https://i.imgur.com/a9owl5Q.png)
4
5
 
5
- ## Installation
6
+ <p align="center">
7
+ <img src="https://i.imgur.com/cBiIazb.png" alt="badge" />
8
+ <img src="https://i.imgur.com/1EIFuXp.png" alt="badge" />
9
+ </p>
6
10
 
11
+
12
+ ## 📦 Installation
7
13
  ```bash
8
14
  npm install @oreohq/ytdl-core@latest
9
15
  ```
10
16
 
11
- Make sure you're installing the latest version of `@oreohq/ytdl-core` to keep up with the latest fixes.
12
-
13
- ## Usage
14
17
 
18
+ ## 🪴 Usage
15
19
  ```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"));
20
+ const ytdl = require('@oreohq/ytdl-core');
23
21
 
24
- // Get video info
25
- ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY").then(info => {
26
- console.log(info.videoDetails.title);
27
- });
22
+ const videoInfo = await ytdl.getBasicInfo('https://www.youtube.com/watch?v=2QjJMT554Bc');
23
+ console.log(videoInfo);
28
24
 
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
- });
25
+ // Download a video
26
+ ytdl('https://www.youtube.com/watch?v=2QjJMT554Bc').pipe(require('fs').createWriteStream('video.mp4'));
33
27
  ```
34
28
 
35
- ### Cookies Support
36
29
 
30
+ ### 🍪 Cookies Support
37
31
  ```js
38
- const ytdl = require("@oreohq/ytdl-core");
32
+ const ytdl = require('@oreohq/ytdl-core');
39
33
 
40
- // (Optional) Below are examples, NOT the recommended options
41
34
  const cookies = [
42
35
  { name: "cookie1", value: "COOKIE1_HERE" },
43
36
  { name: "cookie2", value: "COOKIE2_HERE" },
44
37
  ];
45
38
 
46
- // (Optional) http-cookie-agent / undici agent options
47
- // Below are examples, NOT the recommended options
39
+ // http-cookie-agent / undici agent options
48
40
  const agentOptions = {
49
41
  pipelining: 5,
50
42
  maxRedirections: 0,
@@ -53,13 +45,11 @@ const agentOptions = {
53
45
 
54
46
  // agent should be created once if you don't want to change your cookie
55
47
  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 });
48
+ ytdl.getBasicInfo('https://www.youtube.com/watch?v=2QjJMT554Bc' { agent });
59
49
  ```
60
50
 
61
- #### How to get cookies
62
51
 
52
+ #### How to get cookies
63
53
  - Install [EditThisCookie](http://www.editthiscookie.com/) extension for your browser.
64
54
  - Go to [YouTube](https://www.youtube.com/).
65
55
  - Log in to your account. (You should use a new account for this purpose)
@@ -71,139 +61,58 @@ ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
71
61
  > You can delete your browser's cookies to log it out on your browser.
72
62
  > Or use incognito mode to get your cookies then close it.
73
63
 
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
64
 
109
65
  ### Proxy Support
110
-
111
66
  ```js
112
- const ytdl = require("@oreohq/ytdl-core");
67
+ const ytdl = require('@oreohq/ytdl-core');
113
68
 
114
- const agent = ytdl.createProxyAgent({ uri: "my.proxy.server" });
69
+ const agent = ytdl.createProxyAgent({ uri: 'my.proxy.server' });
70
+ const agentWithCookies = ytdl.createProxyAgent({ uri: 'my.proxy.server' }, [{ name: 'cookie', value: 'COOKIE_HERE' }]);
115
71
 
116
- ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
117
- ytdl.getInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent });
72
+ ytdl.getBasicInfo('https://www.youtube.com/watch?v=2QjJMT554Bc' { agent });
73
+ ytdl.getBasicInfo('https://www.youtube.com/watch?v=2QjJMT554Bc' { agentWithCookies });
118
74
  ```
119
75
 
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
76
 
131
77
  ### 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
78
  To implement IP rotation, you need to assign the desired IP address to the `localAddress` property within `undici.Agent.Options`.
136
79
  Therefore, you'll need to use a different `ytdl.Agent` for each IP address you want to use.
137
-
138
80
  ```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
- });
81
+ const { getRandomIPv6 } = require('@oreohq/ytdl-core/lib/utils');
82
+ const ytdl = require('@oreohq/ytdl-core');
145
83
 
146
- ytdl.getBasicInfo("https://www.youtube.com/watch?v=G9KVla9gwbY", { agent: agentForARandomIP });
84
+ const proxyAgent1 = ytdl.createAgent(undefined, { localAddress: getRandomIPv6('2001:2::/48') });
85
+ ytdl.getBasicInfo('https://www.youtube.com/watch?v=2QjJMT554Bc', { agent: proxyAgent1 });
147
86
 
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 });
87
+ const proxyAgent2 = ytdl.createAgent(undefined, { localAddress: getRandomIPv6('2001:2::/48') });
88
+ ytdl.getBasicInfo('https://www.youtube.com/watch?v=2QjJMT554Bc', { agent: proxyAgent2 });
153
89
  ```
154
90
 
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
91
 
92
+ ## 👻 Rate Limiting
191
93
  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
94
 
193
95
  - Update `@oreohq/ytdl-core` to the latest version
194
96
  - Use proxies (you can find an example [here](#proxy-support))
195
97
  - 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
98
+ - Use cookies (you can find an example [here](#cookies-support)) (wait for current ratelimits to expire first)
199
99
  - Wait it out (it usually goes away within a few days)
200
100
 
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
101
 
102
+ # 🚛 Update Checks
103
+ 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.
104
+ Due to the nature of this library, it is important to always use the latest version as YouTube continues to update.
205
105
  If you'd like to disable this update check, you can do so by providing the `YTDL_NO_UPDATE` env variable.
106
+ ```
107
+ env YTDL_NO_UPDATE=1
108
+ ```
206
109
 
110
+ If you'd like to have it send a webhook message on discord, you can do so by providing the following env variables.
207
111
  ```
208
- env YTDL_NO_UPDATE=1 node myapp.js
209
- ```
112
+ YTDL_WEBHOOK_URL="Your Webhook Url"
113
+ YTDL_WEBHOOK_MENTIONS="String with mentions"
114
+ ```
115
+
116
+
117
+ ## **❔ Support**
118
+ <a href="https://discord.gg/invite/GaczkwfgV9"><img src="https://invidget.switchblade.xyz/GaczkwfgV9" alt="Discord"></a>
package/lib/agent.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { ProxyAgent } = require("undici");
2
+ const { HttpsProxyAgent } = require("https-proxy-agent");
2
3
  const { Cookie, CookieJar, canonicalDomain } = require("tough-cookie");
3
4
  const { CookieAgent, CookieClient } = require("http-cookie-agent/undici");
4
5
 
@@ -94,7 +95,14 @@ exports.createProxyAgent = (options, cookies = []) => {
94
95
  },
95
96
  options,
96
97
  );
97
- return { dispatcher: new ProxyAgent(proxyOptions), jar, localAddress: options.localAddress };
98
+
99
+ // ProxyAgent type that node httplibrary supports
100
+ const agent = new HttpsProxyAgent(options.uri);
101
+
102
+ // ProxyAgent type that undici supports
103
+ const dispatcher = new ProxyAgent(proxyOptions);
104
+
105
+ return { dispatcher, agent, jar, localAddress: options.localAddress };
98
106
  };
99
107
 
100
108
  exports.defaultAgent = createAgent();
@@ -6,11 +6,9 @@ const audioEncodingRanks = ["mp4a", "mp3", "vorbis", "aac", "opus", "flac"];
6
6
  const videoEncodingRanks = ["mp4v", "avc1", "Sorenson H.283", "MPEG-4 Visual", "VP8", "VP9", "H.264"];
7
7
 
8
8
  const getVideoBitrate = format => format.bitrate || 0;
9
- const getVideoEncodingRank = format =>
10
- videoEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
9
+ const getVideoEncodingRank = format => videoEncodingRanks.findIndex(enc => format.codecs?.includes(enc));
11
10
  const getAudioBitrate = format => format.audioBitrate || 0;
12
- const getAudioEncodingRank = format =>
13
- audioEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
11
+ const getAudioEncodingRank = format => audioEncodingRanks.findIndex(enc => format.codecs?.includes(enc));
14
12
 
15
13
  /**
16
14
  * Sort formats by a list of functions.
package/lib/index.js CHANGED
@@ -42,9 +42,7 @@ ytdl.cache = {
42
42
  ytdl.version = require("../package.json").version;
43
43
 
44
44
  const createStream = options => {
45
- const stream = new PassThrough({
46
- highWaterMark: (options && options.highWaterMark) || 1024 * 512,
47
- });
45
+ const stream = new PassThrough({ highWaterMark: options?.highWaterMark || 1024 * 512 });
48
46
  stream._destroy = () => {
49
47
  stream.destroyed = true;
50
48
  };
@@ -105,7 +103,11 @@ const downloadFromInfoCallback = (stream, info, options) => {
105
103
  localAddress: utils.getRandomIPv6(options.IPv6Block),
106
104
  });
107
105
  }
106
+
108
107
  if (options.agent) {
108
+ // Set agent on both the miniget and m3u8stream requests
109
+ options.requestOptions.agent = options.agent.agent;
110
+
109
111
  if (options.agent.jar) {
110
112
  utils.setPropInsensitive(
111
113
  options.requestOptions.headers,
@@ -129,6 +131,7 @@ const downloadFromInfoCallback = (stream, info, options) => {
129
131
  chunkReadahead: +info.live_chunk_readahead,
130
132
  begin: options.begin || (format.isLive && Date.now()),
131
133
  liveBuffer: options.liveBuffer,
134
+ // Now we have passed not only custom "dispatcher" with undici ProxyAgent, but also "agent" field which is compatible for node http
132
135
  requestOptions: options.requestOptions,
133
136
  parser: format.isDashMPD ? "dash-mpd" : "m3u8",
134
137
  id: format.itag,
@@ -148,9 +151,9 @@ const downloadFromInfoCallback = (stream, info, options) => {
148
151
  let shouldBeChunked = dlChunkSize !== 0 && (!format.hasAudio || !format.hasVideo);
149
152
 
150
153
  if (shouldBeChunked) {
151
- let start = (options.range && options.range.start) || 0;
154
+ let start = options.range?.start || 0;
152
155
  let end = start + dlChunkSize;
153
- const rangeEnd = options.range && options.range.end;
156
+ const rangeEnd = options.range?.end;
154
157
 
155
158
  contentLength = options.range
156
159
  ? (rangeEnd ? rangeEnd + 1 : parseInt(format.contentLength)) - start
@@ -183,7 +186,7 @@ const downloadFromInfoCallback = (stream, info, options) => {
183
186
  if (options.begin) {
184
187
  format.url += `&begin=${parseTimestamp(options.begin)}`;
185
188
  }
186
- if (options.range && (options.range.start || options.range.end)) {
189
+ if (options.range?.start || options.range?.end) {
187
190
  requestOptions.headers = Object.assign({}, requestOptions.headers, {
188
191
  Range: `bytes=${options.range.start || "0"}-${options.range.end || ""}`,
189
192
  });
@@ -7,7 +7,7 @@ const TITLE_TO_CATEGORY = {
7
7
  song: { name: "Music", url: "https://music.youtube.com/" },
8
8
  };
9
9
 
10
- const getText = obj => (obj ? (obj.runs ? obj.runs[0].text : obj.simpleText) : null);
10
+ const getText = obj => obj?.runs?.[0]?.text ?? obj?.simpleText;
11
11
 
12
12
  /**
13
13
  * Get video media.
@@ -38,7 +38,7 @@ exports.getMedia = info => {
38
38
  let contents = row.metadataRowRenderer.contents[0];
39
39
  media[title] = getText(contents);
40
40
  let runs = contents.runs;
41
- if (runs && runs[0].navigationEndpoint) {
41
+ if (runs?.[0]?.navigationEndpoint) {
42
42
  media[`${title}_url`] = new URL(
43
43
  runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url,
44
44
  BASE_URL,
@@ -76,7 +76,7 @@ exports.getMedia = info => {
76
76
  return media;
77
77
  };
78
78
 
79
- const isVerified = badges => !!(badges && badges.find(b => b.metadataBadgeRenderer.tooltip === "Verified"));
79
+ const isVerified = badges => !!badges?.find(b => b.metadataBadgeRenderer.tooltip === "Verified");
80
80
 
81
81
  /**
82
82
  * Get video author.
@@ -91,12 +91,7 @@ exports.getAuthor = info => {
91
91
  verified = false;
92
92
  try {
93
93
  let results = info.response.contents.twoColumnWatchNextResults.results.results.contents;
94
- let v = results.find(
95
- v2 =>
96
- v2.videoSecondaryInfoRenderer &&
97
- v2.videoSecondaryInfoRenderer.owner &&
98
- v2.videoSecondaryInfoRenderer.owner.videoOwnerRenderer,
99
- );
94
+ let v = results.find(v2 => v2?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer);
100
95
  let videoOwnerRenderer = v.videoSecondaryInfoRenderer.owner.videoOwnerRenderer;
101
96
  channelId = videoOwnerRenderer.navigationEndpoint.browseEndpoint.browseId;
102
97
  thumbnails = videoOwnerRenderer.thumbnail.thumbnails.map(thumbnail => {
@@ -109,12 +104,12 @@ exports.getAuthor = info => {
109
104
  // Do nothing.
110
105
  }
111
106
  try {
112
- let videoDetails = info.player_response.microformat && info.player_response.microformat.playerMicroformatRenderer;
113
- let id = (videoDetails && videoDetails.channelId) || channelId || info.player_response.videoDetails.channelId;
107
+ let videoDetails = info.player_response.microformat?.playerMicroformatRenderer;
108
+ let id = videoDetails?.channelId || channelId || info.player_response.videoDetails.channelId;
114
109
  let author = {
115
110
  id: id,
116
- name: videoDetails ? videoDetails.ownerChannelName : info.player_response.videoDetails.author,
117
- user: videoDetails ? videoDetails.ownerProfileUrl.split("/").slice(-1)[0] : null,
111
+ name: videoDetails?.ownerChannelName ?? info.player_response.videoDetails.author,
112
+ user: videoDetails?.ownerProfileUrl.split("/").slice(-1)[0] ?? null,
118
113
  channel_url: `https://www.youtube.com/channel/${id}`,
119
114
  external_channel_url: videoDetails ? `https://www.youtube.com/channel/${videoDetails.externalChannelId}` : "",
120
115
  user_url: videoDetails ? new URL(videoDetails.ownerProfileUrl, BASE_URL).toString() : "",
@@ -138,7 +133,7 @@ const parseRelatedVideo = (details, rvsParams) => {
138
133
  let shortViewCount = getText(details.shortViewCountText);
139
134
  let rvsDetails = rvsParams.find(elem => elem.id === details.videoId);
140
135
  if (!/^\d/.test(shortViewCount)) {
141
- shortViewCount = (rvsDetails && rvsDetails.short_view_count_text) || "";
136
+ shortViewCount = rvsDetails?.short_view_count_text || "";
142
137
  }
143
138
  viewCount = (/^\d/.test(viewCount) ? viewCount : shortViewCount).split(" ")[0];
144
139
  let browseEndpoint = details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint;
@@ -173,12 +168,14 @@ const parseRelatedVideo = (details, rvsParams) => {
173
168
  view_count: viewCount.replace(/,/g, ""),
174
169
  length_seconds: details.lengthText
175
170
  ? Math.floor(parseTimestamp(getText(details.lengthText)) / 1000)
176
- : rvsParams && `${rvsParams.length_seconds}`,
171
+ : rvsParams
172
+ ? `${rvsParams.length_seconds}`
173
+ : undefined,
177
174
  thumbnails: details.thumbnail.thumbnails,
178
175
  richThumbnails: details.richThumbnail
179
176
  ? details.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails
180
177
  : [],
181
- isLive: !!(details.badges && details.badges.find(b => b.metadataBadgeRenderer.label === "LIVE NOW")),
178
+ isLive: !!details.badges?.find(b => b.metadataBadgeRenderer.label === "LIVE NOW"),
182
179
  };
183
180
 
184
181
  utils.deprecate(
@@ -288,7 +285,7 @@ exports.cleanVideoDetails = (videoDetails, info) => {
288
285
 
289
286
  // Use more reliable `lengthSeconds` from `playerMicroformatRenderer`.
290
287
  videoDetails.lengthSeconds =
291
- (info.player_response.microformat && info.player_response.microformat.playerMicroformatRenderer.lengthSeconds) ||
288
+ info.player_response.microformat?.playerMicroformatRenderer?.lengthSeconds ||
292
289
  info.player_response.videoDetails.lengthSeconds;
293
290
  return videoDetails;
294
291
  };
@@ -300,11 +297,7 @@ exports.cleanVideoDetails = (videoDetails, info) => {
300
297
  * @returns {Array.<Object>}
301
298
  */
302
299
  exports.getStoryboards = info => {
303
- const parts =
304
- info.player_response.storyboards &&
305
- info.player_response.storyboards.playerStoryboardSpecRenderer &&
306
- info.player_response.storyboards.playerStoryboardSpecRenderer.spec &&
307
- info.player_response.storyboards.playerStoryboardSpecRenderer.spec.split("|");
300
+ const parts = info.player_response?.storyboards?.playerStoryboardSpecRenderer?.spec?.split("|");
308
301
 
309
302
  if (!parts) return [];
310
303
 
@@ -342,16 +335,10 @@ exports.getStoryboards = info => {
342
335
  * @returns {Array.<Object>}
343
336
  */
344
337
  exports.getChapters = info => {
345
- const playerOverlayRenderer =
346
- info.response && info.response.playerOverlays && info.response.playerOverlays.playerOverlayRenderer;
347
- const playerBar =
348
- playerOverlayRenderer &&
349
- playerOverlayRenderer.decoratedPlayerBarRenderer &&
350
- playerOverlayRenderer.decoratedPlayerBarRenderer.decoratedPlayerBarRenderer &&
351
- playerOverlayRenderer.decoratedPlayerBarRenderer.decoratedPlayerBarRenderer.playerBar;
352
- const markersMap =
353
- playerBar && playerBar.multiMarkersPlayerBarRenderer && playerBar.multiMarkersPlayerBarRenderer.markersMap;
354
- const marker = Array.isArray(markersMap) && markersMap.find(m => m.value && Array.isArray(m.value.chapters));
338
+ const playerOverlayRenderer = info.response?.playerOverlays?.playerOverlayRenderer;
339
+ const playerBar = playerOverlayRenderer?.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer?.playerBar;
340
+ const markersMap = playerBar?.multiMarkersPlayerBarRenderer?.markersMap;
341
+ const marker = Array.isArray(markersMap) && markersMap.find(m => Array.isArray(m.value?.chapters));
355
342
  if (!marker) return [];
356
343
  const chapters = marker.value.chapters;
357
344