diff options
| author | salmonik <raisersoft@proton.me> | 2026-04-29 14:22:03 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-04-29 14:22:03 +0200 |
| commit | dba3388567818cb5c60be9d406d3e1cccc84b3ea (patch) | |
| tree | 53a0903f7a4c381c7ad12e3e632ef8dd8ebba39f /cogs | |
| parent | multitude of changes. (diff) | |
| download | senator-sperma-dba3388567818cb5c60be9d406d3e1cccc84b3ea.tar.gz senator-sperma-dba3388567818cb5c60be9d406d3e1cccc84b3ea.zip | |
Refactor database connections and add integration tests
- Use context managers for sqlite3 connections in cron, vclogs, and database utils
- Add URL helper utility for yt-dlp with clean_url_for_ytdlp function
- Add slow integration test markers and new integration test files
- Fix test imports to use conftest directly instead of tests.conftest
- Improve downloader error handling and use COPYPARTY_URL_UPLOAD env var
- Update pytest.ini with slow test markers and warning filters
- Document slow test usage in AGENTS.md
Diffstat (limited to 'cogs')
| -rw-r--r-- | cogs/cron.py | 11 | ||||
| -rw-r--r-- | cogs/downloader.py | 14 | ||||
| -rw-r--r-- | cogs/music/media_processing.py | 2 | ||||
| -rw-r--r-- | cogs/music/play_commands.py | 2 | ||||
| -rw-r--r-- | cogs/utils/database.py | 17 | ||||
| -rw-r--r-- | cogs/utils/url_helper.py | 21 | ||||
| -rw-r--r-- | cogs/vclogs.py | 55 | ||||
| -rw-r--r-- | cogs/web.py | 2 |
8 files changed, 84 insertions, 40 deletions
diff --git a/cogs/cron.py b/cogs/cron.py index b4f494a..aad43d2 100644 --- a/cogs/cron.py +++ b/cogs/cron.py | |||
| @@ -30,13 +30,10 @@ class Cron(commands.Cog): | |||
| 30 | 30 | ||
| 31 | def load_tasks(self): | 31 | def load_tasks(self): |
| 32 | try: | 32 | try: |
| 33 | conn = get_connection() | 33 | with get_connection() as conn: |
| 34 | try: | ||
| 35 | c = conn.cursor() | 34 | c = conn.cursor() |
| 36 | c.execute("SELECT id, cron_expression, type, channel_id, active, audio_filename, message FROM cron_jobs") | 35 | c.execute("SELECT id, cron_expression, type, channel_id, active, audio_filename, message FROM cron_jobs") |
| 37 | rows = c.fetchall() | 36 | rows = c.fetchall() |
| 38 | finally: | ||
| 39 | conn.close() | ||
| 40 | 37 | ||
| 41 | self.scheduled_tasks = [ | 38 | self.scheduled_tasks = [ |
| 42 | { | 39 | { |
| @@ -57,8 +54,7 @@ class Cron(commands.Cog): | |||
| 57 | 54 | ||
| 58 | def save_tasks(self): | 55 | def save_tasks(self): |
| 59 | try: | 56 | try: |
| 60 | conn = get_connection() | 57 | with get_connection() as conn: |
| 61 | try: | ||
| 62 | c = conn.cursor() | 58 | c = conn.cursor() |
| 63 | c.execute("PRAGMA foreign_keys = ON") | 59 | c.execute("PRAGMA foreign_keys = ON") |
| 64 | 60 | ||
| @@ -78,10 +74,7 @@ class Cron(commands.Cog): | |||
| 78 | task.get("message"), | 74 | task.get("message"), |
| 79 | ), | 75 | ), |
| 80 | ) | 76 | ) |
| 81 | |||
| 82 | conn.commit() | 77 | conn.commit() |
| 83 | finally: | ||
| 84 | conn.close() | ||
| 85 | except Exception as e: | 78 | except Exception as e: |
| 86 | logger.error(f"Error saving cron jobs: {e}") | 79 | logger.error(f"Error saving cron jobs: {e}") |
| 87 | 80 | ||
diff --git a/cogs/downloader.py b/cogs/downloader.py index d829ed7..900bafb 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 | import aiofiles | 14 | import aiofiles |
| 15 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 15 | 16 | ||
| 16 | logger = logging.getLogger(__name__) | 17 | logger = logging.getLogger(__name__) |
| 17 | 18 | ||
| @@ -61,6 +62,10 @@ class Downloader(commands.Cog): | |||
| 61 | file_data = response["content"] | 62 | file_data = response["content"] |
| 62 | await ctx.respond(file=discord.File(file_data)) | 63 | await ctx.respond(file=discord.File(file_data)) |
| 63 | else: | 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) | ||
| 64 | logger.error(f"Error: {response['content']}") | 69 | logger.error(f"Error: {response['content']}") |
| 65 | if 'details' in response: | 70 | if 'details' in response: |
| 66 | logger.error(f"Details: {response['details']}") | 71 | logger.error(f"Details: {response['details']}") |
| @@ -68,6 +73,7 @@ class Downloader(commands.Cog): | |||
| 68 | @staticmethod | 73 | @staticmethod |
| 69 | async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str | None = None): | 74 | async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str | None = None): |
| 70 | try: | 75 | try: |
| 76 | url = clean_url_for_ytdlp(url) | ||
| 71 | logger.info(f"Downloading {url} as {fformat}") | 77 | logger.info(f"Downloading {url} as {fformat}") |
| 72 | 78 | ||
| 73 | with tempfile.TemporaryDirectory(delete=False) as temp_dir: | 79 | with tempfile.TemporaryDirectory(delete=False) as temp_dir: |
| @@ -157,12 +163,18 @@ class Downloader(commands.Cog): | |||
| 157 | files = {'f': (filename, content, 'application/octet-stream')} | 163 | files = {'f': (filename, content, 'application/octet-stream')} |
| 158 | headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'} | 164 | headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'} |
| 159 | params = {'j': '1', 'ck': 'b2'} | 165 | params = {'j': '1', 'ck': 'b2'} |
| 160 | base_url = "https://catbox.2137697.xyz/sperma/" | 166 | base_url = os.getenv("COPYPARTY_URL_UPLOAD") |
| 161 | 167 | ||
| 162 | response = requests.post(base_url, files=files, headers=headers, params=params) | 168 | response = requests.post(base_url, files=files, headers=headers, params=params) |
| 163 | 169 | ||
| 164 | if response.status_code in [200, 201]: | 170 | if response.status_code in [200, 201]: |
| 165 | return Downloader._handle_upload_success(response, filename, temp_dir) | 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 | ) | ||
| 166 | else: | 178 | else: |
| 167 | return Downloader._create_error_response( | 179 | return Downloader._create_error_response( |
| 168 | f"Server returned {response.status_code}", | 180 | f"Server returned {response.status_code}", |
diff --git a/cogs/music/media_processing.py b/cogs/music/media_processing.py index b9b814c..91bd406 100644 --- a/cogs/music/media_processing.py +++ b/cogs/music/media_processing.py | |||
| @@ -9,6 +9,7 @@ import os | |||
| 9 | 9 | ||
| 10 | import yt_dlp | 10 | import yt_dlp |
| 11 | from youtubesearchpython import VideosSearch | 11 | from youtubesearchpython import VideosSearch |
| 12 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 12 | 13 | ||
| 13 | logger = logging.getLogger(__name__) | 14 | logger = logging.getLogger(__name__) |
| 14 | 15 | ||
| @@ -36,6 +37,7 @@ class MediaDownloader: | |||
| 36 | } | 37 | } |
| 37 | 38 | ||
| 38 | try: | 39 | try: |
| 40 | url = clean_url_for_ytdlp(url) | ||
| 39 | with yt_dlp.YoutubeDL(ytdl_opts) as ydl: | 41 | with yt_dlp.YoutubeDL(ytdl_opts) as ydl: |
| 40 | info_dict = ydl.extract_info(url, download=True) | 42 | info_dict = ydl.extract_info(url, download=True) |
| 41 | # Get the base filename without extension | 43 | # Get the base filename without extension |
diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py index c53dbc7..bdb021d 100644 --- a/cogs/music/play_commands.py +++ b/cogs/music/play_commands.py | |||
| @@ -19,6 +19,7 @@ from discord.ext import commands | |||
| 19 | from cogs.music import audio_playback, media_processing | 19 | from cogs.music import audio_playback, media_processing |
| 20 | from cogs.utils.file_search import UniversalSearcher | 20 | from cogs.utils.file_search import UniversalSearcher |
| 21 | from cogs.utils.settings import get_uploadurl | 21 | from cogs.utils.settings import get_uploadurl |
| 22 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 22 | 23 | ||
| 23 | logger = logging.getLogger(__name__) | 24 | logger = logging.getLogger(__name__) |
| 24 | 25 | ||
| @@ -193,6 +194,7 @@ class PlayCommands(commands.Cog): | |||
| 193 | } | 194 | } |
| 194 | 195 | ||
| 195 | loop = asyncio.get_event_loop() | 196 | loop = asyncio.get_event_loop() |
| 197 | url = clean_url_for_ytdlp(url) | ||
| 196 | with YoutubeDL(ytdl_opts) as ydl: | 198 | with YoutubeDL(ytdl_opts) as ydl: |
| 197 | info_dict = await loop.run_in_executor( | 199 | info_dict = await loop.run_in_executor( |
| 198 | None, lambda: ydl.extract_info(url, download=True) | 200 | None, lambda: ydl.extract_info(url, download=True) |
diff --git a/cogs/utils/database.py b/cogs/utils/database.py index 67cbc9f..ea7d356 100644 --- a/cogs/utils/database.py +++ b/cogs/utils/database.py | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | import os | 1 | import os |
| 2 | import sqlite3 | 2 | import sqlite3 |
| 3 | import logging | 3 | import logging |
| 4 | from contextlib import contextmanager | ||
| 4 | 5 | ||
| 5 | logger = logging.getLogger(__name__) | 6 | logger = logging.getLogger(__name__) |
| 6 | 7 | ||
| @@ -10,7 +11,8 @@ _initialized = False | |||
| 10 | 11 | ||
| 11 | 12 | ||
| 12 | def get_connection(): | 13 | def get_connection(): |
| 13 | return sqlite3.connect(get_db_path()) | 14 | conn = sqlite3.connect(get_db_path()) |
| 15 | return _ConnectionContextManager(conn) | ||
| 14 | 16 | ||
| 15 | 17 | ||
| 16 | def get_db_path(): | 18 | def get_db_path(): |
| @@ -22,6 +24,19 @@ def set_db_path(path): | |||
| 22 | DB_PATH = path | 24 | DB_PATH = path |
| 23 | 25 | ||
| 24 | 26 | ||
| 27 | class _ConnectionContextManager: | ||
| 28 | """Context manager for sqlite3 connections that auto-closes.""" | ||
| 29 | def __init__(self, conn): | ||
| 30 | self.conn = conn | ||
| 31 | |||
| 32 | def __enter__(self): | ||
| 33 | return self.conn | ||
| 34 | |||
| 35 | def __exit__(self, exc_type, exc_val, exc_tb): | ||
| 36 | self.conn.close() | ||
| 37 | return False | ||
| 38 | |||
| 39 | |||
| 25 | def init_database(): | 40 | def init_database(): |
| 26 | global _initialized | 41 | global _initialized |
| 27 | if _initialized: | 42 | if _initialized: |
diff --git a/cogs/utils/url_helper.py b/cogs/utils/url_helper.py new file mode 100644 index 0000000..eac06f1 --- /dev/null +++ b/cogs/utils/url_helper.py | |||
| @@ -0,0 +1,21 @@ | |||
| 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 | |||
| 7 | def clean_url_for_ytdlp(url: str) -> str: | ||
| 8 | """Clean URL by removing everything after the first '&'. | ||
| 9 | |||
| 10 | This prevents yt-dlp from interpreting additional query parameters | ||
| 11 | (like &list=...) as playlist URLs. | ||
| 12 | |||
| 13 | Example: | ||
| 14 | Input: https://www.youtube.com/watch?v=lPGipwoJiOM&list=RDlPGipwoJiOM& | ||
| 15 | Output: https://www.youtube.com/watch?v=lPGipwoJiOM | ||
| 16 | """ | ||
| 17 | # Find the first '&' and truncate | ||
| 18 | first_ampersand = url.find('&') | ||
| 19 | if first_ampersand != -1: | ||
| 20 | return url[:first_ampersand] | ||
| 21 | return url | ||
diff --git a/cogs/vclogs.py b/cogs/vclogs.py index 404ffe0..e5ca202 100644 --- a/cogs/vclogs.py +++ b/cogs/vclogs.py | |||
| @@ -23,11 +23,10 @@ init_database() | |||
| 23 | 23 | ||
| 24 | def load_activity_data(): | 24 | def load_activity_data(): |
| 25 | try: | 25 | try: |
| 26 | conn = get_connection() | 26 | with get_connection() as conn: |
| 27 | c = conn.cursor() | 27 | c = conn.cursor() |
| 28 | c.execute("SELECT user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name FROM voice_activity") | 28 | c.execute("SELECT user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name FROM voice_activity") |
| 29 | rows = c.fetchall() | 29 | rows = c.fetchall() |
| 30 | conn.close() | ||
| 31 | 30 | ||
| 32 | return { | 31 | return { |
| 33 | row[0]: { | 32 | row[0]: { |
| @@ -48,30 +47,28 @@ def load_activity_data(): | |||
| 48 | 47 | ||
| 49 | def save_activity_data(data): | 48 | def save_activity_data(data): |
| 50 | try: | 49 | try: |
| 51 | conn = get_connection() | 50 | with get_connection() as conn: |
| 52 | c = conn.cursor() | 51 | c = conn.cursor() |
| 53 | c.execute("PRAGMA foreign_keys = ON") | 52 | c.execute("PRAGMA foreign_keys = ON") |
| 54 | 53 | ||
| 55 | c.execute("DELETE FROM voice_activity") | 54 | c.execute("DELETE FROM voice_activity") |
| 56 | 55 | ||
| 57 | for user_id, user_data in data.items(): | 56 | for user_id, user_data in data.items(): |
| 58 | c.execute( | 57 | c.execute( |
| 59 | """INSERT INTO voice_activity (user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name) | 58 | """INSERT INTO voice_activity (user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name) |
| 60 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", | 59 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", |
| 61 | ( | 60 | ( |
| 62 | user_id, | 61 | user_id, |
| 63 | user_data.get("join_time"), | 62 | user_data.get("join_time"), |
| 64 | user_data.get("total_active", 0), | 63 | user_data.get("total_active", 0), |
| 65 | user_data.get("total_afk", 0), | 64 | user_data.get("total_afk", 0), |
| 66 | int(user_data.get("is_afk", False)), | 65 | int(user_data.get("is_afk", False)), |
| 67 | user_data.get("afk_start"), | 66 | user_data.get("afk_start"), |
| 68 | user_data.get("active_start"), | 67 | user_data.get("active_start"), |
| 69 | user_data.get("channel_name"), | 68 | user_data.get("channel_name"), |
| 70 | ), | 69 | ), |
| 71 | ) | 70 | ) |
| 72 | 71 | conn.commit() | |
| 73 | conn.commit() | ||
| 74 | conn.close() | ||
| 75 | logger.info(f"Saved voice activity for {len(data)} users") | 72 | logger.info(f"Saved voice activity for {len(data)} users") |
| 76 | except Exception as e: | 73 | except Exception as e: |
| 77 | logger.error(f"Error saving voice activity: {e}") | 74 | logger.error(f"Error saving voice activity: {e}") |
diff --git a/cogs/web.py b/cogs/web.py index eea13b1..fd22f9f 100644 --- a/cogs/web.py +++ b/cogs/web.py | |||
| @@ -11,6 +11,7 @@ from bs4 import BeautifulSoup | |||
| 11 | import urllib.parse | 11 | import urllib.parse |
| 12 | import yt_dlp | 12 | import yt_dlp |
| 13 | import logging | 13 | import logging |
| 14 | from cogs.utils.url_helper import clean_url_for_ytdlp | ||
| 14 | 15 | ||
| 15 | logger = logging.getLogger(__name__) | 16 | logger = logging.getLogger(__name__) |
| 16 | 17 | ||
| @@ -24,6 +25,7 @@ class Web(commands.Cog): | |||
| 24 | if "youtube.com/watch" not in url: | 25 | if "youtube.com/watch" not in url: |
| 25 | return None | 26 | return None |
| 26 | 27 | ||
| 28 | url = clean_url_for_ytdlp(url) | ||
| 27 | logger.info("youtube link") | 29 | logger.info("youtube link") |
| 28 | try: | 30 | try: |
| 29 | ydl_opts = {"quiet": True, "nowarnings": True} | 31 | ydl_opts = {"quiet": True, "nowarnings": True} |
