diff options
Diffstat (limited to '')
| -rw-r--r-- | cogs/compression.py | 306 |
1 files changed, 0 insertions, 306 deletions
diff --git a/cogs/compression.py b/cogs/compression.py deleted file mode 100644 index 71348dc..0000000 --- a/cogs/compression.py +++ /dev/null | |||
| @@ -1,306 +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 mimetypes | ||
| 7 | import os | ||
| 8 | import asyncio | ||
| 9 | import aiofiles | ||
| 10 | import requests | ||
| 11 | import urllib.parse | ||
| 12 | from typing import List, Optional | ||
| 13 | from os import PathLike | ||
| 14 | import discord | ||
| 15 | from discord.ext import commands | ||
| 16 | from discord import AutocompleteContext | ||
| 17 | from cogs.utils import file_search as search_utils | ||
| 18 | import shlex | ||
| 19 | import logging | ||
| 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 Compression(commands.Cog): | ||
| 35 | def __init__(self, bot): | ||
| 36 | from cogs.utils.temp import get_temp_dir | ||
| 37 | from cogs.utils.settings import get_baseurl | ||
| 38 | self.bot = bot | ||
| 39 | self.temp_dir = get_temp_dir() | ||
| 40 | self._cached_urls = {} | ||
| 41 | self._baseurl = get_baseurl() | ||
| 42 | |||
| 43 | async def _file_autocomplete(self, ctx: AutocompleteContext): | ||
| 44 | query = ctx.value.lower() if ctx.value else "" | ||
| 45 | results = await search_utils.UniversalSearcher.search_copyparty_autocomplete(query, max_results=25) | ||
| 46 | |||
| 47 | user_id = ctx.interaction.user.id | ||
| 48 | self._cached_urls[user_id] = {urllib.parse.unquote(url.split('/')[-1]): url for url in results} | ||
| 49 | |||
| 50 | choices = [] | ||
| 51 | for url in results[:25]: | ||
| 52 | filename = urllib.parse.unquote(url.split('/')[-1]) | ||
| 53 | display_name = filename[:99] + "…" if len(filename) > 100 else filename | ||
| 54 | choices.append(discord.OptionChoice(name=display_name, value=filename)) | ||
| 55 | return choices | ||
| 56 | |||
| 57 | async def _download_from_url(self, url: str) -> Optional[str]: | ||
| 58 | """Download file from copyparty URL and return local path""" | ||
| 59 | try: | ||
| 60 | response = requests.get(url) | ||
| 61 | if response.status_code != 200: | ||
| 62 | return None | ||
| 63 | |||
| 64 | filename = url.split('/')[-1].split('?')[0] | ||
| 65 | filename = urllib.parse.unquote(filename) | ||
| 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 | ||
| 74 | |||
| 75 | async def _upload_to_copyparty(self, file_path: str) -> Optional[str]: | ||
| 76 | """Upload file to copyparty and return download URL""" | ||
| 77 | try: | ||
| 78 | async with aiofiles.open(file_path, 'rb') as f: | ||
| 79 | file_content = await f.read() | ||
| 80 | |||
| 81 | files = {'f': (os.path.basename(file_path), file_content, 'application/octet-stream')} | ||
| 82 | headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'} | ||
| 83 | params = {'j': '1', 'ck': 'b2'} | ||
| 84 | |||
| 85 | response = requests.post(self._baseurl, files=files, headers=headers, params=params) | ||
| 86 | |||
| 87 | if response.status_code in [200, 201]: | ||
| 88 | result = response.json() | ||
| 89 | return result.get('fileurl') | ||
| 90 | except Exception as e: | ||
| 91 | logger.error(f"Upload error: {e}") | ||
| 92 | return None | ||
| 93 | |||
| 94 | def _process_video(self, file_path: str, output_path: str) -> str: | ||
| 95 | """Process video compression""" | ||
| 96 | command = f"ffmpeg -y -i {shlex.quote(file_path)} -c:v libx264 -crf 51 -b:v 100 -vf 'fps=10,scale=trunc(iw*0.5/2)*2:trunc(ih*0.5/2)*2' -preset ultrafast -c:a libopus -b:a 500 {shlex.quote(output_path)}" | ||
| 97 | return command | ||
| 98 | |||
| 99 | def _process_audio(self, file_path: str, output_path: str) -> str: | ||
| 100 | """Process audio compression""" | ||
| 101 | if output_path.lower().endswith('.mp3'): | ||
| 102 | output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '') | ||
| 103 | if not output_path.lower().endswith('.ogg'): | ||
| 104 | output_path = os.path.splitext(output_path)[0] + '.ogg' | ||
| 105 | command = f"ffmpeg -y -i {shlex.quote(file_path)} -c:a libopus -b:a 500 -ar 12000 -ac 1 {shlex.quote(output_path)}" | ||
| 106 | return command | ||
| 107 | |||
| 108 | def _process_image(self, file_path: str, output_path: str) -> tuple[str, str]: | ||
| 109 | """Process image compression""" | ||
| 110 | ext = os.path.splitext(file_path)[1].lower() | ||
| 111 | if ext in [".jpg", ".jpeg"]: | ||
| 112 | command = f"magick {shlex.quote(file_path)} -strip -resize 30% -quality 5 {shlex.quote(output_path)}" | ||
| 113 | elif ext == ".gif": | ||
| 114 | command = f"magick {shlex.quote(file_path)} -coalesce -resize 30% -colors 32 -strip -layers Optimize {shlex.quote(output_path)}" | ||
| 115 | else: | ||
| 116 | output_path = os.path.splitext(output_path)[0] + ".jpg" | ||
| 117 | command = f"magick {shlex.quote(file_path)} -strip -resize 30% -quality 5 {shlex.quote(output_path)}" | ||
| 118 | return command, output_path | ||
| 119 | |||
| 120 | async def _execute_compression(self, command: str) -> bool: | ||
| 121 | """Execute compression command""" | ||
| 122 | proc = await asyncio.create_subprocess_exec( | ||
| 123 | *shlex.split(command), | ||
| 124 | stdout=asyncio.subprocess.PIPE, | ||
| 125 | stderr=asyncio.subprocess.PIPE | ||
| 126 | ) | ||
| 127 | await proc.wait() | ||
| 128 | return proc.returncode == 0 | ||
| 129 | |||
| 130 | async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]: | ||
| 131 | """Compress a single attachment and return the compressed file""" | ||
| 132 | try: | ||
| 133 | file_path: str = os.path.join(self.temp_dir, attachment.filename) | ||
| 134 | output_path = os.path.join(self.temp_dir, f"better_{attachment.filename}") | ||
| 135 | |||
| 136 | # Save the original file | ||
| 137 | await attachment.save(file_path) | ||
| 138 | |||
| 139 | # Process the file | ||
| 140 | file_type = filetype(file_path) | ||
| 141 | logger.info(f"Compression started for: {file_type}") | ||
| 142 | |||
| 143 | command = self._get_compression_command(file_type, file_path, output_path) | ||
| 144 | if command is None: | ||
| 145 | logger.info(f"Unsupported file type: {file_type}") | ||
| 146 | os.remove(file_path) | ||
| 147 | return None | ||
| 148 | |||
| 149 | # Update output_path if it was modified | ||
| 150 | if file_type == "Image": | ||
| 151 | command, output_path = command | ||
| 152 | elif file_type == "Audio": | ||
| 153 | # output_path is already modified in _process_audio | ||
| 154 | pass | ||
| 155 | |||
| 156 | # Run the compression | ||
| 157 | success = await self._execute_compression(command) | ||
| 158 | |||
| 159 | if success and os.path.exists(output_path): | ||
| 160 | logger.info(f"Successfully compressed: {attachment.filename}") | ||
| 161 | return discord.File(output_path, filename=f"compressed_{attachment.filename}") | ||
| 162 | return None | ||
| 163 | |||
| 164 | except Exception as e: | ||
| 165 | logger.error(f"Error processing {attachment.filename}: {str(e)}") | ||
| 166 | return None | ||
| 167 | |||
| 168 | finally: | ||
| 169 | # Clean up temporary files | ||
| 170 | if 'file_path' in locals() and os.path.exists(file_path): | ||
| 171 | os.remove(file_path) | ||
| 172 | |||
| 173 | def _get_compression_command(self, file_type: str, file_path: str, output_path: str): | ||
| 174 | """Get the appropriate compression command based on file type""" | ||
| 175 | if file_type == "Video": | ||
| 176 | return self._process_video(file_path, output_path) | ||
| 177 | elif file_type == "Audio": | ||
| 178 | return self._process_audio(file_path, output_path) | ||
| 179 | elif file_type == "Image": | ||
| 180 | return self._process_image(file_path, output_path) | ||
| 181 | else: | ||
| 182 | return None | ||
| 183 | |||
| 184 | async def _compress_attachments(self, attachments: List[discord.Attachment]) -> List[discord.File]: | ||
| 185 | """Compress multiple attachments and return list of compressed files""" | ||
| 186 | compressed_files = [] | ||
| 187 | for attachment in attachments: | ||
| 188 | compressed_file = await self._compress_attachment(attachment) | ||
| 189 | if compressed_file: | ||
| 190 | compressed_files.append(compressed_file) | ||
| 191 | return compressed_files | ||
| 192 | |||
| 193 | @commands.message_command( | ||
| 194 | name="kompresja", | ||
| 195 | description="skompresuj wideło albo dźwienk :)", | ||
| 196 | ) | ||
| 197 | async def message_compression(self, ctx, message: discord.Message): | ||
| 198 | if not message.attachments: | ||
| 199 | await ctx.respond("Brak załączników w wiadomości", ephemeral=True) | ||
| 200 | return | ||
| 201 | |||
| 202 | await ctx.defer() | ||
| 203 | compressed_files = await self._compress_attachments(message.attachments) | ||
| 204 | |||
| 205 | # Send all compressed files in one message | ||
| 206 | if compressed_files: | ||
| 207 | await ctx.respond( | ||
| 208 | f"Skompresowano {len(compressed_files)} z {len(message.attachments)} plików:", | ||
| 209 | files=compressed_files | ||
| 210 | ) | ||
| 211 | |||
| 212 | # Clean up compressed files | ||
| 213 | for file in compressed_files: | ||
| 214 | if os.path.exists(file.fp.name): # type: ignore[attr-defined] | ||
| 215 | os.remove(file.fp.name) # type: ignore[attr-defined] | ||
| 216 | else: | ||
| 217 | await ctx.respond("Nie udało się skompresować żadnych plików.", ephemeral=True) | ||
| 218 | |||
| 219 | @commands.slash_command( | ||
| 220 | name="kompresja", | ||
| 221 | description="skompresuj wideło albo dźwienk :)", | ||
| 222 | ) | ||
| 223 | @discord.option( | ||
| 224 | name="plik", | ||
| 225 | description="plik z copyparty", | ||
| 226 | required=False, | ||
| 227 | autocomplete=_file_autocomplete | ||
| 228 | ) | ||
| 229 | @discord.option( | ||
| 230 | name="zalacznik", | ||
| 231 | description="plik z discorda", | ||
| 232 | required=False, | ||
| 233 | type=discord.Attachment | ||
| 234 | ) | ||
| 235 | @discord.option( | ||
| 236 | name="serwer?", | ||
| 237 | description="wysłać plik na serwer?", | ||
| 238 | required=False, | ||
| 239 | default=False, | ||
| 240 | type=bool | ||
| 241 | ) | ||
| 242 | async def compression(self, ctx, plik: str = None, zalacznik: discord.Attachment = None, server = False): | ||
| 243 | await ctx.defer() | ||
| 244 | |||
| 245 | file_path = None | ||
| 246 | original_filename = None | ||
| 247 | |||
| 248 | if plik: | ||
| 249 | url = self._cached_urls.get(ctx.author.id, {}).get(plik) | ||
| 250 | if not url: | ||
| 251 | await ctx.respond("Wybierz plik z podpowiedzi", ephemeral=True) | ||
| 252 | return | ||
| 253 | file_path = await self._download_from_url(url) | ||
| 254 | original_filename = plik | ||
| 255 | elif zalacznik: | ||
| 256 | file_path = os.path.join(self.temp_dir, zalacznik.filename) | ||
| 257 | original_filename = zalacznik.filename | ||
| 258 | await zalacznik.save(file_path) | ||
| 259 | else: | ||
| 260 | await ctx.respond("Podaj plik z copyparty lub załącznik", ephemeral=True) | ||
| 261 | return | ||
| 262 | |||
| 263 | if not file_path or not os.path.exists(file_path): | ||
| 264 | await ctx.respond("Nie udało się pobrać pliku", ephemeral=True) | ||
| 265 | return | ||
| 266 | |||
| 267 | try: | ||
| 268 | file_type = filetype(file_path) | ||
| 269 | output_path = os.path.join(self.temp_dir, f"better_{original_filename}") | ||
| 270 | |||
| 271 | command = self._get_compression_command(file_type, file_path, output_path) | ||
| 272 | if command is None: | ||
| 273 | await ctx.respond("Nieobsługiwany typ pliku", ephemeral=True) | ||
| 274 | return | ||
| 275 | |||
| 276 | if file_type == "Image": | ||
| 277 | command, output_path = command | ||
| 278 | elif file_type == "Audio": | ||
| 279 | # Audio output path remains unchanged - already processed in _process_audio | ||
| 280 | pass | ||
| 281 | |||
| 282 | success = await self._execute_compression(command) | ||
| 283 | |||
| 284 | if not success or not os.path.exists(output_path): | ||
| 285 | await ctx.respond("Kompresja nie powiodła się", ephemeral=True) | ||
| 286 | return | ||
| 287 | |||
| 288 | file_size = os.path.getsize(output_path) | ||
| 289 | if file_size > 1024 * 1024 * 10 or server: | ||
| 290 | dl_url = await self._upload_to_copyparty(output_path) | ||
| 291 | if dl_url: | ||
| 292 | await ctx.respond(f"Skompresowano\n{dl_url}") | ||
| 293 | else: | ||
| 294 | await ctx.respond("Nie udało wysłać na serwer", ephemeral=True) | ||
| 295 | else: | ||
| 296 | await ctx.respond(file=discord.File(output_path, filename=f"compressed_{original_filename}")) | ||
| 297 | |||
| 298 | finally: | ||
| 299 | if file_path and os.path.exists(file_path): | ||
| 300 | os.remove(file_path) | ||
| 301 | if 'output_path' in locals() and os.path.exists(output_path): | ||
| 302 | os.remove(output_path) | ||
| 303 | |||
| 304 | |||
| 305 | def setup(bot): | ||
| 306 | bot.add_cog(Compression(bot)) | ||
