Discord Music Bot in Python
March 14, 2023
I miss the days of Groovy. Sometimes it’s fun to just sit in Discord and listen to music with your friends. I thought to myself, why not just build a new music bot for me and my friends to use? It turns out that it’s pretty easy.
First, I did a bit of research into how to make a Discord bot. I’m not going into a lot of details on how that works, but it led me to the point where I created a bot token and installed the proper package to create a bot. I didn’t go straight into setting up the music part of it because I wasn’t ready for it yet. Instead, I simply messed around with posting messages, responding to commands, and figuring out how I should structure my project’s folders.
After that, I was ready to start figuring out how to stream music. An old method people suggested was using a package called yt-dlp. However, I found that it no longer works with Discord…or YouTube…or both. I quickly gave up on that method. It didn’t seem like a scalable way to do things.
Eventually I stumbled accross wavelink. The wavelink package, in conjunction with a running lavalink container would allow me to get music from many different sources and stream it over Discord.
As I do with all of my projects, I set up a docker-compose config to run my code.
version: '3'
services:
rutabega:
build:
dockerfile: ./compose/local/rutabega/Dockerfile
context: .
volumes:
- ./src:/app
env_file:
- ./.envs/local/rutabega/.env
command: python -u main.py
depends_on:
- lavalink
lavalink:
image: fredboat/lavalink
ports:
- 2223:2223
volumes:
- ./.envs/local/lavalink/application.yml:/opt/Lavalink/application.yml
env_file:
- ./.envs/local/lavalink/.env
With the Discord secret token provided to my Python bot through environment variables, I was able to add it to my test Discord server and listen for commands. I created one music cog with the following code, which would allow me to play, queue, skip, and stop songs.
import os
import time
import threading
import wavelink
from discord import client, VoiceChannel, FFmpegPCMAudio
from discord.utils import get
from discord.ext import commands
class Music(commands.Cog):
def __init__(self, client: client):
self.client = client
self.lavalink_host = os.environ.get("LAVALINK_HOST")
self.lavalink_port = int(os.environ.get("LAVALINK_PORT"))
self.lavalink_password = os.environ.get("LAVALINK_PASSWORD")
client.loop.create_task(self.connect_nodes())
self.queue = wavelink.Queue()
async def connect_nodes(self):
"""Connect to our Lavalink nodes."""
await self.client.wait_until_ready()
await wavelink.NodePool.create_node(
bot=self.client,
host=self.lavalink_host,
port=self.lavalink_port,
password=self.lavalink_password,
)
async def disconnect(self, player: wavelink.Player):
try:
print("Disconnecting from voice")
player.queue.clear()
player.cleanup()
await player.disconnect()
except Exception as e:
print(e)
@commands.Cog.listener()
async def on_wavelink_track_end(
self, player: wavelink.Player, track: wavelink.Track, reason):
if player.queue.count > 0:
print(f"Current queue count is {player.queue.count}")
await player.play(player.queue.pop())
else:
await self.disconnect(player=player)
@commands.Cog.listener()
async def on_wavelink_node_ready(self, node: wavelink.Node):
print(f"Node: <{node.identifier}> is ready!")
@commands.Cog.listener()
async def on_ready(self):
print("Music is loaded!")
@commands.command()
async def play(self, ctx: commands.Context, *, search: wavelink.GenericTrack):
try:
if not ctx.voice_client:
# noinspection PyTypeChecker
vc: wavelink.Player = await ctx.author.voice.channel.connect(cls=wavelink.Player)
else:
# noinspection PyTypeChecker
vc: wavelink.Player = ctx.voice_client
vc.queue.put(search)
if vc.queue.count == 1 and not vc.is_playing():
print(f"No other songs in queue. Playing {search.title}")
await vc.play(vc.queue.pop())
else:
await ctx.channel.send(
f'There\'s currently a song playing. I added "{search.title}" to the queue.'
)
except Exception as e:
print(e)
@commands.command()
async def stop(self, ctx: commands.Context, **kwargs):
try:
# noinspection PyTypeChecker
vc: wavelink.Player = ctx.voice_client
await vc.stop()
except Exception as e:
print(e)
@commands.command()
async def next(self, ctx: commands.Context, *, search: wavelink.GenericTrack):
try:
# noinspection PyTypeChecker
vc: wavelink.Player = ctx.voice_client
await vc.stop()
if vc.queue.count > 0:
await vc.play(vc.queue.pop())
else:
await self.disconnect(vc)
except Exception as e:
print(e)
@commands.command()
async def queue(self, ctx: commands.Context):
try:
# noinspection PyTypeChecker
vc: wavelink.Player = ctx.voice_client
if vc.queue.count > 0:
message = """Current songs in queue are:\n"""
for track in vc.queue.__iter__():
message += f"- {track.title}\n"
else:
message = "There are no songs in the queue."
await ctx.channel.send(message)
except Exception as e:
print(e)
Once my code was complete, I was able to build my containers, publish them to AWS ECR, and use them in a simple AWS ECS Fargate deployment. My containers could run constantly and with minimal work on my behalf. This isn’t the cheapest solution. I would probably save some money if I just ran a tiny EC2 instance, and created a unit-file to run my docker containers…but this was just a test project and I really did not want to spend much time on infrastructure.
If you are looking for a Discord music bot, feel free to use my sample code to get started!