From d135265798da8c2698f363237f4cb74b682f3f9e Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 17 Apr 2026 15:36:12 +0200 Subject: make voice great again Signed-off-by: salmonik --- cogs/music/play_commands.py | 184 +++++++++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 88 deletions(-) (limited to 'cogs/music/play_commands.py') diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py index a06c636..a24169f 100644 --- a/cogs/music/play_commands.py +++ b/cogs/music/play_commands.py @@ -3,88 +3,102 @@ # Copyright (C) 2026 Salmonik @ salmonik@tuta.io # https://codeberg.org/salmonik/senator-sperma +import logging import os import urllib.parse -import discord -from typing import List, Tuple, Callable, Awaitable from os import PathLike +from typing import Awaitable, Callable, List, Tuple + +import discord +from discord.commands import OptionChoice +from discord.ext import commands from cogs.music import audio_playback, media_processing from cogs.utils import file_search as search_utils -from discord.ext import commands -from discord.commands import OptionChoice -import logging logger = logging.getLogger(__name__) + class PlayCommands(commands.Cog): """Discord slash commands for music playback.""" - + def __init__(self, bot): self.bot = bot - self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") + self.temp_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp" + ) self.path_mapping = {} # Maps display names to full paths @staticmethod - async def _create_search_embed(candidates: List[str]) -> Tuple[discord.Embed, discord.ui.View]: + 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() + 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] + 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]] + 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): + + 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) - + 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]: + async def _search_autocomplete( + self, ctx: discord.AutocompleteContext + ) -> List[OptionChoice]: """Autocomplete handler for search queries.""" query = ctx.value.lower() try: - results = await search_utils.UniversalSearcher.search_copyparty_autocomplete_audio(query, max_results=25) + results = await search_utils.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 = 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 @@ -93,7 +107,7 @@ class PlayCommands(commands.Cog): self.path_mapping[display] = url if len(display) > 90: - display = display[:87] + '...' + display = display[:87] + "..." choices.append(OptionChoice(display[:100], display[:100])) @@ -106,25 +120,12 @@ class PlayCommands(commands.Cog): logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) return [] - async def _validate_play_inputs(self, file: str, stream: str, ctx: discord.ApplicationContext) -> bool: - """Validate input parameters for play command.""" - if not file and not stream: - await ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True) - return False - - if file and stream: - await ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True) - return False - - return True - - async def _handle_stream_play(self, ctx: discord.ApplicationContext, stream: str) -> None: - """Handle internet stream playback.""" - await audio_playback.AudioPlaybackManager.play(ctx, stream) - - async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None: + 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: @@ -133,21 +134,25 @@ class PlayCommands(commands.Cog): if response.status != 200: await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True) return - - filename = url.split('/')[-1].split('?')[0] + + 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) - - with open(local_path, 'wb') as f: + + with open(local_path, "wb") as f: async for chunk in response.content.iter_chunked(8192): f.write(chunk) - - compressed_path = await media_processing.AudioCompressor.compress(ctx, local_path) + + compressed_path = await media_processing.AudioCompressor.compress( + ctx, local_path + ) if not compressed_path: return - - await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) + + await audio_playback.AudioPlaybackManager.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) @@ -158,10 +163,12 @@ class PlayCommands(commands.Cog): except Exception: pass - async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: + 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) - + if compressed: await self._handle_compressed_file_play(ctx, url) else: @@ -169,46 +176,47 @@ class PlayCommands(commands.Cog): @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") async def play( - self, - ctx: discord.ApplicationContext, - file: str = discord.Option( - str, - "wyszukaj plik z serwera", - autocomplete=_search_autocomplete, - required=False - ), - stream: str = discord.Option( - str, - "odtwórz stream internetowy (e.g. radio)", - required=False - ), - compressed: bool = discord.Option( - bool, - "Czy skompresować dźwięk?", - default=False - ) + 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 not await self._validate_play_inputs(file, stream, ctx): + + if melodia and file: + await ctx.respond( + "Możesz podać plik z serwera LUB upload, nie oba naraz.", ephemeral=True + ) return - - try: - if stream: - await self._handle_stream_play(ctx, stream) - else: - await self._handle_local_file_play(ctx, file, compressed) - except Exception as e: - logger.error(f"Error playing: {str(e)}", exc_info=True) - await ctx.respond(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True) - @discord.slash_command(name="grajcuj_plik", description="grajcuj plik") - async def play_file(self, ctx: discord.ApplicationContext, attachment: discord.Attachment = discord.Option(discord.Attachment, "co zagrać??", required=True)): - await ctx.defer() - file_path: PathLike[str] | str = os.path.join(self.temp_dir, attachment.filename) - await attachment.save(file_path) - await audio_playback.AudioPlaybackManager.play(ctx, file_path) + 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 audio_playback.AudioPlaybackManager.play(ctx, file_path) + else: + try: + 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)) -- cgit v1.2.3