@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/lib/index.js
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
const PassThrough = require("stream").PassThrough;
|
2
|
+
const getInfo = require("./info");
|
3
|
+
const utils = require("./utils");
|
4
|
+
const formatUtils = require("./format-utils");
|
5
|
+
const urlUtils = require("./url-utils");
|
6
|
+
const miniget = require("miniget");
|
7
|
+
const m3u8stream = require("m3u8stream");
|
8
|
+
const { parseTimestamp } = require("m3u8stream");
|
9
|
+
const agent = require("./agent");
|
10
|
+
|
11
|
+
/**
|
12
|
+
* @param {string} link
|
13
|
+
* @param {!Object} options
|
14
|
+
* @returns {ReadableStream}
|
15
|
+
*/
|
16
|
+
const ytdl = (link, options) => {
|
17
|
+
const stream = createStream(options);
|
18
|
+
ytdl.getInfo(link, options).then(
|
19
|
+
info => {
|
20
|
+
downloadFromInfoCallback(stream, info, options);
|
21
|
+
},
|
22
|
+
stream.emit.bind(stream, "error"),
|
23
|
+
);
|
24
|
+
return stream;
|
25
|
+
};
|
26
|
+
module.exports = ytdl;
|
27
|
+
|
28
|
+
ytdl.getBasicInfo = getInfo.getBasicInfo;
|
29
|
+
ytdl.getInfo = getInfo.getInfo;
|
30
|
+
ytdl.chooseFormat = formatUtils.chooseFormat;
|
31
|
+
ytdl.filterFormats = formatUtils.filterFormats;
|
32
|
+
ytdl.validateID = urlUtils.validateID;
|
33
|
+
ytdl.validateURL = urlUtils.validateURL;
|
34
|
+
ytdl.getURLVideoID = urlUtils.getURLVideoID;
|
35
|
+
ytdl.getVideoID = urlUtils.getVideoID;
|
36
|
+
ytdl.createAgent = agent.createAgent;
|
37
|
+
ytdl.createProxyAgent = agent.createProxyAgent;
|
38
|
+
ytdl.cache = {
|
39
|
+
info: getInfo.cache,
|
40
|
+
watch: getInfo.watchPageCache,
|
41
|
+
};
|
42
|
+
ytdl.version = require("../package.json").version;
|
43
|
+
|
44
|
+
const createStream = options => {
|
45
|
+
const stream = new PassThrough({
|
46
|
+
highWaterMark: (options && options.highWaterMark) || 1024 * 512,
|
47
|
+
});
|
48
|
+
stream._destroy = () => {
|
49
|
+
stream.destroyed = true;
|
50
|
+
};
|
51
|
+
return stream;
|
52
|
+
};
|
53
|
+
|
54
|
+
const pipeAndSetEvents = (req, stream, end) => {
|
55
|
+
// Forward events from the request to the stream.
|
56
|
+
["abort", "request", "response", "error", "redirect", "retry", "reconnect"].forEach(event => {
|
57
|
+
req.prependListener(event, stream.emit.bind(stream, event));
|
58
|
+
});
|
59
|
+
req.pipe(stream, { end });
|
60
|
+
};
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Chooses a format to download.
|
64
|
+
*
|
65
|
+
* @param {stream.Readable} stream
|
66
|
+
* @param {Object} info
|
67
|
+
* @param {Object} options
|
68
|
+
*/
|
69
|
+
const downloadFromInfoCallback = (stream, info, options) => {
|
70
|
+
options = options || {};
|
71
|
+
|
72
|
+
let err = utils.playError(info.player_response);
|
73
|
+
if (err) {
|
74
|
+
stream.emit("error", err);
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
|
78
|
+
if (!info.formats.length) {
|
79
|
+
stream.emit("error", Error("This video is unavailable"));
|
80
|
+
return;
|
81
|
+
}
|
82
|
+
|
83
|
+
let format;
|
84
|
+
try {
|
85
|
+
format = formatUtils.chooseFormat(info.formats, options);
|
86
|
+
} catch (e) {
|
87
|
+
stream.emit("error", e);
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
stream.emit("info", info, format);
|
91
|
+
if (stream.destroyed) {
|
92
|
+
return;
|
93
|
+
}
|
94
|
+
|
95
|
+
let contentLength,
|
96
|
+
downloaded = 0;
|
97
|
+
const ondata = chunk => {
|
98
|
+
downloaded += chunk.length;
|
99
|
+
stream.emit("progress", chunk.length, downloaded, contentLength);
|
100
|
+
};
|
101
|
+
|
102
|
+
utils.applyDefaultHeaders(options);
|
103
|
+
if (options.IPv6Block) {
|
104
|
+
options.requestOptions = Object.assign({}, options.requestOptions, {
|
105
|
+
localAddress: utils.getRandomIPv6(options.IPv6Block),
|
106
|
+
});
|
107
|
+
}
|
108
|
+
if (options.agent) {
|
109
|
+
if (options.agent.jar) {
|
110
|
+
utils.setPropInsensitive(
|
111
|
+
options.requestOptions.headers,
|
112
|
+
"cookie",
|
113
|
+
options.agent.jar.getCookieStringSync("https://www.youtube.com"),
|
114
|
+
);
|
115
|
+
}
|
116
|
+
if (options.agent.localAddress) {
|
117
|
+
options.requestOptions.localAddress = options.agent.localAddress;
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
// Download the file in chunks, in this case the default is 10MB,
|
122
|
+
// anything over this will cause youtube to throttle the download
|
123
|
+
const dlChunkSize = typeof options.dlChunkSize === "number" ? options.dlChunkSize : 1024 * 1024 * 10;
|
124
|
+
let req;
|
125
|
+
let shouldEnd = true;
|
126
|
+
|
127
|
+
if (format.isHLS || format.isDashMPD) {
|
128
|
+
req = m3u8stream(format.url, {
|
129
|
+
chunkReadahead: +info.live_chunk_readahead,
|
130
|
+
begin: options.begin || (format.isLive && Date.now()),
|
131
|
+
liveBuffer: options.liveBuffer,
|
132
|
+
requestOptions: options.requestOptions,
|
133
|
+
parser: format.isDashMPD ? "dash-mpd" : "m3u8",
|
134
|
+
id: format.itag,
|
135
|
+
});
|
136
|
+
|
137
|
+
req.on("progress", (segment, totalSegments) => {
|
138
|
+
stream.emit("progress", segment.size, segment.num, totalSegments);
|
139
|
+
});
|
140
|
+
pipeAndSetEvents(req, stream, shouldEnd);
|
141
|
+
} else {
|
142
|
+
const requestOptions = Object.assign({}, options.requestOptions, {
|
143
|
+
maxReconnects: 6,
|
144
|
+
maxRetries: 3,
|
145
|
+
backoff: { inc: 500, max: 10000 },
|
146
|
+
});
|
147
|
+
|
148
|
+
let shouldBeChunked = dlChunkSize !== 0 && (!format.hasAudio || !format.hasVideo);
|
149
|
+
|
150
|
+
if (shouldBeChunked) {
|
151
|
+
let start = (options.range && options.range.start) || 0;
|
152
|
+
let end = start + dlChunkSize;
|
153
|
+
const rangeEnd = options.range && options.range.end;
|
154
|
+
|
155
|
+
contentLength = options.range
|
156
|
+
? (rangeEnd ? rangeEnd + 1 : parseInt(format.contentLength)) - start
|
157
|
+
: parseInt(format.contentLength);
|
158
|
+
|
159
|
+
const getNextChunk = () => {
|
160
|
+
if (stream.destroyed) return;
|
161
|
+
if (!rangeEnd && end >= contentLength) end = 0;
|
162
|
+
if (rangeEnd && end > rangeEnd) end = rangeEnd;
|
163
|
+
shouldEnd = !end || end === rangeEnd;
|
164
|
+
|
165
|
+
requestOptions.headers = Object.assign({}, requestOptions.headers, {
|
166
|
+
Range: `bytes=${start}-${end || ""}`,
|
167
|
+
});
|
168
|
+
req = miniget(format.url, requestOptions);
|
169
|
+
req.on("data", ondata);
|
170
|
+
req.on("end", () => {
|
171
|
+
if (stream.destroyed) return;
|
172
|
+
if (end && end !== rangeEnd) {
|
173
|
+
start = end + 1;
|
174
|
+
end += dlChunkSize;
|
175
|
+
getNextChunk();
|
176
|
+
}
|
177
|
+
});
|
178
|
+
pipeAndSetEvents(req, stream, shouldEnd);
|
179
|
+
};
|
180
|
+
getNextChunk();
|
181
|
+
} else {
|
182
|
+
// Audio only and video only formats don't support begin
|
183
|
+
if (options.begin) {
|
184
|
+
format.url += `&begin=${parseTimestamp(options.begin)}`;
|
185
|
+
}
|
186
|
+
if (options.range && (options.range.start || options.range.end)) {
|
187
|
+
requestOptions.headers = Object.assign({}, requestOptions.headers, {
|
188
|
+
Range: `bytes=${options.range.start || "0"}-${options.range.end || ""}`,
|
189
|
+
});
|
190
|
+
}
|
191
|
+
req = miniget(format.url, requestOptions);
|
192
|
+
req.on("response", res => {
|
193
|
+
if (stream.destroyed) return;
|
194
|
+
contentLength = contentLength || parseInt(res.headers["content-length"]);
|
195
|
+
});
|
196
|
+
req.on("data", ondata);
|
197
|
+
pipeAndSetEvents(req, stream, shouldEnd);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
stream._destroy = () => {
|
202
|
+
stream.destroyed = true;
|
203
|
+
if (req) {
|
204
|
+
req.destroy();
|
205
|
+
req.end();
|
206
|
+
}
|
207
|
+
};
|
208
|
+
};
|
209
|
+
|
210
|
+
/**
|
211
|
+
* Can be used to download video after its `info` is gotten through
|
212
|
+
* `ytdl.getInfo()`. In case the user might want to look at the
|
213
|
+
* `info` object before deciding to download.
|
214
|
+
*
|
215
|
+
* @param {Object} info
|
216
|
+
* @param {!Object} options
|
217
|
+
* @returns {ReadableStream}
|
218
|
+
*/
|
219
|
+
ytdl.downloadFromInfo = (info, options) => {
|
220
|
+
const stream = createStream(options);
|
221
|
+
if (!info.full) {
|
222
|
+
throw Error("Cannot use `ytdl.downloadFromInfo()` when called with info from `ytdl.getBasicInfo()`");
|
223
|
+
}
|
224
|
+
setImmediate(() => {
|
225
|
+
downloadFromInfoCallback(stream, info, options);
|
226
|
+
});
|
227
|
+
return stream;
|
228
|
+
};
|
@@ -0,0 +1,362 @@
|
|
1
|
+
const utils = require("./utils");
|
2
|
+
const qs = require("querystring");
|
3
|
+
const { parseTimestamp } = require("m3u8stream");
|
4
|
+
|
5
|
+
const BASE_URL = "https://www.youtube.com/watch?v=";
|
6
|
+
const TITLE_TO_CATEGORY = {
|
7
|
+
song: { name: "Music", url: "https://music.youtube.com/" },
|
8
|
+
};
|
9
|
+
|
10
|
+
const getText = obj => (obj ? (obj.runs ? obj.runs[0].text : obj.simpleText) : null);
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Get video media.
|
14
|
+
*
|
15
|
+
* @param {Object} info
|
16
|
+
* @returns {Object}
|
17
|
+
*/
|
18
|
+
exports.getMedia = info => {
|
19
|
+
let media = {};
|
20
|
+
let results = [];
|
21
|
+
try {
|
22
|
+
results = info.response.contents.twoColumnWatchNextResults.results.results.contents;
|
23
|
+
} catch (err) {
|
24
|
+
// Do nothing
|
25
|
+
}
|
26
|
+
|
27
|
+
let result = results.find(v => v.videoSecondaryInfoRenderer);
|
28
|
+
if (!result) {
|
29
|
+
return {};
|
30
|
+
}
|
31
|
+
|
32
|
+
try {
|
33
|
+
let metadataRows = (result.metadataRowContainer || result.videoSecondaryInfoRenderer.metadataRowContainer)
|
34
|
+
.metadataRowContainerRenderer.rows;
|
35
|
+
for (let row of metadataRows) {
|
36
|
+
if (row.metadataRowRenderer) {
|
37
|
+
let title = getText(row.metadataRowRenderer.title).toLowerCase();
|
38
|
+
let contents = row.metadataRowRenderer.contents[0];
|
39
|
+
media[title] = getText(contents);
|
40
|
+
let runs = contents.runs;
|
41
|
+
if (runs && runs[0].navigationEndpoint) {
|
42
|
+
media[`${title}_url`] = new URL(
|
43
|
+
runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url,
|
44
|
+
BASE_URL,
|
45
|
+
).toString();
|
46
|
+
}
|
47
|
+
if (title in TITLE_TO_CATEGORY) {
|
48
|
+
media.category = TITLE_TO_CATEGORY[title].name;
|
49
|
+
media.category_url = TITLE_TO_CATEGORY[title].url;
|
50
|
+
}
|
51
|
+
} else if (row.richMetadataRowRenderer) {
|
52
|
+
let contents = row.richMetadataRowRenderer.contents;
|
53
|
+
let boxArt = contents.filter(
|
54
|
+
meta => meta.richMetadataRenderer.style === "RICH_METADATA_RENDERER_STYLE_BOX_ART",
|
55
|
+
);
|
56
|
+
for (let { richMetadataRenderer } of boxArt) {
|
57
|
+
let meta = richMetadataRenderer;
|
58
|
+
media.year = getText(meta.subtitle);
|
59
|
+
let type = getText(meta.callToAction).split(" ")[1];
|
60
|
+
media[type] = getText(meta.title);
|
61
|
+
media[`${type}_url`] = new URL(meta.endpoint.commandMetadata.webCommandMetadata.url, BASE_URL).toString();
|
62
|
+
media.thumbnails = meta.thumbnail.thumbnails;
|
63
|
+
}
|
64
|
+
let topic = contents.filter(meta => meta.richMetadataRenderer.style === "RICH_METADATA_RENDERER_STYLE_TOPIC");
|
65
|
+
for (let { richMetadataRenderer } of topic) {
|
66
|
+
let meta = richMetadataRenderer;
|
67
|
+
media.category = getText(meta.title);
|
68
|
+
media.category_url = new URL(meta.endpoint.commandMetadata.webCommandMetadata.url, BASE_URL).toString();
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
} catch (err) {
|
73
|
+
// Do nothing.
|
74
|
+
}
|
75
|
+
|
76
|
+
return media;
|
77
|
+
};
|
78
|
+
|
79
|
+
const isVerified = badges => !!(badges && badges.find(b => b.metadataBadgeRenderer.tooltip === "Verified"));
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Get video author.
|
83
|
+
*
|
84
|
+
* @param {Object} info
|
85
|
+
* @returns {Object}
|
86
|
+
*/
|
87
|
+
exports.getAuthor = info => {
|
88
|
+
let channelId,
|
89
|
+
thumbnails = [],
|
90
|
+
subscriberCount,
|
91
|
+
verified = false;
|
92
|
+
try {
|
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
|
+
);
|
100
|
+
let videoOwnerRenderer = v.videoSecondaryInfoRenderer.owner.videoOwnerRenderer;
|
101
|
+
channelId = videoOwnerRenderer.navigationEndpoint.browseEndpoint.browseId;
|
102
|
+
thumbnails = videoOwnerRenderer.thumbnail.thumbnails.map(thumbnail => {
|
103
|
+
thumbnail.url = new URL(thumbnail.url, BASE_URL).toString();
|
104
|
+
return thumbnail;
|
105
|
+
});
|
106
|
+
subscriberCount = utils.parseAbbreviatedNumber(getText(videoOwnerRenderer.subscriberCountText));
|
107
|
+
verified = isVerified(videoOwnerRenderer.badges);
|
108
|
+
} catch (err) {
|
109
|
+
// Do nothing.
|
110
|
+
}
|
111
|
+
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;
|
114
|
+
let author = {
|
115
|
+
id: id,
|
116
|
+
name: videoDetails ? videoDetails.ownerChannelName : info.player_response.videoDetails.author,
|
117
|
+
user: videoDetails ? videoDetails.ownerProfileUrl.split("/").slice(-1)[0] : null,
|
118
|
+
channel_url: `https://www.youtube.com/channel/${id}`,
|
119
|
+
external_channel_url: videoDetails ? `https://www.youtube.com/channel/${videoDetails.externalChannelId}` : "",
|
120
|
+
user_url: videoDetails ? new URL(videoDetails.ownerProfileUrl, BASE_URL).toString() : "",
|
121
|
+
thumbnails,
|
122
|
+
verified,
|
123
|
+
subscriber_count: subscriberCount,
|
124
|
+
};
|
125
|
+
if (thumbnails.length) {
|
126
|
+
utils.deprecate(author, "avatar", author.thumbnails[0].url, "author.avatar", "author.thumbnails[0].url");
|
127
|
+
}
|
128
|
+
return author;
|
129
|
+
} catch (err) {
|
130
|
+
return {};
|
131
|
+
}
|
132
|
+
};
|
133
|
+
|
134
|
+
const parseRelatedVideo = (details, rvsParams) => {
|
135
|
+
if (!details) return;
|
136
|
+
try {
|
137
|
+
let viewCount = getText(details.viewCountText);
|
138
|
+
let shortViewCount = getText(details.shortViewCountText);
|
139
|
+
let rvsDetails = rvsParams.find(elem => elem.id === details.videoId);
|
140
|
+
if (!/^\d/.test(shortViewCount)) {
|
141
|
+
shortViewCount = (rvsDetails && rvsDetails.short_view_count_text) || "";
|
142
|
+
}
|
143
|
+
viewCount = (/^\d/.test(viewCount) ? viewCount : shortViewCount).split(" ")[0];
|
144
|
+
let browseEndpoint = details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint;
|
145
|
+
let channelId = browseEndpoint.browseId;
|
146
|
+
let name = getText(details.shortBylineText);
|
147
|
+
let user = (browseEndpoint.canonicalBaseUrl || "").split("/").slice(-1)[0];
|
148
|
+
let video = {
|
149
|
+
id: details.videoId,
|
150
|
+
title: getText(details.title),
|
151
|
+
published: getText(details.publishedTimeText),
|
152
|
+
author: {
|
153
|
+
id: channelId,
|
154
|
+
name,
|
155
|
+
user,
|
156
|
+
channel_url: `https://www.youtube.com/channel/${channelId}`,
|
157
|
+
user_url: `https://www.youtube.com/user/${user}`,
|
158
|
+
thumbnails: details.channelThumbnail.thumbnails.map(thumbnail => {
|
159
|
+
thumbnail.url = new URL(thumbnail.url, BASE_URL).toString();
|
160
|
+
return thumbnail;
|
161
|
+
}),
|
162
|
+
verified: isVerified(details.ownerBadges),
|
163
|
+
|
164
|
+
[Symbol.toPrimitive]() {
|
165
|
+
console.warn(
|
166
|
+
`\`relatedVideo.author\` will be removed in a near future release, ` +
|
167
|
+
`use \`relatedVideo.author.name\` instead.`,
|
168
|
+
);
|
169
|
+
return video.author.name;
|
170
|
+
},
|
171
|
+
},
|
172
|
+
short_view_count_text: shortViewCount.split(" ")[0],
|
173
|
+
view_count: viewCount.replace(/,/g, ""),
|
174
|
+
length_seconds: details.lengthText
|
175
|
+
? Math.floor(parseTimestamp(getText(details.lengthText)) / 1000)
|
176
|
+
: rvsParams && `${rvsParams.length_seconds}`,
|
177
|
+
thumbnails: details.thumbnail.thumbnails,
|
178
|
+
richThumbnails: details.richThumbnail
|
179
|
+
? details.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails
|
180
|
+
: [],
|
181
|
+
isLive: !!(details.badges && details.badges.find(b => b.metadataBadgeRenderer.label === "LIVE NOW")),
|
182
|
+
};
|
183
|
+
|
184
|
+
utils.deprecate(
|
185
|
+
video,
|
186
|
+
"author_thumbnail",
|
187
|
+
video.author.thumbnails[0].url,
|
188
|
+
"relatedVideo.author_thumbnail",
|
189
|
+
"relatedVideo.author.thumbnails[0].url",
|
190
|
+
);
|
191
|
+
utils.deprecate(video, "ucid", video.author.id, "relatedVideo.ucid", "relatedVideo.author.id");
|
192
|
+
utils.deprecate(
|
193
|
+
video,
|
194
|
+
"video_thumbnail",
|
195
|
+
video.thumbnails[0].url,
|
196
|
+
"relatedVideo.video_thumbnail",
|
197
|
+
"relatedVideo.thumbnails[0].url",
|
198
|
+
);
|
199
|
+
return video;
|
200
|
+
} catch (err) {
|
201
|
+
// Skip.
|
202
|
+
}
|
203
|
+
};
|
204
|
+
|
205
|
+
/**
|
206
|
+
* Get related videos.
|
207
|
+
*
|
208
|
+
* @param {Object} info
|
209
|
+
* @returns {Array.<Object>}
|
210
|
+
*/
|
211
|
+
exports.getRelatedVideos = info => {
|
212
|
+
let rvsParams = [],
|
213
|
+
secondaryResults = [];
|
214
|
+
try {
|
215
|
+
rvsParams = info.response.webWatchNextResponseExtensionData.relatedVideoArgs.split(",").map(e => qs.parse(e));
|
216
|
+
} catch (err) {
|
217
|
+
// Do nothing.
|
218
|
+
}
|
219
|
+
try {
|
220
|
+
secondaryResults = info.response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results;
|
221
|
+
} catch (err) {
|
222
|
+
return [];
|
223
|
+
}
|
224
|
+
let videos = [];
|
225
|
+
for (let result of secondaryResults || []) {
|
226
|
+
let details = result.compactVideoRenderer;
|
227
|
+
if (details) {
|
228
|
+
let video = parseRelatedVideo(details, rvsParams);
|
229
|
+
if (video) videos.push(video);
|
230
|
+
} else {
|
231
|
+
let autoplay = result.compactAutoplayRenderer || result.itemSectionRenderer;
|
232
|
+
if (!autoplay || !Array.isArray(autoplay.contents)) continue;
|
233
|
+
for (let content of autoplay.contents) {
|
234
|
+
let video = parseRelatedVideo(content.compactVideoRenderer, rvsParams);
|
235
|
+
if (video) videos.push(video);
|
236
|
+
}
|
237
|
+
}
|
238
|
+
}
|
239
|
+
return videos;
|
240
|
+
};
|
241
|
+
|
242
|
+
/**
|
243
|
+
* Get like count.
|
244
|
+
*
|
245
|
+
* @param {Object} info
|
246
|
+
* @returns {number}
|
247
|
+
*/
|
248
|
+
exports.getLikes = info => {
|
249
|
+
try {
|
250
|
+
let contents = info.response.contents.twoColumnWatchNextResults.results.results.contents;
|
251
|
+
let video = contents.find(r => r.videoPrimaryInfoRenderer);
|
252
|
+
let buttons = video.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons;
|
253
|
+
let accessibilityText = buttons.find(b => b.segmentedLikeDislikeButtonViewModel).segmentedLikeDislikeButtonViewModel
|
254
|
+
.likeButtonViewModel.likeButtonViewModel.toggleButtonViewModel.toggleButtonViewModel.defaultButtonViewModel
|
255
|
+
.buttonViewModel.accessibilityText;
|
256
|
+
return parseInt(accessibilityText.match(/[\d,.]+/)[0].replace(/\D+/g, ""));
|
257
|
+
} catch (err) {
|
258
|
+
return null;
|
259
|
+
}
|
260
|
+
};
|
261
|
+
|
262
|
+
/**
|
263
|
+
* Cleans up a few fields on `videoDetails`.
|
264
|
+
*
|
265
|
+
* @param {Object} videoDetails
|
266
|
+
* @param {Object} info
|
267
|
+
* @returns {Object}
|
268
|
+
*/
|
269
|
+
exports.cleanVideoDetails = (videoDetails, info) => {
|
270
|
+
videoDetails.thumbnails = videoDetails.thumbnail.thumbnails;
|
271
|
+
delete videoDetails.thumbnail;
|
272
|
+
utils.deprecate(
|
273
|
+
videoDetails,
|
274
|
+
"thumbnail",
|
275
|
+
{ thumbnails: videoDetails.thumbnails },
|
276
|
+
"videoDetails.thumbnail.thumbnails",
|
277
|
+
"videoDetails.thumbnails",
|
278
|
+
);
|
279
|
+
videoDetails.description = videoDetails.shortDescription || getText(videoDetails.description);
|
280
|
+
delete videoDetails.shortDescription;
|
281
|
+
utils.deprecate(
|
282
|
+
videoDetails,
|
283
|
+
"shortDescription",
|
284
|
+
videoDetails.description,
|
285
|
+
"videoDetails.shortDescription",
|
286
|
+
"videoDetails.description",
|
287
|
+
);
|
288
|
+
|
289
|
+
// Use more reliable `lengthSeconds` from `playerMicroformatRenderer`.
|
290
|
+
videoDetails.lengthSeconds =
|
291
|
+
(info.player_response.microformat && info.player_response.microformat.playerMicroformatRenderer.lengthSeconds) ||
|
292
|
+
info.player_response.videoDetails.lengthSeconds;
|
293
|
+
return videoDetails;
|
294
|
+
};
|
295
|
+
|
296
|
+
/**
|
297
|
+
* Get storyboards info.
|
298
|
+
*
|
299
|
+
* @param {Object} info
|
300
|
+
* @returns {Array.<Object>}
|
301
|
+
*/
|
302
|
+
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("|");
|
308
|
+
|
309
|
+
if (!parts) return [];
|
310
|
+
|
311
|
+
const url = new URL(parts.shift());
|
312
|
+
|
313
|
+
return parts.map((part, i) => {
|
314
|
+
let [thumbnailWidth, thumbnailHeight, thumbnailCount, columns, rows, interval, nameReplacement, sigh] =
|
315
|
+
part.split("#");
|
316
|
+
|
317
|
+
url.searchParams.set("sigh", sigh);
|
318
|
+
|
319
|
+
thumbnailCount = parseInt(thumbnailCount, 10);
|
320
|
+
columns = parseInt(columns, 10);
|
321
|
+
rows = parseInt(rows, 10);
|
322
|
+
|
323
|
+
const storyboardCount = Math.ceil(thumbnailCount / (columns * rows));
|
324
|
+
|
325
|
+
return {
|
326
|
+
templateUrl: url.toString().replace("$L", i).replace("$N", nameReplacement),
|
327
|
+
thumbnailWidth: parseInt(thumbnailWidth, 10),
|
328
|
+
thumbnailHeight: parseInt(thumbnailHeight, 10),
|
329
|
+
thumbnailCount,
|
330
|
+
interval: parseInt(interval, 10),
|
331
|
+
columns,
|
332
|
+
rows,
|
333
|
+
storyboardCount,
|
334
|
+
};
|
335
|
+
});
|
336
|
+
};
|
337
|
+
|
338
|
+
/**
|
339
|
+
* Get chapters info.
|
340
|
+
*
|
341
|
+
* @param {Object} info
|
342
|
+
* @returns {Array.<Object>}
|
343
|
+
*/
|
344
|
+
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));
|
355
|
+
if (!marker) return [];
|
356
|
+
const chapters = marker.value.chapters;
|
357
|
+
|
358
|
+
return chapters.map(chapter => ({
|
359
|
+
title: getText(chapter.chapterRenderer.title),
|
360
|
+
start_time: chapter.chapterRenderer.timeRangeStartMillis / 1000,
|
361
|
+
}));
|
362
|
+
};
|