diff --git a/README.md b/README.md new file mode 100644 index 0000000..a933c3b --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# 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](https://github.com/disgoorg/disgo) for the Discord gateway and REST client +* [disgolink](https://github.com/disgoorg/disgolink) to talk to Lavalink and manage player state +* [Lavalink](https://lavalink.dev/) 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+: + +```bash +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: + +```bash +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: + +```bash +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 + +```bash +go test ./... +```