aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/music/play_commands.py
diff options
context:
space:
mode:
Diffstat (limited to 'cogs/music/play_commands.py')
-rw-r--r--cogs/music/play_commands.py166
1 files changed, 84 insertions, 82 deletions
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 @@
4# https://codeberg.org/salmonik/senator-sperma 4# https://codeberg.org/salmonik/senator-sperma
5 5
6import os 6import os
7import urllib.parse
7import discord 8import discord
8import mutagen
9from typing import List, Tuple, Callable, Awaitable 9from typing import List, Tuple, Callable, Awaitable
10from os import PathLike 10from os import PathLike
11 11
@@ -37,20 +37,8 @@ class PlayCommands(commands.Cog):
37 view = discord.ui.View(disable_on_timeout=True) 37 view = discord.ui.View(disable_on_timeout=True)
38 38
39 for i, candidate in enumerate(candidates, start=1): 39 for i, candidate in enumerate(candidates, start=1):
40 filename = os.path.basename(candidate) 40 filename = candidate.split('/')[-1].split('?')[0]
41 title = None 41 display_name = os.path.splitext(filename)[0]
42
43 # Try to get metadata if it's an audio file
44 if os.path.splitext(filename)[1].lower() in ['.mp3', '.wav', '.opus']:
45 try:
46 metadata = mutagen.File(candidate)
47 if metadata:
48 title = metadata.get('title', None)
49 except Exception as e:
50 logger.debug(f"Could not read metadata for {filename}: {str(e)}")
51
52 # Add field with title if available, otherwise use filename
53 display_name = title if title else filename
54 embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False) 42 embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False)
55 43
56 return embed, view 44 return embed, view
@@ -86,69 +74,46 @@ class PlayCommands(commands.Cog):
86 74
87 await ctx.respond(embed=embed, view=view, ephemeral=True) 75 await ctx.respond(embed=embed, view=view, ephemeral=True)
88 76
89 def _get_display_name_from_metadata(self, path: str, filename: str) -> str:
90 """Extract display name from audio file metadata."""
91 try:
92 if os.path.splitext(path)[1].lower() in ['.mp3', '.wav', '.opus', '.m4a']:
93 metadata = mutagen.File(path, easy=True)
94
95 title = metadata.get('title', [filename])[0]
96 artist = metadata.get('artist', [''])[0]
97 album_artist = metadata.get('albumartist', [artist])[0]
98
99 if album_artist and title:
100 return f"{album_artist} - {title}"
101 elif artist and title:
102 return f"{artist} - {title}"
103 elif title:
104 return title
105 except Exception as e:
106 logger.debug(f"Could not read metadata for {filename}: {str(e)}")
107
108 return filename
109
110 async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]: 77 async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]:
111 """Autocomplete handler for search queries.""" 78 """Autocomplete handler for search queries."""
112 query = ctx.value.lower() 79 query = ctx.value.lower()
113 try: 80 try:
114 results = await search_utils.UniversalSearcher.search_local_autocomplete(query, max_results=25, filetype="Audio") 81 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25)
115 choices = [] 82 choices = []
116 self.path_mapping = {} # Reset the mapping for each new query 83 self.path_mapping = {} # Reset the mapping for each new query
117 84
118 for path in results: 85 for url in results:
119 filename = os.path.splitext(os.path.basename(path))[0] # Remove extension 86 filename = url.split('/')[-1]
120 display = self._get_display_name_from_metadata(path, filename) 87 filename = filename.split('?')[0] # Remove query params if any
121 88 filename = urllib.parse.unquote(filename) # Decode URL encoding
122 # Clean up the display name (remove any extra whitespace or brackets) 89 display = os.path.splitext(filename)[0] # Remove extension
90
123 display = display.strip() 91 display = display.strip()
124 92
125 # Store the mapping (use the cleaned display name as the key) 93 self.path_mapping[display] = url
126 self.path_mapping[display] = path 94
127 95 if len(display) > 90:
128 # Truncate display name if too long
129 if len(display) > 90: # Leave room for potential numbering
130 display = display[:87] + '...' 96 display = display[:87] + '...'
131 97
132 choices.append(OptionChoice(display[:100], display[:100])) 98 choices.append(OptionChoice(display[:100], display[:100]))
133 99
134 # Limit to 25 choices (Discord's limit)
135 if len(choices) >= 25: 100 if len(choices) >= 25:
136 break 101 break
137 102
138 return choices 103 return choices
139 104
140 except Exception as e: 105 except Exception as e:
141 logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) 106 logger.error(f"Error in autocomplete: {str(e)}", exc_info=True)
142 return [] 107 return []
143 108
144 def _validate_play_inputs(self, file: str, stream: str, ctx: discord.ApplicationContext) -> bool: 109 async def _validate_play_inputs(self, file: str, stream: str, ctx: discord.ApplicationContext) -> bool:
145 """Validate input parameters for play command.""" 110 """Validate input parameters for play command."""
146 if not file and not stream: 111 if not file and not stream:
147 ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True) 112 await ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True)
148 return False 113 return False
149 114
150 if file and stream: 115 if file and stream:
151 ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True) 116 await ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True)
152 return False 117 return False
153 118
154 return True 119 return True
@@ -157,38 +122,75 @@ class PlayCommands(commands.Cog):
157 """Handle internet stream playback.""" 122 """Handle internet stream playback."""
158 await audio_playback.AudioPlaybackManager.play(ctx, stream) 123 await audio_playback.AudioPlaybackManager.play(ctx, stream)
159 124
160 async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, full_path: str) -> None: 125 async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None:
161 """Handle compressed file playback with cleanup.""" 126 """Handle compressed file playback with cleanup."""
162 compressed_path = await media_processing.AudioCompressor.compress(ctx, full_path) 127 is_url = url.startswith(('http://', 'https://'))
163 if not compressed_path: 128
164 await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) 129 if is_url:
165 return 130 import aiohttp
166 131 local_path = None
167 try: 132 try:
168 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) 133 async with aiohttp.ClientSession() as session:
169 except Exception as e: 134 async with session.get(url) as response:
170 # Clean up the compressed file if play fails 135 if response.status != 200:
171 if os.path.exists(compressed_path): 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
172 try: 152 try:
173 os.remove(compressed_path) 153 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True)
174 logger.info(f"Cleaned up temporary file after play error: {compressed_path}") 154 except Exception as e:
175 except Exception as cleanup_error: 155 if os.path.exists(compressed_path):
176 logger.error(f"Error cleaning up file {compressed_path}: {str(cleanup_error)}") 156 try:
177 raise e 157 os.remove(compressed_path)
158 except Exception:
159 pass
160 raise e
161 finally:
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:
173 await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True)
174 return
175
176 try:
177 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True)
178 except Exception as e:
179 if os.path.exists(compressed_path):
180 try:
181 os.remove(compressed_path)
182 except Exception:
183 pass
184 raise e
178 185
179 async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: 186 async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None:
180 """Handle local file playback.""" 187 """Handle file playback from copyparty URL."""
181 full_path = self.path_mapping.get(file, file) 188 url = self.path_mapping.get(file, file)
182
183 if not os.path.exists(full_path):
184 logger.warning(f"File not found: {full_path} (display name: {file})")
185 await ctx.respond("Wybrany plik nie istnieje.", ephemeral=True)
186 return
187 189
188 if compressed: 190 if compressed:
189 await self._handle_compressed_file_play(ctx, full_path) 191 await self._handle_compressed_file_play(ctx, url)
190 else: 192 else:
191 await audio_playback.AudioPlaybackManager.play(ctx, full_path) 193 await audio_playback.AudioPlaybackManager.play(ctx, url)
192 194
193 @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") 195 @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!")
194 async def play( 196 async def play(