mirror of
https://github.com/UiO-Gaming/ShiteMusicBot.git
synced 2026-11-05 13:56:51 +01:00
Merge branch 'dev' into feature/increase_log_size
This commit is contained in:
14 files changed
+287
-176
No files matched your search
@@ -1,5 +1,6 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from discord.ext import commands
|
||||
from discord.flags import MemberCacheFlags
|
||||
|
||||
@@ -8,6 +9,7 @@ import os
|
||||
import time
|
||||
import traceback
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import yaml
|
||||
@@ -35,7 +37,7 @@ def _get_prefix(bot, message):
|
||||
return commands.when_mentioned_or(*prefixes)(bot, message)
|
||||
|
||||
|
||||
class Bot(commands.Bot):
|
||||
class MusicBot(commands.Bot):
|
||||
def __init__(self, datadir, debug: bool = False):
|
||||
intents = discord.Intents.all()
|
||||
super().__init__(command_prefix=_get_prefix,
|
||||
@@ -55,6 +57,7 @@ class Bot(commands.Bot):
|
||||
self.main_logger = logger
|
||||
self.logger = self.main_logger.bot_logger.getChild("Bot")
|
||||
self.logger.debug("Debug: %s" % debug)
|
||||
self.lavalink: Optional[lavalink.Client] = None
|
||||
|
||||
async def on_message(self, message):
|
||||
if message.author.bot:
|
||||
@@ -110,7 +113,7 @@ class Bot(commands.Bot):
|
||||
|
||||
|
||||
def run_bot(datadir, debug: bool = False):
|
||||
bot = Bot(datadir, debug=debug)
|
||||
bot = MusicBot(datadir, debug=debug)
|
||||
bot.run()
|
||||
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ from discord.ext import commands
|
||||
|
||||
import traceback
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class CogManager(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.settings = self.bot.settings
|
||||
|
||||
@commands.group(name='cogmanager', hidden=True)
|
||||
|
||||
+52
-49
@@ -4,14 +4,16 @@ from discord.ext import commands
|
||||
|
||||
import sys
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
from .music.music_errors import WrongTextChannelError, WrongVoiceChannelError
|
||||
from .music import music_errors
|
||||
|
||||
|
||||
class Errors(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("Errors")
|
||||
|
||||
async def base_msg(self, ctx, state: int = 0xFCBA03):
|
||||
@@ -42,60 +44,61 @@ class Errors(commands.Cog):
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
# Music related errors
|
||||
if isinstance(err, commands.CommandInvokeError):
|
||||
if (err.original == 'Join a voicechannel first.'):
|
||||
return await send_error_embed('{errors.join_voice_first}')
|
||||
|
||||
elif (err.original == 'Not connected.'):
|
||||
return await send_error_embed('{errors.not_connected}')
|
||||
|
||||
elif (err.original == 'I need the `CONNECT` and `SPEAK` permissions.'):
|
||||
return await send_error_embed('{errors.need_permission}')
|
||||
|
||||
elif (err.original == 'Not playing'):
|
||||
return await send_error_embed('{not_playing}')
|
||||
|
||||
elif (err.original == 'Not listening'):
|
||||
return await send_error_embed('{have_to_listen}')
|
||||
|
||||
elif (err.original == 'The channel is currently full'):
|
||||
return await send_error_embed('{errors.full_channel}')
|
||||
|
||||
# Regular command invoke errors
|
||||
# User input errors
|
||||
if isinstance(err, commands.UserInputError):
|
||||
if self.bot.debug:
|
||||
self.logger.debug("User input error")
|
||||
self.logger.debug(err)
|
||||
return
|
||||
|
||||
if isinstance(err, WrongVoiceChannelError):
|
||||
if (err.original.startswith("You need to be in my voice channel")):
|
||||
return await send_error_embed(err.channels[0].name, title='{errors.my_channel}')
|
||||
# Music related errors
|
||||
if isinstance(err, music_errors.MusicError):
|
||||
if isinstance(err, music_errors.UserNotConnectedError):
|
||||
return await send_error_embed('{errors.join_voice_first}')
|
||||
|
||||
elif (err.original.startswith('You need to be in the right voice channel')):
|
||||
changed = False
|
||||
response = ctx.localizer.format_str('{settings_check.voicechannel}')
|
||||
for channel_id in err.channels:
|
||||
channel = ctx.guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
response += f'{channel.name}, '
|
||||
changed = True
|
||||
if changed:
|
||||
return await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
else:
|
||||
return await send_error_embed('{errors.right_channel}')
|
||||
elif isinstance(err, music_errors.BotNotConnectedError):
|
||||
return await send_error_embed('{errors.not_connected}')
|
||||
|
||||
if isinstance(err, WrongTextChannelError):
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except Exception as e:
|
||||
self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err))
|
||||
response = ctx.localizer.format_str('{settings_check.textchannel}')
|
||||
for channel_id in err.channels:
|
||||
response += f'<#{channel_id}>, '
|
||||
msg = await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
return await msg.delete(delay=5)
|
||||
elif isinstance(err, music_errors.MissingPermissionsError):
|
||||
return await send_error_embed('{errors.need_permission}')
|
||||
|
||||
elif isinstance(err, music_errors.PlayBackStatusError):
|
||||
if isinstance(err, music_errors.RequirePlayingError):
|
||||
return await send_error_embed('{not_playing}')
|
||||
if isinstance(err, music_errors.RequireListeningError):
|
||||
return await send_error_embed('{have_to_listen}')
|
||||
|
||||
elif isinstance(err, music_errors.ChannelError):
|
||||
if isinstance(err, music_errors.VoiceChannelFullError):
|
||||
return await send_error_embed('{errors.full_channel}')
|
||||
|
||||
elif isinstance(err, music_errors.UserInDifferentVoiceChannelError):
|
||||
return await send_error_embed(err.channels[0].name, title='{errors.my_channel}')
|
||||
|
||||
elif isinstance(err, music_errors.ConfiguredChannelsError):
|
||||
if isinstance(err, music_errors.WrongVoiceChannelError):
|
||||
changed = False
|
||||
response = ctx.localizer.format_str('{settings_check.voicechannel}')
|
||||
for channel_id in err.channels:
|
||||
channel = ctx.guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
response += f'{channel.name}, '
|
||||
changed = True
|
||||
if changed:
|
||||
return await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
else:
|
||||
return await send_error_embed('{errors.right_channel}')
|
||||
|
||||
if isinstance(err, music_errors.WrongTextChannelError):
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except Exception as e:
|
||||
self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err))
|
||||
response = ctx.localizer.format_str('{settings_check.textchannel}')
|
||||
for channel_id in err.channels:
|
||||
response += f'<#{channel_id}>, '
|
||||
msg = await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
return await msg.delete(delay=5)
|
||||
|
||||
if isinstance(err, commands.CheckFailure):
|
||||
pass
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Discord Packages
|
||||
from discord.ext import commands
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ...utils.userinteraction.scroller import ClearOn, Scroller
|
||||
from .helpformatter import coghelper, commandhelper, helper, prefix_cleaner
|
||||
|
||||
|
||||
class Help(commands.Cog):
|
||||
"""Help command"""
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def help(self, ctx): # Takes no args because reasons(using the view directly)
|
||||
|
||||
@@ -6,12 +6,14 @@ from lavalink import __version__ as LavalinkVersion
|
||||
import platform
|
||||
import time
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils import bot_version
|
||||
|
||||
|
||||
class Misc(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
|
||||
def get_uptime(self):
|
||||
now = time.time()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from discord import VoiceChannel
|
||||
from discord.ext import commands, tasks
|
||||
from lavalink.events import (
|
||||
NodeChangedEvent, NodeConnectedEvent, NodeDisconnectedEvent, PlayerUpdateEvent, QueueEndEvent, TrackEndEvent,
|
||||
@@ -14,6 +16,8 @@ from typing import List, Optional
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
# Bot Utilities
|
||||
from musicbot.utils.mixplayer.player import MixPlayer
|
||||
from ...utils import checks, timeformatter
|
||||
@@ -21,22 +25,26 @@ from ...utils.thumbnailer import Thumbnailer
|
||||
from ...utils.userinteraction.paginators import QueuePaginator, TextPaginator
|
||||
from ...utils.userinteraction.scroller import ClearOn, Scroller
|
||||
from ...utils.userinteraction.selector import SelectMode, Selector, SelectorButton, SelectorItem
|
||||
from .decorators import BasicVoiceClient, require_playing, require_queue, require_voice_connection, voteable
|
||||
from .music_errors import WrongTextChannelError
|
||||
from .decorators import require_playing, require_queue, require_voice_connection, voteable
|
||||
from .music_errors import MusicError, WrongTextChannelError
|
||||
from .voice_client import BasicVoiceClient
|
||||
|
||||
time_rx = re.compile('[0-9]+')
|
||||
url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+')
|
||||
|
||||
|
||||
class Music(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("Music")
|
||||
|
||||
self.thumbnailer = Thumbnailer(bot=self.bot)
|
||||
|
||||
self.leave_timer.start()
|
||||
self.bot.lavalink.add_event_hook(self.track_hook)
|
||||
if self.bot.lavalink is None:
|
||||
raise MusicError("Lavalink is not yet initialized")
|
||||
self.lavalink: lavalink.Client = self.bot.lavalink
|
||||
self.lavalink.add_event_hook(self.track_hook)
|
||||
|
||||
async def cog_check(self, ctx):
|
||||
if not ctx.guild:
|
||||
@@ -50,10 +58,17 @@ class Music(commands.Cog):
|
||||
""" Ensures a valid player exists whenever a command is run """
|
||||
# Creates a new only if one doesn't exist, ensures a valid player for all checks.
|
||||
if ctx.guild:
|
||||
self.bot.lavalink.player_manager.create(ctx.guild.id)
|
||||
self.lavalink.player_manager.create(ctx.guild.id)
|
||||
|
||||
def get_player(self, guild: discord.Guild) -> MixPlayer:
|
||||
player: Optional[MixPlayer] = self.lavalink.player_manager.get(guild.id)
|
||||
if player is None:
|
||||
self.logger.error(f"Tried to get player for guild {guild.id} but got None")
|
||||
raise MusicError(f"Tried to get player for guild {guild.id} but got None")
|
||||
return player
|
||||
|
||||
async def enqueue(self, ctx, track, embed, silent=False, check_max_length=True) -> tuple[discord.Embed, bool]:
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
# Only add tracks that don't exceed the max track length
|
||||
if maxlength := self.max_track_length(ctx.guild, player):
|
||||
@@ -96,7 +111,7 @@ class Music(commands.Cog):
|
||||
return embed, True
|
||||
|
||||
def get_current_song_embed(self, ctx, include_time=False):
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
if not player.current:
|
||||
return
|
||||
@@ -138,7 +153,7 @@ class Music(commands.Cog):
|
||||
async def _play(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query. """
|
||||
self.logger.debug("Query: %s" % query)
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
query = query.strip('<>')
|
||||
|
||||
if not url_rx.match(query):
|
||||
@@ -176,7 +191,7 @@ class Music(commands.Cog):
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _seek(self, ctx, *, time: str):
|
||||
""" Seeks to a given position in a track. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
if seconds := time_rx.search(time):
|
||||
# Convert to milliseconds, include sign
|
||||
milliseconds = int(seconds.group())*1000 * (-1 if time.startswith('-1') else 1)
|
||||
@@ -194,7 +209,7 @@ class Music(commands.Cog):
|
||||
@voteable(requester_override=True, react_to_vote=True)
|
||||
async def _skip(self, ctx):
|
||||
""" Skips the current track. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
await player.skip()
|
||||
embed = self.get_current_song_embed(ctx)
|
||||
@@ -205,7 +220,7 @@ class Music(commands.Cog):
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _skip_to(self, ctx, pos: int = 1):
|
||||
""" Plays the queue from a specific point. Disregards tracks before the pos. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
# TODO: Do all queue out of range messages the same way
|
||||
if pos < 1:
|
||||
@@ -227,7 +242,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _stop(self, ctx):
|
||||
""" Stops the player and clears its queue. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
|
||||
@@ -245,7 +260,7 @@ class Music(commands.Cog):
|
||||
@require_queue(require_member_queue=True)
|
||||
async def _queue(self, ctx, *, member: Optional[discord.Member] = None):
|
||||
""" Shows the player's queue. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
pagified_queue = QueuePaginator(ctx.localizer, player, color=ctx.me.color, member=member)
|
||||
scroller = Scroller(ctx, pagified_queue)
|
||||
await scroller.start_scrolling(ClearOn.ManualExit | ClearOn.Timeout)
|
||||
@@ -262,7 +277,7 @@ class Music(commands.Cog):
|
||||
@require_playing()
|
||||
async def _pause(self, ctx):
|
||||
""" Pauses/Resumes the current track. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
await player.set_pause(not player.paused)
|
||||
if player.paused:
|
||||
await ctx.send(ctx.localizer.format_str("{resume.paused}"))
|
||||
@@ -274,7 +289,7 @@ class Music(commands.Cog):
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _shuffle(self, ctx):
|
||||
""" Shuffles your queue. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.shuffle_user_queue(ctx.author)
|
||||
await ctx.send(ctx.localizer.format_str("{shuffle}"))
|
||||
|
||||
@@ -283,7 +298,7 @@ class Music(commands.Cog):
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _move(self, ctx):
|
||||
""" Moves a song in your queue. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
# We create a new selector for each selection
|
||||
def move_selector(ctx, queue: List[AudioTrack], title: str, first: bool):
|
||||
@@ -344,7 +359,7 @@ class Music(commands.Cog):
|
||||
@require_voice_connection()
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _remove(self, ctx):
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
user_queue: List[AudioTrack] = player.user_queue(ctx.author)
|
||||
|
||||
@@ -406,7 +421,7 @@ class Music(commands.Cog):
|
||||
@require_queue()
|
||||
async def _djremove(self, ctx, pos: int, member: Optional[discord.Member] = None):
|
||||
""" Remove a song from either the global queue or a users queue"""
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
# TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs
|
||||
queue = player.user_queue(member) if member else player.queue
|
||||
@@ -419,11 +434,13 @@ class Music(commands.Cog):
|
||||
removed = player.remove_user_track(member, pos - 1)
|
||||
|
||||
if removed:
|
||||
requester = self.bot.get_user(removed.requester)
|
||||
await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title,
|
||||
_user=requester.display_name))
|
||||
if requester := self.bot.get_user(removed.requester):
|
||||
await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title,
|
||||
_user=requester.display_name))
|
||||
else:
|
||||
self.logger.error(f"Did not get a valid user for track requester {removed.requester}")
|
||||
else:
|
||||
self.logger.error("Did not remove a track when trying to remove index {pos}, despite bounds check")
|
||||
self.logger.error(f"Did not remove a track when trying to remove index {pos}, despite bounds check")
|
||||
else:
|
||||
await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue)))
|
||||
|
||||
@@ -433,7 +450,7 @@ class Music(commands.Cog):
|
||||
@require_queue(require_member_queue=True)
|
||||
async def _user_queue_remove(self, ctx, *, member: discord.Member):
|
||||
""" Remove a song from either the global queue or a users queue"""
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.remove_user_queue(member)
|
||||
|
||||
embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color)
|
||||
@@ -444,7 +461,7 @@ class Music(commands.Cog):
|
||||
@commands.command(name='search')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _search(self, ctx, *, query):
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
self.logger.debug("Query: %s" % query)
|
||||
if not query.startswith('ytsearch:') and not query.startswith('scsearch:'):
|
||||
@@ -487,7 +504,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _disconnect(self, ctx):
|
||||
""" Disconnects the player from the voice channel and clears its queue. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
@@ -501,7 +518,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _reconnect(self, ctx):
|
||||
""" Tries to disconnect then reconnect the player in case the bot gets stuck on a song """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
current_channel = player.channel_id
|
||||
|
||||
async def recon():
|
||||
@@ -526,7 +543,7 @@ class Music(commands.Cog):
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _volume(self, ctx, volume: Optional[int] = None):
|
||||
""" Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
if not volume:
|
||||
return await ctx.send(f'🔈 | {player.volume}%')
|
||||
@@ -547,7 +564,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _forceplay(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
query = query.strip('<>')
|
||||
|
||||
if not url_rx.match(query):
|
||||
@@ -584,7 +601,7 @@ class Music(commands.Cog):
|
||||
async def _forcedisconnect(self, ctx):
|
||||
""" Attempts to force disconnect the bot without checking if it is connected initially. """
|
||||
try:
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
except Exception as e:
|
||||
@@ -600,7 +617,7 @@ class Music(commands.Cog):
|
||||
@require_voice_connection()
|
||||
async def _normalize(self, ctx):
|
||||
""" Reset the equalizer and volume """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
await player.set_volume(100)
|
||||
await player.bassboost(False)
|
||||
@@ -613,7 +630,7 @@ class Music(commands.Cog):
|
||||
@checks.dj_or(alone=True)
|
||||
async def _boost(self, ctx, boost: bool = False):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
if boost is not None:
|
||||
await player.bassboost(boost)
|
||||
@@ -628,7 +645,7 @@ class Music(commands.Cog):
|
||||
@checks.dj_or(alone=True)
|
||||
async def _nightcore(self, ctx, boost: bool = False):
|
||||
""" Set a filter mimicking nightcore the music """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
await player.nightcoreify(boost)
|
||||
|
||||
@@ -640,7 +657,7 @@ class Music(commands.Cog):
|
||||
@commands.command(name='history')
|
||||
async def _history(self, ctx):
|
||||
""" Show the last 10 songs played """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
history = player.get_history()
|
||||
if not history:
|
||||
embed = discord.Embed(description='{history.empty}', color=ctx.me.color)
|
||||
@@ -667,7 +684,7 @@ class Music(commands.Cog):
|
||||
async def _lyrics(self, ctx, *query: str):
|
||||
|
||||
# TODO:
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
genius_access_token = self.bot.APIkeys.get('genius', None)
|
||||
|
||||
@@ -734,7 +751,7 @@ class Music(commands.Cog):
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _scrub(self, ctx):
|
||||
""" Shows a set of controls which can be used to skip forward or backwards in the song """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
controls = '{scrub.controls}'
|
||||
|
||||
def seek_callback(player: MixPlayer, seconds):
|
||||
@@ -766,7 +783,7 @@ class Music(commands.Cog):
|
||||
async def _loop(self, ctx):
|
||||
# This is done using subcommands to have separate vote counts for starting and stopping.
|
||||
if ctx.invoked_subcommand is None:
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
embed = discord.Embed(color=ctx.me.color, title='Loop status')
|
||||
|
||||
if player.looping:
|
||||
@@ -780,7 +797,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _loop_start(self, ctx):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.enable_looping(True)
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
embed.description = 'Looping turned on'
|
||||
@@ -792,7 +809,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _loop_stop(self, ctx):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.enable_looping(False)
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
embed.description = 'Looping turned off'
|
||||
@@ -800,19 +817,21 @@ class Music(commands.Cog):
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def cog_unload(self):
|
||||
self.bot.lavalink._event_hooks.clear()
|
||||
self.lavalink._event_hooks.clear()
|
||||
|
||||
async def track_hook(self, event):
|
||||
if isinstance(event, TrackEndEvent):
|
||||
channel = self.bot.get_channel(event.player.fetch('channel'))
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(channel.guild.id)
|
||||
player.skip_voters.clear()
|
||||
if channel := self.bot.get_channel(event.player.fetch('channel')):
|
||||
if isinstance(channel, VoiceChannel):
|
||||
player = self.get_player(channel.guild)
|
||||
player.skip_voters.clear()
|
||||
|
||||
if isinstance(event, TrackStartEvent):
|
||||
pass
|
||||
if isinstance(event, QueueEndEvent):
|
||||
channel = self.bot.get_channel(event.player.fetch('channel'))
|
||||
await self.check_leave_voice(channel.guild)
|
||||
if channel := self.bot.get_channel(event.player.fetch('channel')):
|
||||
if isinstance(channel, VoiceChannel):
|
||||
await self.check_leave_voice(channel.guild)
|
||||
if isinstance(event, PlayerUpdateEvent):
|
||||
pass
|
||||
if isinstance(event, NodeDisconnectedEvent):
|
||||
@@ -827,16 +846,18 @@ class Music(commands.Cog):
|
||||
@commands.Cog.listener()
|
||||
async def on_voice_state_update(self, member: discord.Member, _: discord.VoiceState, after: discord.VoiceState):
|
||||
""" Updates listeners when the bot or a user changes voice state """
|
||||
if self.bot.user is None:
|
||||
return # Bot not logged in
|
||||
if member.id == self.bot.user.id and after.channel is not None:
|
||||
voice_channel = after.channel
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(member.guild.id)
|
||||
player = self.get_player(member.guild)
|
||||
player.clear_listeners()
|
||||
for member in voice_channel.members:
|
||||
if not member.bot:
|
||||
player.update_listeners(member, member.voice)
|
||||
|
||||
if not member.bot:
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(member.guild.id)
|
||||
player = self.get_player(member.guild)
|
||||
if player is not None:
|
||||
player.update_listeners(member, after)
|
||||
await self.check_leave_voice(member.guild)
|
||||
@@ -844,7 +865,7 @@ class Music(commands.Cog):
|
||||
async def check_leave_voice(self, guild: discord.Guild):
|
||||
""" Checks if the bot should leave the voice channel """
|
||||
# TODO, disconnect timer?
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(guild.id)
|
||||
player = self.get_player(guild)
|
||||
if len(player.listeners) == 0 and player.is_connected:
|
||||
if player.queue.empty and player.current is None:
|
||||
await player.stop()
|
||||
@@ -852,8 +873,9 @@ class Music(commands.Cog):
|
||||
await voice_client.disconnect(force=False)
|
||||
|
||||
async def leave_check(self):
|
||||
for player_id in self.bot.lavalink.player_manager.players:
|
||||
await self.check_leave_voice(self.bot.get_guild(player_id))
|
||||
for player_id in self.lavalink.player_manager.players:
|
||||
if guild := self.bot.get_guild(player_id):
|
||||
await self.check_leave_voice(guild)
|
||||
|
||||
@tasks.loop(seconds=10.0)
|
||||
async def leave_timer(self):
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import math
|
||||
|
||||
# Bot Utilities
|
||||
from musicbot.utils.mixplayer.player import MixPlayer
|
||||
from ...utils.checks import is_dj
|
||||
from .music_errors import WrongVoiceChannelError
|
||||
|
||||
|
||||
class BasicVoiceClient(discord.VoiceClient):
|
||||
def __init__(self, client: discord.Client, channel: discord.abc.Connectable):
|
||||
self.client = client
|
||||
self.channel = channel
|
||||
if hasattr(self.client, 'lavalink'):
|
||||
self.lavalink = self.client.lavalink
|
||||
else:
|
||||
raise Exception("client did not have defined lavalink before connect")
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_SERVER_UPDATE', 'd': data})
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_STATE_UPDATE', 'd': data})
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False,
|
||||
self_mute: bool = False) -> None:
|
||||
await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf)
|
||||
|
||||
async def disconnect(self, *, force: bool = False) -> None:
|
||||
player = self.lavalink.player_manager.get(self.channel.guild.id)
|
||||
|
||||
# no need to disconnect if we are not connected
|
||||
if not force and not player.is_connected:
|
||||
return
|
||||
|
||||
# None means disconnect
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
player.channel_id = None
|
||||
self.cleanup()
|
||||
from . import music_errors
|
||||
from .voice_client import BasicVoiceClient
|
||||
|
||||
|
||||
def require_voice_connection(should_connect=False):
|
||||
@@ -53,36 +23,39 @@ def require_voice_connection(should_connect=False):
|
||||
async def ensure_voice_inner(self, ctx, *command_args, **kwargs):
|
||||
""" This check ensures that the bot and command author are in the same voicechannel. """
|
||||
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
if not player:
|
||||
raise music_errors.MusicError("ensure voice could not get lavalink player")
|
||||
|
||||
if not ctx.author.voice or not ctx.author.voice.channel:
|
||||
raise commands.CommandInvokeError('Join a voicechannel first.')
|
||||
raise music_errors.UserNotConnectedError('Join a voicechannel first.')
|
||||
|
||||
if not player.is_connected:
|
||||
if not should_connect:
|
||||
raise commands.CommandInvokeError('Not connected.')
|
||||
raise music_errors.BotNotConnectedError('Not connected.')
|
||||
|
||||
user_voice_channel = ctx.author.voice.channel
|
||||
permissions = user_voice_channel.permissions_for(ctx.me)
|
||||
|
||||
if not permissions.connect or not permissions.speak: # Check user limit too?
|
||||
raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.')
|
||||
raise music_errors.MissingPermissionsError('I need the `CONNECT` and `SPEAK` permissions.')
|
||||
|
||||
if (len(user_voice_channel.members) == user_voice_channel.user_limit) and not permissions.administrator:
|
||||
raise commands.CommandInvokeError('The channel is currently full')
|
||||
raise music_errors.VoiceChannelFullError('The channel is currently full', user_voice_channel)
|
||||
|
||||
# Check against music channel restrictions in bot settings
|
||||
if voice_channels := self.bot.settings.get(ctx.guild, 'channels.music', []):
|
||||
if user_voice_channel.id not in voice_channels:
|
||||
raise WrongVoiceChannelError(
|
||||
raise music_errors.WrongVoiceChannelError(
|
||||
'You need to be in the right voice channel', channels=voice_channels)
|
||||
|
||||
player.store('channel', ctx.channel.id)
|
||||
await ctx.author.voice.channel.connect(cls=BasicVoiceClient)
|
||||
|
||||
elif int(player.channel_id) != ctx.author.voice.channel.id:
|
||||
elif player.channel_id and int(player.channel_id) != ctx.author.voice.channel.id:
|
||||
bot_channel = self.bot.get_channel(int(player.channel_id))
|
||||
raise WrongVoiceChannelError(
|
||||
'You need to be in my voice channel', channels=[bot_channel])
|
||||
raise music_errors.UserInDifferentVoiceChannelError('You need to be in my voice channel',
|
||||
channel=bot_channel)
|
||||
|
||||
await func(self, ctx, *command_args, **kwargs)
|
||||
|
||||
@@ -102,10 +75,10 @@ def require_playing(require_user_listening=False):
|
||||
if player := self.bot.lavalink.player_manager.get(ctx.guild.id):
|
||||
|
||||
if not player.is_playing:
|
||||
raise commands.CommandInvokeError('Not playing')
|
||||
raise music_errors.RequirePlayingError('Not playing')
|
||||
|
||||
if require_user_listening and (ctx.author not in player.listeners):
|
||||
raise commands.CommandInvokeError('Not listening')
|
||||
raise music_errors.RequireListeningError('Not listening')
|
||||
|
||||
else:
|
||||
raise Exception('Player object does not yet exist')
|
||||
|
||||
@@ -1,20 +1,72 @@
|
||||
# define Python user-defined exceptions
|
||||
# Discord Packages
|
||||
from discord.ext.commands import CommandError
|
||||
|
||||
from typing import Optional
|
||||
|
||||
class WrongChannelError(CommandError):
|
||||
def __init__(self, e: str, channels=None):
|
||||
self.original = e
|
||||
|
||||
class MusicError(CommandError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
self.inner_error = inner_error
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class MissingPermissionsError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class BotNotConnectedError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class UserNotConnectedError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class PlayBackStatusError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class RequirePlayingError(PlayBackStatusError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class RequireListeningError(PlayBackStatusError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
# Channel errors
|
||||
class ChannelError(MusicError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
self.channels = channels
|
||||
super().__init__()
|
||||
super().__init__(message=message, inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongVoiceChannelError(WrongChannelError):
|
||||
def __init__(self, e: str, channels=None):
|
||||
super().__init__(e, channels=channels)
|
||||
class VoiceChannelFullError(ChannelError):
|
||||
def __init__(self, message: str, channel, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=[channel], inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongTextChannelError(WrongChannelError):
|
||||
def __init__(self, e: str, channels=None):
|
||||
super().__init__(e, channels=channels)
|
||||
class UserInDifferentVoiceChannelError(ChannelError):
|
||||
def __init__(self, message: str, channel, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=[channel], inner_error=inner_error)
|
||||
|
||||
|
||||
class ConfiguredChannelsError(ChannelError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=channels, inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongVoiceChannelError(ConfiguredChannelsError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=channels, inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongTextChannelError(ConfiguredChannelsError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=channels, inner_error=inner_error)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
from bot import MusicBot
|
||||
from musicbot.cogs.music.music_errors import MusicError
|
||||
|
||||
|
||||
class BasicVoiceClient(discord.VoiceClient):
|
||||
def __init__(self, client: MusicBot, channel: discord.VoiceChannel):
|
||||
self.music_bot = client
|
||||
self.channel = channel
|
||||
if self.music_bot.lavalink:
|
||||
self.lavalink = self.music_bot.lavalink
|
||||
else:
|
||||
raise MusicError("client did not have defined lavalink before connect")
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_SERVER_UPDATE', 'd': data})
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_STATE_UPDATE', 'd': data})
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False,
|
||||
self_mute: bool = False) -> None:
|
||||
await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf)
|
||||
|
||||
async def disconnect(self, *, force: bool = False) -> None:
|
||||
player = self.lavalink.player_manager.get(self.channel.guild.id)
|
||||
|
||||
if player:
|
||||
# no need to disconnect if we are not connected
|
||||
if not force and not player.is_connected:
|
||||
return
|
||||
|
||||
# None means disconnect
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
player.channel_id = None
|
||||
self.cleanup()
|
||||
@@ -1,12 +1,15 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from discord.ext import commands
|
||||
from lavalink import Client, Node
|
||||
|
||||
import codecs
|
||||
|
||||
import yaml
|
||||
|
||||
from bot import MusicBot
|
||||
from musicbot.cogs.music.music_errors import MusicError
|
||||
|
||||
# Bot Utilities
|
||||
from musicbot.utils.settingsmanager import Settings
|
||||
from ..utils.mixplayer import MixPlayer
|
||||
@@ -15,8 +18,8 @@ from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class NodeManager(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.settings: Settings = self.bot.settings
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("NodeManager")
|
||||
|
||||
@@ -25,8 +28,8 @@ class NodeManager(commands.Cog):
|
||||
'musicbot.cogs.music',
|
||||
]
|
||||
|
||||
if not hasattr(self.bot, 'lavalink'):
|
||||
self.bot.lavalink = Client(self.bot.user.id, player=MixPlayer)
|
||||
if self.bot.lavalink is None and self.bot.user:
|
||||
self.bot.lavalink = lavalink.Client(self.bot.user.id, player=MixPlayer)
|
||||
|
||||
self.load_nodes_from_file()
|
||||
|
||||
@@ -38,6 +41,9 @@ class NodeManager(commands.Cog):
|
||||
await self.bot.load_extension(extension)
|
||||
except Exception:
|
||||
self.logger.exception("Loading of extension %s failed" % extension)
|
||||
else:
|
||||
self.logger.error("Failed to initialize lavalink client")
|
||||
raise MusicError("Failed to initialize lavalink client")
|
||||
|
||||
def load_nodes_from_file(self):
|
||||
with codecs.open(f"{self.bot.datadir}/config.yaml", 'r', encoding='utf8') as f:
|
||||
@@ -46,6 +52,9 @@ class NodeManager(commands.Cog):
|
||||
name_cache = []
|
||||
new_nodes = []
|
||||
|
||||
if self.bot.lavalink is None:
|
||||
return new_nodes
|
||||
|
||||
for node in self.bot.lavalink.node_manager.nodes:
|
||||
name_cache.append(node.name)
|
||||
|
||||
@@ -93,7 +102,7 @@ class NodeManager(commands.Cog):
|
||||
embed.add_field(name=f'{await self._regioner(n.region)} **Name:** {n.name}',
|
||||
value=f'**Host:** {n.host}\n **Port:** {n.port}')
|
||||
|
||||
if isinstance(node, Node):
|
||||
if isinstance(node, lavalink.Node):
|
||||
embed.add_field(name=f'{await self._regioner(node.region)} **Name:** {node.name}',
|
||||
value=f'**Host:** {node.host}\n **Port:** {node.port}')
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils import checks
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class Settings(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.settings = self.bot.settings
|
||||
|
||||
def format_prefixes(self, prefixes):
|
||||
|
||||
@@ -4,6 +4,7 @@ import discord
|
||||
import codecs
|
||||
import locale as localee
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -53,7 +54,7 @@ class Settings():
|
||||
with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f:
|
||||
yaml.dump(self.settings, f, indent=2)
|
||||
|
||||
def get(self, identifier, setting, default=''):
|
||||
def get(self, identifier, setting, default: Any = '') -> Any:
|
||||
""" Gets a value from the settings if a default return value is specified
|
||||
it will return the default if no setting is found. If that default is an
|
||||
attribute of the settings class, the value of the attribute will get returned."""
|
||||
|
||||
@@ -2,12 +2,12 @@ from time import perf_counter
|
||||
|
||||
from bs4 import BeautifulSoup as bs4
|
||||
|
||||
from bot import Bot
|
||||
from bot import MusicBot
|
||||
|
||||
|
||||
class Thumbnailer(object):
|
||||
def __init__(self, bot: Bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("Thumbnailer")
|
||||
|
||||
async def get_html(self, url: str):
|
||||
|
||||
@@ -7,6 +7,8 @@ import asyncio
|
||||
from enum import Flag, auto
|
||||
from typing import List, Optional
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from .paginators import BasePaginator, CantScrollException
|
||||
|
||||
|
||||
@@ -42,7 +44,7 @@ class Scroller:
|
||||
if not self.paginator.pages:
|
||||
raise Exception("Paginator contained no pages to display") # TODO: proper error
|
||||
|
||||
self.bot = ctx.bot
|
||||
self.bot: MusicBot = ctx.bot
|
||||
self.channel = ctx.channel
|
||||
self.message: Optional[discord.Message] = None
|
||||
|
||||
|
||||
Reference in new issue
Block a user