disrexom
Version:
A Discord.js module to simplify your music commands and play songs with audio filters on Discord without any API key. Support YouTube, SoundCloud, Bandcamp, Facebook, and 700+ more sites
311 lines • 14.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DisTubeHandler = void 0;
const ytdl_core_1 = __importDefault(require("@distube/ytdl-core"));
const ytpl_1 = __importDefault(require("@distube/ytpl"));
const _1 = require(".");
const __1 = require("..");
/**
* DisTube's Handler
* @extends DisTubeBase
* @private
*/
class DisTubeHandler extends _1.DisTubeBase {
constructor(distube) {
super(distube);
this.ytdlOptions = this.options.ytdlOptions;
if (this.options.youtubeCookie) {
const requestOptions = {
headers: {
cookie: this.options.youtubeCookie,
},
};
if (this.options.youtubeIdentityToken) {
requestOptions.headers["x-youtube-identity-token"] = this.options.youtubeIdentityToken;
}
Object.assign(this.ytdlOptions, { requestOptions });
}
const client = this.client;
if (this.options.leaveOnEmpty) {
client.on("voiceStateUpdate", oldState => {
if (!(oldState === null || oldState === void 0 ? void 0 : oldState.channel))
return;
const queue = this.queues.get(oldState);
if (!queue) {
if ((0, __1.isVoiceChannelEmpty)(oldState)) {
setTimeout(() => {
if (!this.queues.get(oldState) && (0, __1.isVoiceChannelEmpty)(oldState))
this.voices.leave(oldState);
}, this.options.emptyCooldown * 1e3).unref();
}
return;
}
if (queue.emptyTimeout) {
clearTimeout(queue.emptyTimeout);
delete queue.emptyTimeout;
}
if ((0, __1.isVoiceChannelEmpty)(oldState)) {
queue.emptyTimeout = setTimeout(() => {
delete queue.emptyTimeout;
if ((0, __1.isVoiceChannelEmpty)(oldState)) {
queue.voice.leave();
this.emit("empty", queue);
if (queue.stopped)
queue.delete();
}
}, this.options.emptyCooldown * 1e3).unref();
}
});
}
}
/**
* Create a new guild queue
* @param {Discord.Message|Discord.VoiceChannel|Discord.StageChannel} message A user message | a voice channel
* @param {Song|Song[]} song Song to play
* @param {Discord.TextChannel} textChannel A text channel of the queue
* @throws {Error}
* @returns {Promise<Queue|true>} `true` if queue is not generated
*/
async createQueue(message, song, textChannel = message.channel) {
var _a, _b, _c;
const voice = ((_c = (_b = (_a = message) === null || _a === void 0 ? void 0 : _a.member) === null || _b === void 0 ? void 0 : _b.voice) === null || _c === void 0 ? void 0 : _c.channel) || message;
if (!voice || (0, __1.isMessageInstance)(voice))
throw new __1.DisTubeError("NOT_IN_VOICE");
if (!(0, __1.isSupportedVoiceChannel)(voice))
throw new __1.DisTubeError("NOT_SUPPORTED_VOICE");
return this.queues.create(voice, song, textChannel);
}
/**
* @param {string} url url
* @param {boolean} [basic=false] getBasicInfo?
* @returns {Promise<ytdl.videoInfo>}
*/
getYouTubeInfo(url, basic = false) {
if (basic)
return ytdl_core_1.default.getBasicInfo(url, this.ytdlOptions);
return ytdl_core_1.default.getInfo(url, this.ytdlOptions);
}
/**
* Resolve a Song
* @param {Discord.GuildMember} member Requested user
* @param {string|Song|SearchResult|Playlist} song URL | Search string | {@link Song}
* @returns {Promise<Song|Playlist|null>} Resolved
*/
async resolveSong(member, song) {
if (!song)
return null;
if (song instanceof __1.Song || song instanceof __1.Playlist)
return song._patchMember(member);
if (song instanceof __1.SearchResult) {
if (song.type === "video")
return new __1.Song(song, member);
return this.resolvePlaylist(member, song.url);
}
if (typeof song === "object")
return new __1.Song(song, member);
if (ytdl_core_1.default.validateURL(song))
return new __1.Song(await this.getYouTubeInfo(song), member);
if ((0, __1.isURL)(song)) {
for (const plugin of this.distube.extractorPlugins) {
if (await plugin.validate(song))
return plugin.resolve(song, member);
}
throw new __1.DisTubeError("NOT_SUPPORTED_URL");
}
throw new __1.DisTubeError("CANNOT_RESOLVE_SONG", typeof song);
}
/**
* Resole Song[] or url to a Playlist
* @param {Discord.GuildMember} member Requested user
* @param {Song[]|string} playlist Resolvable playlist
* @param {string} [source="youtube"] Playlist source
* @returns {Promise<Playlist>}
*/
async resolvePlaylist(member, playlist, source = "youtube") {
if (playlist instanceof __1.Playlist)
return playlist;
let solvablePlaylist;
if (typeof playlist === "string") {
solvablePlaylist = await (0, ytpl_1.default)(playlist, { limit: Infinity });
solvablePlaylist.items = solvablePlaylist.items
.filter(v => !v.thumbnail.includes("no_thumbnail"))
.map(v => new __1.Song(v, member));
}
else {
solvablePlaylist = playlist;
}
return new __1.Playlist(solvablePlaylist, member, { source });
}
/**
* Create a custom playlist
* @returns {Promise<Playlist>}
* @param {Discord.Message|Discord.GuildMember} message A message from guild channel | A guild member
* @param {Array<string|Song|SearchResult>} songs Array of url, Song or SearchResult
* @param {Object} [properties={}] Additional properties such as `name`
* @param {boolean} [parallel=true] Whether or not fetch the songs in parallel
* @example
* const songs = ["https://www.youtube.com/watch?v=xxx", "https://www.youtube.com/watch?v=yyy"];
* const playlist = await distube.handler.createCustomPlaylist(member, songs, { name: "My playlist name" }, true);
* // Or fetching custom playlist sequentially (reduce lag for low specs)
* const playlist = await distube.handler.createCustomPlaylist(member, songs, false);
* distube.playVoiceChannel(voiceChannel, playlist, { ... });
*/
async createCustomPlaylist(message, songs, properties = {}, parallel = true) {
var _a;
const member = ((_a = message) === null || _a === void 0 ? void 0 : _a.member) || message;
if (!Array.isArray(songs))
throw new __1.DisTubeError("INVALID_TYPE", "Array", songs, "songs");
if (!songs.length)
throw new __1.DisTubeError("EMPTY_ARRAY", "songs");
songs = songs.filter(song => song instanceof __1.Song || (song instanceof __1.SearchResult && song.type === "video") || (0, __1.isURL)(song));
if (!songs.length)
throw new __1.DisTubeError("NO_VALID_SONG");
let resolvedSongs;
if (parallel) {
const promises = songs.map((song) => this.resolveSong(member, song).catch(() => undefined));
resolvedSongs = (await Promise.all(promises)).filter((s) => !!s);
}
else {
const resolved = [];
for (const song of songs) {
resolved.push(await this.resolveSong(member, song).catch(() => undefined));
}
resolvedSongs = resolved.filter((s) => !!s);
}
return new __1.Playlist(resolvedSongs, member, properties);
}
/**
* Play / add a playlist
* @returns {Promise<void>}
* @param {Discord.Message|Discord.VoiceChannel|Discord.StageChannel} message A message | a voice channel
* @param {Playlist|string} playlist A YouTube playlist url | a Playlist
* @param {Discord.TextChannel|boolean} [textChannel] The default text channel of the queue
* @param {boolean} [skip=false] Skip the playing song (if exists) and play the added playlist instantly
* @param {boolean} [unshift=false] Add the playlist to the beginning of the queue (after the playing song if exists)
*/
async handlePlaylist(message, playlist, textChannel, skip = false, unshift = false) {
if (!(playlist instanceof __1.Playlist))
throw new __1.DisTubeError("INVALID_TYPE", "Playlist", playlist, "playlist");
if (!this.options.nsfw && !(textChannel === null || textChannel === void 0 ? void 0 : textChannel.nsfw))
playlist.songs = playlist.songs.filter(s => !s.age_restricted);
if (!playlist.songs.length) {
if (!this.options.nsfw && !(textChannel === null || textChannel === void 0 ? void 0 : textChannel.nsfw))
throw new __1.DisTubeError("EMPTY_FILTERED_PLAYLIST");
throw new __1.DisTubeError("EMPTY_PLAYLIST");
}
const songs = playlist.songs;
const queue = this.queues.get(message);
if (queue) {
queue.addToQueue(songs, skip || unshift ? 1 : -1);
if (skip)
queue.skip();
else
this.emit("addList", queue, playlist);
}
else {
const newQueue = await this.createQueue(message, songs, textChannel);
if (newQueue instanceof __1.Queue) {
if (this.options.emitAddListWhenCreatingQueue)
this.emit("addList", newQueue, playlist);
this.emit("playSong", newQueue, newQueue.songs[0]);
}
}
}
/**
* Search for a song, fire {@link DisTube#event:error} if not found.
* @param {Discord.Message} message A message from guild channel
* @param {string} query The query string
* @returns {Promise<SearchResult?>} Song info
*/
async searchSong(message, query) {
var _a;
if (this.options.searchSongs > 1) {
const searchEvents = [
"searchNoResult",
"searchResult",
"searchCancel",
"searchInvalidAnswer",
"searchDone",
];
for (const evn of searchEvents) {
if (this.distube.listenerCount(evn) === 0) {
/* eslint-disable no-console */
console.warn(`"searchSongs" option is disabled due to missing "${evn}" listener.`);
console.warn(`If you don't want to use "${evn}" event, simply add an empty listener (not recommended):\n` +
`<DisTube>.on("${evn}", () => {})`);
/* eslint-enable no-console */
this.options.searchSongs = 0;
}
}
}
const limit = this.options.searchSongs > 1 ? this.options.searchSongs : 1;
const results = await this.distube
.search(query, {
limit,
safeSearch: this.options.nsfw ? false : !((_a = message.channel) === null || _a === void 0 ? void 0 : _a.nsfw),
})
.catch(() => {
if (!this.emit("searchNoResult", message, query)) {
// eslint-disable-next-line no-console
console.warn("searchNoResult event does not have any listeners! Emits `error` event instead.");
throw new __1.DisTubeError("NO_RESULT");
}
});
if (!results)
return null;
let result = results[0];
if (limit > 1) {
this.emit("searchResult", message, results, query);
const c = message.channel;
const answers = await (c.awaitMessages.length === 0
? c.awaitMessages({
filter: (m) => m.author.id === message.author.id,
max: 1,
time: this.options.searchCooldown * 1e3,
errors: ["time"],
})
: c.awaitMessages((m) => m.author.id === message.author.id, {
max: 1,
time: this.options.searchCooldown * 1e3,
errors: ["time"],
})).catch(() => undefined);
const ans = answers === null || answers === void 0 ? void 0 : answers.first();
if (!ans) {
this.emit("searchCancel", message, query);
return null;
}
const index = parseInt(ans.content, 10);
if (isNaN(index) || index > results.length || index < 1) {
this.emit("searchInvalidAnswer", message, ans, query);
return null;
}
this.emit("searchDone", message, ans, query);
result = results[index - 1];
}
return result;
}
/**
* Create a ytdl stream
* @param {Queue} queue Queue
* @returns {DisTubeStream}
*/
createStream(queue) {
var _a;
const { duration, formats, isLive, source, streamURL } = queue.songs[0];
const filterArgs = [];
queue.filters.forEach((filter) => filterArgs.push(this.distube.filters[filter]));
const ffmpegArgs = ((_a = queue.filters) === null || _a === void 0 ? void 0 : _a.length) ? ["-af", filterArgs.join(",")] : undefined;
const seek = duration ? queue.beginTime : undefined;
const streamOptions = { ffmpegArgs, seek, isLive };
Object.assign(streamOptions, this.ytdlOptions);
if (source === "youtube")
return _1.DisTubeStream.YouTube(formats, streamOptions);
return _1.DisTubeStream.DirectLink(streamURL, streamOptions);
}
}
exports.DisTubeHandler = DisTubeHandler;
exports.default = DisTubeHandler;
//# sourceMappingURL=DisTubeHandler.js.map