diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
| commit | cd8ed8bc5793985df2c1692072a932ceee517b95 (patch) | |
| tree | 42d18a4140259b5c68b4dd05b7fcb398f78b8d7f /cogs/music | |
| parent | Merge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff) | |
| download | senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.tar.gz senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.zip | |
OPEN THE RUST GATES
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
| -rw-r--r-- | cogs/music/join.py | 29 | ||||
| -rw-r--r-- | cogs/music/loop.py | 40 | ||||
| -rw-r--r-- | cogs/music/media_processing.py | 135 | ||||
| -rw-r--r-- | cogs/music/play_commands.py | 320 | ||||
| -rw-r--r-- | cogs/music/stop.py | 29 |
5 files changed, 0 insertions, 553 deletions
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 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import logging | ||
| 7 | |||
| 8 | from discord.ext import commands | ||
| 9 | from cogs.voice import get_voice_manager | ||
| 10 | |||
| 11 | logger = logging.getLogger(__name__) | ||
| 12 | |||
| 13 | |||
| 14 | class Join(commands.Cog): | ||
| 15 | def __init__(self, bot): | ||
| 16 | self.bot = bot | ||
| 17 | |||
| 18 | @commands.slash_command(name="join", description="dołącza do kanalu") | ||
| 19 | async def join(self, ctx): | ||
| 20 | voice_mgr = get_voice_manager(self.bot) | ||
| 21 | channel = ctx.author.voice.channel | ||
| 22 | await voice_mgr.connect(channel) | ||
| 23 | await ctx.respond(content="\u200b", ephemeral=True) | ||
| 24 | await ctx.interaction.delete_original_response() | ||
| 25 | logger.info("Bot connected to voice channel") | ||
| 26 | |||
| 27 | |||
| 28 | def setup(bot): | ||
| 29 | 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 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import logging | ||
| 7 | |||
| 8 | from discord.ext import commands | ||
| 9 | |||
| 10 | logger = logging.getLogger(__name__) | ||
| 11 | |||
| 12 | |||
| 13 | class Loop(commands.Cog): | ||
| 14 | def __init__(self, bot): | ||
| 15 | self.bot = bot | ||
| 16 | self.looping = {} | ||
| 17 | |||
| 18 | def is_looping(self, guild_id): | ||
| 19 | return self.looping.get(guild_id, False) | ||
| 20 | |||
| 21 | def set_looping(self, guild_id, value): | ||
| 22 | self.looping[guild_id] = value | ||
| 23 | |||
| 24 | @commands.slash_command( | ||
| 25 | name="loop", description="włącz/wyłącz loopowanie dla bieżącej melodyji" | ||
| 26 | ) | ||
| 27 | async def loop(self, ctx): | ||
| 28 | guild_id = ctx.guild.id | ||
| 29 | looping = not self.is_looping(guild_id) | ||
| 30 | self.set_looping(guild_id, looping) | ||
| 31 | await ctx.respond( | ||
| 32 | f"looping jest teraz {'włączony' if looping else 'wyłączony'}." | ||
| 33 | ) | ||
| 34 | logger.info( | ||
| 35 | f"Looping has been {'enabled' if looping else 'disabled'} for guild {guild_id}" | ||
| 36 | ) | ||
| 37 | |||
| 38 | |||
| 39 | def setup(bot): | ||
| 40 | 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 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import asyncio | ||
| 7 | import logging | ||
| 8 | import os | ||
| 9 | |||
| 10 | import yt_dlp | ||
| 11 | from youtubesearchpython import VideosSearch | ||
| 12 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 13 | |||
| 14 | logger = logging.getLogger(__name__) | ||
| 15 | |||
| 16 | |||
| 17 | class MediaDownloader: | ||
| 18 | """Handles downloading media from various sources.""" | ||
| 19 | |||
| 20 | @staticmethod | ||
| 21 | async def download(ctx, url): | ||
| 22 | """Download audio from YouTube URL.""" | ||
| 23 | from cogs.utils.temp import get_temp_dir | ||
| 24 | |||
| 25 | temp_dir = get_temp_dir("senator_sperma_music") | ||
| 26 | |||
| 27 | ytdl_opts = { | ||
| 28 | "outtmpl": os.path.join(temp_dir, "%(title)s.%(ext)s"), | ||
| 29 | "postprocessors": [ | ||
| 30 | { | ||
| 31 | "key": "FFmpegExtractAudio", | ||
| 32 | "preferredcodec": "mp3", | ||
| 33 | "preferredquality": "192", | ||
| 34 | } | ||
| 35 | ], | ||
| 36 | "force_ipv4": True, | ||
| 37 | } | ||
| 38 | |||
| 39 | try: | ||
| 40 | url = clean_url_for_ytdlp(url) | ||
| 41 | with yt_dlp.YoutubeDL(ytdl_opts) as ydl: | ||
| 42 | info_dict = ydl.extract_info(url, download=True) | ||
| 43 | # Get the base filename without extension | ||
| 44 | base_filename = os.path.splitext(ydl.prepare_filename(info_dict))[0] | ||
| 45 | # Add .mp3 extension since we're extracting audio | ||
| 46 | filename = f"{base_filename}.mp3" | ||
| 47 | logger.info(f"Downloaded file: {filename}") | ||
| 48 | return filename | ||
| 49 | except Exception as e: | ||
| 50 | logger.error(f"Error downloading video: {e}") | ||
| 51 | await ctx.respond("eror pobirajync wideło", ephemeral=True) | ||
| 52 | |||
| 53 | @staticmethod | ||
| 54 | async def get_url(ctx, query): | ||
| 55 | """Search YouTube and get URL for a query.""" | ||
| 56 | search = VideosSearch(query, limit=1) | ||
| 57 | if search.result()["result"]: | ||
| 58 | title = search.result()["result"][0]["title"] | ||
| 59 | url = search.result()["result"][0]["link"] | ||
| 60 | logger.info(f"Found video: {title}") | ||
| 61 | return url, title | ||
| 62 | else: | ||
| 63 | logger.warning("No results found for the query.") | ||
| 64 | await ctx.respond("nic ni ma", ephemeral=True) | ||
| 65 | return None | ||
| 66 | |||
| 67 | |||
| 68 | class AudioCompressor: | ||
| 69 | """Handles audio compression using FFmpeg.""" | ||
| 70 | |||
| 71 | @staticmethod | ||
| 72 | async def compress(ctx, filename): | ||
| 73 | """Compress audio file to opus format.""" | ||
| 74 | from cogs.utils.temp import get_temp_dir | ||
| 75 | |||
| 76 | if not filename: | ||
| 77 | await ctx.respond("dzie wideło", ephemeral=True) | ||
| 78 | return None | ||
| 79 | |||
| 80 | temp_dir = get_temp_dir("senator_sperma_compressed") | ||
| 81 | |||
| 82 | # Create a temporary output filename in the temp directory | ||
| 83 | base_name = os.path.basename(filename) | ||
| 84 | output_filename = os.path.join( | ||
| 85 | temp_dir, f"{os.path.splitext(base_name)[0]}_compressed.opus" | ||
| 86 | ) | ||
| 87 | |||
| 88 | # Ensure the output directory exists | ||
| 89 | os.makedirs(os.path.dirname(output_filename) or ".", exist_ok=True) | ||
| 90 | |||
| 91 | # Use proper quoting for filenames with spaces | ||
| 92 | ffmpeg_command = [ | ||
| 93 | "ffmpeg", | ||
| 94 | "-y", # Overwrite output file if it exists | ||
| 95 | "-i", | ||
| 96 | filename, | ||
| 97 | "-ac", | ||
| 98 | "1", # Mono audio | ||
| 99 | "-b:a", | ||
| 100 | "500", # 500k bitrate | ||
| 101 | "-acodec", | ||
| 102 | "libopus", | ||
| 103 | output_filename, | ||
| 104 | ] | ||
| 105 | |||
| 106 | try: | ||
| 107 | logger.info(f"Running FFmpeg command: {' '.join(ffmpeg_command)}") | ||
| 108 | process = await asyncio.create_subprocess_exec( | ||
| 109 | *ffmpeg_command, | ||
| 110 | stdout=asyncio.subprocess.PIPE, | ||
| 111 | stderr=asyncio.subprocess.PIPE, | ||
| 112 | ) | ||
| 113 | |||
| 114 | _, stderr = await process.communicate() | ||
| 115 | |||
| 116 | if process.returncode != 0: | ||
| 117 | error_msg = stderr.decode() | ||
| 118 | logger.error(f"FFmpeg error ({process.returncode}): {error_msg}") | ||
| 119 | await ctx.respond("eror kompresujync pilk", ephemeral=True) | ||
| 120 | return None | ||
| 121 | |||
| 122 | if not os.path.exists(output_filename): | ||
| 123 | logger.error(f"Output file was not created: {output_filename}") | ||
| 124 | await ctx.respond( | ||
| 125 | "eror kompresujync pilk - output file not created", ephemeral=True | ||
| 126 | ) | ||
| 127 | return None | ||
| 128 | |||
| 129 | logger.info(f"Successfully compressed file: {output_filename}") | ||
| 130 | return output_filename | ||
| 131 | |||
| 132 | except Exception as e: | ||
| 133 | logger.error(f"Error compressing file: {str(e)}", exc_info=True) | ||
| 134 | await ctx.respond(f"eror kompresujync pilk: {str(e)}", ephemeral=True) | ||
| 135 | 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 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import logging | ||
| 7 | import os | ||
| 8 | import urllib.parse | ||
| 9 | import asyncio | ||
| 10 | from os import PathLike | ||
| 11 | from typing import Awaitable, Callable, List, Tuple | ||
| 12 | import re | ||
| 13 | import requests | ||
| 14 | from yt_dlp import YoutubeDL | ||
| 15 | |||
| 16 | import discord | ||
| 17 | from discord.commands import OptionChoice | ||
| 18 | from discord.ext import commands | ||
| 19 | |||
| 20 | from cogs.music import media_processing | ||
| 21 | from cogs.utils.file_search import UniversalSearcher | ||
| 22 | from cogs.utils.settings import get_uploadurl | ||
| 23 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 24 | from cogs.voice import get_voice_manager | ||
| 25 | |||
| 26 | logger = logging.getLogger(__name__) | ||
| 27 | |||
| 28 | EXTERNAL_URL_PATTERN = re.compile( | ||
| 29 | r"^https://(?=.*(?:youtube|youtu\.be|soundcloud|bandcamp))" | ||
| 30 | ) | ||
| 31 | |||
| 32 | |||
| 33 | class PlayCommands(commands.Cog): | ||
| 34 | """Discord slash commands for music playback.""" | ||
| 35 | |||
| 36 | def __init__(self, bot): | ||
| 37 | from cogs.utils.temp import get_temp_dir | ||
| 38 | |||
| 39 | self.bot = bot | ||
| 40 | self.temp_dir = get_temp_dir() | ||
| 41 | self.path_mapping = {} # Maps display names to full paths | ||
| 42 | |||
| 43 | @staticmethod | ||
| 44 | async def _create_search_embed( | ||
| 45 | candidates: List[str], | ||
| 46 | ) -> Tuple[discord.Embed, discord.ui.View]: | ||
| 47 | """Create an embed and view for search results.""" | ||
| 48 | embed = discord.Embed( | ||
| 49 | title="Wyniki wyszukiwania", | ||
| 50 | description="Wybierz plik do odtworzenia:", | ||
| 51 | color=discord.Color.blue(), | ||
| 52 | ) | ||
| 53 | |||
| 54 | view = discord.ui.View(disable_on_timeout=True) | ||
| 55 | |||
| 56 | for i, candidate in enumerate(candidates, start=1): | ||
| 57 | filename = candidate.split("/")[-1].split("?")[0] | ||
| 58 | display_name = os.path.splitext(filename)[0] | ||
| 59 | embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False) | ||
| 60 | |||
| 61 | return embed, view | ||
| 62 | |||
| 63 | async def _handle_search_results( | ||
| 64 | self, | ||
| 65 | ctx: discord.ApplicationContext, | ||
| 66 | candidates: List[str], | ||
| 67 | play_callback: Callable[[discord.Interaction, str], Awaitable[None]], | ||
| 68 | ) -> None: | ||
| 69 | """Handle the display of search results and button interactions.""" | ||
| 70 | if not candidates: | ||
| 71 | await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) | ||
| 72 | return | ||
| 73 | |||
| 74 | embed, view = await self._create_search_embed(candidates) | ||
| 75 | |||
| 76 | # Add buttons for each candidate | ||
| 77 | for i, candidate in enumerate(candidates, start=1): | ||
| 78 | button = discord.ui.Button(label=f"Play {i}") | ||
| 79 | |||
| 80 | async def button_callback( | ||
| 81 | interaction: discord.Interaction, candidate_path=candidate | ||
| 82 | ): | ||
| 83 | await interaction.response.defer(ephemeral=True) | ||
| 84 | await ctx.interaction.delete_original_response() | ||
| 85 | try: | ||
| 86 | await play_callback(interaction, candidate_path) | ||
| 87 | except Exception as e: | ||
| 88 | logger.error(f"Error in play callback: {str(e)}", exc_info=True) | ||
| 89 | await interaction.followup.send( | ||
| 90 | f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True | ||
| 91 | ) | ||
| 92 | |||
| 93 | button.callback = button_callback | ||
| 94 | view.add_item(button) | ||
| 95 | |||
| 96 | await ctx.respond(embed=embed, view=view, ephemeral=True) | ||
| 97 | |||
| 98 | async def _search_autocomplete( | ||
| 99 | self, ctx: discord.AutocompleteContext | ||
| 100 | ) -> List[OptionChoice]: | ||
| 101 | """Autocomplete handler for search queries.""" | ||
| 102 | if ctx.value.lower().startswith(("http://", "https://")): | ||
| 103 | return [] | ||
| 104 | |||
| 105 | query = ctx.value.lower() | ||
| 106 | try: | ||
| 107 | results = await UniversalSearcher.search_copyparty_autocomplete_audio( | ||
| 108 | query, max_results=25 | ||
| 109 | ) | ||
| 110 | choices = [] | ||
| 111 | self.path_mapping = {} # Reset the mapping for each new query | ||
| 112 | |||
| 113 | for url in results: | ||
| 114 | filename = url.split("/")[-1] | ||
| 115 | filename = filename.split("?")[0] # Remove query params if any | ||
| 116 | filename = urllib.parse.unquote(filename) # Decode URL encoding | ||
| 117 | display = os.path.splitext(filename)[0] # Remove extension | ||
| 118 | |||
| 119 | display = display.strip() | ||
| 120 | |||
| 121 | if len(display) > 100: | ||
| 122 | display = display[:99] + "…" | ||
| 123 | |||
| 124 | self.path_mapping[display] = url | ||
| 125 | choices.append(OptionChoice(display, display)) | ||
| 126 | |||
| 127 | if len(choices) >= 25: | ||
| 128 | break | ||
| 129 | |||
| 130 | return choices | ||
| 131 | |||
| 132 | except Exception as e: | ||
| 133 | logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) | ||
| 134 | return [] | ||
| 135 | |||
| 136 | async def _handle_compressed_file_play( | ||
| 137 | self, ctx: discord.ApplicationContext, url: str | ||
| 138 | ) -> None: | ||
| 139 | """Download from copyparty, compress, then play locally.""" | ||
| 140 | import aiohttp | ||
| 141 | |||
| 142 | local_path = None | ||
| 143 | compressed_path = None | ||
| 144 | try: | ||
| 145 | async with aiohttp.ClientSession() as session: | ||
| 146 | async with session.get(url) as response: | ||
| 147 | if response.status != 200: | ||
| 148 | await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True) | ||
| 149 | return | ||
| 150 | |||
| 151 | filename = url.split("/")[-1].split("?")[0] | ||
| 152 | filename = urllib.parse.unquote(filename) | ||
| 153 | local_path = os.path.join(self.temp_dir, filename) | ||
| 154 | os.makedirs(self.temp_dir, exist_ok=True) | ||
| 155 | |||
| 156 | loop = asyncio.get_event_loop() | ||
| 157 | with open(local_path, "wb") as f: | ||
| 158 | async for chunk in response.content.iter_chunked(8192): | ||
| 159 | await loop.run_in_executor(None, f.write, chunk) | ||
| 160 | |||
| 161 | compressed_path = await media_processing.AudioCompressor.compress( | ||
| 162 | ctx, local_path | ||
| 163 | ) | ||
| 164 | if not compressed_path: | ||
| 165 | return | ||
| 166 | |||
| 167 | await get_voice_manager(self.bot).play( | ||
| 168 | ctx, compressed_path, delete_after_play=True | ||
| 169 | ) | ||
| 170 | except Exception as e: | ||
| 171 | logger.error(f"Error in compressed playback: {str(e)}", exc_info=True) | ||
| 172 | await ctx.respond(f"Błąd: {str(e)}", ephemeral=True) | ||
| 173 | finally: | ||
| 174 | if local_path and os.path.exists(local_path): | ||
| 175 | try: | ||
| 176 | os.remove(local_path) | ||
| 177 | except Exception: | ||
| 178 | pass | ||
| 179 | |||
| 180 | async def _handle_external_url_play( | ||
| 181 | self, ctx: discord.ApplicationContext, url: str | ||
| 182 | ) -> None: | ||
| 183 | """Download from external URL, upload to copyparty, then play.""" | ||
| 184 | local_path = None | ||
| 185 | try: | ||
| 186 | ytdl_opts = { | ||
| 187 | "outtmpl": os.path.join(self.temp_dir, "%(title)s.%(ext)s"), | ||
| 188 | "postprocessors": [ | ||
| 189 | { | ||
| 190 | "key": "FFmpegExtractAudio", | ||
| 191 | "preferredcodec": "mp3", | ||
| 192 | "preferredquality": "192", | ||
| 193 | } | ||
| 194 | ], | ||
| 195 | "force_ipv4": True, | ||
| 196 | } | ||
| 197 | |||
| 198 | loop = asyncio.get_event_loop() | ||
| 199 | url = clean_url_for_ytdlp(url) | ||
| 200 | with YoutubeDL(ytdl_opts) as ydl: | ||
| 201 | info_dict = await loop.run_in_executor( | ||
| 202 | None, lambda: ydl.extract_info(url, download=True) | ||
| 203 | ) | ||
| 204 | title = info_dict.get("title", "nieznany") | ||
| 205 | base_filename = os.path.splitext(ydl.prepare_filename(info_dict))[0] | ||
| 206 | local_path = f"{base_filename}.mp3" | ||
| 207 | |||
| 208 | if not os.path.exists(local_path): | ||
| 209 | await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True) | ||
| 210 | return | ||
| 211 | |||
| 212 | upload_task = asyncio.create_task(self._upload_to_copyparty_async(local_path)) | ||
| 213 | |||
| 214 | await get_voice_manager(self.bot).play( | ||
| 215 | ctx, local_path, delete_after_play=True | ||
| 216 | ) | ||
| 217 | |||
| 218 | try: | ||
| 219 | await asyncio.wait_for(upload_task, timeout=30.0) | ||
| 220 | except asyncio.TimeoutError: | ||
| 221 | logger.warning("Upload timed out") | ||
| 222 | except Exception as e: | ||
| 223 | logger.error(f"Error downloading external URL: {str(e)}", exc_info=True) | ||
| 224 | if local_path and os.path.exists(local_path): | ||
| 225 | try: | ||
| 226 | os.remove(local_path) | ||
| 227 | except Exception: | ||
| 228 | pass | ||
| 229 | await ctx.respond(f"Błąd pobierania: {str(e)}", ephemeral=True) | ||
| 230 | |||
| 231 | async def _upload_to_copyparty_async(self, file_path: str) -> None: | ||
| 232 | """Upload file to copyparty asynchronously (fire and forget).""" | ||
| 233 | try: | ||
| 234 | logger.info(f"Starting async upload: {file_path}") | ||
| 235 | baseurl = get_uploadurl() | ||
| 236 | password = os.getenv("COPYPARTY_PASSWORD", "") | ||
| 237 | |||
| 238 | logger.info(f"Upload - baseurl: {baseurl}, password set: {bool(password)}") | ||
| 239 | |||
| 240 | if baseurl == "http://localhost:3939": | ||
| 241 | logger.warning("COPYPARTY_URL_UPLOAD not set! Using default") | ||
| 242 | |||
| 243 | with open(file_path, "rb") as f: | ||
| 244 | content = f.read() | ||
| 245 | |||
| 246 | files = {"f": (os.path.basename(file_path), content, "application/octet-stream")} | ||
| 247 | headers = {"Cookie": f"cppwd={password}"} | ||
| 248 | params = {"j": "1", "ck": "b2"} | ||
| 249 | |||
| 250 | logger.info(f"Posting to {baseurl}") | ||
| 251 | response = requests.post(baseurl, files=files, headers=headers, params=params) | ||
| 252 | text = response.text | ||
| 253 | logger.info(f"Upload response: {response.status_code} - {text[:200] if text else 'empty'}") | ||
| 254 | |||
| 255 | if response.status_code in [200, 201]: | ||
| 256 | result = response.json() | ||
| 257 | logger.info(f"Uploaded to copyparty: {result.get('fileurl')}") | ||
| 258 | except Exception as e: | ||
| 259 | logger.error(f"Async upload failed: {e}", exc_info=True) | ||
| 260 | |||
| 261 | async def _handle_local_file_play( | ||
| 262 | self, ctx: discord.ApplicationContext, file: str, compressed: bool | ||
| 263 | ) -> None: | ||
| 264 | """Handle file playback from copyparty URL.""" | ||
| 265 | url = self.path_mapping.get(file, file) # Look up URL from display name | ||
| 266 | |||
| 267 | if compressed: | ||
| 268 | await self._handle_compressed_file_play(ctx, url) | ||
| 269 | else: | ||
| 270 | await get_voice_manager(self.bot).play(ctx, url) | ||
| 271 | |||
| 272 | @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") | ||
| 273 | async def play( | ||
| 274 | self, | ||
| 275 | ctx: discord.ApplicationContext, | ||
| 276 | melodia: str = discord.Option( | ||
| 277 | str, | ||
| 278 | "odtwórz plik z serwera bądź strumienia", | ||
| 279 | autocomplete=_search_autocomplete, | ||
| 280 | required=False, | ||
| 281 | ), | ||
| 282 | file: discord.Attachment = discord.Option( | ||
| 283 | discord.Attachment, "plik do zagrania", required=False | ||
| 284 | ), | ||
| 285 | compressed: bool = discord.Option( | ||
| 286 | bool, "Czy skompresować dźwięk?", default=False | ||
| 287 | ), | ||
| 288 | ): | ||
| 289 | """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" | ||
| 290 | await ctx.defer() | ||
| 291 | |||
| 292 | if melodia and file: | ||
| 293 | await ctx.respond( | ||
| 294 | "Możesz podać plik z serwera LUB upload, nie oba naraz.", ephemeral=True | ||
| 295 | ) | ||
| 296 | return | ||
| 297 | |||
| 298 | if not melodia and not file: | ||
| 299 | await ctx.respond("Musisz podać plik do odtworzenia.", ephemeral=True) | ||
| 300 | return | ||
| 301 | |||
| 302 | if file: | ||
| 303 | file_path: PathLike[str] | str = os.path.join(self.temp_dir, file.filename) | ||
| 304 | await file.save(file_path) | ||
| 305 | await get_voice_manager(self.bot).play(ctx, file_path) | ||
| 306 | else: | ||
| 307 | try: | ||
| 308 | if EXTERNAL_URL_PATTERN.match(melodia.lower()): | ||
| 309 | await self._handle_external_url_play(ctx, melodia) | ||
| 310 | else: | ||
| 311 | await self._handle_local_file_play(ctx, melodia, compressed) | ||
| 312 | except Exception as e: | ||
| 313 | logger.error(f"Error playing: {str(e)}", exc_info=True) | ||
| 314 | await ctx.respond( | ||
| 315 | f"Wystąpił błąd podczas odtworzenia: {str(e)}", ephemeral=True | ||
| 316 | ) | ||
| 317 | |||
| 318 | |||
| 319 | def setup(bot): | ||
| 320 | 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 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | from discord.ext import commands | ||
| 7 | |||
| 8 | from cogs.voice import get_voice_manager | ||
| 9 | |||
| 10 | import logging | ||
| 11 | |||
| 12 | logger = logging.getLogger(__name__) | ||
| 13 | |||
| 14 | |||
| 15 | class Stop(commands.Cog): | ||
| 16 | def __init__(self, bot): | ||
| 17 | self.bot = bot | ||
| 18 | |||
| 19 | @commands.slash_command(name="stop", description="stopuj bota") | ||
| 20 | async def stop(self, ctx): | ||
| 21 | await ctx.respond(content="\u200b", ephemeral=True) | ||
| 22 | await ctx.interaction.delete_original_response() | ||
| 23 | voice_mgr = get_voice_manager(self.bot) | ||
| 24 | await voice_mgr.disconnect(ctx.guild) | ||
| 25 | logger.info("Bot disconnected from voice channel") | ||
| 26 | |||
| 27 | |||
| 28 | def setup(bot): | ||
| 29 | bot.add_cog(Stop(bot)) | ||
