aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/music/play_commands.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--cogs/music/play_commands.py108
1 files changed, 105 insertions, 3 deletions
diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py
index a82fa9d..5373436 100644
--- a/cogs/music/play_commands.py
+++ b/cogs/music/play_commands.py
@@ -9,16 +9,22 @@ import urllib.parse
9import asyncio 9import asyncio
10from os import PathLike 10from os import PathLike
11from typing import Awaitable, Callable, List, Tuple 11from typing import Awaitable, Callable, List, Tuple
12import re
13from yt_dlp import YoutubeDL
12 14
13import discord 15import discord
14from discord.commands import OptionChoice 16from discord.commands import OptionChoice
15from discord.ext import commands 17from discord.ext import commands
16 18
17from cogs.music import audio_playback, media_processing 19from cogs.music import audio_playback, media_processing
18from cogs.utils import file_search as search_utils 20from cogs.utils.file_search import UniversalSearcher
19 21
20logger = logging.getLogger(__name__) 22logger = logging.getLogger(__name__)
21 23
24EXTERNAL_URL_PATTERN = re.compile(
25 r"^https://(?=.*(?:youtube|youtu\.be|soundcloud|bandcamp))"
26)
27
22 28
23class PlayCommands(commands.Cog): 29class PlayCommands(commands.Cog):
24 """Discord slash commands for music playback.""" 30 """Discord slash commands for music playback."""
@@ -89,9 +95,12 @@ class PlayCommands(commands.Cog):
89 self, ctx: discord.AutocompleteContext 95 self, ctx: discord.AutocompleteContext
90 ) -> List[OptionChoice]: 96 ) -> List[OptionChoice]:
91 """Autocomplete handler for search queries.""" 97 """Autocomplete handler for search queries."""
98 if ctx.value.lower().startswith(("http://", "https://")):
99 return []
100
92 query = ctx.value.lower() 101 query = ctx.value.lower()
93 try: 102 try:
94 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete_audio( 103 results = await UniversalSearcher.search_copyparty_autocomplete_audio(
95 query, max_results=25 104 query, max_results=25
96 ) 105 )
97 choices = [] 106 choices = []
@@ -164,6 +173,96 @@ class PlayCommands(commands.Cog):
164 except Exception: 173 except Exception:
165 pass 174 pass
166 175
176 async def _handle_external_url_play(
177 self, ctx: discord.ApplicationContext, url: str
178 ) -> None:
179 """Download from external URL, upload to copyparty, then play."""
180 local_path = None
181 try:
182 ytdl_opts = {
183 "outtmpl": os.path.join(self.temp_dir, "%(title)s.%(ext)s"),
184 "postprocessors": [
185 {
186 "key": "FFmpegExtractAudio",
187 "preferredcodec": "mp3",
188 "preferredquality": "192",
189 }
190 ],
191 "force_ipv4": True,
192 }
193
194 loop = asyncio.get_event_loop()
195 with YoutubeDL(ytdl_opts) as ydl:
196 info_dict = await loop.run_in_executor(
197 None, lambda: ydl.extract_info(url, download=True)
198 )
199 title = info_dict.get("title", "nieznany")
200 base_filename = os.path.splitext(ydl.prepare_filename(info_dict))[0]
201 local_path = f"{base_filename}.mp3"
202
203 if not os.path.exists(local_path):
204 await ctx.respond("Nie udało się pobrać pliku.", ephemeral=True)
205 return
206
207 upload_task = asyncio.create_task(self._upload_to_copyparty_async(local_path))
208
209 await audio_playback.AudioPlaybackManager.play(
210 ctx, local_path, delete_after_play=True
211 )
212
213 try:
214 await asyncio.wait_for(upload_task, timeout=30.0)
215 except asyncio.TimeoutError:
216 logger.warning("Upload timed out")
217 except Exception as e:
218 logger.error(f"Error downloading external URL: {str(e)}", exc_info=True)
219 if local_path and os.path.exists(local_path):
220 try:
221 os.remove(local_path)
222 except Exception:
223 pass
224 await ctx.respond(f"Błąd pobierania: {str(e)}", ephemeral=True)
225
226 async def _upload_to_copyparty_async(self, file_path: str) -> None:
227 """Upload file to copyparty asynchronously (fire and forget)."""
228 import aiohttp
229
230 try:
231 logger.info(f"Starting async upload: {file_path}")
232 baseurl = os.getenv("COPYPARTY_URL_UPLOAD")
233 password = os.getenv("COPYPARTY_PASSWORD", "")
234
235 logger.info(f"Upload - baseurl: {baseurl}, password set: {bool(password)}")
236
237 if not baseurl:
238 logger.error("COPYPARTY_URL_UPLOAD not set!")
239 return
240
241 with open(file_path, "rb") as f:
242 content = f.read()
243
244 files = aiohttp.FormData()
245 files.add_field(
246 "f",
247 content,
248 filename=os.path.basename(file_path),
249 content_type="application/octet-stream",
250 )
251 headers = {"Cookie": f"cppwd={password}"}
252 params = {"j": "1", "ck": "b2"}
253
254 logger.info(f"Posting to {baseurl}")
255 async with aiohttp.ClientSession() as session:
256 async with session.post(baseurl, data=files, headers=headers, params=params) as response:
257 text = await response.text()
258 logger.info(f"Upload response: {response.status} - {text[:200] if text else 'empty'}")
259
260 if response.status in [200, 201]:
261 result = await response.json()
262 logger.info(f"Uploaded to copyparty: {result.get('fileurl')}")
263 except Exception as e:
264 logger.error(f"Async upload failed: {e}", exc_info=True)
265
167 async def _handle_local_file_play( 266 async def _handle_local_file_play(
168 self, ctx: discord.ApplicationContext, file: str, compressed: bool 267 self, ctx: discord.ApplicationContext, file: str, compressed: bool
169 ) -> None: 268 ) -> None:
@@ -211,7 +310,10 @@ class PlayCommands(commands.Cog):
211 await audio_playback.AudioPlaybackManager.play(ctx, file_path) 310 await audio_playback.AudioPlaybackManager.play(ctx, file_path)
212 else: 311 else:
213 try: 312 try:
214 await self._handle_local_file_play(ctx, melodia, compressed) 313 if EXTERNAL_URL_PATTERN.match(melodia.lower()):
314 await self._handle_external_url_play(ctx, melodia)
315 else:
316 await self._handle_local_file_play(ctx, melodia, compressed)
215 except Exception as e: 317 except Exception as e:
216 logger.error(f"Error playing: {str(e)}", exc_info=True) 318 logger.error(f"Error playing: {str(e)}", exc_info=True)
217 await ctx.respond( 319 await ctx.respond(