aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-04-20 15:05:51 +0200
committersalmonik <raisersoft@proton.me>2026-04-20 15:05:51 +0200
commitc687d9fa1d89fef8ddc7150d1487dfef07d11abb (patch)
tree61166219a1fe77c22b4623efe9320fcb1b866525
parentlil cron fix (diff)
downloadsenator-sperma-c687d9fa1d89fef8ddc7150d1487dfef07d11abb.tar.gz
senator-sperma-c687d9fa1d89fef8ddc7150d1487dfef07d11abb.zip
uploader.py and voice fixes
Signed-off-by: salmonik <raisersoft@proton.me>
-rw-r--r--cogs/ai.py2
-rw-r--r--cogs/music/audio_playback.py24
-rw-r--r--cogs/uploader.py154
-rw-r--r--cogs/vclogs.py5
-rw-r--r--main.py1
5 files changed, 176 insertions, 10 deletions
diff --git a/cogs/ai.py b/cogs/ai.py
index 6f79ac5..c9237c4 100644
--- a/cogs/ai.py
+++ b/cogs/ai.py
@@ -66,7 +66,7 @@ class AI(commands.Cog):
66 tts_buffer = io.BytesIO() 66 tts_buffer = io.BytesIO()
67 tts_obj.write_to_fp(tts_buffer) 67 tts_obj.write_to_fp(tts_buffer)
68 tts_buffer.seek(0) 68 tts_buffer.seek(0)
69 await audio_playback.AudioPlaybackManager.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True) 69 await audio_playback.AudioPlaybackManager.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True, interrupt=False)
70 70
71 else: 71 else:
72 await ctx.send_followup(response_text) 72 await ctx.send_followup(response_text)
diff --git a/cogs/music/audio_playback.py b/cogs/music/audio_playback.py
index 28c6118..1c715de 100644
--- a/cogs/music/audio_playback.py
+++ b/cogs/music/audio_playback.py
@@ -137,8 +137,16 @@ class AudioPlaybackManager:
137 return absolute_source, display_name, is_url 137 return absolute_source, display_name, is_url
138 138
139 @staticmethod 139 @staticmethod
140 async def execute_playback(ctx, source, display_name, is_url): 140 async def execute_playback(ctx, source, display_name, is_url, interrupt=True):
141 """Execute the actual audio playback.""" 141 """Execute the actual audio playback.
142
143 Args:
144 ctx: Discord context
145 source: Audio source path or URL
146 display_name: Name to display
147 is_url: Whether source is a URL
148 interrupt: If True, stop any currently playing audio. If False, skip if audio is playing.
149 """
142 target_channel = await AudioPlaybackManager.validate_voice_channel(ctx) 150 target_channel = await AudioPlaybackManager.validate_voice_channel(ctx)
143 if not target_channel: 151 if not target_channel:
144 return False 152 return False
@@ -169,9 +177,12 @@ class AudioPlaybackManager:
169 logger.info(f"Moving to voice channel: {target_channel.name}") 177 logger.info(f"Moving to voice channel: {target_channel.name}")
170 await vc.move_to(target_channel) 178 await vc.move_to(target_channel)
171 179
172 # Stop any currently playing audio 180 # Stop any currently playing audio (if interrupt is True)
173 if vc.is_playing() or vc.is_paused(): # type: ignore[union-attr] 181 if interrupt and (vc.is_playing() or vc.is_paused()): # type: ignore[union-attr]
174 vc.stop() # type: ignore[union-attr] 182 vc.stop() # type: ignore[union-attr]
183 elif not interrupt and vc.is_playing(): # type: ignore[union-attr]
184 logger.info("Audio already playing and interrupt=False, skipping playback")
185 return False
175 186
176 # Prepare audio source 187 # Prepare audio source
177 ( 188 (
@@ -232,13 +243,14 @@ class AudioPlaybackManager:
232 return prepared_source, display_name, is_url 243 return prepared_source, display_name, is_url
233 244
234 @staticmethod 245 @staticmethod
235 async def play(ctx, source, delete_after_play=False): 246 async def play(ctx, source, delete_after_play=False, interrupt=True):
236 """Play audio from a local file or URL. 247 """Play audio from a local file or URL.
237 248
238 Args: 249 Args:
239 ctx: Discord context 250 ctx: Discord context
240 source: Either a local file path or a URL to stream from 251 source: Either a local file path or a URL to stream from
241 delete_after_play: If True, delete the file after playing (only for local files) 252 delete_after_play: If True, delete the file after playing (only for local files)
253 interrupt: If True, stop any currently playing audio. If False, skip if audio is playing.
242 """ 254 """
243 # Validate and prepare source 255 # Validate and prepare source
244 ( 256 (
@@ -254,7 +266,7 @@ class AudioPlaybackManager:
254 266
255 # Execute playback 267 # Execute playback
256 success = await AudioPlaybackManager.execute_playback( 268 success = await AudioPlaybackManager.execute_playback(
257 ctx, prepared_source, display_name, is_url 269 ctx, prepared_source, display_name, is_url, interrupt
258 ) 270 )
259 271
260 # Cleanup if needed 272 # Cleanup if needed
diff --git a/cogs/uploader.py b/cogs/uploader.py
new file mode 100644
index 0000000..68a3938
--- /dev/null
+++ b/cogs/uploader.py
@@ -0,0 +1,154 @@
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
6import os
7import aiofiles
8import requests
9import urllib.parse
10from typing import List, Optional
11import discord
12from discord.ext import commands
13from discord import AutocompleteContext
14from cogs.utils import file_search as search_utils
15import logging
16
17logger = logging.getLogger(__name__)
18
19
20class Uploader(commands.Cog):
21 def __init__(self, bot):
22 from cogs.utils.temp import get_temp_dir
23 self.bot = bot
24 self.temp_dir = get_temp_dir()
25 self._cached_urls = {}
26 self._uploadurl = os.getenv("COPYPARTY_URL_UPLOAD")
27
28 async def _file_autocomplete(self, ctx: AutocompleteContext):
29 query = ctx.value.lower() if ctx.value else ""
30 results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25)
31
32 user_id = ctx.interaction.user.id
33 self._cached_urls[user_id] = {urllib.parse.unquote(url.split('/')[-1]): url for url in results}
34
35 choices = []
36 for url in results[:25]:
37 filename = urllib.parse.unquote(url.split('/')[-1])
38 display_name = filename[:99] + "…" if len(filename) > 100 else filename
39 choices.append(discord.OptionChoice(name=display_name, value=filename))
40 return choices
41
42 async def _download_from_url(self, url: str) -> Optional[str]:
43 """Download file from copyparty URL and return local path"""
44 try:
45 response = requests.get(url)
46 if response.status_code != 200:
47 return None
48
49 filename = url.split('/')[-1].split('?')[0]
50 file_path = os.path.join(self.temp_dir, filename)
51
52 async with aiofiles.open(file_path, 'wb') as f:
53 await f.write(response.content)
54 return file_path
55 except Exception as e:
56 logger.error(f"Error downloading from {url}: {e}")
57 return None
58
59 async def _upload_to_copyparty(self, file_path: str) -> Optional[str]:
60 """Upload file to copyparty and return download URL"""
61 try:
62 async with aiofiles.open(file_path, 'rb') as f:
63 file_content = await f.read()
64
65 files = {'f': (os.path.basename(file_path), file_content, 'application/octet-stream')}
66 headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'}
67 params = {'j': '1', 'ck': 'b2'}
68
69 response = requests.post(self._uploadurl, files=files, headers=headers, params=params)
70
71 if response.status_code in [200, 201]:
72 result = response.json()
73 return result.get('fileurl')
74 except Exception as e:
75 logger.error(f"Upload error: {e}")
76 return None
77
78 async def _upload_file(self, file_path: str) -> Optional[str]:
79 """Upload a file and return the URL"""
80 if not self._uploadurl:
81 logger.error("COPYPARTY_URL_UPLOAD not set!")
82 return None
83
84 return await self._upload_to_copyparty(file_path)
85
86 @commands.message_command(
87 name="upload",
88 description="wyślij załączniki na copyparty",
89 )
90 async def message_upload(self, ctx, message: discord.Message):
91 if not message.attachments:
92 await ctx.respond("Brak załączników w wiadomości", ephemeral=True)
93 return
94
95 await ctx.defer()
96
97 uploaded_urls = []
98 failed_count = 0
99
100 for attachment in message.attachments:
101 file_path = os.path.join(self.temp_dir, attachment.filename)
102 await attachment.save(file_path)
103
104 url = await self._upload_file(file_path)
105 if url:
106 uploaded_urls.append(url)
107 else:
108 failed_count += 1
109
110 if os.path.exists(file_path):
111 os.remove(file_path)
112
113 if uploaded_urls:
114 response = f"Wysłano {len(uploaded_urls)} plików:\n" + "\n".join(uploaded_urls)
115 if failed_count > 0:
116 response += f"\n\nNie wysłano {failed_count} plików"
117 await ctx.respond(response)
118 else:
119 await ctx.respond("Nie udało się wysłać żadnych plików", ephemeral=True)
120
121 @commands.slash_command(
122 name="upload",
123 description="wyślij plik na copyparty",
124 )
125 @discord.option(
126 name="zalacznik",
127 description="plik z discorda",
128 required=True,
129 type=discord.Attachment
130 )
131 async def upload(self, ctx, zalacznik: discord.Attachment = None):
132 await ctx.defer()
133
134 file_path = None
135 file_path = os.path.join(self.temp_dir, zalacznik.filename)
136 await zalacznik.save(file_path)
137
138 if not file_path or not os.path.exists(file_path):
139 await ctx.respond("Nie udało się pobrać pliku", ephemeral=True)
140 return
141
142 try:
143 dl_url = await self._upload_file(file_path)
144 if dl_url:
145 await ctx.respond(dl_url)
146 else:
147 await ctx.respond("Nie udało się wysłać pliku", ephemeral=True)
148 finally:
149 if file_path and os.path.exists(file_path):
150 os.remove(file_path)
151
152
153def setup(bot):
154 bot.add_cog(Uploader(bot))
diff --git a/cogs/vclogs.py b/cogs/vclogs.py
index fb0a2e0..d0c4825 100644
--- a/cogs/vclogs.py
+++ b/cogs/vclogs.py
@@ -104,9 +104,8 @@ class VcLogs(commands.Cog):
104 104
105 if voice_client: 105 if voice_client:
106 if is_playing: 106 if is_playing:
107 logger.info("voice already playing, stopping current audio") 107 logger.info("voice already playing, skipping TTS")
108 voice_client.stop() 108 return
109 await asyncio.sleep(0.1)
110 109
111 logger.info("preparing to play audio") 110 logger.info("preparing to play audio")
112 audio_source = discord.FFmpegPCMAudio(tts_file) 111 audio_source = discord.FFmpegPCMAudio(tts_file)
diff --git a/main.py b/main.py
index 1f5e3f6..37a11c6 100644
--- a/main.py
+++ b/main.py
@@ -75,6 +75,7 @@ if __name__ == "__main__":
75 "cogs.cron", 75 "cogs.cron",
76 "cogs.qr", 76 "cogs.qr",
77 "cogs.voice", 77 "cogs.voice",
78 "cogs.uploader",
78 ] 79 ]
79 80
80 try: 81 try: