diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
| commit | cd8ed8bc5793985df2c1692072a932ceee517b95 (patch) | |
| tree | 42d18a4140259b5c68b4dd05b7fcb398f78b8d7f /cogs/downloader.py | |
| parent | Merge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff) | |
| download | senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.tar.gz senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.zip | |
OPEN THE RUST GATES
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs/downloader.py')
| -rw-r--r-- | cogs/downloader.py | 234 |
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 | |||
| 6 | import os | ||
| 7 | import tempfile | ||
| 8 | import logging | ||
| 9 | import requests | ||
| 10 | import discord | ||
| 11 | import re | ||
| 12 | import yt_dlp | ||
| 13 | from discord.ext import commands | ||
| 14 | import aiofiles | ||
| 15 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 16 | |||
| 17 | logger = logging.getLogger(__name__) | ||
| 18 | |||
| 19 | |||
| 20 | class 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 | |||
| 233 | def setup(bot): | ||
| 234 | bot.add_cog(Downloader(bot)) | ||
