'How can i add a queue function in my discord music bot?

I dont know how to put a queue function in here, when i play another song while its already playing, it gives me an error with "Already playing audio".

this is inside a cog btw

here is my play command code:

 @commands.command()
async def play(self, ctx, *, url):
    if ctx.voice_client is None:
        voice_channel = ctx.author.voice.channel
        if ctx.author.voice is None:
            await ctx.send("`You are not in a voice channel!`")
        if (ctx.author.voice):
            await voice_channel.connect()
    else: 
        pass
    FFMPEG_OPTIONS = {'before_options':'-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options' : '-vn'}
    YDL_OPTIONS = {'format':'bestaudio', 'default_search':'auto'}
    vc = ctx.voice_client

    with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl:
        info = ydl.extract_info(url, download=False)

        if 'entries' in info:
            url2 = info['entries'][0]['formats'][0]['url']
            title = info['entries'][0]['title']
        elif 'formats' in  info:
            url2 = info['formats'][0]['url']
            title = info['title']
        
        source = await discord.FFmpegOpusAudio.from_probe(url2, **FFMPEG_OPTIONS)
        if ctx.author.voice is None:
            await ctx.send("`You are not in a voice channel`")
        else:
            await ctx.send(f"`Now Playing: {title}`")
            vc.play(source)


Solution 1:[1]

Before anything, you seem to have answered your own question? "put a queue".

All you had to google was "How to make queue in Python" and this would have shown you the way to queue standard library of python.

Now what you need to do is identify, what type of queue do you want.

For example, for now assume there are three types of queue. One of them is a type of queue where you stack everything together, and then use the top one first and work the way to bottom. If you want to visualize this, a bunch of stacked plates might help. When you wash the plates, you stack the plates over each other and if you want one of the plates, you won't start from the bottom as it's harder to get the one in the bottom without disturbing the top ones. This is what we call a LIFO (last in first out) Queue.

Another type of queue is a FIFO (first in first out) Queue, to visualize this, you need to imagine a line of people in front of a shop for a limited game/figurine/item, if you went first, you will be the first one to get the item and get out, causing the second person to come in. The last person won't be the first one to go with the item.

There is also a type of queue called Priority Queue, this is as the name suggest. Imagine the same line we were talking about before, but except, it doesn't matter when you arrive, there are some VIP people who can get chance first even when you arrived earlier than them. This type of queue expect an iterable when you are putting things, the first element of this iterable is a number which displays the priority. So let's say, we put (1, "hello") then we put (30, "nope"), then we put (15, "wahahah") and then we get() from this queue, it would give (1, "hello"), second time calling get() would give (15, "wahahah") and lastly (30, "nope"). It prioritizes the value from lower to higher.

It is without a doubt, that what you are looking for is a FIFO queue first in first out, i.e the song that you play first, is played first, followed by other songs.

The queue standard library of python has a Queue class which offers this. Similarly, for a LIFO Queue, there is a queue.LifoQueue, you can determine the size of these queues by passing a maxsize kwarg, defaults to 0 which means infinite items. You would want to use this if you are low on RAM.

Now that you know what queues are and know basic types of queues, you are wondering how you could implement this.

For this, stop looking at the code, and write points for what you want to do. Or alternatively, visualize, what a user will do, and what you want you bot to do.

++ User executes play command
++ The bot looks if a `Queue` for the guild.id exists
   ++ If it does, you add the song to this queue, and notify user "Hey, I 
      have added your song to the current queue"
   -- If it doesn't, you create a new Queue for that guild's ID and then 
      insert the current song that will be played. Don't play it yet, 
      otherwise it would be hard to write code in a uniform manner.
++ Now that we have a queue, or at least we know the song has been added, We 
   check if our voice client is currently playing something or not. 
   Thankfully, `discord.VoiceClient` offers us a method of `is_playing()`, 
   this is wonderful as we need this to determine if the voice is currently 
   playing or not. If it isn't, then we can play the voice, then we will 
   start a `task` in the background. This is not `ext.tasks` but an 
   `asyncio.Task`. Think of it as launching a ball in space and never 
   expecting it return anything, and you move on with your life. That is the 
   case, until you await it. Awaiting an `asyncio.Task` object will make it 
   execute on spot, and it will block pretty horribly, we don't want that, so 
   we will just make it a background task, so it does its thing in the 
   background.
++ After song is finished, check if our queue of guild.id has any song, if it doesn't, we leave vc notifying user, if it does, we play that, and create a background task again.

From now on whatever I will say is "my way of approaching this", I don't believe this is the best way but it works.

So here is pseudo logic of out looping function

    async def check_play(self, ctx: commands.Context):
        get our current voice client
        while our voice client exists and it is playing:
            sleep for 1 second asynchronously
        dispatch an event that tells our bot that a track has ended

Now that last line touches in an internal function bot.dispatch. To explain briefly, this is the line that is responsible for 'dispatching' "events" in your bot. on_message, on_raw_message, on_reaction_add, on_raw_reaction_add, on_ready and etc. This means you can create custom events and dispatch them.

I would assume the above paragraph wasn't ample visualization, but here is a small example to help

bot = commands.Bot(...)

@bot.listen()
async def on_rude(eek: str):
    print("Wow the rude person said", eek)

@bot.command()
async def rude(ctx, *, arg):
    bot.dispatch('rude', arg)

# on using ?rude what the hell??
# it will print: Wow the rude person said what the hell??

So now we can use this to our advantage. How about dispatching an on_track_end event which checks if our queue has more songs, and if it does, then we play it, while if it doesn't, we say we ran out of songs and leave the VC.

Now combining all that, here is the code.

# utils/models.py
from queue import Queue

class Playlist:
    def __init__(self, id: int):
        self.id = id
        self.queue: Queue = Queue(maxsize=0) # maxsize <= 0 means infinite size

    def add_song(self, song: str):
        self.queue.put(song)

    def get_song(self):
        return self.queue.get()

    def empty_playlist(self):
        self.queue.clear()

    @property
    def is_empty(self):
        return self.queue.empty()

    @property
    def track_count(self):
        return self.queue.qsize()
# main.py

import functools
from typing import Dict
import asyncio

import discord
from discord.ext import commands
import youtube_dl

from utils.models import Playlist



class Music(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.playlists: Dict[int, Playlist] = {}

    async def check_play(self, ctx: commands.Context):
        client = ctx.voice_client
        while client and client.is_playing():
            await asyncio.sleep(1)
        
        self.bot.dispatch("track_end", ctx)

    @commands.command()
    async def play(self, ctx: commands.Context, *, url: str):
        if ctx.voice_client is None:
            voice_channel = ctx.author.voice.channel
            if ctx.author.voice is None:
                await ctx.send("`You are not in a voice channel!`")
            if (ctx.author.voice):
                await voice_channel.connect()
        else: 
            pass
        FFMPEG_OPTIONS = {'before_options':'-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options' : '-vn'}
        YDL_OPTIONS = {'format':'bestaudio', 'default_search':'auto'}

        with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl:
            info = ydl.extract_info(url, download=False)

            if 'entries' in info:
                url2 = info['entries'][0]['formats'][0]['url']
                title = info['entries'][0]['title']
            elif 'formats' in  info:
                url2 = info['formats'][0]['url']
                title = info['title']
            
            source = await discord.FFmpegOpusAudio.from_probe(url2, **FFMPEG_OPTIONS)
            self.bot.dispatch("play_command", ctx, source, title)
        

    @commands.Cog.listener()
    async def on_play_command(self, ctx: commands.Context, song, title: str):
        playlist = self.playlists.get(ctx.guild.id, Playlist(ctx.guild.id))
        self.playlists[ctx.guild.id] = playlist
        to_add = (song, title)
        playlist.add_song(to_add)
        await ctx.send(f"`Added {title} to the playlist.`")
        if not ctx.voice_client.is_playing():
            self.bot.dispatch("track_end", ctx)

    @commands.Cog.listener()
    async def on_track_end(self, ctx: commands.Context):
        playlist = self.playlists.get(ctx.guild.id)
        if playlist and not playlist.is_empty:
            song, title = playlist.get_song()
        else:
            await ctx.send("No more songs in the playlist")
            return await ctx.guild.voice_client.disconnect()
        await ctx.send(f"Now playing: {title}")
        
        ctx.guild.voice_client.play(song, after=functools.partial(lambda x: self.bot.loop.create_task(self.check_play(ctx))))
        # for the above code, instead of functools.partial, you could also create_task on the next line, I just find using the `after` kwargs much better

def setup(bot):
   bot.add_cog(Music(bot))

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Rose