1 | const PassThrough = require('stream').PassThrough;
|
2 | const getInfo = require('./info');
|
3 | const util = require('./util');
|
4 | const sig = require('./sig');
|
5 | const miniget = require('miniget');
|
6 | const m3u8stream = require('m3u8stream');
|
7 | const parseTime = require('m3u8stream/dist/parse-time');
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const ytdl = (link, options) => {
|
16 | const stream = createStream(options);
|
17 | ytdl.getInfo(link, options, (err, info) => {
|
18 | if (err) {
|
19 | stream.emit('error', err);
|
20 | return;
|
21 | }
|
22 |
|
23 | downloadFromInfoCallback(stream, info, options);
|
24 | });
|
25 |
|
26 | return stream;
|
27 | };
|
28 | module.exports = ytdl;
|
29 |
|
30 | ytdl.getBasicInfo = getInfo.getBasicInfo;
|
31 | ytdl.getInfo = getInfo.getFullInfo;
|
32 | ytdl.chooseFormat = util.chooseFormat;
|
33 | ytdl.filterFormats = util.filterFormats;
|
34 | ytdl.validateID = util.validateID;
|
35 | ytdl.validateURL = util.validateURL;
|
36 | ytdl.getURLVideoID = util.getURLVideoID;
|
37 | ytdl.getVideoID = util.getVideoID;
|
38 | ytdl.cache = {
|
39 | sig: sig.cache,
|
40 | info: getInfo.cache,
|
41 | };
|
42 |
|
43 |
|
44 | const createStream = (options) => {
|
45 | const stream = new PassThrough({
|
46 | highWaterMark: options && options.highWaterMark || null,
|
47 | });
|
48 | stream.destroy = () => { stream._isDestroyed = true; };
|
49 | return stream;
|
50 | };
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | const downloadFromInfoCallback = (stream, info, options) => {
|
61 | options = options || {};
|
62 | let format;
|
63 | try {
|
64 | format = util.chooseFormat(info.formats, options);
|
65 | } catch(e) {
|
66 | setImmediate(() => {
|
67 | stream.emit('error', e);
|
68 | });
|
69 | return;
|
70 | }
|
71 | stream.emit('info', info, format);
|
72 | if (stream._isDestroyed) { return; }
|
73 |
|
74 | let contentLength, downloaded = 0;
|
75 | const ondata = (chunk) => {
|
76 | downloaded += chunk.length;
|
77 | stream.emit('progress', chunk.length, downloaded, contentLength);
|
78 | };
|
79 |
|
80 | let req;
|
81 | if (format.isHLS || format.isDashMPD) {
|
82 | req = m3u8stream(format.url, {
|
83 | chunkReadahead: +info.live_chunk_readahead,
|
84 | begin: options.begin || format.live && Date.now(),
|
85 | liveBuffer: options.liveBuffer,
|
86 | requestOptions: options.requestOptions,
|
87 | parser: format.isDashMPD ? 'dash-mpd' : 'm3u8',
|
88 | id: format.itag,
|
89 | });
|
90 |
|
91 | req.on('progress', (segment, totalSegments) => {
|
92 | stream.emit('progress', segment.size, segment.num, totalSegments);
|
93 | });
|
94 |
|
95 | } else {
|
96 | if (options.begin) {
|
97 | format.url += '&begin=' + parseTime.humanStr(options.begin);
|
98 | }
|
99 | let requestOptions = Object.assign({}, options.requestOptions, {
|
100 | maxReconnects: 6,
|
101 | maxRetries: 3,
|
102 | backoff: { inc: 500, max: 10000 },
|
103 | });
|
104 | if (options.range && (options.range.start || options.range.end)) {
|
105 | requestOptions.headers = Object.assign({}, requestOptions.headers, {
|
106 | Range: `bytes=${options.range.start || '0'}-${options.range.end || ''}`
|
107 | });
|
108 | }
|
109 |
|
110 | req = miniget(format.url, requestOptions);
|
111 |
|
112 | req.on('response', (res) => {
|
113 | if (stream._isDestroyed) { return; }
|
114 | if (!contentLength) {
|
115 | contentLength = parseInt(res.headers['content-length'], 10);
|
116 | }
|
117 | });
|
118 | req.on('data', ondata);
|
119 | }
|
120 |
|
121 | stream.destroy = () => {
|
122 | stream._isDestroyed = true;
|
123 | if (req.abort) req.abort();
|
124 | req.end();
|
125 | req.removeListener('data', ondata);
|
126 | req.unpipe();
|
127 | };
|
128 |
|
129 |
|
130 | [
|
131 | 'abort', 'request', 'response', 'error', 'retry', 'reconnect'
|
132 | ].forEach((event) => {
|
133 | req.prependListener(event, (arg) => {
|
134 | stream.emit(event, arg); });
|
135 | });
|
136 |
|
137 | req.pipe(stream);
|
138 | };
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | ytdl.downloadFromInfo = (info, options) => {
|
150 | const stream = createStream(options);
|
151 | if (!info.full) {
|
152 | throw Error('Cannot use `ytdl.downloadFromInfo()` when called ' +
|
153 | 'with info from `ytdl.getBasicInfo()`');
|
154 | }
|
155 | setImmediate(() => {
|
156 | downloadFromInfoCallback(stream, info, options);
|
157 | });
|
158 | return stream;
|
159 | };
|