""" 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 from .db_manager import 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 # Note: FFmpeg's acompressor ratio max is 20 'options': '-vn -af "volume=8,acompressor=threshold=0.001:ratio=20: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']) # =================================== # Initialization # =================================== def initialize_tables(): """Initialize database tables""" db.initialize_tables() # =================================== # Queue Management # =================================== async def add_song(server_id, details, queued_by, position=None): """ Add a song to the queue 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 (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: # 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 ) return pos async def pop(server_id, ignore=False, skip_mode=False): """ Pop next song from queue 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 """ result = db.get_next_song(str(server_id)) if result is None: return None # 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': db.remove_song(str(server_id), position) return song['url'] # 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': db.remove_song(str(server_id), position) return song_link async def grab_songs(server_id): """ Get current queue Returns: Tuple of (max_position, list_of_songs) """ return db.get_queue(str(server_id), limit=10) 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 shuffle_queue(server_id): """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): """Get current audio effect""" return db.get_effect(str(server_id)) async def set_effect(server_id, fx): """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): """Get emoji for effect""" emojis = { 'none': '✨', '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): """Get description for effect""" 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') # =================================== # 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: # 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(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 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)