Build a discrod music Bot

https://www.theverge.com/2021/8/24/22640024/youtube-discord-groovy-music-bot-closure

這張圖片的 alt 屬性值為空,它的檔案名稱為 99nShh7.png

https://www.theverge.com/2021/9/12/22669502/youtube-discord-rythm-music-bot-closure

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-20.png

Recently, youtube kill’s some of the most used music robots on discord, and other public ones may also be asked to be shut down, so today let us make our discord bot!!!

Step 1 : Create your bot

Goto this website https://discord.com/developers/applications, I have already created one, click the “New Application” button.

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-22-1024x480.png

And name your bot.

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-23.png

If you create the bot successfully, you should see it appear on your main page, just click it.

Go and copy your APPLICATION ID, and fill in this link:

https://discordapp.com/oauth2/authorize?&client_id=xxxxxxxxxxxxx&scope=bot&permissions=8 # replace xxxxx with you Id
這張圖片的 alt 屬性值為空,它的檔案名稱為 image-25-1024x570.png

with this link, you can invite it to your channel.

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-27-1024x568.png

Step 2 : authorization token

Go to the “bot” and click “copy”, this will give you your Token

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-24-1024x615.png

Step 3 : Start coding

Here, I’m will build the bot with python in windows, so you will need python on your computer

open cmd and download discord’s API

pip install discord.py
這張圖片的 alt 屬性值為空,它的檔案名稱為 image-28-1024x436.png

Open sublime and paste the following code to start a simple bot

save and open with cmd.

we know this is not enough…, we need a music bot, not a ping-pong idiot…, next, I will speed up and point out the difficulties and solutions I encountered.

Step 4 : A music bot

A normal music bot should be able to play youtube music and have the following functions.

  • play : Add a song or a playlist to the Queqe and invite the bot to the voice channel
  • pause : pause
  • skip : skip current song
  • list : show the playlist
  • clear : clear the playlist
  • loop : loop the playlist
  • leave : leave the voice channel

we define severals function that we want our music bot have.

Step 5 : Play a music

Let’s see how others play music

with this code we can understand how everything works, first, we need FFmpeg.

https://www.ffmpeg.org/download.html

when a user types a play command, the code will get its message channel (ctx), and get the Author’s voice channel (channel), then with “channel.connect()” to connect the bot to the voice channel.

channel = ctx.message.author.voice.channel #this line will get the user's voice channel
voice =  await channel.connect()   # connect to the user's voice channel

voice.play(discord.FFmpegPCMAudio(song_path), after = lambda e: somefuciton())
# play the music ,  'after' will run when the song is finish.

Step 6 : Create a class

you may ask: why do we need a class?

If our robot is to serve different servers, it means that there will be different ctx, channel, and voice, we also need to queue the songs of each channel, so the best choice is to create a class, each class serves a server.

class MusicBot:
    def __init__(self,channel, voice , ctx):
        self.music_msg = None       # The message for current music
        self.loop      = False      # Enable loop or not
        self.live      = True       # Kill this Musicbot if self.live = False
        self.channel   = channel    # Voice channel
        self.channelid = channel.id # This Music serves channel Id
        self.floder    = "music"    # Folder to store music
        self.ctx       = ctx        # ctx
        self.voice     = voice      # voice client
        self.queqed    = []         # music queqed for play
        self.passed    = []         # Played music
        self.state     = 0          # 0:not playing , 1:playing , 2:pause
        self.rejoin_c  = 0          # rejoin every 20 songs
        self.dont_stop = 0          # will play untill dont_stop = 3 to check again

so I define a class that needs channel, voice, and ctx as input.

Step 7 : Download youtube music

we know we can use youtube_dl to download the music.

pip install youtube-dl
self.ytl  = {
                'format': '249/250/251',
                "outtmpl" : f"{self.floder}/{this_song_name}",
                'noplaylist': False
} 
with youtube_dl.YoutubeDL(self.ytl) as ydl:
    ydl.download([this_song_url])

But I think it is difficult to manage which songs the user has queued and youtube_dl will download the entire playlist before I play the music, of course, I can use a thread, But it is too complex.

So, I will use a youtube data API to grab the playlist’s songs and download one at a time and play it. the following is the youtube API code, however, I will not explain this code since our main character is a discord music bot.

with this API, I can write a function to add music to the queue

def add_thread(self,url):
        print("[*] Adding rest in this thread ... ")
        output = grab_playlist(url,50)
        for each in output[10:50]:
            self.queqed.append(each)
        print("[*] Adding Thread done.")
        print(f"[*] Qeuqed {len(self.queqed)} songs ->",self.channelid)

async def add(self,url):
        print("[*]", url)
        if ("list" in url):
            print("[*] Adding a play list in", self.channelid)
            output = grab_playlist(url,10)
            for each in output[0:10]:
                self.queqed.append(each)
            threading.Thread(target = self.add_thread, args=(url,)).start()

        else:
            print("[*] Adding a single video in", self.channelid)
            self.queqed.append((get_title(url),url))

        print("\n[*]",self.channelid,"-> Enqueued :",len(self.queqed), "the remaining songs will continue add in the background")

        if (self.state ==0):
            self._next()  #  the play music function

add function can handle a video or a playlist, if it is a video, just add it directly, but if it is a playlist, I will first grab the first 10 videos, the rest will throw in a thread to continue, this is to speed up the time to start the song, otherwise, the user will feel impatient.

Play command

Users_class  = {}

@client.command(brief="Play music (My wonderful singing )")
async def play(ctx, url : str):
    if not ctx.message.author.voice:
        await ctx.send('you are not connected to a voice channel')
        return
    else:
        channel = ctx.message.author.voice.channel

    if (channel.id in Users_class):
        if ctx.guild.voice_client not in client.voice_clients:
            await Users_class[channel.id].kill() # kill the music player
            del Users_class[channel.id]
            print("[*] rejoin the voice channel")
            voice =  await channel.connect()
            MB = MusicBot(channel, voice , ctx)
            Users_class[channel.id] = MB
        await Users_class[channel.id].add(url)
    else:
        voice =  await channel.connect()
        MB = MusicBot(channel, voice , ctx)
        print(f"[*] creating Class id : {id(MB)} for serving channel",channel.id)
        Users_class[channel.id] = MB
        await MB.add(url)

In play command, I will create the music bot class and save the class to a dictionary by using channel id as the key.

# play next music
async def _next(self):

    if (len(self.queqed) == 0):

        if (self.loop): # refill the music queqed for play with Played music
            self.queqed = self.passed
            self.passed = []
        else:
            self.state = 0
            return    

    self.state = 1    # State = 1 -> start playing                 
    self.this_song  = self.queqed.pop(0) # get the song 
    self.passed.append(self.this_song)   # Store in history music

    print("[*] playing  :", self.this_song[0],"in", self.channelid)
    this_song_url   = self.this_song[1]
    this_song_name  = self.this_song[0]


    #  This for is beacuse the youtube_dl will change the filename
    #  when handling special char
    for each_char in ['"', "'", ":", "|"]:
        if (each_char in this_song_name):
            this_song_name = this_song_name.replace(each_char,"#")

    # youtube_dl will create a floder for \ and /
    for each_char in ["\\", "/"]:
        if (each_char in this_song_name):
            this_song_name = this_song_name.replace(each_char,"")

    song_path  = os.path.join(self.floder , this_song_name)

    # download youtube music
    if ( not os.path.isfile(song_path)):
        self.ytl  = {
            'format': '249/250/251',
            "outtmpl" : f"{self.floder}/{this_song_name}",
            'noplaylist': False,
        } 
        self.dowloading = await self.ctx.send(f'... Downloading {this_song_name}')
        print("[*] downloading ->", this_song_name,"\n")
        with youtube_dl.YoutubeDL(self.ytl) as ydl:
            ydl.download([this_song_url])
        print("\n[*] ------------ download successful ------------")

    # Check if bot is in the voice channel
    if self.ctx.guild.voice_client not in client.voice_clients:
        print("[*] get kicked from",self.channelid)
        return

    # send currently playing music
    self.music_msg = await self.ctx.send(f':musical_note:  Now playing ({len(self.passed)}/{len(self.queqed)+len(self.passed)}) : {this_song_name} :musical_note:')

    

    ######################   I will explian this later
    FFMPEG_OPTS = {
        'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 
        'options': '-vn'
        }
           self.rejoin_c += 1
   if (self.rejoin_c == 10):
         self.voice.play(
                discord.FFmpegPCMAudio(song_path, **FFMPEG_OPTS), 
                after=lambda e: print("[*] reconnecting the ffmpeg , error : ",e)
                )
        self.rejoin_c = 0

        await asyncio.sleep(5)
   ######################

    # play the music, call _endsong when the song finish
    self.voice.play(
            discord.FFmpegPCMAudio(song_path), 
            after = lambda e: asyncio.run_coroutine_threadsafe(self._endsong(e), client.loop)
            )

    
    # check every three song 
    self.dont_stop +=1
    if (self.dont_stop > 3):
        client.loop.create_task(self.check())


This is a quite long function, but most of them are actually pretty easy to understand, except the FFMPEG_OPTS part, this part is to solve a known problem.

https://support.discord.com/hc/fr/articles/360035010351–Known-Issue-Music-Bots-Not-Playing-Music-From-Certain-Sources

The robot will suddenly have no sound while playing a song, I often encounter this problem when I am testing,

To solve this problem, you have to use that FFMPEG_OPTS variable. It will reconnect the bot to the source.

But there is a bug, I’m not sure if it is my problem or how, my FFmpeg will show “reconnect” Option not found, I still trying to fix it.

Step 8 : Complete every thing

This is my MusicBot with all the functions we want.

Then try to run the bot and see if everything works well.

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-29.png

Step 9 : Develop on server

Now try to put this robot on a server, you can use Heroku or something else, for me , I will put it on the Raspberry Pi

這張圖片的 alt 屬性值為空,它的檔案名稱為 dwzZ6giNPkhGbmumGqcT1PEHXUM6vGh4-KH6_DN6_W43eWgqiB8m7VIbTTmKkWdaTemgixIxJypIOTqtoiBrsGcG7nSMipYBg5k67A-0w4ft3BqpL_HRGePRdXz01S3euElu2lizy2EdfJV1iNwZ5fAT34kkylwHbI3wvwEoJZTZB8bKwSYWvw1YKUJ_qflXvbUVhjiBiQ6MBd95jHfg45hgcuPJSYYmw9UaGIPFTeUGzem7IT7AttdDi15Y3EeRxWd-ljVAurT4kifjwQ73j807SordCQY8M4Znj-xWB-22sS0CsqHsD3-t8QtbDNM8q95K0Y-vAM_X1ZaYIMs1lphaauKtIzhYAYtBsa_uSLwxEWVpFwUz6e5bPm2_mJnIylvd2h3W_qbUH9iltC3B9NVDO1_UBmOTqeS3l7q6l0lUjbGoe7ogyNwILFaFku_Cb9Je9lmhwqkkjvNfk7vqRjgzwJdU9kCWqsR8PBtPXIpCfuap6rxJ0OLem-89UQBtSetp5tZSvwbtvhXEMlRLHzOWBnKJ5QB7gZbng7n_MN84NyudLsFQZF4bRIJ3zLG-zTZFjzCmg3AtlZsdc_yGH8itL_B7C_693uM31TZvk-F47ZM5oD37Q5bP1hMBwe6_ntYX1vnKpKfwHa-5Ro0JXL1Nr_I7FY16IpCu2FGJ9JVQav8-XMGOpQhrbyMyeAihJSVTiy8GRk8lYFU3eAzCJrQ=w678-h903-no

So, first I will use python to open a simple webserver on windows to send my bot’s code to Raspberry Pi .

python -m http.server
這張圖片的 alt 屬性值為空,它的檔案名稱為 image-30.png

Cause I’m connect to the same route, so I can use the local internet to send my bot, use Ip config to get you computer’s local IP

ipconfig
這張圖片的 alt 屬性值為空,它的檔案名稱為 image-35.png

Just wget the ip: port/file and you can grab the file

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-31-1024x535.png

Set up your environment and run the server

pip install google-api-python-client
pip install discord.py
pip install youtube_dl
sudo apt install ffmpeg
這張圖片的 alt 屬性值為空,它的檔案名稱為 image-32-1024x268.png

Congratulations, you successfully made your own robot !!!

分類: Uncategorized,標籤: , 。這篇內容的永久連結

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *