mirror of
https://github.com/UiO-Gaming/ShiteMusicBot.git
synced 2026-11-05 13:56:51 +01:00
Merge pull request #37 from r-Norge/dev
This commit is contained in:
9 files changed
+274
-31
No files matched your search
@@ -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
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
bot_version = "0.1.6"
|
||||
bot_version = "0.1.7"
|
||||
+79
-7
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user