Files
velox-bot/internal/commands/music/public/play.go
T
FernandoJVideira da66eb8dd5 feat(music): let the dashboard add songs, starting a session if needed
New AddToQueue appends to an already-active session without touching
voice (the dashboard can't join a channel on its own), and add_song's
dispatch case now branches: a voice_channel_id in the payload means
"start fresh", so it goes through EnqueueAndPlay (join + play) instead.
Query normalization (plain text -> YouTube search, stripping
autoplay/radio params off pasted YouTube URLs) moves out of /play's
handler into a shared NormalizeQuery, both entry points need it, not
just one.

Starting playback from the dashboard also posts the now-playing embed
into the voice channel's own built-in text chat, matching what /play
already does, instead of leaving it with nowhere to show up.
2026-08-29 19:44:24 +01:00

96 lines
2.6 KiB
Go

package public
import (
"fmt"
"velox-bot/internal/commands/music/shared"
"velox-bot/internal/music"
"github.com/bwmarrin/discordgo"
)
var Play = &discordgo.ApplicationCommand{
Name: "play",
Description: "Play a song via Lavalink",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "query",
Description: "Song name or URL",
Required: true,
},
},
}
func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireMusicEnabled(s, i) {
return
}
data := i.ApplicationCommandData()
if len(data.Options) == 0 {
return
}
query := data.Options[0].StringValue()
// Always acknowledge quickly to avoid "application did not respond".
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Searching...",
},
})
if !memberHasDJRole(s, i.GuildID, i.Member) {
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: ptr("You need the **DJ** role to use music commands."),
})
return
}
vs, err := findUserVoiceState(s, i.GuildID, i.Member.User.ID)
if err != nil {
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: ptr(fmt.Sprintf("Error: %v", err)),
})
return
}
entry, started, err := music.EnqueueAndPlay(i.GuildID, vs.ChannelID, i.ChannelID, query, i.Member.User.Username, i.Member.User.ID)
if err != nil {
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: ptr(fmt.Sprintf("Error: %v", err)),
})
return
}
title := entry.Track.Info.Title
author := entry.Track.Info.Author
length := entry.Track.Info.Length
if started {
// manager will post now-playing message & manage invalidating old buttons
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: ptr(fmt.Sprintf("Starting: **%s** by **%s** `[%ds]`", title, author, length/1000)),
})
} else {
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: ptr(fmt.Sprintf("Added to queue: **%s** by **%s** `[%ds]`", title, author, length/1000)),
})
}
}
func ptr[T any](v T) *T { return new(v) }
func findUserVoiceState(s *discordgo.Session, guildID, userID string) (*discordgo.VoiceState, error) {
g, err := s.State.Guild(guildID)
if err != nil {
return nil, fmt.Errorf("cannot find guild voice state")
}
for _, vs := range g.VoiceStates {
if vs.UserID == userID {
return vs, nil
}
}
return nil, fmt.Errorf("you must be in a voice channel")
}