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