From 7e3c123b43ca5a333bc2f3e6155fcc703b24a21b Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Fri, 11 Jan 2019 14:23:44 +0100 Subject: [PATCH 1/3] Added emoji scroller for queue viewing. --- cogs/music.py | 876 +++++++++++++++---------------- cogs/utils/embedscroller.py | 190 +++++++ cogs/utils/mixplayer/mixqueue.py | 8 +- 3 files changed, 621 insertions(+), 453 deletions(-) create mode 100644 cogs/utils/embedscroller.py diff --git a/cogs/music.py b/cogs/music.py index ec5598b..63d9065 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -22,6 +22,7 @@ from discord.ext import commands from .utils.mixplayer.mixplayer import MixPlayer from .utils import checks +from .utils.embedscroller import EmbedScroller, ScrollerFromLines, QueueScroller from lavasettings import * time_rx = re.compile('[0-9]+') @@ -29,455 +30,432 @@ url_rx = re.compile('https?:\/\/(?:www\.)?.+') class Music: - def __init__(self, bot): - self.bot = bot - - if not hasattr(bot, 'lavalink'): - lavalink.Client(bot=bot, password='youshallnotpass', - loop=bot.loop, log_level=logging.INFO, - host=host, ws_port=ws_port, - rest_port=rest_port, - player=MixPlayer) - self.bot.lavalink.register_hook(self._track_hook) - - def __unload(self): - for guild_id, player in self.bot.lavalink.players: - self.bot.loop.create_task(player.disconnect()) - player.cleanup() - # Clear the players from Lavalink's internal cache - self.bot.lavalink.players.clear() - self.bot.lavalink.unregister_hook(self._track_hook) - - async def _track_hook(self, event): - if isinstance(event, lavalink.Events.StatsUpdateEvent): - return - channel = self.bot.get_channel(event.player.fetch('channel')) - if not channel: - return - - if isinstance(event, lavalink.Events.TrackStartEvent): - embed = discord.Embed(title='Now playing:', description=event.track.title, color=0xEFD26C) - thumbnail_url = await RoxUtils.ThumbNailer.identify(self, event.player.current.identifier, event.player.current.uri) - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - await channel.send(embed=embed) - elif isinstance(event, lavalink.Events.QueueEndEvent): - await channel.send('Queue ended! Why not queue more songs?') - - @commands.command(name='play', aliases=['p']) - @commands.guild_only() - async def _play(self, ctx, *, query: str): - """ Searches and plays a song from a given query. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - query = query.strip('<>') - - if not url_rx.match(query): - query = f'ytsearch:{query}' - - results = await self.bot.lavalink.get_tracks(query) - - if not results or not results['tracks']: - return await ctx.send('Nothing found!') - - embed = discord.Embed(color=0xEFD26C) - - if results['loadType'] == 'PLAYLIST_LOADED': - tracks = results['tracks'] - - for track in tracks: - player.add(requester=ctx.author.id, track=track) - - embed.title = 'Playlist Enqueued!' - embed.description = f'{results["playlistInfo"]["name"]} - {len(tracks)} tracks' - await ctx.send(embed=embed) - else: - track = results['tracks'][0] - embed.title = 'Track Enqueued' - thumbnail_url = await RoxUtils.ThumbNailer.identify(self, track['info']['identifier'], track['info']['uri']) - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - - embed.description = f'[{track["info"]["title"]}]({track["info"]["uri"]})' - await ctx.send(embed=embed) - player.add(requester=ctx.author.id, track=track) - - if not player.is_playing: - await player.play() - - @commands.command(name='previous', aliases=['pv']) - @commands.guild_only() - async def _previous(self, ctx): - """ Plays the previous song. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - try: - await player.play_previous() - except lavalink.NoPreviousTrack: - await ctx.send('There is no previous song to play.') - - @commands.command(name='playnow', aliases=['pn']) - @commands.guild_only() - async def _playnow(self, ctx, *, query: str): - """ Plays immediately a song. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.queue and not player.is_playing: - return await ctx.invoke(self._play, query=query) - - query = query.strip('<>') - - if not url_rx.match(query): - query = f'ytsearch:{query}' - - results = await self.bot.lavalink.get_tracks(query) - - if not results or not results['tracks']: - return await ctx.send('Nothing found!') - - tracks = results['tracks'] - track = tracks.pop(0) - - if results['loadType'] == 'PLAYLIST_LOADED': - for _track in tracks: - player.add(requester=ctx.author.id, track=_track) - - await player.play_now(requester=ctx.author.id, track=track) - - @commands.command(name='skipto', aliases=['st']) - @commands.guild_only() - @checks.is_DJ() - async def _skip_to(self, ctx, index: int): - """ Plays the queue from a specific point. Disregards tracks before the index. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - if index < 1: - return await ctx.send('Invalid specified index.') - if len(player.queue) < index: - return await ctx.send('This index exceeds the queue\'s length.') - await player.skip_to(index - 1) - - @commands.command(name='seek') - @commands.guild_only() - async def _seek(self, ctx, *, time: str): - """ Seeks to a given position in a track. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_playing: - return await ctx.send('Not playing.') - - seconds = time_rx.search(time) - if not seconds: - return await ctx.send('You need to specify the amount of seconds to skip!') - - seconds = int(seconds.group()) * 1000 - if time.startswith('-'): - seconds *= -1 - - track_time = player.position + seconds - await player.seek(track_time) - - await ctx.send(f'Moved track to **{lavalink.Utils.format_time(track_time)}**') - - @commands.command(name='skip', aliases=['forceskip', 'fs']) - @commands.guild_only() - async def _skip(self, ctx): - """ Skips the current track. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_playing: - return await ctx.send('Not playing.') - - await player.skip() - await ctx.send('⏭ | Skipped.') - - @commands.command(name='stop') - @commands.guild_only() - async def _stop(self, ctx): - """ Stops the player and clears its queue. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_playing: - return await ctx.send('Not playing.') - - player.queue.clear() - await player.stop() - await ctx.send('⏹ | Stopped.') - - @commands.command(name='now', aliases=['np', 'n', 'playing']) - @commands.guild_only() - async def _now(self, ctx): - """ Shows some stats about the currently playing song. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - song = 'Nothing' - - if player.current: - position = lavalink.Utils.format_time(player.position) - if player.current.stream: - duration = '🔴 LIVE' - else: - duration = lavalink.Utils.format_time(player.current.duration) - song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})' - - embed = discord.Embed(color=0xEFD26C, title='Now Playing', description=song) - - thumbnail_url = await RoxUtils.ThumbNailer.identify(self, player.current.identifier, player.current.uri) - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - - await ctx.send(embed=embed) - - @commands.command(name='queue', aliases=['q']) - @commands.guild_only() - async def _queue(self, ctx, page: int = 1): - """ Shows the player's queue. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if player.queue.is_empty(): - return await ctx.send('There\'s nothing in the queue! Why not queue something?') - - queue = player.queue.get_queue() - items_per_page = 10 - pages = math.ceil(len(queue) / items_per_page) - start = (page - 1) * items_per_page - end = start + items_per_page - - queue_list = '' - for index, track in enumerate(queue[start:end], start=start): - queue_list += f'`{index + 1}.` [**{track.title}**]({track.uri}) _by <@{track.requester}>_\n' - - embed = discord.Embed(color=0xEFD26C, - description=f'**{len(queue)} tracks**\n\n{queue_list}') - embed.set_footer(text=f'Viewing page {page}/{pages}') - await ctx.send(embed=embed) - - @commands.command(name='myqueue', aliases=['mq']) - @commands.guild_only() - async def _myqueue(self, ctx, page: int = 1): - """ Shows the player's queue. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if player.queue.is_empty(): - return await ctx.send('There\'s nothing in the queue! Why not queue something?') - - queue = player.queue.get_user_queue(ctx.author.id, pos=True) - - if not queue: - return await ctx.send('You haven\'t queued anything') - - items_per_page = 10 - pages = math.ceil(len(queue) / items_per_page) - start = (page - 1) * items_per_page - end = start + items_per_page - - queue_list = '' - for index, (track, globalpos) in enumerate(queue[start:end], start=start): - queue_list += f'`{index + 1}({globalpos + 1}).` [**{track.title}**]({track.uri})\n' - - embed = discord.Embed(color=0xEFD26C, - description=f'**{len(queue)} tracks**\n\n{queue_list}') - embed.set_footer(text=f'Viewing page {page}/{pages}') - await ctx.send(embed=embed) - - @commands.command(name='pause', aliases=['resume']) - @commands.guild_only() - async def _pause(self, ctx): - """ Pauses/Resumes the current track. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_playing: - return await ctx.send('Not playing.') - - if player.paused: - await player.set_pause(False) - await ctx.send('⏯ | Resumed') - else: - await player.set_pause(True) - await ctx.send('⏯ | Paused') - - @commands.command(name='volume', aliases=['vol']) - @commands.guild_only() - async def _volume(self, ctx, volume: int = None): - """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not volume: - return await ctx.send(f'🔈 | {player.volume}%') - - await player.set_volume(volume) - await ctx.send(f'🔈 | Set to {player.volume}%') - - @commands.command(name='shuffle') - @commands.guild_only() - async def _shuffle(self, ctx): - """ Shuffles the player's queue. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - if not player.is_playing: - return await ctx.send('Nothing playing.') - - player.shuffle = not player.shuffle - await ctx.send('🔀 | Shuffle ' + ('enabled' if player.shuffle else 'disabled')) - - @commands.command(name='repeat', aliases=['loop']) - @commands.guild_only() - async def _repeat(self, ctx): - """ Repeats the current song until the command is invoked again. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_playing: - return await ctx.send('Nothing playing.') - - player.repeat = not player.repeat - await ctx.send('🔁 | Repeat ' + ('enabled' if player.repeat else 'disabled')) - - @commands.command(name='remove') - @commands.guild_only() - async def _remove(self, ctx, index: int): - """ Removes an item from the player's queue with the given index. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.queue: - return await ctx.send('Nothing queued.') - - if index > len(player.queue) or index < 1: - return await ctx.send(f'Index has to be **between** 1 and {len(player.queue)}') - - index -= 1 - removed = player.queue.pop(index) - - await ctx.send(f'Removed **{removed.title}** from the queue.') - - @commands.command(name='find') - @commands.guild_only() - async def _find(self, ctx, *, query): - """ Lists the first 10 search results from a given query. """ - await ctx.trigger_typing() - not_reacted = True - react_emoji = {1: "\u0030\u20E3", 2: "\u0031\u20E3", 3: "\u0032\u20E3", 4: "\u0033\u20E3", 5: "\u0034\u20E3", - 6: "\u0035\u20E3", 7: "\u0036\u20E3", 8: "\u0037\u20E3", 9: "\u0038\u20E3", 10: "\u0039\u20E3"} - - if not query.startswith('ytsearch:') and not query.startswith('scsearch:'): - query = 'ytsearch:' + query - - results = await self.bot.lavalink.get_tracks(query) - - if not results or not results['tracks']: - return await ctx.send('Nothing found') - - tracks = results['tracks'][:10] # First 10 results - - o = '' - for index, track in enumerate(tracks, start=1): - track_title = track["info"]["title"] - track_uri = track["info"]["uri"] - - o += f'{react_emoji[index]} [{track_title}]({track_uri})\n' - - embed_start = discord.Embed(color=0xEFD26C, description=o) - start_msg = await ctx.send(embed=embed_start) - await ctx.trigger_typing() - for num_index in range(min(len(tracks), 10)): - index = num_index + 1 - await start_msg.add_reaction(react_emoji[index]) - await start_msg.add_reaction("\u274C") - # React does shit - time_start = time.time() - - while not_reacted: - await ctx.trigger_typing() - embed = discord.Embed(color=0xEFD26C) - timer = time.time() - time_start - msg_id = await ctx.get_message(start_msg.id) - if int(timer) >= 10: - await start_msg.clear_reactions() - embed.title = "Sorry" - embed.description = "Timer expired" - await start_msg.edit(embed=embed) - ping = await ctx.send(content=f"{ctx.author.mention}") - await asyncio.sleep(10) - await ctx.message.delete() - await ping.delete() - await msg_id.delete() - break - for react in msg_id.reactions: - async for user in react.users(): - if user is ctx.author: - if react.emoji[:-1].isdigit(): - not_reacted = False - track_num = int(react.emoji[:-1]) - track_ = tracks[track_num] - await self.send_to_play(ctx, track_) - embed.title = "Song sent to queue" - thumb_url = await RoxUtils.ThumbNailer.identify(self, track_['info']['identifier'],track_['info']['uri']) - if thumb_url: - embed.set_thumbnail(url=thumb_url) - embed.description = f'[{track_["info"]["title"]}]({track_["info"]["uri"]})' - await start_msg.edit(embed=embed) - await start_msg.clear_reactions() - break - if react.emoji == "\u274C": - not_reacted = False - await start_msg.clear_reactions() - embed.title = "Sorry" - embed.description = "Search cancelled by user" - await start_msg.edit(embed=embed) - ping = await ctx.send(content=f"{user.mention}") - await asyncio.sleep(10) - await ctx.message.delete() - await ping.delete() - await msg_id.delete() - break - - @commands.command(name='disconnect', aliases=['dc']) - @commands.guild_only() - async def _disconnect(self, ctx): - """ Disconnects the player from the voice channel and clears its queue. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_connected: - return await ctx.send('Not connected.') - - if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): - return await ctx.send('You\'re not in my voicechannel!') - - player.queue.clear() - await player.disconnect() - await ctx.send('*⃣ | Disconnected.') - - async def send_to_play(self, ctx, track): - await self.ensure_voice_real(ctx) - player = self.bot.lavalink.players.get(ctx.guild.id) - player.add(requester=ctx.author.id, track=track) - if not player.is_playing: - await player.play() - - @_playnow.before_invoke - @_previous.before_invoke - @_play.before_invoke - async def ensure_voice(self, ctx): - await self.ensure_voice_real(ctx) - - async def ensure_voice_real(self, ctx): - """ A few checks to make sure the bot can join a voice channel. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - if not player.is_connected: - if not ctx.author.voice or not ctx.author.voice.channel: - await ctx.send('You aren\'t connected to any voice channel.') - raise commands.CommandInvokeError( - 'Author not connected to voice channel.') - - permissions = ctx.author.voice.channel.permissions_for(ctx.me) - - if not permissions.connect or not permissions.speak: - await ctx.send('Missing permissions `CONNECT` and/or `SPEAK`.') - raise commands.CommandInvokeError( - 'Bot has no permissions CONNECT and/or SPEAK') - - player.store('channel', ctx.channel.id) - await player.connect(ctx.author.voice.channel.id) - else: - if player.connected_channel.id != ctx.author.voice.channel.id: - return await ctx.send('Join my voice channel!') + def __init__(self, bot): + self.bot = bot + + if not hasattr(bot, 'lavalink'): + lavalink.Client(bot=bot, password='youshallnotpass', + loop=bot.loop, log_level=logging.INFO, + host=host, ws_port=ws_port, + rest_port=rest_port, + player=MixPlayer) + self.bot.lavalink.register_hook(self._track_hook) + + def __unload(self): + for guild_id, player in self.bot.lavalink.players: + self.bot.loop.create_task(player.disconnect()) + player.cleanup() + # Clear the players from Lavalink's internal cache + self.bot.lavalink.players.clear() + self.bot.lavalink.unregister_hook(self._track_hook) + + async def _track_hook(self, event): + if isinstance(event, lavalink.Events.StatsUpdateEvent): + return + channel = self.bot.get_channel(event.player.fetch('channel')) + if not channel: + return + + if isinstance(event, lavalink.Events.TrackStartEvent): + embed = discord.Embed(title='Now playing:', description=event.track.title, color=0xEFD26C) + thumbnail_url = await RoxUtils.ThumbNailer.identify(self, event.player.current.identifier, event.player.current.uri) + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + await channel.send(embed=embed) + elif isinstance(event, lavalink.Events.QueueEndEvent): + await channel.send('Queue ended! Why not queue more songs?') + + @commands.command(name='play', aliases=['p']) + @commands.guild_only() + async def _play(self, ctx, *, query: str): + """ Searches and plays a song from a given query. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + query = query.strip('<>') + + if not url_rx.match(query): + query = f'ytsearch:{query}' + + results = await self.bot.lavalink.get_tracks(query) + + if not results or not results['tracks']: + return await ctx.send('Nothing found!') + + embed = discord.Embed(color=0xEFD26C) + + if results['loadType'] == 'PLAYLIST_LOADED': + tracks = results['tracks'] + + for track in tracks: + player.add(requester=ctx.author.id, track=track) + + embed.title = 'Playlist Enqueued!' + embed.description = f'{results["playlistInfo"]["name"]} - {len(tracks)} tracks' + await ctx.send(embed=embed) + else: + track = results['tracks'][0] + embed.title = 'Track Enqueued' + thumbnail_url = await RoxUtils.ThumbNailer.identify(self, track['info']['identifier'], track['info']['uri']) + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + + embed.description = f'[{track["info"]["title"]}]({track["info"]["uri"]})' + await ctx.send(embed=embed) + player.add(requester=ctx.author.id, track=track) + + if not player.is_playing: + await player.play() + + @commands.command(name='previous', aliases=['pv']) + @commands.guild_only() + async def _previous(self, ctx): + """ Plays the previous song. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + try: + await player.play_previous() + except lavalink.NoPreviousTrack: + await ctx.send('There is no previous song to play.') + + @commands.command(name='playnow', aliases=['pn']) + @commands.guild_only() + async def _playnow(self, ctx, *, query: str): + """ Plays immediately a song. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.queue and not player.is_playing: + return await ctx.invoke(self._play, query=query) + + query = query.strip('<>') + + if not url_rx.match(query): + query = f'ytsearch:{query}' + + results = await self.bot.lavalink.get_tracks(query) + + if not results or not results['tracks']: + return await ctx.send('Nothing found!') + + tracks = results['tracks'] + track = tracks.pop(0) + + if results['loadType'] == 'PLAYLIST_LOADED': + for _track in tracks: + player.add(requester=ctx.author.id, track=_track) + + await player.play_now(requester=ctx.author.id, track=track) + + @commands.command(name='skipto', aliases=['st']) + @commands.guild_only() + @checks.is_DJ() + async def _skip_to(self, ctx, index: int): + """ Plays the queue from a specific point. Disregards tracks before the index. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + if index < 1: + return await ctx.send('Invalid specified index.') + if len(player.queue) < index: + return await ctx.send('This index exceeds the queue\'s length.') + await player.skip_to(index - 1) + + @commands.command(name='seek') + @commands.guild_only() + async def _seek(self, ctx, *, time: str): + """ Seeks to a given position in a track. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_playing: + return await ctx.send('Not playing.') + + seconds = time_rx.search(time) + if not seconds: + return await ctx.send('You need to specify the amount of seconds to skip!') + + seconds = int(seconds.group()) * 1000 + if time.startswith('-'): + seconds *= -1 + + track_time = player.position + seconds + await player.seek(track_time) + + await ctx.send(f'Moved track to **{lavalink.Utils.format_time(track_time)}**') + + @commands.command(name='skip', aliases=['forceskip', 'fs']) + @commands.guild_only() + async def _skip(self, ctx): + """ Skips the current track. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_playing: + return await ctx.send('Not playing.') + + await player.skip() + await ctx.send('⏭ | Skipped.') + + @commands.command(name='stop') + @commands.guild_only() + async def _stop(self, ctx): + """ Stops the player and clears its queue. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_playing: + return await ctx.send('Not playing.') + + player.queue.clear() + await player.stop() + await ctx.send('⏹ | Stopped.') + + @commands.command(name='now', aliases=['np', 'n', 'playing']) + @commands.guild_only() + async def _now(self, ctx): + """ Shows some stats about the currently playing song. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + song = 'Nothing' + + if player.current: + position = lavalink.Utils.format_time(player.position) + if player.current.stream: + duration = '🔴 LIVE' + else: + duration = lavalink.Utils.format_time(player.current.duration) + song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})' + + embed = discord.Embed(color=0xEFD26C, title='Now Playing', description=song) + + thumbnail_url = await RoxUtils.ThumbNailer.identify(self, player.current.identifier, player.current.uri) + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + + await ctx.send(embed=embed) + + @commands.command(name='queue', aliases=['q']) + @commands.guild_only() + async def _queue(self, ctx): + """ Shows the player's queue. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if player.queue.is_empty(): + return await ctx.send('There\'s nothing in the queue! Why not queue something?') + + queue = player.queue.get_queue() + scroller = QueueScroller(ctx, queue, lines_per_page=10) + await scroller.start_scrolling() + + @commands.command(name='myqueue', aliases=['mq']) + @commands.guild_only() + async def _myqueue(self, ctx): + """ Shows the player's queue. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if player.queue.is_empty(): + return await ctx.send('There\'s nothing in the queue! Why not queue something?') + + queue = player.queue.get_user_queue(ctx.author.id, dual=True) + if not queue: + return await ctx.send('You haven\'t queued anything') + + scroller = QueueScroller(ctx, queue, lines_per_page=10, user=ctx.author.name) + await scroller.start_scrolling() + + @commands.command(name='pause', aliases=['resume']) + @commands.guild_only() + async def _pause(self, ctx): + """ Pauses/Resumes the current track. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_playing: + return await ctx.send('Not playing.') + + if player.paused: + await player.set_pause(False) + await ctx.send('⏯ | Resumed') + else: + await player.set_pause(True) + await ctx.send('⏯ | Paused') + + @commands.command(name='volume', aliases=['vol']) + @commands.guild_only() + async def _volume(self, ctx, volume: int = None): + """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not volume: + return await ctx.send(f'🔈 | {player.volume}%') + + await player.set_volume(volume) + await ctx.send(f'🔈 | Set to {player.volume}%') + + @commands.command(name='shuffle') + @commands.guild_only() + async def _shuffle(self, ctx): + """ Shuffles the player's queue. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + if not player.is_playing: + return await ctx.send('Nothing playing.') + + player.shuffle = not player.shuffle + await ctx.send('🔀 | Shuffle ' + ('enabled' if player.shuffle else 'disabled')) + + @commands.command(name='repeat', aliases=['loop']) + @commands.guild_only() + async def _repeat(self, ctx): + """ Repeats the current song until the command is invoked again. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_playing: + return await ctx.send('Nothing playing.') + + player.repeat = not player.repeat + await ctx.send('🔁 | Repeat ' + ('enabled' if player.repeat else 'disabled')) + + @commands.command(name='remove') + @commands.guild_only() + async def _remove(self, ctx, index: int): + """ Removes an item from the player's queue with the given index. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.queue: + return await ctx.send('Nothing queued.') + + if index > len(player.queue) or index < 1: + return await ctx.send(f'Index has to be **between** 1 and {len(player.queue)}') + + index -= 1 + removed = player.queue.pop(index) + + await ctx.send(f'Removed **{removed.title}** from the queue.') + + @commands.command(name='find') + @commands.guild_only() + async def _find(self, ctx, *, query): + """ Lists the first 10 search results from a given query. """ + await ctx.trigger_typing() + not_reacted = True + react_emoji = {1: "\u0030\u20E3", 2: "\u0031\u20E3", 3: "\u0032\u20E3", 4: "\u0033\u20E3", 5: "\u0034\u20E3", + 6: "\u0035\u20E3", 7: "\u0036\u20E3", 8: "\u0037\u20E3", 9: "\u0038\u20E3", 10: "\u0039\u20E3"} + + if not query.startswith('ytsearch:') and not query.startswith('scsearch:'): + query = 'ytsearch:' + query + + results = await self.bot.lavalink.get_tracks(query) + + if not results or not results['tracks']: + return await ctx.send('Nothing found') + + tracks = results['tracks'][:10] # First 10 results + + o = '' + for index, track in enumerate(tracks, start=1): + track_title = track["info"]["title"] + track_uri = track["info"]["uri"] + + o += f'{react_emoji[index]} [{track_title}]({track_uri})\n' + + embed_start = discord.Embed(color=0xEFD26C, description=o) + start_msg = await ctx.send(embed=embed_start) + await ctx.trigger_typing() + for num_index in range(min(len(tracks), 10)): + index = num_index + 1 + await start_msg.add_reaction(react_emoji[index]) + await start_msg.add_reaction("\u274C") + # React does shit + time_start = time.time() + + while not_reacted: + await ctx.trigger_typing() + embed = discord.Embed(color=0xEFD26C) + timer = time.time() - time_start + msg_id = await ctx.get_message(start_msg.id) + if int(timer) >= 10: + await start_msg.clear_reactions() + embed.title = "Sorry" + embed.description = "Timer expired" + await start_msg.edit(embed=embed) + ping = await ctx.send(content=f"{ctx.author.mention}") + await asyncio.sleep(10) + await ctx.message.delete() + await ping.delete() + await msg_id.delete() + break + for react in msg_id.reactions: + async for user in react.users(): + if user is ctx.author: + if react.emoji[:-1].isdigit(): + not_reacted = False + track_num = int(react.emoji[:-1]) + track_ = tracks[track_num] + await self.send_to_play(ctx, track_) + embed.title = "Song sent to queue" + thumb_url = await RoxUtils.ThumbNailer.identify(self, track_['info']['identifier'],track_['info']['uri']) + if thumb_url: + embed.set_thumbnail(url=thumb_url) + embed.description = f'[{track_["info"]["title"]}]({track_["info"]["uri"]})' + await start_msg.edit(embed=embed) + await start_msg.clear_reactions() + break + if react.emoji == "\u274C": + not_reacted = False + await start_msg.clear_reactions() + embed.title = "Sorry" + embed.description = "Search cancelled by user" + await start_msg.edit(embed=embed) + ping = await ctx.send(content=f"{user.mention}") + await asyncio.sleep(10) + await ctx.message.delete() + await ping.delete() + await msg_id.delete() + break + + @commands.command(name='disconnect', aliases=['dc']) + @commands.guild_only() + async def _disconnect(self, ctx): + """ Disconnects the player from the voice channel and clears its queue. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_connected: + return await ctx.send('Not connected.') + + if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): + return await ctx.send('You\'re not in my voicechannel!') + + player.queue.clear() + await player.disconnect() + await ctx.send('*⃣ | Disconnected.') + + async def send_to_play(self, ctx, track): + await self.ensure_voice_real(ctx) + player = self.bot.lavalink.players.get(ctx.guild.id) + player.add(requester=ctx.author.id, track=track) + if not player.is_playing: + await player.play() + + @_playnow.before_invoke + @_previous.before_invoke + @_play.before_invoke + async def ensure_voice(self, ctx): + await self.ensure_voice_real(ctx) + + async def ensure_voice_real(self, ctx): + """ A few checks to make sure the bot can join a voice channel. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + + if not player.is_connected: + if not ctx.author.voice or not ctx.author.voice.channel: + await ctx.send('You aren\'t connected to any voice channel.') + raise commands.CommandInvokeError( + 'Author not connected to voice channel.') + + permissions = ctx.author.voice.channel.permissions_for(ctx.me) + + if not permissions.connect or not permissions.speak: + await ctx.send('Missing permissions `CONNECT` and/or `SPEAK`.') + raise commands.CommandInvokeError( + 'Bot has no permissions CONNECT and/or SPEAK') + + player.store('channel', ctx.channel.id) + await player.connect(ctx.author.voice.channel.id) + else: + if player.connected_channel.id != ctx.author.voice.channel.id: + return await ctx.send('Join my voice channel!') def setup(bot): diff --git a/cogs/utils/embedscroller.py b/cogs/utils/embedscroller.py new file mode 100644 index 0000000..9aa1f8a --- /dev/null +++ b/cogs/utils/embedscroller.py @@ -0,0 +1,190 @@ +import asyncio +import discord +import math +import lavalink + +class CantScroll(Exception): + pass + +# Maybe slightly very copied from rdanny +# Thanks danny <3 + +class EmbedScroller: + def __init__(self, ctx, pages): + + self.bot = ctx.bot + self.pages = pages + self.message = ctx.message + self.channel = ctx.channel + self.author = ctx.author + + if len(self.pages) > 1: + self.scrolling = True + else: + self.scrolling = False + self.reaction_emojis = [ + ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page), + ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), + ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), + ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page), + ('❌', self.stop_scrolling), + ] + + if ctx.guild is not None: + self.permissions = self.channel.permissions_for(ctx.guild.me) + else: + self.permissions = self.channel.permissions_for(ctx.bot.user) + + if not self.permissions.embed_links: + raise CantScroll('Bot does not have embed links permission.') + + if not self.permissions.send_messages: + raise CantScroll('Bot cannot send messages.') + + if self.scrolling: + # verify we can actually use the pagination session + if not self.permissions.add_reactions: + raise CantScroll('Bot does not have add reactions permission.') + + if not self.permissions.read_message_history: + raise CantScroll('Bot does not have Read Message History permission.') + + async def send(self): + self.current_page = 0 + # No embeds to scroll through + if not self.pages: + return + + if not self.scrolling: + return await self.channel.send(embed=self.pages[0]) + + self.message = await self.channel.send(embed=self.pages[0]) + for (reaction, _) in self.reaction_emojis: + if len(self.pages) == 2 and reaction in ('\u23ed', '\u23ee'): + continue + + await self.message.add_reaction(reaction) + + async def scroll(self, page): + if page < 0 or page >= len(self.pages): + return + self.current_page = page + await self.message.edit(embed=self.pages[page]) + + async def first_page(self): + await self.scroll(0) + + async def last_page(self): + await self.scroll(len(self.pages) - 1) + + async def next_page(self): + await self.scroll(self.current_page + 1) + + async def previous_page(self): + await self.scroll(self.current_page - 1) + + async def stop_scrolling(self): + self.scrolling = False + await self.message.delete() + + def react_check(self, reaction, user): + if user is None or user.id != self.author.id: + return False + + if reaction.message.id != self.message.id: + return False + + for (emoji, func) in self.reaction_emojis: + if reaction.emoji == emoji: + self.match = func + return True + return False + + async def start_scrolling(self): + """Actually paginate the entries and run the interactive loop if necessary.""" + if not self.scrolling: + await self.send() + else: + # allow us to react to reactions right away if we're paginating + self.bot.loop.create_task(self.send()) + + while self.scrolling: + try: + reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0) + except asyncio.TimeoutError: + self.scrolling = False + try: + await self.message.clear_reactions() + except: + pass + finally: + break + + try: + await self.message.remove_reaction(reaction, user) + except: + pass # can't remove it so don't bother doing so + + await self.match() + + +class ScrollerFromLines(EmbedScroller): + def __init__(self, ctx, description, lines, lines_per_page=10): + pagecount = math.ceil(len(lines) / lines_per_page) + pages = [] + page = '' + for index, line in enumerate(lines): + page += line + '\n' + if index%lines_per_page == lines_per_page - 1: + embed = discord.Embed(color=0xEFD26C, + title=description, + description=page) + embed.set_footer(text=f'Viewing page {index//lines_per_page}/{pagecount}') + pages.append(embed) + page = '' + + if page != '': + embed = discord.Embed(color=0xEFD26C, + description=page) + + embed.set_footer(text=f'Viewing page {pagecount}/{pagecount}') + pages.append(embed) + + super().__init__(ctx=ctx, pages=pages) + + +class QueueScroller(EmbedScroller): + def __init__(self, ctx, queue, lines_per_page=10, user: str=None): + + pagecount = math.ceil(len(queue) / lines_per_page) + + if user is None: + title = f'**Queue** `{len(queue)} songs`' + else: + title = f'**{user}\'s queue** `{len(queue)} songs`' + pages = [] + page = '' + for index, temp in enumerate(queue): + if user is None: + track = temp + page += f'`{index + 1}.` **[{track.title}]({track.uri})** _by <@{track.requester}>_\n' + else: + track, globpos = temp + page += f'`{index + 1}({globpos + 1}).` **[{track.title}]({track.uri})** _by <@{track.requester}>_\n' + + if index%lines_per_page == lines_per_page - 1: + embed = discord.Embed(color=0xEFD26C, + title=title, + description=page) + embed.set_footer(text=f'Viewing page {index//lines_per_page + 1}/{pagecount}') + pages.append(embed) + page = '' + + if page != '': + embed = discord.Embed(color=0xEFD26C, + title=title, + description=page) + embed.set_footer(text=f'Viewing page {pagecount}/{pagecount}') + pages.append(embed) + + super().__init__(ctx=ctx, pages=pages) diff --git a/cogs/utils/mixplayer/mixqueue.py b/cogs/utils/mixplayer/mixqueue.py index 41da5b6..a154d35 100644 --- a/cogs/utils/mixplayer/mixqueue.py +++ b/cogs/utils/mixplayer/mixqueue.py @@ -30,7 +30,7 @@ class MixQueue: return tmp def __bool__(self): - return self.is_empty() + return not self.is_empty() def __iter__(self): global_queue = roundrobin(*[x for x in self.queues.values()]) @@ -54,10 +54,10 @@ class MixQueue: self.queues = OrderedDict() self.priority_queue = [] - # if pos is true also returns global positions of tracks - def get_user_queue(self, requester: int, pos: bool=False): + # if dual is true also returns global positions of tracks + def get_user_queue(self, requester: int, dual: bool=False): queue = self.queues.get(requester, []) - if pos and queue: + if dual and queue: pos = [self._loc_to_glob(requester, i) for i in range(len(queue))] combined = zip(queue, pos) return list(combined) From e3f9642e32fb50eb95a54189ec12a9aab96b55ec Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Fri, 11 Jan 2019 17:01:33 +0100 Subject: [PATCH 2/3] Interface for remove, shuffle and move. --- cogs/music.py | 137 ++++++++++++++++++++---------- cogs/utils/embedscroller.py | 15 ++-- cogs/utils/mixplayer/mixplayer.py | 16 ++-- cogs/utils/mixplayer/mixqueue.py | 13 ++- 4 files changed, 122 insertions(+), 59 deletions(-) diff --git a/cogs/music.py b/cogs/music.py index 63d9065..029a27c 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -9,20 +9,17 @@ Because of the F-Strings, you also must have Python 3.6 or higher installed. import logging import math import re - +import typing import time + import asyncio -from cogs.utils import RoxUtils -from .utils.mixplayer.mixplayer import MixPlayer -from .utils import checks - import discord -import lavalink from discord.ext import commands +import lavalink +from .utils import checks, RoxUtils from .utils.mixplayer.mixplayer import MixPlayer -from .utils import checks -from .utils.embedscroller import EmbedScroller, ScrollerFromLines, QueueScroller +from .utils.embedscroller import QueueScroller from lavasettings import * time_rx = re.compile('[0-9]+') @@ -63,7 +60,7 @@ class Music: embed.set_thumbnail(url=thumbnail_url) await channel.send(embed=embed) elif isinstance(event, lavalink.Events.QueueEndEvent): - await channel.send('Queue ended! Why not queue more songs?') + await channel.send('Queue ended.') @commands.command(name='play', aliases=['p']) @commands.guild_only() @@ -229,31 +226,36 @@ class Music: @commands.command(name='queue', aliases=['q']) @commands.guild_only() - async def _queue(self, ctx): - """ Shows the player's queue. """ + async def _queue(self, ctx, user: discord.Member=None): + """ Shows the global queue or another users queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) - if player.queue.is_empty(): - return await ctx.send('There\'s nothing in the queue! Why not queue something?') + return await ctx.send('The queue is empty') + + if user is None: + queue = player.global_queue() + scroller = QueueScroller(ctx, queue, lines_per_page=10) + await scroller.start_scrolling() - queue = player.queue.get_queue() - scroller = QueueScroller(ctx, queue, lines_per_page=10) - await scroller.start_scrolling() + else: + user_queue = player.user_queue(user.id, dual=True) + if not user_queue: + return await ctx.send(f'{user.name}\'s queue is empty') + + scroller = QueueScroller(ctx, user_queue, lines_per_page=10, user_name=user.name) + await scroller.start_scrolling() @commands.command(name='myqueue', aliases=['mq']) @commands.guild_only() async def _myqueue(self, ctx): - """ Shows the player's queue. """ + """ Shows your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) - if player.queue.is_empty(): - return await ctx.send('There\'s nothing in the queue! Why not queue something?') + user_queue = player.user_queue(ctx.author.id, dual=True) + if not user_queue: + return await ctx.send('Your queue is empty') - queue = player.queue.get_user_queue(ctx.author.id, dual=True) - if not queue: - return await ctx.send('You haven\'t queued anything') - - scroller = QueueScroller(ctx, queue, lines_per_page=10, user=ctx.author.name) + scroller = QueueScroller(ctx, user_queue, lines_per_page=10, user_name=ctx.author.name) await scroller.start_scrolling() @commands.command(name='pause', aliases=['resume']) @@ -287,42 +289,87 @@ class Music: @commands.command(name='shuffle') @commands.guild_only() async def _shuffle(self, ctx): - """ Shuffles the player's queue. """ + """ Shuffles your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) - if not player.is_playing: - return await ctx.send('Nothing playing.') + user_queue = player.user_queue(ctx.author.id) - player.shuffle = not player.shuffle - await ctx.send('🔀 | Shuffle ' + ('enabled' if player.shuffle else 'disabled')) + if not user_queue: + return await ctx.send('Your queue is empty') - @commands.command(name='repeat', aliases=['loop']) + player.shuffle_user_queue(ctx.author.id) + await ctx.send('Your queue has been shuffled') + + @commands.command(name='move', aliases=["m"]) @commands.guild_only() - async def _repeat(self, ctx): - """ Repeats the current song until the command is invoked again. """ + async def _move(self, ctx, from_pos: int, to_pos: int): + """ Moves a song in your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) - if not player.is_playing: - return await ctx.send('Nothing playing.') + user_queue = player.user_queue(ctx.author.id) + if not user_queue: + return await ctx.send('Your queue is empty') - player.repeat = not player.repeat - await ctx.send('🔁 | Repeat ' + ('enabled' if player.repeat else 'disabled')) + if not all(x in range(1,len(user_queue)+1) for x in [from_pos, to_pos]): + return await ctx.send(f'Positions must be between 1 and {len(user_queue)}') + + moved = player.move_user_track(ctx.author.id, from_pos - 1, to_pos - 1) + if moved is None: + return await ctx.send('Check that the positions exist in your queue') + + await ctx.send(f'{moved.title} moved from position {from_pos} to {to_pos} in your queue') @commands.command(name='remove') @commands.guild_only() - async def _remove(self, ctx, index: int): - """ Removes an item from the player's queue with the given index. """ + async def _remove(self, ctx, pos: int): + """ Removes the song at pos from your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) - if not player.queue: - return await ctx.send('Nothing queued.') + if player.queue.is_empty(): + return - if index > len(player.queue) or index < 1: - return await ctx.send(f'Index has to be **between** 1 and {len(player.queue)}') + user_queue = player.user_queue(ctx.author.id) + if not user_queue: + return - index -= 1 - removed = player.queue.pop(index) + if pos > len(user_queue) or pos < 1: + return await ctx.send(f'Position has to be between 1 and {len(user_queue)}') + + removed = player.remove_user_track(ctx.author.id, pos - 1) + + await ctx.send(f'**{removed.title}** removed.') + + @commands.command(name="DJremove") + @commands.guild_only() + @checks.is_DJ() + async def _djremove(self, ctx, pos: int, user: discord.Member=None): + """ Remove a song from either the global queue or a users queue""" + player = self.bot.lavalink.players.get(ctx.guild.id) + + if user is None: + if player.queue.is_empty(): + return + + if pos > len(player.queue) or pos < 1: + return await ctx.send(f'Position has to be between 1 and {len(player.queue)}') + + removed = player.remove_global_track(pos - 1) + requester = self.bot.get_user(removed.requester) + await ctx.send(f'**{removed.title}** queued by {requester.name} removed.') + + else: + if player.queue.is_empty(): + return + + user_queue = player.user_queue(user.id) + if not user_queue: + return + + if pos > len(user_queue) or pos < 1: + return await ctx.send(f'Position has to be between 1 and {len(user_queue)}') + + removed = player.remove_user_track(user.id, pos - 1) + await ctx.send(f'**{removed.title}** queued by {user.name} removed.') - await ctx.send(f'Removed **{removed.title}** from the queue.') @commands.command(name='find') @commands.guild_only() diff --git a/cogs/utils/embedscroller.py b/cogs/utils/embedscroller.py index 9aa1f8a..134636f 100644 --- a/cogs/utils/embedscroller.py +++ b/cogs/utils/embedscroller.py @@ -14,6 +14,7 @@ class EmbedScroller: self.bot = ctx.bot self.pages = pages + self.cmdmsg = ctx.message self.message = ctx.message self.channel = ctx.channel self.author = ctx.author @@ -86,6 +87,7 @@ class EmbedScroller: async def stop_scrolling(self): self.scrolling = False await self.message.delete() + await self.cmdmsg.delete() def react_check(self, reaction, user): if user is None or user.id != self.author.id: @@ -101,11 +103,9 @@ class EmbedScroller: return False async def start_scrolling(self): - """Actually paginate the entries and run the interactive loop if necessary.""" if not self.scrolling: await self.send() else: - # allow us to react to reactions right away if we're paginating self.bot.loop.create_task(self.send()) while self.scrolling: @@ -123,7 +123,7 @@ class EmbedScroller: try: await self.message.remove_reaction(reaction, user) except: - pass # can't remove it so don't bother doing so + pass await self.match() @@ -154,18 +154,19 @@ class ScrollerFromLines(EmbedScroller): class QueueScroller(EmbedScroller): - def __init__(self, ctx, queue, lines_per_page=10, user: str=None): + """ Fugly, but works for now """ + def __init__(self, ctx, queue, lines_per_page=10, user_name: str=None): pagecount = math.ceil(len(queue) / lines_per_page) - if user is None: + if user_name is None: title = f'**Queue** `{len(queue)} songs`' else: - title = f'**{user}\'s queue** `{len(queue)} songs`' + title = f'**{user_name}\'s queue** `{len(queue)} songs`' pages = [] page = '' for index, temp in enumerate(queue): - if user is None: + if user_name is None: track = temp page += f'`{index + 1}.` **[{track.title}]({track.uri})** _by <@{track.requester}>_\n' else: diff --git a/cogs/utils/mixplayer/mixplayer.py b/cogs/utils/mixplayer/mixplayer.py index 8af8ecb..cb3a1fb 100644 --- a/cogs/utils/mixplayer/mixplayer.py +++ b/cogs/utils/mixplayer/mixplayer.py @@ -79,7 +79,7 @@ class MixPlayer(BasePlayer): def add(self, requester: int, track: dict): """ Adds a track to the queue. """ - self.queue.add_track(requester, AudioTrack().build(track, requester)) + return self.queue.add_track(requester, AudioTrack().build(track, requester)) def add_next(self, requester: int, track: dict): """ Adds a track to beginning of the queue """ @@ -87,24 +87,30 @@ class MixPlayer(BasePlayer): def add_at(self, index: int, requester: int, track: dict): """ Adds a track at a specific index in the queue. """ - self.queue.add_track(requester, AudioTrack().build(track, requester), index) + return self.queue.add_track(requester, AudioTrack().build(track, requester), index) def move_user_track(self, requester: int, initial: int, final: int): """ Moves a track in a users queue""" - self.queue.move_user_track(requester, initial, final) + return self.queue.move_user_track(requester, initial, final) def remove_user_track(self, requester: int, pos: int): """ Removes the song at from the queue of requester """ - self.queue.remove_user_track(requester, pos) + return self.queue.remove_user_track(requester, pos) def remove_global_track(self, pos: int): """ Removes the song at in the global queue """ - self.queue.remove_global_track(pos) + return self.queue.remove_global_track(pos) def shuffle_user_queue(self, requester: int): """ Randomly reorders the queue of requester """ self.queue.shuffle_user_queue(requester) + def user_queue(self, user: int, dual: bool=False): + return self.queue.get_user_queue(user, dual) + + def global_queue(self): + return self.queue.get_queue() + async def play(self, track_index: int = 0): """ Plays the first track in the queue, if any or plays a track from the specified index in the queue. """ diff --git a/cogs/utils/mixplayer/mixqueue.py b/cogs/utils/mixplayer/mixqueue.py index a154d35..01fa232 100644 --- a/cogs/utils/mixplayer/mixqueue.py +++ b/cogs/utils/mixplayer/mixqueue.py @@ -79,10 +79,16 @@ class MixQueue: user_queue = self.queues.get(requester) if user_queue is None: self.queues[requester] = [track] + localpos = 0 elif pos is None: user_queue.append(track) + localpos = len(user_queue) - 1 else: user_queue.insert(pos, track) + localpos = pos + + # Return info about track position + return track, self._loc_to_glob(requester, localpos), localpos def add_next_track(self, track: AudioTrack): self.priority_queue.append(track) @@ -91,16 +97,18 @@ class MixQueue: user_queue = self.queues.get(requester) if user_queue is not None: if pos < len(user_queue): - user_queue.pop(pos) + track = user_queue.pop(pos) self._clear_empty() + return track def remove_global_track(self, pos: int): q, pos = self._glob_to_loc(pos) if q is None or pos is None: return queue = self.queues.get(q) - queue.pop(pos) + track = queue.pop(pos) self._clear_empty() + return track def move_user_track(self, requester: int, initial: int, final: int): queue = self.queues.get(requester, []) @@ -108,6 +116,7 @@ class MixQueue: try: track = queue.pop(initial) queue.insert(final, track) + return track except IndexError: pass From d473be5d21c00c32e8c3f3d66fd7b64cb8a09bd7 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Sun, 13 Jan 2019 17:39:03 +0100 Subject: [PATCH 3/3] Vote skipping, commands locked behind DJ. --- bot.py | 22 +++---- cogs/music.py | 99 +++++++++++++++++++++---------- cogs/utils/RoxUtils.py | 68 ++++++++++----------- cogs/utils/mixplayer/mixplayer.py | 61 +++++++++++++++++++ cogs/utils/mixplayer/mixqueue.py | 1 + 5 files changed, 176 insertions(+), 75 deletions(-) diff --git a/bot.py b/bot.py index 944cbb7..f475c2b 100644 --- a/bot.py +++ b/bot.py @@ -10,22 +10,24 @@ from cogs.utils.settings import Settings initial_extension = [ - 'cogs.cogs', - 'cogs.botsettings', - 'cogs.misc', - 'cogs.music' + 'cogs.cogs', + 'cogs.botsettings', + 'cogs.misc', + 'cogs.music' ] with codecs.open("config.json", 'r', encoding='utf8') as f: - config = json.load(f) + config = json.load(f) + +default_prefix = config["default_prefix"] def _get_prefix(bot, message): - if not message.guild: - return default_prefix - prefixes = bot.settings.get_prefix(message.guild.id) - return commands.when_mentioned_or(*prefixes)(bot, message) + if not message.guild: + return default_prefix + prefixes = bot.settings.get_prefix(message.guild.id) + return commands.when_mentioned_or(*prefixes)(bot, message) class Bot(commands.Bot): @@ -101,6 +103,6 @@ def run_bot(debug: bool=False): if __name__ == '__main__': if 'debug' in sys.argv: - run_bot(debug=True) + run_bot(debug=True) else: run_bot(debug=False) diff --git a/cogs/music.py b/cogs/music.py index 029a27c..dd61075 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -28,6 +28,7 @@ url_rx = re.compile('https?:\/\/(?:www\.)?.+') class Music: def __init__(self, bot): + self.bot = bot if not hasattr(bot, 'lavalink'): @@ -46,6 +47,10 @@ class Music: self.bot.lavalink.players.clear() self.bot.lavalink.unregister_hook(self._track_hook) + async def __local_check(self, ctx): + is_guild = ctx.guild is not None + return is_guild + async def _track_hook(self, event): if isinstance(event, lavalink.Events.StatsUpdateEvent): return @@ -63,7 +68,6 @@ class Music: await channel.send('Queue ended.') @commands.command(name='play', aliases=['p']) - @commands.guild_only() async def _play(self, ctx, *, query: str): """ Searches and plays a song from a given query. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -103,19 +107,7 @@ class Music: if not player.is_playing: await player.play() - @commands.command(name='previous', aliases=['pv']) - @commands.guild_only() - async def _previous(self, ctx): - """ Plays the previous song. """ - player = self.bot.lavalink.players.get(ctx.guild.id) - - try: - await player.play_previous() - except lavalink.NoPreviousTrack: - await ctx.send('There is no previous song to play.') - @commands.command(name='playnow', aliases=['pn']) - @commands.guild_only() async def _playnow(self, ctx, *, query: str): """ Plays immediately a song. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -143,11 +135,14 @@ class Music: await player.play_now(requester=ctx.author.id, track=track) @commands.command(name='skipto', aliases=['st']) - @commands.guild_only() @checks.is_DJ() async def _skip_to(self, ctx, index: int): """ Plays the queue from a specific point. Disregards tracks before the index. """ player = self.bot.lavalink.players.get(ctx.guild.id) + + if ctx.author not in player.listeners: + return await ctx.send('You have to be listening to the bot') + if index < 1: return await ctx.send('Invalid specified index.') if len(player.queue) < index: @@ -155,7 +150,7 @@ class Music: await player.skip_to(index - 1) @commands.command(name='seek') - @commands.guild_only() + @checks.is_DJ() async def _seek(self, ctx, *, time: str): """ Seeks to a given position in a track. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -163,6 +158,9 @@ class Music: if not player.is_playing: return await ctx.send('Not playing.') + if ctx.author not in player.listeners: + return await ctx.send('You have to be listening to the bot') + seconds = time_rx.search(time) if not seconds: return await ctx.send('You need to specify the amount of seconds to skip!') @@ -177,7 +175,6 @@ class Music: await ctx.send(f'Moved track to **{lavalink.Utils.format_time(track_time)}**') @commands.command(name='skip', aliases=['forceskip', 'fs']) - @commands.guild_only() async def _skip(self, ctx): """ Skips the current track. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -185,15 +182,25 @@ class Music: if not player.is_playing: return await ctx.send('Not playing.') - await player.skip() - await ctx.send('⏭ | Skipped.') + player.add_skipper(ctx.author) + total = len(player.listeners) + skips = len(player.skip_voters) + if skips >= math.ceil(total/2) or player.current.requester == ctx.author.id: + await player.skip() + await ctx.send('Skipped.') + else: + if skips != 0: + await ctx.send(f'{skips} out of {math.ceil(total/2)} required haters have voted to skip.') @commands.command(name='stop') - @commands.guild_only() + @checks.is_DJ() async def _stop(self, ctx): """ Stops the player and clears its queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + if ctx.author not in player.listeners: + return await ctx.send('You have to be listening to the bot') + if not player.is_playing: return await ctx.send('Not playing.') @@ -202,9 +209,12 @@ class Music: await ctx.send('⏹ | Stopped.') @commands.command(name='now', aliases=['np', 'n', 'playing']) - @commands.guild_only() async def _now(self, ctx): """ Shows some stats about the currently playing song. """ + + #def scrubber(song, length): + + player = self.bot.lavalink.players.get(ctx.guild.id) song = 'Nothing' @@ -225,7 +235,6 @@ class Music: await ctx.send(embed=embed) @commands.command(name='queue', aliases=['q']) - @commands.guild_only() async def _queue(self, ctx, user: discord.Member=None): """ Shows the global queue or another users queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -246,7 +255,6 @@ class Music: await scroller.start_scrolling() @commands.command(name='myqueue', aliases=['mq']) - @commands.guild_only() async def _myqueue(self, ctx): """ Shows your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -259,7 +267,6 @@ class Music: await scroller.start_scrolling() @commands.command(name='pause', aliases=['resume']) - @commands.guild_only() async def _pause(self, ctx): """ Pauses/Resumes the current track. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -275,7 +282,7 @@ class Music: await ctx.send('⏯ | Paused') @commands.command(name='volume', aliases=['vol']) - @commands.guild_only() + @checks.is_DJ() async def _volume(self, ctx, volume: int = None): """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -286,8 +293,23 @@ class Music: await player.set_volume(volume) await ctx.send(f'🔈 | Set to {player.volume}%') + @commands.command(name='boost', aliases=['boo']) + async def _boost(self, ctx, boost: bool=None): + """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ + player = self.bot.lavalink.players.get(ctx.guild.id) + if boost is None: + if player.boosted: + await ctx.send('Bass boost is on') + else: + await ctx.send('Bass boost is off') + else: + await player.bassboost(boost) + if boost: + await ctx.send('Bass boost turned on') + else: + await ctx.send('Bass boost turned off') + @commands.command(name='shuffle') - @commands.guild_only() async def _shuffle(self, ctx): """ Shuffles your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -300,7 +322,6 @@ class Music: await ctx.send('Your queue has been shuffled') @commands.command(name='move', aliases=["m"]) - @commands.guild_only() async def _move(self, ctx, from_pos: int, to_pos: int): """ Moves a song in your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -319,7 +340,6 @@ class Music: await ctx.send(f'{moved.title} moved from position {from_pos} to {to_pos} in your queue') @commands.command(name='remove') - @commands.guild_only() async def _remove(self, ctx, pos: int): """ Removes the song at pos from your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -339,7 +359,6 @@ class Music: await ctx.send(f'**{removed.title}** removed.') @commands.command(name="DJremove") - @commands.guild_only() @checks.is_DJ() async def _djremove(self, ctx, pos: int, user: discord.Member=None): """ Remove a song from either the global queue or a users queue""" @@ -372,7 +391,6 @@ class Music: @commands.command(name='find') - @commands.guild_only() async def _find(self, ctx, *, query): """ Lists the first 10 search results from a given query. """ await ctx.trigger_typing() @@ -453,7 +471,6 @@ class Music: break @commands.command(name='disconnect', aliases=['dc']) - @commands.guild_only() async def _disconnect(self, ctx): """ Disconnects the player from the voice channel and clears its queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) @@ -476,7 +493,6 @@ class Music: await player.play() @_playnow.before_invoke - @_previous.before_invoke @_play.before_invoke async def ensure_voice(self, ctx): await self.ensure_voice_real(ctx) @@ -500,10 +516,31 @@ class Music: player.store('channel', ctx.channel.id) await player.connect(ctx.author.voice.channel.id) + + # Get listeners + voice_channel = ctx.author.voice.channel + while not player.is_connected: + await asyncio.sleep(1) + + for member in voice_channel.members: + if not member.bot: + player.update_listeners(member, member.voice) + + else: if player.connected_channel.id != ctx.author.voice.channel.id: return await ctx.send('Join my voice channel!') + async def on_voice_state_update(self, member, before, after): + if not member.bot: + if after.channel is not None: + player = self.bot.lavalink.players.get(after.channel.guild.id) + player.update_listeners(member, after) + else: + player = self.bot.lavalink.players.get(before.channel.guild.id) + player.update_listeners(member) + + def setup(bot): bot.add_cog(Music(bot)) diff --git a/cogs/utils/RoxUtils.py b/cogs/utils/RoxUtils.py index e63073b..6cf7ab8 100644 --- a/cogs/utils/RoxUtils.py +++ b/cogs/utils/RoxUtils.py @@ -5,44 +5,44 @@ from bs4 import BeautifulSoup as BS class ThumbNailer(object): - def __init__(self): - self.url = self + def __init__(self): + self.url = self - async def connection(self, url): - async with aiohttp.ClientSession() as session: - async with session.get(url, timeout=30) as response: - assert response.status == 200 - html = await response.read() - return ThumbNailer.__parse_result(html) + async def connection(self, url): + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=30) as response: + assert response.status == 200 + html = await response.read() + return ThumbNailer.__parse_result(html) - def __parse_result(html): - try: - soup = BS(html, 'html.parser') - img = soup.find("meta", property="twitter:image")["content"] - return img - except Exception as e: - raise e + def __parse_result(html): + try: + soup = BS(html, 'html.parser') + img = soup.find("meta", property="twitter:image")["content"] + return img + except Exception as e: + raise e - async def identify(self, identifier, uri): - thumbnail_url = "" - if "youtube" in uri: - thumbnail_url = f"https://img.youtube.com/vi/{identifier}/0.jpg" - return thumbnail_url - elif "soundcloud" in uri: - thumbnail_url = await ThumbNailer.connection(ThumbNailer, url=uri) - return thumbnail_url - else: - return None + async def identify(self, identifier, uri): + thumbnail_url = "" + if "youtube" in uri: + thumbnail_url = f"https://img.youtube.com/vi/{identifier}/0.jpg" + return thumbnail_url + elif "soundcloud" in uri: + thumbnail_url = await ThumbNailer.connection(ThumbNailer, url=uri) + return thumbnail_url + else: + return None def thumbnailer(self, identifier, uri): - thumbnail_url = "" - if "youtube" in uri: - thumbnail_url = f"https://img.youtube.com/vi/{identifier}/0.jpg" - return thumbnail_url - elif "soundcloud" in uri: - thumbnail_url = BS(requests.get(uri).text, "html.parser").find("meta", property="twitter:image")["content"] - return thumbnail_url - else: - return None + thumbnail_url = "" + if "youtube" in uri: + thumbnail_url = f"https://img.youtube.com/vi/{identifier}/0.jpg" + return thumbnail_url + elif "soundcloud" in uri: + thumbnail_url = BS(requests.get(uri).text, "html.parser").find("meta", property="twitter:image")["content"] + return thumbnail_url + else: + return None diff --git a/cogs/utils/mixplayer/mixplayer.py b/cogs/utils/mixplayer/mixplayer.py index cb3a1fb..697e9fc 100644 --- a/cogs/utils/mixplayer/mixplayer.py +++ b/cogs/utils/mixplayer/mixplayer.py @@ -3,6 +3,7 @@ from .mixqueue import MixQueue from abc import ABC, abstractmethod from random import randrange +import lavalink from lavalink import BasePlayer from lavalink.AudioTrack import AudioTrack from lavalink.Events import QueueEndEvent, TrackExceptionEvent, TrackEndEvent, TrackStartEvent, TrackStuckEvent @@ -29,6 +30,10 @@ class MixPlayer(BasePlayer): self.queue = MixQueue() self.current = None + self.listeners = set() + self.skip_voters = set() + self.boosted = False + @property def is_playing(self): """ Returns the player's track state. """ @@ -111,6 +116,18 @@ class MixPlayer(BasePlayer): def global_queue(self): return self.queue.get_queue() + def queue_duration(self, include_current: bool=True): + duration = 0 + for track in self.queue: + duration += track.duration + remaining = self.current.duration - self.position + if include_current: + return lavalink.Utils.format_time(duration + remaining) + return lavalink.Utils.format_time(duration) + + def current_pos(self): + return lavalink.Utils.format_time(self.position) + async def play(self, track_index: int = 0): """ Plays the first track in the queue, if any or plays a track from the specified index in the queue. """ @@ -146,6 +163,7 @@ class MixPlayer(BasePlayer): async def skip(self): """ Plays the next track in the queue, if any. """ + self.skip_voters.clear() await self.play() async def set_pause(self, pause: bool): @@ -170,3 +188,46 @@ class MixPlayer(BasePlayer): if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \ isinstance(event, TrackEndEvent) and event.reason == 'FINISHED': await self.play() + + def update_listeners(self, member, voice_state=None): + if self.is_connected: + vc = self.connected_channel + if voice_state is None or vc != voice_state.channel: + self.listeners.discard(member) + self.skip_voters.discard(member) + else: + if voice_state.deaf or voice_state.self_deaf: + self.listeners.discard(member) + self.skip_voters.discard(member) + else: + self.listeners.add(member) + + + def add_skipper(self, member): + if member in self.listeners: + self.skip_voters.add(member) + + + async def bassboost(self, boost: bool=False): + if boost: + boostval = 1 + else: + boostval = 0 + + bands = [ + {"band": 0, "gain": boostval * 0.15}, # 25Hz + {"band": 1, "gain": boostval * 0.15}, # 40Hz + {"band": 2, "gain": boostval * 0.25}, # 63Hz + {"band": 3, "gain": boostval * 0.15}, #100Hz + {"band": 4, "gain": boostval * -0.15}, #160Hz + {"band": 5, "gain": boostval * -0.1}, #250Hz + {"band": 6, "gain": boostval * -0.05}, #400Hz + ] + + if boost: + await self._lavalink.ws.send(op="equalizer", guildId=self.guild_id, bands=bands) + self.boosted = True + else: + await self._lavalink.ws.send(op="equalizer", guildId=self.guild_id, bands=bands) + self.boosted = False + diff --git a/cogs/utils/mixplayer/mixqueue.py b/cogs/utils/mixplayer/mixqueue.py index 01fa232..22540ca 100644 --- a/cogs/utils/mixplayer/mixqueue.py +++ b/cogs/utils/mixplayer/mixqueue.py @@ -3,6 +3,7 @@ from itertools import cycle, islice, chain from random import shuffle from lavalink import AudioTrack + def roundrobin(*iterables): "roundrobin('ABC', 'D', 'EF') --> A D E B F C" # Recipe credited to George Sakkis