161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
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
|
|
}
|