From c687d9fa1d89fef8ddc7150d1487dfef07d11abb Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 20 Apr 2026 15:05:51 +0200 Subject: uploader.py and voice fixes Signed-off-by: salmonik --- cogs/ai.py | 2 +- cogs/music/audio_playback.py | 24 +++++-- cogs/uploader.py | 154 +++++++++++++++++++++++++++++++++++++++++++ cogs/vclogs.py | 5 +- main.py | 1 + 5 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 cogs/uploader.py 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): tts_buffer = io.BytesIO() tts_obj.write_to_fp(tts_buffer) tts_buffer.seek(0) - await audio_playback.AudioPlaybackManager.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True) + await audio_playback.AudioPlaybackManager.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True, interrupt=False) else: 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: return absolute_source, display_name, is_url @staticmethod - async def execute_playback(ctx, source, display_name, is_url): - """Execute the actual audio playback.""" + async def execute_playback(ctx, source, display_name, is_url, interrupt=True): + """Execute the actual audio playback. + + Args: + ctx: Discord context + source: Audio source path or URL + display_name: Name to display + is_url: Whether source is a URL + interrupt: If True, stop any currently playing audio. If False, skip if audio is playing. + """ target_channel = await AudioPlaybackManager.validate_voice_channel(ctx) if not target_channel: return False @@ -169,9 +177,12 @@ class AudioPlaybackManager: logger.info(f"Moving to voice channel: {target_channel.name}") await vc.move_to(target_channel) - # Stop any currently playing audio - if vc.is_playing() or vc.is_paused(): # type: ignore[union-attr] + # Stop any currently playing audio (if interrupt is True) + if interrupt and (vc.is_playing() or vc.is_paused()): # type: ignore[union-attr] vc.stop() # type: ignore[union-attr] + elif not interrupt and vc.is_playing(): # type: ignore[union-attr] + logger.info("Audio already playing and interrupt=False, skipping playback") + return False # Prepare audio source ( @@ -232,13 +243,14 @@ class AudioPlaybackManager: return prepared_source, display_name, is_url @staticmethod - async def play(ctx, source, delete_after_play=False): + async def play(ctx, source, delete_after_play=False, interrupt=True): """Play audio from a local file or URL. Args: ctx: Discord context source: Either a local file path or a URL to stream from delete_after_play: If True, delete the file after playing (only for local files) + interrupt: If True, stop any currently playing audio. If False, skip if audio is playing. """ # Validate and prepare source ( @@ -254,7 +266,7 @@ class AudioPlaybackManager: # Execute playback success = await AudioPlaybackManager.execute_playback( - ctx, prepared_source, display_name, is_url + ctx, prepared_source, display_name, is_url, interrupt ) # 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 @@ +# This file is part of Senator Sperma +# Licensed under the GPL-3.0 License +# Copyright (C) 2026 Salmonik @ salmonik@tuta.io +# https://codeberg.org/salmonik/senator-sperma + +import os +import aiofiles +import requests +import urllib.parse +from typing import List, Optional +import discord +from discord.ext import commands +from discord import AutocompleteContext +from cogs.utils import file_search as search_utils +import logging + +logger = logging.getLogger(__name__) + + +class Uploader(commands.Cog): + def __init__(self, bot): + from cogs.utils.temp import get_temp_dir + self.bot = bot + self.temp_dir = get_temp_dir() + self._cached_urls = {} + self._uploadurl = os.getenv("COPYPARTY_URL_UPLOAD") + + async def _file_autocomplete(self, ctx: AutocompleteContext): + query = ctx.value.lower() if ctx.value else "" + results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25) + + user_id = ctx.interaction.user.id + self._cached_urls[user_id] = {urllib.parse.unquote(url.split('/')[-1]): url for url in results} + + choices = [] + for url in results[:25]: + filename = urllib.parse.unquote(url.split('/')[-1]) + display_name = filename[:99] + "…" if len(filename) > 100 else filename + choices.append(discord.OptionChoice(name=display_name, value=filename)) + return choices + + async def _download_from_url(self, url: str) -> Optional[str]: + """Download file from copyparty URL and return local path""" + try: + response = requests.get(url) + if response.status_code != 200: + return None + + filename = url.split('/')[-1].split('?')[0] + file_path = os.path.join(self.temp_dir, filename) + + async with aiofiles.open(file_path, 'wb') as f: + await f.write(response.content) + return file_path + except Exception as e: + logger.error(f"Error downloading from {url}: {e}") + return None + + async def _upload_to_copyparty(self, file_path: str) -> Optional[str]: + """Upload file to copyparty and return download URL""" + try: + async with aiofiles.open(file_path, 'rb') as f: + file_content = await f.read() + + files = {'f': (os.path.basename(file_path), file_content, 'application/octet-stream')} + headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'} + params = {'j': '1', 'ck': 'b2'} + + response = requests.post(self._uploadurl, files=files, headers=headers, params=params) + + if response.status_code in [200, 201]: + result = response.json() + return result.get('fileurl') + except Exception as e: + logger.error(f"Upload error: {e}") + return None + + async def _upload_file(self, file_path: str) -> Optional[str]: + """Upload a file and return the URL""" + if not self._uploadurl: + logger.error("COPYPARTY_URL_UPLOAD not set!") + return None + + return await self._upload_to_copyparty(file_path) + + @commands.message_command( + name="upload", + description="wyślij załączniki na copyparty", + ) + async def message_upload(self, ctx, message: discord.Message): + if not message.attachments: + await ctx.respond("Brak załączników w wiadomości", ephemeral=True) + return + + await ctx.defer() + + uploaded_urls = [] + failed_count = 0 + + for attachment in message.attachments: + file_path = os.path.join(self.temp_dir, attachment.filename) + await attachment.save(file_path) + + url = await self._upload_file(file_path) + if url: + uploaded_urls.append(url) + else: + failed_count += 1 + + if os.path.exists(file_path): + os.remove(file_path) + + if uploaded_urls: + response = f"Wysłano {len(uploaded_urls)} plików:\n" + "\n".join(uploaded_urls) + if failed_count > 0: + response += f"\n\nNie wysłano {failed_count} plików" + await ctx.respond(response) + else: + await ctx.respond("Nie udało się wysłać żadnych plików", ephemeral=True) + + @commands.slash_command( + name="upload", + description="wyślij plik na copyparty", + ) + @discord.option( + name="zalacznik", + description="plik z discorda", + required=True, + type=discord.Attachment + ) + async def upload(self, ctx, zalacznik: discord.Attachment = None): + await ctx.defer() + + file_path = None + file_path = os.path.join(self.temp_dir, zalacznik.filename) + await zalacznik.save(file_path) + + if not file_path or not os.path.exists(file_path): + await ctx.respond("Nie udało się pobrać pliku", ephemeral=True) + return + + try: + dl_url = await self._upload_file(file_path) + if dl_url: + await ctx.respond(dl_url) + else: + await ctx.respond("Nie udało się wysłać pliku", ephemeral=True) + finally: + if file_path and os.path.exists(file_path): + os.remove(file_path) + + +def setup(bot): + 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): if voice_client: if is_playing: - logger.info("voice already playing, stopping current audio") - voice_client.stop() - await asyncio.sleep(0.1) + logger.info("voice already playing, skipping TTS") + return logger.info("preparing to play audio") 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__": "cogs.cron", "cogs.qr", "cogs.voice", + "cogs.uploader", ] try: -- cgit v1.2.3