Implement slash commands for playback control
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/disgoorg/disgo/discord"
|
||||
"github.com/disgoorg/disgo/events"
|
||||
"github.com/disgoorg/disgolink/v4/disgolink"
|
||||
"github.com/disgoorg/disgolink/v4/lavalink"
|
||||
|
||||
"discord-music-bot/internal/music"
|
||||
)
|
||||
|
||||
type PlayCommand struct {
|
||||
deps *Dependencies
|
||||
}
|
||||
|
||||
func NewPlayCommand(deps *Dependencies) *PlayCommand {
|
||||
return &PlayCommand{deps: deps}
|
||||
}
|
||||
|
||||
func (c *PlayCommand) Definition() discord.ApplicationCommandCreate {
|
||||
return discord.SlashCommandCreate{
|
||||
Name: "play",
|
||||
Description: "Plays a song or playlist, or adds it to the queue",
|
||||
Options: []discord.ApplicationCommandOption{
|
||||
discord.ApplicationCommandOptionString{
|
||||
Name: "query",
|
||||
Description: "Song name, song URL, or playlist URL",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PlayCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error {
|
||||
gid, ok := guildID(event)
|
||||
if !ok {
|
||||
return guildOnlyError(event)
|
||||
}
|
||||
|
||||
channelID, ok := memberVoiceChannel(c.deps.Discord, gid, event.User().ID)
|
||||
if !ok {
|
||||
return rejectEphemeral(event, "Join a voice channel first.")
|
||||
}
|
||||
|
||||
if allowed, reason := ensureCanControlPlayback(c.deps.Discord, gid, event.User().ID); !allowed {
|
||||
return rejectEphemeral(event, reason)
|
||||
}
|
||||
|
||||
query := event.SlashCommandInteractionData().String("query")
|
||||
|
||||
if err := event.DeferCreateMessage(false); err != nil {
|
||||
return fmt.Errorf("deferring response: %w", err)
|
||||
}
|
||||
|
||||
loaded, err := loadTracks(ctx, c.deps.Lavalink, query)
|
||||
if err != nil {
|
||||
return c.followUp(event, err.Error())
|
||||
}
|
||||
|
||||
if err := c.deps.Discord.UpdateVoiceState(ctx, gid, &channelID, false, false); err != nil {
|
||||
return c.followUp(event, fmt.Sprintf("Could not join your voice channel: %s", err))
|
||||
}
|
||||
|
||||
result, err := c.deps.Music.Guild(gid).EnqueueMany(ctx, loaded.tracks)
|
||||
if err != nil {
|
||||
return c.followUp(event, fmt.Sprintf("Could not play that track: %s", err))
|
||||
}
|
||||
|
||||
return c.followUp(event, playSummary(loaded, result))
|
||||
}
|
||||
|
||||
func playSummary(loaded trackLoadResult, result music.EnqueueResult) string {
|
||||
if len(loaded.tracks) <= 1 {
|
||||
title := "that track"
|
||||
if len(loaded.tracks) == 1 {
|
||||
title = loaded.tracks[0].Info.Title
|
||||
}
|
||||
if result.PlayingNow {
|
||||
return fmt.Sprintf("Now playing: **%s**", title)
|
||||
}
|
||||
return fmt.Sprintf("Added to queue: **%s**", title)
|
||||
}
|
||||
|
||||
name := loaded.playlistName
|
||||
if name == "" {
|
||||
name = "the playlist"
|
||||
}
|
||||
|
||||
if result.PlayingNow {
|
||||
return fmt.Sprintf("Now playing **%s** and queued %d more track(s) from **%s**.",
|
||||
result.Started.Info.Title, result.Queued, name)
|
||||
}
|
||||
return fmt.Sprintf("Added %d tracks from **%s** to the queue.", len(loaded.tracks), name)
|
||||
}
|
||||
|
||||
func (c *PlayCommand) followUp(event *events.ApplicationCommandInteractionCreate, content string) error {
|
||||
_, err := event.Client().Rest.UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.NewMessageUpdate().WithContent(content))
|
||||
return err
|
||||
}
|
||||
|
||||
func normalizeQuery(query string) string {
|
||||
if strings.HasPrefix(query, "http://") || strings.HasPrefix(query, "https://") {
|
||||
return query
|
||||
}
|
||||
return "ytsearch:" + query
|
||||
}
|
||||
|
||||
type trackLoadResult struct {
|
||||
tracks []lavalink.Track
|
||||
playlistName string
|
||||
}
|
||||
|
||||
func loadTracks(ctx context.Context, lavalinkClient *disgolink.Client, query string) (trackLoadResult, error) {
|
||||
node := lavalinkClient.BestNode()
|
||||
if node == nil {
|
||||
return trackLoadResult{}, fmt.Errorf("no music node is available right now")
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
result trackLoadResult
|
||||
err error
|
||||
}
|
||||
outcomeCh := make(chan outcome, 1)
|
||||
|
||||
node.Rest.LoadTracksHandler(ctx, normalizeQuery(query), disgolink.NewTrackLoadingResultHandler(
|
||||
func(track lavalink.Track) {
|
||||
outcomeCh <- outcome{result: trackLoadResult{tracks: []lavalink.Track{track}}}
|
||||
},
|
||||
func(playlist lavalink.Playlist) {
|
||||
if len(playlist.Tracks) == 0 {
|
||||
outcomeCh <- outcome{err: fmt.Errorf("that playlist is empty")}
|
||||
return
|
||||
}
|
||||
outcomeCh <- outcome{result: trackLoadResult{
|
||||
tracks: playlist.Tracks,
|
||||
playlistName: playlist.Info.Name,
|
||||
}}
|
||||
},
|
||||
func(tracks []lavalink.Track) {
|
||||
if len(tracks) == 0 {
|
||||
outcomeCh <- outcome{err: fmt.Errorf("no results found for %q", query)}
|
||||
return
|
||||
}
|
||||
outcomeCh <- outcome{result: trackLoadResult{tracks: []lavalink.Track{tracks[0]}}}
|
||||
},
|
||||
func() {
|
||||
outcomeCh <- outcome{err: fmt.Errorf("no results found for %q", query)}
|
||||
},
|
||||
func(err error) {
|
||||
outcomeCh <- outcome{err: fmt.Errorf("failed to load track: %w", err)}
|
||||
},
|
||||
))
|
||||
|
||||
res := <-outcomeCh
|
||||
return res.result, res.err
|
||||
}
|
||||
Reference in New Issue
Block a user