From 7c6249b120ed6867dd71f620298138fcab8d27c8 Mon Sep 17 00:00:00 2001 From: Top1055 <123alexfeetham@gmail.com> Date: Sat, 29 Nov 2025 18:28:28 +0000 Subject: [PATCH] removed all direct database access. readable async functions. docstrings. organisation --- cogs/music/queue.py | 505 ++++++++++++++++++-------------------------- 1 file changed, 202 insertions(+), 303 deletions(-) diff --git a/cogs/music/queue.py b/cogs/music/queue.py index 24b15e1..15da230 100644 --- a/cogs/music/queue.py +++ b/cogs/music/queue.py @@ -1,16 +1,14 @@ -from http import server -import sqlite3 -import random -import time +""" +Queue management for Groovy-Zilean music bot +Now using centralized database manager for cleaner code +""" import discord import asyncio +import time from .translate import search_song -import config - -# Get database path from centralized config -db_path = config.get_db_path() +from .db_manager import db # Base FFmpeg options (will be modified by effects) BASE_FFMPEG_OPTS = { @@ -105,342 +103,224 @@ def get_effect_options(effect_name): return effects.get(effect_name, effects['none']) -# Creates the tables if they don't exist +# =================================== +# Initialization +# =================================== + def initialize_tables(): - # Connect to the database - conn = sqlite3.connect(db_path) - cursor = conn.cursor() + """Initialize database tables""" + db.initialize_tables() - # Create servers table if it doesn't exist - cursor.execute('''CREATE TABLE IF NOT EXISTS servers ( - server_id TEXT PRIMARY KEY, - is_playing INTEGER DEFAULT 0, - song_name TEXT, - song_url TEXT, - song_thumbnail TEXT, - loop_mode TEXT DEFAULT 'off', - volume INTEGER DEFAULT 100, - effect TEXT DEFAULT 'none', - song_start_time REAL DEFAULT 0, - song_duration INTEGER DEFAULT 0 - );''') - # Set all to not playing - cursor.execute("UPDATE servers SET is_playing = 0;") +# =================================== +# Queue Management +# =================================== - # Add new columns if they don't exist (for existing databases) - # Migrations for existing databases - columns = [ - ("loop_mode", "TEXT DEFAULT 'off'"), - ("volume", "INTEGER DEFAULT 100"), - ("effect", "TEXT DEFAULT 'none'"), - ("song_start_time", "REAL DEFAULT 0"), - ("song_duration", "INTEGER DEFAULT 0"), - ("song_thumbnail", "TEXT DEFAULT ''"), - ("song_url", "TEXT DEFAULT ''") # NEW - ] +async def add_song(server_id, details, queued_by, position=None): + """ + Add a song to the queue - for col_name, col_type in columns: - try: - cursor.execute(f"ALTER TABLE servers ADD COLUMN {col_name} {col_type};") - except sqlite3.OperationalError: - pass - - cursor.execute('''CREATE TABLE IF NOT EXISTS songs ( - server_id TEXT NOT NULL, - song_link TEXT, - queued_by TEXT, - position INTEGER NOT NULL, - title TEXT, - thumbnail TEXT, - duration INTEGER, - PRIMARY KEY (position), - FOREIGN KEY (server_id) REFERENCES servers(server_id) - );''') - - cursor.execute("DELETE FROM songs;") - conn.commit() - conn.close() - -# Queue a song in the db -async def add_song(server_id, details, queued_by): - # Connect to db - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - await add_server(server_id, cursor, conn) - - max_order_num = await get_max(server_id, cursor) + 1 + Args: + server_id: Discord server ID + details: Dictionary with song info (url, title, thumbnail, duration) or string + queued_by: Username who queued the song + position: Optional position in queue (None = end of queue) + Returns: + Position in queue + """ if isinstance(details, str): - # Fallback for raw strings - cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""", - (server_id, "Not grabbed", queued_by, max_order_num, details, "Unknown", 0)) + # Fallback for raw strings (legacy support) + pos = db.add_song( + server_id=str(server_id), + song_link="Not grabbed", + queued_by=queued_by, + title=details, + thumbnail="Unknown", + duration=0, + position=position + ) else: - # Save exact duration and thumbnail from the start - cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""", - (server_id, details['url'], queued_by, max_order_num, details['title'], details['thumbnail'], details['duration'])) + # Standard dictionary format + pos = db.add_song( + server_id=str(server_id), + song_link=details['url'], + queued_by=queued_by, + title=details['title'], + thumbnail=details.get('thumbnail', ''), + duration=details.get('duration', 0), + position=position + ) - conn.commit() - conn.close() - - return max_order_num + return pos -# Pop song from server (respects loop mode) async def pop(server_id, ignore=False, skip_mode=False): """ Pop next song from queue - ignore: Skip the song without returning URL - skip_mode: True when called from skip command (affects loop song behavior) + + Args: + server_id: Discord server ID + ignore: Skip the song without returning URL + skip_mode: True when called from skip command (affects loop song behavior) + + Returns: + Song URL or None """ - # Connect to db - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # JUST INCASE! - await add_server(server_id, cursor, conn) - - # Fetch info: link(1), title(4), thumbnail(5), duration(6) - cursor.execute('''SELECT * FROM songs WHERE server_id = ? ORDER BY position LIMIT 1;''', (server_id,)) - result = cursor.fetchone() - conn.commit() - conn.close() + result = db.get_next_song(str(server_id)) if result is None: return None - elif ignore: - await mark_song_as_finished(server_id, result[3]) - return None - elif result[1] == "Not grabbed": - # Lazy load logic - song_list = await search_song(result[4]) - if not song_list: - return None - song = song_list[0] - await set_current_song(server_id, song['title'], song.get('thumbnail', ''), song.get('duration', 0)) + # result format: (server_id, song_link, queued_by, position, title, thumbnail, duration) + server_id_str, song_link, queued_by, position, title, thumbnail, duration = result + + if ignore: + db.remove_song(str(server_id), position) + return None + + # Handle lazy-loaded songs (not yet fetched from YouTube) + if song_link == "Not grabbed": + song_list = await search_song(title) + if not song_list: + db.remove_song(str(server_id), position) + return None + + song = song_list[0] + await set_current_song( + server_id, + song['title'], + song['url'], + song.get('thumbnail', ''), + song.get('duration', 0) + ) # Check loop mode before removing loop_mode = await get_loop_mode(server_id) - if loop_mode != 'song': # Only remove if not looping song - await mark_song_as_finished(server_id, result[3]) + if loop_mode != 'song': + db.remove_song(str(server_id), position) + return song['url'] - # Pre-grabbed logic (Standard) - # result[1] is url, result[5] is thumbnail, result[6] is duration - await set_current_song(server_id, result[4], result[1], result[5], result[6]) + # Standard pre-fetched song + await set_current_song(server_id, title, song_link, thumbnail, duration) # Check loop mode before removing loop_mode = await get_loop_mode(server_id) - if loop_mode != 'song': # Only remove if not looping song - await mark_song_as_finished(server_id, result[3]) + if loop_mode != 'song': + db.remove_song(str(server_id), position) - return result[1] + return song_link -# Add server to db if first time queuing -async def add_server(server_id, cursor, conn): - cursor.execute('SELECT COUNT(*) FROM servers WHERE server_id = ?', (server_id,)) - if cursor.fetchone()[0] == 0: - cursor.execute('''INSERT INTO servers (server_id, loop_mode, volume, effect, song_thumbnail, song_url) - VALUES (?, 'off', 100, 'none', '', '')''', (server_id,)) - conn.commit() - - -# set song as played and update indexes -async def mark_song_as_finished(server_id, order_num): - # Connect to the database - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Update the song as finished - cursor.execute('''DELETE FROM songs - WHERE server_id = ? AND position = ?''', - (server_id, order_num)) - - # Close connection - conn.commit() - conn.close() - - -# set the current playing song of the server -async def set_current_song(server_id, title, url, thumbnail="", duration=0): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - start_time = time.time() - - # Ensure duration is an integer - try: - duration = int(duration) - except: - duration = 0 - - cursor.execute(''' UPDATE servers - SET song_name = ?, song_url = ?, song_thumbnail = ?, song_start_time = ?, song_duration = ? - WHERE server_id = ?''', - (title, url, thumbnail, start_time, duration, server_id)) - - conn.commit() - conn.close() - -# Returns dictionary with title and thumbnail -async def get_current_song(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - cursor.execute(''' SELECT song_name, song_thumbnail, song_url FROM servers WHERE server_id = ? LIMIT 1;''', (server_id,)) - result = cursor.fetchone() - conn.commit() - conn.close() - - if result: - return {'title': result[0], 'thumbnail': result[1], 'url': result[2]} - return {'title': "Nothing", 'thumbnail': None, 'url': ''} - -async def get_current_progress(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - cursor.execute('''SELECT song_start_time, song_duration, is_playing FROM servers WHERE server_id = ? LIMIT 1;''', (server_id,)) - result = cursor.fetchone() - conn.close() # Close quickly - - if not result or result[2] == 0: - return 0, 0, 0.0 - - start_time, duration, _ = result - - if duration is None or duration == 0: - return 0, 0, 0.0 - - elapsed = int(time.time() - start_time) - elapsed = min(elapsed, duration) - percentage = (elapsed / duration) * 100 if duration > 0 else 0 - - return elapsed, duration, percentage - -async def get_max(server_id, cursor): - cursor.execute("SELECT MAX(position) FROM songs WHERE server_id = ?", (server_id,)) - result = cursor.fetchone() - return result[0] if result[0] is not None else -1 - -async def update_server(server_id, playing): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - val = 1 if playing else 0 - cursor.execute("UPDATE servers SET is_playing = ? WHERE server_id = ?", (val, server_id)) - conn.commit() - conn.close() - -async def is_server_playing(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("SELECT is_playing FROM servers WHERE server_id = ?", (server_id,)) - res = cursor.fetchone() - conn.close() - return True if res[0] == 1 else False - -async def clear(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - await update_server(server_id, False) - cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,)) - conn.commit() - conn.close() async def grab_songs(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("SELECT title, duration, queued_by FROM songs WHERE server_id = ? ORDER BY position LIMIT 10", (server_id,)) - songs = cursor.fetchall() - max_pos = await get_max(server_id, cursor) - conn.close() - return max_pos, songs + """ + Get current queue -# --- Effects/Loop/Shuffle/Volume (Simplified Paste) --- -async def get_loop_mode(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("SELECT loop_mode FROM servers WHERE server_id = ?", (server_id,)) - res = cursor.fetchone() - conn.close() - return res[0] if res else 'off' + Returns: + Tuple of (max_position, list_of_songs) + """ + return db.get_queue(str(server_id), limit=10) -async def set_loop_mode(server_id, mode): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("UPDATE servers SET loop_mode = ? WHERE server_id = ?", (mode, server_id)) - conn.commit() - conn.close() -async def get_volume(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("SELECT volume FROM servers WHERE server_id = ?", (server_id,)) - res = cursor.fetchone() - conn.close() - return res[0] if res else 100 +async def clear(server_id): + """Clear the queue for a server""" + db.clear_queue(str(server_id)) + await update_server(server_id, False) -async def set_volume(server_id, vol): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("UPDATE servers SET volume = ? WHERE server_id = ?", (vol, server_id)) - conn.commit() - conn.close() - return vol async def shuffle_queue(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("SELECT position, song_link, queued_by, title, thumbnail, duration FROM songs WHERE server_id = ? ORDER BY position", (server_id,)) - songs = cursor.fetchall() - if len(songs) <= 1: - conn.close() - return False - random.shuffle(songs) - cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,)) - for i, s in enumerate(songs): - cursor.execute("INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)", (server_id, s[1], s[2], i, s[3], s[4], s[5])) - conn.commit() - conn.close() - return True + """Shuffle the queue randomly""" + return db.shuffle_queue(str(server_id)) + + +# =================================== +# Server State Management +# =================================== + +async def update_server(server_id, playing): + """Update server playing status""" + db.set_server_playing(str(server_id), playing) + + +async def is_server_playing(server_id): + """Check if server is currently playing""" + return db.is_server_playing(str(server_id)) + + +async def set_current_song(server_id, title, url, thumbnail="", duration=0): + """Set the currently playing song""" + db.set_current_song( + str(server_id), + title, + url, + thumbnail, + duration, + time.time() # start_time + ) + + +async def get_current_song(server_id): + """Get current song info""" + return db.get_current_song(str(server_id)) + + +async def get_current_progress(server_id): + """Get playback progress (elapsed, duration, percentage)""" + return db.get_current_progress(str(server_id)) + + +# =================================== +# Settings Management +# =================================== + +async def get_loop_mode(server_id): + """Get loop mode: 'off', 'song', or 'queue'""" + return db.get_loop_mode(str(server_id)) + + +async def set_loop_mode(server_id, mode): + """Set loop mode: 'off', 'song', or 'queue'""" + db.set_loop_mode(str(server_id), mode) + + +async def get_volume(server_id): + """Get volume (0-200)""" + return db.get_volume(str(server_id)) + + +async def set_volume(server_id, vol): + """Set volume (0-200)""" + return db.set_volume(str(server_id), vol) + async def get_effect(server_id): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("SELECT effect FROM servers WHERE server_id = ?", (server_id,)) - res = cursor.fetchone() - conn.close() - return res[0] if res else 'none' + """Get current audio effect""" + return db.get_effect(str(server_id)) + async def set_effect(server_id, fx): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - await add_server(server_id, cursor, conn) - cursor.execute("UPDATE servers SET effect = ? WHERE server_id = ?", (fx, server_id)) - conn.commit() - conn.close() + """Set audio effect""" + db.set_effect(str(server_id), fx) + + +# =================================== +# Effect Metadata +# =================================== def list_all_effects(): + """List all available audio effects""" return [ 'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion', 'reverse', 'chipmunk', 'demonic', 'underwater', 'robot', '8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone' ] + def get_effect_emoji(effect_name): - # Short list of emoji mappings + """Get emoji for effect""" emojis = { - 'none': '✨', # Changed to generic Sparkles + 'none': '✨', 'bassboost': '💥', 'nightcore': '⚡', 'slowed': '🐢', @@ -461,7 +341,9 @@ def get_effect_emoji(effect_name): } return emojis.get(effect_name, '✨') + def get_effect_description(effect_name): + """Get description for effect""" descriptions = { 'none': 'Normal audio', 'bassboost': 'MAXIMUM BASS 🔊', @@ -484,43 +366,60 @@ def get_effect_description(effect_name): } return descriptions.get(effect_name, 'Unknown effect') + +# =================================== +# Playback +# =================================== + async def play(ctx): + """ + Main playback loop - plays songs from queue + """ server_id = ctx.guild.id voice_client = ctx.voice_client + if voice_client is None: await update_server(server_id, False) return + # Wait for current song to finish while voice_client.is_playing(): await asyncio.sleep(0.5) + # Get next song url = await pop(server_id) if url is None: await update_server(server_id, False) return try: - # Scale volume down to prevent earrape - # User sees 0-200%, but internally we scale by 0.25 - # So user's 100% = 0.25 actual volume (25%) - vol = await get_volume(server_id) / 100.0 * 0.25 + # Get volume and effect settings + vol = await get_volume(server_id) / 100.0 * 0.25 # Scale down by 0.25 fx = await get_effect(server_id) opts = get_effect_options(fx) + # Create audio source src = discord.FFmpegPCMAudio(url, **opts) src = discord.PCMVolumeTransformer(src, volume=vol) + # After callback - play next song def after(e): - if e: print(e) - if voice_client and not voice_client.is_connected(): return + if e: + print(f"Playback error: {e}") + if voice_client and not voice_client.is_connected(): + return + + # Schedule next song coro = play(ctx) fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop) - try: fut.result() - except: pass + try: + fut.result() + except Exception as ex: + print(f"Error in after callback: {ex}") voice_client.play(src, after=after) + except Exception as e: print(f"Play error: {e}") + # Try next song on error await play(ctx) - -