discord music bot

How To Make A Discord Music Bot With Python In 2023

In this post, we’re going to make our own Discord bot that will be able to play music from Youtube. Since many of the music streaming bots had legal issues because of Youtube, we can’t use them anymore, sadly.

But don’t worry, it’s not the end of the story for music bots just yet. I’m going to show you how to setup a simple Discord bot for playing music, which you can host on your own server. Even more, as long as you don’t monetize it in any way, it should be perfectly fine.

Prerequisites

Before we begin writing any code, we need to create a Discord application, which will allow us to setup a bot user. For that to happen, you’ll need to go to official Discord developer portal and create a new application.

Discord applications dashboard - creating  new application

Then, you’ll need to invite your bot to your server through a link that you can create under OAuth2 tab of your application. Here, you’ll also need to check what kind of permissions you want your bot to have.

Creating the link for getting our Discord bot into server

I’m going to put Administrator privilages for the sake of demonstration. This is not what I would recommend you to do. Usually, it would suffice if you just checked whatever your bot will need.

Start coding

Okay, now that we have our bot setup from the Discord end, we need to give it some functionality so we can use it. Additionally, the way I’ll code this is by separating the Discord bots token into its own file. This will allow me to retrieve it in the python script, without revealing it there.

So let’s go ahead and import the necessary libraries for this project and write the function that will fetch our token.

import os
import json
import asyncio
import discord
from discord.ext import commands
import yt_dlp as youtube_dl

ROOT = os.path.dirname(__file__)
def get_token(token_name):

    auth_file = open(os.path.join(ROOT, 'auth.json'))
    auth_data = json.load(auth_file)
    token = auth_data[token_name]
    return token

Coding the brain of our Discord music bot

In order for our bot to work, we need to create a Cog class, which will hold the functionality of our bot. Furthermore, we can create multiple Cogs for a bot, which we’ll add them to the bot instance we’ll create later on.

class MusicCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

        self.is_playing = False
        self.is_paused = False

        self.music_queue = []
        self.load_queue()

        self.ydl_options = {
            'format': 'bestaudio/best',
            'outtmpl': os.path.join(ROOT, 'yt', '%(extractor)s-%(id)s-%(title)s.%(ext)s'),
            'restrictfilenames': True,
            'noplaylist': True,
            'nocheckcertificate': True,
            'ignoreerrors': False,
            'logtostderr': False,
            'quiet': True,
            'no_warnings': True,
            'default_search': 'auto',
            'source_address': '0.0.0.0',  # bind to ipv4 since ipv6 addresses cause issues sometimes
        }

        self.ffmpeg_options = {
            'options': '-vn'
        }

        self.voice_client = None
    
    def load_queue(self):
        try:
            with open(os.path.join(ROOT, 'queue.json'), 'r') as queue_file:
                self.music_queue = json.load(queue_file)
        except:
            print('Starting from empty queue.')

    def save_queue(self):
        with open(os.path.join(ROOT, 'queue.json'), 'w') as queue_file:
            json.dump(self.music_queue, queue_file, indent=4)
    
    def search_yt(self, item):
        with youtube_dl.YoutubeDL(self.ydl_options) as ydl:
            try:
                info = ydl.extract_info(item, download=True)
                
                if 'entries' in info:
                    info = info['entries'][0]
                    source = info['formats'][0]['url']
                else:
                    source = info['url']
                
                filename = ydl.prepare_filename(info)
            except:
                print('Something went wrong.')
    
        return {
            'source': source,
            'title': info['title'],
            'filename': filename
        }
    
    def play_next(self):
        if len(self.music_queue) > 0:
            self.is_playing = True
            filepath = self.music_queue[0]['filename']
            self.music_queue.pop(0)
            self.save_queue()

            self.voice_client.play(discord.FFmpegPCMAudio(filepath, **self.ffmpeg_options), 
                                   after=lambda e: self.play_next())
        else:
            self.is_playing = False

    async def play_music(self, ctx):
        if len(self.music_queue) > 0:
            self.is_playing = True
            channel = ctx.author.voice.channel

            filepath = self.music_queue[0]['filename']
            await ctx.send(f'Now playing: {self.music_queue[0]["title"]}')

            if self.voice_client == None or not self.voice_client.is_connected():
                self.voice_client = await channel.connect()

                if self.voice_client == None:
                    await ctx.send('Could not connect to the voice channel.')
                    return
            else:
                await self.voice_client.move_to(channel)
            
            self.music_queue.pop(0)
            self.save_queue()
            self.voice_client.play(discord.FFmpegPCMAudio(filepath, **self.ffmpeg_options),
                                   after=lambda e: self.play_next())
        else:
            self.is_playing = False

    
    @commands.hybrid_command(name='play')
    async def play(self, ctx, *, song):
        channel = ctx.author.voice.channel
        if channel is None:
            await ctx.send('You\'re not connected to a voice channel.')
        elif self.is_paused:
            self.voice_client.resume()
        else:
            async with ctx.typing():
                result = self.search_yt(song)
                if type(result) == type(True):
                    await ctx.send('Oops, something went wrong.')
                else:
                    self.music_queue.append(result)
                    self.save_queue()

                    if self.is_playing == False:
                        await self.play_music(ctx)
                    else:
                        await ctx.send(f'Added {result["title"]} to the queue.')

    @commands.hybrid_command(name='pause')
    async def pause(self, ctx):
        if self.is_playing:
            self.is_playing = False
            self.is_paused = True
            self.voice_client.pause()
        elif self.is_paused:
            self.is_paused = False
            self.is_playing = True
            self.voice_client.resume()
        
        await ctx.send('')
    
    @commands.hybrid_command(name='resume')
    async def resume(self, ctx):
        if self.is_paused:
            self.is_paused = False
            self.is_playing = True
            self.voice_client.resume()
        await ctx.send('')
    
    @commands.hybrid_command(name='skip')
    async def skip(self, ctx):
        if self.voice_client != None and self.voice_client:
            self.voice_client.stop()
            await self.play_music()
            await ctx.send('')
    
    @commands.hybrid_command(name='queue')
    async def queue(self, ctx):
        result = ''
        for q in self.music_queue:
            result += q['title'] + '\n'
        
        if result != '':
            await ctx.send(result)
        else:
            await ctx.send('Queue is empty.')
    
    @commands.hybrid_command(name='clear')
    async def clear(self, ctx):
        if self.voice_client != None and self.is_playing:
            self.voice_client.stop()
        self.music_queue = []
        self.save_queue()
        await ctx.send('Queue cleared.')
    
    @commands.hybrid_command(name='leave')
    async def leave(self, ctx):
        async with ctx.typing():
            self.is_playing = False
            self.is_paused = False
            await self.voice_client.disconnect()

There’s a lot going on in this class as you can see. Mostly, it contains functions for our commands, we want our bot to respond to.

This Discord music bot includes a queue functionality, which will also save it to a json file. This allows it to remember queued songs even if it disconnects for some reason.

Create Discord music bot instance & run it

For the last part of this tutorial, you’ll need to plug functionality above into a bot and run it. For that, you’ll need to setup its instance, which requires intents and description. I also added command prefix to it so you can use commands in a few different ways.

Furthermore, you need to keep in mind that it’s very important that you sync the bot commands if you want to create slash commands. In this case, we’re using hybrid commands, which include slash and prefixed commands.

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix=commands.when_mentioned_or('!'),
    description='A music bot.',
    intents=intents
)

@bot.event
async def on_ready():
    print(f'Logged in as {bot.user} (ID: {bot.user.id})')
    print('------')
    await bot.tree.sync()
    
async def main():
    async with bot:
        await bot.add_cog(MusicCog(bot))
        await bot.start(get_token('discord-token'))

asyncio.run(main())

For your bot to grab the token, you’ll need to create a json file and name it auth.json. In this file, you’ll input the following code and replace the “TOKEN” text with your actual token.

{
    "discord-token": "TOKEN"
}

Okay, we’re done! Now all we have to do is run this thing and enjoy the music with our friends.

Conclusion

To conclude, we created a simple Discord bot for playing music from Youtube. I learned a lot while working on this project and I hope this post proves helpful to you as well.

If you liked this tutorial, you can also check out my other Discord bot building tutorials.

Share this article:

Related posts

Discussion(0)