717 lines
20 KiB
Python
717 lines
20 KiB
Python
from http import server
|
|
import sqlite3
|
|
import random
|
|
|
|
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"'
|
|
},
|
|
'distortion': {
|
|
**BASE_FFMPEG_OPTS,
|
|
# Pure bitcrushing distortion
|
|
'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15"'
|
|
},
|
|
'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"'
|
|
},
|
|
}
|
|
|
|
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,
|
|
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)
|
|
try:
|
|
cursor.execute("ALTER TABLE servers ADD COLUMN loop_mode TEXT DEFAULT 'off';")
|
|
except sqlite3.OperationalError:
|
|
pass # Column already exists
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE servers ADD COLUMN volume INTEGER DEFAULT 100;")
|
|
except sqlite3.OperationalError:
|
|
pass # Column already exists
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE servers ADD COLUMN effect TEXT DEFAULT 'none';")
|
|
except sqlite3.OperationalError:
|
|
pass # Column already exists
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE servers ADD COLUMN song_start_time REAL DEFAULT 0;")
|
|
except sqlite3.OperationalError:
|
|
pass # Column already exists
|
|
|
|
try:
|
|
cursor.execute("ALTER TABLE servers ADD COLUMN song_duration INTEGER DEFAULT 0;")
|
|
except sqlite3.OperationalError:
|
|
pass # Column already exists
|
|
|
|
# Create queue table if it doesn't exist
|
|
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)
|
|
);''')
|
|
# Clear all entries
|
|
cursor.execute("DELETE FROM songs;")
|
|
|
|
# Commit the changes and close the connection
|
|
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):
|
|
cursor.execute("""
|
|
INSERT INTO songs (server_id,
|
|
song_link,
|
|
queued_by,
|
|
position,
|
|
title,
|
|
thumbnail,
|
|
duration)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (server_id,
|
|
"Not grabbed",
|
|
queued_by,
|
|
max_order_num,
|
|
details,
|
|
"Unkown",
|
|
"Unkown"))
|
|
else:
|
|
cursor.execute("""
|
|
INSERT INTO songs (server_id,
|
|
song_link,
|
|
queued_by,
|
|
position,
|
|
title,
|
|
thumbnail,
|
|
duration)
|
|
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):
|
|
# Connect to db
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# JUST INCASE!
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
cursor.execute('''SELECT *
|
|
FROM songs
|
|
WHERE server_id = ?
|
|
ORDER BY position
|
|
LIMIT 1;''', (server_id,))
|
|
result = cursor.fetchone()
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
if result == None:
|
|
return None
|
|
elif ignore:
|
|
await mark_song_as_finished(server_id, result[3])
|
|
return None
|
|
elif result[1] == "Not grabbed":
|
|
# Fetch song info
|
|
song = await search_song(result[4])
|
|
if song == []:
|
|
return None
|
|
else:
|
|
song = song[0]
|
|
|
|
await set_current_song(server_id, song['title'], 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']
|
|
|
|
await set_current_song(server_id, result[4], result[6]) # result[6] is duration
|
|
|
|
# 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):
|
|
# Check if the server exists
|
|
cursor.execute('''SELECT COUNT(*)
|
|
FROM servers
|
|
WHERE server_id = ?''', (server_id,))
|
|
|
|
result = cursor.fetchone()
|
|
server_exists = result[0] > 0
|
|
|
|
# If the server doesn't exist, add it
|
|
if not server_exists:
|
|
cursor.execute('''INSERT INTO servers (server_id, loop_mode, volume, effect)
|
|
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, duration=0):
|
|
# Connect to the database
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
import time
|
|
start_time = time.time()
|
|
|
|
cursor.execute(''' UPDATE servers
|
|
SET song_name = ?, song_start_time = ?, song_duration = ?
|
|
WHERE server_id = ?''',
|
|
(title, start_time, duration, server_id))
|
|
|
|
# Close connection
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
async def get_current_song(server_id):
|
|
# Connect to the database
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(''' SELECT song_name
|
|
FROM servers
|
|
WHERE server_id = ?
|
|
LIMIT 1;''',
|
|
(server_id,))
|
|
|
|
result = cursor.fetchone()
|
|
|
|
# Close connection
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return result[0] if result else "Nothing"
|
|
|
|
|
|
async def get_current_progress(server_id):
|
|
"""Get current playback progress (elapsed, duration, percentage)"""
|
|
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()
|
|
|
|
if not result or result[2] == 0: # Not playing
|
|
return 0, 0, 0.0
|
|
|
|
start_time, duration, _ = result
|
|
|
|
if duration == 0:
|
|
return 0, 0, 0.0
|
|
|
|
import time
|
|
elapsed = int(time.time() - start_time)
|
|
elapsed = min(elapsed, duration) # Cap at duration
|
|
percentage = (elapsed / duration) * 100 if duration > 0 else 0
|
|
|
|
return elapsed, duration, percentage
|
|
|
|
return result[0] if result else "Nothing"
|
|
|
|
|
|
# Grab max order from server
|
|
async def get_max(server_id, cursor):
|
|
cursor.execute(f"""
|
|
SELECT MAX(position)
|
|
FROM songs
|
|
WHERE server_id = ?
|
|
""", (server_id,))
|
|
result = cursor.fetchone()
|
|
|
|
# Highnest number or 0
|
|
max_order_num = result[0] if result[0] is not None else -1
|
|
|
|
return max_order_num
|
|
|
|
|
|
# Sets the playing variable in a server to true or false
|
|
async def update_server(server_id, playing: bool):
|
|
# Connect to database
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# add server to db if not present
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
value = 1 if playing else 0
|
|
|
|
# Update field
|
|
cursor.execute("""UPDATE servers
|
|
SET is_playing = ?
|
|
WHERE server_id = ?
|
|
""", (value, server_id))
|
|
|
|
# Close connection
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
async def is_server_playing(server_id):
|
|
# Connect to db
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# add server to db if not present
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
cursor.execute("""SELECT is_playing
|
|
FROM servers
|
|
WHERE server_id = ?""",
|
|
(server_id,))
|
|
|
|
result = cursor.fetchone()
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return True if result[0] == 1 else False
|
|
|
|
|
|
# Delete all songs from a server
|
|
async def clear(server_id):
|
|
# Connect to db
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
await add_server(server_id, cursor, conn)
|
|
await update_server(server_id, False)
|
|
|
|
# Delete all songs from the server
|
|
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
# Grabs all songs from a server for display purposes
|
|
async def grab_songs(server_id):
|
|
# Connect to db
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
# Grabs all songs from the server
|
|
cursor.execute('''SELECT title, duration, queued_by
|
|
FROM songs
|
|
WHERE server_id = ?
|
|
ORDER BY position
|
|
LIMIT 10''', (server_id,))
|
|
songs = cursor.fetchall()
|
|
max = await get_max(server_id, cursor)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return max, songs
|
|
|
|
|
|
# ============= LOOP/SHUFFLE/VOLUME FEATURES =============
|
|
|
|
# Get/Set loop mode
|
|
async def get_loop_mode(server_id):
|
|
"""Get the current loop mode: 'off', 'song', or 'queue'"""
|
|
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,))
|
|
result = cursor.fetchone()
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return result[0] if result else 'off'
|
|
|
|
|
|
async def set_loop_mode(server_id, mode):
|
|
"""Set loop mode: 'off', 'song', or 'queue'"""
|
|
if mode not in ['off', 'song', 'queue']:
|
|
return False
|
|
|
|
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()
|
|
return True
|
|
|
|
|
|
# Get/Set volume
|
|
async def get_volume(server_id):
|
|
"""Get the current volume (0-200)"""
|
|
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,))
|
|
result = cursor.fetchone()
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return result[0] if result else 100
|
|
|
|
|
|
async def set_volume(server_id, volume):
|
|
"""Set volume (0-200)"""
|
|
volume = max(0, min(200, volume)) # Clamp between 0-200
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
cursor.execute("""UPDATE servers
|
|
SET volume = ?
|
|
WHERE server_id = ?""",
|
|
(volume, server_id))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return volume
|
|
|
|
|
|
# Shuffle the queue
|
|
async def shuffle_queue(server_id):
|
|
"""Randomize the order of songs in the queue"""
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
# Get all songs
|
|
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 # Nothing to shuffle
|
|
|
|
# Shuffle the songs (keep positions but randomize order)
|
|
random.shuffle(songs)
|
|
|
|
# Delete all current songs
|
|
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
|
|
|
|
# Re-insert in shuffled order
|
|
for i, song in enumerate(songs):
|
|
cursor.execute("""INSERT INTO songs (server_id, song_link, queued_by, position, title, thumbnail, duration)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(server_id, song[1], song[2], i, song[3], song[4], song[5]))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
|
|
|
|
# ============= AUDIO EFFECTS FEATURES =============
|
|
|
|
async def get_effect(server_id):
|
|
"""Get the current audio effect"""
|
|
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,))
|
|
result = cursor.fetchone()
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return result[0] if result else 'none'
|
|
|
|
|
|
async def set_effect(server_id, effect_name):
|
|
"""Set the audio effect"""
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
await add_server(server_id, cursor, conn)
|
|
|
|
cursor.execute("""UPDATE servers
|
|
SET effect = ?
|
|
WHERE server_id = ?""",
|
|
(effect_name, server_id))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
|
|
|
|
def list_all_effects():
|
|
"""Return a list of all available 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 representation for each effect"""
|
|
emojis = {
|
|
'none': '🔊',
|
|
'bassboost': '🔉💥',
|
|
'nightcore': '⚡🎀',
|
|
'slowed': '🐌💤',
|
|
'distortion': '⚡🔊',
|
|
'reverse': '⏪🔄',
|
|
'chipmunk': '🐿️',
|
|
'demonic': '😈🔥',
|
|
'underwater': '🌊💦',
|
|
'robot': '🤖',
|
|
'8d': '🎧🌀',
|
|
'vibrato': '〰️',
|
|
'tremolo': '📳',
|
|
'echo': '🗣️💭',
|
|
}
|
|
return emojis.get(effect_name, '🔊')
|
|
|
|
|
|
def get_effect_description(effect_name):
|
|
"""Get user-friendly description for each effect"""
|
|
descriptions = {
|
|
'none': 'Normal audio',
|
|
'bassboost': 'MAXIMUM BASS 🔊',
|
|
'nightcore': 'Speed + pitch up (anime vibes)',
|
|
'slowed': 'Slowed + reverb (TikTok aesthetic)',
|
|
'distortion': 'Heavy bitcrushing distortion',
|
|
'reverse': 'Plays audio BACKWARDS',
|
|
'chipmunk': 'High pitched and fast (Alvin mode)',
|
|
'demonic': 'Low pitched and slow (cursed)',
|
|
'underwater': 'Muffled underwater sound',
|
|
'robot': 'Robotic vocoder',
|
|
'8d': 'Panning audio (use headphones!)',
|
|
'vibrato': 'Warbling pitch effect',
|
|
'tremolo': 'Volume oscillation',
|
|
'echo': 'Echo/reverb effect',
|
|
}
|
|
return descriptions.get(effect_name, 'Unknown effect')
|
|
|
|
|
|
# Play and loop songs in server
|
|
async def play(ctx):
|
|
"""Main playback loop - plays songs from queue sequentially with effects"""
|
|
server_id = ctx.guild.id
|
|
voice_client = ctx.voice_client
|
|
|
|
# Safety check
|
|
if voice_client is None:
|
|
await update_server(server_id, False)
|
|
return
|
|
|
|
# Wait until current song finishes
|
|
while voice_client.is_playing():
|
|
await asyncio.sleep(0.5)
|
|
|
|
# Get next song
|
|
url = await pop(server_id)
|
|
|
|
# If no songs left, update status and return
|
|
if url is None:
|
|
await update_server(server_id, False)
|
|
return
|
|
|
|
try:
|
|
# Get volume and effect settings
|
|
volume_percent = await get_volume(server_id)
|
|
volume = volume_percent / 100.0 # Convert to 0.0-2.0 range
|
|
|
|
current_effect = await get_effect(server_id)
|
|
ffmpeg_opts = get_effect_options(current_effect)
|
|
|
|
# Create audio source with effect and volume control
|
|
audio_source = discord.FFmpegPCMAudio(url, **ffmpeg_opts)
|
|
audio_source = discord.PCMVolumeTransformer(audio_source, volume=volume)
|
|
|
|
# Play with callback to continue queue
|
|
def after_playing(error):
|
|
if error:
|
|
print(f"Player error: {error}")
|
|
# Schedule the next song in the event loop
|
|
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 Exception as e:
|
|
print(f"Error playing next song: {e}")
|
|
|
|
voice_client.play(audio_source, after=after_playing)
|
|
|
|
except Exception as e:
|
|
print(f"Error starting playback: {e}")
|
|
# Try to continue with next song
|
|
await play(ctx)
|