mirror of
https://github.com/UiO-Gaming/ShiteMusicBot.git
synced 2026-11-05 13:56:51 +01:00
Implement Selector that inherits from scroller for better code reuse
This commit is contained in:
1 parent
28bbb1effa
commit
8b35088d04
4 files changed
+324
-201
No files matched your search
@@ -1,6 +1,8 @@
|
||||
# Discord Packages
|
||||
from discord.ext import commands
|
||||
|
||||
from musicbot.utils.userinteraction.scroller import ScrollClearSettings
|
||||
|
||||
from ...utils.userinteraction import Scroller
|
||||
from .helpformatter import coghelper, commandhelper, helper, prefix_cleaner
|
||||
|
||||
@@ -32,5 +34,6 @@ class Help(commands.Cog):
|
||||
else:
|
||||
paginator = await coghelper(ctx, thing)
|
||||
|
||||
scroller = Scroller(ctx, paginator)
|
||||
scroller = Scroller(ctx, paginator,
|
||||
ScrollClearSettings.OnTimeout | ScrollClearSettings.OnInteractionExit)
|
||||
await scroller.start_scrolling()
|
||||
+160
-151
@@ -1,24 +1,25 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
import asyncio
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
from ...utils import thumbnailer, timeformatter
|
||||
from .music_errors import WrongTextChannelError
|
||||
import re
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from ...utils import checks
|
||||
from ...utils.userinteraction import Scroller, TextPaginator
|
||||
from .decorators import require_playing, require_voice_connection, voteable
|
||||
from ...utils import checks, timeformatter
|
||||
from ...utils.userinteraction import QueuePaginator, Scroller, Selector
|
||||
from musicbot.utils.userinteraction.scroller import ScrollClearSettings
|
||||
|
||||
from ...utils import checks, thumbnailer, timeformatter
|
||||
from ...utils.userinteraction import QueuePaginator, Scroller, Selector, TextPaginator
|
||||
from ...utils.userinteraction.selector import Selector2, SelectorButton, SelectorItem
|
||||
from .decorators import require_playing, require_queue, require_voice_connection, voteable
|
||||
from .music_errors import WrongTextChannelError
|
||||
|
||||
time_rx = re.compile('[0-9]+')
|
||||
url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+')
|
||||
|
||||
# Discord Packages
|
||||
import lavalink.events
|
||||
|
||||
|
||||
@@ -45,7 +46,7 @@ class Music(commands.Cog):
|
||||
# Creates a new only if one doesn't exist, ensures a valid player for all checks.
|
||||
self.bot.lavalink.player_manager.create(ctx.guild.id)
|
||||
|
||||
async def enqueue(self, ctx, track, embed, silent=False, check_max_length=True):
|
||||
async def enqueue(self, ctx, track, embed, silent=False, check_max_length=True) -> tuple[discord.Embed, bool]:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
# Only add tracks that don't exceed the max track length
|
||||
@@ -61,7 +62,7 @@ class Music(commands.Cog):
|
||||
return embed, False
|
||||
|
||||
# Add thumbnail, turn track into track class
|
||||
thumbnail_url = await thumbnailer.ThumbNailer.identify(self, track.identifier, track.uri)
|
||||
thumbnail_url = None #await thumbnailer.ThumbNailer.identify(self, track.identifier, track.uri)
|
||||
track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url)
|
||||
|
||||
# Add to player
|
||||
@@ -127,14 +128,11 @@ class Music(commands.Cog):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@commands.command(name='play')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _play(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query. """
|
||||
self.logger.debug("Query: %s" % query)
|
||||
print(query)
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
query = query.strip('<>')
|
||||
|
||||
@@ -250,7 +248,7 @@ class Music(commands.Cog):
|
||||
""" Shows the player's queue. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
pagified_queue = QueuePaginator(ctx.localizer, player, color=ctx.me.color, member=member)
|
||||
scroller = Scroller(ctx, pagified_queue)
|
||||
scroller = Scroller(ctx, pagified_queue, ScrollClearSettings.OnInteractionExit | ScrollClearSettings.OnTimeout)
|
||||
await scroller.start_scrolling()
|
||||
|
||||
|
||||
@@ -314,53 +312,52 @@ class Music(commands.Cog):
|
||||
|
||||
selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, round_titles=round_titles,
|
||||
color=ctx.me.color, title=ctx.localizer.format_str("{moved.choose}"))
|
||||
|
||||
|
||||
# Get the initial and final position
|
||||
message, page, selections = await selector.start_scrolling()
|
||||
|
||||
|
||||
try:
|
||||
pos_initial, pos_final = selections[0], selections[1]
|
||||
except (IndexError, TypeError):
|
||||
return await message.delete()
|
||||
|
||||
|
||||
# Move the track
|
||||
moved = player.move_user_track(ctx.author.id, pos_initial, pos_final)
|
||||
|
||||
|
||||
# Create a nice embed explaining what happened
|
||||
song = f'**[{moved.title}]({moved.uri})**'
|
||||
embed = discord.Embed(color=ctx.me.color, description=song, title='{moved.moved}')
|
||||
thumbnail_url = None #moved.extra["thumbnail_url"]
|
||||
member = ctx.guild.get_member(moved.requester)
|
||||
|
||||
if thumbnail_url:
|
||||
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
|
||||
|
||||
embed.add_field(name="{moved.local}", value=f"`{pos_initial + 1} → {pos_final + 1}`", inline=True)
|
||||
embed.add_field(name="{moved.global}", value=f"`{player.queue._loc_to_glob(ctx.author.id, pos_initial) + 1}\
|
||||
→ {player.queue._loc_to_glob(ctx.author.id, pos_final) + 1}`", inline=True)
|
||||
→ {player.queue._loc_to_glob(ctx.author.id, pos_final) + 1}`", inline=True)
|
||||
embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.display_avatar.url)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
|
||||
|
||||
await message.edit(embed=embed)
|
||||
|
||||
|
||||
|
||||
@commands.command(name='remove')
|
||||
@require_voice_connection()
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _remove(self, ctx):
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
user_queue = player.user_queue(ctx.author.id, dual=True)
|
||||
functions = [player.remove_user_track]*len(user_queue)
|
||||
arguments = [(ctx.author.id, i) for i in range(len(user_queue))]
|
||||
identifiers = []
|
||||
|
||||
|
||||
for index, temp in enumerate(user_queue):
|
||||
track, globpos = temp
|
||||
identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1,
|
||||
_globalindex=globpos+1, _title=track.title,
|
||||
_uri=track.uri))
|
||||
|
||||
_globalindex=globpos+1, _title=track.title,
|
||||
_uri=track.uri))
|
||||
|
||||
selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, color=0xFF0000,
|
||||
title='Remove song plz')
|
||||
message, _, removed = await selector.start_scrolling()
|
||||
@@ -368,8 +365,7 @@ class Music(commands.Cog):
|
||||
await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None)
|
||||
else:
|
||||
await message.delete()
|
||||
|
||||
|
||||
|
||||
@commands.command(name="DJremove")
|
||||
@checks.dj_or()
|
||||
@require_voice_connection()
|
||||
@@ -377,23 +373,22 @@ class Music(commands.Cog):
|
||||
async def _djremove(self, ctx, pos: int, member: discord.Member = None):
|
||||
""" Remove a song from either the global queue or a users queue"""
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
# TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs
|
||||
queue = player.user_queue(member.id) if member else player.queue
|
||||
|
||||
|
||||
# TODO: Do all queue out of range messages the same way
|
||||
if pos <= len(queue) and pos >= 1:
|
||||
if member is None:
|
||||
removed = player.remove_global_track(pos - 1)
|
||||
else:
|
||||
removed = player.remove_user_track(member.id, pos - 1)
|
||||
|
||||
|
||||
requester = self.bot.get_user(removed.requester)
|
||||
await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.display_name))
|
||||
else:
|
||||
await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue)))
|
||||
|
||||
|
||||
|
||||
@commands.command(name='removeuser')
|
||||
@checks.dj_or(alone=True)
|
||||
@require_voice_connection()
|
||||
@@ -402,73 +397,103 @@ class Music(commands.Cog):
|
||||
""" Remove a song from either the global queue or a users queue"""
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player.remove_user_queue(member.id)
|
||||
|
||||
|
||||
embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed, _id=member.id)
|
||||
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
@commands.command(name='search')
|
||||
|
||||
@commands.command(name='search2')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _search(self, ctx, *, query):
|
||||
async def _search2(self, ctx, *, query):
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
if not query.startswith('ytsearch:') and not query.startswith('scsearch:'):
|
||||
query = 'ytsearch:' + query
|
||||
|
||||
|
||||
results = await player.node.get_tracks(query)
|
||||
|
||||
|
||||
embed = discord.Embed(description='{nothing_found}', color=0x36393F)
|
||||
if not results or not results['tracks']:
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
|
||||
buttons = []
|
||||
for track in results['tracks']:
|
||||
duration = timeformatter.format_ms(int(track.duration))
|
||||
interaction = SelectorItem(f'[{track.title}]({track.uri}) `{duration}`',
|
||||
wrap_in_button_callback(self.enqueue, ctx, track, discord.Embed(color=ctx.me.color)))
|
||||
buttons.append(interaction)
|
||||
|
||||
search_selector = Selector2(ctx, buttons, color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
|
||||
message, callback_results = await search_selector.start_scrolling()
|
||||
|
||||
song_embed, song_added = callback_results[0]
|
||||
if song_added:
|
||||
embed = ctx.localizer.format_embed(song_embed)
|
||||
await message.edit(embed=embed, view=None)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
|
||||
@commands.command(name='search')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _search(self, ctx, *, query):
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
if not query.startswith('ytsearch:') and not query.startswith('scsearch:'):
|
||||
query = 'ytsearch:' + query
|
||||
|
||||
results = await player.node.get_tracks(query)
|
||||
|
||||
embed = discord.Embed(description='{nothing_found}', color=0x36393F)
|
||||
if not results or not results['tracks']:
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
tracks = results['tracks']
|
||||
result_count = min(len(tracks), 15)
|
||||
|
||||
|
||||
# Setup for the selector
|
||||
identifiers = []
|
||||
functions = [self.enqueue]*result_count
|
||||
arguments = [(ctx, tracks[i], discord.Embed(color=ctx.me.color)) for i in range(result_count)]
|
||||
|
||||
|
||||
for index, track in enumerate(tracks, start=1):
|
||||
duration = timeformatter.format_ms(int(track.duration))
|
||||
identifiers.append(f'`{index}.` [{track.title}]({track.uri}) `{duration}`')
|
||||
if index == result_count:
|
||||
break
|
||||
|
||||
|
||||
search_selector = Selector(ctx, identifiers, functions, arguments, num_selections=5,
|
||||
color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
|
||||
color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
|
||||
# Let the user scroll through results
|
||||
message, current_page, result = await search_selector.start_scrolling()
|
||||
|
||||
|
||||
if result:
|
||||
(embed, added) = result
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await message.edit(embed=embed)
|
||||
|
||||
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
else:
|
||||
await message.delete()
|
||||
|
||||
|
||||
|
||||
@commands.command(name='disconnect')
|
||||
@require_voice_connection()
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _disconnect(self, ctx):
|
||||
""" Disconnects the player from the voice channel and clears its queue. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
await self.voice_client.disconnect()
|
||||
embed = discord.Embed(description='{disconnect.disconnected}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@commands.command(name='reconnect')
|
||||
@require_voice_connection()
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
@@ -476,13 +501,13 @@ class Music(commands.Cog):
|
||||
""" Tries to disconnect then reconnect the player in case the bot gets stuck on a song """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
current_channel = player.channel_id
|
||||
|
||||
|
||||
async def recon():
|
||||
await player.stop()
|
||||
await self.voice_client.disconnect()
|
||||
await asyncio.sleep(1) # Pretend stuff is happening/give everything some time to reset.
|
||||
await self.voice_client.connect()#//connect_to(ctx.guild.id, current_channel)
|
||||
|
||||
|
||||
if player.current:
|
||||
track = player.current
|
||||
start_time = player.position
|
||||
@@ -491,30 +516,28 @@ class Music(commands.Cog):
|
||||
await player.play(track, start_time=start_time)
|
||||
else:
|
||||
await recon()
|
||||
|
||||
|
||||
|
||||
@commands.command(name='volume')
|
||||
@checks.dj_or(alone=True, track_requester=True)
|
||||
@require_playing(require_user_listening=True)
|
||||
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.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
if not volume:
|
||||
return await ctx.send(f'🔈 | {player.volume}%')
|
||||
|
||||
|
||||
is_dj = checks.is_dj(ctx)
|
||||
is_admin = getattr(ctx.author.guild_permissions, 'administrator', None) is True
|
||||
if player.current and (int(player.current.requester) == ctx.author.id and not is_dj and not is_admin):
|
||||
if not 50 <= volume <= 125:
|
||||
return await ctx.send(ctx.localizer.format_str("{volume.out_of_range}"))
|
||||
await player.set_volume(volume)
|
||||
|
||||
|
||||
embed = discord.Embed(description="{volume.set_to}", color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed, _volume=player.volume)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@commands.command(name='forceplay')
|
||||
@require_voice_connection(should_connect=True)
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
@@ -522,37 +545,36 @@ class Music(commands.Cog):
|
||||
""" Searches and plays a song from a given query. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
query = query.strip('<>')
|
||||
|
||||
|
||||
if not url_rx.match(query):
|
||||
query = f'ytsearch:{query}'
|
||||
|
||||
|
||||
results = await player.node.get_tracks(query)
|
||||
|
||||
|
||||
if not results or not results.tracks:
|
||||
return await ctx.send(ctx.localizer.format_str("{nothing_found}"))
|
||||
|
||||
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
|
||||
|
||||
if results.load_type == 'PLAYLIST_LOADED':
|
||||
numtracks = 0
|
||||
for track in results.tracks:
|
||||
_, track_added = await self.enqueue(ctx, track, embed, silent=True, check_max_length=False)
|
||||
if track_added:
|
||||
numtracks += 1
|
||||
|
||||
|
||||
embed.title = '{playlist_enqued}'
|
||||
embed.description = f'{results.playlist_info.name} - {numtracks} {{tracks}}'
|
||||
else:
|
||||
track = results.tracks[0]
|
||||
await self.enqueue(ctx, track, embed, check_max_length=False)
|
||||
|
||||
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
|
||||
|
||||
|
||||
@commands.command(name='forcedisconnect')
|
||||
@checks.dj_or(alone=True)
|
||||
async def _forcedisconnect(self, ctx):
|
||||
@@ -567,44 +589,36 @@ class Music(commands.Cog):
|
||||
embed = discord.Embed(description='{disconnect.disconnected}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
# commands
|
||||
# from .basic_commands import (
|
||||
# _disconnect, _djremove, _forcedisconnect, _forceplay, _move, _myqueue, _now, _pause, _play, _queue, _reconnect,
|
||||
# _remove, _search, _seek, _shuffle, _skip, _skip_to, _stop, _user_queue_remove, _volume)
|
||||
|
||||
|
||||
|
||||
|
||||
@commands.command(name='normalize')
|
||||
@checks.dj_or(alone=True)
|
||||
@require_voice_connection()
|
||||
async def _normalize(self, ctx):
|
||||
""" Reset the equalizer and volume """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
await player.set_volume(100)
|
||||
await player.bassboost(False)
|
||||
|
||||
|
||||
embed = discord.Embed(description='{volume.reset}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@commands.command(name='boost')
|
||||
@checks.dj_or(alone=True)
|
||||
async def _boost(self, ctx, boost: bool = None):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
if boost is not None:
|
||||
await player.bassboost(boost)
|
||||
|
||||
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
embed.description = '{boost.on}' if player.boosted else '{boost.off}'
|
||||
|
||||
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@commands.command(name='history')
|
||||
async def _history(self, ctx):
|
||||
""" Show the last 10 songs played """
|
||||
@@ -616,88 +630,86 @@ class Music(commands.Cog):
|
||||
return await ctx.send(embed=embed)
|
||||
track = history[0]
|
||||
description = ctx.localizer.format_str("{history.current}", _title=track.title, _uri=track.uri,
|
||||
_id=track.requester) + '\n\n'
|
||||
_id=track.requester) + '\n\n'
|
||||
description += ctx.localizer.format_str("{history.previous}", _len=len(history)-1) + '\n'
|
||||
thumbnail_url = None # track.extra["thumbnail_url"]
|
||||
for index, track in enumerate(history[1:], start=1):
|
||||
description += ctx.localizer.format_str("{history.track}", _index=-index, _title=track.title,
|
||||
_uri=track.uri, _id=track.requester) + '\n'
|
||||
|
||||
_uri=track.uri, _id=track.requester) + '\n'
|
||||
|
||||
embed = discord.Embed(title=ctx.localizer.format_str('{history.title}'), color=ctx.me.color,
|
||||
description=description)
|
||||
|
||||
#if thumbnail_url:
|
||||
# embed.set_thumbnail(url=thumbnail_url)
|
||||
description=description)
|
||||
|
||||
if thumbnail_url:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@commands.cooldown(1, 5, commands.BucketType.guild)
|
||||
@commands.command(name='lyrics')
|
||||
async def _lyrics(self, ctx, *query: str):
|
||||
|
||||
|
||||
# TODO:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
genius_access_token = self.bot.APIkeys.get('genius', None)
|
||||
|
||||
|
||||
if genius_access_token is None:
|
||||
return await ctx.send('Missing API key')
|
||||
|
||||
|
||||
excluded_words = ['music', 'video', 'version', 'original', 'lyrics', 'lyric',
|
||||
'official', 'live', 'instrumental', 'audio', 'hd']
|
||||
|
||||
'official', 'live', 'instrumental', 'audio', 'hd']
|
||||
|
||||
query = ' '.join(query)
|
||||
|
||||
|
||||
if not query and player.is_playing:
|
||||
query = player.current.title
|
||||
query = re.sub(r'[-()_\[\]]', '', query)
|
||||
filtered_words = [word for word in query.split() if word.lower() not in excluded_words]
|
||||
query = ' '.join(filtered_words)
|
||||
|
||||
|
||||
async def get_site_content(url):
|
||||
async with self.bot.session.get(url) as resp:
|
||||
response = await resp.read()
|
||||
return response.decode('utf-8')
|
||||
|
||||
|
||||
embed = discord.Embed(description=':mag_right:')
|
||||
status_msg = await ctx.send(embed=embed)
|
||||
|
||||
|
||||
try:
|
||||
url = 'https://api.genius.com/search?' + urllib.parse.urlencode(
|
||||
{'access_token': genius_access_token, 'q': query})
|
||||
{'access_token': genius_access_token, 'q': query})
|
||||
result = await get_site_content(url)
|
||||
response = json.loads(result)
|
||||
song_id = response['response']['hits'][0]['result']['id']
|
||||
except Exception:
|
||||
embed = discord.Embed(description=':x:', color=0xFF0000)
|
||||
return await status_msg.edit(embed=embed)
|
||||
|
||||
|
||||
result = await get_site_content(f'https://api.genius.com/songs/{song_id}?access_token={genius_access_token}')
|
||||
song = json.loads(result)['response']['song']
|
||||
|
||||
|
||||
response = await get_site_content(song['url'])
|
||||
scraped_data = BeautifulSoup(response, 'html.parser')
|
||||
lyrics = scraped_data.find(class_='lyrics').get_text()
|
||||
|
||||
|
||||
paginator = TextPaginator(max_size=2000, max_lines=50, color=0xFFFF64)
|
||||
for line in lyrics.split('\n'):
|
||||
paginator.add_line(line)
|
||||
|
||||
|
||||
await status_msg.delete()
|
||||
|
||||
|
||||
paginator.pages[0].url = song['url']
|
||||
paginator.pages[0].title = song['full_title']
|
||||
paginator.pages[0].set_thumbnail(url=song['header_image_thumbnail_url'])
|
||||
paginator.pages[0].set_author(name='Genius', icon_url='https://i.imgur.com/NmCTsoF.png')
|
||||
|
||||
|
||||
if len(paginator.pages) < 4:
|
||||
for page in paginator.pages:
|
||||
await ctx.send(embed=page)
|
||||
else:
|
||||
paginator.add_page_indicator(ctx.localizer)
|
||||
await Scroller(ctx, paginator).start_scrolling()
|
||||
|
||||
|
||||
|
||||
@commands.command(name='scrub')
|
||||
@checks.dj_or(alone=True)
|
||||
@require_voice_connection()
|
||||
@@ -705,9 +717,9 @@ class Music(commands.Cog):
|
||||
async def _scrub(self, ctx):
|
||||
""" Lists the first 10 search results from a given query. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
|
||||
controls = '{scrub.controls}'
|
||||
|
||||
|
||||
scrubber = [
|
||||
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, -1000),
|
||||
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}', player.seek, -15),
|
||||
@@ -716,17 +728,17 @@ class Music(commands.Cog):
|
||||
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}', player.seek, 15),
|
||||
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, 1000),
|
||||
]
|
||||
|
||||
|
||||
selection = None
|
||||
arg = None
|
||||
|
||||
|
||||
def check(reaction, user):
|
||||
if user is None or user.id != ctx.author.id:
|
||||
return False
|
||||
|
||||
|
||||
if reaction.message.id != msg.id:
|
||||
return False
|
||||
|
||||
|
||||
for (emoji, func, _arg) in scrubber:
|
||||
if emoji == reaction.emoji:
|
||||
nonlocal selection
|
||||
@@ -735,14 +747,14 @@ class Music(commands.Cog):
|
||||
arg = _arg
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
embed = discord.Embed(description='{scrub.add}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
msg = await ctx.send(embed=embed)
|
||||
|
||||
|
||||
for (emoji, _, _) in scrubber:
|
||||
await msg.add_reaction(emoji)
|
||||
|
||||
|
||||
embed.description = controls
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await msg.edit(embed=embed)
|
||||
@@ -759,7 +771,7 @@ class Music(commands.Cog):
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
|
||||
try:
|
||||
await msg.remove_reaction(reaction, user)
|
||||
except discord.Forbidden:
|
||||
@@ -768,22 +780,20 @@ class Music(commands.Cog):
|
||||
if not isinstance(arg, bool):
|
||||
arg = player.position + arg * 1000
|
||||
await selection(arg)
|
||||
|
||||
|
||||
|
||||
@commands.group(name='loop')
|
||||
async def _loop(self, ctx):
|
||||
# This is done using subcommands to have separate vote counts for starting and stopping.
|
||||
if ctx.invoked_subcommand is None:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
embed = discord.Embed(color=ctx.me.color, title='Loop status')
|
||||
|
||||
|
||||
if player.looping:
|
||||
embed.description = 'The bot is currently looping: ♾️'
|
||||
else:
|
||||
embed.description = 'The bot is currently not looping'
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@_loop.command(name='start')
|
||||
@require_playing(require_user_listening=True)
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
@@ -795,8 +805,7 @@ class Music(commands.Cog):
|
||||
embed.description = 'Looping turned on'
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@_loop.command(name='stop')
|
||||
@require_playing(require_user_listening=True)
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
@@ -808,18 +817,16 @@ class Music(commands.Cog):
|
||||
embed.description = 'Looping turned off'
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
async def cog_unload(self):
|
||||
self.bot.lavalink._event_hooks.clear()
|
||||
|
||||
|
||||
|
||||
async def track_hook(self, event):
|
||||
if isinstance(event, lavalink.events.TrackEndEvent):
|
||||
channel = self.bot.get_channel(event.player.fetch('channel'))
|
||||
player = self.bot.lavalink.player_manager.get(channel.guild.id)
|
||||
player.skip_voters.clear()
|
||||
|
||||
|
||||
if isinstance(event, lavalink.events.TrackStartEvent):
|
||||
pass
|
||||
if isinstance(event, lavalink.events.QueueEndEvent):
|
||||
@@ -834,9 +841,8 @@ class Music(commands.Cog):
|
||||
if isinstance(event, lavalink.events.NodeChangedEvent):
|
||||
pass
|
||||
if isinstance(event, lavalink.events.TrackStuckEvent):
|
||||
print(event)
|
||||
|
||||
|
||||
pass
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voice_state_update(self, member, before, after):
|
||||
""" Updates listeners when the bot or a user changes voice state """
|
||||
@@ -847,14 +853,14 @@ class Music(commands.Cog):
|
||||
for member in voice_channel.members:
|
||||
if not member.bot:
|
||||
player.update_listeners(member, member.voice)
|
||||
|
||||
|
||||
if not member.bot:
|
||||
player = self.bot.lavalink.player_manager.get(member.guild.id)
|
||||
if player is not None:
|
||||
player.update_listeners(member, after)
|
||||
await self.check_leave_voice(member.guild)
|
||||
|
||||
|
||||
|
||||
|
||||
async def check_leave_voice(self, guild):
|
||||
""" Checks if the bot should leave the voice channel """
|
||||
# TODO, disconnect timer?
|
||||
@@ -863,13 +869,11 @@ class Music(commands.Cog):
|
||||
if player.queue.empty and player.current is None:
|
||||
await player.stop()
|
||||
await self.voice_client.disconnect()
|
||||
|
||||
|
||||
|
||||
async def leave_check(self):
|
||||
for player_id in self.bot.lavalink.player_manager.players:
|
||||
await self.check_leave_voice(self.bot.get_guild(player_id))
|
||||
|
||||
|
||||
|
||||
@tasks.loop(seconds=10.0)
|
||||
async def leave_timer(self):
|
||||
try:
|
||||
@@ -877,5 +881,10 @@ class Music(commands.Cog):
|
||||
except Exception as err:
|
||||
self.logger.debug("Error in leave_timer loop.\nTraceback: %s" % (err))
|
||||
|
||||
def wrap_in_button_callback(func, *args):
|
||||
async def inner(_interaction, _button):
|
||||
return await func(*args)
|
||||
return inner
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Music(bot))
|
||||
@@ -1,44 +1,54 @@
|
||||
import discord
|
||||
from enum import Flag, auto
|
||||
from typing import Callable, Any, Tuple
|
||||
|
||||
from lavalink.client import asyncio
|
||||
from .paginators import BasePaginator, CantScrollException
|
||||
|
||||
|
||||
class ScrollerButton(discord.ui.Button):
|
||||
def __init__(self, method, label, style=discord.ButtonStyle.gray, **kwargs):
|
||||
super().__init__(label=label, style=style, **kwargs)
|
||||
self.callback = method
|
||||
|
||||
|
||||
class ScrollerNav(discord.ui.Select):
|
||||
def __init__(self, method, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.callback = method
|
||||
|
||||
|
||||
class ScrollClearSettings(Flag):
|
||||
OnTimeout = auto()
|
||||
OnInteractionExit = auto()
|
||||
|
||||
|
||||
class Scroller:
|
||||
def __init__(self, ctx, paginator, timeout=20.0, clear_mode: ScrollClearSettings = ScrollClearSettings.OnInteractionExit, with_nav_bar: bool=True):
|
||||
def __init__(self, ctx, paginator, clear_mode: ScrollClearSettings, timeout=20.0,
|
||||
with_nav_bar: bool = True):
|
||||
|
||||
if not isinstance(paginator, BasePaginator):
|
||||
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
|
||||
|
||||
self.bot = ctx.bot
|
||||
self.pages = paginator.pages
|
||||
self.cmdmsg = ctx.message
|
||||
self.message = ctx.message
|
||||
self.channel = ctx.channel
|
||||
self.author = ctx.author
|
||||
|
||||
# Initialize the view we're using for scrolling
|
||||
self.view = discord.ui.View(timeout=timeout)
|
||||
self.view.on_timeout = self.on_timeout
|
||||
|
||||
self.clear_mode = clear_mode
|
||||
self.use_nav_bar = with_nav_bar
|
||||
self.paginator = paginator
|
||||
self.ctx = ctx
|
||||
|
||||
if len(self.pages) > 1:
|
||||
self.scrolling = True
|
||||
else:
|
||||
self.scrolling = False
|
||||
# No embeds to scroll through
|
||||
if not self.paginator.pages:
|
||||
raise Exception("Paginator contained no pages to display") # TODO: proper error
|
||||
|
||||
self.bot = ctx.bot
|
||||
self.channel = ctx.channel
|
||||
self.message = None
|
||||
|
||||
# Initialize the view we'll be using for scrolling
|
||||
self.view = discord.ui.View(timeout=timeout)
|
||||
self.view.on_timeout = self.on_timeout
|
||||
|
||||
self.is_scrolling_paginator = len(self.paginator.pages) > 1
|
||||
self.scrolling_done = asyncio.Event()
|
||||
|
||||
self.interaction_mapping = [
|
||||
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
|
||||
@@ -48,10 +58,8 @@ class Scroller:
|
||||
('❌', self.stop),
|
||||
]
|
||||
|
||||
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)
|
||||
bot_user = ctx.guild if ctx.guild.me is not None else ctx.bot.user
|
||||
self.permissions = self.channel.permissions_for(bot_user)
|
||||
|
||||
if not self.permissions.embed_links:
|
||||
raise CantScrollException('Bot does not have embed links permission.')
|
||||
@@ -59,68 +67,77 @@ class Scroller:
|
||||
if not self.permissions.send_messages:
|
||||
raise CantScrollException('Bot cannot send messages.')
|
||||
|
||||
if self.scrolling:
|
||||
if not self.permissions.read_message_history:
|
||||
raise CantScrollException('Bot does not have Read Message History permission.')
|
||||
|
||||
async def start_scrolling(self):
|
||||
async def start_scrolling(self) -> discord.Message:
|
||||
self.current_page_number = 0
|
||||
# No embeds to scroll through
|
||||
if not self.pages:
|
||||
return
|
||||
self.build_view()
|
||||
|
||||
if not self.scrolling:
|
||||
return await self.channel.send(embed=self.pages[0])
|
||||
if not self.is_scrolling_paginator:
|
||||
return await self.channel.send(embed=self.paginator.pages[0])
|
||||
|
||||
self.message = await self.channel.send(embed=self.paginator.pages[0], view=self.view)
|
||||
await self.scrolling_done.wait()
|
||||
|
||||
return self.message
|
||||
|
||||
def update_view(self, interaction: discord.Interaction):
|
||||
self.navigator.placeholder = f"Page: {self.current_page_number + 1}/{len(self.paginator.pages)}"
|
||||
|
||||
def build_view(self):
|
||||
for (reaction, callback) in self.interaction_mapping:
|
||||
self.view.add_item(item=ScrollerButton(callback, label=reaction, row=3))
|
||||
self.view.add_item(item=ScrollerButton(method=callback, label=reaction, row=3))
|
||||
|
||||
if self.use_nav_bar:
|
||||
self.navigator = ScrollerNav(self.navigate, placeholder="Navigate to page", row=4)
|
||||
self.view.add_item(item=self.navigator)
|
||||
for (i, _) in enumerate(self.pages):
|
||||
for (i, _) in enumerate(self.paginator.pages):
|
||||
self.navigator.add_option(label=str(i+1), value=str(i))
|
||||
|
||||
self.message = await self.channel.send(embed=self.pages[0], view=self.view)
|
||||
|
||||
async def stop(self, user_stopped: bool):
|
||||
self.scrolling = False
|
||||
self.is_scrolling_paginator = False
|
||||
self.view.stop()
|
||||
self.view.clear_items()
|
||||
if user_stopped and self.clear_mode & ScrollClearSettings.OnInteractionExit or \
|
||||
not user_stopped and self.clear_mode & ScrollClearSettings.OnTimeout:
|
||||
if (user_stopped and self.clear_mode & ScrollClearSettings.OnInteractionExit or
|
||||
not user_stopped and self.clear_mode & ScrollClearSettings.OnTimeout):
|
||||
await self.message.delete()
|
||||
await self.cmdmsg.delete()
|
||||
self.message = None
|
||||
await self.ctx.message.delete()
|
||||
else:
|
||||
await self.message.edit(embed=self.pages[self.current_page_number], view=self.view)
|
||||
await self.message.edit(embed=self.paginator.pages[self.current_page_number])
|
||||
|
||||
async def scroll(self, page, interaction: discord.Interaction):
|
||||
if page < 0 or page >= len(self.pages):
|
||||
# Notify the start_scrolling function that we're done
|
||||
self.scrolling_done.set()
|
||||
|
||||
async def _scroll(self, page, interaction: discord.Interaction):
|
||||
if interaction.user.id != self.ctx.author.id:
|
||||
return await interaction.response.defer()
|
||||
|
||||
if page < 0 or page >= len(self.paginator.pages):
|
||||
return
|
||||
self.current_page_number = page
|
||||
|
||||
if interaction.message:
|
||||
await interaction.message.edit(embed=self.pages[page], view=self.view)
|
||||
# Update the view before we edit the message
|
||||
self.update_view(interaction)
|
||||
await interaction.message.edit(embed=self.paginator.pages[page], view=self.view)
|
||||
await interaction.response.defer()
|
||||
|
||||
async def first_page(self, interaction: discord.Interaction):
|
||||
await self.scroll(0, interaction)
|
||||
await self._scroll(0, interaction)
|
||||
|
||||
async def last_page(self, interaction: discord.Interaction):
|
||||
await self.scroll(len(self.pages) - 1, interaction)
|
||||
await self._scroll(len(self.paginator.pages) - 1, interaction)
|
||||
|
||||
async def next_page(self, interaction: discord.Interaction):
|
||||
await self.scroll(self.current_page_number + 1, interaction)
|
||||
await self._scroll(self.current_page_number + 1, interaction)
|
||||
|
||||
async def previous_page(self, interaction: discord.Interaction):
|
||||
await self.scroll(self.current_page_number - 1, interaction)
|
||||
await self._scroll(self.current_page_number - 1, interaction)
|
||||
|
||||
async def navigate(self, interaction: discord.Interaction):
|
||||
page = int(self.navigator.values[0])
|
||||
await self.scroll(page, interaction)
|
||||
await self._scroll(int(self.navigator.values[0]), interaction)
|
||||
|
||||
async def stop_scrolling(self, _: discord.Interaction):
|
||||
await self.stop(user_stopped=True)
|
||||
|
||||
async def on_timeout(self):
|
||||
await self.stop(user_stopped=False)
|
||||
|
||||
@@ -1,10 +1,104 @@
|
||||
# Discord Packages
|
||||
from __future__ import annotations
|
||||
from locale import resetlocale
|
||||
import discord
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing import List, Callable, Coroutine
|
||||
|
||||
from .paginators import TextPaginator
|
||||
from .scroller import ScrollClearSettings, Scroller
|
||||
|
||||
class SelectorButton(discord.ui.Button):
|
||||
def __init__(self, label, callback: Callable[[discord.Interaction, SelectorButton], Coroutine], style=discord.ButtonStyle.gray, **kwargs):
|
||||
super().__init__(label=label, style=style, **kwargs)
|
||||
|
||||
# The callback we need to supply to disord.py takes only a discord.Interaction
|
||||
# we want to have access to the button that caused that was clicked
|
||||
# so we can update it, i.e. change color
|
||||
def add_button_to_callback(func):
|
||||
async def wrapped_callback(interaction: discord.Interaction):
|
||||
await func(interaction, self)
|
||||
return wrapped_callback
|
||||
|
||||
self.callback = add_button_to_callback(callback)
|
||||
|
||||
|
||||
class SelectorItem:
|
||||
def __init__(self, identifier, callback: Callable[[discord.Interaction, SelectorButton], Coroutine]):
|
||||
self.identifier = identifier
|
||||
self.callback = callback
|
||||
|
||||
|
||||
class Selector2(TextPaginator, Scroller):
|
||||
def __init__(self, ctx, choices: List[SelectorItem], round_titles=None, terminate_on_select: bool = True, max_size=2000, **embed_base):
|
||||
self.match = None
|
||||
if round_titles is None:
|
||||
round_titles = []
|
||||
self.match = None
|
||||
self.terminate_on_select = terminate_on_select
|
||||
self.round_titles = round_titles
|
||||
|
||||
self.max_selections_per_page = 5
|
||||
self.selections = choices
|
||||
self.max_selections = min(len(self.selections), self.max_selections_per_page)
|
||||
|
||||
TextPaginator.__init__(self, max_size=max_size, max_lines=self.max_selections_per_page, **embed_base)
|
||||
for index, selection in enumerate(self.selections, start=1):
|
||||
self.add_line(f"`{index}.` {selection.identifier}")
|
||||
self.close_page()
|
||||
|
||||
Scroller.__init__(self, ctx, self, ScrollClearSettings.OnTimeout)
|
||||
|
||||
# Contains a list of all buttons that can be interacted with, both
|
||||
# currently visible and not.
|
||||
self.buttons = []
|
||||
|
||||
# The list of currently visible buttons, gets cleared upon scroll.
|
||||
self.visible_buttons = []
|
||||
|
||||
# Depending on the selector we might want the result of callbacks
|
||||
# after we have finished interacting with the scroller
|
||||
self.callback_results = []
|
||||
|
||||
def with_update_view(func):
|
||||
async def wrapped_callback(interaction: discord.Interaction, button: SelectorButton):
|
||||
result = await func(interaction, button)
|
||||
self.callback_results.append(result)
|
||||
self.update_view(interaction)
|
||||
await interaction.response.defer()
|
||||
if terminate_on_select:
|
||||
await self.stop(True)
|
||||
return wrapped_callback
|
||||
|
||||
for i, choice in enumerate(self.selections):
|
||||
self.buttons.append(SelectorButton(label=i+1, callback=with_update_view(choice.callback), row=0))
|
||||
|
||||
def build_view(self):
|
||||
super().build_view()
|
||||
self.update_buttons()
|
||||
|
||||
def update_view(self, interaction: discord.Interaction):
|
||||
super().update_view(interaction)
|
||||
self.update_buttons()
|
||||
|
||||
def update_buttons(self):
|
||||
for button in self.visible_buttons:
|
||||
self.view.remove_item(item=button)
|
||||
|
||||
self.visible_buttons = []
|
||||
self.current_page_number
|
||||
start = self.current_page_number * self.max_selections_per_page
|
||||
end = min((self.current_page_number + 1) * self.max_selections_per_page, len(self.selections))
|
||||
|
||||
for button in self.buttons[start:end]:
|
||||
self.view.add_item(item=button)
|
||||
self.visible_buttons.append(button)
|
||||
|
||||
async def start_scrolling(self):
|
||||
message = await super().start_scrolling()
|
||||
return message, self.callback_results
|
||||
|
||||
|
||||
class Selector(TextPaginator):
|
||||
|
||||
Reference in new issue
Block a user