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

Python Discord音乐机器人:播放中途跳至下一曲问题排查

Discord音乐机器人中途跳歌问题排查与修复

核心问题分析

你的机器人偶尔出现中途跳歌,主要是以下几个逻辑漏洞导致:

  • 未处理FFmpeg播放异常:当流媒体遇到网络波动、YouTube临时限流等问题时,after回调会收到错误参数,但原代码直接跳过错误处理,触发下一曲导致跳歌。
  • 重复连接语音频道:play命令每次都尝试调用connect(),即使机器人已在目标频道,会引发语音客户端状态冲突。
  • ctx对象跨线程失效风险:after回调通过run_coroutine_threadsafe传递的ctx可能已过期(如原消息上下文被清理),引发隐性错误。
  • 队列操作无线程安全保护:queues在asyncio主线程和after回调线程中被同时操作,可能导致队列元素丢失或异常。

针对性修复方案

1. 处理FFmpeg播放错误

在after回调中传入错误参数,只有无错误时才继续播放下一曲,同时记录错误日志:

# 替换play和play_next中的after回调逻辑
after=lambda e: asyncio.run_coroutine_threadsafe(play_next(ctx, error=e), client.loop)

2. 避免重复连接语音频道

先检查机器人是否已在频道,仅未连接时才执行连接操作:

# 替换play命令中的语音连接逻辑
voice_client = ctx.guild.voice_client
if not voice_client:
    if ctx.author.voice:
        voice_channel = ctx.author.voice.channel
        voice_client = await voice_channel.connect()
        voice_clients[voice_client.guild.id] = voice_client
    else:
        await ctx.channel.send("请先加入语音频道")
        return
elif voice_client.channel != ctx.author.voice.channel:
    await ctx.channel.send("我已经在其他语音频道了")
    return

3. 队列操作添加线程安全锁

用asyncio.Lock保护队列的读写,避免多线程冲突:

# 全局定义队列锁
queue_locks = {}

# 入队时加锁
async def queue(ctx, search_query, title):
    guild_id = ctx.guild.id
    if guild_id not in queue_locks:
        queue_locks[guild_id] = asyncio.Lock()
    async with queue_locks[guild_id]:
        if guild_id not in queues:
            queues[guild_id] = []
        queues[guild_id].append((search_query, title))
    await ctx.channel.send(f"已添加到队列: {title}")

# 出队时加锁
async def play_next(ctx, error=None):
    guild_id = ctx.guild.id
    if guild_id not in queue_locks:
        queue_locks[guild_id] = asyncio.Lock()
    async with queue_locks[guild_id]:
        if not queues.get(guild_id) or len(queues[guild_id]) == 0:
            await ctx.send("队列已空")
            return
        search_query = queues[guild_id].pop(0)[0]
    # 后续播放逻辑...

4. 增加资源提取错误处理

在ytdl提取流媒体URL时捕获异常,避免提取失败直接跳歌:

# 替换play和play_next中的资源提取逻辑
try:
    data = await loop.run_in_executor(None, lambda: ytdl.extract_info(search_query, download=False))
    if not data or not data.get("url"):
        await ctx.channel.send(f"无法提取歌曲资源: {search_query}")
        await play_next(ctx)  # 跳过错误歌曲,继续下一曲
        return
except Exception as e:
    print(f"提取资源出错: {e}")
    await ctx.channel.send(f"提取歌曲资源时出错: {str(e)}")
    await play_next(ctx)
    return

修改后的完整代码片段

@client.command(name="p")
async def play(ctx, *, search_query: str):
    global bot_disconnect
    try:
        # 检查并连接语音频道(避免重复连接)
        voice_client = ctx.guild.voice_client
        if not voice_client:
            if ctx.author.voice:
                voice_channel = ctx.author.voice.channel
                voice_client = await voice_channel.connect()
                voice_clients[voice_client.guild.id] = voice_client
            else:
                await ctx.channel.send("请先加入语音频道")
                return
        elif voice_client.channel != ctx.author.voice.channel:
            await ctx.channel.send("我已经在其他语音频道了")
            return

        # 提取歌曲信息
        if is_youtube_url(search_query):
            loop = asyncio.get_event_loop()
            try:
                data = await loop.run_in_executor(None, lambda: ytdl.extract_info(search_query, download=False))
                if not data or not data.get("url"):
                    await ctx.channel.send(f"无法提取歌曲资源: {search_query}")
                    return
                video_url = data["webpage_url"]
                song_url = data["url"]
                title = data["title"]
            except Exception as e:
                print(f"提取资源出错: {e}")
                await ctx.channel.send(f"提取歌曲资源时出错: {str(e)}")
                return
        else:
            url = f"ytsearch:{search_query}"
            loop = asyncio.get_event_loop()
            try:
                data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=False))
                if not data or not data.get("entries") or len(data["entries"]) == 0:
                    await ctx.channel.send(f"未找到相关歌曲: {search_query}")
                    return
                entry = data['entries'][0]
                video_url = entry["webpage_url"]
                song_url = entry["url"]
                title = entry["title"]
            except Exception as e:
                print(f"搜索歌曲出错: {e}")
                await ctx.channel.send(f"搜索歌曲时出错: {str(e)}")
                return

        # 处理播放或队列
        if voice_client.is_playing():
            await queue(ctx, search_query=video_url, title=title)
        else:
            try:
                player = discord.FFmpegOpusAudio(song_url, **ffmpeg_options)
                await ctx.channel.send(f"正在播放: {video_url} \n[下载链接]({song_url})")
                bot_disconnect = False
                # 传递错误参数给after回调
                voice_client.play(player, after=lambda e: asyncio.run_coroutine_threadsafe(play_next(ctx, error=e), client.loop))
            except Exception as e:
                print(f"播放初始化出错: {e}")
                await ctx.channel.send(f"无法开始播放: {str(e)}")
    except Exception as e:
        print(e)

# 全局队列锁
queue_locks = {}

async def queue(ctx, search_query, title):
    guild_id = ctx.guild.id
    if guild_id not in queue_locks:
        queue_locks[guild_id] = asyncio.Lock()
    async with queue_locks[guild_id]:
        if guild_id not in queues:
            queues[guild_id] = []
        queues[guild_id].append((search_query, title))
    await ctx.channel.send(f"已添加到队列: {title}")

async def play_next(ctx, error=None):
    guild_id = ctx.guild.id
    # 处理播放错误
    if error:
        print(f"播放出错: {error}")
        await ctx.channel.send(f"播放过程中出现错误: {str(error)}")
    
    # 线程安全取队列
    if guild_id not in queue_locks:
        queue_locks[guild_id] = asyncio.Lock()
    async with queue_locks[guild_id]:
        if not queues.get(guild_id) or len(queues[guild_id]) == 0:
            await ctx.send("队列已空")
            return
        search_query = queues[guild_id].pop(0)[0]
    
    # 提取下一曲资源
    loop = asyncio.get_event_loop()
    try:
        data = await loop.run_in_executor(None, lambda: ytdl.extract_info(search_query, download=False))
        if not data or not data.get("url"):
            await ctx.channel.send(f"无法提取队列中的歌曲资源: {search_query}")
            await play_next(ctx)  # 继续下一曲
            return
        player = discord.FFmpegOpusAudio(data["url"], **ffmpeg_options)
        voice_client = voice_clients.get(guild_id)
        if not voice_client or not voice_client.is_connected():
            await ctx.send("我已退出语音频道,停止播放队列")
            return
        voice_client.play(player, after=lambda e: asyncio.run_coroutine_threadsafe(play_next(ctx, error=e), client.loop))
        await ctx.channel.send(f"正在播放: {data['webpage_url']} \n[下载链接]({data['url']})")
    except Exception as e:
        print(f"队列播放出错: {e}")
        await ctx.channel.send(f"播放队列歌曲时出错: {str(e)}")
        await play_next(ctx)  # 跳过错误歌曲,继续下一曲

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 20:14:54