From 6bc698487654db0e4f416413a6aaf1779251395a Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:04:02 +0100 Subject: [PATCH] modified: Dockerfile modified: app.py modified: requirements.txt --- Dockerfile | 2 - app.py | 306 +---------------------------------------------- requirements.txt | 6 +- 3 files changed, 9 insertions(+), 305 deletions(-) diff --git a/Dockerfile b/Dockerfile index 56eecb8..94bfc4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,6 @@ WORKDIR /app RUN apt-get update && apt-get install -y \ libffi-dev \ libnacl-dev \ - libopus0 \ - ffmpeg \ python3-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/app.py b/app.py index a02b4f2..f16b19b 100644 --- a/app.py +++ b/app.py @@ -7,8 +7,7 @@ from dotenv import load_dotenv import aiomysql import json from datetime import datetime -from typing import Optional, List, Dict, Tuple -import yt_dlp +from typing import Optional, List, Dict # Load environment variables load_dotenv() @@ -79,7 +78,6 @@ intents = discord.Intents.default() intents.message_content = True intents.guilds = True intents.members = True -intents.voice_states = True bot = commands.Bot(command_prefix='!', intents=intents) @@ -93,12 +91,6 @@ async def on_ready(): # Initialize database await init_database() - # Preload opus if available (voice) - try: - if not discord.opus.is_loaded(): - discord.opus.load_opus('libopus') - except Exception: - pass # Set bot status await bot.change_presence( @@ -113,267 +105,6 @@ async def on_ready(): except Exception as e: print(f'Failed to sync commands: {e}') -# ========================= -# YouTube/Voice Player -# ========================= - -COOKIE_FILE = os.getenv('YT_COOKIES_PATH', '/app/cookie.txt') - -class Song: - def __init__(self, title: str, url: str, webpage_url: str, duration: Optional[int]): - self.title = title - self.url = url - self.webpage_url = webpage_url - self.duration = duration - -class GuildMusic: - def __init__(self): - self.queue: asyncio.Queue[Song] = asyncio.Queue() - self.current: Optional[Song] = None - self.play_next = asyncio.Event() - self.player_task: Optional[asyncio.Task] = None - -music_states: Dict[int, GuildMusic] = {} - -def get_guild_music(guild_id: int) -> GuildMusic: - st = music_states.get(guild_id) - if not st: - st = 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', - 'noplaylist': True, - 'quiet': True, - 'no_warnings': True, - 'default_search': 'auto', - 'nocheckcertificate': True, - 'source_address': '0.0.0.0', - } - try: - if os.path.exists(COOKIE_FILE): - opts['cookiefile'] = COOKIE_FILE - except Exception: - pass - return opts - -async def yt_extract(query: str) -> Song: - loop = asyncio.get_event_loop() - def _extract() -> Tuple[str, str, str, Optional[int]]: - with yt_dlp.YoutubeDL(ytdlp_opts()) as ydl: - info = ydl.extract_info(query, download=False) - if 'entries' in info: - info = info['entries'][0] - title = info.get('title') - url = info.get('url') - webpage_url = info.get('webpage_url') or info.get('original_url') or query - duration = info.get('duration') - return title, url, webpage_url, duration - title, url, webpage_url, duration = await loop.run_in_executor(None, _extract) - return Song(title, url, webpage_url, duration) - -FFMPEG_BEFORE_OPTS = '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5' -FFMPEG_OPTS = '-vn' - -async def ensure_voice(ctx: commands.Context) -> Optional[discord.VoiceClient]: - if not ctx.author or not getattr(ctx.author, 'voice', None) or not ctx.author.voice: - await ctx.reply("❌ You need to be in a voice channel.") - return None - channel = ctx.author.voice.channel - if not channel: - await ctx.reply("❌ Can't find your voice channel.") - return None - if ctx.voice_client is None: - try: - return await channel.connect(timeout=30, reconnect=True, self_deaf=True) - except Exception as e: - await ctx.reply(f"❌ Failed to join: {e}") - return None - elif ctx.voice_client.channel != channel: - try: - await ctx.voice_client.move_to(channel) - except Exception as e: - await ctx.reply(f"❌ Failed to move: {e}") - return None - return ctx.voice_client - -async def start_player_loop(ctx: commands.Context): - guild = ctx.guild - if not guild: - return - state = get_guild_music(guild.id) - if state.player_task and not state.player_task.done(): - return - - async def runner(): - while True: - state.play_next.clear() - try: - song = await asyncio.wait_for(state.queue.get(), timeout=300) - except asyncio.TimeoutError: - # idle for 5 minutes, disconnect - if ctx.voice_client and ctx.voice_client.is_connected(): - await ctx.voice_client.disconnect() - break - state.current = song - source = discord.FFmpegPCMAudio(song.url, before_options=FFMPEG_BEFORE_OPTS, options=FFMPEG_OPTS) - ctx.voice_client.play(source, after=lambda e: state.play_next.set()) - await state.play_next.wait() - state.current = None - - state.player_task = asyncio.create_task(runner()) - -@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 _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 _safe_send(ctx, "👋 Disconnected.") - else: - 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 - try: - song = await yt_extract(query) - state = get_guild_music(ctx.guild.id) - await state.queue.put(song) - await _safe_send(ctx, f"▶️ Queued: **{song.title}**") - await start_player_loop(ctx) - except Exception as 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 _safe_send(ctx, "⏭️ Skipped.") - else: - 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: - while True: - state.queue.get_nowait() - except asyncio.QueueEmpty: - pass - if ctx.voice_client and ctx.voice_client.is_playing(): - ctx.voice_client.stop() - 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 _safe_send(ctx, "⏸️ Paused.") - else: - 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 _safe_send(ctx, "▶️ Resumed.") - else: - 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 _safe_send(ctx, f"🎶 Now playing: **{state.current.title}**{dur}\n{state.current.webpage_url}") - else: - 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 _safe_send(ctx, "🗒️ Queue is empty.") - return - # Snapshot queue without draining - items: List[Song] = [] - try: - while True: - items.append(state.queue.get_nowait()) - except asyncio.QueueEmpty: - pass - # Put them back - for s in items: - await state.queue.put(s) - desc = "\n".join([f"{i+1}. {s.title}" for i, s in enumerate(items[:10])]) - more = state.queue.qsize() - len(items[:10]) - if more > 0: - desc += f"\n...and {more} more" - embed = discord.Embed(title="🎼 Queue", description=desc, color=discord.Color.purple()) - 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 - try: - import nacl - details.append(f"PyNaCl: {getattr(nacl, '__version__', 'present')}") - except Exception as e: - details.append(f"PyNaCl: missing ({e})") - # Opus check - try: - loaded = discord.opus.is_loaded() - details.append(f"Opus loaded: {loaded}") - except Exception as e: - details.append(f"Opus check failed: {e}") - # FFmpeg check - ff = "unknown" - try: - proc = await asyncio.create_subprocess_exec('ffmpeg', '-version', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) - out, _ = await proc.communicate() - ff = out.decode(errors='ignore').splitlines()[0][:120] - except FileNotFoundError: - ff = "not found" - except Exception as e: - ff = f"error: {e}" - details.append(f"ffmpeg: {ff}") - # Cookie file - 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 _safe_send(ctx, embed=embed) - @bot.event async def on_guild_join(guild): """Event triggered when the bot joins a server""" @@ -1617,14 +1348,14 @@ async def hoi4leaderboard(ctx, game_type: Optional[str] = "standard", limit: Opt async def on_command_error(ctx, error): """Handles command errors""" if isinstance(error, commands.CheckFailure): - await _safe_send(ctx, "❌ You don't have permission to use this command!") + await ctx.send("❌ You don't have permission to use this command!") elif isinstance(error, commands.CommandNotFound): # Silently ignore command not found errors pass elif isinstance(error, commands.MissingRequiredArgument): - await _safe_send(ctx, f"❌ Missing arguments! Command: `{ctx.command}`") + await ctx.send(f"❌ Missing arguments! Command: `{ctx.command}`") elif isinstance(error, commands.BadArgument): - await _safe_send(ctx, "❌ Invalid argument!") + await ctx.send("❌ Invalid argument!") else: # Log detailed error information print(f"❌ Unknown error in command '{ctx.command}': {type(error).__name__}: {error}") @@ -1633,34 +1364,9 @@ async def on_command_error(ctx, error): # Send detailed error to user if owner if ctx.author.id == OWNER_ID: - await _safe_send(ctx, f"❌ **Error Details (Owner only):**\n```python\n{type(error).__name__}: {str(error)[:1800]}\n```") + await ctx.send(f"❌ **Error Details (Owner only):**\n```python\n{type(error).__name__}: {str(error)[:1800]}\n```") else: - await _safe_send(ctx, "❌ An unknown error occurred!") - -async def _safe_send(ctx: commands.Context, content: str = None, **kwargs): - """Send a message safely for both message and slash contexts, even if the interaction timed out.""" - try: - if getattr(ctx, 'interaction', None): - # If we have an interaction, try normal response first - interaction = ctx.interaction - if not interaction.response.is_done(): - await interaction.response.send_message(content=content, **kwargs) - return - # Otherwise use followup - await interaction.followup.send(content=content, **kwargs) - return - # Fallback to classic send - await ctx.send(content=content, **kwargs) - except discord.NotFound: - # Interaction unknown/expired, try channel.send - try: - channel = getattr(ctx, 'channel', None) - if channel: - await channel.send(content=content, **kwargs) - except Exception: - pass - except Exception: - pass + await ctx.send("❌ An unknown error occurred!") async def main(): """Main function to start the bot""" diff --git a/requirements.txt b/requirements.txt index 834ad38..90db89a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ discord.py==2.3.2 python-dotenv==1.0.0 aiohttp==3.9.1 +asyncpg==0.29.0 +psycopg2-binary==2.9.9 aiomysql==0.2.0 -PyMySQL==1.1.0 -yt-dlp==2025.1.26 -PyNaCl==1.5.0 \ No newline at end of file +PyMySQL==1.1.0 \ No newline at end of file