Skip to content

Commit

Permalink
feat: /play
Browse files Browse the repository at this point in the history
  • Loading branch information
JirayuSrisawat-Github committed Oct 1, 2024
1 parent dc9b595 commit 4acbe76
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 1 deletion.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
"@sapphire/type": "^2.4.4",
"@sapphire/utilities": "^3.15.3",
"colorette": "^2.0.20",
"common-tags": "^1.8.2",
"discord.js": "^14.16.3",
"ms": "^2.1.3",
"sakulink": "^1.2.11"
},
"devDependencies": {
"@sapphire/cli": "^1.9.3",
"@sapphire/prettier-config": "^2.0.0",
"@sapphire/ts-config": "^5.0.0",
"@types/common-tags": "^1.8.4",
"@types/ms": "^0.7.34",
"@types/node": "^20.11.5",
"@types/ws": "^8.5.10",
"eslint": "^9.11.1",
Expand Down
123 changes: 123 additions & 0 deletions src/commands/music/play.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ApplyOptions } from "@sapphire/decorators";
import { Command } from "@sapphire/framework";
import { APIEmbed, GuildMember, InteractionResponse } from "discord.js";
import { defaultVolume } from "../../config";
import ms from "ms";

@ApplyOptions<Command.Options>({
// The description of the command.
description: "Plays a song.",
})
/**
* The `play` command.
*
* @since 1.0.0
*/
export class UserCommand extends Command {
/**
* Registers the command to the registry.
*
* @param registry The registry to register the command to.
* @since 1.0.0
*/
public override registerApplicationCommands(registry: Command.Registry): void {
registry.registerChatInputCommand({
name: this.name,
description: this.description,
options: [
{
name: "query",
description: "The query to search for.",
type: 3,
required: true,
},
],
});
}

/**
* Runs the command when it is invoked.
*
* @param interaction The interaction that invoked the command.
* @since 1.0.0
*/
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction): Promise<InteractionResponse<boolean> | undefined> {
/**
* The query to search for.
*/
const query = interaction.options.getString("query", true);

this.container.logger.debug(`Searching ${query} in ${interaction.guildId!}`);

/**
* Searches for the query and returns the result.
*/
const result = await this.container.client.manager.search(query, interaction.user!);
this.container.logger.debug(JSON.stringify(result));

/**
* If the search result is an error or empty, reply with the error message.
*/
if ("error" === result.loadType || "empty" === result.loadType)
return await interaction.reply({
embeds: [
{
color: 0xff0000,
description: JSON.stringify(result, null, 2),
title: result.loadType,
},
],
});

/**
* Gets the player for the guild or creates a new one if it doesn't exist.
*/
let player = this.container.client.manager.players.get(interaction.guildId!);
if (!player) {
this.container.logger.debug(`Creating new player for ${interaction.guildId!}`);
player = this.container.client.manager.create({
guild: interaction.guildId!,
voiceChannel: (<GuildMember>interaction.member).voice.channelId!,
textChannel: interaction.channelId!,
volume: defaultVolume,
selfDeafen: true,
selfMute: false,
});
}

/**
* If the search result is a single track, add it to the queue.
* If the search result is a playlist, add all the tracks to the queue.
*/
if ("track" === result.loadType || "search" === result.loadType) player.queue.add(result.tracks[0]);
else if ("playlist" === result.loadType) player.queue.add(result.playlist!.tracks);
this.container.logger.debug(`Added ${result.tracks[0].title} to queue in ${interaction.guildId!}`);

/**
* If the player is not playing, play the first track in the queue.
* If the player is not connected, connect it.
*/
if (!player.playing) await player.play();
if ("CONNECTED" !== player.state) player.connect();

/**
* Creates an embed to send to the user.
*/
const embed: APIEmbed = {
color: 0xeab676,
description: `**${"playlist" === result.loadType ? result.playlist!.name : result.tracks[0].title}** - \`${ms(result.tracks[0].duration)}\``,
author: {
name: `Added ${"playlist" === result.loadType ? `${result.tracks.length} tracks` : "1 track"} to queue`,
icon_url: this.container.client.user!.displayAvatarURL({ size: 4096, extension: "jpg" }),
},
footer: {
text: `Requester ${interaction.user.username} | Node: ${player.node.options.identifier}`,
},
};

/**
* Sends the embed to the user.
*/
await interaction.reply({ embeds: [embed] });
}
}
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NodeOptions } from "sakulink";

export const token: string = "";
export const defaultSearchPlatform: string = "youtube music"; // Spotify
export const defaultSearchPlatform: string = "youtube music";
export const defaultVolume: number = 75;
export const nodes: NodeOptions[] = [
{
identifier: "Jirayu_V4",
Expand Down
18 changes: 18 additions & 0 deletions src/structures/Lavalink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,24 @@ export class Lavalink extends Manager {
*/
this.on("queueEnd", (player: Player): void => {
this.client.logger.info(`Ended queue in ${player.guild}[${player.node.options.identifier}]`);

/**
* Destroys the player if the queue is empty after 5 seconds.
* This is done to prevent the player from staying in the guild
* forever when the queue is empty.
*/
setTimeout(() => {
const _player = this.client.manager.players.get(player.guild!)!;
/**
* Gets the current track of the player.
* If there is no current track, the player is destroyed.
*/
this.client.logger.debug(`Checking if the queue is empty in ${player.guild}[${player.node.options.identifier}]`);
if (!_player.queue.current) {
this.client.logger.debug(`The queue is empty in ${player.guild}[${player.node.options.identifier}], destroying the player.`);
player.destroy();
}
}, 5000);
});
}
}

0 comments on commit 4acbe76

Please sign in to comment.