diff --git a/bot.py b/bot.py index ff1aeaf..29ff527 100644 --- a/bot.py +++ b/bot.py @@ -3,6 +3,8 @@ import os import codecs import discord import time +import sys +import traceback from discord.ext import commands from cogs.utils.settings import Settings @@ -28,11 +30,12 @@ def _get_prefix(bot, message): class Bot(commands.Bot): - def __init__(self): + def __init__(self, debug: bool=False): super().__init__(command_prefix=_get_prefix, description=config["description"]) self.settings = Settings(**config) + self.debug = debug for extension in initial_extension: try: @@ -41,33 +44,40 @@ class Bot(commands.Bot): print(e) async def on_command_error(self, ctx, err): - if (isinstance(err, commands.MissingRequiredArgument) or - isinstance(err, commands.BadArgument)): - formatter = ctx.bot.formatter - if ctx.invoked_subcommand is None: - _help = await formatter.format_help_for(ctx, ctx.command) - else: - _help = await formatter.format_help_for(ctx, - ctx.invoked_subcommand) + if not self.debug: + if (isinstance(err, commands.MissingRequiredArgument) or + isinstance(err, commands.BadArgument)): + formatter = ctx.bot.formatter + if ctx.invoked_subcommand is None: + _help = await formatter.format_help_for(ctx, ctx.command) + else: + _help = await formatter.format_help_for(ctx, + ctx.invoked_subcommand) - for message in _help: - await ctx.send(message) + for message in _help: + await ctx.send(message) - if isinstance(err, commands.CommandInvokeError): - pass + if isinstance(err, commands.CommandInvokeError): + pass - elif isinstance(err, commands.NoPrivateMessage): - await ctx.send("Denne kommandoen er ikke tilgjengelig i DMs") + elif isinstance(err, commands.NoPrivateMessage): + await ctx.send("Denne kommandoen er ikke tilgjengelig i DMs") - elif isinstance(err, commands.CheckFailure): - pass + elif isinstance(err, commands.CheckFailure): + pass - elif isinstance(err, commands.CommandNotFound): - pass + elif isinstance(err, commands.CommandNotFound): + pass + else: + tb = err.__traceback__ + traceback.print_tb(tb) + print(err) async def on_ready(self): if not hasattr(self, 'uptime'): self.uptime = time.time() + if self.debug: + print('\n\nDebug mode') print(f'\nLogged in as: {self.user.name}' + f' in {len(self.guilds)} servers.') @@ -84,9 +94,12 @@ class Bot(commands.Bot): print('ifkn', e) -def run_bot(): - bot = Bot() +def run_bot(debug: bool=False): + bot = Bot(debug=debug) bot.run() if __name__ == '__main__': - run_bot() + if 'debug' in sys.argv: + run_bot(debug=True) + else: + run_bot(debug=False) diff --git a/cogs/music.py b/cogs/music.py index 824d135..5c4af05 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -13,6 +13,8 @@ import re import discord import lavalink from discord.ext import commands + +from .utils.mixplayer.mixplayer import MixPlayer from lavasettings import * time_rx = re.compile('[0-9]+') @@ -27,7 +29,8 @@ class Music: lavalink.Client(bot=bot, password='youshallnotpass', loop=bot.loop, log_level=logging.INFO, host=host, ws_port=ws_port, - rest_port=rest_port) + rest_port=rest_port, + player=MixPlayer) self.bot.lavalink.register_hook(self._track_hook) def __unload(self): @@ -48,7 +51,7 @@ class Music: if isinstance(event, lavalink.Events.TrackStartEvent): await channel.send(embed=discord.Embed(title='Now playing:', description=event.track.title, - color=discord.Color.blurple())) + color=0xEFD26C)) elif isinstance(event, lavalink.Events.QueueEndEvent): await channel.send('Queue ended! Why not queue more songs?') @@ -69,7 +72,7 @@ class Music: if not results or not results['tracks']: return await ctx.send('Nothing found!') - embed = discord.Embed(color=discord.Color.blurple()) + embed = discord.Embed(color=0xEFD26C) if results['loadType'] == 'PLAYLIST_LOADED': tracks = results['tracks'] @@ -205,7 +208,7 @@ class Music: duration = lavalink.Utils.format_time(player.current.duration) song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})' - embed = discord.Embed(color=discord.Color.blurple(), + embed = discord.Embed(color=0xEFD26C, title='Now Playing', description=song) await ctx.send(embed=embed) @@ -215,21 +218,49 @@ class Music: """ Shows the player's queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) - if not player.queue: + 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(player.queue) / items_per_page) - + 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(player.queue[start:end], start=start): - queue_list += f'`{index + 1}.` [**{track.title}**]({track.uri})\n' + 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(colour=discord.Color.blurple(), - description=f'**{len(player.queue)} tracks**\n\n{queue_list}') + 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) @@ -322,7 +353,7 @@ class Music: o += f'`{index}.` [{track_title}]({track_uri})\n' - embed = discord.Embed(color=discord.Color.blurple(), description=o) + embed = discord.Embed(color=0xEFD26C, description=o) await ctx.send(embed=embed) @commands.command(name='disconnect', aliases=['dc']) diff --git a/cogs/utils/mixplayer/mixplayer.py b/cogs/utils/mixplayer/mixplayer.py new file mode 100644 index 0000000..70131ea --- /dev/null +++ b/cogs/utils/mixplayer/mixplayer.py @@ -0,0 +1,167 @@ +from .mixqueue import MixQueue + +from abc import ABC, abstractmethod +from random import randrange + +from lavalink import BasePlayer +from lavalink.AudioTrack import AudioTrack +from lavalink.Events import QueueEndEvent, TrackExceptionEvent, TrackEndEvent, TrackStartEvent, TrackStuckEvent + + +''' + +Version of lavalink.py's DefaultPlayer with a custom queue(mixqueue) + +''' + +class MixPlayer(BasePlayer): + def __init__(self, lavalink, guild_id: int): + super().__init__(lavalink, guild_id) + + self._user_data = {} + self.channel_id = None + + self.paused = False + self.position = 0 + self.position_timestamp = 0 + self.volume = 100 + self.shuffle = False + self.repeat = False + + self.queue = MixQueue() + self.current = None + self.previous = None + + @property + def is_playing(self): + """ Returns the player's track state. """ + return self.connected_channel is not None and self.current is not None + + @property + def is_connected(self): + """ Returns the player's connection state. """ + return self.connected_channel is not None + + @property + def connected_channel(self): + """ Returns the voice channel the player is connected to. """ + if not self.channel_id: + return None + + return self._lavalink.bot.get_channel(int(self.channel_id)) + + async def connect(self, channel_id: int): + """ Connects to a voice channel. """ + ws = self._lavalink.bot._connection._get_websocket(int(self.guild_id)) + await ws.voice_state(self.guild_id, str(channel_id)) + + async def disconnect(self): + """ Disconnects from the voice channel, if any. """ + if not self.is_connected: + return + + await self.stop() + + ws = self._lavalink.bot._connection._get_websocket(int(self.guild_id)) + await ws.voice_state(self.guild_id, None) + + def store(self, key: object, value: object): + """ Stores custom user data. """ + self._user_data.update({key: value}) + + def fetch(self, key: object, default=None): + """ Retrieves the related value from the stored user data. """ + return self._user_data.get(key, default) + + def delete(self, key: object): + """ Removes an item from the the stored user data. """ + try: + del self._user_data[key] + except KeyError: + pass + + def add(self, requester: int, track: dict): + """ Adds a track to the queue. """ + #self.queue.append(AudioTrack().build(track, requester)) + self.queue.add_song(requester, AudioTrack().build(track, requester)) + + def add_next(self, requester: int, track: dict): + """ Adds a track to beginning of the queue """ + #self.queue.insert(0, AudioTrack().build(track, requester)) + self.queue.add_next_song(AudioTrack().build(track, requester)) + + def add_at(self, index: int, requester: int, track: dict): + """ Adds a track at a specific index in the queue. """ + #self.queue.insert(min(index, len(self.queue) - 1), AudioTrack().build(track, requester)) + self.queue.add_song(requester, AudioTrack().build(track, requester), index) + + async def play(self, track_index: int = 0, ignore_shuffle: bool = False): + """ Plays the first track in the queue, if any or plays a track from the specified index in the queue. """ + if self.repeat and self.current: + self.queue.append(self.current) + + self.previous = self.current + self.current = None + self.position = 0 + self.paused = False + + if self.queue.is_empty(): + await self.stop() + await self._lavalink.dispatch_event(QueueEndEvent(self)) + else: +# if self.shuffle and not ignore_shuffle: +# track = self.queue.pop(randrange(len(self.queue))) + track = self.queue.pop_first() + + self.current = track + await self._lavalink.ws.send(op='play', guildId=self.guild_id, track=track.track) + await self._lavalink.dispatch_event(TrackStartEvent(self, track)) + + async def play_now(self, requester: int, track: dict): + """ Add track and play it. """ + self.add_next(requester, track) + await self.play(ignore_shuffle=True) + + async def play_at(self, index: int): + """ Play the queue from a specific point. Disregards tracks before the index. """ + self.queue = self.queue[min(index, len(self.queue) - 1):len(self.queue)] + await self.play(ignore_shuffle=True) + + async def play_previous(self): + """ Plays previous track if it exist, if it doesn't raises a NoPreviousTrack error. """ + if not self.previous: + raise NoPreviousTrack + self.queue.insert(0, self.previous) + await self.play(ignore_shuffle=True) + + async def stop(self): + """ Stops the player, if playing. """ + await self._lavalink.ws.send(op='stop', guildId=self.guild_id) + self.current = None + + async def skip(self): + """ Plays the next track in the queue, if any. """ + await self.play() + + async def set_pause(self, pause: bool): + """ Sets the player's paused state. """ + await self._lavalink.ws.send(op='pause', guildId=self.guild_id, pause=pause) + self.paused = pause + + async def set_volume(self, vol: int): + """ Sets the player's volume (150% or 1000% limit imposed by lavalink depending on the version). """ + if self._lavalink._server_version <= 2: + self.volume = max(min(vol, 150), 0) + else: + self.volume = max(min(vol, 1000), 0) + await self._lavalink.ws.send(op='volume', guildId=self.guild_id, volume=self.volume) + + async def seek(self, pos: int): + """ Seeks to a given position in the track. """ + await self._lavalink.ws.send(op='seek', guildId=self.guild_id, position=pos) + + async def handle_event(self, event): + """ Makes the player play the next song from the queue if a song has finished or an issue occurred. """ + if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \ + isinstance(event, TrackEndEvent) and event.reason == 'FINISHED': + await self.play() \ No newline at end of file diff --git a/cogs/utils/mixplayer/mixqueue.py b/cogs/utils/mixplayer/mixqueue.py new file mode 100644 index 0000000..ba209d6 --- /dev/null +++ b/cogs/utils/mixplayer/mixqueue.py @@ -0,0 +1,154 @@ +from collections import OrderedDict +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 + num_active = len(iterables) + nexts = cycle(iter(it).__next__ for it in iterables) + while num_active: + try: + for next in nexts: + yield next() + except StopIteration: + num_active -= 1 + nexts = cycle(islice(nexts, num_active)) + + +class MixQueue: + def __init__(self): + self.queues = OrderedDict() + self.priority_queue = [] + + def __str__(self): + tmp = '' + for requester in self.queues.keys(): + user_queue = list(map(str, self.queues[requester])) + tmp += f'{requester}: {user_queue}\n' + return tmp + + def __iter__(self): + global_queue = roundrobin(*[x for x in self.queues.values()]) + priority_queue = self.priority_queue + out = chain(priority_queue, global_queue) + return out + + def __len__(self): + length = 0 + for i in self: + length += 1 + return length + + def get_queue(self): + return list(self) + + def is_empty(self): + return len(self) == 0 + + def clear(self): + self.queues = OrderedDict() + self.priority_queue = [] + + # if pos is true also returns global positions of songs + def get_user_queue(self, requester: int, pos: bool=False): + queue = self.queues.get(requester, []) + if pos and queue: + pos = [self._loc_to_glob(requester, i) for i in range(len(queue))] + combined = zip(queue, pos) + return list(combined) + return queue + + def pop_first(self): + if self.priority_queue: + next_song = self.priority_queue.pop(0) + return next_song + try: + next_song = self.queues[self.first_queue].pop(0) + self._shuffle() + self._clear_empty() + return next_song + except KeyError: + pass + + def add_song(self, requester: int, track: AudioTrack, pos: int=None): + user_queue = self.queues.get(requester) + if user_queue is None: + self.queues[requester] = [track] + elif pos is None: + user_queue.append(track) + else: + user_queue.insert(pos, track) + + def add_next_song(self, track: AudioTrack): + self.priority_queue.append(track) + + def remove_song_from(self, requester: int, pos: int): + user_queue = self.queues.get(requester) + if user_queue is not None: + if pos < len(user_queue): + user_queue.pop(pos) + self._clear_empty() + + def remove_song_at(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) + self._clear_empty() + + def switch_user_songs(self, requester: int, first: int, second: int): + queue = self.queues.get(requester, []) + if queue: + try: + queue[first], queue[second] = queue[second], queue[first] + except IndexError: + pass + + def shuffle_user_queue(self, requester: int): + queue = self.queues.get(requester, []) + if queue: + shuffle(queue) + + # Switches the order of user queues + def _shuffle(self): + self.queues.move_to_end(self.first_queue) + + # Removes empty user queues + def _clear_empty(self): + to_remove = [q for q in reversed(self.queues) if not self.queues[q]] + for i in to_remove: + self.queues.pop(i) + + # Convert between global and local queue positions + def _loc_to_glob(self, requester: int, pos: int): + queue = self.queues.get(requester, []) + if queue: + try: + song = queue[pos] + for i, s in enumerate(self): + if s == song: + return i + except IndexError: + pass + + def _glob_to_loc(self, pos: int): + song = None + for i, s in enumerate(self): + if i == pos: + song = s + if song is None: + return None, None + for requester, q in self.queues.items(): + for i, s in enumerate(q): + if s == song: + return requester, i + + @property + def first_queue(self): + try: + return list(self.queues.keys())[0] + except IndexError: + pass \ No newline at end of file