From 12fb3704db217cc408b662ec73cdd41e028c0e08 Mon Sep 17 00:00:00 2001 From: Bobby Date: Sun, 13 Apr 2025 18:10:13 +0530 Subject: implement play command; music playback working --- commands/commands.go | 9 ++- commands/play.go | 180 +++++++++++++++++++++++++++++++++++++++--- commands/play_autocomplete.go | 119 ++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+), 13 deletions(-) create mode 100644 commands/play_autocomplete.go (limited to 'commands') 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 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, + }) + } +} -- cgit v1.2.3