-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
128 lines (97 loc) · 4.53 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"use strict";
const fs = require("fs");
const path = require("path");
const pify = require("pify");
const google = require("googleapis");
const urlParser = require("js-video-url-parser");
const isPlaylist = require("is-playlist");
const ytdl = require("ytdl-core");
const PProgress = require("p-progress");
const filenamify = require("filenamify");
let youtube;
function getUrlInfo(url, urlInfo, apiKey, opts) {
if (!isPlaylist(url)) {
const videoOpts = {
key: apiKey,
id: urlInfo.id,
part: "snippet"
};
return pify(youtube.videos.list)(videoOpts).then(res => res.items);
}
const playlistOpts = {
key: apiKey,
playlistId: urlInfo.list,
part: "id,snippet",
maxResults: opts.max + opts.start <= 50 ? opts.max + opts.start : 50
};
return pify(youtube.playlistItems.list)(playlistOpts)
.then(({items}) => items.length > opts.start ? items.slice(opts.start) : []);
}
function downloadVideo(url, ext, downloadPath) {
const opts = {
filter: format => format.bitrate && format.container === ext // NOTE: This was taken from https://github.com/fent/node-ytdl/blob/master/bin/ytdl.js#L181.
};
return new PProgress((resolve, reject, progress) => { // TODO: Check if I should use `p-lazy` to postpone the download to when `.then` is called.
const download = ytdl(url, opts); // TODO: Add support for other formats, including music-only ones. Also, address the issue in https://github.com/fent/node-ytdl-core#handling-separate-streams.
let totalSize;
let downloadedSize = 0;
download.on("response", res => {
totalSize = parseFloat(res.headers["content-length"], 10);
});
download.on("data", data => {
downloadedSize += data.length;
const currentProgress = downloadedSize / totalSize;
progress(currentProgress);
});
download.on("finish", resolve);
download.on("error", reject);
download.pipe(fs.createWriteStream(downloadPath)); // TODO: Check if I should leave this part to the user, instead of doing it in the module.
});
}
module.exports = (url, apiKey, videosPath, opts) => { // TODO: Suport using an OAUTH 2.0 token instead of an API key from authentication.
opts = opts || {}; // TODO: Check if I should validate the options' types as well.
if (typeof url !== "string") {
throw new TypeError(`Expected \`url\` to be a \`string\`, got \`${typeof url}\``);
}
if (typeof apiKey !== "string") {
throw new TypeError(`Expected \`apiKey\` to be a \`string\`, got \`${typeof apiKey}\``);
}
if (typeof videosPath !== "string") {
throw new TypeError(`Expected \`videosPath\` to be a \`string\`, got \`${typeof videosPath}\``);
}
const videoInfo = urlParser.parse(url); // TODO: Maybe resolve the url before parsing it so that minified urls and such could be used.
if (videoInfo.provider !== "youtube") {
throw new Error(`Expected a video from Youtube, got video from ${videoInfo.provider}`);
}
if (videoInfo.mediaType !== "playlist" && videoInfo.mediaType !== "video") {
throw new Error(`Expected a video url, got url to ${videoInfo.mediaType}`); // TODO: Throw error if the given `url` parameter is not an url using the `is-url-superb` package.
}
youtube = google.youtube({
version: "v3",
auth: apiKey
});
opts.max = Math.min(Math.max((opts.max || 5), 0), 50); // TODO: Maybe limit `opts.max` to be between 1 and 50 instead of 0 and 50 and `opts.start` to be between 0 and 49 instead of 0 and 50.
opts.start = Math.min(Math.max((opts.start || 0), 0), 50);
process.on("unhandledRejection", (reason, promise) => {
if (reason.message.includes("The progress percentage can't be lower than the last progress event")) {
promise.catch(() => Promise.resolve());
}
});
return getUrlInfo(url, videoInfo, apiKey, opts)
.then(res => {
const downloads = [];
for (const video of res) {
const videoId = video.snippet.resourceId ? video.snippet.resourceId.videoId : video.id;
const videoTitle = video.snippet.title;
const ext = "mp4"; // TODO: Get this from the user instead of hardcoding it.
const filename = filenamify(videoTitle, {replacement: "-"});
const videoPath = path.join(videosPath, `${filename}.${ext}`); // TODO: Find a better solution than this one for https://github.com/itaisteinherz/videos/issues/1.
const videoUrl = `https://youtu.be/${videoId}`;
const download = downloadVideo(videoUrl, ext, videoPath); // TODO: Add support for other formats, including music-only ones. Also, move to downloading videos synchronously rather than asynchronously.
download.videoTitle = videoTitle;
download.videoUrl = url;
downloads.push(download);
}
return downloads;
});
};