From e5cf23619d256734ae5f62d9ec05c676efbb92ca Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 8 May 2025 09:32:06 +0200 Subject: [PATCH] feat: support multiple radio stations --- main.go | 113 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index 71a6ade..7cb0200 100644 --- a/main.go +++ b/main.go @@ -17,12 +17,30 @@ import ( "github.com/lmittmann/tint" ) +// Station represents a radio station +type Station struct { + Name string // Display name + StreamURL string // URL for the audio stream + Description string // Optional description +} + var ( token string - radioURL = "https://atres-live.europafm.com/live/europafm/bitrate_1.m3u8" - logger *slog.Logger - debug bool - guildID string // For testing in a specific guild + stations = map[string]Station{ + "europafm": { + Name: "Europa FM", + StreamURL: "https://atres-live.europafm.com/live/europafm/bitrate_1.m3u8", + Description: "La radio de éxitos musicales", + }, + "cope": { + Name: "COPE", + StreamURL: "http://flucast31-h-cloud.flumotion.com/cope/net1.mp3", + Description: "A tope con la", + }, + } + logger *slog.Logger + debug bool + guildID string // For testing in a specific guild ) func init() { @@ -97,8 +115,8 @@ func main() { "guild_id", i.GuildID) switch cmdName { - case "europafm": - handleEuropaFMCommand(s, i) + case "radio": + handleRadioCommand(s, i) default: logger.Warn("Unknown command received", "command", cmdName) } @@ -184,18 +202,27 @@ func main() { // Define the command cmd := &discordgo.ApplicationCommand{ - Name: "europafm", - Description: "Control EuropaFM radio streaming", + Name: "radio", + Description: "Controla la transmisión de radio", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "play", - Description: "Start streaming EuropaFM radio in your voice channel", + Description: "Comienza a transmitir una radio en tu canal de voz", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "station", + Description: "Estación de radio a reproducir", + Required: true, + Choices: getStationChoices(), + }, + }, }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "stop", - Description: "Stop streaming and leave the voice channel", + Description: "Detiene la transmisión y abandona el canal de voz", }, }, } @@ -237,7 +264,9 @@ func main() { // Disconnect from all voice channels for _, vc := range dg.VoiceConnections { - vc.Disconnect() + if vc.Disconnect() != nil { + logger.Error("Error disconnecting from voice channel", "error", err) + } } // Close the Discord session @@ -259,7 +288,7 @@ func logRawInteraction(s *discordgo.Session, e *discordgo.Event) { } // Detailed logging for interactions -func logInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { +func logInteraction(_ *discordgo.Session, i *discordgo.InteractionCreate) { if !debug { return } @@ -275,11 +304,11 @@ func logInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { "user", i.User != nil) } -func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { +func handleRadioCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { ctx := context.Background() options := i.ApplicationCommandData().Options if len(options) == 0 { - logger.Warn("Received europafm command with no subcommand", "interaction_id", i.ID) + logger.Warn("Received radio command with no subcommand", "interaction_id", i.ID) return } @@ -291,7 +320,7 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "Processing your request...", + Content: "Procesando tu solicitud...", }, }) @@ -302,15 +331,31 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) switch subCommand { case "play": + // Check if station is specified + stationName := "europafm" // Default value + if len(options[0].Options) > 0 { + stationName = options[0].Options[0].StringValue() + } + + // Get station URL + station, ok := stations[stationName] + if !ok { + logger.Warn("Unknown station requested", "station", stationName) + followupMsg(s, i, "Estación de radio desconocida.") + return + } + + logger = logger.With("station", stationName, "station_name", station.Name) + // Find the guild for that channel guildID := i.GuildID - logger := logger.With("guild_id", guildID) + logger = logger.With("guild_id", guildID) // Find the voice channel the user is in vs, err := findUserVoiceState(ctx, s, guildID, i.Member.User.ID) if err != nil { logger.Warn("User not in voice channel", "error", err) - followupMsg(s, i, "You need to be in a voice channel to use this command.") + followupMsg(s, i, "Necesitas estar en un canal de voz para usar este comando.") return } @@ -327,7 +372,7 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) vc, err := s.ChannelVoiceJoin(guildID, vs.ChannelID, false, true) if err != nil { logger.Error("Error joining voice channel", "error", err) - followupMsg(s, i, "Error joining voice channel. Please try again.") + followupMsg(s, i, "Error al unirse al canal de voz. Por favor, inténtalo de nuevo.") return } @@ -335,7 +380,7 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) time.Sleep(2 * time.Second) // Update message - followupMsg(s, i, "Now streaming EuropaFM radio in your voice channel!") + followupMsg(s, i, fmt.Sprintf("¡Ahora transmitiendo la radio %s en tu canal de voz!", station.Name)) // Start streaming - this is a blocking call, so we run it in a goroutine logger.Info("Starting audio stream") @@ -347,11 +392,11 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) defer logger.Info("Audio streaming ended") // Start playing audio with enhanced error logging - err := playAudioStream(vc, radioURL, stopChan, logger) + err := playAudioStream(vc, station.StreamURL, stopChan, logger) if err != nil { logger.Error("Audio streaming error", "error", err) - followupMsg(s, i, "The audio stream has ended unexpectedly. Please try again later.") + followupMsg(s, i, "La transmisión de audio ha terminado inesperadamente. Por favor, inténtalo más tarde.") } }() @@ -360,13 +405,15 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) vc, err := findExistingVoiceConnection(ctx, s, i.GuildID) if err != nil { logger.Info("No active voice connection found", "error", err) - followupMsg(s, i, "Not currently streaming in any channel.") + followupMsg(s, i, "No hay transmisión activa en ningún canal.") return } logger.Info("Disconnecting from voice channel") - vc.Disconnect() - followupMsg(s, i, "Disconnected from voice channel.") + if vc.Disconnect() != nil { + logger.Error("Error disconnecting from voice channel", "error", err) + } + followupMsg(s, i, "Desconectado del canal de voz.") } } @@ -380,7 +427,7 @@ func followupMsg(s *discordgo.Session, i *discordgo.InteractionCreate, content s } } -func findUserVoiceState(ctx context.Context, s *discordgo.Session, guildID, userID string) (*discordgo.VoiceState, error) { +func findUserVoiceState(_ context.Context, s *discordgo.Session, guildID, userID string) (*discordgo.VoiceState, error) { guild, err := s.State.Guild(guildID) if err != nil { logger.Error("Error getting guild", "guild_id", guildID, "error", err) @@ -396,7 +443,7 @@ func findUserVoiceState(ctx context.Context, s *discordgo.Session, guildID, user return nil, fmt.Errorf("user not in any voice channel") } -func findExistingVoiceConnection(ctx context.Context, s *discordgo.Session, guildID string) (*discordgo.VoiceConnection, error) { +func findExistingVoiceConnection(_ context.Context, s *discordgo.Session, guildID string) (*discordgo.VoiceConnection, error) { for _, vc := range s.VoiceConnections { if vc.GuildID == guildID { return vc, nil @@ -480,3 +527,17 @@ func playAudioStream(vc *discordgo.VoiceConnection, url string, stopChan chan bo return fmt.Errorf("audio stream ended unexpectedly") } + +// getStationChoices generates the choices for the command options based on the stations map +func getStationChoices() []*discordgo.ApplicationCommandOptionChoice { + choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(stations)) + + for key, station := range stations { + choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ + Name: station.Name, + Value: key, + }) + } + + return choices +}