diff --git a/app.py b/app.py index a881925..a02b4f2 100644 --- a/app.py +++ b/app.py @@ -142,6 +142,15 @@ def get_guild_music(guild_id: int) -> GuildMusic: music_states[guild_id] = st return st +async def maybe_defer(ctx: commands.Context): + """Defer interaction responses to avoid 'interaction failed' on long ops (>3s).""" + try: + inter = getattr(ctx, 'interaction', None) + if inter and not inter.response.is_done(): + await inter.response.defer(thinking=True) + except Exception: + pass + def ytdlp_opts() -> dict: opts = { 'format': 'bestaudio/best', @@ -227,20 +236,23 @@ async def start_player_loop(ctx: commands.Context): @bot.hybrid_command(name='join', description='Join your voice channel') async def cmd_join(ctx): + await maybe_defer(ctx) vc = await ensure_voice(ctx) if vc: - await ctx.reply(f"✅ Joined {vc.channel.mention}") + await _safe_send(ctx, f"✅ Joined {vc.channel.mention}") @bot.hybrid_command(name='leave', description='Leave the voice channel') async def cmd_leave(ctx): + await maybe_defer(ctx) if ctx.voice_client and ctx.voice_client.is_connected(): await ctx.voice_client.disconnect() - await ctx.reply("👋 Disconnected.") + await _safe_send(ctx, "👋 Disconnected.") else: - await ctx.reply("ℹ️ Not connected.") + await _safe_send(ctx, "ℹ️ Not connected.") @bot.hybrid_command(name='play', description='Play a YouTube URL or search term') async def cmd_play(ctx, *, query: str): + await maybe_defer(ctx) vc = await ensure_voice(ctx) if not vc: return @@ -248,21 +260,23 @@ async def cmd_play(ctx, *, query: str): song = await yt_extract(query) state = get_guild_music(ctx.guild.id) await state.queue.put(song) - await ctx.reply(f"▶️ Queued: **{song.title}**") + await _safe_send(ctx, f"▶️ Queued: **{song.title}**") await start_player_loop(ctx) except Exception as e: - await ctx.reply(f"❌ Failed to add: {e}") + await _safe_send(ctx, f"❌ Failed to add: {e}") @bot.hybrid_command(name='skip', description='Skip the current track') async def cmd_skip(ctx): + await maybe_defer(ctx) if ctx.voice_client and ctx.voice_client.is_playing(): ctx.voice_client.stop() - await ctx.reply("⏭️ Skipped.") + await _safe_send(ctx, "⏭️ Skipped.") else: - await ctx.reply("ℹ️ Nothing is playing.") + await _safe_send(ctx, "ℹ️ Nothing is playing.") @bot.hybrid_command(name='stop', description='Stop playback and clear the queue') async def cmd_stop(ctx): + await maybe_defer(ctx) state = get_guild_music(ctx.guild.id) # Clear queue try: @@ -272,38 +286,42 @@ async def cmd_stop(ctx): pass if ctx.voice_client and ctx.voice_client.is_playing(): ctx.voice_client.stop() - await ctx.reply("⏹️ Stopped and cleared queue.") + await _safe_send(ctx, "⏹️ Stopped and cleared queue.") @bot.hybrid_command(name='pause', description='Pause playback') async def cmd_pause(ctx): + await maybe_defer(ctx) if ctx.voice_client and ctx.voice_client.is_playing(): ctx.voice_client.pause() - await ctx.reply("⏸️ Paused.") + await _safe_send(ctx, "⏸️ Paused.") else: - await ctx.reply("ℹ️ Nothing is playing.") + await _safe_send(ctx, "ℹ️ Nothing is playing.") @bot.hybrid_command(name='resume', description='Resume playback') async def cmd_resume(ctx): + await maybe_defer(ctx) if ctx.voice_client and ctx.voice_client.is_paused(): ctx.voice_client.resume() - await ctx.reply("▶️ Resumed.") + await _safe_send(ctx, "▶️ Resumed.") else: - await ctx.reply("ℹ️ Nothing is paused.") + await _safe_send(ctx, "ℹ️ Nothing is paused.") @bot.hybrid_command(name='np', description='Show the currently playing track') async def cmd_nowplaying(ctx): + await maybe_defer(ctx) state = get_guild_music(ctx.guild.id) if state.current: dur = f" ({state.current.duration//60}:{state.current.duration%60:02d})" if state.current.duration else "" - await ctx.reply(f"🎶 Now playing: **{state.current.title}**{dur}\n{state.current.webpage_url}") + await _safe_send(ctx, f"🎶 Now playing: **{state.current.title}**{dur}\n{state.current.webpage_url}") else: - await ctx.reply("ℹ️ Nothing is playing.") + await _safe_send(ctx, "ℹ️ Nothing is playing.") @bot.hybrid_command(name='queue', description='Show queued tracks') async def cmd_queue(ctx): + await maybe_defer(ctx) state = get_guild_music(ctx.guild.id) if state.queue.empty(): - await ctx.reply("🗒️ Queue is empty.") + await _safe_send(ctx, "🗒️ Queue is empty.") return # Snapshot queue without draining items: List[Song] = [] @@ -320,10 +338,11 @@ async def cmd_queue(ctx): if more > 0: desc += f"\n...and {more} more" embed = discord.Embed(title="🎼 Queue", description=desc, color=discord.Color.purple()) - await ctx.reply(embed=embed) + await _safe_send(ctx, embed=embed) @bot.hybrid_command(name='voicediag', description='Diagnose voice playback environment') async def cmd_voicediag(ctx): + await maybe_defer(ctx) details = [] details.append(f"discord.py: {discord.__version__}") # PyNaCl check @@ -353,7 +372,7 @@ async def cmd_voicediag(ctx): details.append(f"cookie.txt present: {os.path.exists(COOKIE_FILE)} at {COOKIE_FILE}") embed = discord.Embed(title='Voice Diagnostics', description='\n'.join(details), color=discord.Color.dark_grey()) - await ctx.reply(embed=embed) + await _safe_send(ctx, embed=embed) @bot.event async def on_guild_join(guild):