Files
2026-09-21 18:48:26 +02:00

5.0 KiB

discord music bot

A music bot for Discord written in Go. Plays songs and playlists in a voice channel through Lavalink, with the usual set of commands you'd expect: queue, skip, pause, seek, volume, loop, shuffle.

Why Go

Discord bots are usually a Node or Python thing, but I wanted something that starts instantly, uses barely any memory while idle, and doesn't need a runtime installed on the host. Go fits that well, and disgo turned out to be a solid, well typed library to build on top of.

Stack

  • disgo for the Discord gateway and REST client
  • disgolink to talk to Lavalink and manage player state
  • Lavalink does the actual audio streaming and decoding, running as a separate process
  • godotenv for loading .env files in local development
  • the standard slog package for logging, nothing fancier than that

How it's organized

cmd/bot            entry point, wires everything together and starts the bot
internal/config    reads and validates environment variables
internal/logging   slog setup
internal/lavalink  connects to the Lavalink node
internal/discord   disgo client, voice event handling, command router
internal/commands  one file per slash command
internal/music     per guild queue and player, no Discord imports at all

internal/music is kept independent from disgo on purpose. It's the part with actual logic (queueing, shuffling, looping) so it made sense to be able to test it without spinning up a bot or a Lavalink server. That's what queue_test.go covers.

Running it

You need Go 1.27+ and a Lavalink server. Lavalink is just a jar file, running it locally only needs Java 17+:

java -jar Lavalink.jar

By default that listens on localhost:2333 with password youshallnotpass, both configurable in Lavalink's own application.yml.

Then copy the example env file and fill in your bot token:

cp .env.example .env
Variable Required What it's for
DISCORD_TOKEN yes your bot's token from the Discord Developer Portal
LAVALINK_ADDRESS yes host:port of the Lavalink node
LAVALINK_PASSWORD yes Lavalink node password
LAVALINK_NODE_NAME no just an internal label, defaults to main
LAVALINK_SECURE no set to true if the node uses wss://
DEBUG no true turns on debug level logs

Never commit your .env. If a token leaks anywhere (a commit, a screenshot, whatever) go revoke it right away from the Developer Portal and generate a new one.

Once configured:

go run ./cmd/bot

Commands register globally on startup, which means the bot works in every server it's in without extra setup, though Discord can take up to an hour to propagate a fresh global registration. Existing servers keep working fine while that happens.

Commands

Command Does what
/play query searches and plays a song or a whole playlist, or queues it if something's already playing
/skip skips the current song
/stop stops everything, clears the queue and leaves the channel
/pause pauses
/resume resumes
/queue lists what's coming up
/nowplaying shows the current track
/seek position jumps to a spot in the track, accepts seconds, mm:ss or hh:mm:ss
/volume level sets volume from 0 to 200 percent, default is 100
/loop mode off, track to repeat the current song, or queue to send it to the back once it ends
/shuffle shuffles the upcoming tracks
/remove position removes a track by its position in /queue

/play takes a direct link or plain text, which gets searched on YouTube through ytsearch:. If your Lavalink instance doesn't have YouTube configured you can pass a link from whatever source it does support, or prefix your search manually, scsearch: for SoundCloud for example.

Queueing a playlist link adds every track from it: if nothing's playing yet the first one starts right away and the rest go into the queue, otherwise the whole thing just gets appended at the end.

About voice channels

Only one listening session per server at a time. Once the bot has joined a channel, /play, /skip, /stop, /pause, /resume, /seek, /volume, /loop, /shuffle and /remove only respond to people in that same channel, so another group somewhere else in the server can't take over or interrupt playback. Before the bot has joined anywhere, anyone can start it. /queue and /nowplaying are read only and work for anybody.

If everyone leaves the channel the bot is in (or the last real member moves elsewhere), it stops playback, empties the queue and disconnects on its own.

Adding a command

Create a new file under internal/commands with a struct implementing the Command interface, Definition() and Execute(), copying the shape of any existing command. Then register it in commands.NewRegistry(...) inside cmd/bot/main.go. Nothing else needs touching, the router picks it up automatically.

Tests

go test ./...