from http import server import sqlite3 import random import time import discord import asyncio from .translate import search_song db_path = "./data/music.db" # Base FFmpeg options (will be modified by effects) BASE_FFMPEG_OPTS = { 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn' } # Audio effects configurations def get_effect_options(effect_name): """Get FFmpeg options for a specific audio effect""" effects = { 'none': { **BASE_FFMPEG_OPTS }, 'bassboost': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "bass=g=20,dynaudnorm"' }, 'nightcore': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "asetrate=48000*1.25,atempo=1.25,bass=g=5"' }, 'slowed': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "atempo=0.8,asetrate=48000*0.8,aecho=0.8:0.9:1000:0.3"' }, 'earrape': { **BASE_FFMPEG_OPTS, # Aggressive compression + hard clipping + bitcrushing for maximum distortion 'options': '-vn -af "volume=8,acompressor=threshold=0.001:ratio=30:attack=0.1:release=5,acrusher=bits=8:mix=0.7,volume=2,alimiter=limit=0.8"' }, 'deepfry': { **BASE_FFMPEG_OPTS, # Extreme bitcrushing + bass boost + compression (meme audio effect) 'options': '-vn -af "acrusher=bits=4:mode=log:aa=1,bass=g=15,acompressor=threshold=0.001:ratio=20,volume=3"' }, 'distortion': { **BASE_FFMPEG_OPTS, # Pure bitcrushing distortion 'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15,volume=2"' }, 'reverse': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "areverse"' }, 'chipmunk': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "asetrate=48000*1.5,atempo=1.5"' }, 'demonic': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "asetrate=48000*0.7,atempo=0.7,aecho=0.8:0.9:1000:0.5"' }, 'underwater': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "lowpass=f=800,aecho=0.8:0.88:60:0.4"' }, 'robot': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "afftfilt=real=\'hypot(re,im)*sin(0)\':imag=\'hypot(re,im)*cos(0)\':win_size=512:overlap=0.75"' }, '8d': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "apulsator=hz=0.08"' }, 'vibrato': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "vibrato=f=7:d=0.5"' }, 'tremolo': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "tremolo=f=5:d=0.9"' }, 'echo': { **BASE_FFMPEG_OPTS, 'options': '-vn -af "aecho=0.8:0.88:60:0.4"' }, 'phone': { **BASE_FFMPEG_OPTS, # Sounds like a phone call (bandpass filter) 'options': '-vn -af "bandpass=f=1500:width_type=h:w=1000,volume=2"' }, 'megaphone': { **BASE_FFMPEG_OPTS, # Megaphone/radio effect 'options': '-vn -af "highpass=f=300,lowpass=f=3000,volume=2,acompressor=threshold=0.1:ratio=8"' } } return effects.get(effect_name, effects['none']) # Creates the tables if they don't exist def initialize_tables(): # Connect to the database conn = sqlite3.connect(db_path) 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;") # 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 ] 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 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)) 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'])) conn.commit() conn.close() return max_order_num # 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) """ # 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() 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)) # 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]) 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]) # 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]) return result[1] # 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 # --- 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' 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 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 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' 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() def list_all_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 emojis = { 'none': '✨', # Changed to generic Sparkles 'bassboost': '💥', 'nightcore': '⚡', 'slowed': '🐢', 'earrape': '💀', 'deepfry': '🍟', 'distortion': '〰️', 'reverse': '⏪', 'chipmunk': '🐿️', 'demonic': '😈', 'underwater': '🫧', 'robot': '🤖', '8d': '🎧', 'vibrato': '〰️', 'tremolo': '📳', 'echo': '🗣️', 'phone': '📞', 'megaphone': '📣' } return emojis.get(effect_name, '✨') def get_effect_description(effect_name): descriptions = { 'none': 'Normal audio', 'bassboost': 'MAXIMUM BASS 🔊', 'nightcore': 'Speed + pitch up (anime vibes)', 'slowed': 'Slowed + reverb', 'earrape': '⚠️ Loud volume & distortion', 'deepfry': 'Bits crushed + Bass', 'distortion': 'Heavy distortion', 'reverse': 'Plays audio BACKWARDS', 'chipmunk': 'High pitched and fast', 'demonic': 'Low pitched and slow', 'underwater': 'Muffled underwater sound', 'robot': 'Robotic vocoder', '8d': 'Panning audio (use headphones!)', 'vibrato': 'Warbling pitch effect', 'tremolo': 'Volume oscillation', 'echo': 'Echo/reverb effect', 'phone': 'Sounds like a phone call', 'megaphone': 'Megaphone/radio effect' } return descriptions.get(effect_name, 'Unknown effect') async def play(ctx): server_id = ctx.guild.id voice_client = ctx.voice_client if voice_client is None: await update_server(server_id, False) return while voice_client.is_playing(): await asyncio.sleep(0.5) 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 fx = await get_effect(server_id) opts = get_effect_options(fx) src = discord.FFmpegPCMAudio(url, **opts) src = discord.PCMVolumeTransformer(src, volume=vol) def after(e): if e: print(e) if voice_client and not voice_client.is_connected(): return coro = play(ctx) fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop) try: fut.result() except: pass voice_client.play(src, after=after) except Exception as e: print(f"Play error: {e}") await play(ctx)