如何在我的 discord 音乐机器人中添加队列功能?

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

我不知道如何在此处放置队列功能,当我播放另一首正在播放的歌曲时,它会提示我“正在播放音频”错误。

顺便说一句,这是在一个齿轮里面

这是我的播放命令代码:

 @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)

在任何事情之前,您似乎已经回答了自己的问题? “排队”。

你所要做的 google 就是“如何在 Python 中制作队列”,这会向你展示如何对 python.[=42= 的标准库进行排队]

现在你需要做的是确定你想要什么类型的队列。

例如,现在假设有三种类型的队列。其中之一是一种队列类型,您可以将所有内容堆叠在一起,然后首先使用顶部的队列,然后一直到底部。如果您想将其形象化,一堆堆叠的盘子可能会有所帮助。当你洗盘子时,你把盘子叠在一起,如果你想要其中一个盘子,你不会从底部开始,因为很难在不打扰顶部的情况下把底部的盘子洗干净。这就是我们所说的 LIFO (last in first out) Queue.

另一种类型的队列是 FIFO (first in first out) Queue,要将其形象化,您需要想象一排人在商店前排队等待 limited game/figurine/item,如果您先去,您将第一个拿东西出去,导致第二个人进来。最后一个人不会是第一个拿东西的人。

还有一种队列叫做Priority Queue,顾名思义就是这个。想象一下我们之前谈论的同一条线路,但除了,你什么时候到达并不重要,有一些贵宾即使你比他们早到达也能抢先获得机会。这种类型的队列在你放东西的时候需要一个迭代器,这个迭代器的第一个元素是一个显示优先级的数字。比方说,我们放 (1, "hello") 然后我们放 (30, "nope"),然后我们放 (15, "wahahah") 然后我们从这个队列中放 get(),它会给出 (1, "hello"),第二个调用 get() 的时间会给出 (15, "wahahah"),最后是 (30, "nope")。它按从低到高的顺序排列值。

毫无疑问,您正在寻找的是 FIFO 队列 first in first out,即您首先播放的歌曲首先播放,然后是其他歌曲。

python 的 queue 标准库有一个 Queue class 提供了这个。类似地,对于 LIFO Queue,有一个 queue.LifoQueue,您可以通过传递 maxsize kwarg 来确定这些队列的大小,默认为 0,这意味着无限项。如果你的 RAM 不足,你会想使用它。

既然您知道什么是队列并且知道队列的基本类型,您想知道如何实现它。

为此,别再看代码了,写点自己想做的事情。或者,可视化用户将做什么以及您希望机器人做什么。

++ 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.

从现在开始,无论我说什么都是“我的方法”,我不认为这是最好的方法,但它有效。

所以这是out循环函数的伪逻辑

    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

现在最后一行涉及内部函数 bot.dispatch。简而言之,这是负责机器人中 'dispatching' “事件”的行。 on_messageon_raw_messageon_reaction_addon_raw_reaction_addon_ready 等。这意味着您可以创建 自定义事件 和派遣他们。

我认为上面的段落不够直观,但这里有一个小例子可以提供帮助

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??

所以现在我们可以利用它来发挥我们的优势。如何调度一个 on_track_end 事件来检查我们的队列是否有更多歌曲,如果有,那么我们播放它,如果没有,我们说我们 运行 没有歌曲并离开VC.

现在结合所有这些,这是代码。

# 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))