diff --git a/cogs/music/main.py b/cogs/music/main.py index 645fcf1..fc9dfca 100644 --- a/cogs/music/main.py +++ b/cogs/music/main.py @@ -7,6 +7,7 @@ import cogs.music.translate as translate import datetime import pytz +import asyncio from cogs.music.help import music_help @@ -55,27 +56,96 @@ class music(commands.Cog): @commands.command( help="Queues a song into the bot", - aliases=['p', 'qeue', 'q']) - @commands.check(util.in_server) + aliases=['p']) async def play(self, ctx: Context, *, url=None): if url is None: 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 - #TODO potentially save requests before getting stream link - audio = translate.main(url) + await ctx.message.add_reaction('👍') + 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 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 await queue.update_server(server, True) 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() diff --git a/cogs/music/queue.py b/cogs/music/queue.py index f47aaf6..28c030d 100644 --- a/cogs/music/queue.py +++ b/cogs/music/queue.py @@ -29,17 +29,21 @@ def initialize_tables(): cursor.execute("UPDATE servers SET is_playing = 0;") # 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, song_link TEXT, queued_by TEXT, position INTEGER NOT NULL, - has_played INTEGER DEFAULT 0, + + title TEXT, + thumbnail TEXT, + duration INTEGER, + PRIMARY KEY (position), FOREIGN KEY (server_id) REFERENCES servers(server_id) );''') # Clear all entries - cursor.execute("DELETE FROM queue;") + cursor.execute("DELETE FROM songs;") # Commit the changes and close the connection conn.commit() @@ -47,7 +51,7 @@ def initialize_tables(): # 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 conn = sqlite3.connect(db_path) 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 cursor.execute(""" - INSERT INTO queue (server_id, song_link, queued_by, position) - VALUES (?, ?, ?, ?) - """, (server_id, song_link, queued_by, max_order_num)) + 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 + # Add server to db if first time queuing 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() # Update the song as finished - cursor.execute('''DELETE FROM queue + cursor.execute('''DELETE FROM songs WHERE server_id = ? AND position = ?''', (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): cursor.execute(f""" SELECT MAX(position) - FROM queue + FROM songs WHERE server_id = ? """, (server_id,)) result = cursor.fetchone() @@ -129,7 +147,7 @@ async def pop(server_id): return None cursor.execute('''SELECT song_link - FROM queue + FROM songs WHERE server_id = ? AND position = ? ''', (server_id, max_order)) result = cursor.fetchone() @@ -178,7 +196,6 @@ async def is_server_playing(server_id): (server_id,)) result = cursor.fetchone() - print(result) conn.commit() conn.close() @@ -196,19 +213,42 @@ async def clear(server_id): await update_server(server_id, False) # 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.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 async def play(ctx): server_id = ctx.guild.id - # Wait until song is stopped playing - #while ctx.voice_client.is_playing(): - #await asyncio.sleep(1) + # Wait until song is stopped playing fully + while ctx.voice_client.is_playing(): + await asyncio.sleep(1) # check next song url = await pop(server_id) @@ -222,11 +262,12 @@ async def play(ctx): ctx.voice_client.play( AstroPlayer(ctx, url, FFMPEG_OPTS)) +# call play on ffmpeg exit class AstroPlayer(discord.FFmpegPCMAudio): def __init__(self, ctx, source, options) -> None: self.ctx = ctx super().__init__(source, **options) def _kill_process(self): - super()._kill_process - asyncio.run(play(self.ctx)) + super()._kill_process() + asyncio.create_task(play(self.ctx)) diff --git a/cogs/music/translate.py b/cogs/music/translate.py index 55bc319..2773b7b 100644 --- a/cogs/music/translate.py +++ b/cogs/music/translate.py @@ -6,6 +6,7 @@ ydl_opts = { 'format': 'bestaudio/best', 'quiet': True, 'default_search': 'ytsearch', + 'ignoreerrors': True, } def main(url): @@ -41,9 +42,19 @@ def main(url): def search_song(search): with ytdlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(f"ytsearch1:{search}", download=False) - audio_url = info['entries'][0]['url'] # Get audio stream URL - return [audio_url] + try: + info = ydl.extract_info(f"ytsearch1:{search}", download=False) + 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): @@ -56,10 +67,39 @@ def spotify_playlist(url): def song_download(url): with ytdlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(url, download=False) - audio_url = info['url'] # Get audio stream URL - return [audio_url] + try: + info = ydl.extract_info(url, download=False) + 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): - 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 diff --git a/cogs/music/util.py b/cogs/music/util.py index e74132f..ffe5fee 100644 --- a/cogs/music/util.py +++ b/cogs/music/util.py @@ -1,5 +1,7 @@ +import discord from discord.ext.commands.context import Context from discord.ext.commands.converter import CommandError +import config # 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) -# Check if command was entered in a server -async def in_server(ctx: Context): - return ctx.guild != None +# Build a display message for queuing a new song +async def queue_message(ctx: Context, data: dict): + 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"