You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Discord.py音乐机器人播放与队列功能异常求助

解决Discord.py音乐机器人的播放队列问题

嘿,我看到你在开发Discord.py音乐机器人时遇到了两个播放相关的问题,让我一步步帮你解决它们!

问题1:播放新曲目时直接跳过当前歌曲

问题原因

你的ensure_voice装饰器里有这样一段代码:

elif ctx.voice_client.is_playing():
    ctx.voice_client.stop()

这段代码会在当前有歌曲播放时直接终止它,然后播放新曲目,这就是为什么新曲目会跳过当前歌曲的原因。我们需要改成当前播放时将新曲目加入队列,而非停止当前播放。

解决方案

首先修改ensure_voice函数,移除停止当前播放的逻辑:

@play_queue.before_invoke
@play.before_invoke
async def ensure_voice(self, ctx):
    if ctx.voice_client is None:
        if ctx.author.voice:
            await ctx.author.voice.channel.connect()
        else:
            await ctx.send("You are not connected to a voice channel.")
            raise commands.CommandError("Author not connected to a voice channel.")
    # 移除停止当前播放的代码

然后修改play命令的逻辑,判断当前是否在播放:如果是,就把曲目加入队列;否则直接播放:

@commands.command()
async def play(self, ctx, *, url):
    try:
        # 先检查是否在播放,如果是则加入队列
        if ctx.voice_client.is_playing():
            global queue
            queue.append(url)
            await ctx.send(f'``{url}`` was added to the queue!')
            return
        
        async with ctx.typing():
            player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
            # 在after回调里自动播放队列的下一首
            ctx.voice_client.play(player, after=lambda e: asyncio.run_coroutine_threadsafe(self.play_next(ctx), self.bot.loop) if not e else print('Player error: %s' % e))
        await ctx.send(f':mag_right: **Searching for** ``{url}``\n<:youtube:763374159567781890> **Now Playing:** ``{player.title}``')
    except Exception as e:
        await ctx.send(f"Something went wrong - please try again later!\nError: {str(e)}")

问题2:play_queue命令仅播放队列第一首歌

问题原因

你用for循环直接遍历队列并调用play,但ctx.voice_client.play是非阻塞的,循环会瞬间执行完所有play调用,导致只有最后一个请求生效(或者第一个,取决于执行顺序),其他的被覆盖。我们需要利用after回调来实现播放完一首后自动播放下一首的逻辑。

解决方案

首先添加一个辅助方法play_next,用来处理队列的下一首播放:

async def play_next(self, ctx):
    global queue
    if len(queue) > 0:
        next_url = queue.pop(0)
        try:
            player = await YTDLSource.from_url(next_url, loop=self.bot.loop, stream=True)
            ctx.voice_client.play(player, after=lambda e: asyncio.run_coroutine_threadsafe(self.play_next(ctx), self.bot.loop) if not e else print('Player error: %s' % e))
            await ctx.send(f'<:youtube:763374159567781890> **Now Playing:** ``{player.title}``')
        except Exception as e:
            await ctx.send(f"Failed to play next track: {str(e)}")
            # 继续尝试播放下一首
            await self.play_next(ctx)
    else:
        await ctx.send("Queue is now done!")

然后修改play_queue命令,只需要触发第一首的播放,剩下的交给after回调处理:

@commands.command()
async def play_queue(self, ctx):
    global queue
    if len(queue) == 0:
        await ctx.send("Queue is empty!")
        return
    
    # 如果当前没有播放,就开始播放队列的第一首
    if not ctx.voice_client.is_playing():
        await self.play_next(ctx)
    else:
        await ctx.send("Bot is already playing music! Queue will continue after current track.")

修改后的完整代码

import discord
import asyncio
import os
import youtube_dl
import urllib.parse, urllib.request, re
import requests
from discord.ext import commands
from discord import Embed, FFmpegPCMAudio
from discord.utils import get

''' INSTALLING YOUTUBE-DL
pip install -U git+https://github.com/l1ving/youtube-dl '''

queue = []

youtube_dl.utils.bug_reports_message = lambda: ''

ytdl_format_options = {
    'format': 'bestaudio/best',
    'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
    'restrictfilenames': True,
    'noplaylist': True,
    'nocheckcertificate': True,
    'ignoreerrors': False,
    'logtostderr': False,
    'quiet': True,
    'no_warnings': True,
    'default_search': 'auto',
    'source_address': '0.0.0.0'
}

ffmpeg_options = {
    'options': '-vn'
}

ytdl = youtube_dl.YoutubeDL(ytdl_format_options)

class YTDLSource(discord.PCMVolumeTransformer):
    def __init__(self, source, *, data, volume=0.5):
        super().__init__(source, volume)
        self.data = data
        self.title = data.get('title')
        self.url = data.get('url')

    @classmethod
    async def from_url(cls, url, *, loop=None, stream=False, play=False):
        loop = loop or asyncio.get_event_loop()
        data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream or play))

        if 'entries' in data:
            data = data['entries'][0]

        filename = data['url'] if stream else ytdl.prepare_filename(data)
        return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)

class Music(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    async def play_next(self, ctx):
        global queue
        if len(queue) > 0:
            next_url = queue.pop(0)
            try:
                player = await YTDLSource.from_url(next_url, loop=self.bot.loop, stream=True)
                ctx.voice_client.play(player, after=lambda e: asyncio.run_coroutine_threadsafe(self.play_next(ctx), self.bot.loop) if not e else print('Player error: %s' % e))
                await ctx.send(f'<:youtube:763374159567781890> **Now Playing:** ``{player.title}``')
            except Exception as e:
                await ctx.send(f"Failed to play next track: {str(e)}")
                await self.play_next(ctx)
        else:
            await ctx.send("Queue is now done!")

    @commands.command()
    async def join(self, ctx):
        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
            await ctx.send(f'Connected to ``{channel}``')
            await channel.connect()

    @commands.command()
    async def play(self, ctx, *, url):
        try:
            if ctx.voice_client.is_playing():
                global queue
                queue.append(url)
                await ctx.send(f'``{url}`` was added to the queue!')
                return
            
            async with ctx.typing():
                player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
                ctx.voice_client.play(player, after=lambda e: asyncio.run_coroutine_threadsafe(self.play_next(ctx), self.bot.loop) if not e else print('Player error: %s' % e))
            await ctx.send(f':mag_right: **Searching for** ``{url}``\n<:youtube:763374159567781890> **Now Playing:** ``{player.title}``')
        except Exception as e:
            await ctx.send(f"Something went wrong - please try again later!\nError: {str(e)}")

    @commands.command()
    async def play_queue(self, ctx):
        global queue
        if len(queue) == 0:
            await ctx.send("Queue is empty!")
            return
        
        if not ctx.voice_client.is_playing():
            await self.play_next(ctx)
        else:
            await ctx.send("Bot is already playing music! Queue will continue after current track.")

    @commands.command()
    async def pause(self, ctx):
        voice = get(self.bot.voice_clients, guild=ctx.guild)
        voice.pause()
        user = ctx.message.author.mention
        await ctx.send(f"Bot was paused by {user}")

    @commands.command()
    async def resume(self, ctx):
        voice = get(self.bot.voice_clients, guild=ctx.guild)
        voice.resume()
        user = ctx.message.author.mention
        await ctx.send(f"Bot was resumed by {user}")

    @commands.command()
    async def add_queue(self, ctx, url):
        global queue
        try:
            queue.append(url)
            user = ctx.message.author.mention
            await ctx.send(f'``{url}`` was added to the queue by {user}!')
        except:
            await ctx.send(f"Couldnt add {url} to the queue!")

    @commands.command()
    async def remove_queue(self, ctx, number):
        global queue
        try:
            del(queue[int(number)])
            if len(queue) < 1:
                await ctx.send("Your queue is empty now!")
            else:
                await ctx.send(f'Your queue is now {queue}')
        except:
            await ctx.send("List index out of range - the queue starts at 0")

    @commands.command()
    async def clear_queue(self, ctx):
        global queue
        queue.clear()
        user = ctx.message.author.mention
        await ctx.send(f"The queue was cleared by {user}")

    @commands.command()
    async def view_queue(self, ctx):
        if len(queue) < 1:
            await ctx.send("The queue is empty - nothing to see here!")
        else:
            await ctx.send(f'Your queue is now {queue}')

    @commands.command()
    async def leave(self, ctx):
        voice_client = ctx.message.guild.voice_client
        user = ctx.message.author.mention
        await voice_client.disconnect()
        await ctx.send(f'Disconnected from {user}')

    @play_queue.before_invoke
    @play.before_invoke
    async def ensure_voice(self, ctx):
        if ctx.voice_client is None:
            if ctx.author.voice:
                await ctx.author.voice.channel.connect()
            else:
                await ctx.send("You are not connected to a voice channel.")
                raise commands.CommandError("Author not connected to a voice channel.")
        # 移除了停止当前播放的逻辑

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

关键修改点总结

  1. 移除了ensure_voice中停止当前播放的代码,改为在play命令中判断是否播放,是则加入队列。
  2. 添加了play_next辅助方法,利用after回调实现队列的自动续播。
  3. 修改了play_queue命令,不再用循环,而是触发第一首播放,后续由回调处理。

这样修改后,你的机器人就能:

  • 播放新曲目时,如果当前有歌曲在播放,自动加入队列等待。
  • 执行play_queue时,依次播放队列中的所有曲目,直到队列清空。

内容的提问来源于stack exchange,提问作者user14200689

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 20:52:36