Discord Music Bot: Make Your Own in Python or JavaScript
The famous music bots fell one after another and the ones left keep changing their terms. Your own bot that plays your music in a voice channel takes an afternoon: here it is in full, with a song queue, plus what you need to keep up to date so Discord lets it into voice.
Why the music bots disappeared
In 2021, YouTube sent letters to the two most used music bots on Discord because they played songs taken from its videos: Groovy shut down on August 30 and Rythm on September 15. YouTube's and Spotify's terms don't allow downloading or rebroadcasting their content outside their apps, so any bot that does it lives on borrowed time.
That's why this bot plays music you give it: files uploaded to a folder. Make it music that's yours or that you have permission to use (your own recordings, royalty-free music or Creative Commons-licensed music, respecting what its licence asks). What's played on a server is the responsibility of whoever runs it (Terms).
Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.
What voice needs (and why old tutorials don't work)
FFmpeg, the program that reads the MP3 (or OGG, FLAC…) and turns it into audio for Discord. On your computer you have to install it; on NapNodes it's already there.
The voice libraries: in Python, discord.py with its voice extra (discord.py[voice], which brings PyNaCl and davey); in JavaScript, @discordjs/voice and @discordjs/opus. If the console says “PyNaCl library needed in order to use voice”, that extra is missing.
And up-to-date versions. Since March 2026 Discord requires end-to-end encryption (the DAVE protocol) on voice and video calls, and bots are no exception. You need discord.py 2.7 or newer, or @discordjs/voice 0.19.1 or newer: with an older library the bot can't join the channel, or joins and can't be heard. It's what breaks in almost every tutorial from previous years.
In the Developer Portal you don't need to turn on any special intent. When inviting the bot, the “Connect” and “Speak” permissions (plus the ones for slash commands) are enough. If you haven't built a bot yet, start with your first bot in Python or in JavaScript: the token and the invite work the same way.
The bot in Python (discord.py)
Three files and a music folder next to them with your songs. It has four commands: /play (with autocomplete: start typing and it suggests songs from the folder), /skip, /stop and /list.
discord.py[voice]>=2.7
python-dotenvDISCORD_TOKEN=paste-your-token-hereimport os
from pathlib import Path
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv() # reads the token from the .env file
FOLDER = Path("music").resolve() # your songs, uploaded in “Files”
FORMATS = {".mp3", ".ogg", ".opus", ".flac", ".wav", ".m4a"}
intents = discord.Intents.default() # already includes voice states
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
queues: dict[int, list[Path]] = {} # one queue per Discord server
def songs():
return sorted(p for p in FOLDER.iterdir() if p.suffix.lower() in FORMATS)
def play_next(vc: discord.VoiceClient, server: int):
"""Plays the first one in the queue; it calls itself again when each song ends."""
queue = queues.get(server)
if not queue or not vc.is_connected():
return
path = queue.pop(0)
audio = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(str(path)), volume=0.5)
vc.play(audio, after=lambda error: play_next(vc, server))
async def autocomplete(interaction: discord.Interaction, current: str):
return [
app_commands.Choice(name=p.stem[:100], value=p.name)
for p in songs()
if current.lower() in p.stem.lower()
][:25]
@tree.command(name="play", description="Plays a song from the music folder (or adds it to the queue)")
@app_commands.describe(song="Start typing and pick one")
@app_commands.autocomplete(song=autocomplete)
@app_commands.guild_only()
async def play(interaction: discord.Interaction, song: str):
path = (FOLDER / song).resolve()
if path.parent != FOLDER or not path.is_file(): # only what's in music/
await interaction.response.send_message("I can't find that song.", ephemeral=True)
return
voice = interaction.user.voice
if voice is None or voice.channel is None:
await interaction.response.send_message("Join a voice channel first.", ephemeral=True)
return
await interaction.response.defer() # connecting to voice can take more than 3 s
vc = interaction.guild.voice_client
if vc is None:
vc = await voice.channel.connect()
elif vc.channel != voice.channel:
await vc.move_to(voice.channel)
queues.setdefault(interaction.guild.id, []).append(path)
if vc.is_playing():
await interaction.followup.send(f"➕ Queued: **{path.stem}**")
else:
play_next(vc, interaction.guild.id)
await interaction.followup.send(f"▶️ Now playing: **{path.stem}**")
@tree.command(name="skip", description="Skips to the next song in the queue")
@app_commands.guild_only()
async def skip(interaction: discord.Interaction):
vc = interaction.guild.voice_client
if vc and vc.is_playing():
vc.stop() # stopping it makes play_next() play the next one
await interaction.response.send_message("⏭️ Next")
@tree.command(name="stop", description="Stops the music, clears the queue and leaves the channel")
@app_commands.guild_only()
async def stop(interaction: discord.Interaction):
queues.pop(interaction.guild.id, None)
vc = interaction.guild.voice_client
if vc:
await vc.disconnect()
await interaction.response.send_message("⏹️ Music stopped")
@tree.command(name="list", description="The songs in the music folder")
async def song_list(interaction: discord.Interaction):
names = [p.stem for p in songs()] or ["(the music folder is empty)"]
await interaction.response.send_message("\n".join(names)[:2000], ephemeral=True)
@client.event
async def on_ready():
await tree.sync() # publishes /play, /skip, /stop and /list
print(f"Logged in as {client.user}")
client.run(os.environ["DISCORD_TOKEN"])The same bot in JavaScript (discord.js)
The same four commands. Here you do have to request the voice states intent (GuildVoiceStates): without it, the bot doesn't know which channel you're in and won't join. It isn't a privileged intent, so there's nothing to turn on in the portal.
{
"name": "my-music-bot",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.19.2",
"discord.js": "^14.27.0",
"dotenv": "^16.4.0"
}
}DISCORD_TOKEN=paste-your-token-hererequire("dotenv").config(); // reads the token from the .env file
const fs = require("node:fs");
const path = require("node:path");
const { Client, Events, GatewayIntentBits, MessageFlags, SlashCommandBuilder, InteractionContextType } = require("discord.js");
const { AudioPlayerStatus, createAudioPlayer, createAudioResource, getVoiceConnection, joinVoiceChannel } = require("@discordjs/voice");
const FOLDER = path.resolve("music"); // your songs, uploaded in “Files”
const FORMATS = [".mp3", ".ogg", ".opus", ".flac", ".wav", ".m4a"];
// Without GuildVoiceStates the bot doesn't know which voice channel you're in and can't join it
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
const players = new Map(); // one player and one queue per Discord server
const songs = () => fs.readdirSync(FOLDER).filter((f) => FORMATS.includes(path.extname(f).toLowerCase())).sort();
function playerFor(server) {
if (!players.has(server)) {
const p = { player: createAudioPlayer(), queue: [] };
// When a song ends, the next one in the queue
p.player.on(AudioPlayerStatus.Idle, () => playNext(p));
p.player.on("error", (e) => console.error("Playback error:", e.message));
players.set(server, p);
}
return players.get(server);
}
function playNext(p) {
const file = p.queue.shift();
if (!file) return;
const audio = createAudioResource(file, { inlineVolume: true });
audio.volume.setVolume(0.5);
p.player.play(audio);
}
const commands = [
new SlashCommandBuilder()
.setName("play")
.setDescription("Plays a song from the music folder (or adds it to the queue)")
.setContexts(InteractionContextType.Guild)
.addStringOption((o) => o.setName("song").setDescription("Start typing and pick one").setRequired(true).setAutocomplete(true)),
new SlashCommandBuilder().setName("skip").setDescription("Skips to the next song in the queue").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("stop").setDescription("Stops the music, clears the queue and leaves the channel").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("list").setDescription("The songs in the music folder"),
];
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(commands.map((x) => x.toJSON())); // publishes /play, /skip, /stop and /list
console.log(`Logged in as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (i) => {
if (i.isAutocomplete()) {
const typed = i.options.getFocused().toLowerCase();
const options = songs()
.filter((f) => f.toLowerCase().includes(typed))
.slice(0, 25)
.map((f) => ({ name: path.parse(f).name.slice(0, 100), value: f }));
return i.respond(options);
}
if (!i.isChatInputCommand()) return;
if (i.commandName === "play") {
const file = path.resolve(FOLDER, i.options.getString("song"));
if (path.dirname(file) !== FOLDER || !fs.existsSync(file)) {
return i.reply({ content: "I can't find that song.", flags: MessageFlags.Ephemeral }); // only what's in music/
}
const channel = i.member.voice.channel;
if (!channel) return i.reply({ content: "Join a voice channel first.", flags: MessageFlags.Ephemeral });
// Joins your channel (or moves to it if it was already in another one)
const connection = joinVoiceChannel({ channelId: channel.id, guildId: i.guildId, adapterCreator: i.guild.voiceAdapterCreator });
const p = playerFor(i.guildId);
connection.subscribe(p.player);
p.queue.push(file);
const name = path.parse(file).name;
if (p.player.state.status === AudioPlayerStatus.Idle) {
playNext(p);
return i.reply(`▶️ Now playing: **${name}**`);
}
return i.reply(`➕ Queued: **${name}**`);
}
if (i.commandName === "skip") {
players.get(i.guildId)?.player.stop(); // stopping it goes to Idle and the next one plays
return i.reply("⏭️ Next");
}
if (i.commandName === "stop") {
const p = players.get(i.guildId);
if (p) {
p.queue = [];
p.player.stop();
}
getVoiceConnection(i.guildId)?.destroy();
return i.reply("⏹️ Music stopped");
}
if (i.commandName === "list") {
const names = songs().map((f) => path.parse(f).name);
return i.reply({ content: (names.join("\n") || "(the music folder is empty)").slice(0, 2000), flags: MessageFlags.Ephemeral });
}
});
client.login(process.env.DISCORD_TOKEN);How it works inside
Each Discord server has its own queue. /play joins your voice channel (or moves to it), adds the song and, if nothing was playing, plays it. When a song ends the next one starts on its own; /skip cuts the one playing, which is the same as ending it.
The bot only plays files from the music folder: it checks that what it's asked for is inside it, so nobody can ask it for “../.env” and make it read something else. Autocomplete shows at most 25 songs, Discord's limit; if you have more, type part of the name.
In Python, /play first answers “thinking…” (defer): joining a voice channel can take longer than the 3 seconds Discord gives to reply, and without it you'd see “The application did not respond”. Volume is at 50 % so it doesn't drown out voices; change it in volume (Python) or setVolume (JavaScript).
How much memory and disk it needs
We measured it with this code: the bot starts at about 45 MB in Python and about 120 MB in Node.js (the cache of the servers it's on adds a bit more), and each channel where music is playing adds an FFmpeg of about 50 MB. For your Discord server and a few friends', 512 MB is comfortable; 256 MB is tight in Python and leaves no headroom in Node.js. If it'll play on many servers at once, 1 GB (the RAM calculator works it out).
An MP3 song takes about 3–5 MB, and the libraries (node_modules, or the Python packages) about 50 MB. If you run out of disk, OGG at lower quality takes half the space and you won't notice on a voice call.
Online all the time, on NapNodes
Create the bot
Free or on a plan (pricing). The free one has 256 MB and is renewed every week with one click; music in Python fits, but if you have lots of songs or want it in Node.js, better a plan.
Pick the language
The bot starts out on Node.js. If it's the Python one, in “Settings” → “Language”, “Switch to Python” (your files aren't touched).
Upload the files and the music
In “Files”, upload the three files; with “Create Directory” make music and upload the songs there. If there are many, upload them in a .zip and use “Decompress”. Or set your GitHub repository in “Settings”, without the .env or the music if it isn't yours to publish.
Start it
“Start”. The libraries install themselves at startup and FFmpeg is already there; the console will show “Logged in as…”. Join a voice channel and type /play.
If there's no sound
/play doesn't appear when you type the slash: commands take a little while the first time; close and reopen Discord. And check you invited the bot with the commands permission (applications.commands).
It says “Join a voice channel first” while you're in one: in JavaScript, GuildVoiceStates is missing from the intents; in both, the bot has to be able to see that channel.
It doesn't join the channel: it's missing “Connect” on that channel, or the channel is full. It joins and can't be heard: it's missing “Speak”, or the library predates DAVE (the versions are above). The panel console shows the exact error.
It cuts out or stutters: it's almost always memory. Check the memory graph in “Performance”; if it hits the ceiling, move up a plan or play on fewer channels at once.
Frequently asked questions
Why are there no music bots on Discord any more?
Because they played music from YouTube, and its terms don't allow it: in 2021 YouTube made Groovy and Rythm, the two most used, shut down. Your own bot with your own music doesn't depend on that.
Can you make a music bot that plays YouTube or Spotify?
Technically yes, but their terms forbid downloading or rebroadcasting their content outside their apps, and that's what made the big bots shut down. This guide uses music files you have the right to use.
Why does my music bot join the channel but play nothing?
Most often in 2026: the library is old. Since March 2026 Discord requires DAVE encryption on voice; you need discord.py 2.7 or @discordjs/voice 0.19.1 or newer. Otherwise, check the “Speak” permission and that FFmpeg is installed.
How much RAM does a Discord music bot need?
Measured with this guide's bot: it starts at about 45 MB in Python or 120 MB in Node.js, plus about 50 MB for each channel where it plays. 512 MB is comfortable for a few servers.
Shall we set it up for you?
Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.
Keep reading
- Discord Role Bot: Buttons So Everyone Picks Their Own Roles (Python and JavaScript)
Make your own Discord self-role bot in Python or JavaScript: a message with buttons to add and remove roles, no database and no reaction roles needed.
- Discord Welcome Bot: Join Message and Auto Role (Python and JavaScript)
Build a Discord bot that greets new members and gives them a role automatically, in Python or JavaScript: the missing intent, role order and rules screening.
- How to Make an AI Discord Bot (ChatGPT, Gemini…) in Python
Make a Discord bot that answers with AI: an /ask command in Python that uses ChatGPT, Gemini or another AI, with its key kept safe and your credit protected.