Einen eigenen discord.py Musikbot von Grund auf zu bauen, ist eine der lohnendsten Arten, Python zu lernen. In dieser Anleitung kombinieren wir die Bibliothek discord.py, das Werkzeug yt-dlp, das Audioquellen auflöst, und FFmpeg, das dieses Audio nach Discord streamt, um einen Bot zu erstellen, der Titel über Slash-Befehle abspielt und eine Warteschlange verwaltet. Das Ziel ist nicht nur etwas, das „funktioniert", sondern eine klare, erweiterbare Grundlage.
Was du brauchst, bevor du den Bot startest
Bereiten wir zuerst die Umgebung vor. Du brauchst eine aktuelle Python-Version (3.9 oder höher empfohlen), einen Discord-Anwendungs-/Bot-Token und FFmpeg auf dem System installiert. FFmpeg ist ein eigenständiges Programm; du installierst es mit dem Paketmanager deines Betriebssystems statt mit pip (apt install ffmpeg unter Linux, brew install ffmpeg unter macOS oder den offiziellen Build herunterladen und zum PATH hinzufügen unter Windows).
Auf der Discord-Seite erstellst du eine Anwendung im Discord Developer Portal, holst den Token aus dem Reiter „Bot" und aktivierst den Message Content Intent sowie die Berechtigungen zum Beitreten von Sprachkanälen. Codiere deinen Token niemals fest; bewahre ihn in einer .env-Datei auf.
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]"
Die Installation des Extras discord.py[voice] bringt auch die Abhängigkeit PyNaCl mit, die für Sprachverbindungen nötig ist. Ohne sie kann der Bot keinem Sprachkanal beitreten.
Das Grundgerüst: Bot und Slash-Befehle
Moderne Bots verwenden Slash-Befehle statt Textbefehlen wie !play. discord.py stellt sie über app_commands bereit. Das folgende Grundgerüst liest den Token aus der Umgebung und synchronisiert die Befehle.
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"Angemeldet als: {bot.user}")
bot.run(os.getenv("DISCORD_TOKEN"))
Die Audioquelle mit yt-dlp auflösen
Um einen Link in einen Audiostream zu verwandeln, nutzen wir yt-dlp. Statt die ganze Datei herunterzuladen, holen wir direkt die Stream-URL; das ist schneller und belegt keinen Speicherplatz. Die folgende Hilfsklasse erzeugt aus einem Suchbegriff oder einer URL eine abspielbare FFmpegOpusAudio-Quelle.
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)
Zwei Dinge sind hier wichtig: extract_info ist ein blockierender (synchroner) Aufruf, deshalb führen wir ihn mit run_in_executor in einem eigenen Thread aus; andernfalls würde die Event-Loop des Bots einfrieren. Außerdem sorgen die -reconnect-Flags in before_options dafür, dass der Stream sich neu verbindet, falls er bei langen Titeln abbricht.
Die Warteschlange verwalten
Das Herzstück eines guten Musikbots ist die Warteschlange. Du brauchst eine eigene Warteschlange pro Server (Guild), denn es ist üblich, dass der Bot in mehreren Servern gleichzeitig abspielt. Für eine einfache Struktur können wir die Warteschlangen in einem Dictionary mit der Server-ID als Schlüssel halten.
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),
)
Der after-Callback der play-Methode startet automatisch den nächsten Titel, sobald einer endet. Vorsicht: Dieser Callback läuft in einem eigenen Thread, du kannst darin also nicht direkt await verwenden. Wenn du asynchrone Arbeit erledigen musst, übergib sie mit bot.loop.call_soon_threadsafe oder asyncio.run_coroutine_threadsafe zurück an die Event-Loop.
Die Befehle verdrahten: beitreten, abspielen, überspringen
Jetzt können wir Titel an Slash-Befehle binden. Der Befehl /play tritt dem Sprachkanal bei, in dem der Nutzer ist, löst die Quelle auf, fügt sie der Warteschlange hinzu und startet die Wiedergabe, wenn nichts läuft.
@bot.tree.command(name="play", description="Einen Song abspielen")
async def play(interaction: discord.Interaction, suche: str):
await interaction.response.defer()
if not interaction.user.voice:
await interaction.followup.send("Tritt zuerst einem Sprachkanal bei.")
return
channel = interaction.user.voice.channel
vc = interaction.guild.voice_client
if vc is None:
vc = await channel.connect()
track = await Track.from_query(suche)
get_queue(interaction.guild.id).append(track)
await interaction.followup.send(f"Zur Warteschlange hinzugefügt: {track.title}")
if not vc.is_playing():
play_next(vc, interaction.guild.id)
@bot.tree.command(name="skip", description="Zum nächsten Song springen")
async def skip(interaction: discord.Interaction):
vc = interaction.guild.voice_client
if vc and vc.is_playing():
vc.stop() # der 'after'-Callback startet den nächsten Titel
await interaction.response.send_message("Übersprungen.")
else:
await interaction.response.send_message("Es läuft nichts.")
Der Aufruf interaction.response.defer() ist entscheidend: Die Auflösung mit yt-dlp kann einige Sekunden dauern, und Discord lässt Interaktionen, die nicht innerhalb von 3 Sekunden beantwortet werden, verfallen. Mit defer zeigen wir einen „Denkt nach"-Status und senden danach über followup die echte Antwort.
Worauf du vor dem Livegang achten solltest
Wenn der Bot läuft, ist die Arbeit noch nicht erledigt. Ein paar praktische Punkte:
- Fehlerbehandlung: Wird eine Quelle nicht gefunden oder ist sie altersbeschränkt, wirft
yt-dlpeine Ausnahme; umschließe deine Befehle mittry/except. - Prüfung auf leeren Kanal: Trenne den Bot automatisch, wenn niemand mehr da ist, damit er keine Ressourcen verschwendet.
- Urheberrecht und Bedingungen: Respektiere die Nutzungsbedingungen deiner Inhaltsquellen; spiele nur ab, was erlaubt ist.
- Hosting: Für den 24/7-Betrieb ist ein kleiner VPS oder ein Container der zuverlässigste Ansatz.
Häufige Fragen
Warum verbindet sich der Bot, aber es kommt kein Ton?
Die häufigste Ursache ist, dass FFmpeg nicht installiert ist oder nicht im PATH liegt. Führe ffmpeg -version im Terminal aus, um das zu prüfen. Außerdem funktioniert der Audiostream nicht, wenn PyNaCl nicht installiert ist; behebe das mit pip install "discord.py[voice]".
Meine Slash-Befehle erscheinen nicht, was soll ich tun?
Die globale Befehlssynchronisation kann bis zu einer Stunde dauern, bis sie sich über Discord verbreitet. Während der Entwicklung macht das Synchronisieren auf einen einzigen Server (Guild) sie sofort sichtbar; nutze dafür tree.sync(guild=discord.Object(id=...)).
discord.py oder eine andere Bibliothek?
Auf der Python-Seite ist discord.py die ausgereifteste und am besten dokumentierte Option; sie unterstützt Slash-Befehle, Sprache und moderne API-Funktionen vollständig. Wenn du JavaScript bevorzugst, bietet discord.js einen ähnlichen Weg, aber jedes Beispiel in dieser Anleitung ist in Python.
Möchtest du deinen Bot auf die nächste Stufe heben? Wir können gemeinsam Funktionen wie Filter, Playlists, ein Web-Panel oder Multi-Server-Unterstützung hinzufügen. Nimm Kontakt auf, um über dein Projekt zu sprechen.