aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord-bots

discord.py muziekbot: stap voor stap met yt-dlp en FFmpeg

Je eigen discord.py muziekbot vanaf nul bouwen is een van de leukste manieren om Python te leren. In deze gids combineren we de discord.py-bibliotheek, de tool yt-dlp die audiobronnen ontsluit, en FFmpeg dat die audio naar Discord streamt, om een bot te maken die nummers afspeelt via slash-commando's en een wachtrij beheert. Het doel is niet alleen iets dat "werkt", maar een heldere, uitbreidbare basis.

Wat je nodig hebt voordat je de bot start

Laten we eerst de omgeving voorbereiden. Je hebt een moderne Python-versie nodig (3.9 of hoger aanbevolen), een Discord-applicatie/bot-token en FFmpeg geïnstalleerd op het systeem. FFmpeg is een apart programma; je installeert het met de pakketbeheerder van je besturingssysteem in plaats van met pip (apt install ffmpeg op Linux, brew install ffmpeg op macOS, of de officiële build downloaden en aan PATH toevoegen op Windows).

Aan de Discord-kant maak je een applicatie aan in het Discord Developer Portal, haal je het token op uit het tabblad "Bot", en schakel je de Message Content Intent in samen met de rechten om spraakkanalen te betreden. Hardcode je token nooit; bewaar het in een .env-bestand.

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]"

Het installeren van de extra discord.py[voice] haalt ook de PyNaCl-afhankelijkheid binnen die nodig is voor spraakverbindingen. Zonder die kan de bot geen spraakkanaal betreden.

Het skelet: bot en slash-commando's

Moderne bots gebruiken slash-commando's in plaats van tekstcommando's zoals !play. discord.py stelt ze beschikbaar via app_commands. Het onderstaande skelet leest het token uit de omgeving en synchroniseert de commando's.

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"Ingelogd als: {bot.user}")

bot.run(os.getenv("DISCORD_TOKEN"))

De audiobron ontsluiten met yt-dlp

Om een link in een audiostream om te zetten, gebruiken we yt-dlp. In plaats van het hele bestand te downloaden, halen we direct de stream-URL op; dat is sneller en gebruikt geen schijfruimte. De onderstaande hulpklasse produceert een afspeelbare FFmpegOpusAudio-bron uit een zoekterm of 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)

Twee dingen zijn hier belangrijk: extract_info is een blokkerende (synchrone) aanroep, dus we voeren die uit in een aparte thread met run_in_executor; anders zou de event-loop van de bot vastlopen. Daarnaast zorgen de -reconnect-vlaggen in before_options ervoor dat de stream opnieuw verbindt als hij wegvalt tijdens lange nummers.

De wachtrij beheren

Het hart van een goede muziekbot is de wachtrij. Je hebt een aparte wachtrij per server (guild) nodig, want het is gebruikelijk dat de bot in meerdere servers tegelijk afspeelt. Voor een eenvoudige structuur kunnen we wachtrijen bewaren in een dictionary met de server-id als sleutel.

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),
    )

De after-callback van de play-methode start automatisch het volgende nummer wanneer er een eindigt. Let op: deze callback draait in een aparte thread, dus je kunt er niet direct await in gebruiken. Moet je async werk doen, geef het dan terug aan de event-loop met bot.loop.call_soon_threadsafe of asyncio.run_coroutine_threadsafe.

De commando's koppelen: meedoen, afspelen, overslaan

Nu kunnen we nummers koppelen aan slash-commando's. Het /play-commando voegt zich bij het spraakkanaal waar de gebruiker in zit, ontsluit de bron, voegt die toe aan de wachtrij en start de weergave als er niets speelt.

@bot.tree.command(name="play", description="Speel een nummer af")
async def play(interaction: discord.Interaction, zoekterm: str):
    await interaction.response.defer()
    if not interaction.user.voice:
        await interaction.followup.send("Ga eerst een spraakkanaal binnen.")
        return

    channel = interaction.user.voice.channel
    vc = interaction.guild.voice_client
    if vc is None:
        vc = await channel.connect()

    track = await Track.from_query(zoekterm)
    get_queue(interaction.guild.id).append(track)
    await interaction.followup.send(f"Toegevoegd aan wachtrij: {track.title}")

    if not vc.is_playing():
        play_next(vc, interaction.guild.id)

@bot.tree.command(name="skip", description="Ga naar het volgende nummer")
async def skip(interaction: discord.Interaction):
    vc = interaction.guild.voice_client
    if vc and vc.is_playing():
        vc.stop()  # de 'after'-callback start het volgende nummer
        await interaction.response.send_message("Overgeslagen.")
    else:
        await interaction.response.send_message("Er speelt niets.")

De aanroep interaction.response.defer() is cruciaal: het ontsluiten met yt-dlp kan enkele seconden duren, en Discord laat interacties die binnen 3 seconden niet worden beantwoord verlopen. Met defer tonen we een "aan het nadenken"-status en sturen we daarna het echte antwoord via followup.

Waar je op moet letten voordat je live gaat

Als de bot werkt, is het werk nog niet klaar. Een paar praktische punten:

  • Foutafhandeling: als een bron niet gevonden wordt of leeftijdsbeperkt is, gooit yt-dlp een uitzondering; omwikkel je commando's met try/except.
  • Controle op leeg kanaal: koppel de bot automatisch los als er niemand meer is, zodat hij geen bronnen verspilt.
  • Auteursrecht en voorwaarden: respecteer de gebruiksvoorwaarden van je contentbronnen; speel alleen af wat is toegestaan.
  • Hosting: om 24/7 te draaien is een kleine VPS of een container de meest betrouwbare aanpak.

Veelgestelde vragen

Waarom verbindt de bot wel, maar komt er geen geluid uit?

De meest voorkomende oorzaak is dat FFmpeg niet geïnstalleerd is of niet in de PATH staat. Voer ffmpeg -version uit in de terminal om dit te controleren. Daarnaast werkt de audiostream niet als PyNaCl niet is geïnstalleerd; los het op met pip install "discord.py[voice]".

Mijn slash-commando's verschijnen niet, wat moet ik doen?

Globale commando-synchronisatie kan tot een uur duren om zich over Discord te verspreiden. Tijdens het ontwikkelen maakt het synchroniseren naar één server (guild) ze direct zichtbaar; gebruik daarvoor tree.sync(guild=discord.Object(id=...)).

discord.py of een andere bibliotheek?

Aan de Python-kant is discord.py de meest volwassen en best gedocumenteerde optie; het ondersteunt slash-commando's, spraak en moderne API-functies volledig. Geef je de voorkeur aan JavaScript, dan biedt discord.js een vergelijkbaar pad, maar elk voorbeeld in deze gids is in Python.

Wil je je bot naar een hoger niveau tillen? We kunnen samen functies toevoegen zoals filters, afspeellijsten, een webpaneel of multi-serverondersteuning. Neem contact op om over je project te praten.

Bu kategorideki tüm yazılar →

Devamı için