aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/music/play.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--cogs/music/play.py272
1 files changed, 172 insertions, 100 deletions
diff --git a/cogs/music/play.py b/cogs/music/play.py
index 8390ff5..c9abe4b 100644
--- a/cogs/music/play.py
+++ b/cogs/music/play.py
@@ -1,10 +1,11 @@
1import os 1import os
2
3import discord 2import discord
4import mutagen 3import mutagen
4from typing import List, Tuple, Optional, Callable, Awaitable, Union
5 5
6from . import play_utils 6from . import play_utils
7from discord.ext import commands 7from discord.ext import commands
8from discord.commands import Option, OptionChoice
8import logging 9import logging
9 10
10logger = logging.getLogger(__name__) 11logger = logging.getLogger(__name__)
@@ -13,113 +14,184 @@ class Play(commands.Cog):
13 def __init__(self, bot): 14 def __init__(self, bot):
14 self.bot = bot 15 self.bot = bot
15 self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") 16 self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp")
17 self.path_mapping = {} # Maps display names to full paths
16 18
17 @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") 19 async def _create_search_embed(self, candidates: List[str]) -> Tuple[discord.Embed, discord.ui.View]:
18 async def play( 20 """Create an embed and view for search results."""
19 self, 21 embed = discord.Embed(
20 ctx: discord.ApplicationContext, 22 title="Wyniki wyszukiwania",
21 query: str = discord.Option(str, "co zagrać??", required=True), 23 description="Wybierz plik do odtworzenia:",
22 source: str = discord.Option(str, "co zagrać??", required=True, choices=["local", "soundcloud", "youtube"]) 24 color=discord.Color.blue()
23 25 )
24 ): 26
25 await ctx.defer(ephemeral=True) 27 view = discord.ui.View(disable_on_timeout=True)
26 if source == "local": 28
27 candidates = await play_utils.PlayUtils(self.bot).search_local(query) 29 for i, candidate in enumerate(candidates, start=1):
28 if candidates: 30 filename = os.path.basename(candidate)
29 embed = discord.Embed(title="Wyniki wyszukiwania", description="Wybierz plik do odtworzenia:", color=discord.Color.blue()) 31 title = None
30 view = discord.ui.View(disable_on_timeout=True) 32
31 for i, candidate in enumerate(candidates, start=1): 33 # Try to get metadata if it's an audio file
32 filename = os.path.basename(candidate) 34 if os.path.splitext(filename)[1].lower() in ['.mp3', '.wav', '.opus']:
33 metadata = None 35 try:
34 if os.path.splitext(filename)[1].lower() in ['.mp3', '.wav', '.opus']: 36 metadata = mutagen.File(candidate)
35 metadata = mutagen.File(candidate) 37 if metadata:
36 title = metadata.get('title', None) 38 title = metadata.get('title', None)
37 if metadata and title: 39 except Exception as e:
38 embed.add_field(name=f"{i}. {title}", value="\u200b", inline=False) 40 logger.debug(f"Could not read metadata for {filename}: {str(e)}")
39 else: 41
40 embed.add_field(name=f"{i}. {filename}", value="\u200b", inline=False) 42 # Add field with title if available, otherwise use filename
41 button = discord.ui.Button(label=f"Play {i}") 43 display_name = title if title else filename
42 async def button_callback(interaction: discord.Interaction, candidate=candidate): 44 embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False)
43 await ctx.interaction.delete_original_response() 45
44 await play_utils.PlayUtils(self.bot).play(ctx, candidate) 46 return embed, view
45 button.callback = button_callback 47
46 view.add_item(button) 48 async def _handle_search_results(
47 await ctx.respond(embed=embed, view=view, ephemeral=True) 49 self,
48 else: 50 ctx: discord.ApplicationContext,
49 await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) 51 candidates: List[str],
52 play_callback: Callable[[discord.Interaction, str], Awaitable[None]]
53 ) -> None:
54 """Handle the display of search results and button interactions."""
55 if not candidates:
56 await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True)
57 return
58
59 embed, view = await self._create_search_embed(candidates)
60
61 # Add buttons for each candidate
62 for i, candidate in enumerate(candidates, start=1):
63 button = discord.ui.Button(label=f"Play {i}")
64
65 async def button_callback(interaction: discord.Interaction, candidate_path=candidate):
66 await interaction.response.defer(ephemeral=True)
67 await ctx.interaction.delete_original_response()
68 try:
69 await play_callback(interaction, candidate_path)
70 except Exception as e:
71 logger.error(f"Error in play callback: {str(e)}", exc_info=True)
72 await interaction.followup.send(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True)
73
74 button.callback = button_callback
75 view.add_item(button)
76
77 await ctx.respond(embed=embed, view=view, ephemeral=True)
50 78
51 @discord.slash_command(name="grajcuj_zkompresowane", description="grajcujmy wszyscy w wysokiej jakosci!") 79 async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]:
52 async def play_compressed( 80 """Autocomplete handler for search queries."""
81 query = ctx.value.lower()
82 try:
83 results = await play_utils.PlayUtils.search_local_autocomplete(query, max_results=25)
84 choices = []
85 self.path_mapping = {} # Reset the mapping for each new query
86
87 for path in results:
88 filename = os.path.splitext(os.path.basename(path))[0] # Remove extension
89 display = filename # Default display name
90
91 # Try to get metadata for display
92 try:
93 if os.path.splitext(path)[1].lower() in ['.mp3', '.wav', '.opus', '.m4a']:
94 metadata = mutagen.File(path, easy=True)
95
96 # Get metadata fields, falling back to filename if not available
97 title = metadata.get('title', [filename])[0]
98 artist = metadata.get('artist', [''])[0]
99 album_artist = metadata.get('albumartist', [artist])[0]
100
101 # Build display name based on available metadata
102 if album_artist and title:
103 display = f"{album_artist} - {title}"
104 elif artist and title:
105 display = f"{artist} - {title}"
106 elif title:
107 display = title
108
109 except Exception as e:
110 logger.debug(f"Could not read metadata for {filename}: {str(e)}")
111
112 # Clean up the display name (remove any extra whitespace or brackets)
113 display = display.strip()
114
115 # Store the mapping (use the cleaned display name as the key)
116 self.path_mapping[display] = path
117
118 # Truncate display name if too long
119 if len(display) > 90: # Leave room for potential numbering
120 display = display[:87] + '...'
121
122 choices.append(OptionChoice(display[:100], display[:100]))
123
124 # Limit to 25 choices (Discord's limit)
125 if len(choices) >= 25:
126 break
127
128 return choices
129
130 except Exception as e:
131 logger.error(f"Error in autocomplete: {str(e)}", exc_info=True)
132 return []
133
134 @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!")
135 async def play(
53 self, 136 self,
54 ctx: discord.ApplicationContext, 137 ctx: discord.ApplicationContext,
55 query: str = discord.Option(str, "co zagrać??", required=True), 138 file: str = discord.Option(
56 source: str = discord.Option(str, "skąd zagrać??", required=True, 139 str,
57 choices=["local", "soundcloud", "youtube"]) 140 "Wybierz plik do odtworzenia",
141 autocomplete=_search_autocomplete,
142 required=True
143 ),
144 source: str = discord.Option(
145 str,
146 "Źródło dźwięku",
147 choices=["local", "soundcloud", "youtube"],
148 default="local"
149 ),
150 compressed: bool = discord.Option(
151 bool,
152 "Czy skompresować dźwięk?",
153 default=False
154 )
58 ): 155 ):
59 await ctx.defer(ephemeral=True) 156 """Odtwarza wybrany plik dźwiękowy z opcjonalną kompresją."""
60 if source == "local": 157 await ctx.defer()
61 candidates = await play_utils.PlayUtils(self.bot).search_local(query) 158
62 if candidates: 159 if source != "local":
63 embed = discord.Embed(title="Wyniki wyszukiwania", description="Wybierz plik do odtworzenia:", 160 await ctx.respond("Tylko źródło 'local' jest obecnie obsługiwane w tej komendzie.", ephemeral=True)
64 color=discord.Color.blue()) 161 return
65 view = discord.ui.View(disable_on_timeout=True) 162
66 for i, candidate in enumerate(candidates, start=1): 163 # Look up the full path from the display name
67 filename = os.path.basename(candidate) 164 full_path = self.path_mapping.get(file, file)
68 metadata = None 165
69 if os.path.splitext(filename)[1].lower() in ['.mp3', '.wav', '.opus']: 166 if not os.path.exists(full_path):
70 metadata = mutagen.File(candidate) 167 logger.warning(f"File not found: {full_path} (display name: {file})")
71 title = metadata.get('title', None) 168 await ctx.respond("Wybrany plik nie istnieje.", ephemeral=True)
72 if metadata and title: 169 return
73 embed.add_field(name=f"{i}. {title}", value="\u200b", inline=False) 170
74 else: 171 try:
75 embed.add_field(name=f"{i}. {filename}", value="\u200b", inline=False) 172 if compressed:
76 button = discord.ui.Button(label=f"Play {i}") 173 # Compress the file before playing
77 174 compressed_path = await play_utils.PlayUtils(self.bot).compress(ctx, full_path)
78 async def button_callback(interaction: discord.Interaction, candidate=candidate): 175 if not compressed_path:
79 await interaction.response.defer(ephemeral=True) 176 await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True)
177 return
178
179 try:
180 await play_utils.PlayUtils(self.bot).play(ctx, compressed_path, delete_after_play=True)
181 except Exception as e:
182 # Clean up the compressed file if play fails
183 if os.path.exists(compressed_path):
80 try: 184 try:
81 await ctx.interaction.delete_original_response() 185 os.remove(compressed_path)
82 # Compress the file before playing 186 logger.info(f"Cleaned up temporary file after play error: {compressed_path}")
83 compressed_path = await play_utils.PlayUtils(self.bot).compress(ctx, candidate) 187 except Exception as cleanup_error:
84 if compressed_path: 188 logger.error(f"Error cleaning up file {compressed_path}: {str(cleanup_error)}")
85 try: 189 raise e
86 await play_utils.PlayUtils(self.bot).play(ctx, compressed_path, delete_after_play=True)
87 except Exception as e:
88 # Clean up the compressed file if play fails
89 if os.path.exists(compressed_path):
90 try:
91 os.remove(compressed_path)
92 logger.info(f"Cleaned up temporary file after play error: {compressed_path}")
93 except Exception as cleanup_error:
94 logger.error(f"Error cleaning up file {compressed_path}: {str(cleanup_error)}")
95 raise e
96 except Exception as e:
97 logger.error(f"Error in play_compressed: {str(e)}", exc_info=True)
98 await ctx.respond(f"Error playing compressed file: {str(e)}", ephemeral=True)
99
100 button.callback = button_callback
101 view.add_item(button)
102 await ctx.respond(embed=embed, view=view, ephemeral=True)
103 else: 190 else:
104 await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) 191 await play_utils.PlayUtils(self.bot).play(ctx, full_path)
105 else: 192 except Exception as e:
106 # For YouTube/SoundCloud 193 logger.error(f"Error playing file: {str(e)}", exc_info=True)
107 try: 194 await ctx.respond(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True)
108 url, title = await play_utils.PlayUtils(self.bot).get_url(ctx, query)
109 if not url:
110 return
111
112 filename = await play_utils.PlayUtils(self.bot).download(ctx, url)
113 if not filename:
114 return
115
116 # Compress the downloaded file
117 compressed_filename = await play_utils.PlayUtils(self.bot).compress(ctx, filename)
118 if compressed_filename:
119 await play_utils.PlayUtils(self.bot).play(ctx, compressed_filename, title)
120 except Exception as e:
121 logger.error(f"Error in play_compressed: {str(e)}")
122 await ctx.respond(f"Error: {str(e)}", ephemeral=True)
123 195
124 @discord.slash_command(name="grajcuj_plik", description="grajcuj plik") 196 @discord.slash_command(name="grajcuj_plik", description="grajcuj plik")
125 async def play_file(self, ctx: discord.ApplicationContext, attachment: discord.Attachment = discord.Option(discord.Attachment, "co zagrać??", required=True)): 197 async def play_file(self, ctx: discord.ApplicationContext, attachment: discord.Attachment = discord.Option(discord.Attachment, "co zagrać??", required=True)):