aboutsummaryrefslogtreecommitdiffstats
path: root/cogs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--cogs/music/audio_playback.py14
-rw-r--r--cogs/music/play_commands.py91
-rw-r--r--cogs/utils/file_search.py35
3 files changed, 75 insertions, 65 deletions
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 @@
5 5
6import os 6import os
7import logging 7import logging
8from pathlib import Path 8import urllib.parse
9
10import discord 9import discord
11import mutagen 10import mutagen
12 11
@@ -74,13 +73,17 @@ class AudioPlaybackManager:
74 async def prepare_audio_source(source, is_url, display_name): 73 async def prepare_audio_source(source, is_url, display_name):
75 """Prepare audio source for playback. Returns (source, final_display_name).""" 74 """Prepare audio source for playback. Returns (source, final_display_name)."""
76 if is_url: 75 if is_url:
77 # For URLs, use FFmpeg with the direct URL 76 # For URLs, use FFmpeg with the direct URL and extract clean filename
78 ffmpeg_options = { 77 ffmpeg_options = {
79 'options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 78 'options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
80 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5' 79 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5'
81 } 80 }
82 audio_source = discord.FFmpegPCMAudio(source, **ffmpeg_options) 81 audio_source = discord.FFmpegPCMAudio(source, **ffmpeg_options)
83 return audio_source, display_name 82 # Extract clean filename for URL (remove extension and decode URL encoding)
83 filename = os.path.basename(source.split('?')[0]) # Remove query parameters
84 filename = urllib.parse.unquote(filename) # Decode URL encoding
85 clean_filename = os.path.splitext(filename)[0] # Remove file extension
86 return audio_source, clean_filename
84 else: 87 else:
85 # For local files, use default options 88 # For local files, use default options
86 audio_source = discord.FFmpegPCMAudio(source=source) 89 audio_source = discord.FFmpegPCMAudio(source=source)
@@ -93,8 +96,11 @@ class AudioPlaybackManager:
93 if title: 96 if title:
94 display_name = title 97 display_name = title
95 logger.debug(f"Read metadata - title: {title}") 98 logger.debug(f"Read metadata - title: {title}")
99 else:
100 display_name = os.path.splitext(os.path.basename(source))[0]
96 except Exception as e: 101 except Exception as e:
97 logger.warning(f"Could not read metadata from {source}: {str(e)}") 102 logger.warning(f"Could not read metadata from {source}: {str(e)}")
103 display_name = os.path.splitext(os.path.basename(source))[0]
98 104
99 return audio_source, display_name 105 return audio_source, display_name
100 106
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):
78 """Autocomplete handler for search queries.""" 78 """Autocomplete handler for search queries."""
79 query = ctx.value.lower() 79 query = ctx.value.lower()
80 try: 80 try:
81 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25) 81 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete_audio(query, max_results=25)
82 choices = [] 82 choices = []
83 self.path_mapping = {} # Reset the mapping for each new query 83 self.path_mapping = {} # Reset the mapping for each new query
84 84
@@ -123,65 +123,40 @@ class PlayCommands(commands.Cog):
123 await audio_playback.AudioPlaybackManager.play(ctx, stream) 123 await audio_playback.AudioPlaybackManager.play(ctx, stream)
124 124
125 async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None: 125 async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None:
126 """Handle compressed file playback with cleanup.""" 126 """Download from copyparty, compress, then play locally."""
127 is_url = url.startswith(('http://', 'https://')) 127 import aiohttp
128 128 local_path = None
129 if is_url: 129 compressed_path = None
130 import aiohttp 130 try:
131 local_path = None 131 async with aiohttp.ClientSession() as session:
132 try: 132 async with session.get(url) as response:
133 async with aiohttp.ClientSession() as session: 133 if response.status != 200:
134 async with session.get(url) as response: 134 await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True)
135 if response.status != 200: 135 return
136 await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True)
137 return
138
139 filename = url.split('/')[-1].split('?')[0]
140 local_path = os.path.join(self.temp_dir, filename)
141 os.makedirs(self.temp_dir, exist_ok=True)
142
143 with open(local_path, 'wb') as f:
144 async for chunk in response.content.iter_chunked(8192):
145 f.write(chunk)
146
147 compressed_path = await media_processing.AudioCompressor.compress(ctx, local_path)
148 if not compressed_path:
149 await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True)
150 return
151 136
152 try: 137 filename = url.split('/')[-1].split('?')[0]
153 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) 138 filename = urllib.parse.unquote(filename)
154 except Exception as e: 139 local_path = os.path.join(self.temp_dir, filename)
155 if os.path.exists(compressed_path): 140 os.makedirs(self.temp_dir, exist_ok=True)
156 try: 141
157 os.remove(compressed_path) 142 with open(local_path, 'wb') as f:
158 except Exception: 143 async for chunk in response.content.iter_chunked(8192):
159 pass 144 f.write(chunk)
160 raise e 145
161 finally: 146 compressed_path = await media_processing.AudioCompressor.compress(ctx, local_path)
162 if os.path.exists(local_path):
163 try:
164 os.remove(local_path)
165 except Exception:
166 pass
167 except Exception as e:
168 logger.error(f"Error downloading file from URL: {str(e)}", exc_info=True)
169 await ctx.respond(f"Błąd pobierania pliku: {str(e)}", ephemeral=True)
170 else:
171 compressed_path = await media_processing.AudioCompressor.compress(ctx, url)
172 if not compressed_path: 147 if not compressed_path:
173 await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True)
174 return 148 return
175 149
176 try: 150 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True)
177 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) 151 except Exception as e:
178 except Exception as e: 152 logger.error(f"Error in compressed playback: {str(e)}", exc_info=True)
179 if os.path.exists(compressed_path): 153 await ctx.respond(f"Błąd: {str(e)}", ephemeral=True)
180 try: 154 finally:
181 os.remove(compressed_path) 155 if local_path and os.path.exists(local_path):
182 except Exception: 156 try:
183 pass 157 os.remove(local_path)
184 raise e 158 except Exception:
159 pass
185 160
186 async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: 161 async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None:
187 """Handle file playback from copyparty URL.""" 162 """Handle file playback from copyparty URL."""
@@ -209,14 +184,14 @@ class PlayCommands(commands.Cog):
209 ), 184 ),
210 compressed: bool = discord.Option( 185 compressed: bool = discord.Option(
211 bool, 186 bool,
212 "Czy skompresować dźwięk? (tylko dla plików lokalnych)", 187 "Czy skompresować dźwięk?",
213 default=False 188 default=False
214 ) 189 )
215 ): 190 ):
216 """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" 191 """Odtwarza wybrany plik dźwiękowy lub stream internetowy."""
217 await ctx.defer() 192 await ctx.defer()
218 193
219 if not self._validate_play_inputs(file, stream, ctx): 194 if not await self._validate_play_inputs(file, stream, ctx):
220 return 195 return
221 196
222 try: 197 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
7import logging 7import logging
8import aiohttp 8import aiohttp
9import os 9import os
10from typing import List
10from dotenv import load_dotenv 11from dotenv import load_dotenv
11from cogs.utils.settings import get_baseurl 12from cogs.utils.settings import get_baseurl
12 13
@@ -19,12 +20,13 @@ class UniversalSearcher:
19 """Handles searching for files via copyparty API.""" 20 """Handles searching for files via copyparty API."""
20 21
21 @staticmethod 22 @staticmethod
22 async def search_copyparty(query: str, max_results: int = 25): 23 async def search_copyparty(query: str, max_results: int = 25, file_extensions: List[str] = None):
23 """Search for files using copyparty API (jPOST method). 24 """Search for files using copyparty API (jPOST method).
24 25
25 Args: 26 Args:
26 query: Search query string 27 query: Search query string
27 max_results: Maximum number of results to return 28 max_results: Maximum number of results to return
29 file_extensions: Optional list of file extensions to filter by (e.g., ['mp3', 'wav'])
28 30
29 Returns: 31 Returns:
30 List of matching file URLs 32 List of matching file URLs
@@ -34,7 +36,16 @@ class UniversalSearcher:
34 36
35 query = query.strip() 37 query = query.strip()
36 logger.info(f"Starting copyparty search for query: '{query}'") 38 logger.info(f"Starting copyparty search for query: '{query}'")
37 query = f'name like "*{query}*"' 39
40 # Build the query with optional extension filtering
41 name_query = f'name like "*{query}*"'
42 if file_extensions:
43 # Create extension filters using copyparty's .ext$ syntax
44 ext_filters = [f"name like \"*.{ext.lower()}\"" for ext in file_extensions]
45 ext_query = " or ".join(ext_filters)
46 query = f"{name_query} and ({ext_query})"
47 else:
48 query = name_query
38 49
39 try: 50 try:
40 baseurl = get_baseurl() 51 baseurl = get_baseurl()
@@ -115,7 +126,7 @@ class UniversalSearcher:
115 126
116 @staticmethod 127 @staticmethod
117 async def search_copyparty_autocomplete(query: str, max_results: int = 10): 128 async def search_copyparty_autocomplete(query: str, max_results: int = 10):
118 """Wrapper for autocomplete searches using copyparty. 129 """Wrapper for autocomplete searches using copyparty (general search - no file type filtering).
119 130
120 Args: 131 Args:
121 query: Search query string 132 query: Search query string
@@ -128,3 +139,21 @@ class UniversalSearcher:
128 return [] 139 return []
129 140
130 return await UniversalSearcher.search_copyparty(query, max_results) 141 return await UniversalSearcher.search_copyparty(query, max_results)
142
143 @staticmethod
144 async def search_copyparty_autocomplete_audio(query: str, max_results: int = 10):
145 """Wrapper for autocomplete searches using copyparty (audio files only).
146
147 Args:
148 query: Search query string
149 max_results: Maximum number of results to return
150
151 Returns:
152 List of matching file URLs (audio files only)
153 """
154 if not query:
155 return []
156
157 # Audio file extensions for music commands
158 audio_extensions = ['mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'mp4', "mkv"]
159 return await UniversalSearcher.search_copyparty(query, max_results, audio_extensions)