aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/compression.py
diff options
context:
space:
mode:
Diffstat (limited to 'cogs/compression.py')
-rw-r--r--cogs/compression.py306
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
6import mimetypes
7import os
8import asyncio
9import aiofiles
10import requests
11import urllib.parse
12from typing import List, Optional
13from os import PathLike
14import discord
15from discord.ext import commands
16from discord import AutocompleteContext
17from cogs.utils import file_search as search_utils
18import shlex
19import logging
20
21logger = logging.getLogger(__name__)
22
23def 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
34class 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
305def setup(bot):
306 bot.add_cog(Compression(bot))