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