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
相关产品推荐
相关产品推荐

