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

113
main.go
View file

@ -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
}