From 18ea01dc33cf5ef8402f4ef9193801bf7288e106 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 28 Mar 2026 00:17:18 +0100 Subject: make it work + visual changes --- cogs/music/audio_playback.py | 14 +++++-- cogs/music/play_commands.py | 91 ++++++++++++++++---------------------------- cogs/utils/file_search.py | 35 +++++++++++++++-- 3 files changed, 75 insertions(+), 65 deletions(-) (limited to 'cogs') diff --git a/cogs/music/audio_playback.py b/cogs/music/audio_playback.py index 3d4f453..d23809a 100644 --- a/cogs/music/audio_playback.py +++ b/cogs/music/audio_playback.py @@ -5,8 +5,7 @@ import os import logging -from pathlib import Path - +import urllib.parse import discord import mutagen @@ -74,13 +73,17 @@ class AudioPlaybackManager: async def prepare_audio_source(source, is_url, display_name): """Prepare audio source for playback. Returns (source, final_display_name).""" if is_url: - # For URLs, use FFmpeg with the direct URL + # For URLs, use FFmpeg with the direct URL and extract clean filename ffmpeg_options = { 'options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5' } audio_source = discord.FFmpegPCMAudio(source, **ffmpeg_options) - return audio_source, display_name + # Extract clean filename for URL (remove extension and decode URL encoding) + filename = os.path.basename(source.split('?')[0]) # Remove query parameters + filename = urllib.parse.unquote(filename) # Decode URL encoding + clean_filename = os.path.splitext(filename)[0] # Remove file extension + return audio_source, clean_filename else: # For local files, use default options audio_source = discord.FFmpegPCMAudio(source=source) @@ -93,8 +96,11 @@ class AudioPlaybackManager: if title: display_name = title logger.debug(f"Read metadata - title: {title}") + else: + display_name = os.path.splitext(os.path.basename(source))[0] except Exception as e: logger.warning(f"Could not read metadata from {source}: {str(e)}") + display_name = os.path.splitext(os.path.basename(source))[0] return audio_source, display_name diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py index 23808d3..a06c636 100644 --- a/cogs/music/play_commands.py +++ b/cogs/music/play_commands.py @@ -78,7 +78,7 @@ class PlayCommands(commands.Cog): """Autocomplete handler for search queries.""" query = ctx.value.lower() try: - results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(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 @@ -123,65 +123,40 @@ class PlayCommands(commands.Cog): await audio_playback.AudioPlaybackManager.play(ctx, stream) async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None: - """Handle compressed file playback with cleanup.""" - 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 + """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 - 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 - 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) + 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: + 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: - 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 + 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) + finally: + if local_path and os.path.exists(local_path): + try: + os.remove(local_path) + except Exception: + pass async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: """Handle file playback from copyparty URL.""" @@ -209,14 +184,14 @@ class PlayCommands(commands.Cog): ), compressed: bool = discord.Option( bool, - "Czy skompresować dźwięk? (tylko dla plików lokalnych)", + "Czy skompresować dźwięk?", default=False ) ): """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" await ctx.defer() - if not self._validate_play_inputs(file, stream, ctx): + if not await self._validate_play_inputs(file, stream, ctx): return try: diff --git a/cogs/utils/file_search.py b/cogs/utils/file_search.py index 8d3e5d6..7100f83 100644 --- a/cogs/utils/file_search.py +++ b/cogs/utils/file_search.py @@ -7,6 +7,7 @@ import json import logging import aiohttp import os +from typing import List from dotenv import load_dotenv from cogs.utils.settings import get_baseurl @@ -19,12 +20,13 @@ class UniversalSearcher: """Handles searching for files via copyparty API.""" @staticmethod - async def search_copyparty(query: str, max_results: int = 25): + async def search_copyparty(query: str, max_results: int = 25, file_extensions: List[str] = None): """Search for files using copyparty API (jPOST method). Args: query: Search query string max_results: Maximum number of results to return + file_extensions: Optional list of file extensions to filter by (e.g., ['mp3', 'wav']) Returns: List of matching file URLs @@ -34,7 +36,16 @@ class UniversalSearcher: query = query.strip() logger.info(f"Starting copyparty search for query: '{query}'") - query = f'name like "*{query}*"' + + # Build the query with optional extension filtering + name_query = f'name like "*{query}*"' + if file_extensions: + # Create extension filters using copyparty's .ext$ syntax + ext_filters = [f"name like \"*.{ext.lower()}\"" for ext in file_extensions] + ext_query = " or ".join(ext_filters) + query = f"{name_query} and ({ext_query})" + else: + query = name_query try: baseurl = get_baseurl() @@ -115,7 +126,7 @@ class UniversalSearcher: @staticmethod async def search_copyparty_autocomplete(query: str, max_results: int = 10): - """Wrapper for autocomplete searches using copyparty. + """Wrapper for autocomplete searches using copyparty (general search - no file type filtering). Args: query: Search query string @@ -128,3 +139,21 @@ class UniversalSearcher: return [] return await UniversalSearcher.search_copyparty(query, max_results) + + @staticmethod + async def search_copyparty_autocomplete_audio(query: str, max_results: int = 10): + """Wrapper for autocomplete searches using copyparty (audio files only). + + Args: + query: Search query string + max_results: Maximum number of results to return + + Returns: + List of matching file URLs (audio files only) + """ + if not query: + return [] + + # Audio file extensions for music commands + audio_extensions = ['mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'mp4', "mkv"] + return await UniversalSearcher.search_copyparty(query, max_results, audio_extensions) -- cgit v1.2.3