EngineBotMusic
A music bot
Overview
- EngineBot — the engine, in
src/core/. Loader, router, Components v2 helpers, database drivers. Knows nothing about music. - Music — the bot, in
src/music/,src/commands/music/,src/components/,src/events/. Built on the engine.
| Commands | UI | Voice | Multiserver | Database |
|---|---|---|---|---|
| Prefix & Slash | Components v2 | DAVE (E2EE) ✅ | Yes ✅ | 8 drivers ✅ |
The bot
/play <link or words>— plays it, or queues it. Also!play/!p./player— brings the panel back down after the channel has scrolled.
Everything else is on the panel itself:
| Button | What it does |
|---|---|
| ⏮️ | Restart the track, or step back to the last one within the first 5s |
| ⏸️ / ▶️ | Pause and resume |
| ⏭️ | Skip |
| ⏹️ | Stop and leave |
| 🔁 | Off → repeat track → repeat queue |
| Add | Queue a link or a search, without leaving the panel |
| 🔀 | Shuffle what's waiting |
| 📜 | The full queue, privately |
| 🔊 | Volume, 0 to 200 |
The panel shows the artist, the track on air, a generated progress bar that advances on
its own, and the next few tracks waiting. It repaints on every click and on a slow tick,
both thinned through coalesce() so Discord's rate limit is never the thing that breaks.
Add opens a small box for a link or a few words. What lands is announced in the channel under whoever asked for it, and the panel is reposted below that line so it stays the last message — a panel buried above the announcements is a panel nobody can press.
The command list is deliberately short: anything the panel already does well has no command
of its own. What's left is what a button can't do — /seek, /playnext, /search,
/lyrics, /volume, /skip <count>, /queue <page>.
The bot leaves on its own a minute after the last listener does.
Why it feels fast
Measured on this machine, one click used to cost about five seconds. Where it went:
| Step | Cost |
|---|---|
yt-dlp lookup (spawn + network) |
~2500 ms |
| Card render, no cover art | ~20 ms |
| Card render, blurred cover + PNG encode | ~21 ms |
| PNG encode alone | ~10 ms |
The image was never the problem — the whole card is ~20 ms. It was yt-dlp, and the panel
waiting on it. Three things fixed that:
Stream URLs are cached per track for an hour (src/music/resolve.js), and the next
track's URL is resolved in the background while the current one plays, so skip lands on a
URL already in hand. An expired link fails on open, so play() drops it and retries once on
a fresh one rather than dying on a stale entry.
The panel no longer waits for audio. Advancing the queue is instant; opening the stream is what takes seconds. So the queue moves, the panel is drawn from the new state, and the stream catches up on its own — the repaint used to sit behind the whole download.
The panel no longer waits for its own image. A fresh panel goes out in two passes: the text and the controls first, the card as an edit once it's drawn. Everything below the image builds in ~0.4 ms; the card is ~20 ms of canvas plus a 21 KB upload, and it used to go first.
The card is drawn under setImmediate(), which matters more than it looks: canvas work is
synchronous once it starts, so simply not awaiting it still blocked the send for the full
render. Handing it to the next tick took first paint from 23 ms to 0.45 ms.
Without the card, the panel states the track in text, so the first pass is never a blank box. An in-place repaint (the 10 s ticker) deliberately stays a single edit — the card is already on screen there, and blanking it to fill it back in would make the image flicker.
Instant controls skip the lock. Pause, shuffle and loop touch nothing but local state, so they no longer queue up behind a skip that's still resolving. Every button used to be as slow as the slowest one in flight.
Adding still costs a real lookup — nothing can remove that — so the Add button greys out and reads Adding… for its duration, and comes back whatever happens, including on a failed search.
Emojis
Buttons use the bot's application emojis through
@beret.27/discord-app-emojis.
src/music/emojis.js asks for play, pause, skip, previous, stop, shuffle,
loop, queue, volume, mute, music, artist, disc and live, and each one falls
back to its Unicode twin when the application doesn't have it. Drop matching images into
./emojis/ (play.png, skip.png, …) and the bot uploads them on first boot, and the
buttons pick them up with no code change.
Voice
Audio goes out end-to-end encrypted over DAVE, Discord's voice E2EE. @discordjs/voice
turns it on as soon as @snazzah/davey is installed, which is a dependency here;
joinVoiceChannel passes daveEncryption: true to make the choice visible, and the
connect log says whether the session negotiated it.
Tracks are resolved with yt-dlp (through youtube-dl-exec) — the stream URL is fetched
at play time, not queue time, because those URLs expire in a few hours and a long queue
would outlive them.
The engine
| Driver | What it is |
|---|---|
auto |
SQLite on Node 22.5+, else enginedb. No dependency either way. |
sqlite |
A SQLite file, through node:sqlite. |
enginedb |
The engine's format. Append-only log, folded into a snapshot. |
fusion |
Speed first. All in RAM. Hungry. |
spectral |
Memory first. Interned values, deflated on disk. |
json |
One readable JSON file. |
text |
One key<TAB>value line per entry. Copy it for your own. |
mysql |
A MySQL server. Needs npm install mysql2. |
memory |
Saves nothing. For tests. |
Your own driver: extend Driver, point driver at the file's path.
Setup
npm install
cp .env.example .env # then put your token in it
npm start
The token is all it needs. The app id is read from it. Turn on the Message Content intent for the prefix commands, and the Server Members/voice states the bot already asks for come with the invite.
License
MIT