Bot de música para Discord: hazte el tuyo en Python o JavaScript
Los bots de música famosos fueron cayendo y los que quedan cambian de condiciones cada poco. Un bot propio que pone tu música en el canal de voz se hace en una tarde: aquí va completo, con cola de canciones, y lo que hay que tener al día para que Discord le deje entrar en la voz.
Por qué desaparecieron los bots de música
En 2021, YouTube mandó cartas a los dos bots de música más usados de Discord porque ponían canciones sacadas de sus vídeos: Groovy cerró el 30 de agosto y Rythm el 15 de septiembre. Las condiciones de YouTube y de Spotify no dejan descargar ni retransmitir su contenido fuera de sus aplicaciones, así que cualquier bot que lo haga vive con fecha de caducidad.
Por eso este bot pone música que tú le das: archivos subidos a una carpeta. Que sea música tuya o que tengas permiso para usar (tus grabaciones, música libre de derechos o con licencia Creative Commons, respetando lo que pida su licencia). Lo que se pone en un servidor es responsabilidad de quien lo lleva (Términos).
¿Prefieres no montarlo tú? Hosting de bots de Discord desde 1,50 € al mes, sin permanencia y con devolución íntegra en 72 horas. O crea un bot de Discord gratis, sin tarjeta.
Lo que necesita la voz (y por qué los tutoriales viejos no funcionan)
FFmpeg, el programa que lee el MP3 (o el OGG, FLAC…) y lo convierte en audio para Discord. En tu ordenador hay que instalarlo; en NapNodes ya viene puesto.
Las librerías de voz: en Python, discord.py con su extra de voz (discord.py[voice], que trae PyNaCl y davey); en JavaScript, @discordjs/voice y @discordjs/opus. Si en la consola sale «PyNaCl library needed in order to use voice», es que falta ese extra.
Y versiones al día. Desde marzo de 2026 Discord exige cifrado de extremo a extremo (el protocolo DAVE) en las llamadas de voz y vídeo, y el bot no es una excepción. Hace falta discord.py 2.7 o más nuevo, o @discordjs/voice 0.19.1 o más nuevo: con una librería anterior el bot no consigue entrar en el canal o entra y no se le oye. Es lo que falla en casi todos los tutoriales de años anteriores.
En el portal de desarrolladores no hace falta activar ningún intent especial. Al invitar al bot, con los permisos «Connect» y «Speak» (además de los de los comandos con barra) basta. Si aún no has hecho ningún bot, empieza por tu primer bot en Python o en JavaScript: el token y la invitación se sacan igual.
El bot en Python (discord.py)
Tres ficheros y una carpeta musica al lado con tus canciones. Tiene cuatro comandos: /pon (con autocompletado: empiezas a escribir y te sugiere las canciones de la carpeta), /salta, /para y /lista.
discord.py[voice]>=2.7
python-dotenvDISCORD_TOKEN=pega-aquí-tu-tokenimport os
from pathlib import Path
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv() # lee el token del fichero .env
CARPETA = Path("musica").resolve() # tus canciones, subidas en «Archivos»
FORMATOS = {".mp3", ".ogg", ".opus", ".flac", ".wav", ".m4a"}
intents = discord.Intents.default() # ya incluye los estados de voz
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
colas: dict[int, list[Path]] = {} # una cola por servidor de Discord
def canciones():
return sorted(p for p in CARPETA.iterdir() if p.suffix.lower() in FORMATOS)
def siguiente(vc: discord.VoiceClient, servidor: int):
"""Pone la primera de la cola; se vuelve a llamar sola al acabar cada canción."""
cola = colas.get(servidor)
if not cola or not vc.is_connected():
return
ruta = cola.pop(0)
audio = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(str(ruta)), volume=0.5)
vc.play(audio, after=lambda error: siguiente(vc, servidor))
async def autocompletar(interaction: discord.Interaction, actual: str):
return [
app_commands.Choice(name=p.stem[:100], value=p.name)
for p in canciones()
if actual.lower() in p.stem.lower()
][:25]
@tree.command(name="pon", description="Pone una canción de la carpeta musica (o la añade a la cola)")
@app_commands.describe(cancion="Empieza a escribir y elige")
@app_commands.autocomplete(cancion=autocompletar)
@app_commands.guild_only()
async def pon(interaction: discord.Interaction, cancion: str):
ruta = (CARPETA / cancion).resolve()
if ruta.parent != CARPETA or not ruta.is_file(): # solo lo que hay en musica/
await interaction.response.send_message("No encuentro esa canción.", ephemeral=True)
return
voz = interaction.user.voice
if voz is None or voz.channel is None:
await interaction.response.send_message("Entra antes en un canal de voz.", ephemeral=True)
return
await interaction.response.defer() # conectar a la voz puede tardar más de 3 s
vc = interaction.guild.voice_client
if vc is None:
vc = await voz.channel.connect()
elif vc.channel != voz.channel:
await vc.move_to(voz.channel)
colas.setdefault(interaction.guild.id, []).append(ruta)
if vc.is_playing():
await interaction.followup.send(f"➕ En la cola: **{ruta.stem}**")
else:
siguiente(vc, interaction.guild.id)
await interaction.followup.send(f"▶️ Suena: **{ruta.stem}**")
@tree.command(name="salta", description="Pasa a la siguiente canción de la cola")
@app_commands.guild_only()
async def salta(interaction: discord.Interaction):
vc = interaction.guild.voice_client
if vc and vc.is_playing():
vc.stop() # al pararla, siguiente() pone la que toca
await interaction.response.send_message("⏭️ Siguiente")
@tree.command(name="para", description="Para la música, vacía la cola y sale del canal")
@app_commands.guild_only()
async def para(interaction: discord.Interaction):
colas.pop(interaction.guild.id, None)
vc = interaction.guild.voice_client
if vc:
await vc.disconnect()
await interaction.response.send_message("⏹️ Música parada")
@tree.command(name="lista", description="Las canciones que hay en la carpeta musica")
async def lista(interaction: discord.Interaction):
nombres = [p.stem for p in canciones()] or ["(la carpeta musica está vacía)"]
await interaction.response.send_message("\n".join(nombres)[:2000], ephemeral=True)
@client.event
async def on_ready():
await tree.sync() # publica /pon, /salta, /para y /lista
print(f"Conectado como {client.user}")
client.run(os.environ["DISCORD_TOKEN"])El mismo bot en JavaScript (discord.js)
Los mismos cuatro comandos. Aquí sí hay que pedir el intent de estados de voz (GuildVoiceStates): sin él, el bot no sabe en qué canal estás y no entra. No es un intent privilegiado, así que no hay que activar nada en el portal.
{
"name": "mi-bot-de-musica",
"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=pega-aquí-tu-tokenrequire("dotenv").config(); // lee el token del fichero .env
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 CARPETA = path.resolve("musica"); // tus canciones, subidas en «Archivos»
const FORMATOS = [".mp3", ".ogg", ".opus", ".flac", ".wav", ".m4a"];
// Sin GuildVoiceStates el bot no sabe en qué canal de voz estás ni puede entrar
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
const reproductores = new Map(); // un reproductor y una cola por servidor de Discord
const canciones = () => fs.readdirSync(CARPETA).filter((f) => FORMATOS.includes(path.extname(f).toLowerCase())).sort();
function reproductorDe(servidor) {
if (!reproductores.has(servidor)) {
const r = { player: createAudioPlayer(), cola: [] };
// Al acabar una canción, la siguiente de la cola
r.player.on(AudioPlayerStatus.Idle, () => siguiente(r));
r.player.on("error", (e) => console.error("Error al reproducir:", e.message));
reproductores.set(servidor, r);
}
return reproductores.get(servidor);
}
function siguiente(r) {
const ruta = r.cola.shift();
if (!ruta) return;
const audio = createAudioResource(ruta, { inlineVolume: true });
audio.volume.setVolume(0.5);
r.player.play(audio);
}
const comandos = [
new SlashCommandBuilder()
.setName("pon")
.setDescription("Pone una canción de la carpeta musica (o la añade a la cola)")
.setContexts(InteractionContextType.Guild)
.addStringOption((o) => o.setName("cancion").setDescription("Empieza a escribir y elige").setRequired(true).setAutocomplete(true)),
new SlashCommandBuilder().setName("salta").setDescription("Pasa a la siguiente canción de la cola").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("para").setDescription("Para la música, vacía la cola y sale del canal").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("lista").setDescription("Las canciones que hay en la carpeta musica"),
];
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(comandos.map((x) => x.toJSON())); // publica /pon, /salta, /para y /lista
console.log(`Conectado como ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (i) => {
if (i.isAutocomplete()) {
const escrito = i.options.getFocused().toLowerCase();
const opciones = canciones()
.filter((f) => f.toLowerCase().includes(escrito))
.slice(0, 25)
.map((f) => ({ name: path.parse(f).name.slice(0, 100), value: f }));
return i.respond(opciones);
}
if (!i.isChatInputCommand()) return;
if (i.commandName === "pon") {
const ruta = path.resolve(CARPETA, i.options.getString("cancion"));
if (path.dirname(ruta) !== CARPETA || !fs.existsSync(ruta)) {
return i.reply({ content: "No encuentro esa canción.", flags: MessageFlags.Ephemeral }); // solo lo que hay en musica/
}
const canal = i.member.voice.channel;
if (!canal) return i.reply({ content: "Entra antes en un canal de voz.", flags: MessageFlags.Ephemeral });
// Entra en tu canal (o se cambia a él si ya estaba en otro)
const conexion = joinVoiceChannel({ channelId: canal.id, guildId: i.guildId, adapterCreator: i.guild.voiceAdapterCreator });
const r = reproductorDe(i.guildId);
conexion.subscribe(r.player);
r.cola.push(ruta);
const nombre = path.parse(ruta).name;
if (r.player.state.status === AudioPlayerStatus.Idle) {
siguiente(r);
return i.reply(`▶️ Suena: **${nombre}**`);
}
return i.reply(`➕ En la cola: **${nombre}**`);
}
if (i.commandName === "salta") {
reproductores.get(i.guildId)?.player.stop(); // al pararla, pasa a Idle y suena la siguiente
return i.reply("⏭️ Siguiente");
}
if (i.commandName === "para") {
const r = reproductores.get(i.guildId);
if (r) {
r.cola = [];
r.player.stop();
}
getVoiceConnection(i.guildId)?.destroy();
return i.reply("⏹️ Música parada");
}
if (i.commandName === "lista") {
const nombres = canciones().map((f) => path.parse(f).name);
return i.reply({ content: (nombres.join("\n") || "(la carpeta musica está vacía)").slice(0, 2000), flags: MessageFlags.Ephemeral });
}
});
client.login(process.env.DISCORD_TOKEN);Cómo funciona por dentro
Cada servidor de Discord tiene su cola. /pon entra en tu canal de voz (o se cambia a él), añade la canción y, si no sonaba nada, la pone. Al acabar una canción salta sola la siguiente; /salta corta la que suena, que es lo mismo que acabarla.
El bot solo pone archivos de la carpeta musica: comprueba que lo que le piden está dentro, para que nadie pueda pedirle «../.env» y hacerle leer otra cosa. El autocompletado enseña como mucho 25 canciones, el límite de Discord; si tienes más, escribe parte del nombre.
En Python, /pon responde primero «pensando…» (defer): entrar en un canal de voz puede tardar más de los 3 segundos que Discord da para contestar, y sin eso saldría «La aplicación no ha respondido». El volumen va al 50 % para que no tape las voces; cámbialo en volume (Python) o setVolume (JavaScript).
Cuánta memoria y disco necesita
Lo hemos medido con este código: el bot en Python parte de unos 45 MB y en Node.js de unos 120 MB (la caché de los servidores donde esté suma algo más), y cada canal en el que suena música añade un FFmpeg de unos 50 MB. Para tu servidor de Discord y el de unos amigos, 512 MB van holgados; con 256 MB va justo en Python y no da margen en Node.js. Si va a sonar en muchos servidores a la vez, 1 GB (la calculadora de RAM lo calcula).
Una canción en MP3 ocupa unos 3–5 MB, y las librerías (node_modules, o los paquetes de Python) unos 50 MB. Si te quedas sin disco, el OGG a menor calidad ocupa la mitad y en una llamada de voz no se nota.
Encendido siempre, en NapNodes
Crea el bot
Gratis o en un plan (precios). El gratis tiene 256 MB y se renueva cada semana con un clic; con música en Python cabe, pero si tienes muchas canciones o lo quieres en Node.js, mejor un plan.
Elige el lenguaje
El bot nace en Node.js. Si es el de Python, en «Configuración» → «Lenguaje», «Pasar a Python» (sin tocar tus archivos).
Sube los ficheros y la música
En «Archivos», sube los tres ficheros; con «Nueva carpeta» crea musica y sube ahí las canciones. Si son muchas, súbelas en un .zip y usa «Descomprimir». O pon tu repositorio de GitHub en «Configuración», sin el .env ni la música si no son tuyos para publicarlos.
Enciende
«Encender». Las librerías se instalan solas al arrancar y FFmpeg ya está; en la consola verás «Conectado como…». Entra en un canal de voz y escribe /pon.
Si no suena
No sale /pon al escribir la barra: los comandos tardan un poco la primera vez; cierra y abre Discord. Y comprueba que invitaste al bot con el permiso de comandos (applications.commands).
Dice «Entra antes en un canal de voz» estando dentro: en JavaScript, falta GuildVoiceStates en los intents; en los dos, el bot tiene que ver ese canal.
No entra en el canal: le faltan «Connect» en ese canal, o el canal está lleno. Entra y no se le oye: le falta «Speak», o la librería es anterior a DAVE (arriba tienes las versiones). La consola del panel enseña el error exacto.
Se corta o va a saltos: casi siempre es memoria. Mira la gráfica de memoria de «Rendimiento»; si toca el techo, sube de plan o pon menos canales a la vez.
Preguntas frecuentes
¿Por qué ya no hay bots de música en Discord?
Porque ponían música de YouTube, y sus condiciones no lo permiten: en 2021 YouTube hizo cerrar a Groovy y a Rythm, los dos más usados. Un bot propio con tu música no depende de eso.
¿Se puede hacer un bot de música que ponga YouTube o Spotify?
Técnicamente sí, pero sus condiciones prohíben descargar o retransmitir su contenido fuera de sus aplicaciones, y es lo que hizo cerrar a los bots grandes. Esta guía usa archivos de música que tengas derecho a usar.
¿Por qué mi bot de música entra en el canal pero no suena?
Lo más habitual en 2026: la librería es vieja. Desde marzo de 2026 Discord exige el cifrado DAVE en la voz; hace falta discord.py 2.7 o @discordjs/voice 0.19.1 o más nuevos. Si no, revisa el permiso «Speak» y que FFmpeg esté instalado.
¿Cuánta RAM necesita un bot de música de Discord?
Medido con el bot de esta guía: parte de unos 45 MB en Python o 120 MB en Node.js, más unos 50 MB por cada canal donde suene. 512 MB van holgados para unos pocos servidores.
¿Te ha servido esta guía?
¿Lo montamos?
Hosting de bots de Discord desde 1,50 € al mes, sin sobreventa: tu RAM es solo tuya. Y si quieres empezar sin pagar, tienes un bot de Discord gratis, sin tarjeta.
Sigue leyendo
- Bot de bienvenida para Discord: mensaje al entrar y rol automático (Python y JavaScript)
Haz un bot de Discord que salude a quien entra y le dé un rol solo, en Python o JavaScript: el permiso que siempre falta, la jerarquía de roles y las normas.
- Bot de niveles para Discord: XP por mensajes, ranking y roles (Python y JavaScript)
Haz tu propio bot de niveles de Discord en Python o JavaScript: XP por mensajes contra el spam, /nivel, /ranking y roles al subir, guardado en SQLite.
- Cómo hacer un bot de Discord con IA (ChatGPT, Gemini…) en Python
Haz un bot de Discord que contesta con IA: un comando /pregunta en Python que usa ChatGPT, Gemini u otra IA, con su clave a salvo y sin que te gasten el saldo.