package main import ( "fmt" "io" "log" "os" "os/exec" "os/signal" "syscall" "time" dg "github.com/bwmarrin/discordgo" "github.com/javif89/dotenv" ) // var commandPrefix string = "!" var commandHandlers = map[string]func(s *dg.Session, i *dg.InteractionCreate){ "play": playCommand, } func main() { env := dotenv.Load(".env") botToken := env.Get("DISCORD_BOT_TOKEN") log.Println("Starting bot") var discord *dg.Session var err error discord, err = dg.New(fmt.Sprintf("Bot %s", botToken)) if err != nil { log.Println("Error starting the bot", err) return } discord.AddHandler(ready) // discord.AddHandler(handleCommand) // This will receive an event when a message is sent discord.Identify.Intents = dg.IntentsGuilds | dg.IntentsGuildMessages | dg.IntentsGuildVoiceStates err = discord.Open() if err != nil { log.Println("Error opening Discord session: ", err) } // Register slash commands discord.AddHandler(handleSlashCommand) log.Println("Bot is running. Press CTRL+C to exit.") sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-sc log.Println("Bot is exiting...") log.Println("Removing commands") appId := env.Get("DISCORD_APP_ID") cmds, _ := discord.ApplicationCommands(appId, "338782945110392832") for _, cmd := range cmds { discord.ApplicationCommandDelete(appId, "338782945110392832", cmd.ID) } discord.Close() log.Println("Goodbye!") } func playCommand(s *dg.Session, i *dg.InteractionCreate) { log.Println("Handling play command") options := i.ApplicationCommandData().Options url := options[0].StringValue() s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ Type: dg.InteractionResponseChannelMessageWithSource, Data: &dg.InteractionResponseData{ Content: fmt.Sprintf("Playing dat music baybeee"), }, }) downloadVideo(url) // Get the channel msgServer, err := s.State.Guild(i.GuildID) if err != nil { log.Println("Failed to get server for action") } var voiceChannelId string for _, vs := range msgServer.VoiceStates { if vs.UserID == i.Member.User.ID { voiceChannelId = vs.ChannelID } } voiceChannel, err := s.ChannelVoiceJoin(msgServer.ID, voiceChannelId, false, true) if err != nil { log.Println("Failed to join voice channel") } log.Println("Joined channel") playOnVoiceChannel(voiceChannel) voiceChannel.Speaking(false) log.Println("Disconnecting from voice channel") time.Sleep(time.Second * 2) voiceChannel.Disconnect() log.Println("Disconnected") } func playOnVoiceChannel(voiceChannel *dg.VoiceConnection) { log.Println("Starting ffmpeg stream") // I got the original implementation from: // https://github.com/nhooyr/botatouille/blob/7e1cd9d5a8d517fd43fd11599b2a62bf832a5c96/cmd/botatouille/music/music.go#L62-L104 // after hours of searching. ffmpeg := exec.Command( "ffmpeg", "-i", "vid.webm", "-hide_banner", "-loglevel", "quiet", "-f", "data", "-map", "0:a", "-ar", "48k", "-ac", "2", "-acodec", "libopus", "-b:a", "128k", "pipe:1") ffmpegOut, err := ffmpeg.StdoutPipe() if err != nil { log.Fatal(err) } err = ffmpeg.Start() if err != nil { log.Fatal(err) } packets := [][]byte{} startTime := time.Now() bytes_sent := 0 for { // I read in the RFC that frames will not be bigger than this size p := make([]byte, 960) n, err := ffmpegOut.Read(p) bytes_sent = bytes_sent + n if err != nil { log.Printf("Bytes: %d", n) if err == io.EOF { log.Println("Done streaming") break } log.Fatal(err) } // log.Printf("Read packet: %d bytes", n) packets = append(packets, p[:n]) } elapsedTime := time.Since(startTime) log.Printf("bytes sent = %d", bytes_sent) log.Printf("Took %s seconds to run", elapsedTime) // log.Printf("Bytes/Sec = %s", elapsedTime) voiceChannel.Speaking(true) time.Sleep(time.Second * 2) log.Println("Playing sound") startTime = time.Now() bytes_sent = 0 for _, p := range packets { log.Printf("Sending packet: %d bytes", len(p)) bytes_sent += len(p) // packets_sent := packets_sent + p voiceChannel.OpusSend <- p } elapsedTime = time.Since(startTime) log.Printf("Packets = %d", len(packets)) log.Printf("Network bytes sent = %d", bytes_sent) log.Printf("Took %s seconds to run", elapsedTime) log.Println("Ended stream") } func handleSlashCommand(s *dg.Session, i *dg.InteractionCreate) { log.Println("Command received?") if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { h(s, i) } } func ready(s *dg.Session, event *dg.Ready) { commands := []*dg.ApplicationCommand{ { Name: "play", Description: "Play a song from youtube, spotify, apple music, etc", Options: []*dg.ApplicationCommandOption{ { Type: dg.ApplicationCommandOptionString, Name: "url", Description: "URL to the song", Required: true, }, }, }, } env := dotenv.Load(".env") appId := env.Get("DISCORD_APP_ID") log.Println("Bot is ready") log.Println("Bulk registering commands") _, err := s.ApplicationCommandBulkOverwrite(appId, "338782945110392832", commands) if err != nil { log.Fatal(err) } log.Println("Done") } func downloadVideo(url string) error { log.Printf("Downloading: %s", url) os.Remove("vid.webm") cmd := exec.Command( "yt-dlp", "-i", "-q", "-f", "bestaudio", "-o", "vid.%(ext)s", url) err := cmd.Run() log.Println("Downloaded") return err }