Compare commits

..

2 commits

Author SHA1 Message Date
ea57995b75 Fix skipping and add better playing visualization 2025-01-29 06:56:46 +01:00
277b25e451 Update gitignore 2025-01-29 06:50:54 +01:00
3 changed files with 239 additions and 223 deletions

2
.gitignore vendored
View file

@ -130,7 +130,7 @@ dist
.pnp.* .pnp.*
lavalink/ lavalink/
Lavalink.jar Lavalink.jar
settings.json# Devenv # Devenv
.devenv* .devenv*
devenv.local.nix devenv.local.nix

View file

@ -1 +1 @@
{"requestChannels":{"777923126981558282":{"channelId":"1219764003421556816","messageId":"1296249229009752138"}}} {"requestChannels":{"777923126981558282":{"channelId":"1334001448832335945","messageId":"1334034171164753973"}}}

View file

@ -1,5 +1,5 @@
// Import discord.js
import { import {
type APIEmbed,
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
ButtonStyle, ButtonStyle,
@ -8,36 +8,59 @@ import {
EmbedBuilder, EmbedBuilder,
GatewayIntentBits, GatewayIntentBits,
type Message, type Message,
SlashCommandBuilder,
Interaction, Interaction,
} from "npm:discord.js"; } from "discord.js";
import { Connectors, Track } from "shoukaku"; // Import shoukaku
import { Connectors } from "shoukaku";
// Import kazagumo
import { import {
Kazagumo, Kazagumo,
type KazagumoPlayer, type KazagumoPlayer,
type KazagumoTrack, type KazagumoTrack,
type Payload, type Payload,
} from "npm:kazagumo"; } from "kazagumo";
import KazagumoPlugin from "npm:kazagumo-spotify"; // Import spotify and deezer plugins
import prettyMilliseconds from "npm:pretty-ms"; import KazagumoPlugin from "kazagumo-spotify";
import Deezer from "kazagumo-deezer";
// Import pretty-ms for better time formatting
import prettyMilliseconds from "pretty-ms";
// Import dotenv
import * as dotenv from "dotenv";
// Enable writing and reading files
import { readFileSync, writeFileSync } from "fs";
//Import process
import process from "process";
// Set GatewayIntents
const { Guilds, GuildVoiceStates, GuildMessages, MessageContent } = const { Guilds, GuildVoiceStates, GuildMessages, MessageContent } =
GatewayIntentBits; GatewayIntentBits;
import * as dotenv from "npm:dotenv"; // Setup dotenv
import { readFileSync, writeFileSync } from "node:fs";
import process from "node:process";
import Deezer from "npm:kazagumo-deezer";
dotenv.config(); dotenv.config();
// Check if spotify is setup
if (!process.env.SPOTIFY_ID || !process.env.SPOTIFY_SECRET) { if (!process.env.SPOTIFY_ID || !process.env.SPOTIFY_SECRET) {
throw "Spotify Credentials missing"; throw "Spotify Credentials missing";
} }
// Define settings interface
interface requestChannels {
[guildId: string]: {
channelId: string;
messageId: string;
};
}
interface settings {
requestChannels: requestChannels;
}
// Initialize and load settings from file
let settings: settings; let settings: settings;
try { try {
@ -46,6 +69,7 @@ try {
settings = { requestChannels: {} }; // Init if no data found settings = { requestChannels: {} }; // Init if no data found
} }
// Setup lavalink config
const Nodes = [ const Nodes = [
{ {
name: "lavalink", name: "lavalink",
@ -55,19 +79,26 @@ const Nodes = [
}, },
]; ];
// Setup discord client
const client = new Client({ const client = new Client({
intents: [Guilds, GuildVoiceStates, GuildMessages, MessageContent], intents: [Guilds, GuildVoiceStates, GuildMessages, MessageContent],
}); });
// Setup kazagumo
const kazagumo = new Kazagumo( const kazagumo = new Kazagumo(
{ {
// Set youtube as fallback search engine
defaultSearchEngine: "youtube", defaultSearchEngine: "youtube",
// Setup support for sharding i guess? Only here cause the shoukaku example says its important
send: (guildId: string, payload: Payload) => { send: (guildId: string, payload: Payload) => {
const guild = client.guilds.cache.get(guildId); const guild = client.guilds.cache.get(guildId);
if (guild) guild.shard.send(payload); if (guild) guild.shard.send(payload);
}, },
// Setup plugins
plugins: [ plugins: [
new Deezer({playlistLimit:20}), new Deezer(),
new KazagumoPlugin({ new KazagumoPlugin({
clientId: process.env.SPOTIFY_ID, clientId: process.env.SPOTIFY_ID,
clientSecret: process.env.SPOTIFY_SECRET, clientSecret: process.env.SPOTIFY_SECRET,
@ -82,32 +113,102 @@ const kazagumo = new Kazagumo(
Nodes Nodes
); );
interface requestChannels {
[guildId: string]: { // Setup empty embed
channelId: string; const emptyEmbed = new EmbedBuilder()
messageId: string; .setAuthor({
}; name: "Moe",
} iconURL: "https://cdn.m3.fyi/MoeLogo.gif",
interface settings { })
requestChannels: requestChannels; .setTitle("Nothing is being played right now")
} .setDescription("Enter message to search")
.addFields(
{
name: "Author",
value: "None",
inline: true,
},
{
name: "Duration",
value: "NaN:NaN",
inline: true,
}
)
.setColor("#ff0047");
// Setup buttons
const play = new ButtonBuilder()
.setCustomId("play")
.setLabel("Play/Pause")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943073793122355");
const stop = new ButtonBuilder()
.setCustomId("stop")
.setLabel("Stop Playing")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943074258694184");
const skip = new ButtonBuilder()
.setCustomId("skip")
.setLabel("Skip Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943074233516072");
const loop = new ButtonBuilder()
.setCustomId("loop")
.setLabel("Loop Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943073667289099");
const shuffle = new ButtonBuilder()
.setCustomId("shuffle")
.setLabel("Shuffle Queue")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325437962678352");
const seek = new ButtonBuilder()
.setCustomId("seek")
.setLabel("Seek Forward")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325511878889504");
const previous = new ButtonBuilder()
.setCustomId("previous")
.setLabel("Previous Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325512071831562");
// Add buttons to action row
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
play,
stop,
skip,
loop
);
const row2 = new ActionRowBuilder<ButtonBuilder>().addComponents(
shuffle,
seek,
previous
);
client.on("ready", () => { client.on("ready", () => {
// console.log(client)
console.log(`${client.user!.tag}·Ready!`); console.log(`${client.user!.tag}·Ready!`);
}); });
kazagumo.shoukaku.on("ready", (name: string) => kazagumo.shoukaku.on("ready", (name: string) =>
console.log(`Lavalink ${name}: Ready!`) console.log(`Lavalink ${name}: Ready!`)
); );
kazagumo.shoukaku.on("error", (name: string, error: Error) => kazagumo.shoukaku.on("error", (name: string, error: Error) =>
console.error(`Lavalink ${name}: Error Caught,`, error) console.error(`Lavalink ${name}: Error Caught,`, error)
); );
kazagumo.shoukaku.on("close", (name: string, code: unknown, reason: unknown) => kazagumo.shoukaku.on("close", (name: string, code: unknown, reason: unknown) =>
console.warn( console.warn(
`Lavalink ${name}: Closed, Code ${code}, Reason ${reason || "No reason"}` `Lavalink ${name}: Closed, Code ${code}, Reason ${reason || "No reason"}`
) )
); );
kazagumo.shoukaku.on("debug", (name: string, info: string) => kazagumo.shoukaku.on("debug", (name: string, info: string) =>
console.debug(`Lavalink ${name}: Debug,`, info) console.debug(`Lavalink ${name}: Debug,`, info)
); );
@ -118,156 +219,119 @@ kazagumo.shoukaku.on("disconnect", (name: string, count: number) => {
console.warn(`Lavalink ${name}: Disconnected`); console.warn(`Lavalink ${name}: Disconnected`);
}); });
kazagumo.on("playerStart", (player: KazagumoPlayer, track: KazagumoTrack) => { kazagumo.on("playerStart", (player: KazagumoPlayer, track: KazagumoTrack) => {
// Exit if player is not initialized yet
if (!player.textId) return; if (!player.textId) return;
const channel = client.channels.cache.get(player.textId); const channel = client.channels.cache.get(player.textId);
if (!channel) return; if (!channel) return;
if (channel.type === ChannelType.GuildText) { if (channel.type === ChannelType.GuildText) {
channel // TODO:Also update player embed
.send({ channel.send({
content: `Now playing **${track.title}** by **${track.author}**`, content: `Now playing **${track.title}** by **${track.author}**`,
}) })
.then((x: any) => { .then((x: any) => {
//TODO: Use embed message instead
player.data.set("message", x);
setTimeout(() => x.delete(), 3000); setTimeout(() => x.delete(), 3000);
}); });
} }
}); });
// Called when track ends
kazagumo.on("playerEnd", (player: KazagumoPlayer) => { kazagumo.on("playerEnd", (player: KazagumoPlayer) => {
//TODO: Use embed message instead // Replace old embed with embed of next track
let currentTrack = player.queue[0]
let track_author: string;
if (currentTrack.author) {
track_author = currentTrack.author;
} else {
track_author = "";
}
let track_duration: string;
if (currentTrack.length) {
track_duration = prettyMilliseconds(currentTrack.length);
} else {
track_duration = "NaN:NaN";
}
let track_thumbnail: string;
if (currentTrack.thumbnail) {
track_thumbnail = currentTrack.thumbnail.toString();
} else {
track_thumbnail = "NaN:NaN";
}
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setAuthor({ .setAuthor({
name: "Moe", name: "Now playing",
iconURL: "https://cdn.m3.fyi/MoeLogo.gif", iconURL: "https://cdn.m3.fyi/MoeLogo.gif",
}) })
.setTitle("Nothing is being played right now") .setTitle(
.setDescription("Enter message to search") player.queue.length > 0
? `Playing ${player.queue.length} from ${player.data.get("playlist")}`
: `Playing ${currentTrack.title}`
)
.setDescription(`[${currentTrack.title}](${currentTrack.uri})`)
.addFields( .addFields(
{ {
name: "Author", name: "Author",
value: "None", value: track_author,
inline: true, inline: true,
}, },
{ {
name: "Duration", name: "Duration",
value: "NaN:NaN", value: track_duration,
inline: true, inline: true,
} }
) )
.setThumbnail(track_thumbnail)
.setColor("#ff0047"); .setColor("#ff0047");
player.data.get("message")?.edit({ embeds: [embed] });
player.data.get("message")?.edit({ embed: [embed] });
}); });
// Called when disconnecting
kazagumo.on("playerEmpty", (player: KazagumoPlayer) => { kazagumo.on("playerEmpty", (player: KazagumoPlayer) => {
// Return if player is not initialized
if (!player.textId) return; if (!player.textId) return;
const channel = client.channels.cache.get(player.textId); const channel = client.channels.cache.get(player.textId);
if (!channel) return; if (!channel) return;
if (channel.type === ChannelType.GuildText) { if (channel.type === ChannelType.GuildText) {
//TODO: Use embed message instead
channel // Replace old embed with empty one
.send({ content: "Destroyed player due to inactivity." }) // also needed here as playerEnd doesn't get called if only one song is in the queue
player.data.get("message")?.edit({ embeds: [emptyEmbed] });
// Send message before disconnecting
channel.send({ content: "Destroyed player due to inactivity." })
.then((x: any) => { .then((x: any) => {
player.data.set("message", x);
setTimeout(() => x.delete(), 3000); setTimeout(() => x.delete(), 3000);
}); });
} }
player.destroy(); player.destroy();
}); });
// TODO: Better command handling
// Called when a new message is send
client.on("messageCreate", async (msg: Message) => { client.on("messageCreate", async (msg: Message) => {
console.log(msg.content); if (msg.author.bot) return; // Ignore bot messages
if (msg.author.bot) return; if (!msg.guild) return; // Ignore dms
if (!msg.guild) return; if (!msg.member) return; // Ignore Interactions
if (!msg.member) return; if (msg.channel.type !== ChannelType.GuildText) return; // Only listen in text channels
if (msg.channel.type !== ChannelType.GuildText) return;
// If message contains command prefix
if (msg.content.startsWith(".")) { if (msg.content.startsWith(".")) {
// Split prefix for easier comparison
let cmd = msg.content.split(".")[1]; let cmd = msg.content.split(".")[1];
switch (cmd) { switch (cmd) {
// Initialize channel on server
case "init": case "init":
if (msg.guildId && msg.channelId) { if (msg.guildId && msg.channelId) {
if (!settings.requestChannels[msg.guildId]) { if (!settings.requestChannels[msg.guildId]) {
await msg.delete(); await msg.delete();
const play = new ButtonBuilder()
.setCustomId("play")
.setLabel("Play/Pause")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943073793122355");
const stop = new ButtonBuilder()
.setCustomId("stop")
.setLabel("Stop Playing")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943074258694184");
const skip = new ButtonBuilder()
.setCustomId("skip")
.setLabel("Skip Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943074233516072");
const loop = new ButtonBuilder()
.setCustomId("loop")
.setLabel("Loop Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943073667289099");
const shuffle = new ButtonBuilder()
.setCustomId("shuffle")
.setLabel("Shuffle Queue")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325437962678352");
const seek = new ButtonBuilder()
.setCustomId("seek")
.setLabel("Seek Forward")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325511878889504");
const previous = new ButtonBuilder()
.setCustomId("previous")
.setLabel("Previous Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325512071831562");
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
play,
stop,
skip,
loop
);
const row2 = new ActionRowBuilder<ButtonBuilder>().addComponents(
shuffle,
seek,
previous
);
const embed = new EmbedBuilder()
.setAuthor({
name: "Moe",
iconURL: "https://cdn.m3.fyi/MoeLogo.gif",
})
.setTitle("Nothing is being played right now")
.setDescription("Enter message to search")
.addFields(
{
name: "Author",
value: "None",
inline: true,
},
{
name: "Duration",
value: "NaN:NaN",
inline: true,
}
)
.setColor("#ff0047");
const response = await msg.channel.send({ const response = await msg.channel.send({
embeds: [embed], embeds: [emptyEmbed],
components: [row, row2], components: [row, row2],
}); });
@ -294,29 +358,26 @@ client.on("messageCreate", async (msg: Message) => {
} }
return; return;
} }
if (msg.channel.name === "moe-song-requests") { // Return if guild has not setup bot yet
if (!Object.keys(settings.requestChannels).includes(msg.guild.id)) {
return
}
// If the msg channel is the request channel, use message as search query
if (msg.channel.id === settings.requestChannels[msg.guild.id].channelId) {
let controls; let controls;
if (!settings.requestChannels[msg.guild.id]) { let channelId = settings.requestChannels[msg.guild.id].channelId;
let answer = await msg.reply("Use .init first"); let messageId = settings.requestChannels[msg.guild.id].messageId;
console.log(answer); let requestChannel = msg.guild.channels.cache.get(channelId);
setTimeout(() => msg.delete(), 3000); if (requestChannel && requestChannel.type === ChannelType.GuildText) {
setTimeout(() => answer.delete(), 3000); controls = await requestChannel.messages.fetch(messageId);
return;
} else {
let channelId = settings.requestChannels[msg.guild.id].channelId;
let messageId = settings.requestChannels[msg.guild.id].messageId;
let channel = msg.guild.channels.cache.get(channelId);
if (channel && channel.type === ChannelType.GuildText) {
controls = await channel.messages.fetch(messageId);
}
// let controls = msg.channel.guild
// get(requestChannels[msg.guild.id]);
// client.channels.fetch(requestChannels[msg.guild.id])
} }
// let controls = msg.channel.guild
// get(requestChannels[msg.guild.id]);
// client.channels.fetch(requestChannels[msg.guild.id])
const query = msg.content; const query = msg.content;
const { channel } = msg.member.voice; let voiceChannel = msg.member.voice.channel;
if (!channel) { if (!voiceChannel) {
let answer = await msg.reply( let answer = await msg.reply(
"You need to be in a voice channel to use this command!" "You need to be in a voice channel to use this command!"
); );
@ -328,19 +389,15 @@ client.on("messageCreate", async (msg: Message) => {
const player = await kazagumo.createPlayer({ const player = await kazagumo.createPlayer({
guildId: msg.guild.id, guildId: msg.guild.id,
textId: msg.channel.id, textId: msg.channel.id,
voiceId: channel.id, voiceId: voiceChannel.id,
volume: 40, volume: 40,
}); });
player.data.set("message", controls); player.data.set("message", controls);
console.log("Player created, Searching with deezer"); let result = await kazagumo.search(query, { requester: msg.author, engine: "deezer" })
let result = await kazagumo.search(query, { requester: msg.author, engine: "deezer"})
console.log(result.tracks);
if (result.tracks.length === 0) { if (result.tracks.length === 0) {
console.log("Searching with youtube"); result = await kazagumo.search(query, { requester: msg.author })
result = await kazagumo.search(query, { requester: msg.author})
if (result.tracks.length === 0) { if (result.tracks.length === 0) {
console.log("no yt result found")
let answer = await msg.reply("No results found!"); let answer = await msg.reply("No results found!");
setTimeout(() => answer.delete(), 3000); setTimeout(() => answer.delete(), 3000);
return; return;
@ -351,60 +408,6 @@ client.on("messageCreate", async (msg: Message) => {
for (const track of result.tracks) player.queue.add(track); for (const track of result.tracks) player.queue.add(track);
else player.queue.add(result.tracks[0]); else player.queue.add(result.tracks[0]);
const play = new ButtonBuilder()
.setCustomId("play")
.setLabel("Play/Pause")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943073793122355");
const stop = new ButtonBuilder()
.setCustomId("stop")
.setLabel("Stop Playing")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943074258694184");
const skip = new ButtonBuilder()
.setCustomId("skip")
.setLabel("Skip Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943074233516072");
const loop = new ButtonBuilder()
.setCustomId("loop")
.setLabel("Loop Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("889943073667289099");
const shuffle = new ButtonBuilder()
.setCustomId("shuffle")
.setLabel("Shuffle Queue")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325437962678352");
const seek = new ButtonBuilder()
.setCustomId("seek")
.setLabel("Seek Forward")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325511878889504");
const previous = new ButtonBuilder()
.setCustomId("previous")
.setLabel("Previous Song")
.setStyle(ButtonStyle.Secondary)
.setEmoji("890325512071831562");
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
play,
stop,
skip,
loop
);
const row2 = new ActionRowBuilder<ButtonBuilder>().addComponents(
shuffle,
seek,
previous
);
let track_author: string; let track_author: string;
if (result.tracks[0].author) { if (result.tracks[0].author) {
track_author = result.tracks[0].author; track_author = result.tracks[0].author;
@ -448,11 +451,11 @@ client.on("messageCreate", async (msg: Message) => {
) )
.setThumbnail(track_thumbnail) .setThumbnail(track_thumbnail)
.setColor("#ff0047"); .setColor("#ff0047");
if (result.type === "PLAYLIST") player.data.set("playlist", result.playlistName);
if (!player.playing && !player.paused) player.play(); if (!player.playing && !player.paused) player.play();
if (controls) { if (controls) {
const response = controls.edit({ controls.edit({
embeds: [embed], embeds: [embed],
components: [row, row2], components: [row, row2],
}); });
@ -464,7 +467,6 @@ client.on("messageCreate", async (msg: Message) => {
}); });
client.on("interactionCreate", async (interaction: Interaction) => { client.on("interactionCreate", async (interaction: Interaction) => {
console.log(interaction.id);
if (interaction.isButton()) { if (interaction.isButton()) {
const guild = client.guilds.cache.get(interaction.guildId!); const guild = client.guilds.cache.get(interaction.guildId!);
const member = guild?.members.cache.get(interaction.member!.user.id); const member = guild?.members.cache.get(interaction.member!.user.id);
@ -477,69 +479,83 @@ client.on("interactionCreate", async (interaction: Interaction) => {
// volume: 40 // volume: 40
// }) // })
switch (interaction.customId) { switch (interaction.customId) {
// Toggle Play/Pause
case "play": case "play":
player.pause(!player.paused); player.pause(!player.paused);
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Toggled Pause", content: "Toggled Pause",
}); });
break; break;
// Stop playing and disconnect
case "stop": case "stop":
// player.disconnect();
player.data.get("message")?.edit({ embeds: [emptyEmbed] });
player.destroy(); player.destroy();
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Stopped Playing", content: "Stopped Playing",
}); });
break; break;
// Skip currently playing song
case "skip": case "skip":
player.skip(); player.skip();
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Skipped Song", content: "Skipped Song",
}); });
break; break;
// Skip back to previously playing song
case "previous":
let track = player.getPrevious(true);
await interaction.reply({
flags: "Ephemeral",
content: "Queued previous song",
});
player.play(track);
break;
// Enable/Disable Looping
case "loop": case "loop":
if (player.loop != "none") { if (player.loop != "none") {
player.setLoop("none"); player.setLoop("none");
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Disabled Looping", content: "Disabled Looping",
}); });
} else { } else {
player.setLoop("track"); player.setLoop("track");
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Started Looping", content: "Started Looping",
}); });
} }
break; break;
// Shuffle current queue
case "shuffle": case "shuffle":
player.queue.shuffle(); player.queue.shuffle();
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Shuffled Queue", content: "Shuffled Queue",
}); });
break; break;
// TODO: Skip forward 5 seconds
case "seek": case "seek":
// let position = player.position / 1000; // let position = player.position / 1000;
// position = position | 0; // position = position | 0;
// console.log(position); // console.log(position);
// player.seek(position + 5); // player.seek(position + 5);
await interaction.reply({ await interaction.reply({
ephemeral: true, flags: "Ephemeral",
content: "Does not work :pensive:", content: "Does not work :pensive:",
// content: "Skipped 5s forward", // content: "Skipped 5s forward",
}); });
break; break;
case "previous":
let track = player.getPrevious();
await interaction.reply({
ephemeral: true,
content: "Queued previous song",
});
player.play(track[0]);
break;
default: default:
break; break;