Merge pull request #37 from r-Norge/dev

This commit is contained in:
Roxedus authored and GitHub committed 2021-02-13 15:05:52 +01:00
commit 45af909882
9 files changed
+274 -31

No files matched your search

+1 -1
View File
@@ -105,4 +105,4 @@ jobs:
env:
REPO: ${{ steps.vars.outputs.repo }}
if: github.event_name != 'pull_request'
run: docker push $REPO
run: docker push -a $REPO
+18 -8
View File
@@ -36,14 +36,18 @@ class Music(commands.Cog):
ws = self.bot._connection._get_websocket(guild_id)
await ws.voice_state(str(guild_id), channel_id)
async def enqueue(self, ctx, track, embed, silent=False):
async def enqueue(self, ctx, track, embed, silent=False, check_max_length=True):
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
# Only add tracks that don't exceed the max track length
if maxlength := self.max_track_length(ctx.guild, player):
if track['info']['length'] > maxlength:
if track['info']['length'] > maxlength and check_max_length:
if track['info']['isStream']:
length = '{live}'
else:
length = timeformatter.format_ms(track['info']['length'])
embed.description = ctx.localizer.format_str("{enqueue.toolong}",
_length=timeformatter.format_ms(track['info']['length']),
_length=length,
_max=timeformatter.format_ms(maxlength))
return embed, False
@@ -55,7 +59,10 @@ class Music(commands.Cog):
track, pos_global, pos_local = player.add(requester=ctx.author.id, track=track)
if player.current is not None and not silent:
until_play = player.queue_duration(include_current=True, end_pos=pos_global)
if player.current.stream:
until_play = '--:--'
else:
until_play = player.queue_duration(include_current=True, end_pos=pos_global)
embed.add_field(name="{enqueue.position}", value=f"`{pos_local + 1}({pos_global + 1})`", inline=True)
embed.add_field(name="{enqueue.playing_in}", value=f"`{until_play} ({{enqueue.estimated}})`",
inline=True)
@@ -66,7 +73,10 @@ class Music(commands.Cog):
if thumbnail_url:
embed.set_thumbnail(url=thumbnail_url)
duration = timeformatter.format_ms(int(track.duration))
if track.stream:
duration = '{live}'
else:
duration = timeformatter.format_ms(int(track.duration))
embed.description = f'[{track.title}]({track.uri})\n**{duration}**'
return embed, True
@@ -110,12 +120,12 @@ class Music(commands.Cog):
# commands
from .basic_commands import (
_disconnect, _djremove, _move, _myqueue, _now, _pause, _play, _queue, _reconnect, _remove, _search, _seek,
_shuffle, _skip, _skip_to, _stop, _user_queue_remove, _volume)
_disconnect, _djremove, _forceplay, _move, _myqueue, _now, _pause, _play, _queue, _reconnect, _remove, _search,
_seek, _shuffle, _skip, _skip_to, _stop, _user_queue_remove, _volume)
# events
from .events import check_leave_voice, cog_unload, leave_check, leave_timer, on_voice_state_update, track_hook
# commands
from .extra_commands import _boost, _history, _lyrics, _normalize, _scrub
from .extra_commands import _boost, _history, _loop, _loop_start, _loop_stop, _lyrics, _normalize, _scrub
def setup(bot):
+60 -11
View File
@@ -77,7 +77,7 @@ async def _seek(self, ctx, *, time: str):
@commands.command(name='skip')
@require_voice_connection()
@require_playing()
@voteable(requester_override=True)
@voteable(requester_override=True, react_to_vote=True)
async def _skip(self, ctx):
""" Skips the current track. """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
@@ -112,7 +112,7 @@ async def _skip_to(self, ctx, pos: int = 1):
@commands.command(name='stop')
@require_voice_connection()
@require_playing(require_user_listening=True)
@voteable(DJ_override=True)
@voteable(DJ_override=True, react_to_vote=True)
async def _stop(self, ctx):
""" Stops the player and clears its queue. """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
@@ -204,7 +204,11 @@ async def _move(self, ctx):
# Get the initial and final position
message, page, selections = await selector.start_scrolling()
pos_initial, pos_final = selections[0], selections[1]
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)
@@ -247,7 +251,10 @@ async def _remove(self, ctx):
selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, color=0xFF0000,
title='Remove song plz')
message, _, removed = await selector.start_scrolling()
await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None)
if removed:
await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None)
else:
await message.delete()
@commands.command(name="DJremove")
@@ -322,18 +329,22 @@ async def _search(self, ctx, *, query):
search_selector = Selector(ctx, identifiers, functions, arguments, num_selections=5,
color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
# Let the user scroll through results
message, current_page, (embed, added) = await search_selector.start_scrolling()
message, current_page, result = await search_selector.start_scrolling()
embed = ctx.localizer.format_embed(embed)
await message.edit(embed=embed)
if result:
(embed, added) = result
embed = ctx.localizer.format_embed(embed)
await message.edit(embed=embed)
if not player.is_playing:
await player.play()
if not player.is_playing:
await player.play()
else:
await message.delete()
@commands.command(name='disconnect')
@require_voice_connection()
@voteable(DJ_override=True)
@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)
@@ -348,7 +359,7 @@ async def _disconnect(self, ctx):
@commands.command(name='reconnect')
@require_voice_connection()
@voteable(DJ_override=True)
@voteable(DJ_override=True, react_to_vote=True)
async def _reconnect(self, ctx):
""" Tries to disconnect then reconnect the player in case the bot gets stuck on a song """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
@@ -390,3 +401,41 @@ async def _volume(self, ctx, volume: int = None):
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)
async def _forceplay(self, ctx, *, query: str):
""" 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['loadType'] == '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["playlistInfo"]["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()
+46 -2
View File
@@ -2,6 +2,7 @@
import discord
from discord.ext import commands
import asyncio
import functools
import inspect
import math
@@ -127,7 +128,7 @@ def require_queue(require_member_queue=False, require_author_queue=False):
return ensure_queue
def voteable(requester_override=False, DJ_override=False):
def voteable(requester_override=False, DJ_override=False, react_to_vote=False):
def make_voteable(func):
@functools.wraps(func)
async def voteable_inner(self, ctx, *command_args, **kwargs):
@@ -140,11 +141,54 @@ def voteable(requester_override=False, DJ_override=False):
enough_votes = votes/total >= threshold/100
DJ = DJ_override and is_dj(ctx)
requester = player.current.requester == ctx.author.id and requester_override
requester = player.current is None or (player.current.requester == ctx.author.id and requester_override)
if enough_votes or requester or DJ:
await func(self, ctx, *command_args, **kwargs)
player.clear_votes()
elif react_to_vote:
embed = discord.Embed(title="Votes",
description=f"{votes} out of {math.ceil(total*threshold/100)} required votes.",
color=ctx.me.color)
embed.set_footer(text=f'{{requested_by}} {ctx.author.name}', icon_url=ctx.author.avatar_url)
msg = await ctx.send(embed=ctx.localizer.format_embed(embed))
await msg.add_reaction('👍')
def check(reaction, user):
if not reaction.message.id == msg.id:
return False
if user is None or user not in player.listeners:
return False
if reaction.emoji == '👍' and user.id not in player.get_voters(func.__name__):
player.add_vote(func.__name__, user)
return True
return False
while True:
try:
reaction, user = await self.bot.wait_for('reaction_add', timeout=15.0, check=check)
except asyncio.TimeoutError:
await msg.clear_reactions()
embed.title = ''
embed.set_footer(text='{time_expired}')
await msg.edit(embed=ctx.localizer.format_embed(embed))
break
total = len(player.listeners)
votes = len(player.get_voters(func.__name__))
embed.description = f"{votes} out of {math.ceil(total*threshold/100)} required votes."
await msg.edit(embed=ctx.localizer.format_embed(embed))
if votes/total >= threshold/100:
player.clear_votes()
await msg.delete()
await func(self, ctx, *command_args, **kwargs)
break
else:
if votes != 0:
needed = math.ceil(total*threshold/100)
+41 -1
View File
@@ -16,7 +16,7 @@ from bs4 import BeautifulSoup
from ..utils import checks
from ..utils.paginator import Scroller, TextPaginator
from .decorators import require_playing, require_voice_connection
from .decorators import require_playing, require_voice_connection, voteable
@commands.command(name='normalize')
@@ -213,3 +213,43 @@ async def _scrub(self, ctx):
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)
async def _loop_start(self, ctx):
""" Set the equalizer to bass boost the music """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
player.enable_looping(True)
embed = discord.Embed(color=ctx.me.color)
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)
async def _loop_stop(self, ctx):
""" Set the equalizer to bass boost the music """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
player.enable_looping(False)
embed = discord.Embed(color=ctx.me.color)
embed.description = 'Looping turned off'
embed = ctx.localizer.format_embed(embed)
await ctx.send(embed=embed)
+1 -1
View File
@@ -1 +1 @@
bot_version = "0.1.6"
bot_version = "0.1.7"
+79 -7
View File
@@ -102,6 +102,13 @@ class MixPlayer(DefaultPlayer):
self.clear_votes()
await self.play()
async def stop(self):
""" Stops the player. """
await self.node._send(op='stop', guildId=self.guild_id)
self.current = None
self.queue.looping = False
self.clear_votes()
def update_listeners(self, member, voice_state):
if self.is_connected:
vc = int(self.channel_id)
@@ -137,6 +144,15 @@ class MixPlayer(DefaultPlayer):
for category, votes in self.voteables.items():
votes.clear()
def enable_looping(self, looping):
if (not self.queue.looping) and looping:
self.queue.enable_looping(looping)
if track := self.current:
if looping:
self.queue.add_track(track.requester, track)
elif not looping and self.queue.looping:
self.queue.enable_looping(looping)
async def handle_event(self, event):
""" Handles the given event as necessary. """
if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \
@@ -163,6 +179,10 @@ class MixPlayer(DefaultPlayer):
self.boosted = False
await self.reset_equalizer()
@property
def looping(self):
return self.queue.looping
def roundrobin(*iterables):
"""roundrobin('ABC', 'D', 'EF') --> A D E B F C"""
@@ -183,6 +203,8 @@ class MixQueue:
self.queues = OrderedDict()
self.priority_queue = []
self._history = deque(maxlen=11) # 10 + current
self.looping = False
self.loop_offset = 0
def __str__(self):
tmp = ''
@@ -207,17 +229,25 @@ class MixQueue:
return length
def get_queue(self):
return list(self)
if self.looping:
offset = self.loop_offset
return list(self)[offset:] + list(self)[:offset]
else:
return list(self)
def clear(self):
self.queues = OrderedDict()
self.priority_queue = []
self.looping = False
self.loop_offset = 0
# 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 dual and queue:
pos = [self._loc_to_glob(requester, i) for i in range(len(queue))]
if self.looping:
pos = [(p + self.loop_offset) % len(self) for p in pos]
combined = zip(queue, pos)
return list(combined)
return queue
@@ -228,11 +258,26 @@ class MixQueue:
self._history.append(next_track)
return next_track
try:
next_track = self.queues[self.first_queue].pop(0)
self._shuffle()
self._clear_empty()
self._history.append(next_track)
return next_track
if self.looping:
try:
queue = list(self)
next_track = queue[self.loop_offset]
self.loop_offset += 1
if self.loop_offset >= len(self):
self.loop_offset = 0
self._history.append(next_track)
return next_track
except IndexError as e:
print(e)
print(self.loop_offset)
print(len(self))
self.loop_offset = min(self.loop_offset, len(self)-1)
else:
next_track = self.queues[self.first_queue].pop(0)
self._shuffle()
self._clear_empty()
self._history.append(next_track)
return next_track
except KeyError:
pass
@@ -257,17 +302,31 @@ class MixQueue:
def remove_user_queue(self, requester: int):
user_queue = self.queues.get(requester, [])
if user_queue:
self.queues.pop(requester)
if self.looping:
# Should work, kinda hacky.
for pos, track in reversed(list(enumerate(user_queue))):
self.remove_user_track(requester, pos)
else:
self.queues.pop(requester)
def remove_user_track(self, requester: int, pos: int):
user_queue = self.queues.get(requester)
if user_queue is not None:
if pos < len(user_queue):
glob_pos = self._loc_to_glob(requester, pos)
if glob_pos <= self.loop_offset:
self.loop_offset -= 1
track = user_queue.pop(pos)
self._clear_empty()
return track
def remove_global_track(self, pos: int):
# Get the actual index of the song
pos = list(self).index(self.get_queue()[pos])
if pos <= self.loop_offset:
self.loop_offset -= 1
q, pos = self._glob_to_loc(pos)
if q is None or pos is None:
return
@@ -291,6 +350,19 @@ class MixQueue:
if queue:
shuffle(queue)
def enable_looping(self, looping):
if (not self.looping) and looping: # Not enabled and turning on
self.looping = looping
self.loop_offset = 0
elif self.looping and not looping: # Enabled to turning off
tmp = [track for track in self.get_queue()]
for key, _ in self.queues.items():
self.queues = OrderedDict()
self.looping = looping
for track in tmp:
self.add_track(track.requester, track)
self.loop_offset = 0
# Switches the order of user queues
def _shuffle(self):
self.queues.move_to_end(self.first_queue)
+14
View File
@@ -91,6 +91,20 @@ scrub:
reconnect:
aliases: [rc]
description: 'disconnects then reconnects the bot in case of bugged songs'
forceplay:
aliases: [forceplay, fp]
args: '<query>' # This is just a cosmetic representation of the arguments.
description: 'Searches and plays a song from a given query.'
loop:
aliases: [loop, looping]
description: 'Loops the queue'
sub_commands:
start:
aliases: ['on', start, begin]
description: 'Starts looping the queue'
stop:
aliases: ['off', stop, end]
description: 'Stops looping the queue'
# Misc
info:
+14
View File
@@ -88,6 +88,20 @@ lyrics:
scrub:
aliases: [kontroller]
description: 'Gir deg en kontroller som du kan styre musikken med.'
forceplay:
aliases: [tvingspill, ts]
args: '<sang>' # This is just a cosmetic representation of the arguments.
description: 'Søker opp og spiller av en sang.'
loop:
aliases: [gjenta, repeter]
description: 'Styr om køen skal gjenta seg'
sub_commands:
start:
aliases: [på, start, begynn]
description: 'Får køen til å gjenta seg'
stop:
aliases: [av, stopp, slutt]
description: 'Får køen til å slutte å gjenta seg'
# Misc
info: