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

521 lines
17 KiB
Python

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:
vol = await get_volume(server_id) / 100.0
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)