aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/compression.py
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-04-18 00:24:50 +0200
committersalmonik <raisersoft@proton.me>2026-04-18 00:24:50 +0200
commita2b5b10625126b7e98643af9fe40569c9e004047 (patch)
tree2b59a929d068bc28bbc21e8c8d8836e4d29270c0 /cogs/compression.py
parentadd mathematical plotting (diff)
downloadsenator-sperma-a2b5b10625126b7e98643af9fe40569c9e004047.tar.gz
senator-sperma-a2b5b10625126b7e98643af9fe40569c9e004047.zip
refactor + preparing bot for public use
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
-rw-r--r--cogs/compression.py202
1 files changed, 163 insertions, 39 deletions
diff --git a/cogs/compression.py b/cogs/compression.py
index 1fcefb5..150239f 100644
--- a/cogs/compression.py
+++ b/cogs/compression.py
@@ -6,10 +6,16 @@
6import mimetypes 6import mimetypes
7import os 7import os
8import asyncio 8import asyncio
9import aiofiles
10import requests
11import urllib.parse
9from typing import List, Optional 12from typing import List, Optional
10from os import PathLike 13from os import PathLike
11import discord 14import discord
12from discord.ext import commands 15from discord.ext import commands
16from discord import AutocompleteContext
17from cogs.utils import file_search as search_utils
18import shlex
13import logging 19import logging
14 20
15logger = logging.getLogger(__name__) 21logger = logging.getLogger(__name__)
@@ -27,87 +33,142 @@ def filetype(filename):
27 33
28class Compression(commands.Cog): 34class Compression(commands.Cog):
29 def __init__(self, bot): 35 def __init__(self, bot):
36 from cogs.utils.temp import get_temp_dir
37 from cogs.utils.settings import get_baseurl
30 self.bot = bot 38 self.bot = bot
31 self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp") 39 self.temp_dir = get_temp_dir()
32 if not os.path.exists(self.temp_dir): 40 self._cached_urls = {}
33 os.makedirs(self.temp_dir) 41 self._baseurl = os.getenv("COPYPARTY_URL") or 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 file_path = os.path.join(self.temp_dir, filename)
66
67 async with aiofiles.open(file_path, 'wb') as f:
68 await f.write(response.content)
69 return file_path
70 except Exception as e:
71 logger.error(f"Error downloading from {url}: {e}")
72 return None
73
74 async def _upload_to_copyparty(self, file_path: str) -> Optional[str]:
75 """Upload file to copyparty and return download URL"""
76 try:
77 async with aiofiles.open(file_path, 'rb') as f:
78 file_content = await f.read()
79
80 files = {'f': (os.path.basename(file_path), file_content, 'application/octet-stream')}
81 headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'}
82 params = {'j': '1', 'ck': 'b2'}
83
84 response = requests.post(self._baseurl, files=files, headers=headers, params=params)
85
86 if response.status_code in [200, 201]:
87 result = response.json()
88 return result.get('fileurl')
89 except Exception as e:
90 logger.error(f"Upload error: {e}")
91 return None
34 92
35 def _process_video(self, file_path: str, output_path: str) -> str: 93 def _process_video(self, file_path: str, output_path: str) -> str:
36 """Process video compression""" 94 """Process video compression"""
37 command = f"ffmpeg -y -i {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 {output_path}" 95 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)}"
38 return command 96 return command
39 97
40 def _process_audio(self, file_path: str, output_path: str) -> str: 98 def _process_audio(self, file_path: str, output_path: str) -> str:
41 """Process audio compression""" 99 """Process audio compression"""
42 if output_path.lower().endswith('.mp3'): 100 if output_path.lower().endswith('.mp3'):
43 output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '') 101 output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '')
44 if not output_path.lower().endswith('.ogg'): 102 if not output_path.lower().endswith('.ogg'):
45 output_path = os.path.splitext(output_path)[0] + '.ogg' 103 output_path = os.path.splitext(output_path)[0] + '.ogg'
46 command = f"ffmpeg -y -i {file_path} -c:a libopus -b:a 500 -ar 12000 -ac 1 {output_path}" 104 command = f"ffmpeg -y -i {shlex.quote(file_path)} -c:a libopus -b:a 500 -ar 12000 -ac 1 {shlex.quote(output_path)}"
47 return command 105 return command
48 106
49 def _process_image(self, file_path: str, output_path: str) -> tuple[str, str]: 107 def _process_image(self, file_path: str, output_path: str) -> tuple[str, str]:
50 """Process image compression""" 108 """Process image compression"""
51 ext = os.path.splitext(file_path)[1].lower() 109 ext = os.path.splitext(file_path)[1].lower()
52 if ext in [".jpg", ".jpeg"]: 110 if ext in [".jpg", ".jpeg"]:
53 command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}" 111 command = f"magick {shlex.quote(file_path)} -strip -resize 30% -quality 5 {shlex.quote(output_path)}"
54 elif ext == ".gif": 112 elif ext == ".gif":
55 command = f"magick {file_path} -coalesce -resize 30% -colors 32 -strip -layers Optimize {output_path}" 113 command = f"magick {shlex.quote(file_path)} -coalesce -resize 30% -colors 32 -strip -layers Optimize {shlex.quote(output_path)}"
56 else: 114 else:
57 output_path = os.path.splitext(output_path)[0] + ".jpg" 115 output_path = os.path.splitext(output_path)[0] + ".jpg"
58 command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}" 116 command = f"magick {shlex.quote(file_path)} -strip -resize 30% -quality 5 {shlex.quote(output_path)}"
59 return command, output_path 117 return command, output_path
60 118
61 async def _execute_compression(self, command: str) -> bool: 119 async def _execute_compression(self, command: str) -> bool:
62 """Execute compression command""" 120 """Execute compression command"""
63 proc = await asyncio.create_subprocess_exec(command) 121 proc = await asyncio.create_subprocess_exec(
122 *shlex.split(command),
123 stdout=asyncio.subprocess.PIPE,
124 stderr=asyncio.subprocess.PIPE
125 )
64 await proc.wait() 126 await proc.wait()
65 return proc.returncode == 0 127 return proc.returncode == 0
66 128
67 async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]: 129 async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]:
68 """Compress a single attachment and return the compressed file""" 130 """Compress a single attachment and return the compressed file"""
69 file_path = PathLike[str]
70 try: 131 try:
71 file_path: PathLike[str] | str = os.path.join(self.temp_dir, attachment.filename) 132 file_path: str = os.path.join(self.temp_dir, attachment.filename)
72 output_path = os.path.join(self.temp_dir, f"better_{attachment.filename}") 133 output_path = os.path.join(self.temp_dir, f"better_{attachment.filename}")
73 134
74 # Save the original file 135 # Save the original file
75 await attachment.save(file_path) 136 await attachment.save(file_path)
76 137
77 # Process the file 138 # Process the file
78 file_type = filetype(file_path) 139 file_type = filetype(file_path)
79 logger.info(f"Compression started for: {file_type}") 140 logger.info(f"Compression started for: {file_type}")
80 141
81 command = self._get_compression_command(file_type, file_path, output_path) 142 command = self._get_compression_command(file_type, file_path, output_path)
82 if command is None: 143 if command is None:
83 logger.info(f"Unsupported file type: {file_type}") 144 logger.info(f"Unsupported file type: {file_type}")
84 os.remove(file_path) 145 os.remove(file_path)
85 return None 146 return None
86 147
87 # Update output_path if it was modified 148 # Update output_path if it was modified
88 if file_type == "Image": 149 if file_type == "Image":
89 command, output_path = command 150 command, output_path = command
90 elif file_type == "Audio": 151 elif file_type == "Audio":
91 # output_path is already modified in _process_audio 152 # output_path is already modified in _process_audio
92 pass 153 pass
93 154
94 # Run the compression 155 # Run the compression
95 success = await self._execute_compression(command) 156 success = await self._execute_compression(command)
96 157
97 if success and os.path.exists(output_path): 158 if success and os.path.exists(output_path):
98 logger.info(f"Successfully compressed: {attachment.filename}") 159 logger.info(f"Successfully compressed: {attachment.filename}")
99 return discord.File(output_path, filename=f"compressed_{attachment.filename}") 160 return discord.File(output_path, filename=f"compressed_{attachment.filename}")
100 return None 161 return None
101 162
102 except Exception as e: 163 except Exception as e:
103 logger.error(f"Error processing {attachment.filename}: {str(e)}") 164 logger.error(f"Error processing {attachment.filename}: {str(e)}")
104 return None 165 return None
105 166
106 finally: 167 finally:
107 # Clean up temporary files 168 # Clean up temporary files
108 if 'file_path' in locals() and os.path.exists(file_path): 169 if 'file_path' in locals() and os.path.exists(file_path):
109 os.remove(file_path) 170 os.remove(file_path)
110 171
111 def _get_compression_command(self, file_type: str, file_path: str, output_path: str): 172 def _get_compression_command(self, file_type: str, file_path: str, output_path: str):
112 """Get the appropriate compression command based on file type""" 173 """Get the appropriate compression command based on file type"""
113 if file_type == "Video": 174 if file_type == "Video":
@@ -136,21 +197,21 @@ class Compression(commands.Cog):
136 if not message.attachments: 197 if not message.attachments:
137 await ctx.respond("Brak załączników w wiadomości", ephemeral=True) 198 await ctx.respond("Brak załączników w wiadomości", ephemeral=True)
138 return 199 return
139 200
140 await ctx.defer() 201 await ctx.defer()
141 compressed_files = await self._compress_attachments(message.attachments) 202 compressed_files = await self._compress_attachments(message.attachments)
142 203
143 # Send all compressed files in one message 204 # Send all compressed files in one message
144 if compressed_files: 205 if compressed_files:
145 await ctx.respond( 206 await ctx.respond(
146 f"Skompresowano {len(compressed_files)} z {len(message.attachments)} plików:", 207 f"Skompresowano {len(compressed_files)} z {len(message.attachments)} plików:",
147 files=compressed_files 208 files=compressed_files
148 ) 209 )
149 210
150 # Clean up compressed files 211 # Clean up compressed files
151 for file in compressed_files: 212 for file in compressed_files:
152 if os.path.exists(file.fp.name): 213 if os.path.exists(file.fp.name): # type: ignore[attr-defined]
153 os.remove(file.fp.name) 214 os.remove(file.fp.name) # type: ignore[attr-defined]
154 else: 215 else:
155 await ctx.respond("Nie udało się skompresować żadnych plików.", ephemeral=True) 216 await ctx.respond("Nie udało się skompresować żadnych plików.", ephemeral=True)
156 217
@@ -160,20 +221,83 @@ class Compression(commands.Cog):
160 ) 221 )
161 @discord.option( 222 @discord.option(
162 name="plik", 223 name="plik",
163 description="plik do skompresowania", 224 description="plik z copyparty",
164 required=True, 225 required=False,
226 autocomplete=_file_autocomplete
227 )
228 @discord.option(
229 name="zalacznik",
230 description="plik z discorda",
231 required=False,
165 type=discord.Attachment 232 type=discord.Attachment
166 ) 233 )
167 async def compression(self, ctx, plik: discord.Attachment): 234 @discord.option(
235 name="serwer?",
236 description="wysłać plik na serwer?",
237 required=False,
238 default=False,
239 type=bool
240 )
241 async def compression(self, ctx, plik: str = None, zalacznik: discord.Attachment = None, server = False):
168 await ctx.defer() 242 await ctx.defer()
169 compressed_file = await self._compress_attachment(plik) 243
170 244 file_path = None
171 if compressed_file: 245 original_filename = None
172 await ctx.respond(file=compressed_file) 246
173 if os.path.exists(compressed_file.fp.name): 247 if plik:
174 os.remove(compressed_file.fp.name) 248 url = self._cached_urls.get(ctx.author.id, {}).get(plik)
249 if not url:
250 await ctx.respond("Wybierz plik z podpowiedzi", ephemeral=True)
251 return
252 file_path = await self._download_from_url(url)
253 original_filename = plik
254 elif zalacznik:
255 file_path = os.path.join(self.temp_dir, zalacznik.filename)
256 original_filename = zalacznik.filename
257 await zalacznik.save(file_path)
175 else: 258 else:
176 await ctx.respond("Nie udało się skompresować pliku.", ephemeral=True) 259 await ctx.respond("Podaj plik z copyparty lub załącznik", ephemeral=True)
260 return
261
262 if not file_path or not os.path.exists(file_path):
263 await ctx.respond("Nie udało się pobrać pliku", ephemeral=True)
264 return
265
266 try:
267 file_type = filetype(file_path)
268 output_path = os.path.join(self.temp_dir, f"better_{original_filename}")
269
270 command = self._get_compression_command(file_type, file_path, output_path)
271 if command is None:
272 await ctx.respond("Nieobsługiwany typ pliku", ephemeral=True)
273 return
274
275 if file_type == "Image":
276 command, output_path = command
277 elif file_type == "Audio":
278 pass
279
280 success = await self._execute_compression(command)
281
282 if not success or not os.path.exists(output_path):
283 await ctx.respond("Kompresja nie powiodła się", ephemeral=True)
284 return
285
286 file_size = os.path.getsize(output_path)
287 if file_size > 1024 * 1024 * 10 or server:
288 dl_url = await self._upload_to_copyparty(output_path)
289 if dl_url:
290 await ctx.respond(f"Skompresowano\n{dl_url}")
291 else:
292 await ctx.respond("Nie udało wysłać na serwer", ephemeral=True)
293 else:
294 await ctx.respond(file=discord.File(output_path, filename=f"compressed_{original_filename}"))
295
296 finally:
297 if file_path and os.path.exists(file_path):
298 os.remove(file_path)
299 if 'output_path' in locals() and os.path.exists(output_path):
300 os.remove(output_path)
177 301
178 302
179def setup(bot): 303def setup(bot):