removed all direct database access. readable async functions. docstrings. organisation

This commit is contained in:
2025-11-29 18:28:28 +00:00
parent 09fa7988f1
commit 7c6249b120

View File

@@ -1,16 +1,14 @@
from http import server """
import sqlite3 Queue management for Groovy-Zilean music bot
import random Now using centralized database manager for cleaner code
import time """
import discord import discord
import asyncio import asyncio
import time
from .translate import search_song from .translate import search_song
import config from .db_manager import db
# Get database path from centralized config
db_path = config.get_db_path()
# Base FFmpeg options (will be modified by effects) # Base FFmpeg options (will be modified by effects)
BASE_FFMPEG_OPTS = { BASE_FFMPEG_OPTS = {
@@ -105,342 +103,224 @@ def get_effect_options(effect_name):
return effects.get(effect_name, effects['none']) return effects.get(effect_name, effects['none'])
# Creates the tables if they don't exist # ===================================
# Initialization
# ===================================
def initialize_tables(): def initialize_tables():
# Connect to the database """Initialize database tables"""
conn = sqlite3.connect(db_path) db.initialize_tables()
cursor = conn.cursor()
# 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) async def add_song(server_id, details, queued_by, position=None):
# Migrations for existing databases """
columns = [ Add a song to the queue
("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
]
for col_name, col_type in columns: Args:
try: server_id: Discord server ID
cursor.execute(f"ALTER TABLE servers ADD COLUMN {col_name} {col_type};") details: Dictionary with song info (url, title, thumbnail, duration) or string
except sqlite3.OperationalError: queued_by: Username who queued the song
pass position: Optional position in queue (None = end of queue)
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
Returns:
Position in queue
"""
if isinstance(details, str): if isinstance(details, str):
# Fallback for raw strings # Fallback for raw strings (legacy support)
cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""", pos = db.add_song(
(server_id, "Not grabbed", queued_by, max_order_num, details, "Unknown", 0)) server_id=str(server_id),
song_link="Not grabbed",
queued_by=queued_by,
title=details,
thumbnail="Unknown",
duration=0,
position=position
)
else: else:
# Save exact duration and thumbnail from the start # Standard dictionary format
cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""", pos = db.add_song(
(server_id, details['url'], queued_by, max_order_num, details['title'], details['thumbnail'], details['duration'])) 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() return pos
conn.close()
return max_order_num
# Pop song from server (respects loop mode)
async def pop(server_id, ignore=False, skip_mode=False): async def pop(server_id, ignore=False, skip_mode=False):
""" """
Pop next song from queue Pop next song from queue
Args:
server_id: Discord server ID
ignore: Skip the song without returning URL ignore: Skip the song without returning URL
skip_mode: True when called from skip command (affects loop song behavior) skip_mode: True when called from skip command (affects loop song behavior)
Returns:
Song URL or None
""" """
# Connect to db result = db.get_next_song(str(server_id))
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()
if result is None: if result is None:
return 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 # Check loop mode before removing
loop_mode = await get_loop_mode(server_id) loop_mode = await get_loop_mode(server_id)
if loop_mode != 'song': # Only remove if not looping song if loop_mode != 'song':
await mark_song_as_finished(server_id, result[3]) db.remove_song(str(server_id), position)
return song['url'] return song['url']
# Pre-grabbed logic (Standard) # Standard pre-fetched song
# result[1] is url, result[5] is thumbnail, result[6] is duration await set_current_song(server_id, title, song_link, thumbnail, duration)
await set_current_song(server_id, result[4], result[1], result[5], result[6])
# Check loop mode before removing # Check loop mode before removing
loop_mode = await get_loop_mode(server_id) loop_mode = await get_loop_mode(server_id)
if loop_mode != 'song': # Only remove if not looping song if loop_mode != 'song':
await mark_song_as_finished(server_id, result[3]) 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): async def grab_songs(server_id):
conn = sqlite3.connect(db_path) """
cursor = conn.cursor() Get current queue
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
# --- Effects/Loop/Shuffle/Volume (Simplified Paste) --- Returns:
async def get_loop_mode(server_id): Tuple of (max_position, list_of_songs)
conn = sqlite3.connect(db_path) """
cursor = conn.cursor() return db.get_queue(str(server_id), limit=10)
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'
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): async def clear(server_id):
conn = sqlite3.connect(db_path) """Clear the queue for a server"""
cursor = conn.cursor() db.clear_queue(str(server_id))
await add_server(server_id, cursor, conn) await update_server(server_id, False)
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 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): async def shuffle_queue(server_id):
conn = sqlite3.connect(db_path) """Shuffle the queue randomly"""
cursor = conn.cursor() return db.shuffle_queue(str(server_id))
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: # Server State Management
conn.close() # ===================================
return False
random.shuffle(songs) async def update_server(server_id, playing):
cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,)) """Update server playing status"""
for i, s in enumerate(songs): db.set_server_playing(str(server_id), playing)
cursor.execute("INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)", (server_id, s[1], s[2], i, s[3], s[4], s[5]))
conn.commit()
conn.close() async def is_server_playing(server_id):
return True """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): async def get_effect(server_id):
conn = sqlite3.connect(db_path) """Get current audio effect"""
cursor = conn.cursor() return db.get_effect(str(server_id))
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'
async def set_effect(server_id, fx): async def set_effect(server_id, fx):
conn = sqlite3.connect(db_path) """Set audio effect"""
cursor = conn.cursor() db.set_effect(str(server_id), fx)
await add_server(server_id, cursor, conn)
cursor.execute("UPDATE servers SET effect = ? WHERE server_id = ?", (fx, server_id))
conn.commit() # ===================================
conn.close() # Effect Metadata
# ===================================
def list_all_effects(): def list_all_effects():
"""List all available audio effects"""
return [ return [
'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion', 'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion',
'reverse', 'chipmunk', 'demonic', 'underwater', 'robot', 'reverse', 'chipmunk', 'demonic', 'underwater', 'robot',
'8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone' '8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone'
] ]
def get_effect_emoji(effect_name): def get_effect_emoji(effect_name):
# Short list of emoji mappings """Get emoji for effect"""
emojis = { emojis = {
'none': '', # Changed to generic Sparkles 'none': '',
'bassboost': '💥', 'bassboost': '💥',
'nightcore': '', 'nightcore': '',
'slowed': '🐢', 'slowed': '🐢',
@@ -461,7 +341,9 @@ def get_effect_emoji(effect_name):
} }
return emojis.get(effect_name, '') return emojis.get(effect_name, '')
def get_effect_description(effect_name): def get_effect_description(effect_name):
"""Get description for effect"""
descriptions = { descriptions = {
'none': 'Normal audio', 'none': 'Normal audio',
'bassboost': 'MAXIMUM BASS 🔊', 'bassboost': 'MAXIMUM BASS 🔊',
@@ -484,43 +366,60 @@ def get_effect_description(effect_name):
} }
return descriptions.get(effect_name, 'Unknown effect') return descriptions.get(effect_name, 'Unknown effect')
# ===================================
# Playback
# ===================================
async def play(ctx): async def play(ctx):
"""
Main playback loop - plays songs from queue
"""
server_id = ctx.guild.id server_id = ctx.guild.id
voice_client = ctx.voice_client voice_client = ctx.voice_client
if voice_client is None: if voice_client is None:
await update_server(server_id, False) await update_server(server_id, False)
return return
# Wait for current song to finish
while voice_client.is_playing(): while voice_client.is_playing():
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
# Get next song
url = await pop(server_id) url = await pop(server_id)
if url is None: if url is None:
await update_server(server_id, False) await update_server(server_id, False)
return return
try: try:
# Scale volume down to prevent earrape # Get volume and effect settings
# User sees 0-200%, but internally we scale by 0.25 vol = await get_volume(server_id) / 100.0 * 0.25 # Scale down by 0.25
# So user's 100% = 0.25 actual volume (25%)
vol = await get_volume(server_id) / 100.0 * 0.25
fx = await get_effect(server_id) fx = await get_effect(server_id)
opts = get_effect_options(fx) opts = get_effect_options(fx)
# Create audio source
src = discord.FFmpegPCMAudio(url, **opts) src = discord.FFmpegPCMAudio(url, **opts)
src = discord.PCMVolumeTransformer(src, volume=vol) src = discord.PCMVolumeTransformer(src, volume=vol)
# After callback - play next song
def after(e): def after(e):
if e: print(e) if e:
if voice_client and not voice_client.is_connected(): return print(f"Playback error: {e}")
if voice_client and not voice_client.is_connected():
return
# Schedule next song
coro = play(ctx) coro = play(ctx)
fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop) fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop)
try: fut.result() try:
except: pass fut.result()
except Exception as ex:
print(f"Error in after callback: {ex}")
voice_client.play(src, after=after) voice_client.play(src, after=after)
except Exception as e: except Exception as e:
print(f"Play error: {e}") print(f"Play error: {e}")
# Try next song on error
await play(ctx) await play(ctx)