@distube/ytdl-core
Version:
DisTube fork of ytdl-core. YouTube video downloader in pure javascript.
232 lines (209 loc) • 7.02 kB
JavaScript
const PassThrough = require("stream").PassThrough;
const getInfo = require("./info");
const utils = require("./utils");
const formatUtils = require("./format-utils");
const urlUtils = require("./url-utils");
const miniget = require("miniget");
const m3u8stream = require("m3u8stream");
const { parseTimestamp } = require("m3u8stream");
const agent = require("./agent");
/**
* @param {string} link
* @param {!Object} options
* @returns {ReadableStream}
*/
const ytdl = (link, options) => {
const stream = createStream(options);
ytdl.getInfo(link, options).then(
info => {
downloadFromInfoCallback(stream, info, options);
},
stream.emit.bind(stream, "error"),
);
return stream;
};
module.exports = ytdl;
ytdl.getBasicInfo = getInfo.getBasicInfo;
ytdl.getInfo = getInfo.getInfo;
ytdl.chooseFormat = formatUtils.chooseFormat;
ytdl.filterFormats = formatUtils.filterFormats;
ytdl.validateID = urlUtils.validateID;
ytdl.validateURL = urlUtils.validateURL;
ytdl.getURLVideoID = urlUtils.getURLVideoID;
ytdl.getVideoID = urlUtils.getVideoID;
ytdl.createAgent = agent.createAgent;
ytdl.createProxyAgent = agent.createProxyAgent;
ytdl.cache = {
info: getInfo.cache,
watch: getInfo.watchPageCache,
};
ytdl.version = require("../package.json").version;
const createStream = options => {
const stream = new PassThrough({ highWaterMark: options?.highWaterMark || 1024 * 512 });
stream._destroy = () => {
stream.destroyed = true;
};
return stream;
};
const pipeAndSetEvents = (req, stream, end) => {
// Forward events from the request to the stream.
["abort", "request", "response", "error", "redirect", "retry", "reconnect"].forEach(event => {
req.prependListener(event, stream.emit.bind(stream, event));
});
req.pipe(stream, { end });
};
/**
* Chooses a format to download.
*
* @param {stream.Readable} stream
* @param {Object} info
* @param {Object} options
*/
const downloadFromInfoCallback = (stream, info, options) => {
options = options || {};
let err = utils.playError(info.player_response);
if (err) {
stream.emit("error", err);
return;
}
if (!info.formats.length) {
stream.emit("error", Error("This video is unavailable"));
return;
}
let format;
try {
format = formatUtils.chooseFormat(info.formats, options);
} catch (e) {
stream.emit("error", e);
return;
}
stream.emit("info", info, format);
if (stream.destroyed) {
return;
}
let contentLength,
downloaded = 0;
const ondata = chunk => {
downloaded += chunk.length;
stream.emit("progress", chunk.length, downloaded, contentLength);
};
utils.applyDefaultHeaders(options);
if (options.IPv6Block) {
options.requestOptions = Object.assign({}, options.requestOptions, {
localAddress: utils.getRandomIPv6(options.IPv6Block),
});
}
if (options.agent) {
// Set agent on both the miniget and m3u8stream requests
options.requestOptions.agent = options.agent.agent;
if (options.agent.jar) {
utils.setPropInsensitive(
options.requestOptions.headers,
"cookie",
options.agent.jar.getCookieStringSync("https://www.youtube.com"),
);
}
if (options.agent.localAddress) {
options.requestOptions.localAddress = options.agent.localAddress;
}
}
// Download the file in chunks, in this case the default is 10MB,
// anything over this will cause youtube to throttle the download
const dlChunkSize = typeof options.dlChunkSize === "number" ? options.dlChunkSize : 1024 * 1024 * 10;
let req;
let shouldEnd = true;
if (format.isHLS || format.isDashMPD) {
req = m3u8stream(format.url, {
chunkReadahead: +info.live_chunk_readahead,
begin: options.begin || (format.isLive && Date.now()),
liveBuffer: options.liveBuffer,
// Now we have passed not only custom "dispatcher" with undici ProxyAgent, but also "agent" field which is compatible for node http
requestOptions: options.requestOptions,
parser: format.isDashMPD ? "dash-mpd" : "m3u8",
id: format.itag,
});
req.on("progress", (segment, totalSegments) => {
stream.emit("progress", segment.size, segment.num, totalSegments);
});
pipeAndSetEvents(req, stream, shouldEnd);
} else {
const requestOptions = Object.assign({}, options.requestOptions, {
maxReconnects: 6,
maxRetries: 3,
backoff: { inc: 500, max: 10000 },
});
let shouldBeChunked = dlChunkSize !== 0 && (!format.hasAudio || !format.hasVideo);
if (shouldBeChunked) {
let start = options.range?.start || 0;
let end = start + dlChunkSize;
const rangeEnd = options.range?.end;
contentLength = options.range
? (rangeEnd ? rangeEnd + 1 : parseInt(format.contentLength)) - start
: parseInt(format.contentLength);
const getNextChunk = () => {
if (stream.destroyed) return;
if (!rangeEnd && end >= contentLength) end = 0;
if (rangeEnd && end > rangeEnd) end = rangeEnd;
shouldEnd = !end || end === rangeEnd;
requestOptions.headers = Object.assign({}, requestOptions.headers, {
Range: `bytes=${start}-${end || ""}`,
});
req = miniget(format.url, requestOptions);
req.on("data", ondata);
req.on("end", () => {
if (stream.destroyed) return;
if (end && end !== rangeEnd) {
start = end + 1;
end += dlChunkSize;
getNextChunk();
}
});
pipeAndSetEvents(req, stream, shouldEnd);
};
getNextChunk();
} else {
// Audio only and video only formats don't support begin
if (options.begin) {
format.url += `&begin=${parseTimestamp(options.begin)}`;
}
if (options.range?.start || options.range?.end) {
requestOptions.headers = Object.assign({}, requestOptions.headers, {
Range: `bytes=${options.range.start || "0"}-${options.range.end || ""}`,
});
}
req = miniget(format.url, requestOptions);
req.on("response", res => {
if (stream.destroyed) return;
contentLength = contentLength || parseInt(res.headers["content-length"]);
});
req.on("data", ondata);
pipeAndSetEvents(req, stream, shouldEnd);
}
}
stream._destroy = () => {
stream.destroyed = true;
if (req) {
req.destroy();
req.end();
}
};
};
/**
* 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
* @returns {ReadableStream}
*/
ytdl.downloadFromInfo = (info, options) => {
const stream = createStream(options);
if (!info.full) {
throw Error("Cannot use `ytdl.downloadFromInfo()` when called with info from `ytdl.getBasicInfo()`");
}
setImmediate(() => {
downloadFromInfoCallback(stream, info, options);
});
return stream;
};