From 8e6a2c7ef6a76abd3e370a9c1ef84126c08508a8 Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:18:09 +0100 Subject: [PATCH] modified: Dockerfile modified: app.py modified: requirements.txt --- Dockerfile | 4 +- app.py | 240 ----------------------------------------------- requirements.txt | 6 +- 3 files changed, 4 insertions(+), 246 deletions(-) diff --git a/Dockerfile b/Dockerfile index c3cef81..94bfc4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y \ libffi-dev \ - libsodium23 \ - libopus0 \ - ffmpeg \ + libnacl-dev \ python3-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/app.py b/app.py index 9621b79..f16b19b 100644 --- a/app.py +++ b/app.py @@ -8,8 +8,6 @@ import aiomysql import json from datetime import datetime from typing import Optional, List, Dict -import yt_dlp -from functools import partial # Load environment variables load_dotenv() @@ -90,21 +88,6 @@ async def on_ready(): print(f'Bot ID: {bot.user.id}') print(f'Discord.py Version: {discord.__version__}') print('------') - # Voice libs diagnostics - try: - if not discord.opus.is_loaded(): - # Common soname on Debian-based distros - discord.opus.load_opus('libopus.so.0') - print(f"🎧 Opus loaded: {discord.opus.is_loaded()}") - except Exception as e: - print(f"âš ī¸ Could not load Opus: {e}") - try: - import nacl - print(f"🔐 PyNaCl available: {getattr(nacl, '__version__', 'unknown')}") - except Exception as e: - print(f"âš ī¸ PyNaCl import failed: {e}") - cpath = _cookies_path() - print(f"đŸĒ yt-dlp cookies: {'found at ' + cpath if cpath else 'not found'}") # Initialize database await init_database() @@ -1361,229 +1344,6 @@ async def hoi4leaderboard(ctx, game_type: Optional[str] = "standard", limit: Opt except Exception as e: await ctx.send(f"❌ Error getting leaderboard: {str(e)}") -# ===================== -# YouTube Music Player đŸŽĩ -# ===================== - -# FFmpeg options for reconnect/resilience -FFMPEG_BEFORE_OPTS = "-nostdin -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5" -FFMPEG_OPTS = "-vn" - -def _cookies_path() -> Optional[str]: - # Coolify mount default - default_path = "/app/cookie.txt" - p = os.getenv("YTDLP_COOKIES_PATH", default_path) - return p if os.path.exists(p) else None - -def _ytdl_opts() -> dict: - opts = { - "format": "bestaudio/best", - "noplaylist": True, - "default_search": "auto", - "quiet": True, - "nocheckcertificate": True, - "source_address": "0.0.0.0", - "cachedir": False, - } - cpath = _cookies_path() - if cpath: - opts["cookiefile"] = cpath - return opts - -class Track: - def __init__(self, title: str, webpage_url: str, requested_by: discord.Member): - self.title = title - self.webpage_url = webpage_url - self.requested_by = requested_by - -class GuildMusic: - def __init__(self, guild: discord.Guild): - self.guild = guild - self.voice: Optional[discord.VoiceClient] = None - self.queue: asyncio.Queue[Track] = asyncio.Queue() - self.current: Optional[Track] = None - self.player_task: Optional[asyncio.Task] = None - self.volume: float = 0.5 - - async def ensure_voice(self, ctx): - if ctx.author.voice is None or ctx.author.voice.channel is None: - raise commands.CommandError("You must be in a voice channel to use this.") - channel = ctx.author.voice.channel - if self.voice is None or not self.voice.is_connected(): - self.voice = await channel.connect(timeout=15.0, reconnect=True) - else: - if self.voice.channel != channel: - await self.voice.move_to(channel) - - async def enqueue(self, track: Track): - await self.queue.put(track) - if self.player_task is None or self.player_task.done(): - self.player_task = asyncio.create_task(self._player_loop()) - - async def _player_loop(self): - while True: - try: - self.current = await asyncio.wait_for(self.queue.get(), timeout=300.0) - except asyncio.TimeoutError: - # idle timeout: disconnect if alone - if self.voice and self.voice.is_connected(): - try: - await self.voice.disconnect() - except Exception: - pass - self.current = None - break - - # Extract stream url - ytdl = yt_dlp.YoutubeDL(_ytdl_opts()) - info = await asyncio.get_running_loop().run_in_executor(None, lambda: ytdl.extract_info(self.current.webpage_url, download=False)) - if "entries" in info: - info = info["entries"][0] - stream_url = info.get("url") - title = info.get("title") or self.current.title - - # Play via FFmpeg - if not self.voice or not self.voice.is_connected(): - self.current = None - continue - - audio = discord.FFmpegPCMAudio(stream_url, before_options=FFMPEG_BEFORE_OPTS, options=FFMPEG_OPTS) - source = discord.PCMVolumeTransformer(audio, volume=self.volume) - done_evt = asyncio.Event() - loop = asyncio.get_running_loop() - - def after_play(err): - try: - if err: - logging.warning(f"Player error: {err}") - finally: - # Signal to loop that track ended from voice thread - loop.call_soon_threadsafe(done_evt.set) - - self.voice.play(source, after=after_play) - - # Announce now playing in a text channel? Optional: skip spam in prod; leave to /queue - try: - await done_evt.wait() - except Exception: - pass - finally: - self.current = None - - async def stop(self): - # Clear queue and stop current - try: - while not self.queue.empty(): - self.queue.get_nowait() - self.queue.task_done() - except Exception: - pass - if self.voice and self.voice.is_playing(): - self.voice.stop() - - async def leave(self): - await self.stop() - if self.voice and self.voice.is_connected(): - await self.voice.disconnect() - self.voice = None - -MUSIC: Dict[int, GuildMusic] = {} - -def get_music(guild: discord.Guild) -> GuildMusic: - state = MUSIC.get(guild.id) - if not state: - state = GuildMusic(guild) - MUSIC[guild.id] = state - return state - -async def _extract_basic(query: str, requester: discord.Member) -> Track: - ytdl = yt_dlp.YoutubeDL(_ytdl_opts()) - info = await asyncio.get_running_loop().run_in_executor(None, lambda: ytdl.extract_info(query, download=False)) - if "entries" in info: - info = info["entries"][0] - title = info.get("title", query) - webpage_url = info.get("webpage_url", query) - return Track(title=title, webpage_url=webpage_url, requested_by=requester) - -@bot.hybrid_command(name="play", description="Play a YouTube URL or search query in your voice channel") -async def play(ctx: commands.Context, *, query: str): - try: - if not ctx.guild: - await ctx.reply("This command can only be used in a server.") - return - music = get_music(ctx.guild) - await music.ensure_voice(ctx) - track = await _extract_basic(query, ctx.author if isinstance(ctx.author, discord.Member) else None) - await music.enqueue(track) - cookies_note = " with cookies" if _cookies_path() else "" - await ctx.reply(f"â–ļī¸ Queued: **{track.title}**{cookies_note}") - except commands.CommandError as e: - await ctx.reply(f"❌ {e}") - except Exception as e: - await ctx.reply(f"❌ Failed to play: {e}") - -@bot.hybrid_command(name="skip", description="Skip the current track") -async def skip(ctx: commands.Context): - music = get_music(ctx.guild) - if music.voice and music.voice.is_playing(): - music.voice.stop() - await ctx.reply("â­ī¸ Skipped.") - else: - await ctx.reply("Nothing is playing.") - -@bot.hybrid_command(name="pause", description="Pause playback") -async def pause(ctx: commands.Context): - music = get_music(ctx.guild) - if music.voice and music.voice.is_playing(): - music.voice.pause() - await ctx.reply("â¸ī¸ Paused.") - else: - await ctx.reply("Nothing is playing.") - -@bot.hybrid_command(name="resume", description="Resume playback") -async def resume(ctx: commands.Context): - music = get_music(ctx.guild) - if music.voice and music.voice.is_paused(): - music.voice.resume() - await ctx.reply("â–ļī¸ Resumed.") - else: - await ctx.reply("Nothing is paused.") - -@bot.hybrid_command(name="stop", description="Stop playback and clear the queue") -async def stop_cmd(ctx: commands.Context): - music = get_music(ctx.guild) - await music.stop() - await ctx.reply("âšī¸ Stopped and cleared queue.") - -@bot.hybrid_command(name="leave", description="Leave the voice channel") -async def leave(ctx: commands.Context): - music = get_music(ctx.guild) - await music.leave() - await ctx.reply("👋 Disconnected.") - -@bot.hybrid_command(name="queue", description="Show the next few queued tracks") -async def show_queue(ctx: commands.Context): - music = get_music(ctx.guild) - items: List[Track] = [] - try: - # Peek up to 10 items without removing them - n = min(10, music.queue.qsize()) - tmp = [] - for _ in range(n): - item = await music.queue.get() - items.append(item) - tmp.append(item) - for item in tmp: - await music.queue.put(item) - except Exception: - pass - if not items: - await ctx.reply("Queue is empty.") - return - desc = "\n".join(f"â€ĸ {t.title}" for t in items) - embed = discord.Embed(title="đŸŽĩ Queue", description=desc, color=discord.Color.blurple()) - await ctx.reply(embed=embed) - @bot.event async def on_command_error(ctx, error): """Handles command errors""" diff --git a/requirements.txt b/requirements.txt index 3bdd41f..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==2024.08.06 -PyNaCl==1.5.0 \ No newline at end of file +PyMySQL==1.1.0 \ No newline at end of file