diff options
Diffstat (limited to 'cogs/music/play.py')
| -rw-r--r-- | cogs/music/play.py | 219 |
1 files changed, 0 insertions, 219 deletions
diff --git a/cogs/music/play.py b/cogs/music/play.py deleted file mode 100644 index c7dd9f1..0000000 --- a/cogs/music/play.py +++ /dev/null | |||
| @@ -1,219 +0,0 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import os | ||
| 7 | import discord | ||
| 8 | import mutagen | ||
| 9 | from typing import List, Tuple, Callable, Awaitable | ||
| 10 | from os import PathLike | ||
| 11 | |||
| 12 | from cogs.music import play_utils | ||
| 13 | from discord.ext import commands | ||
| 14 | from discord.commands import OptionChoice | ||
| 15 | import logging | ||
| 16 | |||
| 17 | logger = logging.getLogger(__name__) | ||
| 18 | |||
| 19 | class Play(commands.Cog): | ||
| 20 | def __init__(self, bot): | ||
| 21 | self.bot = bot | ||
| 22 | self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") | ||
| 23 | self.path_mapping = {} # Maps display names to full paths | ||
| 24 | |||
| 25 | @staticmethod | ||
| 26 | async def _create_search_embed(candidates: List[str]) -> Tuple[discord.Embed, discord.ui.View]: | ||
| 27 | """Create an embed and view for search results.""" | ||
| 28 | embed = discord.Embed( | ||
| 29 | title="Wyniki wyszukiwania", | ||
| 30 | description="Wybierz plik do odtworzenia:", | ||
| 31 | color=discord.Color.blue() | ||
| 32 | ) | ||
| 33 | |||
| 34 | view = discord.ui.View(disable_on_timeout=True) | ||
| 35 | |||
| 36 | for i, candidate in enumerate(candidates, start=1): | ||
| 37 | filename = os.path.basename(candidate) | ||
| 38 | title = None | ||
| 39 | |||
| 40 | # Try to get metadata if it's an audio file | ||
| 41 | if os.path.splitext(filename)[1].lower() in ['.mp3', '.wav', '.opus']: | ||
| 42 | try: | ||
| 43 | metadata = mutagen.File(candidate) | ||
| 44 | if metadata: | ||
| 45 | title = metadata.get('title', None) | ||
| 46 | except Exception as e: | ||
| 47 | logger.debug(f"Could not read metadata for {filename}: {str(e)}") | ||
| 48 | |||
| 49 | # Add field with title if available, otherwise use filename | ||
| 50 | display_name = title if title else filename | ||
| 51 | embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False) | ||
| 52 | |||
| 53 | return embed, view | ||
| 54 | |||
| 55 | async def _handle_search_results( | ||
| 56 | self, | ||
| 57 | ctx: discord.ApplicationContext, | ||
| 58 | candidates: List[str], | ||
| 59 | play_callback: Callable[[discord.Interaction, str], Awaitable[None]] | ||
| 60 | ) -> None: | ||
| 61 | """Handle the display of search results and button interactions.""" | ||
| 62 | if not candidates: | ||
| 63 | await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) | ||
| 64 | return | ||
| 65 | |||
| 66 | embed, view = await self._create_search_embed(candidates) | ||
| 67 | |||
| 68 | # Add buttons for each candidate | ||
| 69 | for i, candidate in enumerate(candidates, start=1): | ||
| 70 | button = discord.ui.Button(label=f"Play {i}") | ||
| 71 | |||
| 72 | async def button_callback(interaction: discord.Interaction, candidate_path=candidate): | ||
| 73 | await interaction.response.defer(ephemeral=True) | ||
| 74 | await ctx.interaction.delete_original_response() | ||
| 75 | try: | ||
| 76 | await play_callback(interaction, candidate_path) | ||
| 77 | except Exception as e: | ||
| 78 | logger.error(f"Error in play callback: {str(e)}", exc_info=True) | ||
| 79 | await interaction.followup.send(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True) | ||
| 80 | |||
| 81 | button.callback = button_callback | ||
| 82 | view.add_item(button) | ||
| 83 | |||
| 84 | await ctx.respond(embed=embed, view=view, ephemeral=True) | ||
| 85 | |||
| 86 | async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]: | ||
| 87 | """Autocomplete handler for search queries.""" | ||
| 88 | query = ctx.value.lower() | ||
| 89 | try: | ||
| 90 | results = await play_utils.PlayUtils.search_local_autocomplete(query, max_results=25) | ||
| 91 | choices = [] | ||
| 92 | self.path_mapping = {} # Reset the mapping for each new query | ||
| 93 | |||
| 94 | for path in results: | ||
| 95 | filename = os.path.splitext(os.path.basename(path))[0] # Remove extension | ||
| 96 | display = filename # Default display name | ||
| 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 | |||
| 119 | # Clean up the display name (remove any extra whitespace or brackets) | ||
| 120 | display = display.strip() | ||
| 121 | |||
| 122 | # Store the mapping (use the cleaned display name as the key) | ||
| 123 | self.path_mapping[display] = path | ||
| 124 | |||
| 125 | # Truncate display name if too long | ||
| 126 | if len(display) > 90: # Leave room for potential numbering | ||
| 127 | display = display[:87] + '...' | ||
| 128 | |||
| 129 | choices.append(OptionChoice(display[:100], display[:100])) | ||
| 130 | |||
| 131 | # Limit to 25 choices (Discord's limit) | ||
| 132 | if len(choices) >= 25: | ||
| 133 | break | ||
| 134 | |||
| 135 | return choices | ||
| 136 | |||
| 137 | except Exception as e: | ||
| 138 | logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) | ||
| 139 | return [] | ||
| 140 | |||
| 141 | @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") | ||
| 142 | async def play( | ||
| 143 | self, | ||
| 144 | ctx: discord.ApplicationContext, | ||
| 145 | file: str = discord.Option( | ||
| 146 | str, | ||
| 147 | "wyszukaj plik z serwera", | ||
| 148 | autocomplete=_search_autocomplete, | ||
| 149 | required=False | ||
| 150 | ), | ||
| 151 | stream: str = discord.Option( | ||
| 152 | str, | ||
| 153 | "odtwórz stream internetowy (e.g. radio)", | ||
| 154 | required=False | ||
| 155 | ), | ||
| 156 | compressed: bool = discord.Option( | ||
| 157 | bool, | ||
| 158 | "Czy skompresować dźwięk? (tylko dla plików lokalnych)", | ||
| 159 | default=False | ||
| 160 | ) | ||
| 161 | ): | ||
| 162 | """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" | ||
| 163 | await ctx.defer() | ||
| 164 | |||
| 165 | # Check if either file or stream is provided | ||
| 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 | ||
| 173 | |||
| 174 | try: | ||
| 175 | if stream: | ||
| 176 | # Handle internet stream | ||
| 177 | await play_utils.PlayUtils(self.bot).play(ctx, stream) | ||
| 178 | else: | ||
| 179 | # Handle local file | ||
| 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: | ||
| 208 | 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) | ||
| 210 | |||
| 211 | @discord.slash_command(name="grajcuj_plik", description="grajcuj plik") | ||
| 212 | async def play_file(self, ctx: discord.ApplicationContext, attachment: discord.Attachment = discord.Option(discord.Attachment, "co zagrać??", required=True)): | ||
| 213 | await ctx.defer() | ||
| 214 | file_path: PathLike[str] | str = os.path.join(self.temp_dir, attachment.filename) | ||
| 215 | await attachment.save(file_path) | ||
| 216 | await play_utils.PlayUtils(self.bot).play(ctx, file_path) | ||
| 217 | |||
| 218 | def setup(bot): | ||
| 219 | bot.add_cog(Play(bot)) \ No newline at end of file | ||
