mrmBot-Matrix/utils/soundplayer.js

270 lines
12 KiB
JavaScript
Raw Normal View History

2023-03-15 14:09:09 +00:00
import * as logger from "./logger.js";
import fs from "fs";
import format from "format-duration";
import { Shoukaku, Connectors } from "shoukaku";
import { setTimeout } from "timers/promises";
export const players = new Map();
export const queues = new Map();
export const skipVotes = new Map();
export let manager;
export let nodes = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).lava;
export let connected = false;
export function connect(client) {
manager = new Shoukaku(new Connectors.OceanicJS(client), nodes, { moveOnDisconnect: true, resume: true, reconnectInterval: 500, reconnectTries: 1 });
manager.on("error", (node, error) => {
logger.error(`An error occurred on Lavalink node ${node}: ${error}`);
});
manager.on("debug", (node, info) => {
logger.debug(`Debug event from Lavalink node ${node}: ${info}`);
});
manager.once("ready", () => {
logger.log(`Successfully connected to ${manager.nodes.size} Lavalink node(s).`);
connected = true;
});
}
export async function reload(client) {
if (!manager) connect(client);
const activeNodes = manager.nodes;
const json = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" });
nodes = JSON.parse(json).lava;
const names = nodes.map((a) => a.name);
for (const name in activeNodes) {
if (!names.includes(name)) {
manager.removeNode(name);
}
}
for (const node of nodes) {
if (!activeNodes.has(node.name)) {
manager.addNode(node);
}
}
if (!manager.nodes.size) connected = false;
return manager.nodes.size;
}
export async function play(client, sound, options, music = false) {
if (!connected) return { content: "I'm not connected to any audio servers!", flags: 64 };
if (!manager) return { content: "The sound commands are still starting up!", flags: 64 };
if (!options.channel.guild) return { content: "This command only works in servers!", flags: 64 };
if (!options.member.voiceState) return { content: "You need to be in a voice channel first!", flags: 64 };
if (!options.channel.guild.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I can't join this voice channel!", flags: 64 };
const voiceChannel = options.channel.guild.channels.get(options.member.voiceState.channelID) ?? await client.rest.channels.get(options.member.voiceState.channelID);
if (!voiceChannel.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I don't have permission to join this voice channel!", flags: 64 };
if (!music && manager.players.has(options.channel.guildID)) return { content: "I can't play a sound effect while other audio is playing!", flags: 64 };
const node = manager.getNode();
if (!music && !nodes.filter(obj => obj.name === node.name)[0].local) {
sound = sound.replace(/\.\//, "https://raw.githubusercontent.com/esmBot/esmBot/master/");
}
let response;
try {
response = await node.rest.resolve(sound);
if (!response) return { content: "🔊 I couldn't get a response from the audio server.", flags: 64 };
if (response.loadType === "NO_MATCHES" || response.loadType === "LOAD_FAILED") return { content: "I couldn't find that song!", flags: 64 };
} catch (e) {
logger.error(e);
return { content: "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit.", flags: 64 };
}
const oldQueue = queues.get(voiceChannel.guildID);
if (!response.tracks || response.tracks.length === 0) return { content: "I couldn't find that song!", flags: 64 };
if (process.env.YT_DISABLED === "true" && response.tracks[0].info.sourceName === "youtube") return "YouTube playback is disabled on this instance.";
if (music) {
const sortedTracks = response.tracks.map((val) => { return val.track; });
const playlistTracks = response.playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]];
queues.set(voiceChannel.guildID, oldQueue ? [...oldQueue, ...playlistTracks] : playlistTracks);
}
const playerMeta = players.get(options.channel.guildID);
let player;
if (node.players.has(voiceChannel.guildID)) {
player = node.players.get(voiceChannel.guildID);
} else if (playerMeta?.player) {
const storedState = playerMeta?.player?.connection.state;
if (storedState && storedState === 1) {
player = playerMeta?.player;
}
}
const connection = player ?? await node.joinChannel({
guildId: voiceChannel.guildID,
channelId: voiceChannel.id,
shardId: voiceChannel.guild.shard.id,
deaf: true
});
if (oldQueue?.length && music) {
return `Your ${response.playlistInfo.name ? "playlist" : "tune"} \`${response.playlistInfo.name ? response.playlistInfo.name.trim() : (response.tracks[0].info.title !== "" ? response.tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`;
} else {
nextSong(client, options, connection, response.tracks[0].track, response.tracks[0].info, music, voiceChannel, playerMeta?.host ?? options.member.id, playerMeta?.loop ?? false, playerMeta?.shuffle ?? false);
return;
}
}
export async function nextSong(client, options, connection, track, info, music, voiceChannel, host, loop = false, shuffle = false, lastTrack = null) {
skipVotes.delete(voiceChannel.guildID);
const parts = Math.floor((0 / info.length) * 10);
let playingMessage;
if (music && lastTrack === track && players.has(voiceChannel.guildID)) {
playingMessage = players.get(voiceChannel.guildID).playMessage;
} else {
try {
const content = !music ? { content: "🔊 Playing sound..." } : {
embeds: [{
color: 16711680,
author: {
name: "Now Playing",
iconURL: client.user.avatarURL()
},
fields: [{
name: " Title",
value: info.title?.trim() !== "" ? info.title : "(blank)"
},
{
name: "🎤 Artist",
value: info.author?.trim() !== "" ? info.author : "(blank)"
},
{
name: "💬 Channel",
value: voiceChannel.name
},
{
name: "🌐 Node",
value: connection.node?.name ?? "Unknown"
},
{
name: `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
value: `0:00/${info.isStream ? "∞" : format(info.length)}`
}]
}]
};
if (options.type === "classic") {
playingMessage = await client.rest.channels.createMessage(options.channel.id, content);
} else {
if ((Date.now() - options.interaction.createdAt) >= 900000) { // discord interactions are only valid for 15 minutes
playingMessage = await client.rest.channels.createMessage(options.channel.id, content);
} else if (lastTrack && lastTrack !== track) {
playingMessage = await options.interaction.createFollowup(content);
} else {
playingMessage = await options.interaction[options.interaction.acknowledged ? "editOriginal" : "createMessage"](content);
if (!playingMessage) playingMessage = await options.interaction.getOriginal();
}
}
} catch {
// no-op
}
}
connection.removeAllListeners("exception");
connection.removeAllListeners("stuck");
connection.removeAllListeners("end");
connection.setVolume(0.70);
connection.playTrack({ track });
players.set(voiceChannel.guildID, { player: connection, type: music ? "music" : "sound", host, voiceChannel, originalChannel: options.channel, loop, shuffle, playMessage: playingMessage });
connection.once("exception", (exception) => errHandle(exception, client, connection, playingMessage, voiceChannel, options));
connection.on("stuck", () => {
const nodeName = manager.getNode().name;
connection.move(nodeName);
connection.resume();
});
connection.on("end", async (data) => {
if (data.reason === "REPLACED") return;
let queue = queues.get(voiceChannel.guildID);
const player = players.get(voiceChannel.guildID);
if (player && process.env.STAYVC === "true") {
player.type = "idle";
players.set(voiceChannel.guildID, player);
}
let newQueue;
if (player?.shuffle) {
if (player.loop) {
queue.push(queue.shift());
} else {
queue = queue.slice(1);
}
queue.unshift(queue.splice(Math.floor(Math.random() * queue.length), 1)[0]);
newQueue = queue;
} else if (player?.loop) {
queue.push(queue.shift());
newQueue = queue;
} else {
newQueue = queue ? queue.slice(1) : [];
}
queues.set(voiceChannel.guildID, newQueue);
if (newQueue.length !== 0) {
const newTrack = await connection.node.rest.decode(newQueue[0]);
nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track);
try {
if (options.type === "classic") {
if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
}
} catch {
// no-op
}
} else if (process.env.STAYVC !== "true") {
await setTimeout(400);
connection.node.leaveChannel(voiceChannel.guildID);
players.delete(voiceChannel.guildID);
queues.delete(voiceChannel.guildID);
skipVotes.delete(voiceChannel.guildID);
try {
const content = `🔊 The voice channel session in \`${voiceChannel.name}\` has ended.`;
if (options.type === "classic") {
await client.rest.channels.createMessage(options.channel.id, { content });
} else {
if ((Date.now() - options.interaction.createdAt) >= 900000) {
await client.rest.channels.createMessage(options.channel.id, { content });
} else {
await options.interaction.createFollowup({ content });
}
}
} catch {
// no-op
}
}
if (options.type === "classic") {
try {
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
} catch {
// no-op
}
}
});
}
export async function errHandle(exception, client, connection, playingMessage, voiceChannel, options, closed) {
try {
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
const playMessage = players.get(voiceChannel.guildID).playMessage;
if (playMessage.channel.messages.has(playMessage.id)) await playMessage.delete();
} catch {
// no-op
}
players.delete(voiceChannel.guildID);
queues.delete(voiceChannel.guildID);
skipVotes.delete(voiceChannel.guildID);
logger.error(exception);
try {
connection.node.leaveChannel(voiceChannel.guildID);
} catch {
// no-op
}
connection.removeAllListeners("exception");
connection.removeAllListeners("stuck");
connection.removeAllListeners("end");
try {
const content = closed ? `🔊 I got disconnected by Discord and tried to reconnect; however, I got this error instead:\n\`\`\`${exception}\`\`\`` : `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${exception.type}: ${exception.error}\`\`\``;
if (options.type === "classic") {
await client.rest.channels.createMessage(playingMessage.channel.id, { content });
} else {
if ((Date.now() - options.interaction.createdAt) >= 900000) {
await client.rest.channels.createMessage(options.channel.id, { content });
} else {
await options.interaction.createFollowup({ content });
}
}
} catch {
// no-op
}
}