const PassThrough = require('stream').PassThrough; const deprecate = require('util').deprecate; const getInfo = require('./info'); const util = require('./util'); const sig = require('./sig'); const request = require('miniget'); const m3u8stream = require('m3u8stream'); const parseTime = require('m3u8stream/lib/parse-time'); /** * @param {String} link * @param {!Object} options * @return {ReadableStream} */ const ytdl = module.exports = function ytdl(link, options) { const stream = createStream(options); ytdl.getInfo(link, options, (err, info) => { if (err) { stream.emit('error', err); return; } downloadFromInfoCallback(stream, info, options); }); return stream; }; ytdl.getBasicInfo = getInfo.getBasicInfo; ytdl.getInfo = getInfo.getFullInfo; ytdl.chooseFormat = util.chooseFormat; ytdl.filterFormats = util.filterFormats; ytdl.validateID = util.validateID; ytdl.validateURL = util.validateURL; ytdl.validateLink = deprecate(util.validateURL, 'ytdl.validateLink: Renamed to ytdl.validateURL'); ytdl.getURLVideoID = util.getURLVideoID; ytdl.getVideoID = util.getVideoID; ytdl.cache = { sig: sig.cache, info: getInfo.cache, }; function createStream(options) { const stream = new PassThrough({ highWaterMark: options && options.highWaterMark || null, }); stream.destroy = () => { stream._isDestroyed = true; }; return stream; } /** * Chooses a format to download. * * @param {stream.Readable} stream * @param {Object} info * @param {Object} options */ function downloadFromInfoCallback(stream, info, options) { options = options || {}; const format = util.chooseFormat(info.formats, options); if (format instanceof Error) { // The caller expects this function to be async. setImmediate(() => { stream.emit('error', format); }); return; } stream.emit('info', info, format); if (stream._isDestroyed) { return; } let url = format.url; if (format.live) { let req = m3u8stream(url, { chunkReadahead: +info.live_chunk_readahead, begin: options.begin || Date.now(), liveBuffer: options.liveBuffer, requestOptions: options.requestOptions, parser: /\/manifest\/dash\//.test(format.url) ? 'dash-mpd' : 'm3u8', id: format.itag, }); req.on('error', stream.emit.bind(stream, 'error')); stream.destroy = req.end.bind(req); req.pipe(stream); } else { if (options.begin) { url += '&begin=' + parseTime(options.begin); } let requestOptions = Object.assign({}, options.requestOptions, { maxReconnects: 5 }); if (options.range && (options.range.start || options.range.end)) { requestOptions.headers = Object.assign({}, requestOptions.headers, { Range: `bytes=${options.range.start || '0'}-${options.range.end || ''}` }); } const req = request(url, requestOptions); const ondata = (chunk) => { downloaded += chunk.length; stream.emit('progress', chunk.length, downloaded, contentLength); }; stream.destroy = () => { stream._isDestroyed = true; req.abort(); req.removeListener('data', ondata); req.unpipe(); }; // Forward events from the request to the stream. [ 'abort', 'request', 'response', 'error', 'retry', 'reconnect' ].forEach((event) => { req.on(event, (arg) => { stream.emit(event, arg); }); }); let contentLength, downloaded = 0; req.on('response', (res) => { if (stream._isDestroyed) { return; } if (!contentLength) { contentLength = parseInt(res.headers['content-length'], 10); } }); req.on('data', ondata); req.pipe(stream); } } /** * Can be used to download video after its `info` is gotten through * `ytdl.getInfo()`. In case the user might want to look at the * `info` object before deciding to download. * * @param {Object} info * @param {!Object} options */ ytdl.downloadFromInfo = (info, options) => { const stream = createStream(options); if (!info.full) { throw new Error('Cannot use `ytdl.downloadFromInfo()` when called ' + 'with info from `ytdl.getBasicInfo()`'); } setImmediate(() => { downloadFromInfoCallback(stream, info, options); }); return stream; };