Skip to content

EngineBotMusic

by Béret Node.js Other

A music bot

EngineBotMusic screenshot 1
README.md

EngineBot Music

Discord bot engine, and the music bot running on it - by levraiberet

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

Bash
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