aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/downloader.py
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-07-08 02:13:35 +0200
committersalmonik <raisersoft@proton.me>2026-07-08 02:13:35 +0200
commitcd8ed8bc5793985df2c1692072a932ceee517b95 (patch)
tree42d18a4140259b5c68b4dd05b7fcb398f78b8d7f /cogs/downloader.py
parentMerge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff)
downloadsenator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.tar.gz
senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.zip
OPEN THE RUST GATES
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
-rw-r--r--cogs/downloader.py234
1 files changed, 0 insertions, 234 deletions
diff --git a/cogs/downloader.py b/cogs/downloader.py
deleted file mode 100644
index 900bafb..0000000
--- a/cogs/downloader.py
+++ /dev/null
@@ -1,234 +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 os
7import tempfile
8import logging
9import requests
10import discord
11import re
12import yt_dlp
13from discord.ext import commands
14import aiofiles
15from cogs.utils.url_helper import clean_url_for_ytdlp
16
17logger = logging.getLogger(__name__)
18
19
20class Downloader(commands.Cog):
21 def __init__(self, bot):
22 self.bot = bot
23
24 URL_PATTERN = re.compile(
25 r'https://(?:[A-Za-z0-9-]+\.)*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,63}/\S*'
26 )
27
28 @commands.message_command(
29 name="download",
30 description="Download and convert video/audio from internet"
31 )
32 async def message_download(self, ctx, message: discord.Message):
33 # check if message has a valid url
34 url = [m.group(0) for m in self.URL_PATTERN.finditer(message.content)]
35 if url:
36 for url in url:
37 await self.download(ctx=ctx, url=url, fformat="mp4")
38 else:
39 await ctx.respond("Brak załączników w wiadomości", ephemeral=True)
40
41 @commands.slash_command(
42 name="download",
43 description="Download and convert video/audio from internet"
44 )
45 async def download(
46 self,
47 ctx: discord.ApplicationContext,
48 url: str = discord.Option(str, description="Link media", required=True),
49 catbox: bool = discord.Option(bool, description="wysłać na serwer??", required=False, default=False),
50 fformat: str = discord.Option(str, description="jaki format? (mp4, mp3, wav, etc.)", required=False, default="mp4"),
51 filename_user: str = discord.Option(str, description="jaka nazwa pliku?", required=False)
52 ):
53 """Download and convert media from a URL to the specified format"""
54 await ctx.defer()
55 # Call the function and get the response
56 response = await self.download_standalone(url=url, catbox=catbox, fformat=fformat, filename_user=filename_user)
57 if isinstance(response, dict):
58 if response["status"] == "success":
59 if response["type"] == "url":
60 await ctx.respond(f"pobioron i wysyłon: {response['content']}")
61 elif response["type"] == "file":
62 file_data = response["content"]
63 await ctx.respond(file=discord.File(file_data))
64 else:
65 error_msg = response["content"]
66 if 'details' in response:
67 error_msg += f"\n{response['details']}"
68 await ctx.respond(f"Błąd: {error_msg}", ephemeral=True)
69 logger.error(f"Error: {response['content']}")
70 if 'details' in response:
71 logger.error(f"Details: {response['details']}")
72
73 @staticmethod
74 async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str | None = None):
75 try:
76 url = clean_url_for_ytdlp(url)
77 logger.info(f"Downloading {url} as {fformat}")
78
79 with tempfile.TemporaryDirectory(delete=False) as temp_dir:
80 ydl_opts = Downloader._get_ydl_options(filename_user, fformat, temp_dir)
81
82 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
83 ydl.download([url])
84
85 actual_file, filename = Downloader._find_and_prepare_file(temp_dir, filename_user)
86
87 if Downloader._should_upload_to_server(actual_file, catbox):
88 return await Downloader._upload_to_server(actual_file, filename, temp_dir)
89 else:
90 return Downloader._return_file_response(actual_file, filename, temp_dir)
91
92 except Exception as e:
93 return Downloader._create_error_response("Download failed", str(e), None)
94
95 @staticmethod
96 def _sanitize_filename(name: str) -> str:
97 invalid = '<>:"/\\|?*'
98 for c in invalid:
99 name = name.replace(c, '')
100 return name.strip()
101
102 @staticmethod
103 def _get_ydl_options(filename: str = None, fformat: str = None, temp_dir: str = None) -> dict:
104 if filename:
105 filename = Downloader._sanitize_filename(filename)[:100]
106 ydl_opts = {
107 'outtmpl': os.path.join(temp_dir, filename if filename is not None else '%(title).100s.%(ext)s'),
108 'format': 'bestaudio/best' if fformat in ['mp3', 'wav', 'm4a'] else 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
109 'merge_output_format': 'mp4',
110 'postprocessors': [],
111 'remote-components': 'ejs:github',
112 'js-runtimes': {'deno': {'path': '/usr/bin/deno'}},
113 }
114
115 if fformat in ['mp3', 'wav', 'm4a']:
116 ydl_opts['postprocessors'].append({
117 'key': 'FFmpegExtractAudio',
118 'preferredcodec': fformat,
119 'preferredquality': '192',
120 })
121 elif fformat != 'mp4':
122 ydl_opts['postprocessors'].append({
123 'key': 'FFmpegVideoConvertor',
124 'preferedformat': fformat,
125 })
126
127 return ydl_opts
128
129 @staticmethod
130 def _find_and_prepare_file(temp_dir: str, filename_user: str) -> tuple:
131 files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))]
132 if not files:
133 raise FileNotFoundError("No file was downloaded")
134
135 actual_file = os.path.join(temp_dir, files[0])
136 extension = actual_file.split('.')[-1]
137 extension = Downloader._sanitize_filename(extension)
138
139 if not filename_user:
140 filename = os.path.basename(actual_file)
141 filename = Downloader._sanitize_filename(filename)
142 if len(filename) > 100:
143 filename = filename[:100]
144 else:
145 filename_user = Downloader._sanitize_filename(filename_user)[:100]
146 filename = filename_user + '.' + extension
147 actual_file = os.path.join(temp_dir, filename)
148
149 if not os.path.exists(actual_file):
150 raise FileNotFoundError(f"Downloaded file not found: {actual_file}")
151
152 return actual_file, filename
153
154 @staticmethod
155 def _should_upload_to_server(actual_file: str, catbox: bool) -> bool:
156 return os.path.getsize(actual_file) > 1024*1024*10 or catbox
157
158 @staticmethod
159 async def _upload_to_server(actual_file: str, filename: str, temp_dir: str) -> dict:
160 try:
161 async with aiofiles.open(actual_file, 'rb') as f:
162 content = await f.read()
163 files = {'f': (filename, content, 'application/octet-stream')}
164 headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'}
165 params = {'j': '1', 'ck': 'b2'}
166 base_url = os.getenv("COPYPARTY_URL_UPLOAD")
167
168 response = requests.post(base_url, files=files, headers=headers, params=params)
169
170 if response.status_code in [200, 201]:
171 return Downloader._handle_upload_success(response, filename, temp_dir)
172 elif response.status_code == 413:
173 return Downloader._create_error_response(
174 "File too large for upload",
175 "Plik jest za duży, by wysłać go na serwer (limit to ok. 100MB)",
176 temp_dir
177 )
178 else:
179 return Downloader._create_error_response(
180 f"Server returned {response.status_code}",
181 response.text or 'Unknown error',
182 temp_dir
183 )
184 except Exception as upload_error:
185 return Downloader._create_error_response("Upload failed", str(upload_error), temp_dir)
186
187 @staticmethod
188 def _handle_upload_success(response: requests.Response, filename: str, temp_dir: str) -> dict:
189 try:
190 result = response.json()
191 if 'fileurl' in result:
192 return {
193 "status": "success",
194 "type": "url",
195 "content": result['fileurl'],
196 "filename": filename,
197 "temp_dir": temp_dir
198 }
199 else:
200 return Downloader._create_error_response(
201 "Invalid response from file server",
202 str(result),
203 temp_dir
204 )
205 except ValueError:
206 return Downloader._create_error_response(
207 "Failed to parse server response",
208 response.text[:200],
209 temp_dir
210 )
211
212 @staticmethod
213 def _return_file_response(actual_file: str, filename: str, temp_dir: str) -> dict:
214 return {
215 "status": "success",
216 "type": "file",
217 "content": actual_file,
218 "filename": filename,
219 "temp_dir": temp_dir
220 }
221
222 @staticmethod
223 def _create_error_response(content: str, details: str, temp_dir: str | None) -> dict:
224 return {
225 "status": "error",
226 "type": "error",
227 "content": content,
228 "details": details,
229 "temp_dir": temp_dir
230 }
231
232
233def setup(bot):
234 bot.add_cog(Downloader(bot))