From 456842d6f1c36d5a04cb4f8821e641be80e077f9 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 23 Mar 2026 17:43:36 +0100 Subject: play music using the server --- cogs/music/play_commands.py | 166 ++++++++++++++++++++++---------------------- 1 file changed, 84 insertions(+), 82 deletions(-) (limited to 'cogs/music') diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py index e3ca4c1..23808d3 100644 --- a/cogs/music/play_commands.py +++ b/cogs/music/play_commands.py @@ -4,8 +4,8 @@ # https://codeberg.org/salmonik/senator-sperma import os +import urllib.parse import discord -import mutagen from typing import List, Tuple, Callable, Awaitable from os import PathLike @@ -37,20 +37,8 @@ class PlayCommands(commands.Cog): view = discord.ui.View(disable_on_timeout=True) for i, candidate in enumerate(candidates, start=1): - filename = os.path.basename(candidate) - title = None - - # Try to get metadata if it's an audio file - if os.path.splitext(filename)[1].lower() in ['.mp3', '.wav', '.opus']: - try: - metadata = mutagen.File(candidate) - if metadata: - title = metadata.get('title', None) - except Exception as e: - logger.debug(f"Could not read metadata for {filename}: {str(e)}") - - # Add field with title if available, otherwise use filename - display_name = title if title else filename + 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 @@ -86,69 +74,46 @@ class PlayCommands(commands.Cog): await ctx.respond(embed=embed, view=view, ephemeral=True) - def _get_display_name_from_metadata(self, path: str, filename: str) -> str: - """Extract display name from audio file metadata.""" - try: - if os.path.splitext(path)[1].lower() in ['.mp3', '.wav', '.opus', '.m4a']: - metadata = mutagen.File(path, easy=True) - - title = metadata.get('title', [filename])[0] - artist = metadata.get('artist', [''])[0] - album_artist = metadata.get('albumartist', [artist])[0] - - if album_artist and title: - return f"{album_artist} - {title}" - elif artist and title: - return f"{artist} - {title}" - elif title: - return title - except Exception as e: - logger.debug(f"Could not read metadata for {filename}: {str(e)}") - - return filename - 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_local_autocomplete(query, max_results=25, filetype="Audio") + results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25) choices = [] self.path_mapping = {} # Reset the mapping for each new query - - for path in results: - filename = os.path.splitext(os.path.basename(path))[0] # Remove extension - display = self._get_display_name_from_metadata(path, filename) - - # Clean up the display name (remove any extra whitespace or brackets) + + 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() - - # Store the mapping (use the cleaned display name as the key) - self.path_mapping[display] = path - - # Truncate display name if too long - if len(display) > 90: # Leave room for potential numbering + + self.path_mapping[display] = url + + if len(display) > 90: display = display[:87] + '...' - + choices.append(OptionChoice(display[:100], display[:100])) - - # Limit to 25 choices (Discord's limit) + if len(choices) >= 25: break - + return choices - + except Exception as e: logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) return [] - def _validate_play_inputs(self, file: str, stream: str, ctx: discord.ApplicationContext) -> bool: + 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: - ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True) + await ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True) return False if file and stream: - ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True) + await ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True) return False return True @@ -157,38 +122,75 @@ class PlayCommands(commands.Cog): """Handle internet stream playback.""" await audio_playback.AudioPlaybackManager.play(ctx, stream) - async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, full_path: str) -> None: + async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None: """Handle compressed file playback with cleanup.""" - compressed_path = await media_processing.AudioCompressor.compress(ctx, full_path) - if not compressed_path: - await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) - return - - try: - await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) - except Exception as e: - # Clean up the compressed file if play fails - if os.path.exists(compressed_path): + is_url = url.startswith(('http://', 'https://')) + + if is_url: + import aiohttp + local_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] + local_path = os.path.join(self.temp_dir, filename) + os.makedirs(self.temp_dir, exist_ok=True) + + 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) + if not compressed_path: + await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) + return + try: - os.remove(compressed_path) - logger.info(f"Cleaned up temporary file after play error: {compressed_path}") - except Exception as cleanup_error: - logger.error(f"Error cleaning up file {compressed_path}: {str(cleanup_error)}") - raise e + await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) + except Exception as e: + if os.path.exists(compressed_path): + try: + os.remove(compressed_path) + except Exception: + pass + raise e + finally: + if os.path.exists(local_path): + try: + os.remove(local_path) + except Exception: + pass + except Exception as e: + logger.error(f"Error downloading file from URL: {str(e)}", exc_info=True) + await ctx.respond(f"Błąd pobierania pliku: {str(e)}", ephemeral=True) + else: + compressed_path = await media_processing.AudioCompressor.compress(ctx, url) + if not compressed_path: + await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) + return + + try: + await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) + except Exception as e: + if os.path.exists(compressed_path): + try: + os.remove(compressed_path) + except Exception: + pass + raise e async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: - """Handle local file playback.""" - full_path = self.path_mapping.get(file, file) - - if not os.path.exists(full_path): - logger.warning(f"File not found: {full_path} (display name: {file})") - await ctx.respond("Wybrany plik nie istnieje.", ephemeral=True) - return + """Handle file playback from copyparty URL.""" + url = self.path_mapping.get(file, file) if compressed: - await self._handle_compressed_file_play(ctx, full_path) + await self._handle_compressed_file_play(ctx, url) else: - await audio_playback.AudioPlaybackManager.play(ctx, full_path) + await audio_playback.AudioPlaybackManager.play(ctx, url) @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") async def play( -- cgit v1.2.3