From cd8ed8bc5793985df2c1692072a932ceee517b95 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 8 Jul 2026 02:13:35 +0200 Subject: OPEN THE RUST GATES Signed-off-by: salmonik --- cogs/music/join.py | 29 ---- cogs/music/loop.py | 40 ------ cogs/music/media_processing.py | 135 ----------------- cogs/music/play_commands.py | 320 ----------------------------------------- cogs/music/stop.py | 29 ---- 5 files changed, 553 deletions(-) delete mode 100644 cogs/music/join.py delete mode 100644 cogs/music/loop.py delete mode 100644 cogs/music/media_processing.py delete mode 100644 cogs/music/play_commands.py delete mode 100644 cogs/music/stop.py (limited to 'cogs/music') diff --git a/cogs/music/join.py b/cogs/music/join.py deleted file mode 100644 index d68ca79..0000000 --- a/cogs/music/join.py +++ /dev/null @@ -1,29 +0,0 @@ -# This file is part of Senator Sperma -# Licensed under the GPL-3.0 License -# Copyright (C) 2026 Salmonik @ salmonik@tuta.io -# https://codeberg.org/salmonik/senator-sperma - -import logging - -from discord.ext import commands -from cogs.voice import get_voice_manager - -logger = logging.getLogger(__name__) - - -class Join(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.slash_command(name="join", description="dołącza do kanalu") - async def join(self, ctx): - voice_mgr = get_voice_manager(self.bot) - channel = ctx.author.voice.channel - await voice_mgr.connect(channel) - await ctx.respond(content="\u200b", ephemeral=True) - await ctx.interaction.delete_original_response() - logger.info("Bot connected to voice channel") - - -def setup(bot): - bot.add_cog(Join(bot)) diff --git a/cogs/music/loop.py b/cogs/music/loop.py deleted file mode 100644 index 6e2c838..0000000 --- a/cogs/music/loop.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file is part of Senator Sperma -# Licensed under the GPL-3.0 License -# Copyright (C) 2026 Salmonik @ salmonik@tuta.io -# https://codeberg.org/salmonik/senator-sperma - -import logging - -from discord.ext import commands - -logger = logging.getLogger(__name__) - - -class Loop(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.looping = {} - - def is_looping(self, guild_id): - return self.looping.get(guild_id, False) - - def set_looping(self, guild_id, value): - self.looping[guild_id] = value - - @commands.slash_command( - name="loop", description="włącz/wyłącz loopowanie dla bieżącej melodyji" - ) - async def loop(self, ctx): - guild_id = ctx.guild.id - looping = not self.is_looping(guild_id) - self.set_looping(guild_id, looping) - await ctx.respond( - f"looping jest teraz {'włączony' if looping else 'wyłączony'}." - ) - logger.info( - f"Looping has been {'enabled' if looping else 'disabled'} for guild {guild_id}" - ) - - -def setup(bot): - bot.add_cog(Loop(bot)) diff --git a/cogs/music/media_processing.py b/cogs/music/media_processing.py deleted file mode 100644 index 91bd406..0000000 --- a/cogs/music/media_processing.py +++ /dev/null @@ -1,135 +0,0 @@ -# This file is part of Senator Sperma -# Licensed under the GPL-3.0 License -# Copyright (C) 2026 Salmonik @ salmonik@tuta.io -# https://codeberg.org/salmonik/senator-sperma - -import asyncio -import logging -import os - -import yt_dlp -from youtubesearchpython import VideosSearch -from cogs.utils.url_helper import clean_url_for_ytdlp - -logger = logging.getLogger(__name__) - - -class MediaDownloader: - """Handles downloading media from various sources.""" - - @staticmethod - async def download(ctx, url): - """Download audio from YouTube URL.""" - from cogs.utils.temp import get_temp_dir - - temp_dir = get_temp_dir("senator_sperma_music") - - ytdl_opts = { - "outtmpl": os.path.join(temp_dir, "%(title)s.%(ext)s"), - "postprocessors": [ - { - "key": "FFmpegExtractAudio", - "preferredcodec": "mp3", - "preferredquality": "192", - } - ], - "force_ipv4": True, - } - - try: - url = clean_url_for_ytdlp(url) - with yt_dlp.YoutubeDL(ytdl_opts) as ydl: - info_dict = ydl.extract_info(url, download=True) - # Get the base filename without extension - base_filename = os.path.splitext(ydl.prepare_filename(info_dict))[0] - # Add .mp3 extension since we're extracting audio - filename = f"{base_filename}.mp3" - logger.info(f"Downloaded file: {filename}") - return filename - except Exception as e: - logger.error(f"Error downloading video: {e}") - await ctx.respond("eror pobirajync wideło", ephemeral=True) - - @staticmethod - async def get_url(ctx, query): - """Search YouTube and get URL for a query.""" - search = VideosSearch(query, limit=1) - if search.result()["result"]: - title = search.result()["result"][0]["title"] - url = search.result()["result"][0]["link"] - logger.info(f"Found video: {title}") - return url, title - else: - logger.warning("No results found for the query.") - await ctx.respond("nic ni ma", ephemeral=True) - return None - - -class AudioCompressor: - """Handles audio compression using FFmpeg.""" - - @staticmethod - async def compress(ctx, filename): - """Compress audio file to opus format.""" - from cogs.utils.temp import get_temp_dir - - if not filename: - await ctx.respond("dzie wideło", ephemeral=True) - return None - - temp_dir = get_temp_dir("senator_sperma_compressed") - - # Create a temporary output filename in the temp directory - base_name = os.path.basename(filename) - output_filename = os.path.join( - temp_dir, f"{os.path.splitext(base_name)[0]}_compressed.opus" - ) - - # Ensure the output directory exists - os.makedirs(os.path.dirname(output_filename) or ".", exist_ok=True) - - # Use proper quoting for filenames with spaces - ffmpeg_command = [ - "ffmpeg", - "-y", # Overwrite output file if it exists - "-i", - filename, - "-ac", - "1", # Mono audio - "-b:a", - "500", # 500k bitrate - "-acodec", - "libopus", - output_filename, - ] - - try: - logger.info(f"Running FFmpeg command: {' '.join(ffmpeg_command)}") - process = await asyncio.create_subprocess_exec( - *ffmpeg_command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - _, stderr = await process.communicate() - - if process.returncode != 0: - error_msg = stderr.decode() - logger.error(f"FFmpeg error ({process.returncode}): {error_msg}") - await ctx.respond("eror kompresujync pilk", ephemeral=True) - return None - - if not os.path.exists(output_filename): - logger.error(f"Output file was not created: {output_filename}") - await ctx.respond( - "eror kompresujync pilk - output file not created", ephemeral=True - ) - return None - - logger.info(f"Successfully compressed file: {output_filename}") - return output_filename - - except Exception as e: - logger.error(f"Error compressing file: {str(e)}", exc_info=True) - await ctx.respond(f"eror kompresujync pilk: {str(e)}", ephemeral=True) - return None diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py deleted file mode 100644 index c6a55b0..0000000 --- a/cogs/music/play_commands.py +++ /dev/null @@ -1,320 +0,0 @@ -# This file is part of Senator Sperma -# Licensed under the GPL-3.0 License -# Copyright (C) 2026 Salmonik @ salmonik@tuta.io -# https://codeberg.org/salmonik/senator-sperma - -import logging -import os -import urllib.parse -import asyncio -from os import PathLike -from typing import Awaitable, Callable, List, Tuple -import re -import requests -from yt_dlp import YoutubeDL - -import discord -from discord.commands import OptionChoice -from discord.ext import commands - -from cogs.music import media_processing -from cogs.utils.file_search import UniversalSearcher -from cogs.utils.settings import get_uploadurl -from cogs.utils.url_helper import clean_url_for_ytdlp -from cogs.voice import get_voice_manager - -logger = logging.getLogger(__name__) - -EXTERNAL_URL_PATTERN = re.compile( - r"^https://(?=.*(?:youtube|youtu\.be|soundcloud|bandcamp))" -) - - -class PlayCommands(commands.Cog): - """Discord slash commands for music playback.""" - - def __init__(self, bot): - from cogs.utils.temp import get_temp_dir - - self.bot = bot - self.temp_dir = get_temp_dir() - self.path_mapping = {} # Maps display names to full paths - - @staticmethod - async def _create_search_embed( - candidates: List[str], - ) -> Tuple[discord.Embed, discord.ui.View]: - """Create an embed and view for search results.""" - embed = discord.Embed( - title="Wyniki wyszukiwania", - description="Wybierz plik do odtworzenia:", - color=discord.Color.blue(), - ) - - view = discord.ui.View(disable_on_timeout=True) - - for i, candidate in enumerate(candidates, start=1): - filename = candidate.split("/")[-1].split("?")[0] - display_name = os.path.splitext(filename)[0] - embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False) - - return embed, view - - async def _handle_search_results( - self, - ctx: discord.ApplicationContext, - candidates: List[str], - play_callback: Callable[[discord.Interaction, str], Awaitable[None]], - ) -> None: - """Handle the display of search results and button interactions.""" - if not candidates: - await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) - return - - embed, view = await self._create_search_embed(candidates) - - # Add buttons for each candidate - for i, candidate in enumerate(candidates, start=1): - button = discord.ui.Button(label=f"Play {i}") - - async def button_callback( - interaction: discord.Interaction, candidate_path=candidate - ): - await interaction.response.defer(ephemeral=True) - await ctx.interaction.delete_original_response() - try: - await play_callback(interaction, candidate_path) - except Exception as e: - logger.error(f"Error in play callback: {str(e)}", exc_info=True) - await interaction.followup.send( - f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True - ) - - button.callback = button_callback - view.add_item(button) - - await ctx.respond(embed=embed, view=view, ephemeral=True) - - async def _search_autocomplete( - self, ctx: discord.AutocompleteContext - ) -> List[OptionChoice]: - """Autocomplete handler for search queries.""" - if ctx.value.lower().startswith(("http://", "https://")): - return [] - - query = ctx.value.lower() - try: - results = await UniversalSearcher.search_copyparty_autocomplete_audio( - query, max_results=25 - ) - choices = [] - self.path_mapping = {} # Reset the mapping for each new query - - for url in results: - filename = url.split("/")[-1] - filename = filename.split("?")[0] # Remove query params if any - filename = urllib.parse.unquote(filename) # Decode URL encoding - display = os.path.splitext(filename)[0] # Remove extension - - display = display.strip() - - if len(display) > 100: - display = display[:99] + "…" - - self.path_mapping[display] = url - choices.append(OptionChoice(display, display)) - - if len(choices) >= 25: - break - - return choices - - except Exception as e: - logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) - return [] - - async def _handle_compressed_file_play( - self, ctx: discord.ApplicationContext, url: str - ) -> None: - """Download from copyparty, compress, then play locally.""" - import aiohttp - - local_path = None - compressed_path = None - try: - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - if response.status != 200: - await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True) - return - - filename = url.split("/")[-1].split("?")[0] - filename = urllib.parse.unquote(filename) - local_path = os.path.join(self.temp_dir, filename) - os.makedirs(self.temp_dir, exist_ok=True) - - loop = asyncio.get_event_loop() - with open(local_path, "wb") as f: - async for chunk in response.content.iter_chunked(8192): - await loop.run_in_executor(None, f.write, chunk) - - compressed_path = await media_processing.AudioCompressor.compress( - ctx, local_path - ) - if not compressed_path: - return - - await get_voice_manager(self.bot).play( - ctx, compressed_path, delete_after_play=True - ) - except Exception as e: - logger.error(f"Error in compressed playback: {str(e)}", exc_info=True) - await ctx.respond(f"Błąd: {str(e)}", ephemeral=True) - finally: - if local_path and os.path.exists(local_path): - try: - os.remove(local_path) - except Exception: - pass - - async def _handle_external_url_play( - self, ctx: discord.ApplicationContext, url: str - ) -> None: - """Download from external URL, upload to copyparty, then play.""" - local_path = None - try: - ytdl_opts = { - "outtmpl": os.path.join(self.temp_dir, "%(title)s.%(ext)s"), - "postprocessors": [ - { - "key": "FFmpegExtractAudio", - "preferredcodec": "mp3", - "preferredquality": "192", - } - ], - "force_ipv4": True, - } - - loop = asyncio.get_event_loop() - url = clean_url_for_ytdlp(url) - with YoutubeDL(ytdl_opts) as ydl: - info_dict = await loop.run_in_executor( - None, lambda: ydl.extract_info(url, download=True) - ) - title = info_dict.get("title", "nieznany") - base_filename = os.path.splitext(ydl.prepare_filename(info_dict))[0] - local_path = f"{base_filename}.mp3" - - if not os.path.exists(local_path): - await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True) - return - - upload_task = asyncio.create_task(self._upload_to_copyparty_async(local_path)) - - await get_voice_manager(self.bot).play( - ctx, local_path, delete_after_play=True - ) - - try: - await asyncio.wait_for(upload_task, timeout=30.0) - except asyncio.TimeoutError: - logger.warning("Upload timed out") - except Exception as e: - logger.error(f"Error downloading external URL: {str(e)}", exc_info=True) - if local_path and os.path.exists(local_path): - try: - os.remove(local_path) - except Exception: - pass - await ctx.respond(f"Błąd pobierania: {str(e)}", ephemeral=True) - - async def _upload_to_copyparty_async(self, file_path: str) -> None: - """Upload file to copyparty asynchronously (fire and forget).""" - try: - logger.info(f"Starting async upload: {file_path}") - baseurl = get_uploadurl() - password = os.getenv("COPYPARTY_PASSWORD", "") - - logger.info(f"Upload - baseurl: {baseurl}, password set: {bool(password)}") - - if baseurl == "http://localhost:3939": - logger.warning("COPYPARTY_URL_UPLOAD not set! Using default") - - with open(file_path, "rb") as f: - content = f.read() - - files = {"f": (os.path.basename(file_path), content, "application/octet-stream")} - headers = {"Cookie": f"cppwd={password}"} - params = {"j": "1", "ck": "b2"} - - logger.info(f"Posting to {baseurl}") - response = requests.post(baseurl, files=files, headers=headers, params=params) - text = response.text - logger.info(f"Upload response: {response.status_code} - {text[:200] if text else 'empty'}") - - if response.status_code in [200, 201]: - result = response.json() - logger.info(f"Uploaded to copyparty: {result.get('fileurl')}") - except Exception as e: - logger.error(f"Async upload failed: {e}", exc_info=True) - - async def _handle_local_file_play( - self, ctx: discord.ApplicationContext, file: str, compressed: bool - ) -> None: - """Handle file playback from copyparty URL.""" - url = self.path_mapping.get(file, file) # Look up URL from display name - - if compressed: - await self._handle_compressed_file_play(ctx, url) - else: - await get_voice_manager(self.bot).play(ctx, url) - - @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") - async def play( - self, - ctx: discord.ApplicationContext, - melodia: str = discord.Option( - str, - "odtwórz plik z serwera bądź strumienia", - autocomplete=_search_autocomplete, - required=False, - ), - file: discord.Attachment = discord.Option( - discord.Attachment, "plik do zagrania", required=False - ), - compressed: bool = discord.Option( - bool, "Czy skompresować dźwięk?", default=False - ), - ): - """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" - await ctx.defer() - - if melodia and file: - await ctx.respond( - "Możesz podać plik z serwera LUB upload, nie oba naraz.", ephemeral=True - ) - return - - if not melodia and not file: - await ctx.respond("Musisz podać plik do odtworzenia.", ephemeral=True) - return - - if file: - file_path: PathLike[str] | str = os.path.join(self.temp_dir, file.filename) - await file.save(file_path) - await get_voice_manager(self.bot).play(ctx, file_path) - else: - try: - if EXTERNAL_URL_PATTERN.match(melodia.lower()): - await self._handle_external_url_play(ctx, melodia) - else: - await self._handle_local_file_play(ctx, melodia, compressed) - except Exception as e: - logger.error(f"Error playing: {str(e)}", exc_info=True) - await ctx.respond( - f"Wystąpił błąd podczas odtworzenia: {str(e)}", ephemeral=True - ) - - -def setup(bot): - bot.add_cog(PlayCommands(bot)) diff --git a/cogs/music/stop.py b/cogs/music/stop.py deleted file mode 100644 index 9c8d5cf..0000000 --- a/cogs/music/stop.py +++ /dev/null @@ -1,29 +0,0 @@ -# This file is part of Senator Sperma -# Licensed under the GPL-3.0 License -# Copyright (C) 2026 Salmonik @ salmonik@tuta.io -# https://codeberg.org/salmonik/senator-sperma - -from discord.ext import commands - -from cogs.voice import get_voice_manager - -import logging - -logger = logging.getLogger(__name__) - - -class Stop(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.slash_command(name="stop", description="stopuj bota") - async def stop(self, ctx): - await ctx.respond(content="\u200b", ephemeral=True) - await ctx.interaction.delete_original_response() - voice_mgr = get_voice_manager(self.bot) - await voice_mgr.disconnect(ctx.guild) - logger.info("Bot disconnected from voice channel") - - -def setup(bot): - bot.add_cog(Stop(bot)) -- cgit v1.2.3