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 }