dbd.js-new
Version:
dbd.js-new - Simplifica la programación de un bot de discord, dbd.js-new es una versión de dbd.js con soporte
628 lines (508 loc) • 14.2 kB
JavaScript
const os = require("os");
const fs = require("fs");
const ytdl = require("ytdl-core");
const prism = require("prism-media");
const scdl = require("soundcloud-downloader/dist/download-media").default;
const { Transform } = require("stream");
//Better FFmpegArgs™️
const FFMPEG_ARGS = [
"-analyzeduration",
"0",
"-loglevel",
"0",
"-preset",
"veryfast",
"-f",
"s16le",
"-ar",
"48000",
"-ac",
"2",
"-vn",
];
const HIGHWATERMARK = 8192;
async function youtube(url, cookie, ffmpegArgs, song) {
function filter(format) {
return (
format.codecs === "opus" &&
format.container === "webm" &&
format.audioSampleRate == 48000
);
}
//Choose Medium
function chooseFormat(sourceFormats, isLive) {
let filter = (format) => format.audioBitrate;
if (isLive) filter = (format) => format.audioBitrate && format.isHLS;
const formats = sourceFormats
.filter(filter)
.sort((a, b) => a.audioBitrate - b.audioBitrate)
.sort((a, b) => a.audioSampleRate - b.audioSampleRate)
.sort((a, b) => a.audioChannels - b.audioChannels);
const filteredFormat = formats.filter((format) => !format.bitrate);
const format = filteredFormat.length
? filteredFormat[Math.floor(filteredFormat.length / 3)] ||
filteredFormat[Math.floor(filteredFormat.length) / 2] ||
filteredFormat[1] ||
filteredFormat[0]
: formats[Math.floor(formats.length / 3)] ||
formats[Math.floor(formats.length / 2)] ||
formats[1] ||
formats[0];
return format;
}
const options = {
highWatermark: HIGHWATERMARK,
dlChunkSize: 1 << 20,
requestOptions: {
headers: {
cookie: cookie,
"user-agent":
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) (dbd.js; https://www.npmjs.com/package/dbd.js)",
},
},
};
let info;
if (!song.info) {
info = await ytdl.getInfo(url, options);
info = {
full: true,
formats: info.formats,
player_response: info.player_response,
live_chunk_readahead: info.live_chunk_readahead,
};
} else info = song.info;
const formats = info.formats
.filter(filter)
.sort((a, b) => a.audioBitrate - b.audioBitrate);
let format =
formats[Math.floor(formats.length / 3)] ||
formats[Math.floor(formats.length / 2)] ||
formats[1] ||
formats[0];
const canDemux = format && !info.player_response.videoDetails.isLiveContent;
if (
!info.player_response.videoDetails.isLiveContent &&
require("os").freemem() / 1024 / 1024 > 100
)
song.info = info;
if (canDemux && (ffmpegArgs ? !ffmpegArgs.filter : 1)) {
options.format = format;
const audio = ytdl.downloadFromInfo(info, options);
const demuxer = new prism.opus.WebmDemuxer({
highWaterMark: HIGHWATERMARK,
});
const streams = {
input: new prism.opus.Decoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
volume: new prism.VolumeTransformer({
type: "s16le",
volume: 1,
highWatermark: HIGHWATERMARK,
}),
opus: new prism.opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
seek: ffmpegArgs ? ffmpegArgs.seek || 0 : 0,
};
audio.on("error", (error) => {
streams.input.emit("error", error);
});
demuxer.on("error", (error) => {
streams.input.emit("error", error);
});
const lines = [audio, demuxer, streams.input, streams.volume, streams.opus];
if (ffmpegArgs && ffmpegArgs.seek) {
const seekable = new Seekable({
time: ffmpegArgs.seek,
rate: 48000,
depth: 16,
channels: 2,
highWatermark: HIGHWATERMARK,
});
streams.seekable = seekable;
lines.splice(3, 0, seekable);
}
require("stream").pipeline(...lines, (error) => {
if (error) {
streams.input.emit("error", error);
}
});
streams.opus.once("close", () => {
for (const line of lines) line.destroy();
});
return streams;
}
if (!canDemux)
format = chooseFormat(
info.formats,
info.player_response.videoDetails.isLiveContent
);
if (!format) {
const stream = new (require("stream").Readable)({
read: () => {},
});
setImmediate(() => stream.push(null));
return {
opus: stream,
};
}
options.format = format;
const thread = await _getThreads();
const args = [...FFMPEG_ARGS];
args.push("-threads", thread);
//Custom FFmpegArgs™️
if (ffmpegArgs && !info.player_response.videoDetails.isLiveContent) {
_parseFFmpeg(args, ffmpegArgs);
}
const streams = {
input: ytdl.downloadFromInfo(info, options),
ffmpeg: new prism.FFmpeg({
args,
highWatermark: HIGHWATERMARK,
}),
volume: new prism.VolumeTransformer({
type: "s16le",
volume: 1,
highWatermark: HIGHWATERMARK,
}),
opus: new prism.opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
seek: (ffmpegArgs ? ffmpegArgs.seek : 0) || 0,
};
require("stream").pipeline(
streams.input,
streams.ffmpeg,
streams.volume,
streams.opus,
(error) => {
if (error) {
streams.input.emit("error", error);
}
}
);
streams.opus.once("close", () => {
streams.input.destroy();
streams.ffmpeg.destroy();
streams.volume.destroy();
streams.opus.destroy();
});
return streams;
}
async function soundcloud(url, sdl, ffmpegArgs, song) {
function chooseTranscoding(track) {
const transcodings = sdl.filterMedia(track.media.transcodings, {
protocol: "progressive",
});
return transcodings[0] || track.media.transcodings[0];
}
let info;
if (!song.info) {
info = await sdl.getInfo(url);
info = {
media: info.media,
downloadable: info.downloadable,
streamable: info.streamable,
};
} else info = song.info;
if (!(info.downloadable || info.streamable)) {
const stream = new (require("stream").Readable)({
read: () => {},
});
setImmediate(() => stream.push(null));
return {
opus: stream,
};
}
const transcodings = sdl.filterMedia(info.media.transcodings, {
format: 'audio/ogg; codecs="opus"',
});
let transcoding =
transcodings.find(({ format }) => format.protocol === "progressive") ||
transcodings[0];
if (require("os").freemem() / 1024 / 1024 > 100) song.info = info;
if (
transcoding &&
transcoding.format.mime_type === 'audio/ogg; codecs="opus"' &&
(ffmpegArgs ? !ffmpegArgs.filter : 1)
) {
const audio = await scdl(transcoding, await sdl.getClientID(), sdl.axios);
const demuxer = new prism.opus.OggDemuxer({ highWatermark: HIGHWATERMARK });
const streams = {
input: new prism.opus.Decoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
volume: new prism.VolumeTransformer({
type: "s16le",
volume: 1,
highWatermark: HIGHWATERMARK,
}),
opus: new prism.opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
seek: ffmpegArgs ? ffmpegArgs.seek || 0 : 0,
};
audio.on("error", (error) => {
streams.input.emit("error", error);
});
demuxer.on("error", (error) => {
streams.input.emit("error", error);
});
const lines = [audio, demuxer, streams.input, streams.volume, streams.opus];
if (ffmpegArgs && ffmpegArgs.seek) {
const seekable = new Seekable({
time: ffmpegArgs.seek,
rate: 48000,
depth: 16,
channels: 2,
highWatermark: HIGHWATERMARK,
});
streams.seekable = seekable;
lines.splice(3, 0, seekable);
}
require("stream").pipeline(...lines, (error) => {
if (error) {
streams.input.emit("error", error);
}
});
streams.opus.once("close", () => {
for (const line of lines) line.destroy();
});
return streams;
}
if (!transcoding) transcoding = chooseTranscoding(info);
if (!transcoding) {
const stream = new (require("stream").Readable)({
read: () => {},
});
setImmediate(() => stream.push(null));
return {
opus: stream,
};
}
const thread = await _getThreads();
const args = [...FFMPEG_ARGS];
args.push("-threads", thread);
if (ffmpegArgs) {
_parseFFmpeg(args, ffmpegArgs);
}
const streams = {
input: await scdl(transcoding, await sdl.getClientID(), sdl.axios),
ffmpeg: new prism.FFmpeg({
args,
highWatermark: HIGHWATERMARK,
}),
volume: new prism.VolumeTransformer({
type: "s16le",
volume: 1,
highWatermark: HIGHWATERMARK,
}),
opus: new prism.opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
seek: (ffmpegArgs ? ffmpegArgs.seek : 0) || 0,
};
require("stream").pipeline(
streams.input,
streams.ffmpeg,
streams.volume,
streams.opus,
(error) => {
if (error) {
streams.input.emit("error", error);
}
}
);
streams.opus.once("close", () => {
streams.input.destroy();
streams.ffmpeg.destroy();
streams.volume.destroy();
streams.opus.destroy();
});
return streams;
}
async function local(path, ffmpegArgs) {
const thread = await _getThreads();
const args = [...FFMPEG_ARGS];
args.push("-threads", thread);
if (ffmpegArgs) {
_parseFFmpeg(args, ffmpegArgs);
}
const streams = {
input: fs.createReadStream(path, {
highWaterMark: HIGHWATERMARK,
}),
ffmpeg: new prism.FFmpeg({
args,
highWatermark: HIGHWATERMARK,
}),
volume: new prism.VolumeTransformer({
type: "s16le",
volume: 1,
highWatermark: HIGHWATERMARK,
}),
opus: new prism.opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
highWatermark: HIGHWATERMARK,
}),
seek: (ffmpegArgs ? ffmpegArgs.seek : 0) || 0,
};
require("stream").pipeline(
streams.input,
streams.ffmpeg,
streams.volume,
streams.opus,
(error) => {
if (error) {
streams.input.emit("error", error);
}
}
);
streams.opus.once("close", () => {
streams.input.destroy();
streams.ffmpeg.destroy();
streams.volume.destroy();
streams.opus.destroy();
});
return streams;
}
function play(voiceConnection, streams) {
const { player } = voiceConnection;
player.destroyDispatcher();
const dispatcher = player.createDispatcher(
{
volume: 1,
bitrate: 64,
fec: true,
seek: streams.seek,
highWaterMark: 24,
},
streams
);
streams.opus.pipe(dispatcher);
return dispatcher;
}
/**
* SeekableStream for PCM audio stream
* @extends {Transform}
*/
class Seekable extends Transform {
/**
* @param {Object} options The option for SeekableStream
* @param {!Number} options.time The time to seek (in seconds)
* @param {!Number} options.rate The sampling rate of the PCMStream
* @param {!Number} options.depth The bitdepth of the PCMStream
* @param {!Number} options.channels The channels of the PCMStream
*/
constructor(options) {
if (
!(options instanceof Object) ||
!options.rate ||
!options.depth ||
!options.channels
)
throw new TypeError("Invalid options given");
super(options);
this._bps = options.rate * (options.depth / 8) * options.channels;
this._total = this._bps * options.time;
}
async _transform(chunk, enc, cb) {
if (!this._total) this.push(chunk);
else if (this._total >= chunk.length) this._total -= chunk.length;
else {
this.push(chunk.slice(this._total));
this._total = 0;
}
cb();
}
}
module.exports = {
play,
soundcloud,
youtube,
local,
Seekable,
};
async function _getThreads() {
if (os.cpus().length < 5) return "1";
let totalTime = global.oldCPUTime;
let idleTime = global.oldIdleTime;
//Get CPU usage of 100ms
if (!totalTime || !idleTime)
await new Promise((res) => {
if (!totalTime) global.oldCPUTime = [];
if (!idleTime) global.oldIdleTime = [];
const cpus = os.cpus();
for (let i = 0; i < cpus.length; i++) {
const cpu = cpus[i];
global.oldCPUTime[i] = 0;
global.oldIdleTime[i] = 0;
for (const type of Object.keys(cpu.times)) {
global.oldCPUTime[i] += cpu.times[type];
if (type === "idle") global.oldIdleTime[i] += cpu.times[type];
}
}
totalTime = global.oldCPUTime;
idleTime = global.oldIdleTime;
setTimeout(res, 100);
});
const cpus = os.cpus();
const rcmnd = Math.round(cpus.length * 0.3);
let threads = 0;
for (let i = 0; i < cpus.length; i++) {
const cpu = cpus[i];
let total = -totalTime[i];
let idle = -idleTime[i];
global.oldCPUTime[i] = 0;
global.oldIdleTime[i] = 0;
for (const type of Object.keys(cpu.times)) {
const time = cpu.times[type];
total += time;
global.oldCPUTime[i] += time;
if (type === "idle") {
idle += time;
global.oldIdleTime[i] += time;
}
}
const percentage = Math.round((idle / total) * 100);
//Add, if its 20% idle
if (percentage >= 20) threads++;
}
const count = Math.max(Math.min(rcmnd, threads), 1);
return `${count}`;
}
function _parseFFmpeg(ffmpeg, args) {
if (!isNaN(args.seek)) ffmpeg.unshift("-ss", String(args.seek));
if (args.filter && Object.keys(args.filter).length) {
let filters = [];
Object.entries(args.filter).map((filter) => {
if (!filter[1].length) {
filters.push(filter[0]);
return;
}
filters.push(filter.join("="));
});
ffmpeg.push("-af", filters.join(","));
}
}