Créer son propre bot musique discord.py à partir de zéro est l'une des façons les plus gratifiantes d'apprendre Python. Dans ce guide, nous combinerons la bibliothèque discord.py, l'outil yt-dlp qui résout les sources audio, et FFmpeg qui diffuse cet audio vers Discord, pour créer un bot qui joue des morceaux via des commandes slash et gère une file d'attente. L'objectif n'est pas seulement que ça « marche », mais de poser une base claire et extensible.
Ce dont vous avez besoin avant de lancer le bot
Préparons d'abord l'environnement. Il vous faut une version récente de Python (3.9 ou supérieure recommandée), un jeton d'application/bot Discord, et FFmpeg installé sur le système. FFmpeg est un programme à part ; on l'installe avec le gestionnaire de paquets du système d'exploitation, pas avec pip (apt install ffmpeg sous Linux, brew install ffmpeg sous macOS, ou en téléchargeant la build officielle et en l'ajoutant au PATH sous Windows).
Côté Discord, créez une application sur le Discord Developer Portal, récupérez le jeton dans l'onglet « Bot », et activez le Message Content Intent ainsi que les permissions pour rejoindre les salons vocaux. Ne codez jamais votre jeton en dur ; gardez-le dans un fichier .env.
python -m venv venv
source venv/bin/activate # Windows : venv\Scripts\activate
pip install -U discord.py yt-dlp python-dotenv
pip install -U "discord.py[voice]"
Installer l'extra discord.py[voice] ajoute aussi la dépendance PyNaCl nécessaire aux connexions vocales. Sans elle, le bot ne peut pas rejoindre un salon vocal.
Le squelette : bot et commandes slash
Les bots modernes utilisent des commandes slash au lieu de commandes textuelles comme !play. discord.py les expose via app_commands. Le squelette ci-dessous lit le jeton depuis l'environnement et synchronise les commandes.
import os
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv()
intents = discord.Intents.default()
intents.message_content = True
class MusicBot(discord.Client):
def __init__(self):
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync()
bot = MusicBot()
@bot.event
async def on_ready():
print(f"Connecté en tant que : {bot.user}")
bot.run(os.getenv("DISCORD_TOKEN"))
Résoudre la source audio avec yt-dlp
Pour transformer un lien en flux audio, on utilise yt-dlp. Plutôt que de télécharger tout le fichier, on récupère directement l'URL du flux ; c'est plus rapide et ça n'utilise pas le disque. La classe utilitaire ci-dessous produit une source FFmpegOpusAudio jouable à partir d'un terme de recherche ou d'une URL.
import asyncio
import yt_dlp
YTDL_OPTS = {
"format": "bestaudio/best",
"noplaylist": True,
"quiet": True,
"default_search": "ytsearch",
"source_address": "0.0.0.0",
}
FFMPEG_OPTS = {
"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5",
"options": "-vn",
}
ytdl = yt_dlp.YoutubeDL(YTDL_OPTS)
class Track:
def __init__(self, data):
self.title = data.get("title")
self.url = data.get("url")
@classmethod
async def from_query(cls, query):
loop = asyncio.get_event_loop()
data = await loop.run_in_executor(
None, lambda: ytdl.extract_info(query, download=False)
)
if "entries" in data:
data = data["entries"][0]
return cls(data)
Deux points sont importants ici : extract_info est un appel bloquant (synchrone), on l'exécute donc dans un thread distinct avec run_in_executor ; sinon la boucle d'événements du bot se figerait. De plus, les options -reconnect dans before_options permettent au flux de se reconnecter s'il coupe pendant les morceaux longs.
Gérer la file d'attente
Le cœur d'un bon bot musique, c'est la file d'attente. Il faut une file séparée par serveur (guild), car il est courant que le bot joue dans plusieurs serveurs en même temps. Pour une structure simple, on peut garder les files dans un dictionnaire indexé par l'identifiant du serveur.
from collections import deque
queues = {} # guild_id -> deque[Track]
def get_queue(guild_id):
if guild_id not in queues:
queues[guild_id] = deque()
return queues[guild_id]
def play_next(voice_client, guild_id):
queue = get_queue(guild_id)
if not queue:
return
track = queue.popleft()
source = discord.FFmpegOpusAudio(track.url, **FFMPEG_OPTS)
voice_client.play(
source,
after=lambda e: play_next(voice_client, guild_id),
)
Le rappel after de la méthode play lance automatiquement le morceau suivant à la fin d'un titre. Attention : ce rappel s'exécute dans un thread distinct, vous ne pouvez donc pas utiliser await directement à l'intérieur. Si vous devez faire un travail asynchrone, renvoyez-le vers la boucle d'événements avec bot.loop.call_soon_threadsafe ou asyncio.run_coroutine_threadsafe.
Brancher les commandes : rejoindre, jouer, passer
Nous pouvons maintenant lier les morceaux aux commandes slash. La commande /play rejoint le salon vocal où se trouve l'utilisateur, résout la source, l'ajoute à la file, et démarre la lecture si rien n'est en cours.
@bot.tree.command(name="play", description="Jouer une chanson")
async def play(interaction: discord.Interaction, requete: str):
await interaction.response.defer()
if not interaction.user.voice:
await interaction.followup.send("Rejoignez d'abord un salon vocal.")
return
channel = interaction.user.voice.channel
vc = interaction.guild.voice_client
if vc is None:
vc = await channel.connect()
track = await Track.from_query(requete)
get_queue(interaction.guild.id).append(track)
await interaction.followup.send(f"Ajouté à la file : {track.title}")
if not vc.is_playing():
play_next(vc, interaction.guild.id)
@bot.tree.command(name="skip", description="Passer à la chanson suivante")
async def skip(interaction: discord.Interaction):
vc = interaction.guild.voice_client
if vc and vc.is_playing():
vc.stop() # le rappel 'after' lance le morceau suivant
await interaction.response.send_message("Passé.")
else:
await interaction.response.send_message("Rien n'est en cours.")
L'appel interaction.response.defer() est crucial : la résolution avec yt-dlp peut prendre quelques secondes, et Discord met en délai d'expiration les interactions sans réponse au bout de 3 secondes. Avec defer, on affiche un état « réflexion » puis on envoie la vraie réponse via followup.
À vérifier avant la mise en production
Quand le bot fonctionne, le travail n'est pas terminé. Quelques points pratiques :
- Gestion des erreurs : si une source est introuvable ou soumise à une restriction d'âge,
yt-dlplève une exception ; entourez vos commandes d'untry/except. - Vérification du salon vide : déconnectez le bot automatiquement quand il ne reste personne pour ne pas gaspiller de ressources.
- Droits d'auteur et conditions : respectez les conditions d'utilisation de vos sources de contenu ; ne jouez que ce que vous êtes autorisé à jouer.
- Hébergement : pour tourner 24h/24, un petit VPS ou un conteneur est l'approche la plus fiable.
Questions fréquentes
Pourquoi le bot se connecte mais aucun son ne sort ?
La cause la plus fréquente est que FFmpeg n'est pas installé ou n'est pas dans le PATH. Lancez ffmpeg -version dans le terminal pour vérifier. De plus, si PyNaCl n'est pas installé, le flux audio ne fonctionnera pas ; corrigez avec pip install "discord.py[voice]".
Mes commandes slash n'apparaissent pas, que faire ?
La synchronisation globale des commandes peut prendre jusqu'à une heure pour se propager sur Discord. Pendant le développement, synchroniser les commandes sur un seul serveur (guild) les rend visibles instantanément ; utilisez tree.sync(guild=discord.Object(id=...)) pour cela.
discord.py ou une autre bibliothèque ?
Côté Python, discord.py est l'option la plus mature et la mieux documentée ; elle prend pleinement en charge les commandes slash, la voix et les fonctionnalités modernes de l'API. Si vous préférez JavaScript, discord.js propose une voie similaire, mais tous les exemples de ce guide sont en Python.
Vous voulez faire passer votre bot au niveau supérieur ? Nous pouvons travailler ensemble pour ajouter des fonctionnalités comme des filtres, des playlists, un panneau web ou la prise en charge multi-serveurs. Contactez-moi pour parler de votre projet.