feat: support multiple radio stations

This commit is contained in:
Felipe M 2025-05-08 09:32:06 +02:00
parent 83843de4ee
commit e5cf23619d
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8

107
main.go
View file

@ -17,9 +17,27 @@ import (
"github.com/lmittmann/tint" "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 ( var (
token string token string
radioURL = "https://atres-live.europafm.com/live/europafm/bitrate_1.m3u8" 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 logger *slog.Logger
debug bool debug bool
guildID string // For testing in a specific guild guildID string // For testing in a specific guild
@ -97,8 +115,8 @@ func main() {
"guild_id", i.GuildID) "guild_id", i.GuildID)
switch cmdName { switch cmdName {
case "europafm": case "radio":
handleEuropaFMCommand(s, i) handleRadioCommand(s, i)
default: default:
logger.Warn("Unknown command received", "command", cmdName) logger.Warn("Unknown command received", "command", cmdName)
} }
@ -184,18 +202,27 @@ func main() {
// Define the command // Define the command
cmd := &discordgo.ApplicationCommand{ cmd := &discordgo.ApplicationCommand{
Name: "europafm", Name: "radio",
Description: "Control EuropaFM radio streaming", Description: "Controla la transmisión de radio",
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{ {
Type: discordgo.ApplicationCommandOptionSubCommand, Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "play", 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, Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "stop", 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 // Disconnect from all voice channels
for _, vc := range dg.VoiceConnections { for _, vc := range dg.VoiceConnections {
vc.Disconnect() if vc.Disconnect() != nil {
logger.Error("Error disconnecting from voice channel", "error", err)
}
} }
// Close the Discord session // Close the Discord session
@ -259,7 +288,7 @@ func logRawInteraction(s *discordgo.Session, e *discordgo.Event) {
} }
// Detailed logging for interactions // Detailed logging for interactions
func logInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { func logInteraction(_ *discordgo.Session, i *discordgo.InteractionCreate) {
if !debug { if !debug {
return return
} }
@ -275,11 +304,11 @@ func logInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
"user", i.User != nil) "user", i.User != nil)
} }
func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { func handleRadioCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background() ctx := context.Background()
options := i.ApplicationCommandData().Options options := i.ApplicationCommandData().Options
if len(options) == 0 { 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 return
} }
@ -291,7 +320,7 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate)
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ 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 { switch subCommand {
case "play": 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 // Find the guild for that channel
guildID := i.GuildID guildID := i.GuildID
logger := logger.With("guild_id", guildID) logger = logger.With("guild_id", guildID)
// Find the voice channel the user is in // Find the voice channel the user is in
vs, err := findUserVoiceState(ctx, s, guildID, i.Member.User.ID) vs, err := findUserVoiceState(ctx, s, guildID, i.Member.User.ID)
if err != nil { if err != nil {
logger.Warn("User not in voice channel", "error", err) 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 return
} }
@ -327,7 +372,7 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate)
vc, err := s.ChannelVoiceJoin(guildID, vs.ChannelID, false, true) vc, err := s.ChannelVoiceJoin(guildID, vs.ChannelID, false, true)
if err != nil { if err != nil {
logger.Error("Error joining voice channel", "error", err) 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 return
} }
@ -335,7 +380,7 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Update message // 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 // Start streaming - this is a blocking call, so we run it in a goroutine
logger.Info("Starting audio stream") logger.Info("Starting audio stream")
@ -347,11 +392,11 @@ func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate)
defer logger.Info("Audio streaming ended") defer logger.Info("Audio streaming ended")
// Start playing audio with enhanced error logging // Start playing audio with enhanced error logging
err := playAudioStream(vc, radioURL, stopChan, logger) err := playAudioStream(vc, station.StreamURL, stopChan, logger)
if err != nil { if err != nil {
logger.Error("Audio streaming error", "error", err) 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) vc, err := findExistingVoiceConnection(ctx, s, i.GuildID)
if err != nil { if err != nil {
logger.Info("No active voice connection found", "error", err) 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 return
} }
logger.Info("Disconnecting from voice channel") logger.Info("Disconnecting from voice channel")
vc.Disconnect() if vc.Disconnect() != nil {
followupMsg(s, i, "Disconnected from voice channel.") 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) guild, err := s.State.Guild(guildID)
if err != nil { if err != nil {
logger.Error("Error getting guild", "guild_id", guildID, "error", err) 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") 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 { for _, vc := range s.VoiceConnections {
if vc.GuildID == guildID { if vc.GuildID == guildID {
return vc, nil return vc, nil
@ -480,3 +527,17 @@ func playAudioStream(vc *discordgo.VoiceConnection, url string, stopChan chan bo
return fmt.Errorf("audio stream ended unexpectedly") 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
}