Discord Music Agent

2026 · Full-stack · AI · github ↗

A Discord music bot that turns "some chill vibes", a half-remembered lyric or a Spotify link into a queue of studio masters, played bit-exact and gapless.

1980s-playlist
1980s playlist
Why this exists

Every Discord music bot I'd used had the same problem: they were search engines with a play button. You had to know exactly what you wanted — the right title, the right artist, spelled the right way. If you asked for "that sad song from the movie," you got nothing. And what you got back was whatever YouTube ranked first: a music video, a lyric video, a fan upload with the intro cut off.

I wanted a bot that worked the way a friend would. You say "play some chill vibes," it picks something. You say "that one song that goes da-da-da-daaa," it tries to figure it out. You paste a Spotify link, it plays the same recording. You say "what's the weather," it politely declines instead of burning a token pretending to search.

So I built one around a Gemini agent, and then spent most of the time on everything around the agent.

How it thinks

Every /play request goes through a GeminiAgent that classifies it: play a specific thing, clarify with a shortlist, suggest picks for a mood, build a playlist, queue an album, start a station ("songs like Rush"), or reject because it isn't music. Direct commands like /skip, /jump and /pause never reach the model; the per-guild MusicAgent handles them locally.

Gemini also does the one thing a regex is bad at: picking the right recording. Every search hits two sources, the YouTube Music catalog (through InnerTube, no API key) and regular YouTube via yt-dlp. The model picks the best candidate from the merged list with a fixed preference order: catalog song, then Official Audio, then lyric video, then the music video, then anything else. Regular YouTube only wins for things the catalog doesn't have, like DJ sets, live sets and podcasts. A regex ranker is the fallback when the model is unavailable.

slash-commands
slash commands
What the model is not allowed to do

An LLM knows a lot of music and none of this year's. So the bot does not let it invent tracks. /playlist chill afrobeats finds playlists real people made for that vibe and queues one; /playlist <theme> drafts from real catalog tracks, with a recent: dial that sets how much of the mix comes from playlists built for the current year (60% by default). Stations come from the catalog's own recommendation graph: 25 tracks, no search, no guessing. The model curates and refines; it never fabricates a title.

Spotify and Apple Music links work the same way. Nothing is streamed from either service. Each track's title, artist and duration is matched against the YouTube Music catalog, so you get the same studio master. Spotify has withdrawn playlist access from app-only tokens, so playlists fall back to the public embed page, which still lists up to 100 tracks and needs no credentials.

The audio path

This is the part I'm happiest with. yt-dlp's WebM/Opus stream is piped straight to Discord with zero transcoding, as long as the volume is at 100%, so what arrives is bit-exact. The next track is fetched while the current one plays, so handovers are instant instead of paying yt-dlp's ~2.5s cold start. Shared volume is the one exception: moving it off 100% re-encodes at 128 kbps, so the CPU cost is only paid when someone actually uses it. Every track posts its own now-playing panel with transport and volume controls, and the previous panel is removed so stale buttons can't act on the current song.

The README has a section on Discord-side settings because the bot can't fix them: the default 64 kbps channel bitrate crushes music, and the app's noise suppression and gain control are tuned for calls.

The self-hosted constraint

YouTube challenges datacenter IPs aggressively, so the bot is self-hosted by design: a home server or a Raspberry Pi on a residential IP. That constraint kept the scope small — no multi-tenant infra, no abuse layer, one person, one box. For anyone who does want a VPS, a one-shot deploy/setup.sh installs Node, ffmpeg and yt-dlp and registers a systemd service, and a cookie file from a throwaway account is what keeps YouTube from asking the server to prove it isn't a bot. The bot says so in-channel when the cookies expire.

theme-based-suggestions
Smart suggestions for playlist based on theme
Small things that matter
  • /setup carves out a dedicated music channel and refuses commands anywhere else, which kills the "someone played a song in #general" problem.
  • Playlist drafts are keyed per user with a five-minute idle TTL, so two people refining at once don't trample each other.
  • Environment variables go through Zod at boot; a missing token fails immediately with a clear error instead of dying on the first /play.
  • Pino redacts tokens and keys from logs automatically, so output can be piped anywhere.
Tech stack

TypeScript, Node.js 22, discord.js v14 with @discordjs/voice and the native Opus encoder, Gemini, youtubei.js, yt-dlp, Pino, Zod, Vitest.

Stack
Typescript · Node.js · Discord.js · Gemini · yt-dlp · Vitest · Zod
© 2026 Adepeju Orefejo