Discord.py 机器人歌曲排队:voice_client.play() 在开头播放所有循环

Discord.py bot song queueing: voice_client.play() plays all loops at beginning

我有一个允许用户排队歌曲的 discord 机器人,这里是代码的简化版本:

class Music(commands.Cog):
  def __init__(self, bot: Bot, config: Dict[str, Any]):
      self.bot = bot
      self.queue = asyncio.Queue()
      self.urlqueue = list()
      self.yt_api = YoutubeAPI(config["YD_DL_OPTS"], DL_DIR)
      self.load_queue.start()

  @commands.command(name="play", aliases=["p"], pass_context=True, usage=DOCS_PLAY)
  async def play(self, ctx: Context, *args):
      if ctx.voice_client.is_playing():
          ctx.voice_client.stop()
      await self.play_song(ctx)

  @commands.command(name="queue", aliases=["q"], pass_context=True, usage=DOCS_QUEUE)
  async def queue(self, ctx: Context, *args):    
      self.urlqueue.append(args[0])

  @tasks.loop(seconds=5.0)
  async def load_queue(self):
      if len(self.urlqueue) == 0:
          return
      for track in self.yt_api.create_tracks(self.urlqueue.pop(0)):
          if self.yt_api.download_track(track):
              await self.queue.put(track)
              logger.info("queued track [{}]".format(track.title))

  async def play_song(self, ctx: Context):
      logger.info("getting track [{}]".format(track.title))
      track = await self.queue.get()
      logger.info("playing track [{}]".format(track.title))
      await ctx.send(content="playing track {}".format(track.title))
      ctx.voice_client.play(discord.FFmpegPCMAudio(track.filename), after=await self.after(ctx))
      ctx.voice_client.is_playing()

  async def after(self, ctx):
      if not self.queue.empty() and not ctx.voice_client.is_playing():
          logger.info("looping start")
          await self.play_song(ctx)
          logger.info("looping end")

  def cog_unload(self):
      self.load_queue.cancel()

当使用 queue 命令传递 url 时,将通过循环 load_queue() 方法创建、下载曲目并将其添加到 asyncio.Queue()

调用 play 命令时出现此问题,在第 ctx.voice_client.play() 行的方法 play_song() 中, after 参数在第一首曲目被调用之前立即被调用甚至玩过。这导致该方法首先快速循环遍历队列中的所有歌曲并尝试一次播放所有歌曲。最后只有队列中的第一首歌曲真正播放,因为其他歌曲都遇到了 ClientException('Already playing audio.') 异常。日志如下所示:

2021-06-18 10:38:09,004 | INFO     | 0033 | Bot online!
2021-06-18 10:38:23,442 | INFO     | 0222 | queued track [i miss you]
2021-06-18 10:38:26,882 | INFO     | 0222 | queued track [When you taste an energy drink for the first time]
2021-06-18 10:38:32,828 | INFO     | 0205 | joined General by request of Admin
2021-06-18 10:38:32,829 | INFO     | 0226 | got track [i miss you]
2021-06-18 10:38:32,829 | INFO     | 0230 | playing track [i miss you]
2021-06-18 10:38:32,832 | INFO     | 0236 | loop start
2021-06-18 10:38:32,832 | INFO     | 0226 | got track [When you taste an energy drink for the first time]
2021-06-18 10:38:32,833 | INFO     | 0230 | playing track [When you taste an energy drink for the first time]
2021-06-18 10:38:32,837 | INFO     | 0238 | loop end
Ignoring exception in command play:
Traceback (most recent call last):
  File "C:\Users\kenmu\Repos\DiscordBot\venv\lib\site-packages\discord\ext\commands\core.py", line 85, in wrapped
    ret = await coro(*args, **kwargs)
  File "C:\Users\kenmu\Repos\DiscordBot\music.py", line 91, in play
    await self.play_song(ctx)
  File "C:\Users\kenmu\Repos\DiscordBot\music.py", line 231, in play_song
    ctx.voice_client.play(discord.FFmpegPCMAudio(track.filename), after=await self.after(ctx))
  File "C:\Users\kenmu\Repos\DiscordBot\venv\lib\site-packages\discord\voice_client.py", line 558, in play
    raise ClientException('Already playing audio.')
discord.errors.ClientException: Already playing audio.

如您所见,它试图在几毫秒内遍历整个队列。我以为 after 参数是在歌曲播放完毕后触发的?歌曲结束后如何让它触发?有没有更好的方法来处理歌曲队列的播放?

According to the docs:

The finalizer, after is called after the source has been exhausted or an error occurred.

...

  • after (Callable[[Exception], Any]) – The finalizer that is called after the stream is exhausted. This function must have a single parameter, error, that denotes an optional exception that was raised during playing.

您在设置 after 参数时调用了 await self.after(ctx),甚至在调用 ctx.voice_client.play 之前。您需要为它提供一个可调用对象(例如一个函数),该函数接受播放期间引发的异常,如果没有引发异常,则默认为 None

像这样:

    async def play_song(self, ctx: Context):
        logger.info("getting track [{}]".format(track.title))
        track = await self.queue.get()
        logger.info("playing track [{}]".format(track.title))
        await ctx.send(content="playing track {}".format(track.title))
        ctx.voice_client.play(
            discord.FFmpegPCMAudio(track.filename),
            after=lambda ex: asyncio.get_running_loop().create_task(self.after(ctx))
        )
        ctx.voice_client.is_playing()

    async def after(self, ctx):
        if not self.queue.empty() and not ctx.voice_client.is_playing():
            logger.info("looping start")
            await self.play_song(ctx)
            logger.info("looping end")

但是您不应该使用 after 参数来播放队列中的下一首歌曲。你可以 see how it's done (with a while loop in the player_loop coroutine) here.