diff options
| author | salmonik <raisersoft@proton.me> | 2026-04-18 00:24:50 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-04-18 00:24:50 +0200 |
| commit | a2b5b10625126b7e98643af9fe40569c9e004047 (patch) | |
| tree | 2b59a929d068bc28bbc21e8c8d8836e4d29270c0 /cogs/conversion.py | |
| parent | add mathematical plotting (diff) | |
| download | senator-sperma-a2b5b10625126b7e98643af9fe40569c9e004047.tar.gz senator-sperma-a2b5b10625126b7e98643af9fe40569c9e004047.zip | |
refactor + preparing bot for public use
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs/conversion.py')
| -rw-r--r-- | cogs/conversion.py | 198 |
1 files changed, 147 insertions, 51 deletions
diff --git a/cogs/conversion.py b/cogs/conversion.py index f61d30f..1117caf 100644 --- a/cogs/conversion.py +++ b/cogs/conversion.py | |||
| @@ -12,6 +12,11 @@ import asyncio | |||
| 12 | import aiofiles | 12 | import aiofiles |
| 13 | import mimetypes | 13 | import mimetypes |
| 14 | import subprocess | 14 | import subprocess |
| 15 | import urllib.parse | ||
| 16 | import shlex | ||
| 17 | from typing import Optional | ||
| 18 | from discord import AutocompleteContext | ||
| 19 | from cogs.utils import file_search as search_utils | ||
| 15 | 20 | ||
| 16 | logger = logging.getLogger(__name__) | 21 | logger = logging.getLogger(__name__) |
| 17 | 22 | ||
| @@ -29,28 +34,63 @@ def filetype(filename): | |||
| 29 | class Conversion(commands.Cog): | 34 | class Conversion(commands.Cog): |
| 30 | def __init__(self, bot): | 35 | def __init__(self, bot): |
| 31 | self.bot = bot | 36 | self.bot = bot |
| 37 | from cogs.utils.temp import get_temp_dir | ||
| 38 | from cogs.utils.settings import get_baseurl | ||
| 39 | self.temp_dir = get_temp_dir() | ||
| 40 | self._cached_urls = {} | ||
| 41 | self._baseurl = os.getenv("COPYPARTY_URL") or get_baseurl() | ||
| 42 | self._uploadurl = os.getenv("COPYPARTY_URL_UPLOAD") | ||
| 43 | |||
| 44 | async def _file_autocomplete(self, ctx: AutocompleteContext): | ||
| 45 | query = ctx.value.lower() if ctx.value else "" | ||
| 46 | results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25) | ||
| 47 | |||
| 48 | user_id = ctx.interaction.user.id | ||
| 49 | self._cached_urls[user_id] = {urllib.parse.unquote(url.split('/')[-1]): url for url in results} | ||
| 50 | |||
| 51 | choices = [] | ||
| 52 | for url in results[:25]: | ||
| 53 | filename = urllib.parse.unquote(url.split('/')[-1]) | ||
| 54 | display_name = filename[:99] + "…" if len(filename) > 100 else filename | ||
| 55 | choices.append(discord.OptionChoice(name=display_name, value=filename)) | ||
| 56 | return choices | ||
| 57 | |||
| 58 | async def _download_from_url(self, url: str) -> Optional[str]: | ||
| 59 | """Download file from copyparty URL and return local path""" | ||
| 60 | try: | ||
| 61 | response = requests.get(url) | ||
| 62 | if response.status_code != 200: | ||
| 63 | return None | ||
| 64 | |||
| 65 | filename = url.split('/')[-1].split('?')[0] | ||
| 66 | file_path = os.path.join(self.temp_dir, filename) | ||
| 67 | |||
| 68 | async with aiofiles.open(file_path, 'wb') as f: | ||
| 69 | await f.write(response.content) | ||
| 70 | return file_path | ||
| 71 | except Exception as e: | ||
| 72 | logger.error(f"Error downloading from {url}: {e}") | ||
| 73 | return None | ||
| 32 | 74 | ||
| 33 | async def _setup_file_paths(self, plik, output_format): | 75 | async def _setup_file_paths(self, plik, output_format): |
| 34 | """Setup input and output file paths""" | 76 | """Setup input and output file paths""" |
| 35 | bot_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 77 | from cogs.utils.temp import get_temp_dir |
| 36 | temp_dir = os.path.join(bot_dir, "temp") | 78 | |
| 37 | if not os.path.exists(temp_dir): | 79 | temp_dir = get_temp_dir() |
| 38 | os.makedirs(temp_dir) | ||
| 39 | |||
| 40 | file_path = os.path.join(temp_dir, plik.filename) | 80 | file_path = os.path.join(temp_dir, plik.filename) |
| 41 | filename_noext = os.path.splitext(plik.filename)[0] | 81 | filename_noext = os.path.splitext(plik.filename)[0] |
| 42 | await plik.save(file_path) | 82 | await plik.save(file_path) |
| 43 | 83 | ||
| 44 | input_path = file_path | 84 | input_path = file_path |
| 45 | output_path = os.path.join(temp_dir, f"{filename_noext}k.{output_format}") | 85 | output_path = os.path.join(temp_dir, f"{filename_noext}.{output_format}") |
| 46 | return input_path, output_path | 86 | return input_path, output_path |
| 47 | 87 | ||
| 48 | def _build_conversion_command(self, file_type, input_path, output_path, args): | 88 | def _build_conversion_command(self, file_type, input_path, output_path, args): |
| 49 | """Build the appropriate conversion command based on file type""" | 89 | """Build the appropriate conversion command based on file type""" |
| 50 | if file_type in ["Video", "Audio"]: | 90 | if file_type in ["Video", "Audio"]: |
| 51 | return f"ffmpeg -i {input_path} {args} {output_path}" | 91 | return f"ffmpeg -i {shlex.quote(input_path)} {args} {shlex.quote(output_path)}" |
| 52 | elif file_type == "Image": | 92 | elif file_type == "Image": |
| 53 | return f"magick {input_path} {args} {output_path}" | 93 | return f"magick {shlex.quote(input_path)} {args} {shlex.quote(output_path)}" |
| 54 | else: | 94 | else: |
| 55 | return None | 95 | return None |
| 56 | 96 | ||
| @@ -58,30 +98,31 @@ class Conversion(commands.Cog): | |||
| 58 | """Upload file to copyparty and return download URL""" | 98 | """Upload file to copyparty and return download URL""" |
| 59 | try: | 99 | try: |
| 60 | async with aiofiles.open(output_path, 'rb') as f: | 100 | async with aiofiles.open(output_path, 'rb') as f: |
| 61 | files = { | 101 | file_content = await f.read() |
| 62 | 'f': (output_path, f, 'application/octet-stream') | 102 | |
| 63 | } | 103 | files = { |
| 64 | headers = { | 104 | 'f': (os.path.basename(output_path), file_content, 'application/octet-stream') |
| 65 | 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' | 105 | } |
| 66 | } | 106 | headers = { |
| 67 | params = { | 107 | 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' |
| 68 | 'j': '1', | 108 | } |
| 69 | 'ck': 'b2' | 109 | params = { |
| 70 | } | 110 | 'j': '1', |
| 71 | 111 | 'ck': 'b2' | |
| 72 | base_url = "https://catbox.2137697.xyz/sperma/" | 112 | } |
| 73 | response = requests.post(base_url, files=files, headers=headers, params=params) | 113 | |
| 74 | 114 | response = requests.post(self._uploadurl, files=files, headers=headers, params=params) | |
| 75 | if response.status_code in [200, 201]: | 115 | |
| 76 | result = response.json() | 116 | if response.status_code in [200, 201]: |
| 77 | if 'fileurl' in result: | 117 | result = response.json() |
| 78 | return result['fileurl'] | 118 | if 'fileurl' in result: |
| 79 | else: | 119 | return result['fileurl'] |
| 80 | raise ValueError(f"Server response missing fileurl: {result}") | ||
| 81 | else: | 120 | else: |
| 82 | error_msg = response.text if response.text else 'Unknown error' | 121 | raise ValueError(f"Server response missing fileurl: {result}") |
| 83 | raise requests.RequestException(f"HTTP {response.status_code}: {error_msg}") | 122 | else: |
| 84 | 123 | error_msg = response.text if response.text else 'Unknown error' | |
| 124 | raise requests.RequestException(f"HTTP {response.status_code}: {error_msg}") | ||
| 125 | |||
| 85 | except Exception as e: | 126 | except Exception as e: |
| 86 | logger.error(f"copyparty upload error: {e}", exc_info=True) | 127 | logger.error(f"copyparty upload error: {e}", exc_info=True) |
| 87 | raise | 128 | raise |
| @@ -105,44 +146,99 @@ class Conversion(commands.Cog): | |||
| 105 | 146 | ||
| 106 | @commands.slash_command(name="konwersja", description="konwertuj plik na inny format") | 147 | @commands.slash_command(name="konwersja", description="konwertuj plik na inny format") |
| 107 | @discord.option( | 148 | @discord.option( |
| 108 | "plik", | 149 | name="plik", |
| 109 | description="plik do konwersji", | 150 | description="plik z copyparty", |
| 110 | required=True, | 151 | required=False, |
| 152 | autocomplete=_file_autocomplete | ||
| 153 | ) | ||
| 154 | @discord.option( | ||
| 155 | name="zalacznik", | ||
| 156 | description="plik z discorda", | ||
| 157 | required=False, | ||
| 111 | type=discord.Attachment | 158 | type=discord.Attachment |
| 112 | ) | 159 | ) |
| 113 | @discord.option( | 160 | @discord.option( |
| 114 | "format", | 161 | name="serwer?", |
| 162 | description="wysłać plik na serwer?", | ||
| 163 | required=False, | ||
| 164 | type=bool, | ||
| 165 | default=False | ||
| 166 | ) | ||
| 167 | @discord.option( | ||
| 168 | name="format", | ||
| 115 | description="format konwersji", | 169 | description="format konwersji", |
| 116 | required=True, | 170 | required=True, |
| 117 | type=str | 171 | type=str |
| 118 | ) | 172 | ) |
| 119 | async def convert(self, ctx, plik, output_format, args): | 173 | async def convert(self, ctx, plik: str = None, zalacznik: discord.Attachment = None, server: bool = False, output_format: str = None): |
| 120 | await ctx.defer() | 174 | await ctx.defer() |
| 121 | 175 | ||
| 176 | input_path = None | ||
| 177 | original_filename = None | ||
| 178 | |||
| 122 | try: | 179 | try: |
| 123 | input_path, output_path = await self._setup_file_paths(plik, output_format) | 180 | if plik: |
| 181 | url = self._cached_urls.get(ctx.author.id, {}).get(plik) | ||
| 182 | if not url: | ||
| 183 | await ctx.respond("Wybierz plik z podpowiedzi", ephemeral=True) | ||
| 184 | return | ||
| 185 | input_path = await self._download_from_url(url) | ||
| 186 | original_filename = plik | ||
| 187 | elif zalacznik: | ||
| 188 | input_path = os.path.join(self.temp_dir, zalacznik.filename) | ||
| 189 | original_filename = zalacznik.filename | ||
| 190 | await zalacznik.save(input_path) | ||
| 191 | else: | ||
| 192 | await ctx.respond("Podaj plik z copyparty lub załącznik", ephemeral=True) | ||
| 193 | return | ||
| 194 | |||
| 195 | if not input_path or not os.path.exists(input_path): | ||
| 196 | await ctx.respond("Nie udało się pobrać pliku", ephemeral=True) | ||
| 197 | return | ||
| 198 | |||
| 199 | output_path = os.path.join(self.temp_dir, f"{os.path.splitext(original_filename)[0]}.{output_format}") | ||
| 200 | |||
| 124 | file_type = filetype(input_path) | 201 | file_type = filetype(input_path) |
| 125 | logger.info(f"Conversion started for: {file_type}") | 202 | logger.info(f"Conversion started for: {file_type}") |
| 126 | 203 | ||
| 127 | command = self._build_conversion_command(file_type, input_path, output_path, args) | 204 | command = self._build_conversion_command(file_type, input_path, output_path, "") |
| 205 | logger.info(f"Running: {command}") | ||
| 128 | if not command: | 206 | if not command: |
| 129 | await ctx.respond("Plik jest nieobsługiwany", ephemeral=True) | 207 | await ctx.respond("Plik jest nieobsługiwany", ephemeral=True) |
| 130 | logger.info("Unsupported file type") | ||
| 131 | return | 208 | return |
| 132 | 209 | ||
| 133 | proc = await asyncio.create_subprocess_exec(*command.split()) | 210 | proc = await asyncio.create_subprocess_exec( |
| 134 | await proc.wait() | 211 | *shlex.split(command), |
| 135 | 212 | stdout=asyncio.subprocess.PIPE, | |
| 136 | await self._handle_file_output(ctx, output_path) | 213 | stderr=asyncio.subprocess.PIPE |
| 214 | ) | ||
| 215 | stdout, stderr = await proc.communicate() | ||
| 216 | |||
| 217 | if proc.returncode != 0: | ||
| 218 | logger.error(f"FFmpeg error: {stderr.decode() if stderr else 'unknown'}") | ||
| 219 | await ctx.respond(f"Błąd konwersji: {stderr.decode() if stderr else 'unknown'}", ephemeral=True) | ||
| 220 | return | ||
| 221 | |||
| 222 | if os.path.getsize(output_path) > 1024 * 1024 * 10 or server: | ||
| 223 | dl_url = await self._upload_to_copyparty(output_path) | ||
| 224 | if dl_url: | ||
| 225 | await ctx.respond(f"Skonwertowano\n{dl_url}") | ||
| 226 | else: | ||
| 227 | await ctx.respond("Nie udało się wysłać na serwer", ephemeral=True) | ||
| 228 | else: | ||
| 229 | await ctx.respond(file=discord.File(output_path, filename=f"{original_filename}")) | ||
| 230 | |||
| 137 | logger.info(f"Conversion completed for: {file_type}") | 231 | logger.info(f"Conversion completed for: {file_type}") |
| 138 | 232 | ||
| 139 | except subprocess.CalledProcessError as e: | 233 | except subprocess.CalledProcessError as e: |
| 140 | await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True) | 234 | await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True) |
| 141 | except Exception as e: | 235 | except Exception as e: |
| 142 | await ctx.respond(f"An error occurred: {str(e)}", ephemeral=True) | 236 | await ctx.respond(f"An error occurred: {str(e)}", ephemeral=True) |
| 143 | finally: | 237 | finally: |
| 144 | if 'input_path' in locals(): | 238 | if input_path and os.path.exists(input_path): |
| 145 | self._cleanup_files(input_path, output_path) | 239 | os.remove(input_path) |
| 240 | if 'output_path' in locals() and os.path.exists(output_path): | ||
| 241 | os.remove(output_path) | ||
| 146 | 242 | ||
| 147 | def setup(bot): | 243 | def setup(bot): |
| 148 | bot.add_cog(Conversion(bot)) \ No newline at end of file | 244 | bot.add_cog(Conversion(bot)) |
