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