summaryrefslogtreecommitdiff
path: root/commands
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-04-13 18:10:13 +0530
committerBobby <[email protected]>2025-04-13 18:10:13 +0530
commit12fb3704db217cc408b662ec73cdd41e028c0e08 (patch)
tree4cd3a34898d362773d77a31e03695cb2480286c7 /commands
parent1b271061415b33a8f18d1d3d960bc750b9557b69 (diff)
downloadai-12fb3704db217cc408b662ec73cdd41e028c0e08.tar.xz
ai-12fb3704db217cc408b662ec73cdd41e028c0e08.zip
implement play command; music playback working
Diffstat (limited to 'commands')
-rw-r--r--commands/commands.go9
-rw-r--r--commands/play.go180
-rw-r--r--commands/play_autocomplete.go119
3 files changed, 295 insertions, 13 deletions
diff --git a/commands/commands.go b/commands/commands.go
index 53dc3dc..00f09f8 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -9,10 +9,11 @@ var (
Description: "Search and play a song from Spotify or YouTube",
Options: []*discordgo.ApplicationCommandOption{
{
- Type: discordgo.ApplicationCommandOptionString,
- Name: "query",
- Description: "Search query for the song/playlist (or URL)",
- Required: true,
+ Type: discordgo.ApplicationCommandOptionString,
+ Name: "query",
+ Description: "Search query for the song/playlist (or URL)",
+ Required: true,
+ Autocomplete: true,
},
},
},
diff --git a/commands/play.go b/commands/play.go
index 6c7fae4..b8d39fd 100644
--- a/commands/play.go
+++ b/commands/play.go
@@ -1,24 +1,186 @@
package commands
import (
+ "ai/types"
+ "ai/utils/logger"
+ "ai/utils/music"
"fmt"
+ "strings"
"github.com/bwmarrin/discordgo"
)
-// Command: Play
-// Takes in a search query and autocompletes the search query to play a song from Spotify or YouTube
-// Also takes in a URL to a Spotify Song/Playlist or YouTube Video/Playlist
-// Joins the user's voice channel and plays the song. Adds to the queue if a song is already playing
-// in any of the voice channels. User must be in a voice channel to use this command
-// Usage: /play <search query or URL>
func Play(s *discordgo.Session, i *discordgo.InteractionCreate) {
- // For now return the query as reply
- reply := fmt.Sprintf("Playing: %s", i.ApplicationCommandData().Options[0].StringValue())
+ options := i.ApplicationCommandData().Options
+ input := options[0].StringValue()
+
+ // Special cases from autocomplete
+ if input == "min_chars" {
+ respondWithError(s, i, "Enter at least 3 characters to search.")
+ return
+ }
+
+ if input == "no_results" || input == "search_error" {
+ respondWithError(s, i, "No results found for your query. Try a different search term.")
+ return
+ }
+
+ // Check if the user is in a voice channel BEFORE deferring response
+ guildID := i.GuildID
+ userID := i.Member.User.ID
+
+ isSameVC, userChannelID := music.IsUserInSameVC(s, guildID, userID)
+
+ if userChannelID == "" {
+ respondWithError(s, i, "You must be in a voice channel to use this command.")
+ return
+ }
+
+ // Check if bot is already in a voice channel but not the same as the user
+ voice, exists := music.VoiceConnection[guildID]
+ if exists && !isSameVC {
+ channel, err := s.Channel(voice.ChannelID)
+ if err == nil {
+ respondWithError(s, i, fmt.Sprintf("I'm already in the voice channel **%s**. You must be in the same voice channel to control playback.", channel.Name))
+ } else {
+ respondWithError(s, i, "I'm already in a different voice channel. You must be in the same voice channel to control playback.")
+ }
+ return
+ }
+
+ // Now that we've checked all error conditions, defer the response
+ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
+ })
+
+ // Process track selection or URL
+ var trackURL, trackID, trackTitle string
+ var sourceType types.SourceType
+
+ if strings.Contains(input, "|") {
+ parts := strings.Split(input, "|")
+ if len(parts) >= 3 {
+ sourceType = types.SourceType(parts[0])
+ trackID = parts[1]
+ trackURL = parts[2]
+
+ trackInfo, err := music.GetTrackInfo(trackID, sourceType)
+ if err != nil {
+ trackTitle = "Selected track"
+ } else {
+ trackTitle = trackInfo.Title
+ }
+
+ if sourceType == types.Spotify {
+ ytTrack, err := music.GetYouTubeForSpotify(trackInfo.Title, trackInfo.Artist)
+ if err != nil {
+ updateResponse(s, i, "❌ Error fetching YouTube equivalent for Spotify track.")
+ return
+ }
+ trackURL = ytTrack.URL
+ trackID = ytTrack.ID
+ }
+ } else {
+ // malformed selection
+ updateResponse(s, i, "❌ Invalid track selection. Please try again.")
+ return
+ }
+ } else {
+ // Direct URL or search query
+ if music.IsYouTubeURL(input) {
+ trackInfo, err := music.GetYouTubeInfo(input)
+ if err != nil {
+ updateResponse(s, i, "❌ Failed to get information for this YouTube URL.")
+ return
+ }
+ trackURL = input
+ trackID = trackInfo.ID
+ trackTitle = trackInfo.Title
+ sourceType = types.YouTube
+ } else if music.IsSpotifyURL(input) {
+ trackInfo, err := music.GetSpotifyInfo(input)
+ if err != nil {
+ updateResponse(s, i, "❌ Failed to get information for this Spotify URL.")
+ return
+ }
+
+ // Get YouTube equivalent
+ ytTrack, err := music.GetYouTubeForSpotify(trackInfo.Title, trackInfo.Artist)
+ if err != nil {
+ updateResponse(s, i, "❌ Error fetching YouTube equivalent for Spotify track.")
+ return
+ }
+ trackURL = ytTrack.URL
+ trackID = ytTrack.ID
+ trackTitle = ytTrack.Title
+ sourceType = types.Spotify
+ } else {
+ // treat as search query
+ results, err := music.Search(input, 1)
+ if err != nil || len(results) == 0 {
+ updateResponse(s, i, "❌ No results found for your search query.")
+ return
+ }
+
+ result := results[0]
+ trackTitle = result.Title
+ trackID = result.ID
+ sourceType = result.SourceType
+
+ if result.SourceType == types.Spotify {
+ ytTrack, err := music.GetYouTubeForSpotify(result.Title, result.Artist)
+ if err != nil {
+ updateResponse(s, i, "❌ Error fetching YouTube equivalent for Spotify track.")
+ return
+ }
+ trackURL = ytTrack.URL
+ trackID = ytTrack.ID
+ } else {
+ trackURL = result.URL
+ }
+ }
+ }
+
+ // Join voice channel
+ voice, err := music.JoinVoiceChannel(s, guildID, userChannelID)
+ if err != nil {
+ logger.Log(fmt.Sprintf("Failed to join voice channel: %v", err), types.LogOptions{
+ Prefix: "Play Command",
+ Level: types.Error,
+ })
+ updateResponse(s, i, "❌ Failed to join your voice channel.")
+ return
+ }
+
+ // Send "now playing" message first
+ updateResponse(s, i, fmt.Sprintf("🎵 Now playing: **%s**", trackTitle))
+
+ // Play the track
+ go func() {
+ err := voice.PlayYouTube(trackURL, trackID)
+ if err != nil {
+ logger.Log(fmt.Sprintf("Failed to play track: %v", err), types.LogOptions{
+ Prefix: "Play Command",
+ Level: types.Error,
+ })
+ // Also update the message to show the error to the user
+ updateResponse(s, i, fmt.Sprintf("❌ Error playing **%s**: %v", trackTitle, err))
+ }
+ }()
+}
+
+func respondWithError(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
- Content: reply,
+ Content: message,
+ Flags: discordgo.MessageFlagsEphemeral,
},
})
}
+
+func updateResponse(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
+ s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
+ Content: &message,
+ })
+}
diff --git a/commands/play_autocomplete.go b/commands/play_autocomplete.go
new file mode 100644
index 0000000..dee02d1
--- /dev/null
+++ b/commands/play_autocomplete.go
@@ -0,0 +1,119 @@
+package commands
+
+import (
+ "ai/types"
+ "ai/utils/logger"
+ "ai/utils/music"
+ "fmt"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+func PlayAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) {
+ var focusedOption *discordgo.ApplicationCommandInteractionDataOption
+
+ for _, option := range i.ApplicationCommandData().Options {
+ if option.Focused {
+ focusedOption = option
+ break
+ }
+ }
+
+ if focusedOption == nil {
+ return
+ }
+
+ query := focusedOption.StringValue()
+
+ if len(query) < 3 {
+ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+ Type: discordgo.InteractionApplicationCommandAutocompleteResult,
+ Data: &discordgo.InteractionResponseData{
+ Choices: []*discordgo.ApplicationCommandOptionChoice{
+ {
+ Name: "Please enter at least 3 characters",
+ Value: "min_chars",
+ },
+ },
+ },
+ })
+ return
+ }
+
+ // Search for tracks
+ results, err := music.Search(query, 10)
+ if err != nil {
+ logger.Log(fmt.Sprintf("Search error: %v", err), types.LogOptions{
+ Prefix: "Play Autocomplete",
+ Level: types.Error,
+ })
+
+ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+ Type: discordgo.InteractionApplicationCommandAutocompleteResult,
+ Data: &discordgo.InteractionResponseData{
+ Choices: []*discordgo.ApplicationCommandOptionChoice{
+ {
+ Name: "Error searching. Try again later.",
+ Value: "search_error",
+ },
+ },
+ },
+ })
+ return
+ }
+
+ // Create choices for autocomplete
+ choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, 25)
+
+ for i, result := range results {
+ if i >= 25 {
+ break // Discord limits to 25 choices
+ }
+
+ // Format display name
+ var displayName string
+ if result.SourceType == types.YouTube {
+ displayName = fmt.Sprintf("▶️ %s - %s", result.Title, result.Artist)
+ } else {
+ displayName = fmt.Sprintf("🎵 %s - %s", result.Title, result.Artist)
+ }
+
+ // Truncate name if needed
+ if len(displayName) > 100 {
+ displayName = displayName[:97] + "..."
+ }
+
+ // Use just source type and ID as value to avoid length issues
+ valueStr := fmt.Sprintf("%s|%s|%s", result.SourceType, result.ID, result.URL)
+ if len(valueStr) > 100 {
+ valueStr = fmt.Sprintf("%s|%s", result.SourceType, result.ID)
+ }
+
+ choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
+ Name: displayName,
+ Value: valueStr,
+ })
+ }
+
+ if len(choices) == 0 {
+ choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
+ Name: "No results found",
+ Value: "no_results",
+ })
+ }
+
+ // Send response
+ err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+ Type: discordgo.InteractionApplicationCommandAutocompleteResult,
+ Data: &discordgo.InteractionResponseData{
+ Choices: choices,
+ },
+ })
+
+ if err != nil {
+ logger.Log(fmt.Sprintf("Failed to send autocomplete response: %v", err), types.LogOptions{
+ Prefix: "Play Autocomplete",
+ Level: types.Error,
+ })
+ }
+}