diff options
| author | salmonik <raisersoft@proton.me> | 2026-02-04 16:19:22 +0100 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-02-04 16:19:22 +0100 |
| commit | 72abc43db8f732743c65d6f6a820369ab8665766 (patch) | |
| tree | 094fafdb0baf2e06a02045b1b2c0be88931a379a | |
| parent | refactor music (diff) | |
| download | senator-sperma-72abc43db8f732743c65d6f6a820369ab8665766.tar.gz senator-sperma-72abc43db8f732743c65d6f6a820369ab8665766.zip | |
more asynchronous work and other minor improvements
| -rw-r--r-- | cogs/ai.py | 4 | ||||
| -rw-r--r-- | cogs/compression.py | 76 | ||||
| -rw-r--r-- | cogs/conversion.py | 181 | ||||
| -rw-r--r-- | cogs/downloader.py | 253 | ||||
| -rw-r--r-- | cogs/jarvis.py | 218 | ||||
| -rw-r--r-- | cogs/math.py | 7 | ||||
| -rw-r--r-- | cogs/tts.py | 138 | ||||
| -rw-r--r-- | cogs/vclogs.py | 29 | ||||
| -rw-r--r-- | cogs/voice.py | 78 | ||||
| -rw-r--r-- | cogs/web.py | 89 | ||||
| -rw-r--r-- | main.py | 15 | ||||
| -rw-r--r-- | requirements.txt | 2 |
12 files changed, 623 insertions, 467 deletions
| @@ -9,7 +9,7 @@ from discord.ext import commands | |||
| 9 | import logging | 9 | import logging |
| 10 | import requests | 10 | import requests |
| 11 | from gtts import gTTS | 11 | from gtts import gTTS |
| 12 | from cogs.music.play_utils import PlayUtils | 12 | from cogs.music.play_commands import PlayCommands |
| 13 | import io | 13 | import io |
| 14 | 14 | ||
| 15 | logger = logging.getLogger(__name__) | 15 | logger = logging.getLogger(__name__) |
| @@ -65,7 +65,7 @@ class AI(commands.Cog): | |||
| 65 | tts_buffer = io.BytesIO() | 65 | tts_buffer = io.BytesIO() |
| 66 | tts_obj.write_to_fp(tts_buffer) | 66 | tts_obj.write_to_fp(tts_buffer) |
| 67 | tts_buffer.seek(0) | 67 | tts_buffer.seek(0) |
| 68 | await PlayUtils.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True) | 68 | await PlayCommands.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True) |
| 69 | 69 | ||
| 70 | else: | 70 | else: |
| 71 | await ctx.send_followup(response_text) | 71 | await ctx.send_followup(response_text) |
diff --git a/cogs/compression.py b/cogs/compression.py index d235c6d..1fcefb5 100644 --- a/cogs/compression.py +++ b/cogs/compression.py | |||
| @@ -5,7 +5,7 @@ | |||
| 5 | 5 | ||
| 6 | import mimetypes | 6 | import mimetypes |
| 7 | import os | 7 | import os |
| 8 | import subprocess | 8 | import asyncio |
| 9 | from typing import List, Optional | 9 | from typing import List, Optional |
| 10 | from os import PathLike | 10 | from os import PathLike |
| 11 | import discord | 11 | import discord |
| @@ -32,6 +32,38 @@ class Compression(commands.Cog): | |||
| 32 | if not os.path.exists(self.temp_dir): | 32 | if not os.path.exists(self.temp_dir): |
| 33 | os.makedirs(self.temp_dir) | 33 | os.makedirs(self.temp_dir) |
| 34 | 34 | ||
| 35 | def _process_video(self, file_path: str, output_path: str) -> str: | ||
| 36 | """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}" | ||
| 38 | return command | ||
| 39 | |||
| 40 | def _process_audio(self, file_path: str, output_path: str) -> str: | ||
| 41 | """Process audio compression""" | ||
| 42 | if output_path.lower().endswith('.mp3'): | ||
| 43 | output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '') | ||
| 44 | if not output_path.lower().endswith('.ogg'): | ||
| 45 | 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}" | ||
| 47 | return command | ||
| 48 | |||
| 49 | def _process_image(self, file_path: str, output_path: str) -> tuple[str, str]: | ||
| 50 | """Process image compression""" | ||
| 51 | ext = os.path.splitext(file_path)[1].lower() | ||
| 52 | if ext in [".jpg", ".jpeg"]: | ||
| 53 | command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}" | ||
| 54 | elif ext == ".gif": | ||
| 55 | command = f"magick {file_path} -coalesce -resize 30% -colors 32 -strip -layers Optimize {output_path}" | ||
| 56 | else: | ||
| 57 | output_path = os.path.splitext(output_path)[0] + ".jpg" | ||
| 58 | command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}" | ||
| 59 | return command, output_path | ||
| 60 | |||
| 61 | async def _execute_compression(self, command: str) -> bool: | ||
| 62 | """Execute compression command""" | ||
| 63 | proc = await asyncio.create_subprocess_exec(command) | ||
| 64 | await proc.wait() | ||
| 65 | return proc.returncode == 0 | ||
| 66 | |||
| 35 | async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]: | 67 | async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]: |
| 36 | """Compress a single attachment and return the compressed file""" | 68 | """Compress a single attachment and return the compressed file""" |
| 37 | file_path = PathLike[str] | 69 | file_path = PathLike[str] |
| @@ -46,32 +78,23 @@ class Compression(commands.Cog): | |||
| 46 | file_type = filetype(file_path) | 78 | file_type = filetype(file_path) |
| 47 | logger.info(f"Compression started for: {file_type}") | 79 | logger.info(f"Compression started for: {file_type}") |
| 48 | 80 | ||
| 49 | if file_type == "Video": | 81 | command = self._get_compression_command(file_type, file_path, output_path) |
| 50 | 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}" | 82 | if command is None: |
| 51 | elif file_type == "Audio": | ||
| 52 | if output_path.lower().endswith('.mp3'): | ||
| 53 | output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '') | ||
| 54 | if not output_path.lower().endswith('.ogg'): | ||
| 55 | output_path = os.path.splitext(output_path)[0] + '.ogg' | ||
| 56 | command = f"ffmpeg -y -i {file_path} -c:a libopus -b:a 500 -ar 12000 -ac 1 {output_path}" | ||
| 57 | elif file_type == "Image": | ||
| 58 | ext = os.path.splitext(file_path)[1].lower() | ||
| 59 | if ext in [".jpg", ".jpeg"]: | ||
| 60 | command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}" | ||
| 61 | elif ext == ".gif": | ||
| 62 | command = f"magick {file_path} -coalesce -resize 30% -colors 32 -strip -layers Optimize {output_path}" | ||
| 63 | else: | ||
| 64 | output_path = os.path.splitext(output_path)[0] + ".jpg" | ||
| 65 | command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}" | ||
| 66 | else: | ||
| 67 | logger.info(f"Unsupported file type: {file_type}") | 83 | logger.info(f"Unsupported file type: {file_type}") |
| 68 | os.remove(file_path) | 84 | os.remove(file_path) |
| 69 | return None | 85 | return None |
| 70 | 86 | ||
| 87 | # Update output_path if it was modified | ||
| 88 | if file_type == "Image": | ||
| 89 | command, output_path = command | ||
| 90 | elif file_type == "Audio": | ||
| 91 | # output_path is already modified in _process_audio | ||
| 92 | pass | ||
| 93 | |||
| 71 | # Run the compression | 94 | # Run the compression |
| 72 | subprocess.run(command, check=True, shell=True) | 95 | success = await self._execute_compression(command) |
| 73 | 96 | ||
| 74 | if os.path.exists(output_path): | 97 | if success and os.path.exists(output_path): |
| 75 | logger.info(f"Successfully compressed: {attachment.filename}") | 98 | logger.info(f"Successfully compressed: {attachment.filename}") |
| 76 | return discord.File(output_path, filename=f"compressed_{attachment.filename}") | 99 | return discord.File(output_path, filename=f"compressed_{attachment.filename}") |
| 77 | return None | 100 | return None |
| @@ -84,6 +107,17 @@ class Compression(commands.Cog): | |||
| 84 | # Clean up temporary files | 107 | # Clean up temporary files |
| 85 | if 'file_path' in locals() and os.path.exists(file_path): | 108 | if 'file_path' in locals() and os.path.exists(file_path): |
| 86 | os.remove(file_path) | 109 | os.remove(file_path) |
| 110 | |||
| 111 | def _get_compression_command(self, file_type: str, file_path: str, output_path: str): | ||
| 112 | """Get the appropriate compression command based on file type""" | ||
| 113 | if file_type == "Video": | ||
| 114 | return self._process_video(file_path, output_path) | ||
| 115 | elif file_type == "Audio": | ||
| 116 | return self._process_audio(file_path, output_path) | ||
| 117 | elif file_type == "Image": | ||
| 118 | return self._process_image(file_path, output_path) | ||
| 119 | else: | ||
| 120 | return None | ||
| 87 | 121 | ||
| 88 | async def _compress_attachments(self, attachments: List[discord.Attachment]) -> List[discord.File]: | 122 | async def _compress_attachments(self, attachments: List[discord.Attachment]) -> List[discord.File]: |
| 89 | """Compress multiple attachments and return list of compressed files""" | 123 | """Compress multiple attachments and return list of compressed files""" |
diff --git a/cogs/conversion.py b/cogs/conversion.py index 11f4f1b..f61d30f 100644 --- a/cogs/conversion.py +++ b/cogs/conversion.py | |||
| @@ -8,8 +8,10 @@ import requests | |||
| 8 | from discord.ext import commands | 8 | from discord.ext import commands |
| 9 | import logging | 9 | import logging |
| 10 | import os | 10 | import os |
| 11 | import subprocess | 11 | import asyncio |
| 12 | import aiofiles | ||
| 12 | import mimetypes | 13 | import mimetypes |
| 14 | import subprocess | ||
| 13 | 15 | ||
| 14 | logger = logging.getLogger(__name__) | 16 | logger = logging.getLogger(__name__) |
| 15 | 17 | ||
| @@ -28,108 +30,119 @@ class Conversion(commands.Cog): | |||
| 28 | def __init__(self, bot): | 30 | def __init__(self, bot): |
| 29 | self.bot = bot | 31 | self.bot = bot |
| 30 | 32 | ||
| 31 | @commands.slash_command(name="konwersja", description="konwertuj plik na inny format") | 33 | async def _setup_file_paths(self, plik, output_format): |
| 32 | @discord.option( | 34 | """Setup input and output file paths""" |
| 33 | "plik", | ||
| 34 | description="plik do konwersji", | ||
| 35 | required=True, | ||
| 36 | type=discord.Attachment | ||
| 37 | ) | ||
| 38 | @discord.option( | ||
| 39 | "format", | ||
| 40 | description="format konwersji", | ||
| 41 | required=True, | ||
| 42 | type=str | ||
| 43 | ) | ||
| 44 | async def convert(self, ctx, plik, output_format, args): | ||
| 45 | await ctx.defer() | ||
| 46 | # Save files to the temp directory inside the bot's root directory | ||
| 47 | bot_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 35 | bot_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 48 | temp_dir = os.path.join(bot_dir, "temp") | 36 | temp_dir = os.path.join(bot_dir, "temp") |
| 49 | if not os.path.exists(temp_dir): | 37 | if not os.path.exists(temp_dir): |
| 50 | os.makedirs(temp_dir) | 38 | os.makedirs(temp_dir) |
| 39 | |||
| 51 | file_path = os.path.join(temp_dir, plik.filename) | 40 | file_path = os.path.join(temp_dir, plik.filename) |
| 52 | filename_noext = os.path.splitext(plik.filename)[0] | 41 | filename_noext = os.path.splitext(plik.filename)[0] |
| 53 | await plik.save(file_path) | 42 | await plik.save(file_path) |
| 54 | 43 | ||
| 55 | input_path = file_path | 44 | input_path = file_path |
| 56 | output_path = os.path.join(temp_dir, f"{filename_noext}k.{output_format}") | 45 | output_path = os.path.join(temp_dir, f"{filename_noext}k.{output_format}") |
| 57 | file_type = filetype(file_path) | 46 | return input_path, output_path |
| 58 | logger.info(f"Conversion started for: {file_type}") | 47 | |
| 59 | if file_type == "Video" or file_type == "Audio": | 48 | def _build_conversion_command(self, file_type, input_path, output_path, args): |
| 60 | # Execute ffmpeg command to compress video | 49 | """Build the appropriate conversion command based on file type""" |
| 61 | ffmpeg_command = f"ffmpeg -i {input_path} {args} {output_path}" | 50 | if file_type in ["Video", "Audio"]: |
| 51 | return f"ffmpeg -i {input_path} {args} {output_path}" | ||
| 62 | elif file_type == "Image": | 52 | elif file_type == "Image": |
| 63 | img_command = f"magick {input_path} {args} {output_path}" | 53 | return f"magick {input_path} {args} {output_path}" |
| 64 | ffmpeg_command = img_command | ||
| 65 | else: | 54 | else: |
| 66 | await ctx.respond("Plik jest nieobsługiwany", ephemeral=True) | 55 | return None |
| 67 | logger.info("Unsupported file type") | ||
| 68 | return | ||
| 69 | 56 | ||
| 57 | async def _upload_to_copyparty(self, output_path): | ||
| 58 | """Upload file to copyparty and return download URL""" | ||
| 70 | try: | 59 | try: |
| 71 | subprocess.run(ffmpeg_command, check=True, shell=True) | 60 | async with aiofiles.open(output_path, 'rb') as f: |
| 72 | # Upload the compressed file | 61 | files = { |
| 73 | if os.path.getsize(output_path) > 1024*1024*10: | 62 | 'f': (output_path, f, 'application/octet-stream') |
| 74 | try: | 63 | } |
| 64 | headers = { | ||
| 65 | 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' | ||
| 66 | } | ||
| 67 | params = { | ||
| 68 | 'j': '1', | ||
| 69 | 'ck': 'b2' | ||
| 70 | } | ||
| 71 | |||
| 72 | base_url = "https://catbox.2137697.xyz/sperma/" | ||
| 73 | response = requests.post(base_url, files=files, headers=headers, params=params) | ||
| 74 | |||
| 75 | if response.status_code in [200, 201]: | ||
| 76 | result = response.json() | ||
| 77 | if 'fileurl' in result: | ||
| 78 | return result['fileurl'] | ||
| 79 | else: | ||
| 80 | raise ValueError(f"Server response missing fileurl: {result}") | ||
| 81 | else: | ||
| 82 | error_msg = response.text if response.text else 'Unknown error' | ||
| 83 | raise requests.RequestException(f"HTTP {response.status_code}: {error_msg}") | ||
| 84 | |||
| 85 | except Exception as e: | ||
| 86 | logger.error(f"copyparty upload error: {e}", exc_info=True) | ||
| 87 | raise | ||
| 75 | 88 | ||
| 76 | # Upload to Copyparty | 89 | async def _handle_file_output(self, ctx, output_path): |
| 77 | with open(output_path, 'rb') as f: | 90 | """Handle file output based on size""" |
| 78 | files = { | 91 | if os.path.getsize(output_path) > 1024*1024*10: |
| 79 | 'f': (output_path, f, 'application/octet-stream') | 92 | try: |
| 80 | } | 93 | dl_url = await self._upload_to_copyparty(output_path) |
| 81 | headers = { | 94 | await ctx.followup.send(f"wysyłon\n{dl_url}\n") |
| 82 | 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' | 95 | except Exception as upload_error: |
| 83 | } | 96 | await ctx.followup.send(f"copyparty mnie nienawidzi :cccc\n {str(upload_error)}") |
| 84 | params = { | 97 | else: |
| 85 | 'j': '1', # Return JSON response | 98 | await ctx.respond(file=discord.File(output_path)) |
| 86 | 'ck': 'b2' # Use blake2b checksum | ||
| 87 | } | ||
| 88 | |||
| 89 | # Ensure the URL ends with a single slash | ||
| 90 | base_url = "https://catbox.2137697.xyz/sperma/" | ||
| 91 | |||
| 92 | response = requests.post( | ||
| 93 | base_url, | ||
| 94 | files=files, | ||
| 95 | headers=headers, | ||
| 96 | params=params | ||
| 97 | ) | ||
| 98 | 99 | ||
| 99 | if response.status_code == 200 or response.status_code == 201: | 100 | def _cleanup_files(self, *file_paths): |
| 100 | try: | 101 | """Clean up temporary files""" |
| 101 | result = response.json() | 102 | for path in file_paths: |
| 102 | if 'fileurl' in result: | 103 | if os.path.exists(path): |
| 103 | dl_url = result['fileurl'] | 104 | os.remove(path) |
| 104 | await ctx.followup.send( | ||
| 105 | f"wysyłon\n" | ||
| 106 | f"{dl_url}\n" | ||
| 107 | ) | ||
| 108 | else: | ||
| 109 | await ctx.followup.send( | ||
| 110 | f"Odpowiedź serwera nie zawiera wymaganych danych: {result}") | ||
| 111 | except ValueError: | ||
| 112 | await ctx.followup.send(f"Nieprawidłowa odpowiedź z serwera: {response.text}") | ||
| 113 | else: | ||
| 114 | error_msg = response.text if response.text else 'Unknown error' | ||
| 115 | await ctx.followup.send( | ||
| 116 | f"nie udało się wysłać pliku (HTTP {response.status_code}): {error_msg}") | ||
| 117 | 105 | ||
| 118 | except Exception as upload_error: | 106 | @commands.slash_command(name="konwersja", description="konwertuj plik na inny format") |
| 119 | await ctx.followup.send(f"copyparty mnie nienawidzi :cccc\n {str(upload_error)}") | 107 | @discord.option( |
| 120 | logger.error(f"copyparty upload error: {upload_error}", exc_info=True) | 108 | "plik", |
| 121 | else: | 109 | description="plik do konwersji", |
| 122 | await ctx.respond(file=discord.File(output_path)) | 110 | required=True, |
| 111 | type=discord.Attachment | ||
| 112 | ) | ||
| 113 | @discord.option( | ||
| 114 | "format", | ||
| 115 | description="format konwersji", | ||
| 116 | required=True, | ||
| 117 | type=str | ||
| 118 | ) | ||
| 119 | async def convert(self, ctx, plik, output_format, args): | ||
| 120 | await ctx.defer() | ||
| 121 | |||
| 122 | try: | ||
| 123 | input_path, output_path = await self._setup_file_paths(plik, output_format) | ||
| 124 | file_type = filetype(input_path) | ||
| 125 | logger.info(f"Conversion started for: {file_type}") | ||
| 126 | |||
| 127 | command = self._build_conversion_command(file_type, input_path, output_path, args) | ||
| 128 | if not command: | ||
| 129 | await ctx.respond("Plik jest nieobsługiwany", ephemeral=True) | ||
| 130 | logger.info("Unsupported file type") | ||
| 131 | return | ||
| 132 | |||
| 133 | proc = await asyncio.create_subprocess_exec(*command.split()) | ||
| 134 | await proc.wait() | ||
| 135 | |||
| 136 | await self._handle_file_output(ctx, output_path) | ||
| 123 | logger.info(f"Conversion completed for: {file_type}") | 137 | logger.info(f"Conversion completed for: {file_type}") |
| 124 | # Delete compressed file | 138 | |
| 125 | os.remove(input_path) | ||
| 126 | os.remove(output_path) | ||
| 127 | except subprocess.CalledProcessError as e: | 139 | except subprocess.CalledProcessError as e: |
| 128 | await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True) | 140 | await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True) |
| 129 | if os.path.exists(input_path): | 141 | except Exception as e: |
| 130 | os.remove(input_path) | 142 | await ctx.respond(f"An error occurred: {str(e)}", ephemeral=True) |
| 131 | if os.path.exists(output_path): | 143 | finally: |
| 132 | os.remove(output_path) | 144 | if 'input_path' in locals(): |
| 145 | self._cleanup_files(input_path, output_path) | ||
| 133 | 146 | ||
| 134 | def setup(bot): | 147 | def setup(bot): |
| 135 | bot.add_cog(Conversion(bot)) \ No newline at end of file | 148 | bot.add_cog(Conversion(bot)) \ No newline at end of file |
diff --git a/cogs/downloader.py b/cogs/downloader.py index 0331ae0..d2835d8 100644 --- a/cogs/downloader.py +++ b/cogs/downloader.py | |||
| @@ -12,6 +12,7 @@ import re | |||
| 12 | import yt_dlp | 12 | import yt_dlp |
| 13 | from discord.ext import commands | 13 | from discord.ext import commands |
| 14 | from dotenv import load_dotenv | 14 | from dotenv import load_dotenv |
| 15 | import aiofiles | ||
| 15 | 16 | ||
| 16 | logger = logging.getLogger(__name__) | 17 | logger = logging.getLogger(__name__) |
| 17 | 18 | ||
| @@ -68,152 +69,144 @@ class Downloader(commands.Cog): | |||
| 68 | if 'details' in response: | 69 | if 'details' in response: |
| 69 | logger.error(f"Details: {response['details']}") | 70 | logger.error(f"Details: {response['details']}") |
| 70 | 71 | ||
| 71 | |||
| 72 | @staticmethod | 72 | @staticmethod |
| 73 | async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str = None): | 73 | async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str = None): |
| 74 | try: | 74 | try: |
| 75 | logger.info(f"Downloading {url} as {fformat}") | 75 | logger.info(f"Downloading {url} as {fformat}") |
| 76 | 76 | ||
| 77 | with tempfile.TemporaryDirectory(delete=False) as temp_dir: | 77 | with tempfile.TemporaryDirectory(delete=False) as temp_dir: |
| 78 | # Configure yt-dlp to use the original filename | 78 | ydl_opts = Downloader._get_ydl_options(fformat, temp_dir) |
| 79 | ydl_opts = { | ||
| 80 | 'outtmpl': os.path.join(temp_dir, '%(title)s.%(ext)s'), | ||
| 81 | 'format': 'bestaudio/best' if fformat in ['mp3', 'wav', | ||
| 82 | 'm4a'] else 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', | ||
| 83 | 'merge_output_format': 'mp4', | ||
| 84 | 'postprocessors': [], | ||
| 85 | 'remote-components': 'ejs:github', | ||
| 86 | 'js-runtimes': {'deno': {'path': '/usr/bin/deno'}}, | ||
| 87 | 'o': '"%(title)s.%(ext)s"' | ||
| 88 | } | ||
| 89 | |||
| 90 | # Add post-processing for audio formats | ||
| 91 | if fformat in ['mp3', 'wav', 'm4a']: | ||
| 92 | ydl_opts['postprocessors'].append({ | ||
| 93 | 'key': 'FFmpegExtractAudio', | ||
| 94 | 'preferredcodec': fformat, | ||
| 95 | 'preferredquality': '192', | ||
| 96 | }) | ||
| 97 | elif fformat != 'mp4': # For other video formats | ||
| 98 | ydl_opts['postprocessors'].append({ | ||
| 99 | 'key': 'FFmpegVideoConvertor', | ||
| 100 | 'preferedformat': fformat, | ||
| 101 | }) | ||
| 102 | 79 | ||
| 103 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: | 80 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: |
| 104 | ydl.download([url]) | 81 | ydl.download([url]) |
| 105 | 82 | ||
| 106 | # Find the downloaded file (should be the only file in the temp directory) | 83 | actual_file, filename = Downloader._find_and_prepare_file(temp_dir, filename_user) |
| 107 | files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))] | ||
| 108 | if not files: | ||
| 109 | raise Exception(f"No file was downloaded") | ||
| 110 | |||
| 111 | actual_file = os.path.join(temp_dir, files[0]) | ||
| 112 | extension = actual_file.split('.')[-1] | ||
| 113 | 84 | ||
| 114 | if not filename_user: | 85 | if Downloader._should_upload_to_server(actual_file, catbox): |
| 115 | filename = os.path.basename(actual_file) | 86 | return await Downloader._upload_to_server(actual_file, filename, temp_dir) |
| 116 | else: | 87 | else: |
| 117 | filename = filename_user + '.' + extension | 88 | return Downloader._return_file_response(actual_file, filename, temp_dir) |
| 118 | actual_file = os.path.join(temp_dir, filename) | 89 | |
| 119 | 90 | except Exception as e: | |
| 120 | if not os.path.exists(actual_file): | 91 | return Downloader._create_error_response("Download failed", str(e), temp_dir if 'temp_dir' in locals() else None) |
| 121 | raise Exception(f"Downloaded file not found: {actual_file}") | 92 | |
| 122 | 93 | @staticmethod | |
| 123 | if os.path.getsize(actual_file) > 1024*1024*10 or catbox: | 94 | def _get_ydl_options(fformat: str, temp_dir: str) -> dict: |
| 124 | try: | 95 | ydl_opts = { |
| 125 | 96 | 'outtmpl': os.path.join(temp_dir, '%(title)s.%(ext)s'), | |
| 126 | # Upload to Copyparty | 97 | 'format': 'bestaudio/best' if fformat in ['mp3', 'wav', 'm4a'] else 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', |
| 127 | with open(actual_file, 'rb') as f: | 98 | 'merge_output_format': 'mp4', |
| 128 | files = { | 99 | 'postprocessors': [], |
| 129 | 'f': (filename, f, 'application/octet-stream') | 100 | 'remote-components': 'ejs:github', |
| 130 | } | 101 | 'js-runtimes': {'deno': {'path': '/usr/bin/deno'}}, |
| 131 | headers = { | 102 | 'o': '"%(title)s.%(ext)s"' |
| 132 | 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' | 103 | } |
| 133 | } | 104 | |
| 134 | params = { | 105 | if fformat in ['mp3', 'wav', 'm4a']: |
| 135 | 'j': '1', # Return JSON response | 106 | ydl_opts['postprocessors'].append({ |
| 136 | 'ck': 'b2' # Use blake2b checksum | 107 | 'key': 'FFmpegExtractAudio', |
| 137 | } | 108 | 'preferredcodec': fformat, |
| 138 | 109 | 'preferredquality': '192', | |
| 139 | # Ensure the URL ends with a single slash | 110 | }) |
| 140 | base_url = "https://catbox.2137697.xyz/sperma/" | 111 | elif fformat != 'mp4': |
| 141 | 112 | ydl_opts['postprocessors'].append({ | |
| 142 | response = requests.post( | 113 | 'key': 'FFmpegVideoConvertor', |
| 143 | base_url, | 114 | 'preferedformat': fformat, |
| 144 | files=files, | 115 | }) |
| 145 | headers=headers, | 116 | |
| 146 | params=params | 117 | return ydl_opts |
| 147 | ) | 118 | |
| 148 | 119 | @staticmethod | |
| 149 | if response.status_code == 200 or response.status_code == 201: | 120 | def _find_and_prepare_file(temp_dir: str, filename_user: str) -> tuple: |
| 150 | try: | 121 | files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))] |
| 151 | result = response.json() | 122 | if not files: |
| 152 | if 'fileurl' in result: | 123 | raise FileNotFoundError("No file was downloaded") |
| 153 | dl_url = result['fileurl'] | 124 | |
| 154 | return { | 125 | actual_file = os.path.join(temp_dir, files[0]) |
| 155 | "status": "success", | 126 | extension = actual_file.split('.')[-1] |
| 156 | "type": "url", | 127 | |
| 157 | "content": dl_url, | 128 | if not filename_user: |
| 158 | "filename": filename, | 129 | filename = os.path.basename(actual_file) |
| 159 | "temp_dir": temp_dir | 130 | else: |
| 160 | } | 131 | filename = filename_user + '.' + extension |
| 161 | else: | 132 | actual_file = os.path.join(temp_dir, filename) |
| 162 | return { | 133 | |
| 163 | "status": "error", | 134 | if not os.path.exists(actual_file): |
| 164 | "type": "error", | 135 | raise FileNotFoundError(f"Downloaded file not found: {actual_file}") |
| 165 | "content": "Invalid response from file server", | 136 | |
| 166 | "details": str(result), | 137 | return actual_file, filename |
| 167 | "temp_dir": temp_dir | ||
| 168 | } | ||
| 169 | except ValueError: | ||
| 170 | return { | ||
| 171 | "status": "error", | ||
| 172 | "type": "error", | ||
| 173 | "content": "Failed to parse server response", | ||
| 174 | "details": response.text[:200], # First 200 chars of response | ||
| 175 | "temp_dir": temp_dir | ||
| 176 | } | ||
| 177 | else: | ||
| 178 | error_msg = response.text if response.text else 'Unknown error' | ||
| 179 | return { | ||
| 180 | "status": "error", | ||
| 181 | "type": "error", | ||
| 182 | "content": f"Server returned {response.status_code}", | ||
| 183 | "details": error_msg, | ||
| 184 | "temp_dir": temp_dir | ||
| 185 | } | ||
| 186 | |||
| 187 | except Exception as upload_error: | ||
| 188 | return { | ||
| 189 | "status": "error", | ||
| 190 | "type": "error", | ||
| 191 | "content": "Upload failed", | ||
| 192 | "details": str(upload_error), | ||
| 193 | "temp_dir": temp_dir | ||
| 194 | } | ||
| 195 | 138 | ||
| 139 | @staticmethod | ||
| 140 | def _should_upload_to_server(actual_file: str, catbox: bool) -> bool: | ||
| 141 | return os.path.getsize(actual_file) > 1024*1024*10 or catbox | ||
| 142 | |||
| 143 | @staticmethod | ||
| 144 | async def _upload_to_server(actual_file: str, filename: str, temp_dir: str) -> dict: | ||
| 145 | try: | ||
| 146 | async with aiofiles.open(actual_file, 'rb') as f: | ||
| 147 | content = await f.read() | ||
| 148 | files = {'f': (filename, content, 'application/octet-stream')} | ||
| 149 | headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'} | ||
| 150 | params = {'j': '1', 'ck': 'b2'} | ||
| 151 | base_url = "https://catbox.2137697.xyz/sperma/" | ||
| 152 | |||
| 153 | response = requests.post(base_url, files=files, headers=headers, params=params) | ||
| 154 | |||
| 155 | if response.status_code in [200, 201]: | ||
| 156 | return Downloader._handle_upload_success(response, filename, temp_dir) | ||
| 196 | else: | 157 | else: |
| 197 | # If we have a file to return directly | 158 | return Downloader._create_error_response( |
| 198 | return { | 159 | f"Server returned {response.status_code}", |
| 199 | "status": "success", | 160 | response.text or 'Unknown error', |
| 200 | "type": "file", | 161 | temp_dir |
| 201 | "content": actual_file, | 162 | ) |
| 202 | "filename": filename, | 163 | except Exception as upload_error: |
| 203 | "temp_dir": temp_dir | 164 | return Downloader._create_error_response("Upload failed", str(upload_error), temp_dir) |
| 204 | } | ||
| 205 | 165 | ||
| 206 | except Exception as e: | 166 | @staticmethod |
| 207 | error_msg = f"Error in download command: {e}" | 167 | def _handle_upload_success(response: requests.Response, filename: str, temp_dir: str) -> dict: |
| 208 | logger.error(error_msg, exc_info=True) | 168 | try: |
| 209 | return { | 169 | result = response.json() |
| 210 | "status": "error", | 170 | if 'fileurl' in result: |
| 211 | "type": "error", | 171 | return { |
| 212 | "content": "Download failed", | 172 | "status": "success", |
| 213 | "details": str(e), | 173 | "type": "url", |
| 214 | "temp_dir": temp_dir | 174 | "content": result['fileurl'], |
| 215 | } | 175 | "filename": filename, |
| 176 | "temp_dir": temp_dir | ||
| 177 | } | ||
| 178 | else: | ||
| 179 | return Downloader._create_error_response( | ||
| 180 | "Invalid response from file server", | ||
| 181 | str(result), | ||
| 182 | temp_dir | ||
| 183 | ) | ||
| 184 | except ValueError: | ||
| 185 | return Downloader._create_error_response( | ||
| 186 | "Failed to parse server response", | ||
| 187 | response.text[:200], | ||
| 188 | temp_dir | ||
| 189 | ) | ||
| 216 | 190 | ||
| 191 | @staticmethod | ||
| 192 | def _return_file_response(actual_file: str, filename: str, temp_dir: str) -> dict: | ||
| 193 | return { | ||
| 194 | "status": "success", | ||
| 195 | "type": "file", | ||
| 196 | "content": actual_file, | ||
| 197 | "filename": filename, | ||
| 198 | "temp_dir": temp_dir | ||
| 199 | } | ||
| 200 | |||
| 201 | @staticmethod | ||
| 202 | def _create_error_response(content: str, details: str, temp_dir: str) -> dict: | ||
| 203 | return { | ||
| 204 | "status": "error", | ||
| 205 | "type": "error", | ||
| 206 | "content": content, | ||
| 207 | "details": details, | ||
| 208 | "temp_dir": temp_dir | ||
| 209 | } | ||
| 217 | 210 | ||
| 218 | 211 | ||
| 219 | def setup(bot): | 212 | def setup(bot): |
diff --git a/cogs/jarvis.py b/cogs/jarvis.py index 0096b38..b199c8a 100644 --- a/cogs/jarvis.py +++ b/cogs/jarvis.py | |||
| @@ -29,126 +29,164 @@ class Jarvis(commands.Cog): | |||
| 29 | 29 | ||
| 30 | if message.mentions: | 30 | if message.mentions: |
| 31 | if message.mentions[0].id == self.bot.user.id: | 31 | if message.mentions[0].id == self.bot.user.id: |
| 32 | content = await self.get_original(message) | 32 | await self.jarvis(message) |
| 33 | await self.jarvis(message, content) | ||
| 34 | 33 | ||
| 35 | elif "jarvis" in message.content.lower(): | 34 | elif "jarvis" in message.content.lower(): |
| 36 | content = await self.get_original(message) | 35 | await self.jarvis(message) |
| 37 | await self.jarvis(message, content) | ||
| 38 | 36 | ||
| 39 | @staticmethod | 37 | @staticmethod |
| 40 | async def jarvis(message, content): | 38 | async def jarvis(message): |
| 41 | url = "https://openrouter.ai/api/v1/chat/completions" | ||
| 42 | headers = { | 39 | headers = { |
| 43 | 40 | "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}", | |
| 44 | "Authorization": f"Bearer {os.getenv("OPENROUTER_API_KEY")}", | ||
| 45 | "Content-Type": "application/json" | 41 | "Content-Type": "application/json" |
| 46 | } | 42 | } |
| 47 | 43 | ||
| 48 | # Build messages array | 44 | messages = Jarvis._build_messages(message) |
| 49 | messages = [ | 45 | payload = Jarvis._build_payload(messages) |
| 50 | { | 46 | |
| 51 | "role": "system", | 47 | response_data = await Jarvis._make_api_request(payload, headers) |
| 52 | "content": "your task is to tell the program what to do and how to respond after receiving a message.\n" | 48 | response_message, action = Jarvis._parse_response(response_data) |
| 53 | "message field has to be filled out in polish.\n" | 49 | |
| 54 | "act like jarvis, be a personal assistant to user, do not use emojis\n" | 50 | channel = message.channel |
| 55 | "do not ask the user if they want to do an action. the user will tell you that" | 51 | await channel.send(response_message) |
| 56 | } | 52 | |
| 57 | ] | 53 | if action == "download": |
| 54 | await Jarvis._handle_download_action(message, channel) | ||
| 55 | |||
| 56 | @staticmethod | ||
| 57 | def _build_messages(message): | ||
| 58 | messages = [{ | ||
| 59 | "role": "system", | ||
| 60 | "content": "your task is to tell the program what to do and how to respond after receiving a message.\n" | ||
| 61 | "message field has to be filled out in polish.\n" | ||
| 62 | "act like jarvis, be a personal assistant to user, do not use emojis\n" | ||
| 63 | "do not ask the user if they want to do an action. the user will tell you that" | ||
| 64 | }] | ||
| 58 | 65 | ||
| 59 | # Add original message with display name | ||
| 60 | messages.append({ | 66 | messages.append({ |
| 61 | "role": "user", | 67 | "role": "user", |
| 62 | "content": f"[from {message.author.display_name}]: {message.content}" | 68 | "content": f"[from {message.author.display_name}]: {message.content}" |
| 63 | }) | 69 | }) |
| 64 | 70 | ||
| 65 | # If it's a reply, add the original message and reply as separate messages | 71 | if Jarvis._is_reply(message): |
| 66 | if message.reference and (message.type == discord.MessageType.reply or message.reference.resolved): | 72 | messages.extend(Jarvis._build_reply_messages(message)) |
| 67 | if message.reference.resolved: | 73 | |
| 68 | original_msg = message.reference.resolved | 74 | return messages |
| 69 | messages.append({ | 75 | |
| 70 | "role": "user", | 76 | @staticmethod |
| 71 | "content": f"[original message from {original_msg.author.display_name}]: {original_msg.content}" | 77 | def _is_reply(message): |
| 72 | }) | 78 | return message.reference and (message.type == discord.MessageType.reply or message.reference.resolved) |
| 73 | messages.append({ | 79 | |
| 74 | "role": "user", | 80 | @staticmethod |
| 75 | "content": f"[reply from {message.author.display_name}]: {message.content}" | 81 | def _build_reply_messages(message): |
| 76 | }) | 82 | original_msg = message.reference.resolved |
| 77 | 83 | return [ | |
| 78 | payload = { | 84 | { |
| 85 | "role": "user", | ||
| 86 | "content": f"[original message from {original_msg.author.display_name}]: {original_msg.content}" | ||
| 87 | }, | ||
| 88 | { | ||
| 89 | "role": "user", | ||
| 90 | "content": f"[reply from {message.author.display_name}]: {message.content}" | ||
| 91 | } | ||
| 92 | ] | ||
| 93 | |||
| 94 | @staticmethod | ||
| 95 | def _build_payload(messages): | ||
| 96 | return { | ||
| 79 | "model": "arcee-ai/trinity-large-preview:free", | 97 | "model": "arcee-ai/trinity-large-preview:free", |
| 80 | "messages": messages, | 98 | "messages": messages, |
| 81 | "response_format": { | 99 | "response_format": Jarvis._get_response_schema(), |
| 82 | "type": "json_schema", | 100 | "plugins": [{"id": "response-healing"}] |
| 83 | "json_schema": { | 101 | } |
| 84 | "name": "response", | 102 | |
| 85 | "strict": True, | 103 | @staticmethod |
| 86 | "schema": { | 104 | def _get_response_schema(): |
| 87 | "type": "object", | 105 | return { |
| 88 | "properties": { | 106 | "type": "json_schema", |
| 89 | "message": { | 107 | "json_schema": { |
| 90 | "type": "string", | 108 | "name": "response", |
| 91 | "description": "the message that user will receive", | 109 | "strict": True, |
| 92 | }, | 110 | "schema": { |
| 93 | "action": { | 111 | "type": "object", |
| 94 | "type": "string", | 112 | "properties": { |
| 95 | "description": "the action to do, either download, compress or none" | 113 | "message": { |
| 96 | } | 114 | "type": "string", |
| 115 | "description": "the message that user will receive", | ||
| 97 | }, | 116 | }, |
| 98 | "required": ["message", "action"], | 117 | "action": { |
| 99 | "additionalProperties": False, | 118 | "type": "string", |
| 119 | "description": "the action to do, either download, compress or none" | ||
| 100 | } | 120 | } |
| 101 | } | 121 | }, |
| 102 | }, | 122 | "required": ["message", "action"], |
| 103 | "plugins": [ | 123 | "additionalProperties": False, |
| 104 | {"id": "response-healing"} | 124 | } |
| 105 | ] | 125 | } |
| 106 | } | 126 | } |
| 107 | 127 | ||
| 128 | @staticmethod | ||
| 129 | async def _make_api_request(payload, headers): | ||
| 130 | url = "https://openrouter.ai/api/v1/chat/completions" | ||
| 108 | response = requests.post(url=url, headers=headers, json=payload) | 131 | response = requests.post(url=url, headers=headers, json=payload) |
| 109 | # Parse the JSON response | ||
| 110 | response_data = response.json() | 132 | response_data = response.json() |
| 111 | logger.info(response_data) | 133 | logger.info(response_data) |
| 112 | # Extract the content string | 134 | return response_data |
| 113 | content = response_data['choices'][0]['message']['content'] | ||
| 114 | 135 | ||
| 115 | # Remove the code block markers and parse the inner JSON | 136 | @staticmethod |
| 137 | def _parse_response(response_data): | ||
| 138 | content = response_data['choices'][0]['message']['content'] | ||
| 116 | inner_json_str = content.strip('```json\n').strip('```').strip() | 139 | inner_json_str = content.strip('```json\n').strip('```').strip() |
| 117 | inner_data = json.loads(inner_json_str) | 140 | inner_data = json.loads(inner_json_str) |
| 118 | 141 | ||
| 119 | # Now you can access the message and action | 142 | response_message = inner_data['message'] |
| 120 | response_message = inner_data['message'] # "Proszę podać plik do kompresji lub link do pobrania." | 143 | action = inner_data['action'] |
| 121 | action = inner_data['action'] # "none OR download OR compress" | 144 | |
| 122 | channel = message.channel | ||
| 123 | logger.info(response_message) | 145 | logger.info(response_message) |
| 124 | logger.info(action) | 146 | logger.info(action) |
| 147 | |||
| 148 | return response_message, action | ||
| 125 | 149 | ||
| 126 | await channel.send(response_message) | 150 | @staticmethod |
| 151 | async def _handle_download_action(message, channel): | ||
| 152 | url_match = Jarvis._extract_url(message.content) | ||
| 153 | if not url_match: | ||
| 154 | await channel.send("Nie znaleziono poprawnego linku w odpowiedzi.") | ||
| 155 | return | ||
| 127 | 156 | ||
| 128 | if action == "download": | 157 | response = await Downloader.download_standalone(url=url_match) |
| 129 | # Extract the URL from the message using regex | 158 | await Jarvis._process_download_response(response, channel) |
| 130 | url_match = re.search(r'https://(?:[A-Za-z0-9-]+\.)*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,63}/\S*', message.content).group(0) | 159 | |
| 131 | if not url_match: | 160 | @staticmethod |
| 132 | await channel.send("Nie znaleziono poprawnego linku w odpowiedzi.") | 161 | def _extract_url(content): |
| 133 | return | 162 | match = re.search(r'https://(?:[A-Za-z0-9-]+\.)*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,63}/\S*', content) |
| 134 | 163 | return match.group(0) if match else None | |
| 135 | response = await Downloader.download_standalone(url=url_match) | 164 | |
| 136 | if isinstance(response, dict): | 165 | @staticmethod |
| 137 | if response["status"] == "success": | 166 | async def _process_download_response(response, channel): |
| 138 | if response["type"] == "url": | 167 | if not isinstance(response, dict): |
| 139 | await channel.send(f"pobioron i wysyłon: {response['content']}") | 168 | return |
| 140 | elif response["type"] == "file": | 169 | |
| 141 | file_data = response["content"] | 170 | if response["status"] == "success": |
| 142 | await channel.send("wysyłon:", file=discord.File(file_data)) | 171 | await Jarvis._handle_successful_download(response, channel) |
| 143 | shutil.rmtree(response["temp_dir"]) | 172 | else: |
| 144 | else: | 173 | await Jarvis._handle_failed_download(response) |
| 145 | logger.error(f"Error: {response['content']}") | 174 | |
| 146 | if 'details' in response: | 175 | @staticmethod |
| 147 | logger.error(f"Details: {response['details']}") | 176 | async def _handle_successful_download(response, channel): |
| 148 | shutil.rmtree(response["temp_dir"]) | 177 | if response["type"] == "url": |
| 149 | 178 | await channel.send(f"pobioron i wysyłon: {response['content']}") | |
| 150 | elif action == "compress": | 179 | elif response["type"] == "file": |
| 151 | pass | 180 | file_data = response["content"] |
| 181 | await channel.send("wysyłon:", file=discord.File(file_data)) | ||
| 182 | shutil.rmtree(response["temp_dir"]) | ||
| 183 | |||
| 184 | @staticmethod | ||
| 185 | async def _handle_failed_download(response): | ||
| 186 | logger.error(f"Error: {response['content']}") | ||
| 187 | if 'details' in response: | ||
| 188 | logger.error(f"Details: {response['details']}") | ||
| 189 | shutil.rmtree(response["temp_dir"]) | ||
| 152 | 190 | ||
| 153 | @staticmethod | 191 | @staticmethod |
| 154 | async def get_original(message): | 192 | async def get_original(message): |
diff --git a/cogs/math.py b/cogs/math.py index 4bd62a0..bf164b3 100644 --- a/cogs/math.py +++ b/cogs/math.py | |||
| @@ -4,7 +4,7 @@ | |||
| 4 | # https://codeberg.org/salmonik/senator-sperma | 4 | # https://codeberg.org/salmonik/senator-sperma |
| 5 | 5 | ||
| 6 | import logging | 6 | import logging |
| 7 | import subprocess | 7 | import asyncio |
| 8 | 8 | ||
| 9 | from discord import ApplicationContext | 9 | from discord import ApplicationContext |
| 10 | from discord.ext import commands | 10 | from discord.ext import commands |
| @@ -22,8 +22,9 @@ class Math(commands.Cog): | |||
| 22 | ) | 22 | ) |
| 23 | async def math(self, ctx: ApplicationContext, equation: str): | 23 | async def math(self, ctx: ApplicationContext, equation: str): |
| 24 | try: | 24 | try: |
| 25 | math = subprocess.check_output(f"qalc -e '{equation}'", shell=True) | 25 | math = await asyncio.create_subprocess_exec(f"qalc -e '{equation}'") |
| 26 | await ctx.respond(f"{equation} = {math.decode('utf-8').strip()}") | 26 | result = await math.wait() |
| 27 | await ctx.respond(f"{equation} = {result.decode('utf-8').strip()}") | ||
| 27 | logger.info(f"Math command executed: {equation}") | 28 | logger.info(f"Math command executed: {equation}") |
| 28 | except Exception as e: | 29 | except Exception as e: |
| 29 | await ctx.respond(e, ephemeral=True) | 30 | await ctx.respond(e, ephemeral=True) |
diff --git a/cogs/tts.py b/cogs/tts.py index 3c2f9ce..158006c 100644 --- a/cogs/tts.py +++ b/cogs/tts.py | |||
| @@ -6,7 +6,7 @@ | |||
| 6 | import os | 6 | import os |
| 7 | import json | 7 | import json |
| 8 | import discord | 8 | import discord |
| 9 | import tempfile | 9 | import aiofiles |
| 10 | import logging | 10 | import logging |
| 11 | from discord.ext import commands | 11 | from discord.ext import commands |
| 12 | from gtts import gTTS | 12 | from gtts import gTTS |
| @@ -18,80 +18,102 @@ class TTS(commands.Cog): | |||
| 18 | def __init__(self, bot): | 18 | def __init__(self, bot): |
| 19 | self.bot = bot | 19 | self.bot = bot |
| 20 | 20 | ||
| 21 | async def _load_settings(self): | ||
| 22 | """Load volume settings from settings.json""" | ||
| 23 | volume = 1.0 | ||
| 24 | if os.path.exists('settings.json'): | ||
| 25 | try: | ||
| 26 | async with aiofiles.open('settings.json') as f: | ||
| 27 | data = json.load(f) | ||
| 28 | volume = data.get('volume', 1.0) | ||
| 29 | except (json.JSONDecodeError, KeyError) as e: | ||
| 30 | logger.error(f"Error loading settings: {e}") | ||
| 31 | return volume | ||
| 32 | |||
| 33 | async def _handle_file_output(self, ctx, temp_path, text): | ||
| 34 | """Handle TTS file output""" | ||
| 35 | try: | ||
| 36 | await ctx.respond(file=discord.File(temp_path, filename='tts.mp3')) | ||
| 37 | logger.info(f"Sent TTS for text: {text}") | ||
| 38 | except Exception as e: | ||
| 39 | logger.error(f"Error sending file: {e}") | ||
| 40 | await ctx.respond("eror :c", ephemeral=True) | ||
| 41 | |||
| 42 | async def _get_or_connect_voice_client(self, channel): | ||
| 43 | """Get existing voice client or connect to new channel""" | ||
| 44 | voice_client = discord.utils.get(self.bot.voice_clients, guild=channel.guild) | ||
| 45 | |||
| 46 | if not voice_client or not voice_client.is_connected(): | ||
| 47 | voice_client = await channel.connect() | ||
| 48 | elif voice_client.channel != channel: | ||
| 49 | await voice_client.move_to(channel) | ||
| 50 | |||
| 51 | return voice_client | ||
| 52 | |||
| 53 | async def _handle_voice_output(self, ctx, temp_path, text, volume): | ||
| 54 | """Handle TTS voice channel output""" | ||
| 55 | await ctx.respond(content="\u200b", ephemeral=True) | ||
| 56 | await ctx.interaction.delete_original_response() | ||
| 57 | |||
| 58 | # Check if the user is in a voice channel | ||
| 59 | if not (ctx.author.voice and ctx.author.voice.channel): | ||
| 60 | logger.info("User is not in a voice channel") | ||
| 61 | return | ||
| 62 | |||
| 63 | channel = ctx.author.voice.channel | ||
| 64 | try: | ||
| 65 | voice_client = await self._get_or_connect_voice_client(channel) | ||
| 66 | |||
| 67 | # Play the TTS audio with volume adjustment | ||
| 68 | audio_source = discord.FFmpegPCMAudio( | ||
| 69 | temp_path, | ||
| 70 | options=f'-filter:a "volume={volume}"' | ||
| 71 | ) | ||
| 72 | voice_client.play(audio_source) | ||
| 73 | logger.info(f"Playing TTS: {text}") | ||
| 74 | |||
| 75 | # Wait for the audio to finish playing | ||
| 76 | while voice_client.is_playing(): | ||
| 77 | await asyncio.sleep(0.1) | ||
| 78 | |||
| 79 | except Exception as e: | ||
| 80 | logger.error(f"Error in voice channel handling: {e}") | ||
| 81 | await ctx.respond("eror :c", ephemeral=True) | ||
| 82 | |||
| 83 | async def _cleanup_temp_file(self, temp_path): | ||
| 84 | """Clean up temporary file""" | ||
| 85 | try: | ||
| 86 | if temp_path and os.path.exists(temp_path): | ||
| 87 | os.unlink(temp_path) | ||
| 88 | except Exception as e: | ||
| 89 | logger.error(f"Error cleaning up temp file {temp_path}: {e}") | ||
| 90 | |||
| 21 | @commands.slash_command(name="tts", description="Generuje TTS dla danego tekstu") | 91 | @commands.slash_command(name="tts", description="Generuje TTS dla danego tekstu") |
| 22 | async def tts(self, ctx, | 92 | async def tts(self, ctx, |
| 23 | text: str = discord.Option(str, "Tekst do przeczytania", required=True), | 93 | text: str = discord.Option(str, "Tekst do przeczytania", required=True), |
| 24 | output: str = discord.Option(str, "Wybierz sposób odtwarzania", choices=["file", "vc"], default="vc") | 94 | output: str = discord.Option(str, "Wybierz sposób odtwarzania", choices=["file", "vc"], default="vc") |
| 25 | ): | 95 | ): |
| 26 | await ctx.defer() | 96 | await ctx.defer() |
| 27 | # Create a temporary file for TTS output | ||
| 28 | with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file: | ||
| 29 | temp_path = temp_file.name | ||
| 30 | 97 | ||
| 98 | async with NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file: | ||
| 99 | temp_path = temp_file.name | ||
| 100 | |||
| 31 | try: | 101 | try: |
| 32 | # Generate and save TTS to temp file | 102 | # Generate and save TTS to temp file |
| 33 | tts = gTTS(text=text, lang='pl') | 103 | tts = gTTS(text=text, lang='pl') |
| 34 | tts.save(temp_path) | 104 | tts.save(temp_path) |
| 35 | 105 | ||
| 36 | # Load settings if available | 106 | # Load settings |
| 37 | volume = 1.0 # Default volume | 107 | volume = await self._load_settings() |
| 38 | if os.path.exists('settings.json'): | ||
| 39 | try: | ||
| 40 | with open('settings.json') as f: | ||
| 41 | data = json.load(f) | ||
| 42 | volume = data.get('volume', 1.0) | ||
| 43 | except (json.JSONDecodeError, KeyError) as e: | ||
| 44 | logger.error(f"Error loading settings: {e}") | ||
| 45 | 108 | ||
| 109 | # Handle output based on user choice | ||
| 46 | if output == "file": | 110 | if output == "file": |
| 47 | try: | 111 | await self._handle_file_output(ctx, temp_path, text) |
| 48 | await ctx.respond(file=discord.File(temp_path, filename='tts.mp3')) | ||
| 49 | logger.info(f"Sent TTS for text: {text}") | ||
| 50 | except Exception as e: | ||
| 51 | logger.error(f"Error sending file: {e}") | ||
| 52 | await ctx.respond("eror :c", ephemeral=True) | ||
| 53 | |||
| 54 | elif output == "vc": | 112 | elif output == "vc": |
| 55 | await ctx.respond(content="\u200b", ephemeral=True) | 113 | await self._handle_voice_output(ctx, temp_path, text, volume) |
| 56 | await ctx.interaction.delete_original_response() | ||
| 57 | |||
| 58 | # Check if the user is in a voice channel | ||
| 59 | if not (ctx.author.voice and ctx.author.voice.channel): | ||
| 60 | logger.info("User is not in a voice channel") | ||
| 61 | return | ||
| 62 | |||
| 63 | channel = ctx.author.voice.channel | ||
| 64 | try: | ||
| 65 | # Connect to the voice channel if not already connected | ||
| 66 | voice_client = discord.utils.get(self.bot.voice_clients, guild=channel.guild) | ||
| 67 | if not voice_client or not voice_client.is_connected(): | ||
| 68 | voice_client = await channel.connect() | ||
| 69 | elif voice_client.channel != channel: | ||
| 70 | await voice_client.move_to(channel) | ||
| 71 | |||
| 72 | # Play the TTS audio with volume adjustment | ||
| 73 | audio_source = discord.FFmpegPCMAudio( | ||
| 74 | temp_path, | ||
| 75 | options=f'-filter:a "volume={volume}"' | ||
| 76 | ) | ||
| 77 | voice_client.play(audio_source) | ||
| 78 | logger.info(f"Playing TTS: {text}") | ||
| 79 | |||
| 80 | # Wait for the audio to finish playing | ||
| 81 | while voice_client.is_playing(): | ||
| 82 | await asyncio.sleep(0.1) | ||
| 83 | |||
| 84 | except Exception as e: | ||
| 85 | logger.error(f"Error in voice channel handling: {e}") | ||
| 86 | await ctx.respond("eror :c", ephemeral=True) | ||
| 87 | 114 | ||
| 88 | finally: | 115 | finally: |
| 89 | # Clean up the temporary file | 116 | await self._cleanup_temp_file(temp_path) |
| 90 | try: | ||
| 91 | if temp_path and os.path.exists(temp_path): | ||
| 92 | os.unlink(temp_path) | ||
| 93 | except Exception as e: | ||
| 94 | logger.error(f"Error cleaning up temp file {temp_path}: {e}") | ||
| 95 | 117 | ||
| 96 | def setup(bot): | 118 | def setup(bot): |
| 97 | bot.add_cog(TTS(bot)) \ No newline at end of file | 119 | bot.add_cog(TTS(bot)) \ No newline at end of file |
diff --git a/cogs/vclogs.py b/cogs/vclogs.py index 40a618c..bc317a4 100644 --- a/cogs/vclogs.py +++ b/cogs/vclogs.py | |||
| @@ -10,6 +10,7 @@ import os | |||
| 10 | import discord | 10 | import discord |
| 11 | from discord.ext import commands | 11 | from discord.ext import commands |
| 12 | from gtts import gTTS | 12 | from gtts import gTTS |
| 13 | import aiofiles | ||
| 13 | 14 | ||
| 14 | logger = logging.getLogger(__name__) | 15 | logger = logging.getLogger(__name__) |
| 15 | 16 | ||
| @@ -20,15 +21,14 @@ class VcLogs(commands.Cog): | |||
| 20 | self.bot = bot | 21 | self.bot = bot |
| 21 | self.settings_check() | 22 | self.settings_check() |
| 22 | 23 | ||
| 24 | settings_file = 'settings.json' | ||
| 25 | |||
| 23 | @staticmethod | 26 | @staticmethod |
| 24 | def settings_check(): | 27 | def settings_check(): |
| 25 | if not os.path.exists('settings.json'): | 28 | if not os.path.exists(settings_file): |
| 26 | with open('settings.json', 'w') as f: | 29 | with open(settings_file, 'w') as f: |
| 27 | json.dump({'volume': '5'}, f, indent=4) | 30 | json.dump({'volume': '5'}, f, indent=4) |
| 28 | logger.info("settings.json created") | 31 | logger.info("settings.json created") |
| 29 | with open('settings.json', 'r') as f: | ||
| 30 | settings = json.load(f) | ||
| 31 | volume = settings.get('volume', '5') | ||
| 32 | 32 | ||
| 33 | @staticmethod | 33 | @staticmethod |
| 34 | async def handle_state_change(member, channel, state_type, is_enabled, current_time, log_channel): | 34 | async def handle_state_change(member, channel, state_type, is_enabled, current_time, log_channel): |
| @@ -62,12 +62,13 @@ class VcLogs(commands.Cog): | |||
| 62 | return tts_text, channel | 62 | return tts_text, channel |
| 63 | 63 | ||
| 64 | async def play_tts(self, channel, text): | 64 | async def play_tts(self, channel, text): |
| 65 | tts_file = "temp_tts.mp3" | ||
| 65 | # Generate TTS audio | 66 | # Generate TTS audio |
| 66 | tts = gTTS(text=text, lang='pl') | 67 | tts = gTTS(text=text, lang='pl') |
| 67 | tts.save('temp_tts.mp3') | 68 | tts.save(tts_file) |
| 68 | 69 | ||
| 69 | if os.path.exists('settings.json'): | 70 | if os.path.exists(settings_file): |
| 70 | with open('settings.json') as f: | 71 | async with aiofiles.open(settings_file) as f: |
| 71 | data = json.load(f) | 72 | data = json.load(f) |
| 72 | volume = data['volume'] | 73 | volume = data['volume'] |
| 73 | else: | 74 | else: |
| @@ -82,23 +83,23 @@ class VcLogs(commands.Cog): | |||
| 82 | 83 | ||
| 83 | # Play the TTS audio | 84 | # Play the TTS audio |
| 84 | if not voice_client.is_playing(): | 85 | if not voice_client.is_playing(): |
| 85 | audio_source = discord.FFmpegPCMAudio('temp_tts.mp3', options=f'-filter:a "volume={volume}"') | 86 | audio_source = discord.FFmpegPCMAudio(tts_file, options=f'-filter:a "volume={volume}"') |
| 86 | voice_client.play(audio_source) | 87 | voice_client.play(audio_source) |
| 87 | while voice_client.is_playing(): | 88 | while voice_client.is_playing(): |
| 88 | await discord.utils.sleep_until(datetime.datetime.now() + datetime.timedelta(seconds=1)) | 89 | await discord.utils.sleep_until(datetime.datetime.now() + datetime.timedelta(seconds=1)) |
| 89 | if os.path.exists('temp_tts.mp3'): | 90 | if os.path.exists(tts_file): |
| 90 | os.remove('temp_tts.mp3') | 91 | os.remove(tts_file) |
| 91 | 92 | ||
| 92 | @commands.slash_command( | 93 | @commands.slash_command( |
| 93 | name="log_volume" | 94 | name="log_volume" |
| 94 | ) | 95 | ) |
| 95 | async def volume(self, ctx: discord.ApplicationContext, volume: int): | 96 | async def volume(self, ctx: discord.ApplicationContext, volume: int): |
| 96 | try: | 97 | try: |
| 97 | if not os.path.exists('settings.json'): | 98 | if not os.path.exists(settings_file): |
| 98 | with open('settings.json', 'w') as f: | 99 | async with aiofiles.open(settings_file, 'w') as f: |
| 99 | json.dump({'volume': '5'}, f, indent=4) | 100 | json.dump({'volume': '5'}, f, indent=4) |
| 100 | logger.info("settings.json created") | 101 | logger.info("settings.json created") |
| 101 | with open('settings.json', 'w') as f: | 102 | async with aiofiles.open(settings_file, 'w') as f: |
| 102 | json.dump({"volume": volume}, f, indent=4) | 103 | json.dump({"volume": volume}, f, indent=4) |
| 103 | await ctx.followup.send(f"Volume has been set to: {volume}") | 104 | await ctx.followup.send(f"Volume has been set to: {volume}") |
| 104 | logger.info(f"Volume changed to: {volume}") | 105 | logger.info(f"Volume changed to: {volume}") |
diff --git a/cogs/voice.py b/cogs/voice.py index 261af49..dd5b74a 100644 --- a/cogs/voice.py +++ b/cogs/voice.py | |||
| @@ -14,43 +14,69 @@ class Voice(commands.Cog): | |||
| 14 | def __init__(self, bot): | 14 | def __init__(self, bot): |
| 15 | self.bot = bot | 15 | self.bot = bot |
| 16 | 16 | ||
| 17 | async def ensure_voice_client(self, ctx): | 17 | async def _validate_user_voice_state(self, ctx): |
| 18 | """Ensure we have a valid voice client connection.""" | 18 | """Validate that user is in a voice channel.""" |
| 19 | if not ctx.author.voice or not ctx.author.voice.channel: | 19 | if not ctx.author.voice or not ctx.author.voice.channel: |
| 20 | await ctx.send("You need to be in a voice channel first!") | 20 | await ctx.send("You need to be in a voice channel first!") |
| 21 | return False | ||
| 22 | return True | ||
| 23 | |||
| 24 | async def _create_voice_client(self, ctx): | ||
| 25 | """Create a new voice client connection.""" | ||
| 26 | try: | ||
| 27 | voice_client = await ctx.author.voice.channel.connect(timeout=30.0, reconnect=True) | ||
| 28 | logger.info(f"Connected to voice channel: {ctx.author.voice.channel}") | ||
| 29 | return voice_client | ||
| 30 | except asyncio.TimeoutError: | ||
| 31 | await ctx.send("Connection to voice channel timed out.") | ||
| 32 | return None | ||
| 33 | except discord.ClientException as e: | ||
| 34 | logger.error(f"Failed to connect to voice channel: {e}") | ||
| 35 | await ctx.send("Failed to connect to voice channel. Please try again.", ephemeral=True) | ||
| 36 | return None | ||
| 37 | |||
| 38 | async def _move_voice_client(self, voice_client, ctx): | ||
| 39 | """Move existing voice client to user's channel.""" | ||
| 40 | try: | ||
| 41 | await voice_client.move_to(ctx.author.voice.channel) | ||
| 42 | logger.info(f"Moved to voice channel: {ctx.author.voice.channel}") | ||
| 43 | return voice_client | ||
| 44 | except asyncio.TimeoutError: | ||
| 45 | await ctx.send("Connection to voice channel timed out.") | ||
| 46 | return None | ||
| 47 | except discord.ClientException as e: | ||
| 48 | logger.error(f"Failed to move to voice channel: {e}") | ||
| 49 | await ctx.send("Failed to move to your voice channel. Please try again.", ephemeral=True) | ||
| 50 | return None | ||
| 51 | |||
| 52 | async def _verify_connection(self, voice_client): | ||
| 53 | """Verify and repair voice client connection if needed.""" | ||
| 54 | if not voice_client.is_connected(): | ||
| 55 | try: | ||
| 56 | await voice_client.connect(reconnect=True, timeout=10.0) | ||
| 57 | return voice_client | ||
| 58 | except Exception as e: | ||
| 59 | logger.error(f"Failed to reconnect to voice: {e}") | ||
| 60 | return None | ||
| 61 | return voice_client | ||
| 62 | |||
| 63 | async def ensure_voice_client(self, ctx): | ||
| 64 | """Ensure we have a valid voice client connection.""" | ||
| 65 | if not await self._validate_user_voice_state(ctx): | ||
| 21 | return None | 66 | return None |
| 22 | 67 | ||
| 23 | voice_client = discord.utils.get(self.bot.voice_clients, guild=ctx.guild) | 68 | voice_client = discord.utils.get(self.bot.voice_clients, guild=ctx.guild) |
| 24 | 69 | ||
| 25 | # If no voice client exists, create one | 70 | # If no voice client exists, create one |
| 26 | if not voice_client or not voice_client.is_connected(): | 71 | if not voice_client or not voice_client.is_connected(): |
| 27 | try: | 72 | return await self._create_voice_client(ctx) |
| 28 | voice_client = await ctx.author.voice.channel.connect(timeout=30.0, reconnect=True) | 73 | |
| 29 | logger.info(f"Connected to voice channel: {ctx.author.voice.channel}") | ||
| 30 | except asyncio.TimeoutError: | ||
| 31 | await ctx.send("Connection to voice channel timed out.") | ||
| 32 | except discord.ClientException as e: | ||
| 33 | logger.error(f"Failed to connect to voice channel: {e}") | ||
| 34 | await ctx.send("Failed to connect to voice channel. Please try again.", ephemeral=True) | ||
| 35 | # If we're in a different channel, move to the user's channel | 74 | # If we're in a different channel, move to the user's channel |
| 36 | elif voice_client.channel != ctx.author.voice.channel: | 75 | if voice_client.channel != ctx.author.voice.channel: |
| 37 | try: | 76 | return await self._move_voice_client(voice_client, ctx) |
| 38 | await voice_client.move_to(ctx.author.voice.channel) | ||
| 39 | logger.info(f"Moved to voice channel: {ctx.author.voice.channel}") | ||
| 40 | except asyncio.TimeoutError: | ||
| 41 | await ctx.send("Connection to voice channel timed out.") | ||
| 42 | except discord.ClientException as e: | ||
| 43 | logger.error(f"Failed to move to voice channel: {e}") | ||
| 44 | await ctx.send("Failed to move to your voice channel. Please try again.", ephemeral=True) | ||
| 45 | 77 | ||
| 46 | # Verify the connection is healthy | 78 | # Verify the connection is healthy |
| 47 | if not voice_client.is_connected(): | 79 | return await self._verify_connection(voice_client) |
| 48 | try: | ||
| 49 | await voice_client.connect(reconnect=True, timeout=10.0) | ||
| 50 | except Exception as e: | ||
| 51 | logger.error(f"Failed to reconnect to voice: {e}") | ||
| 52 | await ctx.send("Failed to establish voice connection. Please try again.") | ||
| 53 | return None | ||
| 54 | 80 | ||
| 55 | async def connect(self, ctx): | 81 | async def connect(self, ctx): |
| 56 | """Connect to the user's voice channel with improved connection handling.""" | 82 | """Connect to the user's voice channel with improved connection handling.""" |
diff --git a/cogs/web.py b/cogs/web.py index a9f8fbb..fcec8f9 100644 --- a/cogs/web.py +++ b/cogs/web.py | |||
| @@ -18,6 +18,49 @@ class Web(commands.Cog): | |||
| 18 | def __init__(self, bot): | 18 | def __init__(self, bot): |
| 19 | self.bot = bot | 19 | self.bot = bot |
| 20 | 20 | ||
| 21 | def _extract_youtube_title(self, url): | ||
| 22 | """Extract YouTube video title if URL is a YouTube link.""" | ||
| 23 | if not url.startswith('https://www.youtube.com/'): | ||
| 24 | return None | ||
| 25 | |||
| 26 | logger.info("youtube link") | ||
| 27 | try: | ||
| 28 | parsed_url = urllib.parse.urlparse(url) | ||
| 29 | video_id = urllib.parse.parse_qs(parsed_url.query).get('v', [None])[0] | ||
| 30 | if video_id: | ||
| 31 | yt = pytube.YouTube(f'https://www.youtube.com/watch?v={video_id}') | ||
| 32 | return yt.title | ||
| 33 | except Exception: | ||
| 34 | pass | ||
| 35 | return None | ||
| 36 | |||
| 37 | def _create_button(self, index, link): | ||
| 38 | """Create a button for sending search result.""" | ||
| 39 | button = discord.ui.Button(label=f"Send Result {index}") | ||
| 40 | |||
| 41 | async def button_callback(interaction: discord.Interaction): | ||
| 42 | await interaction.response.send_message(link.get('href')) | ||
| 43 | |||
| 44 | button.callback = button_callback | ||
| 45 | return button | ||
| 46 | |||
| 47 | def _process_search_results(self, links, query): | ||
| 48 | """Process search results and create embed with buttons.""" | ||
| 49 | embed = Embed(title="Search Results", description=f"Top results for: {query}", color=0x1E90FF) | ||
| 50 | view = discord.ui.View(disable_on_timeout=True) | ||
| 51 | |||
| 52 | for i, link in enumerate(links[:5], start=1): | ||
| 53 | title = link.text | ||
| 54 | youtube_title = self._extract_youtube_title(link.get('href')) | ||
| 55 | if youtube_title: | ||
| 56 | title = youtube_title | ||
| 57 | |||
| 58 | embed.add_field(name=f"Result {i}", value=f"[{title}]({link.get('href')})", inline=False) | ||
| 59 | button = self._create_button(i, link) | ||
| 60 | view.add_item(button) | ||
| 61 | |||
| 62 | return embed, view | ||
| 63 | |||
| 21 | @commands.slash_command( | 64 | @commands.slash_command( |
| 22 | name="web", | 65 | name="web", |
| 23 | description="Search the web using DuckDuckGo" | 66 | description="Search the web using DuckDuckGo" |
| @@ -35,40 +78,22 @@ class Web(commands.Cog): | |||
| 35 | 78 | ||
| 36 | logger.info(f"Query: {query}") | 79 | logger.info(f"Query: {query}") |
| 37 | 80 | ||
| 38 | if response.status_code == 200: | 81 | if response.status_code != 200: |
| 39 | soup = BeautifulSoup(response.text, 'html.parser') | ||
| 40 | links = soup.find_all('a', class_='result__a') | ||
| 41 | if links: | ||
| 42 | embed = Embed(title="Search Results", description=f"Top results for: {query}", color=0x1E90FF) | ||
| 43 | view = discord.ui.View(disable_on_timeout=True) | ||
| 44 | for i, link in enumerate(links[:5], start=1): | ||
| 45 | title = link.text | ||
| 46 | if link.get('href').startswith('https://www.youtube.com/'): | ||
| 47 | logger.info("youtube link") | ||
| 48 | try: | ||
| 49 | parsed_url = urllib.parse.urlparse(link.get('href')) | ||
| 50 | video_id = urllib.parse.parse_qs(parsed_url.query).get('v', [None])[0] | ||
| 51 | if video_id: | ||
| 52 | yt = pytube.YouTube(f'https://www.youtube.com/watch?v={video_id}') | ||
| 53 | title = yt.title | ||
| 54 | except Exception: | ||
| 55 | pass | ||
| 56 | |||
| 57 | embed.add_field(name=f"Result {i}", value=f"[{title}]({link.get('href')})", inline=False) | ||
| 58 | button = discord.ui.Button(label=f"Send Result {i}") | ||
| 59 | |||
| 60 | async def button_callback(interaction: discord.Interaction, link=link): | ||
| 61 | await interaction.response.send_message(link.get('href')) | ||
| 62 | button.callback = button_callback | ||
| 63 | view.add_item(button) | ||
| 64 | await ctx.respond(embed=embed, view=view, ephemeral=True) | ||
| 65 | logger.info("Search results sent") | ||
| 66 | else: | ||
| 67 | await ctx.respond(content="No results found.", ephemeral=True) | ||
| 68 | logger.warning("No results found") | ||
| 69 | else: | ||
| 70 | await ctx.respond(content=f"Error: {response.status_code}", ephemeral=True) | 82 | await ctx.respond(content=f"Error: {response.status_code}", ephemeral=True) |
| 71 | logger.error(f"Error: {response.status_code}") | 83 | logger.error(f"Error: {response.status_code}") |
| 84 | return | ||
| 85 | |||
| 86 | soup = BeautifulSoup(response.text, 'html.parser') | ||
| 87 | links = soup.find_all('a', class_='result__a') | ||
| 88 | |||
| 89 | if not links: | ||
| 90 | await ctx.respond(content="No results found.", ephemeral=True) | ||
| 91 | logger.warning("No results found") | ||
| 92 | return | ||
| 93 | |||
| 94 | embed, view = self._process_search_results(links, query) | ||
| 95 | await ctx.respond(embed=embed, view=view, ephemeral=True) | ||
| 96 | logger.info("Search results sent") | ||
| 72 | 97 | ||
| 73 | def setup(bot): | 98 | def setup(bot): |
| 74 | bot.add_cog(Web(bot)) | 99 | bot.add_cog(Web(bot)) |
| @@ -8,6 +8,7 @@ import os | |||
| 8 | import sys | 8 | import sys |
| 9 | import json | 9 | import json |
| 10 | import asyncio | 10 | import asyncio |
| 11 | import aiofiles | ||
| 11 | import discord | 12 | import discord |
| 12 | from discord.ext import commands | 13 | from discord.ext import commands |
| 13 | from dotenv import load_dotenv | 14 | from dotenv import load_dotenv |
| @@ -28,27 +29,29 @@ asyncio.set_event_loop(loop) | |||
| 28 | 29 | ||
| 29 | bot = commands.Bot(intents=discord.Intents.all(), loop=loop) | 30 | bot = commands.Bot(intents=discord.Intents.all(), loop=loop) |
| 30 | 31 | ||
| 32 | settings = "settings.json" | ||
| 33 | |||
| 31 | @bot.event | 34 | @bot.event |
| 32 | async def on_ready(): | 35 | async def on_ready(): |
| 33 | logger.info("ŁUKASZ SPERMA AKTYWACJA") | 36 | logger.info("ŁUKASZ SPERMA AKTYWACJA") |
| 34 | # send the message to channel ID | 37 | # send the message to channel ID |
| 35 | channel = bot.get_channel(1081667823799111804) | 38 | channel = bot.get_channel(1081667823799111804) |
| 36 | await channel.send("ŁUKASZ SPERMA AKTYWACJA") | 39 | await channel.send("ŁUKASZ SPERMA AKTYWACJA") |
| 37 | if not os.path.exists('settings.json'): | 40 | if not os.path.exists(settings): |
| 38 | with open('settings.json', 'w') as f: | 41 | async with aiofiles.open(settings, 'w') as f: |
| 39 | json.dump({ | 42 | json.dump({ |
| 40 | 'volume': 1.0, | 43 | 'volume': 1.0, |
| 41 | 'audio_paths': ['audio', 'music', 'sounds'] | 44 | 'audio_paths': ['audio', 'music', 'sounds'] |
| 42 | }, f) | 45 | }, f) |
| 43 | if os.path.exists('settings.json'): | 46 | if os.path.exists(settings): |
| 44 | try: | 47 | try: |
| 45 | with open('settings.json') as f: | 48 | async with aiofiles.open(settings) as f: |
| 46 | data = json.load(f) | 49 | data = json.load(f) |
| 47 | assert 'volume' in data and isinstance(data['volume'], float) | 50 | assert 'volume' in data and isinstance(data['volume'], float) |
| 48 | assert 'audio_paths' in data and isinstance(data['audio_paths'], list) | 51 | assert 'audio_paths' in data and isinstance(data['audio_paths'], list) |
| 49 | except (json.JSONDecodeError, AssertionError) as error: | 52 | except (json.JSONDecodeError, AssertionError) as error: |
| 50 | logger.error(f"Error loading settings.json: {error}") | 53 | logger.error(f"Error loading settings.json: {error}") |
| 51 | with open('settings.json', 'w') as f: | 54 | async with aiofiles.open(settings, 'w') as f: |
| 52 | json.dump({ | 55 | json.dump({ |
| 53 | 'volume': 1.0, | 56 | 'volume': 1.0, |
| 54 | 'audio_paths': ['audio', 'music', 'sounds'] | 57 | 'audio_paths': ['audio', 'music', 'sounds'] |
| @@ -69,7 +72,7 @@ if __name__ == "__main__": | |||
| 69 | 'cogs.downloader', | 72 | 'cogs.downloader', |
| 70 | 'cogs.say', | 73 | 'cogs.say', |
| 71 | 'cogs.jarvis', | 74 | 'cogs.jarvis', |
| 72 | 'cogs.music.play', | 75 | 'cogs.music.play_commands', |
| 73 | 'cogs.music.stop', | 76 | 'cogs.music.stop', |
| 74 | 'cogs.music.join', | 77 | 'cogs.music.join', |
| 75 | ] | 78 | ] |
diff --git a/requirements.txt b/requirements.txt index 74013da..8000108 100644 --- a/requirements.txt +++ b/requirements.txt | |||
| @@ -21,7 +21,7 @@ idna==3.11 | |||
| 21 | multidict==6.7.1 | 21 | multidict==6.7.1 |
| 22 | mutagen==1.47.0 | 22 | mutagen==1.47.0 |
| 23 | propcache==0.4.1 | 23 | propcache==0.4.1 |
| 24 | py-cord==2.7.0 | 24 | py-cord==2.6.1 |
| 25 | pycparser==3.0 | 25 | pycparser==3.0 |
| 26 | pycryptodomex==3.23.0 | 26 | pycryptodomex==3.23.0 |
| 27 | PyNaCl==1.6.2 | 27 | PyNaCl==1.6.2 |
