Files
groovy-zilean/cogs/music/queue.py

426 lines
12 KiB
Python

"""
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)