diff options
Diffstat (limited to 'cogs/conversion.py')
| -rw-r--r-- | cogs/conversion.py | 245 |
1 files changed, 0 insertions, 245 deletions
diff --git a/cogs/conversion.py b/cogs/conversion.py deleted file mode 100644 index f9e4c84..0000000 --- a/cogs/conversion.py +++ /dev/null | |||
| @@ -1,245 +0,0 @@ | |||
| 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 | |||
| 6 | import discord | ||
| 7 | import requests | ||
| 8 | from discord.ext import commands | ||
| 9 | import logging | ||
| 10 | import os | ||
| 11 | import asyncio | ||
| 12 | import aiofiles | ||
| 13 | import mimetypes | ||
| 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 | ||
| 20 | |||
| 21 | logger = logging.getLogger(__name__) | ||
| 22 | |||
| 23 | def filetype(filename): | ||
| 24 | mime_type, _ = mimetypes.guess_type(filename) | ||
| 25 | if mime_type: | ||
| 26 | if mime_type.startswith('video/'): | ||
| 27 | return "Video" | ||
| 28 | elif mime_type.startswith('audio/'): | ||
| 29 | return "Audio" | ||
| 30 | elif mime_type.startswith('image/'): | ||
| 31 | return "Image" | ||
| 32 | return "Unknown" | ||
| 33 | |||
| 34 | class Conversion(commands.Cog): | ||
| 35 | def __init__(self, bot): | ||
| 36 | self.bot = bot | ||
| 37 | from cogs.utils.temp import get_temp_dir | ||
| 38 | from cogs.utils.settings import get_baseurl, get_uploadurl | ||
| 39 | self.temp_dir = get_temp_dir() | ||
| 40 | self._cached_urls = {} | ||
| 41 | self._baseurl = get_baseurl() | ||
| 42 | self._uploadurl = get_uploadurl() | ||
| 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 | filename = urllib.parse.unquote(filename) | ||
| 67 | file_path = os.path.join(self.temp_dir, filename) | ||
| 68 | |||
| 69 | async with aiofiles.open(file_path, 'wb') as f: | ||
| 70 | await f.write(response.content) | ||
| 71 | return file_path | ||
| 72 | except Exception as e: | ||
| 73 | logger.error(f"Error downloading from {url}: {e}") | ||
| 74 | return None | ||
| 75 | |||
| 76 | async def _setup_file_paths(self, plik, output_format): | ||
| 77 | """Setup input and output file paths""" | ||
| 78 | from cogs.utils.temp import get_temp_dir | ||
| 79 | |||
| 80 | temp_dir = get_temp_dir() | ||
| 81 | file_path = os.path.join(temp_dir, plik.filename) | ||
| 82 | filename_noext = os.path.splitext(plik.filename)[0] | ||
| 83 | await plik.save(file_path) | ||
| 84 | |||
| 85 | input_path = file_path | ||
| 86 | output_path = os.path.join(temp_dir, f"{filename_noext}.{output_format}") | ||
| 87 | return input_path, output_path | ||
| 88 | |||
| 89 | def _build_conversion_command(self, file_type, input_path, output_path, args): | ||
| 90 | """Build the appropriate conversion command based on file type""" | ||
| 91 | if file_type in ["Video", "Audio"]: | ||
| 92 | return f"ffmpeg -i {shlex.quote(input_path)} {args} {shlex.quote(output_path)}" | ||
| 93 | elif file_type == "Image": | ||
| 94 | return f"magick {shlex.quote(input_path)} {args} {shlex.quote(output_path)}" | ||
| 95 | else: | ||
| 96 | return None | ||
| 97 | |||
| 98 | async def _upload_to_copyparty(self, output_path): | ||
| 99 | """Upload file to copyparty and return download URL""" | ||
| 100 | try: | ||
| 101 | async with aiofiles.open(output_path, 'rb') as f: | ||
| 102 | file_content = await f.read() | ||
| 103 | |||
| 104 | files = { | ||
| 105 | 'f': (os.path.basename(output_path), file_content, 'application/octet-stream') | ||
| 106 | } | ||
| 107 | headers = { | ||
| 108 | 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' | ||
| 109 | } | ||
| 110 | params = { | ||
| 111 | 'j': '1', | ||
| 112 | 'ck': 'b2' | ||
| 113 | } | ||
| 114 | |||
| 115 | response = requests.post(self._uploadurl, files=files, headers=headers, params=params) | ||
| 116 | |||
| 117 | if response.status_code in [200, 201]: | ||
| 118 | result = response.json() | ||
| 119 | if 'fileurl' in result: | ||
| 120 | return result['fileurl'] | ||
| 121 | else: | ||
| 122 | raise ValueError(f"Server response missing fileurl: {result}") | ||
| 123 | else: | ||
| 124 | error_msg = response.text if response.text else 'Unknown error' | ||
| 125 | raise requests.RequestException(f"HTTP {response.status_code}: {error_msg}") | ||
| 126 | |||
| 127 | except Exception as e: | ||
| 128 | logger.error(f"copyparty upload error: {e}", exc_info=True) | ||
| 129 | raise | ||
| 130 | |||
| 131 | async def _handle_file_output(self, ctx, output_path): | ||
| 132 | """Handle file output based on size""" | ||
| 133 | if os.path.getsize(output_path) > 1024*1024*10: | ||
| 134 | try: | ||
| 135 | dl_url = await self._upload_to_copyparty(output_path) | ||
| 136 | await ctx.followup.send(f"wysyłon\n{dl_url}\n") | ||
| 137 | except Exception as upload_error: | ||
| 138 | await ctx.followup.send(f"copyparty mnie nienawidzi :cccc\n {str(upload_error)}") | ||
| 139 | else: | ||
| 140 | await ctx.respond(file=discord.File(output_path)) | ||
| 141 | |||
| 142 | def _cleanup_files(self, *file_paths): | ||
| 143 | """Clean up temporary files""" | ||
| 144 | for path in file_paths: | ||
| 145 | if os.path.exists(path): | ||
| 146 | os.remove(path) | ||
| 147 | |||
| 148 | @commands.slash_command(name="konwersja", description="konwertuj plik na inny format") | ||
| 149 | @discord.option( | ||
| 150 | name="plik", | ||
| 151 | description="plik z copyparty", | ||
| 152 | required=False, | ||
| 153 | autocomplete=_file_autocomplete | ||
| 154 | ) | ||
| 155 | @discord.option( | ||
| 156 | name="zalacznik", | ||
| 157 | description="plik z discorda", | ||
| 158 | required=False, | ||
| 159 | type=discord.Attachment | ||
| 160 | ) | ||
| 161 | @discord.option( | ||
| 162 | name="serwer?", | ||
| 163 | description="wysłać plik na serwer?", | ||
| 164 | required=False, | ||
| 165 | type=bool, | ||
| 166 | default=False | ||
| 167 | ) | ||
| 168 | @discord.option( | ||
| 169 | name="format", | ||
| 170 | description="format konwersji", | ||
| 171 | required=True, | ||
| 172 | type=str | ||
| 173 | ) | ||
| 174 | async def convert(self, ctx, plik: str = None, zalacznik: discord.Attachment = None, server: bool = False, output_format: str = None): | ||
| 175 | await ctx.defer() | ||
| 176 | |||
| 177 | input_path = None | ||
| 178 | original_filename = None | ||
| 179 | |||
| 180 | try: | ||
| 181 | if plik: | ||
| 182 | url = self._cached_urls.get(ctx.author.id, {}).get(plik) | ||
| 183 | if not url: | ||
| 184 | await ctx.respond("Wybierz plik z podpowiedzi", ephemeral=True) | ||
| 185 | return | ||
| 186 | input_path = await self._download_from_url(url) | ||
| 187 | original_filename = plik | ||
| 188 | elif zalacznik: | ||
| 189 | input_path = os.path.join(self.temp_dir, zalacznik.filename) | ||
| 190 | original_filename = zalacznik.filename | ||
| 191 | await zalacznik.save(input_path) | ||
| 192 | else: | ||
| 193 | await ctx.respond("Podaj plik z copyparty lub załącznik", ephemeral=True) | ||
| 194 | return | ||
| 195 | |||
| 196 | if not input_path or not os.path.exists(input_path): | ||
| 197 | await ctx.respond("Nie udało się pobrać pliku", ephemeral=True) | ||
| 198 | return | ||
| 199 | |||
| 200 | output_path = os.path.join(self.temp_dir, f"{os.path.splitext(original_filename)[0]}.{output_format}") | ||
| 201 | |||
| 202 | file_type = filetype(input_path) | ||
| 203 | logger.info(f"Conversion started for: {file_type}") | ||
| 204 | |||
| 205 | command = self._build_conversion_command(file_type, input_path, output_path, "") | ||
| 206 | logger.info(f"Running: {command}") | ||
| 207 | if not command: | ||
| 208 | await ctx.respond("Plik jest nieobsługiwany", ephemeral=True) | ||
| 209 | return | ||
| 210 | |||
| 211 | proc = await asyncio.create_subprocess_exec( | ||
| 212 | *shlex.split(command), | ||
| 213 | stdout=asyncio.subprocess.PIPE, | ||
| 214 | stderr=asyncio.subprocess.PIPE | ||
| 215 | ) | ||
| 216 | stdout, stderr = await proc.communicate() | ||
| 217 | |||
| 218 | if proc.returncode != 0: | ||
| 219 | logger.error(f"FFmpeg error: {stderr.decode() if stderr else 'unknown'}") | ||
| 220 | await ctx.respond(f"Błąd konwersji: {stderr.decode() if stderr else 'unknown'}", ephemeral=True) | ||
| 221 | return | ||
| 222 | |||
| 223 | if os.path.getsize(output_path) > 1024 * 1024 * 10 or server: | ||
| 224 | dl_url = await self._upload_to_copyparty(output_path) | ||
| 225 | if dl_url: | ||
| 226 | await ctx.respond(f"Skonwertowano\n{dl_url}") | ||
| 227 | else: | ||
| 228 | await ctx.respond("Nie udało się wysłać na serwer", ephemeral=True) | ||
| 229 | else: | ||
| 230 | await ctx.respond(file=discord.File(output_path, filename=f"{original_filename}")) | ||
| 231 | |||
| 232 | logger.info(f"Conversion completed for: {file_type}") | ||
| 233 | |||
| 234 | except subprocess.CalledProcessError as e: | ||
| 235 | await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True) | ||
| 236 | except Exception as e: | ||
| 237 | await ctx.respond(f"An error occurred: {str(e)}", ephemeral=True) | ||
| 238 | finally: | ||
| 239 | if input_path and os.path.exists(input_path): | ||
| 240 | os.remove(input_path) | ||
| 241 | if 'output_path' in locals() and os.path.exists(output_path): | ||
| 242 | os.remove(output_path) | ||
| 243 | |||
| 244 | def setup(bot): | ||
| 245 | bot.add_cog(Conversion(bot)) | ||
