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 sig = require('./sig');
|
7 | const miniget = require('miniget');
|
8 | const m3u8stream = require('m3u8stream');
|
9 | const parseTime = require('m3u8stream/dist/parse-time');
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const ytdl = (link, options) => {
|
18 | const stream = createStream(options);
|
19 | ytdl.getInfo(link, options).then(info => {
|
20 | downloadFromInfoCallback(stream, info, options);
|
21 | }, stream.emit.bind(stream, 'error'));
|
22 | return stream;
|
23 | };
|
24 | module.exports = ytdl;
|
25 |
|
26 | ytdl.getBasicInfo = getInfo.getBasicInfo;
|
27 | ytdl.getInfo = getInfo.getInfo;
|
28 | ytdl.chooseFormat = formatUtils.chooseFormat;
|
29 | ytdl.filterFormats = formatUtils.filterFormats;
|
30 | ytdl.validateID = urlUtils.validateID;
|
31 | ytdl.validateURL = urlUtils.validateURL;
|
32 | ytdl.getURLVideoID = urlUtils.getURLVideoID;
|
33 | ytdl.getVideoID = urlUtils.getVideoID;
|
34 | ytdl.cache = {
|
35 | sig: sig.cache,
|
36 | info: getInfo.cache,
|
37 | cookie: getInfo.cookieCache,
|
38 | };
|
39 |
|
40 |
|
41 | const createStream = options => {
|
42 | const stream = new PassThrough({
|
43 | highWaterMark: (options && options.highWaterMark) || 1024 * 512,
|
44 | });
|
45 | stream.destroy = () => { stream._isDestroyed = true; };
|
46 | return stream;
|
47 | };
|
48 |
|
49 |
|
50 | const pipeAndSetEvents = (req, stream, end) => {
|
51 |
|
52 | [
|
53 | 'abort', 'request', 'response', 'error', 'redirect', 'retry', 'reconnect',
|
54 | ].forEach(event => {
|
55 | req.prependListener(event, stream.emit.bind(stream, event));
|
56 | });
|
57 | req.pipe(stream, { end });
|
58 | };
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | const downloadFromInfoCallback = (stream, info, options) => {
|
69 | options = options || {};
|
70 |
|
71 | let err = utils.playError(info, ['UNPLAYABLE', 'LIVE_STREAM_OFFLINE']);
|
72 | if (err) {
|
73 | stream.emit('error', err);
|
74 | return;
|
75 | }
|
76 |
|
77 | if (!info.formats.length) {
|
78 | stream.emit('error', Error('This video is unavailable'));
|
79 | return;
|
80 | }
|
81 |
|
82 | let format;
|
83 | try {
|
84 | format = formatUtils.chooseFormat(info.formats, options);
|
85 | } catch (e) {
|
86 | stream.emit('error', e);
|
87 | return;
|
88 | }
|
89 | stream.emit('info', info, format);
|
90 | if (stream._isDestroyed) { return; }
|
91 |
|
92 | let contentLength, downloaded = 0;
|
93 | const ondata = chunk => {
|
94 | downloaded += chunk.length;
|
95 | stream.emit('progress', chunk.length, downloaded, contentLength);
|
96 | };
|
97 |
|
98 |
|
99 |
|
100 | const dlChunkSize = options.dlChunkSize || 1024 * 1024 * 10;
|
101 | let req;
|
102 | let shouldEnd = true;
|
103 |
|
104 | if (format.isHLS || format.isDashMPD) {
|
105 | req = m3u8stream(format.url, {
|
106 | chunkReadahead: +info.live_chunk_readahead,
|
107 | begin: options.begin || (format.isLive && Date.now()),
|
108 | liveBuffer: options.liveBuffer,
|
109 | requestOptions: options.requestOptions,
|
110 | parser: format.isDashMPD ? 'dash-mpd' : 'm3u8',
|
111 | id: format.itag,
|
112 | });
|
113 |
|
114 | req.on('progress', (segment, totalSegments) => {
|
115 | stream.emit('progress', segment.size, segment.num, totalSegments);
|
116 | });
|
117 | pipeAndSetEvents(req, stream, shouldEnd);
|
118 | } else {
|
119 | const requestOptions = Object.assign({}, options.requestOptions, {
|
120 | maxReconnects: 6,
|
121 | maxRetries: 3,
|
122 | backoff: { inc: 500, max: 10000 },
|
123 | });
|
124 |
|
125 | let shouldBeChunked = dlChunkSize !== 0 && (!format.hasAudio || !format.hasVideo);
|
126 |
|
127 | if (shouldBeChunked) {
|
128 | let start = (options.range && options.range.start) || 0;
|
129 | let end = start + dlChunkSize;
|
130 | const rangeEnd = options.range && options.range.end;
|
131 |
|
132 | contentLength = options.range ?
|
133 | (rangeEnd ? rangeEnd + 1 : parseInt(format.contentLength)) - start :
|
134 | parseInt(format.contentLength);
|
135 |
|
136 | const getNextChunk = () => {
|
137 | if (!rangeEnd && end >= contentLength) end = 0;
|
138 | if (rangeEnd && end > rangeEnd) end = rangeEnd;
|
139 | shouldEnd = !end || end === rangeEnd;
|
140 |
|
141 | requestOptions.headers = Object.assign({}, requestOptions.headers, {
|
142 | Range: `bytes=${start}-${end || ''}`,
|
143 | });
|
144 |
|
145 | req = miniget(format.url, requestOptions);
|
146 | req.on('data', ondata);
|
147 | req.on('end', () => {
|
148 | if (stream._isDestroyed) { return; }
|
149 | if (end && end !== rangeEnd) {
|
150 | start = end + 1;
|
151 | end += dlChunkSize;
|
152 | getNextChunk();
|
153 | }
|
154 | });
|
155 | pipeAndSetEvents(req, stream, shouldEnd);
|
156 | };
|
157 | getNextChunk();
|
158 | } else {
|
159 |
|
160 | if (options.begin) {
|
161 | format.url += `&begin=${parseTime.humanStr(options.begin)}`;
|
162 | }
|
163 | if (options.range && (options.range.start || options.range.end)) {
|
164 | requestOptions.headers = Object.assign({}, requestOptions.headers, {
|
165 | Range: `bytes=${options.range.start || '0'}-${options.range.end || ''}`,
|
166 | });
|
167 | }
|
168 | req = miniget(format.url, requestOptions);
|
169 | req.on('response', res => {
|
170 | if (stream._isDestroyed) { return; }
|
171 | contentLength = contentLength || parseInt(res.headers['content-length']);
|
172 | });
|
173 | req.on('data', ondata);
|
174 | pipeAndSetEvents(req, stream, shouldEnd);
|
175 | }
|
176 | }
|
177 |
|
178 | stream.destroy = () => {
|
179 | stream._isDestroyed = true;
|
180 | if (req.abort) req.abort();
|
181 | req.end();
|
182 | req.removeListener('data', ondata);
|
183 | req.unpipe();
|
184 | };
|
185 | };
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | ytdl.downloadFromInfo = (info, options) => {
|
198 | const stream = createStream(options);
|
199 | if (!info.full) {
|
200 | throw Error('Cannot use `ytdl.downloadFromInfo()` when called ' +
|
201 | 'with info from `ytdl.getBasicInfo()`');
|
202 | }
|
203 | setImmediate(() => {
|
204 | downloadFromInfoCallback(stream, info, options);
|
205 | });
|
206 | return stream;
|
207 | };
|