diff options
| author | salmonik <raisersoft@proton.me> | 2026-02-04 16:16:26 +0100 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-02-04 16:16:26 +0100 |
| commit | ce61bf6bd5949046d15f58609f572fedcb8a073e (patch) | |
| tree | cafaa12f11a06337bda5de08881e395ac66c1c11 /cogs/music/play_commands.py | |
| parent | better reply context (diff) | |
| download | senator-sperma-ce61bf6bd5949046d15f58609f572fedcb8a073e.tar.gz senator-sperma-ce61bf6bd5949046d15f58609f572fedcb8a073e.zip | |
refactor music
Diffstat (limited to '')
| -rw-r--r-- | cogs/music/play_commands.py (renamed from cogs/music/play.py) | 145 |
1 files changed, 81 insertions, 64 deletions
diff --git a/cogs/music/play.py b/cogs/music/play_commands.py index c7dd9f1..b25ce84 100644 --- a/cogs/music/play.py +++ b/cogs/music/play_commands.py | |||
| @@ -9,14 +9,16 @@ import mutagen | |||
| 9 | from typing import List, Tuple, Callable, Awaitable | 9 | from typing import List, Tuple, Callable, Awaitable |
| 10 | from os import PathLike | 10 | from os import PathLike |
| 11 | 11 | ||
| 12 | from cogs.music import play_utils | 12 | from cogs.music import search_utils, audio_playback, media_processing |
| 13 | from discord.ext import commands | 13 | from discord.ext import commands |
| 14 | from discord.commands import OptionChoice | 14 | from discord.commands import OptionChoice |
| 15 | import logging | 15 | import logging |
| 16 | 16 | ||
| 17 | logger = logging.getLogger(__name__) | 17 | logger = logging.getLogger(__name__) |
| 18 | 18 | ||
| 19 | class Play(commands.Cog): | 19 | class PlayCommands(commands.Cog): |
| 20 | """Discord slash commands for music playback.""" | ||
| 21 | |||
| 20 | def __init__(self, bot): | 22 | def __init__(self, bot): |
| 21 | self.bot = bot | 23 | self.bot = bot |
| 22 | self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") | 24 | self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") |
| @@ -83,38 +85,38 @@ class Play(commands.Cog): | |||
| 83 | 85 | ||
| 84 | await ctx.respond(embed=embed, view=view, ephemeral=True) | 86 | await ctx.respond(embed=embed, view=view, ephemeral=True) |
| 85 | 87 | ||
| 88 | def _get_display_name_from_metadata(self, path: str, filename: str) -> str: | ||
| 89 | """Extract display name from audio file metadata.""" | ||
| 90 | try: | ||
| 91 | if os.path.splitext(path)[1].lower() in ['.mp3', '.wav', '.opus', '.m4a']: | ||
| 92 | metadata = mutagen.File(path, easy=True) | ||
| 93 | |||
| 94 | title = metadata.get('title', [filename])[0] | ||
| 95 | artist = metadata.get('artist', [''])[0] | ||
| 96 | album_artist = metadata.get('albumartist', [artist])[0] | ||
| 97 | |||
| 98 | if album_artist and title: | ||
| 99 | return f"{album_artist} - {title}" | ||
| 100 | elif artist and title: | ||
| 101 | return f"{artist} - {title}" | ||
| 102 | elif title: | ||
| 103 | return title | ||
| 104 | except Exception as e: | ||
| 105 | logger.debug(f"Could not read metadata for {filename}: {str(e)}") | ||
| 106 | |||
| 107 | return filename | ||
| 108 | |||
| 86 | async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]: | 109 | async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]: |
| 87 | """Autocomplete handler for search queries.""" | 110 | """Autocomplete handler for search queries.""" |
| 88 | query = ctx.value.lower() | 111 | query = ctx.value.lower() |
| 89 | try: | 112 | try: |
| 90 | results = await play_utils.PlayUtils.search_local_autocomplete(query, max_results=25) | 113 | results = await search_utils.AudioSearcher.search_local_autocomplete(query, max_results=25) |
| 91 | choices = [] | 114 | choices = [] |
| 92 | self.path_mapping = {} # Reset the mapping for each new query | 115 | self.path_mapping = {} # Reset the mapping for each new query |
| 93 | 116 | ||
| 94 | for path in results: | 117 | for path in results: |
| 95 | filename = os.path.splitext(os.path.basename(path))[0] # Remove extension | 118 | filename = os.path.splitext(os.path.basename(path))[0] # Remove extension |
| 96 | display = filename # Default display name | 119 | display = self._get_display_name_from_metadata(path, filename) |
| 97 | |||
| 98 | # Try to get metadata for display | ||
| 99 | try: | ||
| 100 | if os.path.splitext(path)[1].lower() in ['.mp3', '.wav', '.opus', '.m4a']: | ||
| 101 | metadata = mutagen.File(path, easy=True) | ||
| 102 | |||
| 103 | # Get metadata fields, falling back to filename if not available | ||
| 104 | title = metadata.get('title', [filename])[0] | ||
| 105 | artist = metadata.get('artist', [''])[0] | ||
| 106 | album_artist = metadata.get('albumartist', [artist])[0] | ||
| 107 | |||
| 108 | # Build display name based on available metadata | ||
| 109 | if album_artist and title: | ||
| 110 | display = f"{album_artist} - {title}" | ||
| 111 | elif artist and title: | ||
| 112 | display = f"{artist} - {title}" | ||
| 113 | elif title: | ||
| 114 | display = title | ||
| 115 | |||
| 116 | except Exception as e: | ||
| 117 | logger.debug(f"Could not read metadata for {filename}: {str(e)}") | ||
| 118 | 120 | ||
| 119 | # Clean up the display name (remove any extra whitespace or brackets) | 121 | # Clean up the display name (remove any extra whitespace or brackets) |
| 120 | display = display.strip() | 122 | display = display.strip() |
| @@ -138,6 +140,55 @@ class Play(commands.Cog): | |||
| 138 | logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) | 140 | logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) |
| 139 | return [] | 141 | return [] |
| 140 | 142 | ||
| 143 | def _validate_play_inputs(self, file: str, stream: str, ctx: discord.ApplicationContext) -> bool: | ||
| 144 | """Validate input parameters for play command.""" | ||
| 145 | if not file and not stream: | ||
| 146 | ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True) | ||
| 147 | return False | ||
| 148 | |||
| 149 | if file and stream: | ||
| 150 | ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True) | ||
| 151 | return False | ||
| 152 | |||
| 153 | return True | ||
| 154 | |||
| 155 | async def _handle_stream_play(self, ctx: discord.ApplicationContext, stream: str) -> None: | ||
| 156 | """Handle internet stream playback.""" | ||
| 157 | await audio_playback.AudioPlaybackManager.play(ctx, stream) | ||
| 158 | |||
| 159 | async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, full_path: str) -> None: | ||
| 160 | """Handle compressed file playback with cleanup.""" | ||
| 161 | compressed_path = await media_processing.AudioCompressor.compress(ctx, full_path) | ||
| 162 | if not compressed_path: | ||
| 163 | await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) | ||
| 164 | return | ||
| 165 | |||
| 166 | try: | ||
| 167 | await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) | ||
| 168 | except Exception as e: | ||
| 169 | # Clean up the compressed file if play fails | ||
| 170 | if os.path.exists(compressed_path): | ||
| 171 | try: | ||
| 172 | os.remove(compressed_path) | ||
| 173 | logger.info(f"Cleaned up temporary file after play error: {compressed_path}") | ||
| 174 | except Exception as cleanup_error: | ||
| 175 | logger.error(f"Error cleaning up file {compressed_path}: {str(cleanup_error)}") | ||
| 176 | raise e | ||
| 177 | |||
| 178 | async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: | ||
| 179 | """Handle local file playback.""" | ||
| 180 | full_path = self.path_mapping.get(file, file) | ||
| 181 | |||
| 182 | if not os.path.exists(full_path): | ||
| 183 | logger.warning(f"File not found: {full_path} (display name: {file})") | ||
| 184 | await ctx.respond("Wybrany plik nie istnieje.", ephemeral=True) | ||
| 185 | return | ||
| 186 | |||
| 187 | if compressed: | ||
| 188 | await self._handle_compressed_file_play(ctx, full_path) | ||
| 189 | else: | ||
| 190 | await audio_playback.AudioPlaybackManager.play(ctx, full_path) | ||
| 191 | |||
| 141 | @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") | 192 | @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") |
| 142 | async def play( | 193 | async def play( |
| 143 | self, | 194 | self, |
| @@ -162,48 +213,14 @@ class Play(commands.Cog): | |||
| 162 | """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" | 213 | """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" |
| 163 | await ctx.defer() | 214 | await ctx.defer() |
| 164 | 215 | ||
| 165 | # Check if either file or stream is provided | 216 | if not self._validate_play_inputs(file, stream, ctx): |
| 166 | if not file and not stream: | ||
| 167 | await ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True) | ||
| 168 | return | ||
| 169 | |||
| 170 | if file and stream: | ||
| 171 | await ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True) | ||
| 172 | return | 217 | return |
| 173 | 218 | ||
| 174 | try: | 219 | try: |
| 175 | if stream: | 220 | if stream: |
| 176 | # Handle internet stream | 221 | await self._handle_stream_play(ctx, stream) |
| 177 | await play_utils.PlayUtils(self.bot).play(ctx, stream) | ||
| 178 | else: | 222 | else: |
| 179 | # Handle local file | 223 | await self._handle_local_file_play(ctx, file, compressed) |
| 180 | full_path = self.path_mapping.get(file, file) | ||
| 181 | |||
| 182 | if not os.path.exists(full_path): | ||
| 183 | logger.warning(f"File not found: {full_path} (display name: {file})") | ||
| 184 | await ctx.respond("Wybrany plik nie istnieje.", ephemeral=True) | ||
| 185 | return | ||
| 186 | |||
| 187 | if compressed: | ||
| 188 | # Compress the file before playing | ||
| 189 | compressed_path = await play_utils.PlayUtils(self.bot).compress(ctx, full_path) | ||
| 190 | if not compressed_path: | ||
| 191 | await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) | ||
| 192 | return | ||
| 193 | |||
| 194 | try: | ||
| 195 | await play_utils.PlayUtils(self.bot).play(ctx, compressed_path, delete_after_play=True) | ||
| 196 | except Exception as e: | ||
| 197 | # Clean up the compressed file if play fails | ||
| 198 | if os.path.exists(compressed_path): | ||
| 199 | try: | ||
| 200 | os.remove(compressed_path) | ||
| 201 | logger.info(f"Cleaned up temporary file after play error: {compressed_path}") | ||
| 202 | except Exception as cleanup_error: | ||
| 203 | logger.error(f"Error cleaning up file {compressed_path}: {str(cleanup_error)}") | ||
| 204 | raise e | ||
| 205 | else: | ||
| 206 | await play_utils.PlayUtils(self.bot).play(ctx, full_path) | ||
| 207 | except Exception as e: | 224 | except Exception as e: |
| 208 | logger.error(f"Error playing: {str(e)}", exc_info=True) | 225 | logger.error(f"Error playing: {str(e)}", exc_info=True) |
| 209 | await ctx.respond(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True) | 226 | await ctx.respond(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True) |
| @@ -213,7 +230,7 @@ class Play(commands.Cog): | |||
| 213 | await ctx.defer() | 230 | await ctx.defer() |
| 214 | file_path: PathLike[str] | str = os.path.join(self.temp_dir, attachment.filename) | 231 | file_path: PathLike[str] | str = os.path.join(self.temp_dir, attachment.filename) |
| 215 | await attachment.save(file_path) | 232 | await attachment.save(file_path) |
| 216 | await play_utils.PlayUtils(self.bot).play(ctx, file_path) | 233 | await audio_playback.AudioPlaybackManager.play(ctx, file_path) |
| 217 | 234 | ||
| 218 | def setup(bot): | 235 | def setup(bot): |
| 219 | bot.add_cog(Play(bot)) \ No newline at end of file | 236 | bot.add_cog(PlayCommands(bot)) |
