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

Posted

技术标签:

【中文标题】Discord.py 机器人歌曲排队:voice_client.play() 在开始时播放所有循环【英文标题】:Discord.py bot song queueing: voice_client.play() plays all loops at beginning 【发布时间】:2021-09-03 07:32:53 【问题描述】:

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

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参数是在歌曲播放完后触发的?歌曲完成后如何触发它?有没有更好的方法来处理歌曲队列的播放?

【问题讨论】:

【参考方案1】:

According to the docs:

终结器,after 在源耗尽或发生错误后调用。

...

after (Callable[[Exception], Any]) – 在流耗尽后调用的终结器。此函数必须有一个参数 error,表示在播放期间引发的可选异常。

当您设置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。

【讨论】:

这是有道理的,只是我不能将 await 放入 lambda 中,否则会出现以下错误:```` after=lambda ex: await self. after(ctx)) ^ SyntaxError: 'await' outside async function ``` 知道如何让它工作吗? @kmurrell 我已经用您当前设计应该能够运行的代码更新了答案,但我想强调这不是使用 discord.py 处理音乐队列的正确方法 -的after 参数旨在处理播放时发生的异常,而不是调度任务。 很好的答案,感谢您提供修复和更好的解决方案!

以上是关于Discord.py 机器人歌曲排队:voice_client.play() 在开始时播放所有循环的主要内容,如果未能解决你的问题,请参考以下文章

discord.py和discord.py的区别[voice]

voice_client 和 url 突然在 discord.py 机器人中不起作用

如何改进我的队列系统 - Discord.py

FFmpeg 不工作(discord.py)

discord.py - FFmpegPCMAudio 使用大量 CPU

如何使用 discord.py 机器人断开人们与语音频道的连接?