aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/music/play_commands.py
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-04-17 15:36:12 +0200
committersalmonik <raisersoft@proton.me>2026-04-17 15:36:12 +0200
commitd135265798da8c2698f363237f4cb74b682f3f9e (patch)
treebca914d297e74a6188478a773ba7333c10fe4a34 /cogs/music/play_commands.py
parentMerge branch 'main' of https://codeberg.org/salmonik/senator-sperma (diff)
downloadsenator-sperma-d135265798da8c2698f363237f4cb74b682f3f9e.tar.gz
senator-sperma-d135265798da8c2698f363237f4cb74b682f3f9e.zip
make voice great again
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
-rw-r--r--cogs/music/play_commands.py184
1 files changed, 96 insertions, 88 deletions
diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py
index a06c636..a24169f 100644
--- a/cogs/music/play_commands.py
+++ b/cogs/music/play_commands.py
@@ -3,88 +3,102 @@
3# Copyright (C) 2026 Salmonik @ salmonik@tuta.io 3# Copyright (C) 2026 Salmonik @ salmonik@tuta.io
4# https://codeberg.org/salmonik/senator-sperma 4# https://codeberg.org/salmonik/senator-sperma
5 5
6import logging
6import os 7import os
7import urllib.parse 8import urllib.parse
8import discord
9from typing import List, Tuple, Callable, Awaitable
10from os import PathLike 9from os import PathLike
10from typing import Awaitable, Callable, List, Tuple
11
12import discord
13from discord.commands import OptionChoice
14from discord.ext import commands
11 15
12from cogs.music import audio_playback, media_processing 16from cogs.music import audio_playback, media_processing
13from cogs.utils import file_search as search_utils 17from cogs.utils import file_search as search_utils
14from discord.ext import commands
15from discord.commands import OptionChoice
16import logging
17 18
18logger = logging.getLogger(__name__) 19logger = logging.getLogger(__name__)
19 20
21
20class PlayCommands(commands.Cog): 22class PlayCommands(commands.Cog):
21 """Discord slash commands for music playback.""" 23 """Discord slash commands for music playback."""
22 24
23 def __init__(self, bot): 25 def __init__(self, bot):
24 self.bot = bot 26 self.bot = bot
25 self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") 27 self.temp_dir = os.path.join(
28 os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp"
29 )
26 self.path_mapping = {} # Maps display names to full paths 30 self.path_mapping = {} # Maps display names to full paths
27 31
28 @staticmethod 32 @staticmethod
29 async def _create_search_embed(candidates: List[str]) -> Tuple[discord.Embed, discord.ui.View]: 33 async def _create_search_embed(
34 candidates: List[str],
35 ) -> Tuple[discord.Embed, discord.ui.View]:
30 """Create an embed and view for search results.""" 36 """Create an embed and view for search results."""
31 embed = discord.Embed( 37 embed = discord.Embed(
32 title="Wyniki wyszukiwania", 38 title="Wyniki wyszukiwania",
33 description="Wybierz plik do odtworzenia:", 39 description="Wybierz plik do odtworzenia:",
34 color=discord.Color.blue() 40 color=discord.Color.blue(),
35 ) 41 )
36 42
37 view = discord.ui.View(disable_on_timeout=True) 43 view = discord.ui.View(disable_on_timeout=True)
38 44
39 for i, candidate in enumerate(candidates, start=1): 45 for i, candidate in enumerate(candidates, start=1):
40 filename = candidate.split('/')[-1].split('?')[0] 46 filename = candidate.split("/")[-1].split("?")[0]
41 display_name = os.path.splitext(filename)[0] 47 display_name = os.path.splitext(filename)[0]
42 embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False) 48 embed.add_field(name=f"{i}. {display_name}", value="\u200b", inline=False)
43 49
44 return embed, view 50 return embed, view
45 51
46 async def _handle_search_results( 52 async def _handle_search_results(
47 self, 53 self,
48 ctx: discord.ApplicationContext, 54 ctx: discord.ApplicationContext,
49 candidates: List[str], 55 candidates: List[str],
50 play_callback: Callable[[discord.Interaction, str], Awaitable[None]] 56 play_callback: Callable[[discord.Interaction, str], Awaitable[None]],
51 ) -> None: 57 ) -> None:
52 """Handle the display of search results and button interactions.""" 58 """Handle the display of search results and button interactions."""
53 if not candidates: 59 if not candidates:
54 await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) 60 await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True)
55 return 61 return
56 62
57 embed, view = await self._create_search_embed(candidates) 63 embed, view = await self._create_search_embed(candidates)
58 64
59 # Add buttons for each candidate 65 # Add buttons for each candidate
60 for i, candidate in enumerate(candidates, start=1): 66 for i, candidate in enumerate(candidates, start=1):
61 button = discord.ui.Button(label=f"Play {i}") 67 button = discord.ui.Button(label=f"Play {i}")
62 68
63 async def button_callback(interaction: discord.Interaction, candidate_path=candidate): 69 async def button_callback(
70 interaction: discord.Interaction, candidate_path=candidate
71 ):
64 await interaction.response.defer(ephemeral=True) 72 await interaction.response.defer(ephemeral=True)
65 await ctx.interaction.delete_original_response() 73 await ctx.interaction.delete_original_response()
66 try: 74 try:
67 await play_callback(interaction, candidate_path) 75 await play_callback(interaction, candidate_path)
68 except Exception as e: 76 except Exception as e:
69 logger.error(f"Error in play callback: {str(e)}", exc_info=True) 77 logger.error(f"Error in play callback: {str(e)}", exc_info=True)
70 await interaction.followup.send(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True) 78 await interaction.followup.send(
71 79 f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True
80 )
81
72 button.callback = button_callback 82 button.callback = button_callback
73 view.add_item(button) 83 view.add_item(button)
74 84
75 await ctx.respond(embed=embed, view=view, ephemeral=True) 85 await ctx.respond(embed=embed, view=view, ephemeral=True)
76 86
77 async def _search_autocomplete(self, ctx: discord.AutocompleteContext) -> List[OptionChoice]: 87 async def _search_autocomplete(
88 self, ctx: discord.AutocompleteContext
89 ) -> List[OptionChoice]:
78 """Autocomplete handler for search queries.""" 90 """Autocomplete handler for search queries."""
79 query = ctx.value.lower() 91 query = ctx.value.lower()
80 try: 92 try:
81 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete_audio(query, max_results=25) 93 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete_audio(
94 query, max_results=25
95 )
82 choices = [] 96 choices = []
83 self.path_mapping = {} # Reset the mapping for each new query 97 self.path_mapping = {} # Reset the mapping for each new query
84 98
85 for url in results: 99 for url in results:
86 filename = url.split('/')[-1] 100 filename = url.split("/")[-1]
87 filename = filename.split('?')[0] # Remove query params if any 101 filename = filename.split("?")[0] # Remove query params if any
88 filename = urllib.parse.unquote(filename) # Decode URL encoding 102 filename = urllib.parse.unquote(filename) # Decode URL encoding
89 display = os.path.splitext(filename)[0] # Remove extension 103 display = os.path.splitext(filename)[0] # Remove extension
90 104
@@ -93,7 +107,7 @@ class PlayCommands(commands.Cog):
93 self.path_mapping[display] = url 107 self.path_mapping[display] = url
94 108
95 if len(display) > 90: 109 if len(display) > 90:
96 display = display[:87] + '...' 110 display = display[:87] + "..."
97 111
98 choices.append(OptionChoice(display[:100], display[:100])) 112 choices.append(OptionChoice(display[:100], display[:100]))
99 113
@@ -106,25 +120,12 @@ class PlayCommands(commands.Cog):
106 logger.error(f"Error in autocomplete: {str(e)}", exc_info=True) 120 logger.error(f"Error in autocomplete: {str(e)}", exc_info=True)
107 return [] 121 return []
108 122
109 async def _validate_play_inputs(self, file: str, stream: str, ctx: discord.ApplicationContext) -> bool: 123 async def _handle_compressed_file_play(
110 """Validate input parameters for play command.""" 124 self, ctx: discord.ApplicationContext, url: str
111 if not file and not stream: 125 ) -> None:
112 await ctx.respond("Musisz podać plik do odtworzenia lub adres streamu.", ephemeral=True)
113 return False
114
115 if file and stream:
116 await ctx.respond("Możesz podać tylko plik LUB stream, nie oba naraz.", ephemeral=True)
117 return False
118
119 return True
120
121 async def _handle_stream_play(self, ctx: discord.ApplicationContext, stream: str) -> None:
122 """Handle internet stream playback."""
123 await audio_playback.AudioPlaybackManager.play(ctx, stream)
124
125 async def _handle_compressed_file_play(self, ctx: discord.ApplicationContext, url: str) -> None:
126 """Download from copyparty, compress, then play locally.""" 126 """Download from copyparty, compress, then play locally."""
127 import aiohttp 127 import aiohttp
128
128 local_path = None 129 local_path = None
129 compressed_path = None 130 compressed_path = None
130 try: 131 try:
@@ -133,21 +134,25 @@ class PlayCommands(commands.Cog):
133 if response.status != 200: 134 if response.status != 200:
134 await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True) 135 await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True)
135 return 136 return
136 137
137 filename = url.split('/')[-1].split('?')[0] 138 filename = url.split("/")[-1].split("?")[0]
138 filename = urllib.parse.unquote(filename) 139 filename = urllib.parse.unquote(filename)
139 local_path = os.path.join(self.temp_dir, filename) 140 local_path = os.path.join(self.temp_dir, filename)
140 os.makedirs(self.temp_dir, exist_ok=True) 141 os.makedirs(self.temp_dir, exist_ok=True)
141 142
142 with open(local_path, 'wb') as f: 143 with open(local_path, "wb") as f:
143 async for chunk in response.content.iter_chunked(8192): 144 async for chunk in response.content.iter_chunked(8192):
144 f.write(chunk) 145 f.write(chunk)
145 146
146 compressed_path = await media_processing.AudioCompressor.compress(ctx, local_path) 147 compressed_path = await media_processing.AudioCompressor.compress(
148 ctx, local_path
149 )
147 if not compressed_path: 150 if not compressed_path:
148 return 151 return
149 152
150 await audio_playback.AudioPlaybackManager.play(ctx, compressed_path, delete_after_play=True) 153 await audio_playback.AudioPlaybackManager.play(
154 ctx, compressed_path, delete_after_play=True
155 )
151 except Exception as e: 156 except Exception as e:
152 logger.error(f"Error in compressed playback: {str(e)}", exc_info=True) 157 logger.error(f"Error in compressed playback: {str(e)}", exc_info=True)
153 await ctx.respond(f"Błąd: {str(e)}", ephemeral=True) 158 await ctx.respond(f"Błąd: {str(e)}", ephemeral=True)
@@ -158,10 +163,12 @@ class PlayCommands(commands.Cog):
158 except Exception: 163 except Exception:
159 pass 164 pass
160 165
161 async def _handle_local_file_play(self, ctx: discord.ApplicationContext, file: str, compressed: bool) -> None: 166 async def _handle_local_file_play(
167 self, ctx: discord.ApplicationContext, file: str, compressed: bool
168 ) -> None:
162 """Handle file playback from copyparty URL.""" 169 """Handle file playback from copyparty URL."""
163 url = self.path_mapping.get(file, file) 170 url = self.path_mapping.get(file, file)
164 171
165 if compressed: 172 if compressed:
166 await self._handle_compressed_file_play(ctx, url) 173 await self._handle_compressed_file_play(ctx, url)
167 else: 174 else:
@@ -169,46 +176,47 @@ class PlayCommands(commands.Cog):
169 176
170 @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!") 177 @discord.slash_command(name="grajcuj", description="grajcujmy wszyscy!")
171 async def play( 178 async def play(
172 self, 179 self,
173 ctx: discord.ApplicationContext, 180 ctx: discord.ApplicationContext,
174 file: str = discord.Option( 181 melodia: str = discord.Option(
175 str, 182 str,
176 "wyszukaj plik z serwera", 183 "odtwórz plik z serwera bądź strumienia",
177 autocomplete=_search_autocomplete, 184 autocomplete=_search_autocomplete,
178 required=False 185 required=False,
179 ), 186 ),
180 stream: str = discord.Option( 187 file: discord.Attachment = discord.Option(
181 str, 188 discord.Attachment, "plik do zagrania", required=False
182 "odtwórz stream internetowy (e.g. radio)", 189 ),
183 required=False 190 compressed: bool = discord.Option(
184 ), 191 bool, "Czy skompresować dźwięk?", default=False
185 compressed: bool = discord.Option( 192 ),
186 bool,
187 "Czy skompresować dźwięk?",
188 default=False
189 )
190 ): 193 ):
191 """Odtwarza wybrany plik dźwiękowy lub stream internetowy.""" 194 """Odtwarza wybrany plik dźwiękowy lub stream internetowy."""
192 await ctx.defer() 195 await ctx.defer()
193 196
194 if not await self._validate_play_inputs(file, stream, ctx): 197 if melodia and file:
198 await ctx.respond(
199 "Możesz podać plik z serwera LUB upload, nie oba naraz.", ephemeral=True
200 )
195 return 201 return
196
197 try:
198 if stream:
199 await self._handle_stream_play(ctx, stream)
200 else:
201 await self._handle_local_file_play(ctx, file, compressed)
202 except Exception as e:
203 logger.error(f"Error playing: {str(e)}", exc_info=True)
204 await ctx.respond(f"Wystąpił błąd podczas odtwarzania: {str(e)}", ephemeral=True)
205 202
206 @discord.slash_command(name="grajcuj_plik", description="grajcuj plik") 203 if not melodia and not file:
207 async def play_file(self, ctx: discord.ApplicationContext, attachment: discord.Attachment = discord.Option(discord.Attachment, "co zagrać??", required=True)): 204 await ctx.respond("Musisz podać plik do odtworzenia.", ephemeral=True)
208 await ctx.defer() 205 return
209 file_path: PathLike[str] | str = os.path.join(self.temp_dir, attachment.filename) 206
210 await attachment.save(file_path) 207 if file:
211 await audio_playback.AudioPlaybackManager.play(ctx, file_path) 208 file_path: PathLike[str] | str = os.path.join(self.temp_dir, file.filename)
209 await file.save(file_path)
210 await audio_playback.AudioPlaybackManager.play(ctx, file_path)
211 else:
212 try:
213 await self._handle_local_file_play(ctx, melodia, compressed)
214 except Exception as e:
215 logger.error(f"Error playing: {str(e)}", exc_info=True)
216 await ctx.respond(
217 f"Wystąpił błąd podczas odtworzenia: {str(e)}", ephemeral=True
218 )
219
212 220
213def setup(bot): 221def setup(bot):
214 bot.add_cog(PlayCommands(bot)) 222 bot.add_cog(PlayCommands(bot))