Merge branch 'queue_pagify' into 'master'

Queue scroller, vote skipping.

See merge request Ev-1/shite-music-bot!5
This commit is contained in:
Ev-1 committed 2019-01-13 16:41:45 +00:00
commit 5748823cd4
6 files changed
+842 -510

No files matched your search

+12 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
+191
View File
@@ -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)
+72 -5
View File
@@ -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
+16 -6
View File
@@ -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