can queue playlists, databse stores more song info, display queue command works, skip needs looking at
This commit is contained in:
@@ -7,6 +7,7 @@ import cogs.music.translate as translate
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
import pytz
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from cogs.music.help import music_help
|
from cogs.music.help import music_help
|
||||||
|
|
||||||
@@ -55,27 +56,96 @@ class music(commands.Cog):
|
|||||||
|
|
||||||
@commands.command(
|
@commands.command(
|
||||||
help="Queues a song into the bot",
|
help="Queues a song into the bot",
|
||||||
aliases=['p', 'qeue', 'q'])
|
aliases=['p'])
|
||||||
@commands.check(util.in_server)
|
|
||||||
async def play(self, ctx: Context, *, url=None):
|
async def play(self, ctx: Context, *, url=None):
|
||||||
if url is None:
|
if url is None:
|
||||||
raise commands.CommandError("Must provide a link or search query")
|
raise commands.CommandError("Must provide a link or search query")
|
||||||
|
elif ctx.guild is None:
|
||||||
|
raise commands.CommandError("Command must be issued in a server")
|
||||||
|
|
||||||
server = ctx.guild.id
|
server = ctx.guild.id
|
||||||
|
|
||||||
#TODO potentially save requests before getting stream link
|
await ctx.message.add_reaction('👍')
|
||||||
audio = translate.main(url)
|
await util.join_vc(ctx)
|
||||||
|
|
||||||
|
msg = await ctx.send("Fetching song(s)...")
|
||||||
|
async with ctx.typing():
|
||||||
|
#TODO potentially save requests before getting stream link
|
||||||
|
# Grab video details such as title thumbnail duration
|
||||||
|
audio = translate.main(url)
|
||||||
|
|
||||||
|
await msg.delete()
|
||||||
|
|
||||||
|
if len(audio) == 0:
|
||||||
|
await ctx.message.add_reaction('🚫')
|
||||||
|
await ctx.send("Failed to find song!")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
#TODO make sure user isn't queuing in dm for some stupid reason
|
#TODO make sure user isn't queuing in dm for some stupid reason
|
||||||
for song in audio:
|
for song in audio:
|
||||||
await queue.add_song(server, song, ctx.author.id)
|
song['position'] = await queue.add_song(
|
||||||
|
server,
|
||||||
|
song,
|
||||||
|
ctx.author.display_name)
|
||||||
|
|
||||||
await ctx.message.add_reaction('👍')
|
await util.queue_message(ctx, audio[0])
|
||||||
|
|
||||||
await util.join_vc(ctx)
|
|
||||||
|
|
||||||
if await queue.is_server_playing(ctx.guild.id):
|
if await queue.is_server_playing(server):
|
||||||
return
|
return
|
||||||
|
|
||||||
await queue.update_server(server, True)
|
await queue.update_server(server, True)
|
||||||
await queue.play(ctx)
|
await queue.play(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@commands.command(
|
||||||
|
help="Display the current music queue",
|
||||||
|
aliases=['q', 'songs'])
|
||||||
|
async def queue(self, ctx: Context):
|
||||||
|
|
||||||
|
server = ctx.guild
|
||||||
|
|
||||||
|
# Perform usual checks
|
||||||
|
if server is None:
|
||||||
|
raise commands.CommandError("Command must be issued in a server")
|
||||||
|
|
||||||
|
|
||||||
|
# Grab all songs from this server
|
||||||
|
n, songs = await queue.grab_songs(server.id)
|
||||||
|
|
||||||
|
# Check once more
|
||||||
|
if len(songs) == 0:
|
||||||
|
await ctx.send("🚫 This server has no queue currently. Start the party by queuing up a song!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Display songs
|
||||||
|
await util.display_server_queue(ctx, songs, n)
|
||||||
|
|
||||||
|
|
||||||
|
@commands.command(
|
||||||
|
help="Skips the current song that is playing, can include number to skip more songs",
|
||||||
|
aliases=['s'])
|
||||||
|
async def skip(self, ctx: Context, n='1'):
|
||||||
|
server = ctx.guild
|
||||||
|
|
||||||
|
if server is None:
|
||||||
|
raise commands.CommandError("Command must be issued in a server")
|
||||||
|
|
||||||
|
if ctx.voice_client is None:
|
||||||
|
raise commands.CommandError("I'm not in a voice channel")
|
||||||
|
|
||||||
|
if not n.isdigit():
|
||||||
|
raise commands.CommandError("Please enter a number to skip")
|
||||||
|
n = int(n)
|
||||||
|
|
||||||
|
if n <= 0:
|
||||||
|
raise commands.CommandError("Please enter a positive number")
|
||||||
|
|
||||||
|
# Skip specificed number of songs
|
||||||
|
for _ in range(n-1):
|
||||||
|
await queue.pop(server.id)
|
||||||
|
|
||||||
|
# Safe to ignore error for now
|
||||||
|
ctx.voice_client.stop()
|
||||||
|
|||||||
@@ -29,17 +29,21 @@ def initialize_tables():
|
|||||||
cursor.execute("UPDATE servers SET is_playing = 0;")
|
cursor.execute("UPDATE servers SET is_playing = 0;")
|
||||||
|
|
||||||
# Create queue table if it doesn't exist
|
# Create queue table if it doesn't exist
|
||||||
cursor.execute('''CREATE TABLE IF NOT EXISTS queue (
|
cursor.execute('''CREATE TABLE IF NOT EXISTS songs (
|
||||||
server_id TEXT NOT NULL,
|
server_id TEXT NOT NULL,
|
||||||
song_link TEXT,
|
song_link TEXT,
|
||||||
queued_by TEXT,
|
queued_by TEXT,
|
||||||
position INTEGER NOT NULL,
|
position INTEGER NOT NULL,
|
||||||
has_played INTEGER DEFAULT 0,
|
|
||||||
|
title TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
duration INTEGER,
|
||||||
|
|
||||||
PRIMARY KEY (position),
|
PRIMARY KEY (position),
|
||||||
FOREIGN KEY (server_id) REFERENCES servers(server_id)
|
FOREIGN KEY (server_id) REFERENCES servers(server_id)
|
||||||
);''')
|
);''')
|
||||||
# Clear all entries
|
# Clear all entries
|
||||||
cursor.execute("DELETE FROM queue;")
|
cursor.execute("DELETE FROM songs;")
|
||||||
|
|
||||||
# Commit the changes and close the connection
|
# Commit the changes and close the connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -47,7 +51,7 @@ def initialize_tables():
|
|||||||
|
|
||||||
|
|
||||||
# Queue a song in the db
|
# Queue a song in the db
|
||||||
async def add_song(server_id, song_link, queued_by):
|
async def add_song(server_id, details, queued_by):
|
||||||
# Connect to db
|
# Connect to db
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -57,13 +61,27 @@ async def add_song(server_id, song_link, queued_by):
|
|||||||
max_order_num = await get_max(server_id, cursor) + 1
|
max_order_num = await get_max(server_id, cursor) + 1
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO queue (server_id, song_link, queued_by, position)
|
INSERT INTO songs (server_id,
|
||||||
VALUES (?, ?, ?, ?)
|
song_link,
|
||||||
""", (server_id, song_link, queued_by, max_order_num))
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
return max_order_num
|
||||||
|
|
||||||
|
|
||||||
# Add server to db if first time queuing
|
# Add server to db if first time queuing
|
||||||
async def add_server(server_id, cursor, conn):
|
async def add_server(server_id, cursor, conn):
|
||||||
@@ -89,7 +107,7 @@ async def mark_song_as_finished(server_id, order_num):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Update the song as finished
|
# Update the song as finished
|
||||||
cursor.execute('''DELETE FROM queue
|
cursor.execute('''DELETE FROM songs
|
||||||
WHERE server_id = ? AND position = ?''',
|
WHERE server_id = ? AND position = ?''',
|
||||||
(server_id, order_num))
|
(server_id, order_num))
|
||||||
|
|
||||||
@@ -102,7 +120,7 @@ async def mark_song_as_finished(server_id, order_num):
|
|||||||
async def get_max(server_id, cursor):
|
async def get_max(server_id, cursor):
|
||||||
cursor.execute(f"""
|
cursor.execute(f"""
|
||||||
SELECT MAX(position)
|
SELECT MAX(position)
|
||||||
FROM queue
|
FROM songs
|
||||||
WHERE server_id = ?
|
WHERE server_id = ?
|
||||||
""", (server_id,))
|
""", (server_id,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
@@ -129,7 +147,7 @@ async def pop(server_id):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
cursor.execute('''SELECT song_link
|
cursor.execute('''SELECT song_link
|
||||||
FROM queue
|
FROM songs
|
||||||
WHERE server_id = ? AND position = ?
|
WHERE server_id = ? AND position = ?
|
||||||
''', (server_id, max_order))
|
''', (server_id, max_order))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
@@ -178,7 +196,6 @@ async def is_server_playing(server_id):
|
|||||||
(server_id,))
|
(server_id,))
|
||||||
|
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
print(result)
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -196,19 +213,42 @@ async def clear(server_id):
|
|||||||
await update_server(server_id, False)
|
await update_server(server_id, False)
|
||||||
|
|
||||||
# Delete all songs from the server
|
# Delete all songs from the server
|
||||||
cursor.execute('''DELETE FROM queue WHERE server_id = ?''', (server_id,))
|
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
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
|
||||||
|
|
||||||
|
|
||||||
# Play and loop songs in server
|
# Play and loop songs in server
|
||||||
async def play(ctx):
|
async def play(ctx):
|
||||||
server_id = ctx.guild.id
|
server_id = ctx.guild.id
|
||||||
|
|
||||||
# Wait until song is stopped playing
|
# Wait until song is stopped playing fully
|
||||||
#while ctx.voice_client.is_playing():
|
while ctx.voice_client.is_playing():
|
||||||
#await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# check next song
|
# check next song
|
||||||
url = await pop(server_id)
|
url = await pop(server_id)
|
||||||
@@ -222,11 +262,12 @@ async def play(ctx):
|
|||||||
ctx.voice_client.play(
|
ctx.voice_client.play(
|
||||||
AstroPlayer(ctx, url, FFMPEG_OPTS))
|
AstroPlayer(ctx, url, FFMPEG_OPTS))
|
||||||
|
|
||||||
|
# call play on ffmpeg exit
|
||||||
class AstroPlayer(discord.FFmpegPCMAudio):
|
class AstroPlayer(discord.FFmpegPCMAudio):
|
||||||
def __init__(self, ctx, source, options) -> None:
|
def __init__(self, ctx, source, options) -> None:
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
super().__init__(source, **options)
|
super().__init__(source, **options)
|
||||||
|
|
||||||
def _kill_process(self):
|
def _kill_process(self):
|
||||||
super()._kill_process
|
super()._kill_process()
|
||||||
asyncio.run(play(self.ctx))
|
asyncio.create_task(play(self.ctx))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ ydl_opts = {
|
|||||||
'format': 'bestaudio/best',
|
'format': 'bestaudio/best',
|
||||||
'quiet': True,
|
'quiet': True,
|
||||||
'default_search': 'ytsearch',
|
'default_search': 'ytsearch',
|
||||||
|
'ignoreerrors': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def main(url):
|
def main(url):
|
||||||
@@ -41,9 +42,19 @@ def main(url):
|
|||||||
|
|
||||||
def search_song(search):
|
def search_song(search):
|
||||||
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
info = ydl.extract_info(f"ytsearch1:{search}", download=False)
|
try:
|
||||||
audio_url = info['entries'][0]['url'] # Get audio stream URL
|
info = ydl.extract_info(f"ytsearch1:{search}", download=False)
|
||||||
return [audio_url]
|
except:
|
||||||
|
return []
|
||||||
|
if info is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
info = info['entries'][0] # Get audio stream URL
|
||||||
|
data = {'url': info['url'],
|
||||||
|
'title': info['title'],
|
||||||
|
'thumbnail': info['thumbnail'],
|
||||||
|
'duration': info['duration']} # Grab data
|
||||||
|
return [data]
|
||||||
|
|
||||||
|
|
||||||
def spotify_song(url):
|
def spotify_song(url):
|
||||||
@@ -56,10 +67,39 @@ def spotify_playlist(url):
|
|||||||
|
|
||||||
def song_download(url):
|
def song_download(url):
|
||||||
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
info = ydl.extract_info(url, download=False)
|
try:
|
||||||
audio_url = info['url'] # Get audio stream URL
|
info = ydl.extract_info(url, download=False)
|
||||||
return [audio_url]
|
except:
|
||||||
|
return []
|
||||||
|
if info is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
print(info.keys())
|
||||||
|
|
||||||
|
data = {'url': info['url'],
|
||||||
|
'title': info['title'],
|
||||||
|
'thumbnail': info['thumbnail'],
|
||||||
|
'duration': info['duration']} # Grab data
|
||||||
|
return [data]
|
||||||
|
|
||||||
|
|
||||||
def playlist_download(url):
|
def playlist_download(url):
|
||||||
return []
|
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
try:
|
||||||
|
info = ydl.extract_info(url, download=False)
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
if info is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
info = info['entries'] # Grabbing all songs in playlist
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
for song in info:
|
||||||
|
data = {'url': song['url'],
|
||||||
|
'title': song['title'],
|
||||||
|
'thumbnail': song['thumbnail'],
|
||||||
|
'duration': song['duration']} # Grab data
|
||||||
|
urls.append(data)
|
||||||
|
|
||||||
|
return urls
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import discord
|
||||||
from discord.ext.commands.context import Context
|
from discord.ext.commands.context import Context
|
||||||
from discord.ext.commands.converter import CommandError
|
from discord.ext.commands.converter import CommandError
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
# Joining/moving to the user's vc in a guild
|
# Joining/moving to the user's vc in a guild
|
||||||
@@ -45,6 +47,48 @@ async def leave_vc(ctx: Context):
|
|||||||
await ctx.voice_client.disconnect(force=False)
|
await ctx.voice_client.disconnect(force=False)
|
||||||
|
|
||||||
|
|
||||||
# Check if command was entered in a server
|
# Build a display message for queuing a new song
|
||||||
async def in_server(ctx: Context):
|
async def queue_message(ctx: Context, data: dict):
|
||||||
return ctx.guild != None
|
msg = discord.Embed(
|
||||||
|
title=f"{ctx.author.display_name} queued a song!",
|
||||||
|
color=config.get_color("main"))
|
||||||
|
|
||||||
|
msg.set_thumbnail(url=data['thumbnail'])
|
||||||
|
msg.add_field(name=data['title'],
|
||||||
|
value=f"Duration: {format_time(data['duration'])}" + '\n'
|
||||||
|
+ f"Position: {data['position']}")
|
||||||
|
|
||||||
|
await ctx.send(embed=msg)
|
||||||
|
|
||||||
|
|
||||||
|
# Build an embed message that shows the queue
|
||||||
|
async def display_server_queue(ctx: Context, songs, n):
|
||||||
|
server = ctx.guild
|
||||||
|
|
||||||
|
msg = discord.Embed(
|
||||||
|
title=f"{server.name}'s Queue!",
|
||||||
|
color=config.get_color("main"))
|
||||||
|
|
||||||
|
display = ""
|
||||||
|
for i, song in enumerate(songs):
|
||||||
|
display += f"``{i + 1}.`` {song[0]} - {format_time(song[1])} Queued by {song[2]}\n"
|
||||||
|
msg.add_field(name="Songs:",
|
||||||
|
value=display,
|
||||||
|
inline=True)
|
||||||
|
if n > 10:
|
||||||
|
msg.set_footer(text=f"and {n - 10} more!..")
|
||||||
|
|
||||||
|
await ctx.send(embed=msg)
|
||||||
|
|
||||||
|
|
||||||
|
# Converts seconds into more readable format
|
||||||
|
def format_time(seconds):
|
||||||
|
minutes, seconds = divmod(seconds, 60)
|
||||||
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
|
||||||
|
if hours > 0:
|
||||||
|
return f"{hours}:{minutes:02d}:{seconds:02d}"
|
||||||
|
elif minutes > 0:
|
||||||
|
return f"{minutes}:{seconds:02d}"
|
||||||
|
else:
|
||||||
|
return f"{seconds} seconds"
|
||||||
|
|||||||
Reference in New Issue
Block a user