mirror of
https://github.com/UiO-Gaming/ShiteMusicBot.git
synced 2026-11-05 13:56:51 +01:00
48 files changed
+679
-620
No files matched your search
@@ -5,7 +5,7 @@ jobs:
|
||||
flake8:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CheckFiles: "bot.py cogs/"
|
||||
CheckFiles: "bot.py musicbot/"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -64,18 +64,18 @@ jobs:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
id: tagger
|
||||
run: |
|
||||
version=$(grep -i bot_version cogs/utils/bot_version.py | cut -d" " -f3 | tr -d \")
|
||||
version=$(grep -i bot_version musicbot/cogs/utils/bot_version.py | cut -d" " -f3 | tr -d \")
|
||||
a=(${version//./ })
|
||||
((a[2]++))
|
||||
new_version=$(echo "${a[0]}.${a[1]}.${a[2]}")
|
||||
sed -i "s/${version}/${new_version}/g" cogs/utils/bot_version.py
|
||||
sed -i "s/${version}/${new_version}/g" musicbot/cogs/utils/bot_version.py
|
||||
echo "::set-output name=tag::${new_version}"
|
||||
|
||||
- name: Push changed files, and tags
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
add: cogs/utils/bot_version.py
|
||||
add: musicbot/cogs/utils/bot_version.py
|
||||
author_name: MarlinBotCode
|
||||
message: Bump Version
|
||||
tag: v${{ steps.tagger.outputs.tag }}
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Translations, get the bot in your preferred langauge!
|
||||
- Translations, get the bot in your preferred langauge! Works for both commands and replies.
|
||||
- Round Robin style queue, you won't have to wait forever for your songs to play.
|
||||
- Noice per server customization options.
|
||||
- Noice per server customization options, including per server localization.
|
||||
- Toit embeds.
|
||||
- More stuff to come?
|
||||
- Lyrics command that sometimes gives the correct lyrics
|
||||
- DJ roles
|
||||
|
||||
@@ -17,10 +16,11 @@
|
||||
|
||||
1. Change or copy data/config.yaml.example to config.yaml
|
||||
2. Create or find a [lavalink](https://github.com/Frederikam/Lavalink) server you can use.
|
||||
3. Add your bot token and lavalink server.
|
||||
3. Add your bot token and lavalink server to the bot settings in your config.yaml.
|
||||
4. Edit any other settings you want.
|
||||
5. install requirements `python -m pip install -r requirements.txt`
|
||||
6. run the bot :)
|
||||
5. (Optional but recommended) Create a virtual environment and activate it.
|
||||
6. install requirements `python -m pip install -r requirements.txt`
|
||||
7. run the bot `python bot.py` :)
|
||||
|
||||
### Docker
|
||||
|
||||
|
||||
@@ -12,23 +12,20 @@ from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
import aiohttp
|
||||
import yaml
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.utils.alias import Aliaser
|
||||
from cogs.utils.context import Context
|
||||
from cogs.utils.localizer import Localizer, LocalizerWrapper
|
||||
from cogs.utils.logger import BotLogger
|
||||
from cogs.utils.settingsmanager import Settings
|
||||
from musicbot.utils.localisation import Aliaser, LocalizedContext, Localizer, LocalizerWrapper
|
||||
from musicbot.utils.logger import BotLogger
|
||||
from musicbot.utils.settingsmanager import Settings
|
||||
|
||||
initial_extensions = [
|
||||
'cogs.errors',
|
||||
'cogs.cogmanager',
|
||||
'cogs.settings',
|
||||
'cogs.misc',
|
||||
'cogs.helpformatter'
|
||||
'musicbot.cogs.errors',
|
||||
'musicbot.cogs.cogmanager',
|
||||
'musicbot.cogs.settings',
|
||||
'musicbot.cogs.misc',
|
||||
'musicbot.cogs.helpformatter'
|
||||
]
|
||||
|
||||
on_ready_extensions = [
|
||||
'cogs.nodemanager'
|
||||
'musicbot.cogs.nodemanager'
|
||||
]
|
||||
|
||||
|
||||
@@ -73,7 +70,7 @@ class Bot(commands.Bot):
|
||||
await self.process_commands(message)
|
||||
|
||||
async def process_commands(self, message):
|
||||
ctx = await self.get_context(message, cls=Context)
|
||||
ctx = await self.get_context(message, cls=LocalizedContext)
|
||||
|
||||
# Replace aliases with commands
|
||||
ctx = self.aliaser.get_command(ctx)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
bot_version = "0.1.12"
|
||||
@@ -1,297 +0,0 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class CantScroll(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BasePaginator:
|
||||
def __init__(self, max_size=2000, max_units=10):
|
||||
self._pages = []
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._max_size = max_size
|
||||
self._max_units = max_units
|
||||
|
||||
def close_page(self):
|
||||
pass
|
||||
|
||||
def add_page_indicator(self, localizer, localizer_str=None, **kvpairs):
|
||||
self.close_page()
|
||||
if localizer_str:
|
||||
for i, page in enumerate(self._pages, start=1):
|
||||
page.set_footer(text=localizer.format_str(localizer_str, _current=i,
|
||||
_total=len(self._pages), **kvpairs))
|
||||
else:
|
||||
for i, page in enumerate(self._pages, start=1):
|
||||
page.set_footer(text=f"{i}/{len(self._pages)}")
|
||||
|
||||
def append_paginator(self, paginator):
|
||||
if not isinstance(paginator, BasePaginator):
|
||||
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
|
||||
self.close_page()
|
||||
for page in paginator.pages:
|
||||
self.pages.append(page)
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
if self._current_page_size > 0:
|
||||
self.close_page()
|
||||
return self._pages
|
||||
|
||||
|
||||
class TextPaginator(BasePaginator):
|
||||
def __init__(self, max_size=2000, max_lines=10, **embed_base):
|
||||
super().__init__(max_size=max_size, max_units=max_lines)
|
||||
self._current_page = []
|
||||
self.embed_base = embed_base
|
||||
|
||||
def close_page(self):
|
||||
if self._current_page_size > 0:
|
||||
embed = discord.Embed(**self.embed_base)
|
||||
embed.description = '\n'.join(self._current_page)
|
||||
self._pages.append(embed)
|
||||
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._current_page = []
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
if len(line) > self._max_size:
|
||||
raise RuntimeError(f'Line exceeds maximum page size {self._max_size}')
|
||||
|
||||
if self._current_page_size + len(line) + 1 > self._max_size:
|
||||
self.close_page()
|
||||
|
||||
if self._current_units >= self._max_units:
|
||||
self.close_page()
|
||||
|
||||
self._current_page_size += len(line) + 1
|
||||
self._current_units += 1
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._current_page_size += 1
|
||||
|
||||
|
||||
class FieldPaginator(BasePaginator):
|
||||
def __init__(self, max_size=5000, max_fields=8, **embed_base):
|
||||
super().__init__(max_size=max_size, max_units=max_fields)
|
||||
self._embed_base = embed_base
|
||||
self._current_page = discord.Embed(**self._embed_base)
|
||||
|
||||
def close_page(self):
|
||||
if self._current_page_size > 0:
|
||||
self._pages.append(self._current_page)
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._current_page = discord.Embed(**self._embed_base)
|
||||
|
||||
def add_field(self, name, value, inline=False):
|
||||
field = {"name": name, "value": value}
|
||||
fieldsize = sum([len(val) for _, val in field.items()])
|
||||
if len(name) > 256 or len(value) > 1024:
|
||||
raise RuntimeError('Maximum field size exceeded, max name length: 256. max value length: 1024.')
|
||||
|
||||
if self._current_page_size + fieldsize > self._max_size:
|
||||
self.close_page()
|
||||
|
||||
if self._current_units >= self._max_units:
|
||||
self.close_page()
|
||||
|
||||
field["inline"] = inline
|
||||
self._current_page_size += fieldsize
|
||||
self._current_units += 1
|
||||
self._current_page.add_field(**field)
|
||||
|
||||
|
||||
class QueuePaginator(TextPaginator):
|
||||
def __init__(self, localizer, player, color, member: discord.Member = None, include_current: bool = False):
|
||||
self.localizer = localizer
|
||||
|
||||
queue = player.global_queue() if not member else player.user_queue(member.id, dual=True)
|
||||
duration = player.queue_duration(member=member, include_current=include_current)
|
||||
|
||||
if member:
|
||||
title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue),
|
||||
_duration=duration)
|
||||
else:
|
||||
title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration)
|
||||
|
||||
super().__init__(max_lines=10, **{"color": color, "title": title})
|
||||
|
||||
for index, temp in enumerate(queue):
|
||||
if member is None:
|
||||
track = temp
|
||||
queued_track = localizer.format_str("{queue.globaltrack}", _index=index+1, _title=track.title,
|
||||
_uri=track.uri, _user_id=track.requester)
|
||||
else:
|
||||
track, globpos = temp
|
||||
queued_track = localizer.format_str("{queue.usertrack}", _index=index+1, _globalindex=globpos+1,
|
||||
_title=track.title, _uri=track.uri)
|
||||
|
||||
self.add_line(queued_track)
|
||||
self.add_page_indicator(self.localizer, "{queue.pageindicator}")
|
||||
|
||||
|
||||
class HelpPaginator(FieldPaginator):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def force_close_page(self):
|
||||
self._pages.append(self._current_page)
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._current_page = discord.Embed(**self._embed_base)
|
||||
|
||||
def add_command_field(self, cmd_dict):
|
||||
if not isinstance(cmd_dict, dict):
|
||||
return
|
||||
aliases = cmd_dict.get('aliases', [])
|
||||
args = cmd_dict.get('args', '')
|
||||
description = cmd_dict.get('description', '')
|
||||
|
||||
cmd = str(aliases[0])
|
||||
|
||||
cmd += ' ' + args
|
||||
self.add_field(name=cmd, value=description, inline=False)
|
||||
|
||||
|
||||
# Maybe slightly very copied from rdanny
|
||||
# Thanks danny <3
|
||||
|
||||
class Scroller:
|
||||
def __init__(self, ctx, paginator, timeout=120.0, clear_on_timeout=False, clear_on_exit=True, clear_command=True):
|
||||
|
||||
if not isinstance(paginator, BasePaginator):
|
||||
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
|
||||
|
||||
self.bot = ctx.bot
|
||||
self.pages = paginator.pages
|
||||
self.cmdmsg = ctx.message
|
||||
self.message = ctx.message
|
||||
self.channel = ctx.channel
|
||||
self.author = ctx.author
|
||||
|
||||
self.timeout = timeout
|
||||
self.timeout_delete = clear_on_timeout
|
||||
self.exit_delete = clear_on_exit
|
||||
self.clear_command = clear_command
|
||||
|
||||
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
|
||||
if self.exit_delete:
|
||||
await self.message.delete()
|
||||
if self.clear_command:
|
||||
await self.cmdmsg.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):
|
||||
if not self.scrolling:
|
||||
await self.send()
|
||||
else:
|
||||
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=self.timeout)
|
||||
except asyncio.TimeoutError:
|
||||
self.scrolling = False
|
||||
try:
|
||||
await self.message.clear_reactions()
|
||||
if self.timeout_delete:
|
||||
await self.message.delete()
|
||||
if self.clear_command:
|
||||
await self.cmdmsg.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
try:
|
||||
await self.message.remove_reaction(reaction, user)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
await self.match()
|
||||
@@ -91,6 +91,9 @@ scrub:
|
||||
reconnect:
|
||||
aliases: [rc]
|
||||
description: 'disconnects then reconnects the bot in case of bugged songs'
|
||||
forcedisconnect:
|
||||
aliases: [fd]
|
||||
description: 'Forces the bot to disconnect'
|
||||
forceplay:
|
||||
aliases: [forceplay, fp]
|
||||
args: '<query>' # This is just a cosmetic representation of the arguments.
|
||||
|
||||
@@ -3,9 +3,8 @@ from discord.ext import commands
|
||||
|
||||
import traceback
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.helpformatter import commandhelper
|
||||
from cogs.utils.paginator import Scroller
|
||||
from ..utils.userinteraction import Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class CogManager(commands.Cog):
|
||||
@@ -4,11 +4,9 @@ from discord.ext import commands
|
||||
|
||||
import traceback
|
||||
|
||||
from cogs.music.music_errors import WrongTextChannelError, WrongVoiceChannelError
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.helpformatter import commandhelper
|
||||
from cogs.utils.paginator import Scroller
|
||||
from ..utils.userinteraction import Scroller
|
||||
from .helpformatter import commandhelper
|
||||
from .music.music_errors import WrongTextChannelError, WrongVoiceChannelError
|
||||
|
||||
|
||||
class Errors(commands.Cog):
|
||||
@@ -0,0 +1,9 @@
|
||||
from .help import Help
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
__all__ = ['Help', 'commandhelper']
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.remove_command("help")
|
||||
bot.add_cog(Help(bot))
|
||||
@@ -0,0 +1,36 @@
|
||||
# Discord Packages
|
||||
from discord.ext import commands
|
||||
|
||||
from ...utils.userinteraction import Scroller
|
||||
from .helpformatter import coghelper, commandhelper, helper, prefix_cleaner
|
||||
|
||||
|
||||
class Help(commands.Cog):
|
||||
"""Help command"""
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def help(self, ctx): # Takes no args because reasons(using the view directly)
|
||||
ctx.view.skip_ws()
|
||||
v = ctx.view
|
||||
invoker = v.buffer[v.index:v.end]
|
||||
ctx = self.bot.aliaser.get_subcommand(ctx, group=None, parents=[])
|
||||
command = v.buffer[v.index:v.end]
|
||||
|
||||
ctx = prefix_cleaner(ctx)
|
||||
|
||||
if not command:
|
||||
paginator = await helper(ctx)
|
||||
|
||||
if command:
|
||||
thing = ctx.bot.get_cog(command) or ctx.bot.get_command(command)
|
||||
if not thing:
|
||||
return await ctx.send(ctx.localizer.format_str("{notcommand}", _command=command))
|
||||
if isinstance(thing, commands.Command):
|
||||
paginator = commandhelper(ctx, thing, invoker)
|
||||
else:
|
||||
paginator = await coghelper(ctx, thing)
|
||||
|
||||
scroller = Scroller(ctx, paginator)
|
||||
await scroller.start_scrolling()
|
||||
@@ -3,8 +3,7 @@ from discord.ext import commands
|
||||
|
||||
import re
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.utils.paginator import HelpPaginator, Scroller
|
||||
from ...utils.userinteraction import HelpPaginator
|
||||
|
||||
usermention = r"<@!?\d{17,19}>"
|
||||
|
||||
@@ -110,39 +109,3 @@ def prefix_cleaner(ctx):
|
||||
pref = pref[0]
|
||||
ctx.prefix = pref
|
||||
return ctx
|
||||
|
||||
|
||||
class Help(commands.Cog):
|
||||
"""Help command"""
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def help(self, ctx): # Takes no args because reasons(using the view directly)
|
||||
ctx.view.skip_ws()
|
||||
v = ctx.view
|
||||
invoker = v.buffer[v.index:v.end]
|
||||
ctx = self.bot.aliaser.get_subcommand(ctx, group=None, parents=[])
|
||||
command = v.buffer[v.index:v.end]
|
||||
|
||||
ctx = prefix_cleaner(ctx)
|
||||
|
||||
if not command:
|
||||
paginator = await helper(ctx)
|
||||
|
||||
if command:
|
||||
thing = ctx.bot.get_cog(command) or ctx.bot.get_command(command)
|
||||
if not thing:
|
||||
return await ctx.send(ctx.localizer.format_str("{notcommand}", _command=command))
|
||||
if isinstance(thing, commands.Command):
|
||||
paginator = commandhelper(ctx, thing, invoker)
|
||||
else:
|
||||
paginator = await coghelper(ctx, thing)
|
||||
|
||||
scroller = Scroller(ctx, paginator)
|
||||
await scroller.start_scrolling()
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.remove_command("help")
|
||||
bot.add_cog(Help(bot))
|
||||
@@ -6,8 +6,7 @@ from lavalink import __version__ as LavalinkVersion
|
||||
import platform
|
||||
import time
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.utils import bot_version
|
||||
from ..utils import bot_version
|
||||
|
||||
|
||||
class Misc(commands.Cog):
|
||||
@@ -3,7 +3,7 @@ import discord
|
||||
import lavalink
|
||||
from discord.ext import commands
|
||||
|
||||
from ..utils import thumbnailer, timeformatter
|
||||
from ...utils import thumbnailer, timeformatter
|
||||
from .music_errors import WrongTextChannelError
|
||||
|
||||
|
||||
@@ -9,9 +9,8 @@ from discord.ext import commands
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from ..utils import checks, timeformatter
|
||||
from ..utils.paginator import QueuePaginator, Scroller
|
||||
from ..utils.selector import Selector
|
||||
from ...utils import checks, timeformatter
|
||||
from ...utils.userinteraction import QueuePaginator, Scroller, Selector
|
||||
from .decorators import require_playing, require_queue, require_voice_connection, voteable
|
||||
|
||||
time_rx = re.compile('[0-9]+')
|
||||
@@ -7,9 +7,8 @@ import functools
|
||||
import inspect
|
||||
import math
|
||||
|
||||
from cogs.music.music_errors import WrongVoiceChannelError
|
||||
|
||||
from ..utils.checks import is_dj
|
||||
from ...utils.checks import is_dj
|
||||
from .music_errors import WrongVoiceChannelError
|
||||
|
||||
|
||||
def require_voice_connection(should_connect=False):
|
||||
File renamed without changes.
@@ -14,8 +14,8 @@ import urllib
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from ..utils import checks
|
||||
from ..utils.paginator import Scroller, TextPaginator
|
||||
from ...utils import checks
|
||||
from ...utils.userinteraction import Scroller, TextPaginator
|
||||
from .decorators import require_playing, require_voice_connection, voteable
|
||||
|
||||
|
||||
File renamed without changes.
@@ -8,10 +8,9 @@ import codecs
|
||||
|
||||
import yaml
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.helpformatter import commandhelper
|
||||
from cogs.utils.paginator import Scroller
|
||||
from .utils.mixplayer import MixPlayer
|
||||
from ..utils.mixplayer import MixPlayer
|
||||
from ..utils.userinteraction import Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class NodeManager(commands.Cog):
|
||||
@@ -21,7 +20,7 @@ class NodeManager(commands.Cog):
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("NodeManager")
|
||||
|
||||
music_extensions = [
|
||||
'cogs.music',
|
||||
'musicbot.cogs.music',
|
||||
]
|
||||
|
||||
if not hasattr(bot, 'lavalink'):
|
||||
@@ -2,10 +2,9 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.helpformatter import commandhelper
|
||||
from cogs.utils import checks
|
||||
from cogs.utils.paginator import Scroller
|
||||
from ..utils import checks
|
||||
from ..utils.userinteraction import Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class Settings(commands.Cog):
|
||||
@@ -0,0 +1 @@
|
||||
bot_version = "0.2.12"
|
||||
File renamed without changes.
@@ -0,0 +1,5 @@
|
||||
from .alias import Aliaser
|
||||
from .localizedcontext import LocalizedContext
|
||||
from .localizer import Localizer, LocalizerWrapper
|
||||
|
||||
__all__ = ['Aliaser', 'LocalizerWrapper', 'Localizer', 'LocalizedContext']
|
||||
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
class Context(commands.Context):
|
||||
class LocalizedContext(commands.Context):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if self.message.guild:
|
||||
@@ -0,0 +1,4 @@
|
||||
from .localizer import Localizer
|
||||
from .localizerwrapper import LocalizerWrapper
|
||||
|
||||
__all__ = ['LocalizerWrapper', 'Localizer']
|
||||
@@ -11,7 +11,7 @@ def flatten(d, parent_key='', sep='/'):
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
new_key = parent_key + sep + k if parent_key else k
|
||||
if isinstance(v, collections.MutableMapping):
|
||||
if isinstance(v, collections.abc.MutableMapping):
|
||||
items.extend(flatten(v, new_key, sep=sep).items())
|
||||
else:
|
||||
items.append((new_key, v))
|
||||
@@ -8,8 +8,7 @@ from os import path
|
||||
|
||||
import yaml
|
||||
|
||||
# Bot Utilities
|
||||
from cogs.utils.dict_utils import SafeDict, flatten
|
||||
from .dict_utils import SafeDict, flatten
|
||||
|
||||
"""
|
||||
Localizer for bot
|
||||
@@ -152,19 +151,3 @@ class Localizer:
|
||||
def format_embed(self, embed, lang=None, prefix=None, **kvpairs):
|
||||
raw = embed.to_dict()
|
||||
return Embed.from_dict(self.format_dict(raw, lang, prefix, **kvpairs))
|
||||
|
||||
|
||||
class LocalizerWrapper:
|
||||
def __init__(self, localizer, lang=None, prefix=None):
|
||||
self.localizer = localizer
|
||||
self.lang = lang
|
||||
self.prefix = prefix
|
||||
|
||||
def format_str(self, s, **kvpairs):
|
||||
return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs)
|
||||
|
||||
def format_dict(self, d, **kvpairs):
|
||||
return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs)
|
||||
|
||||
def format_embed(self, embed, **kvpairs):
|
||||
return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs)
|
||||
@@ -0,0 +1,14 @@
|
||||
class LocalizerWrapper:
|
||||
def __init__(self, localizer, lang=None, prefix=None):
|
||||
self.localizer = localizer
|
||||
self.lang = lang
|
||||
self.prefix = prefix
|
||||
|
||||
def format_str(self, s, **kvpairs):
|
||||
return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs)
|
||||
|
||||
def format_dict(self, d, **kvpairs):
|
||||
return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs)
|
||||
|
||||
def format_embed(self, embed, **kvpairs):
|
||||
return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs)
|
||||
File renamed without changes.
@@ -0,0 +1,3 @@
|
||||
from .player import MixPlayer
|
||||
|
||||
__all__ = ['MixPlayer']
|
||||
@@ -1,189 +1,11 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from lavalink import AudioTrack, DefaultPlayer, Node
|
||||
from lavalink.events import QueueEndEvent, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent
|
||||
from lavalink import AudioTrack
|
||||
|
||||
import typing
|
||||
from collections import OrderedDict, deque
|
||||
from itertools import chain, cycle, islice
|
||||
from random import shuffle
|
||||
|
||||
|
||||
class MixPlayer(DefaultPlayer):
|
||||
def __init__(self, guild_id: int, node: Node):
|
||||
super().__init__(guild_id, node)
|
||||
|
||||
self.queue = MixQueue()
|
||||
|
||||
self.listeners = set()
|
||||
self.voteables = {}
|
||||
self.skip_voters = set()
|
||||
self.boosted = False
|
||||
|
||||
def add(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
|
||||
""" Adds a track to the queue. """
|
||||
return self.queue.add_track(requester, track, pos)
|
||||
|
||||
def add_next(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
|
||||
""" Adds a track to beginning of the queue """
|
||||
self.queue.add_next_track(track)
|
||||
|
||||
def move_user_track(self, requester: int, initial: int, final: int):
|
||||
""" Moves a track in a users queue"""
|
||||
return self.queue.move_user_track(requester, initial, final)
|
||||
|
||||
def remove_user_queue(self, requester: int):
|
||||
self.queue.remove_user_queue(requester)
|
||||
|
||||
def remove_user_track(self, requester: int, pos: int):
|
||||
""" Removes the song at <pos> from the queue of requester """
|
||||
return self.queue.remove_user_track(requester, pos)
|
||||
|
||||
def remove_global_track(self, pos: int):
|
||||
""" Removes the song at <pos> in the global queue """
|
||||
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()
|
||||
|
||||
def get_history(self):
|
||||
return self.queue.history
|
||||
|
||||
def queue_duration(self, include_current: bool = False, member: discord.Member = None, end_pos: int = None):
|
||||
duration = 0
|
||||
queue = self.user_queue(member.id) if member else self.queue
|
||||
|
||||
for i, track in enumerate(queue):
|
||||
if i == end_pos:
|
||||
break
|
||||
duration += int(track.duration)
|
||||
|
||||
remaining = self.current.duration - self.position
|
||||
if include_current:
|
||||
if self.current:
|
||||
remaining = self.current.duration - self.position
|
||||
return lavalink.utils.format_time(duration + remaining)
|
||||
return lavalink.utils.format_time(duration)
|
||||
|
||||
async def play(self, track: AudioTrack = None, start_time: int = 0):
|
||||
|
||||
self.current = None
|
||||
self.last_update = 0
|
||||
self.last_position = 0
|
||||
self.position_timestamp = 0
|
||||
self.paused = False
|
||||
|
||||
if not track:
|
||||
if self.queue.empty:
|
||||
self.boosted = False
|
||||
await self.stop()
|
||||
await self.node._dispatch_event(QueueEndEvent(self))
|
||||
return
|
||||
else:
|
||||
track = self.queue.pop_first()
|
||||
|
||||
self.current = track
|
||||
await self.node._send(op='play', guildId=self.guild_id,
|
||||
track=track.track, startTime=start_time)
|
||||
await self.node._dispatch_event(TrackStartEvent(self, track))
|
||||
|
||||
async def skip(self, pos: int = 0):
|
||||
""" Plays the next track in the queue, if any. """
|
||||
for i in range(pos):
|
||||
_ = self.queue.pop_first()
|
||||
self.clear_votes()
|
||||
await self.play()
|
||||
|
||||
async def stop(self):
|
||||
""" Stops the player. """
|
||||
await self.node._send(op='stop', guildId=self.guild_id)
|
||||
self.current = None
|
||||
self.queue.looping = False
|
||||
self.clear_votes()
|
||||
|
||||
def update_listeners(self, member, voice_state):
|
||||
if self.is_connected:
|
||||
vc = int(self.channel_id)
|
||||
if voice_state.channel is None or voice_state.channel.id != vc:
|
||||
self.listeners.discard(member)
|
||||
self.remove_member_votes(member)
|
||||
else:
|
||||
if voice_state.deaf or voice_state.self_deaf:
|
||||
self.listeners.discard(member)
|
||||
self.remove_member_votes(member)
|
||||
else:
|
||||
self.listeners.add(member)
|
||||
|
||||
def clear_listeners(self):
|
||||
self.listeners.clear()
|
||||
|
||||
def add_vote(self, category, member):
|
||||
if member in self.listeners:
|
||||
if category not in self.voteables:
|
||||
self.voteables[category] = set()
|
||||
self.voteables[category].add(member)
|
||||
|
||||
def remove_member_votes(self, member):
|
||||
for category, votes in self.voteables.items():
|
||||
votes.discard(member)
|
||||
|
||||
def get_voters(self, category):
|
||||
if category not in self.voteables:
|
||||
self.voteables[category] = set()
|
||||
return self.voteables[category]
|
||||
|
||||
def clear_votes(self):
|
||||
for category, votes in self.voteables.items():
|
||||
votes.clear()
|
||||
|
||||
def enable_looping(self, looping):
|
||||
if (not self.queue.looping) and looping:
|
||||
self.queue.enable_looping(looping)
|
||||
if track := self.current:
|
||||
if looping:
|
||||
self.queue.add_track(track.requester, track)
|
||||
elif not looping and self.queue.looping:
|
||||
self.queue.enable_looping(looping)
|
||||
|
||||
async def handle_event(self, event):
|
||||
""" Handles the given event as necessary. """
|
||||
if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \
|
||||
isinstance(event, TrackEndEvent) and event.reason == 'FINISHED':
|
||||
self.skip_voters.clear()
|
||||
for category, votes in self.voteables.items():
|
||||
votes.clear()
|
||||
await self.play()
|
||||
|
||||
async def bassboost(self, boost: bool = False):
|
||||
if boost:
|
||||
gains = [
|
||||
(0, 0.15),
|
||||
(1, 0.15),
|
||||
(2, 0.25),
|
||||
(3, 0.15),
|
||||
(4, -0.15),
|
||||
(5, -0.1),
|
||||
(6, -0.05)
|
||||
]
|
||||
self.boosted = True
|
||||
await self.set_gains(*gains)
|
||||
else:
|
||||
self.boosted = False
|
||||
await self.reset_equalizer()
|
||||
|
||||
@property
|
||||
def looping(self):
|
||||
return self.queue.looping
|
||||
|
||||
|
||||
def roundrobin(*iterables):
|
||||
"""roundrobin('ABC', 'D', 'EF') --> A D E B F C"""
|
||||
# Recipe credited to George Sakkis
|
||||
@@ -0,0 +1,183 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from lavalink import AudioTrack, DefaultPlayer, Node
|
||||
from lavalink.events import QueueEndEvent, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent
|
||||
|
||||
import typing
|
||||
|
||||
from .mixqueue import MixQueue
|
||||
|
||||
|
||||
class MixPlayer(DefaultPlayer):
|
||||
def __init__(self, guild_id: int, node: Node):
|
||||
super().__init__(guild_id, node)
|
||||
|
||||
self.queue = MixQueue()
|
||||
|
||||
self.listeners = set()
|
||||
self.voteables = {}
|
||||
self.skip_voters = set()
|
||||
self.boosted = False
|
||||
|
||||
def add(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
|
||||
""" Adds a track to the queue. """
|
||||
return self.queue.add_track(requester, track, pos)
|
||||
|
||||
def add_next(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
|
||||
""" Adds a track to beginning of the queue """
|
||||
self.queue.add_next_track(track)
|
||||
|
||||
def move_user_track(self, requester: int, initial: int, final: int):
|
||||
""" Moves a track in a users queue"""
|
||||
return self.queue.move_user_track(requester, initial, final)
|
||||
|
||||
def remove_user_queue(self, requester: int):
|
||||
self.queue.remove_user_queue(requester)
|
||||
|
||||
def remove_user_track(self, requester: int, pos: int):
|
||||
""" Removes the song at <pos> from the queue of requester """
|
||||
return self.queue.remove_user_track(requester, pos)
|
||||
|
||||
def remove_global_track(self, pos: int):
|
||||
""" Removes the song at <pos> in the global queue """
|
||||
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()
|
||||
|
||||
def get_history(self):
|
||||
return self.queue.history
|
||||
|
||||
def queue_duration(self, include_current: bool = False, member: discord.Member = None, end_pos: int = None):
|
||||
duration = 0
|
||||
queue = self.user_queue(member.id) if member else self.queue
|
||||
|
||||
for i, track in enumerate(queue):
|
||||
if i == end_pos:
|
||||
break
|
||||
duration += int(track.duration)
|
||||
|
||||
remaining = self.current.duration - self.position
|
||||
if include_current:
|
||||
if self.current:
|
||||
remaining = self.current.duration - self.position
|
||||
return lavalink.utils.format_time(duration + remaining)
|
||||
return lavalink.utils.format_time(duration)
|
||||
|
||||
async def play(self, track: AudioTrack = None, start_time: int = 0):
|
||||
|
||||
self.current = None
|
||||
self.last_update = 0
|
||||
self.last_position = 0
|
||||
self.position_timestamp = 0
|
||||
self.paused = False
|
||||
|
||||
if not track:
|
||||
if self.queue.empty:
|
||||
self.boosted = False
|
||||
await self.stop()
|
||||
await self.node._dispatch_event(QueueEndEvent(self))
|
||||
return
|
||||
else:
|
||||
track = self.queue.pop_first()
|
||||
|
||||
self.current = track
|
||||
await self.node._send(op='play', guildId=self.guild_id,
|
||||
track=track.track, startTime=start_time)
|
||||
await self.node._dispatch_event(TrackStartEvent(self, track))
|
||||
|
||||
async def skip(self, pos: int = 0):
|
||||
""" Plays the next track in the queue, if any. """
|
||||
for i in range(pos):
|
||||
_ = self.queue.pop_first()
|
||||
self.clear_votes()
|
||||
await self.play()
|
||||
|
||||
async def stop(self):
|
||||
""" Stops the player. """
|
||||
await self.node._send(op='stop', guildId=self.guild_id)
|
||||
self.current = None
|
||||
self.queue.looping = False
|
||||
self.clear_votes()
|
||||
|
||||
def update_listeners(self, member, voice_state):
|
||||
if self.is_connected:
|
||||
vc = int(self.channel_id)
|
||||
if voice_state.channel is None or voice_state.channel.id != vc:
|
||||
self.listeners.discard(member)
|
||||
self.remove_member_votes(member)
|
||||
else:
|
||||
if voice_state.deaf or voice_state.self_deaf:
|
||||
self.listeners.discard(member)
|
||||
self.remove_member_votes(member)
|
||||
else:
|
||||
self.listeners.add(member)
|
||||
|
||||
def clear_listeners(self):
|
||||
self.listeners.clear()
|
||||
|
||||
def add_vote(self, category, member):
|
||||
if member in self.listeners:
|
||||
if category not in self.voteables:
|
||||
self.voteables[category] = set()
|
||||
self.voteables[category].add(member)
|
||||
|
||||
def remove_member_votes(self, member):
|
||||
for category, votes in self.voteables.items():
|
||||
votes.discard(member)
|
||||
|
||||
def get_voters(self, category):
|
||||
if category not in self.voteables:
|
||||
self.voteables[category] = set()
|
||||
return self.voteables[category]
|
||||
|
||||
def clear_votes(self):
|
||||
for category, votes in self.voteables.items():
|
||||
votes.clear()
|
||||
|
||||
def enable_looping(self, looping):
|
||||
if (not self.queue.looping) and looping:
|
||||
self.queue.enable_looping(looping)
|
||||
if track := self.current:
|
||||
if looping:
|
||||
self.queue.add_track(track.requester, track)
|
||||
elif not looping and self.queue.looping:
|
||||
self.queue.enable_looping(looping)
|
||||
|
||||
async def handle_event(self, event):
|
||||
""" Handles the given event as necessary. """
|
||||
if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \
|
||||
isinstance(event, TrackEndEvent) and event.reason == 'FINISHED':
|
||||
self.skip_voters.clear()
|
||||
for category, votes in self.voteables.items():
|
||||
votes.clear()
|
||||
await self.play()
|
||||
|
||||
async def bassboost(self, boost: bool = False):
|
||||
if boost:
|
||||
gains = [
|
||||
(0, 0.15),
|
||||
(1, 0.15),
|
||||
(2, 0.25),
|
||||
(3, 0.15),
|
||||
(4, -0.15),
|
||||
(5, -0.1),
|
||||
(6, -0.05)
|
||||
]
|
||||
self.boosted = True
|
||||
await self.set_gains(*gains)
|
||||
else:
|
||||
self.boosted = False
|
||||
await self.reset_equalizer()
|
||||
|
||||
@property
|
||||
def looping(self):
|
||||
return self.queue.looping
|
||||
@@ -0,0 +1,3 @@
|
||||
from .settingsmanager import Settings
|
||||
|
||||
__all__ = ['Settings']
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
class DictMapper:
|
||||
"""
|
||||
Class for creating and interacting with nested dictionaries through lists of keys
|
||||
E.g. mapping ["a", "b", "c"] to example_dictionary["a"]["b"]["c"].
|
||||
|
||||
DictMapper.get(d["a"], ["b", "c"]) and DictMapper.get(d, ["a", "b", "c"]) are equivalent.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def set(d, keys, val):
|
||||
key = keys[0]
|
||||
if len(keys) == 1:
|
||||
if val is None:
|
||||
try:
|
||||
d.pop(key)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
d[key] = val
|
||||
return
|
||||
if key in d.keys():
|
||||
if not isinstance(d[key], dict):
|
||||
d[key] = {}
|
||||
DictMapper.set(d[key], keys[1:], val)
|
||||
else:
|
||||
d[key] = {}
|
||||
DictMapper.set(d[key], keys[1:], val)
|
||||
|
||||
@staticmethod
|
||||
def get(d, keys):
|
||||
key = keys[0]
|
||||
try:
|
||||
if len(keys) > 1 and isinstance(d[key], dict):
|
||||
return DictMapper.get(d[key], keys[1:])
|
||||
else:
|
||||
return d[key]
|
||||
except KeyError:
|
||||
return None
|
||||
@@ -7,8 +7,10 @@ import os
|
||||
|
||||
import yaml
|
||||
|
||||
from .dictmapper import DictMapper
|
||||
|
||||
class Settings:
|
||||
|
||||
class Settings():
|
||||
def __init__(self, datadir, **default_settings):
|
||||
self._DATA_PATH = f"{datadir}/bot/"
|
||||
self._SETTINGS_PATH = self._DATA_PATH + 'settings.yaml'
|
||||
@@ -34,35 +36,6 @@ class Settings:
|
||||
with codecs.open(self._SETTINGS_PATH, "r", encoding='utf8') as f:
|
||||
self.settings = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
def _set(self, d, keys, val):
|
||||
key = keys[0]
|
||||
if len(keys) == 1:
|
||||
if val is None:
|
||||
try:
|
||||
d.pop(key)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
d[key] = val
|
||||
return
|
||||
if key in d.keys():
|
||||
if not isinstance(d[key], dict):
|
||||
d[key] = {}
|
||||
self._set(d[key], keys[1:], val)
|
||||
else:
|
||||
d[key] = {}
|
||||
self._set(d[key], keys[1:], val)
|
||||
|
||||
def _get(self, d, keys):
|
||||
key = keys[0]
|
||||
try:
|
||||
if len(keys) > 1 and isinstance(d[key], dict):
|
||||
return self._get(d[key], keys[1:])
|
||||
else:
|
||||
return d[key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def set(self, identifier, setting, value):
|
||||
""" Set value in settings, will overwrite any existing values. """
|
||||
guild_name = None
|
||||
@@ -75,7 +48,7 @@ class Settings:
|
||||
|
||||
if guild_name:
|
||||
self.settings[identifier]["_servername"] = guild_name
|
||||
self._set(self.settings[identifier], setting.split('.'), value)
|
||||
DictMapper.set(self.settings[identifier], setting.split('.'), value)
|
||||
|
||||
with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f:
|
||||
yaml.dump(self.settings, f, indent=2)
|
||||
@@ -95,7 +68,7 @@ class Settings:
|
||||
if identifier not in self.settings.keys():
|
||||
return default
|
||||
|
||||
value = self._get(self.settings[identifier], setting.split('.'))
|
||||
value = DictMapper.get(self.settings[identifier], setting.split('.'))
|
||||
if value is not None:
|
||||
return value
|
||||
else:
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
from .paginators import BasePaginator, CantScrollException, FieldPaginator, HelpPaginator, QueuePaginator, TextPaginator
|
||||
from .scroller import Scroller
|
||||
from .selector import Selector
|
||||
|
||||
__all__ = ['Scroller', 'Selector', 'QueuePaginator', 'HelpPaginator',
|
||||
'TextPaginator', 'FieldPaginator', 'CantScrollException', 'BasePaginator']
|
||||
@@ -0,0 +1,7 @@
|
||||
from .basepaginator import BasePaginator, CantScrollException
|
||||
from .fieldpaginator import FieldPaginator
|
||||
from .helppaginator import HelpPaginator
|
||||
from .queuepaginator import QueuePaginator
|
||||
from .textpaginator import TextPaginator
|
||||
|
||||
__all__ = ['QueuePaginator', 'HelpPaginator', 'TextPaginator', 'FieldPaginator', 'CantScrollException', 'BasePaginator']
|
||||
@@ -0,0 +1,37 @@
|
||||
class CantScrollException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BasePaginator:
|
||||
def __init__(self, max_size=2000, max_units=10):
|
||||
self._pages = []
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._max_size = max_size
|
||||
self._max_units = max_units
|
||||
|
||||
def close_page(self):
|
||||
pass
|
||||
|
||||
def add_page_indicator(self, localizer, localizer_str=None, **kvpairs):
|
||||
self.close_page()
|
||||
if localizer_str:
|
||||
for i, page in enumerate(self._pages, start=1):
|
||||
page.set_footer(text=localizer.format_str(localizer_str, _current=i,
|
||||
_total=len(self._pages), **kvpairs))
|
||||
else:
|
||||
for i, page in enumerate(self._pages, start=1):
|
||||
page.set_footer(text=f"{i}/{len(self._pages)}")
|
||||
|
||||
def append_paginator(self, paginator):
|
||||
if not isinstance(paginator, BasePaginator):
|
||||
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
|
||||
self.close_page()
|
||||
for page in paginator.pages:
|
||||
self.pages.append(page)
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
if self._current_page_size > 0:
|
||||
self.close_page()
|
||||
return self._pages
|
||||
@@ -0,0 +1,35 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
from .basepaginator import BasePaginator
|
||||
|
||||
|
||||
class FieldPaginator(BasePaginator):
|
||||
def __init__(self, max_size=5000, max_fields=8, **embed_base):
|
||||
super().__init__(max_size=max_size, max_units=max_fields)
|
||||
self._embed_base = embed_base
|
||||
self._current_page = discord.Embed(**self._embed_base)
|
||||
|
||||
def close_page(self):
|
||||
if self._current_page_size > 0:
|
||||
self._pages.append(self._current_page)
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._current_page = discord.Embed(**self._embed_base)
|
||||
|
||||
def add_field(self, name, value, inline=False):
|
||||
field = {"name": name, "value": value}
|
||||
fieldsize = sum([len(val) for _, val in field.items()])
|
||||
if len(name) > 256 or len(value) > 1024:
|
||||
raise RuntimeError('Maximum field size exceeded, max name length: 256. max value length: 1024.')
|
||||
|
||||
if self._current_page_size + fieldsize > self._max_size:
|
||||
self.close_page()
|
||||
|
||||
if self._current_units >= self._max_units:
|
||||
self.close_page()
|
||||
|
||||
field["inline"] = inline
|
||||
self._current_page_size += fieldsize
|
||||
self._current_units += 1
|
||||
self._current_page.add_field(**field)
|
||||
@@ -0,0 +1,27 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
from .fieldpaginator import FieldPaginator
|
||||
|
||||
|
||||
class HelpPaginator(FieldPaginator):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def force_close_page(self):
|
||||
self._pages.append(self._current_page)
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._current_page = discord.Embed(**self._embed_base)
|
||||
|
||||
def add_command_field(self, cmd_dict):
|
||||
if not isinstance(cmd_dict, dict):
|
||||
return
|
||||
aliases = cmd_dict.get('aliases', [])
|
||||
args = cmd_dict.get('args', '')
|
||||
description = cmd_dict.get('description', '')
|
||||
|
||||
cmd = str(aliases[0])
|
||||
|
||||
cmd += ' ' + args
|
||||
self.add_field(name=cmd, value=description, inline=False)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
from .textpaginator import TextPaginator
|
||||
|
||||
|
||||
class QueuePaginator(TextPaginator):
|
||||
def __init__(self, localizer, player, color, member: discord.Member = None, include_current: bool = False):
|
||||
self.localizer = localizer
|
||||
|
||||
queue = player.global_queue() if not member else player.user_queue(member.id, dual=True)
|
||||
duration = player.queue_duration(member=member, include_current=include_current)
|
||||
|
||||
if member:
|
||||
title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue),
|
||||
_duration=duration)
|
||||
else:
|
||||
title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration)
|
||||
|
||||
super().__init__(max_lines=10, **{"color": color, "title": title})
|
||||
|
||||
for index, temp in enumerate(queue):
|
||||
if member is None:
|
||||
track = temp
|
||||
queued_track = localizer.format_str("{queue.globaltrack}", _index=index+1, _title=track.title,
|
||||
_uri=track.uri, _user_id=track.requester)
|
||||
else:
|
||||
track, globpos = temp
|
||||
queued_track = localizer.format_str("{queue.usertrack}", _index=index+1, _globalindex=globpos+1,
|
||||
_title=track.title, _uri=track.uri)
|
||||
|
||||
self.add_line(queued_track)
|
||||
self.add_page_indicator(self.localizer, "{queue.pageindicator}")
|
||||
@@ -0,0 +1,39 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
from .basepaginator import BasePaginator
|
||||
|
||||
|
||||
class TextPaginator(BasePaginator):
|
||||
def __init__(self, max_size=2000, max_lines=10, **embed_base):
|
||||
super().__init__(max_size=max_size, max_units=max_lines)
|
||||
self._current_page = []
|
||||
self.embed_base = embed_base
|
||||
|
||||
def close_page(self):
|
||||
if self._current_page_size > 0:
|
||||
embed = discord.Embed(**self.embed_base)
|
||||
embed.description = '\n'.join(self._current_page)
|
||||
self._pages.append(embed)
|
||||
|
||||
self._current_page_size = 0
|
||||
self._current_units = 0
|
||||
self._current_page = []
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
if len(line) > self._max_size:
|
||||
raise RuntimeError(f'Line exceeds maximum page size {self._max_size}')
|
||||
|
||||
if self._current_page_size + len(line) + 1 > self._max_size:
|
||||
self.close_page()
|
||||
|
||||
if self._current_units >= self._max_units:
|
||||
self.close_page()
|
||||
|
||||
self._current_page_size += len(line) + 1
|
||||
self._current_units += 1
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._current_page_size += 1
|
||||
@@ -0,0 +1,143 @@
|
||||
|
||||
# This was at some point *heavliy* inspired by the paginator implementation by Rapptz for r.Danny
|
||||
# https://github.com/Rapptz/RoboDanny/blob/b0401e046814f42597c6e0280fef2432cc49cc5d/cogs/utils/paginator.py
|
||||
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
import asyncio
|
||||
|
||||
from .paginators import BasePaginator, CantScrollException
|
||||
|
||||
|
||||
class Scroller:
|
||||
def __init__(self, ctx, paginator, timeout=120.0, clear_on_timeout=False, clear_on_exit=True, clear_command=True):
|
||||
|
||||
if not isinstance(paginator, BasePaginator):
|
||||
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
|
||||
|
||||
self.bot = ctx.bot
|
||||
self.pages = paginator.pages
|
||||
self.cmdmsg = ctx.message
|
||||
self.message = ctx.message
|
||||
self.channel = ctx.channel
|
||||
self.author = ctx.author
|
||||
|
||||
self.timeout = timeout
|
||||
self.timeout_delete = clear_on_timeout
|
||||
self.exit_delete = clear_on_exit
|
||||
self.clear_command = clear_command
|
||||
|
||||
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 CantScrollException('Bot does not have embed links permission.')
|
||||
|
||||
if not self.permissions.send_messages:
|
||||
raise CantScrollException('Bot cannot send messages.')
|
||||
|
||||
if self.scrolling:
|
||||
# verify we can actually use the pagination session
|
||||
if not self.permissions.add_reactions:
|
||||
raise CantScrollException('Bot does not have add reactions permission.')
|
||||
|
||||
if not self.permissions.read_message_history:
|
||||
raise CantScrollException('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
|
||||
if self.exit_delete:
|
||||
await self.message.delete()
|
||||
if self.clear_command:
|
||||
await self.cmdmsg.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):
|
||||
if not self.scrolling:
|
||||
await self.send()
|
||||
else:
|
||||
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=self.timeout)
|
||||
except asyncio.TimeoutError:
|
||||
self.scrolling = False
|
||||
try:
|
||||
await self.message.clear_reactions()
|
||||
if self.timeout_delete:
|
||||
await self.message.delete()
|
||||
if self.clear_command:
|
||||
await self.cmdmsg.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
try:
|
||||
await self.message.remove_reaction(reaction, user)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
await self.match()
|
||||
@@ -4,7 +4,7 @@ import discord
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
from .paginator import TextPaginator
|
||||
from .paginators import TextPaginator
|
||||
|
||||
|
||||
class Selector(TextPaginator):
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
discord.py == 1.7.*
|
||||
git+https://github.com/Devoxin/Lavalink.py@dev
|
||||
lavalink == 3.1.*
|
||||
asyncio
|
||||
pyyaml
|
||||
BeautifulSoup4
|
||||
Reference in new issue
Block a user