mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-21 18:27:59 -05:00
Merge V3/feature/audio into V3/develop (a.k.a. audio refactor) (#3459)
This commit is contained in:
25
redbot/cogs/audio/core/commands/__init__.py
Normal file
25
redbot/cogs/audio/core/commands/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from ..cog_utils import CompositeMetaClass
|
||||
from .audioset import AudioSetCommands
|
||||
from .controller import PlayerControllerCommands
|
||||
from .equalizer import EqualizerCommands
|
||||
from .llset import LavalinkSetupCommands
|
||||
from .localtracks import LocalTrackCommands
|
||||
from .miscellaneous import MiscellaneousCommands
|
||||
from .player import PlayerCommands
|
||||
from .playlists import PlaylistCommands
|
||||
from .queue import QueueCommands
|
||||
|
||||
|
||||
class Commands(
|
||||
AudioSetCommands,
|
||||
PlayerControllerCommands,
|
||||
EqualizerCommands,
|
||||
LavalinkSetupCommands,
|
||||
LocalTrackCommands,
|
||||
MiscellaneousCommands,
|
||||
PlayerCommands,
|
||||
PlaylistCommands,
|
||||
QueueCommands,
|
||||
metaclass=CompositeMetaClass,
|
||||
):
|
||||
"""Class joining all command subclasses"""
|
||||
1307
redbot/cogs/audio/core/commands/audioset.py
Normal file
1307
redbot/cogs/audio/core/commands/audioset.py
Normal file
File diff suppressed because it is too large
Load Diff
841
redbot/cogs/audio/core/commands/controller.py
Normal file
841
redbot/cogs/audio/core/commands/controller.py
Normal file
@@ -0,0 +1,841 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.chat_formatting import humanize_number
|
||||
from redbot.core.utils.menus import start_adding_reactions
|
||||
from redbot.core.utils.predicates import ReactionPredicate
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.player_controller")
|
||||
|
||||
|
||||
class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.command(name="disconnect")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_disconnect(self, ctx: commands.Context):
|
||||
"""Disconnect from the voice channel."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
else:
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (
|
||||
(vote_enabled or (vote_enabled and dj_enabled))
|
||||
and not can_skip
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Disconnect"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Disconnect"),
|
||||
description=_("You need the DJ role to disconnect."),
|
||||
)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Disconnect"),
|
||||
description=_("You need the DJ role to disconnect."),
|
||||
)
|
||||
|
||||
await self.send_embed_msg(ctx, title=_("Disconnecting..."))
|
||||
self.bot.dispatch("red_audio_audio_disconnect", ctx.guild)
|
||||
self.update_player_lock(ctx, False)
|
||||
eq = player.fetch("eq")
|
||||
player.queue = []
|
||||
player.store("playing_song", None)
|
||||
if eq:
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
await player.stop()
|
||||
await player.disconnect()
|
||||
|
||||
@commands.command(name="now")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_now(self, ctx: commands.Context):
|
||||
"""Now playing."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
expected: Union[Tuple[str, ...]] = ("⏮", "⏹", "⏯", "⏭", "\N{CROSS MARK}")
|
||||
emoji = {"prev": "⏮", "stop": "⏹", "pause": "⏯", "next": "⏭", "close": "\N{CROSS MARK}"}
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if player.current:
|
||||
arrow = await self.draw_time(ctx)
|
||||
pos = self.format_time(player.position)
|
||||
if player.current.is_stream:
|
||||
dur = "LIVE"
|
||||
else:
|
||||
dur = self.format_time(player.current.length)
|
||||
song = self.get_track_description(player.current, self.local_folder_current_path) or ""
|
||||
song += _("\n Requested by: **{track.requester}**")
|
||||
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
||||
song = song.format(track=player.current, arrow=arrow, pos=pos, dur=dur)
|
||||
else:
|
||||
song = _("Nothing.")
|
||||
|
||||
if player.fetch("np_message") is not None:
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await player.fetch("np_message").delete()
|
||||
embed = discord.Embed(title=_("Now Playing"), description=song)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
|
||||
if guild_data["thumbnail"] and player.current and player.current.thumbnail:
|
||||
embed.set_thumbnail(url=player.current.thumbnail)
|
||||
shuffle = guild_data["shuffle"]
|
||||
repeat = guild_data["repeat"]
|
||||
autoplay = guild_data["auto_play"]
|
||||
text = ""
|
||||
text += (
|
||||
_("Auto-Play")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if autoplay else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Shuffle")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if shuffle else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Repeat")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if repeat else "\N{CROSS MARK}")
|
||||
)
|
||||
|
||||
message = await self.send_embed_msg(ctx, embed=embed, footer=text)
|
||||
|
||||
player.store("np_message", message)
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
if (
|
||||
(dj_enabled or vote_enabled)
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return
|
||||
|
||||
if not player.queue and not autoplay:
|
||||
expected = ("⏹", "⏯", "\N{CROSS MARK}")
|
||||
task: Optional[asyncio.Task]
|
||||
if player.current:
|
||||
task = start_adding_reactions(message, expected[:5])
|
||||
else:
|
||||
task = None
|
||||
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=ReactionPredicate.with_emojis(expected, message, ctx.author),
|
||||
timeout=30.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._clear_react(message, emoji)
|
||||
else:
|
||||
if task is not None:
|
||||
task.cancel()
|
||||
reacts = {v: k for k, v in emoji.items()}
|
||||
react = reacts[r.emoji]
|
||||
if react == "prev":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_prev)
|
||||
elif react == "stop":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_stop)
|
||||
elif react == "pause":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_pause)
|
||||
elif react == "next":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_skip)
|
||||
elif react == "close":
|
||||
await message.delete()
|
||||
|
||||
@commands.command(name="pause")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_pause(self, ctx: commands.Context):
|
||||
"""Pause or resume a playing track."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Manage Tracks"),
|
||||
description=_("You must be in the voice channel to pause or resume."),
|
||||
)
|
||||
if dj_enabled and not can_skip and not await self.is_requester_alone(ctx):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Manage Tracks"),
|
||||
description=_("You need the DJ role to pause or resume tracks."),
|
||||
)
|
||||
|
||||
if not player.current:
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
description = self.get_track_description(player.current, self.local_folder_current_path)
|
||||
|
||||
if player.current and not player.paused:
|
||||
await player.pause()
|
||||
return await self.send_embed_msg(ctx, title=_("Track Paused"), description=description)
|
||||
if player.current and player.paused:
|
||||
await player.pause(False)
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Track Resumed"), description=description
|
||||
)
|
||||
|
||||
await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
@commands.command(name="prev")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_prev(self, ctx: commands.Context):
|
||||
"""Skip to the start of the previously played track."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("You must be in the voice channel to skip the track."),
|
||||
)
|
||||
if (vote_enabled or (vote_enabled and dj_enabled)) and not can_skip and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not (can_skip or is_requester) and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_(
|
||||
"You need the DJ role or be the track requester "
|
||||
"to enqueue the previous song tracks."
|
||||
),
|
||||
)
|
||||
|
||||
if player.fetch("prev_song") is None:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("No previous track.")
|
||||
)
|
||||
else:
|
||||
track = player.fetch("prev_song")
|
||||
player.add(player.fetch("prev_requester"), track)
|
||||
self.bot.dispatch("red_audio_track_enqueue", player.channel.guild, track, ctx.author)
|
||||
queue_len = len(player.queue)
|
||||
bump_song = player.queue[-1]
|
||||
player.queue.insert(0, bump_song)
|
||||
player.queue.pop(queue_len)
|
||||
await player.skip()
|
||||
description = self.get_track_description(
|
||||
player.current, self.local_folder_current_path
|
||||
)
|
||||
embed = discord.Embed(title=_("Replaying Track"), description=description)
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
|
||||
@commands.command(name="seek")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_seek(self, ctx: commands.Context, seconds: Union[int, str]):
|
||||
"""Seek ahead or behind on a track by seconds or a to a specific time.
|
||||
|
||||
Accepts seconds or a value formatted like 00:00:00 (`hh:mm:ss`) or 00:00 (`mm:ss`).
|
||||
"""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("You must be in the voice channel to use seek."),
|
||||
)
|
||||
|
||||
if vote_enabled and not can_skip and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
|
||||
if dj_enabled and not (can_skip or is_requester) and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("You need the DJ role or be the track requester to use seek."),
|
||||
)
|
||||
|
||||
if player.current:
|
||||
if player.current.is_stream:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Seek Tracks"), description=_("Can't seek on a stream.")
|
||||
)
|
||||
else:
|
||||
try:
|
||||
int(seconds)
|
||||
abs_position = False
|
||||
except ValueError:
|
||||
abs_position = True
|
||||
seconds = self.time_convert(seconds)
|
||||
if seconds == 0:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("Invalid input for the time to seek."),
|
||||
)
|
||||
if not abs_position:
|
||||
time_sec = int(seconds) * 1000
|
||||
seek = player.position + time_sec
|
||||
if seek <= 0:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Moved {num_seconds}s to 00:00:00").format(
|
||||
num_seconds=seconds
|
||||
),
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Moved {num_seconds}s to {time}").format(
|
||||
num_seconds=seconds, time=self.format_time(seek)
|
||||
),
|
||||
)
|
||||
await player.seek(seek)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Moved to {time}").format(time=self.format_time(seconds * 1000)),
|
||||
)
|
||||
await player.seek(seconds * 1000)
|
||||
else:
|
||||
await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
@commands.group(name="shuffle", autohelp=False)
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_shuffle(self, ctx: commands.Context):
|
||||
"""Toggle shuffle."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You need the DJ role to toggle shuffle."),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You must be in the voice channel to toggle shuffle."),
|
||||
)
|
||||
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
await self.config.guild(ctx.guild).shuffle.set(not shuffle)
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Shuffle tracks: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not shuffle else _("Disabled")
|
||||
),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
@command_shuffle.command(name="bumped")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_shuffle_bumpped(self, ctx: commands.Context):
|
||||
"""Toggle bumped track shuffle.
|
||||
|
||||
Set this to disabled if you wish to avoid bumped songs being shuffled.
|
||||
This takes priority over `[p]shuffle`.
|
||||
"""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You need the DJ role to toggle shuffle."),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You must be in the voice channel to toggle shuffle."),
|
||||
)
|
||||
|
||||
bumped = await self.config.guild(ctx.guild).shuffle_bumped()
|
||||
await self.config.guild(ctx.guild).shuffle_bumped.set(not bumped)
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Shuffle bumped tracks: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not bumped else _("Disabled")
|
||||
),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
@commands.command(name="skip")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_skip(self, ctx: commands.Context, skip_to_track: int = None):
|
||||
"""Skip to the next track, or to a given track number."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("You must be in the voice channel to skip the music."),
|
||||
)
|
||||
if not player.current:
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
if dj_enabled and not vote_enabled:
|
||||
if not (can_skip or is_requester) and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_(
|
||||
"You need the DJ role or be the track requester to skip tracks."
|
||||
),
|
||||
)
|
||||
if (
|
||||
is_requester
|
||||
and not can_skip
|
||||
and isinstance(skip_to_track, int)
|
||||
and skip_to_track > 1
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("You can only skip the current track."),
|
||||
)
|
||||
|
||||
if vote_enabled:
|
||||
if not can_skip:
|
||||
if skip_to_track is not None:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_(
|
||||
"Can't skip to a specific track in vote mode without the DJ role."
|
||||
),
|
||||
)
|
||||
if ctx.author.id in self.skip_votes[ctx.message.guild]:
|
||||
self.skip_votes[ctx.message.guild].remove(ctx.author.id)
|
||||
reply = _("I removed your vote to skip.")
|
||||
else:
|
||||
self.skip_votes[ctx.message.guild].append(ctx.author.id)
|
||||
reply = _("You voted to skip.")
|
||||
|
||||
num_votes = len(self.skip_votes[ctx.message.guild])
|
||||
vote_mods = []
|
||||
for member in player.channel.members:
|
||||
can_skip = await self._can_instaskip(ctx, member)
|
||||
if can_skip:
|
||||
vote_mods.append(member)
|
||||
num_members = len(player.channel.members) - len(vote_mods)
|
||||
vote = int(100 * num_votes / num_members)
|
||||
percent = await self.config.guild(ctx.guild).vote_percent()
|
||||
if vote >= percent:
|
||||
self.skip_votes[ctx.message.guild] = []
|
||||
await self.send_embed_msg(ctx, title=_("Vote threshold met."))
|
||||
return await self._skip_action(ctx)
|
||||
else:
|
||||
reply += _(
|
||||
" Votes: {num_votes}/{num_members}"
|
||||
" ({cur_percent}% out of {required_percent}% needed)"
|
||||
).format(
|
||||
num_votes=humanize_number(num_votes),
|
||||
num_members=humanize_number(num_members),
|
||||
cur_percent=vote,
|
||||
required_percent=percent,
|
||||
)
|
||||
return await self.send_embed_msg(ctx, title=reply)
|
||||
else:
|
||||
return await self._skip_action(ctx, skip_to_track)
|
||||
else:
|
||||
return await self._skip_action(ctx, skip_to_track)
|
||||
|
||||
@commands.command(name="stop")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_stop(self, ctx: commands.Context):
|
||||
"""Stop playback and clear the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Stop Player"),
|
||||
description=_("You must be in the voice channel to stop the music."),
|
||||
)
|
||||
if (vote_enabled or (vote_enabled and dj_enabled)) and not can_skip and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Stop Player"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Stop Player"),
|
||||
description=_("You need the DJ role to stop the music."),
|
||||
)
|
||||
if (
|
||||
player.is_playing
|
||||
or (not player.is_playing and player.paused)
|
||||
or player.queue
|
||||
or getattr(player.current, "extras", {}).get("autoplay")
|
||||
):
|
||||
eq = player.fetch("eq")
|
||||
if eq:
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
player.queue = []
|
||||
player.store("playing_song", None)
|
||||
player.store("prev_requester", None)
|
||||
player.store("prev_song", None)
|
||||
player.store("requester", None)
|
||||
await player.stop()
|
||||
await self.send_embed_msg(ctx, title=_("Stopping..."))
|
||||
|
||||
@commands.command(name="summon")
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_summon(self, ctx: commands.Context):
|
||||
"""Summon the bot to a voice channel."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (vote_enabled or (vote_enabled and dj_enabled)) and not can_skip and not is_alone:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("There are other people listening."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not (can_skip or is_requester) and not is_alone:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("You need the DJ role to summon the bot."),
|
||||
)
|
||||
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
else:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ctx.author.voice.channel == player.channel:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return
|
||||
await player.move_to(ctx.author.voice.channel)
|
||||
except AttributeError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
|
||||
@commands.command(name="volume")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_volume(self, ctx: commands.Context, vol: int = None):
|
||||
"""Set the volume, 1% - 150%."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if not vol:
|
||||
vol = await self.config.guild(ctx.guild).volume()
|
||||
embed = discord.Embed(title=_("Current Volume:"), description=str(vol) + "%")
|
||||
if not self._player_check(ctx):
|
||||
embed.set_footer(text=_("Nothing playing."))
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
if self._player_check(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Change Volume"),
|
||||
description=_("You must be in the voice channel to change the volume."),
|
||||
)
|
||||
if dj_enabled and not can_skip and not await self._has_dj_role(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Change Volume"),
|
||||
description=_("You need the DJ role to change the volume."),
|
||||
)
|
||||
if vol < 0:
|
||||
vol = 0
|
||||
if vol > 150:
|
||||
vol = 150
|
||||
await self.config.guild(ctx.guild).volume.set(vol)
|
||||
if self._player_check(ctx):
|
||||
await lavalink.get_player(ctx.guild.id).set_volume(vol)
|
||||
else:
|
||||
await self.config.guild(ctx.guild).volume.set(vol)
|
||||
if self._player_check(ctx):
|
||||
await lavalink.get_player(ctx.guild.id).set_volume(vol)
|
||||
embed = discord.Embed(title=_("Volume:"), description=str(vol) + "%")
|
||||
if not self._player_check(ctx):
|
||||
embed.set_footer(text=_("Nothing playing."))
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
|
||||
@commands.command(name="repeat")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_repeat(self, ctx: commands.Context):
|
||||
"""Toggle repeat."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if dj_enabled and not can_skip and not await self._has_dj_role(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Repeat"),
|
||||
description=_("You need the DJ role to toggle repeat."),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Repeat"),
|
||||
description=_("You must be in the voice channel to toggle repeat."),
|
||||
)
|
||||
|
||||
autoplay = await self.config.guild(ctx.guild).auto_play()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
msg = ""
|
||||
msg += _("Repeat tracks: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not repeat else _("Disabled")
|
||||
)
|
||||
await self.config.guild(ctx.guild).repeat.set(not repeat)
|
||||
if repeat is not True and autoplay is True:
|
||||
msg += _("\nAuto-play has been disabled.")
|
||||
await self.config.guild(ctx.guild).auto_play.set(False)
|
||||
|
||||
embed = discord.Embed(title=_("Setting Changed"), description=msg)
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
@commands.command(name="remove")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_remove(self, ctx: commands.Context, index_or_url: Union[int, str]):
|
||||
"""Remove a specific track number from the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing queued."))
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_("You need the DJ role to remove tracks."),
|
||||
)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_("You must be in the voice channel to manage the queue."),
|
||||
)
|
||||
if isinstance(index_or_url, int):
|
||||
if index_or_url > len(player.queue) or index_or_url < 1:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_(
|
||||
"Song number must be greater than 1 and within the queue limit."
|
||||
),
|
||||
)
|
||||
index_or_url -= 1
|
||||
removed = player.queue.pop(index_or_url)
|
||||
removed_title = self.get_track_description(removed, self.local_folder_current_path)
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed track from queue"),
|
||||
description=_("Removed {track} from the queue.").format(track=removed_title),
|
||||
)
|
||||
else:
|
||||
clean_tracks = []
|
||||
removed_tracks = 0
|
||||
async for track in AsyncIter(player.queue):
|
||||
if track.uri != index_or_url:
|
||||
clean_tracks.append(track)
|
||||
else:
|
||||
removed_tracks += 1
|
||||
player.queue = clean_tracks
|
||||
if removed_tracks == 0:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_("Removed 0 tracks, nothing matches the URL provided."),
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed track from queue"),
|
||||
description=_(
|
||||
"Removed {removed_tracks} tracks from queue "
|
||||
"which matched the URL provided."
|
||||
).format(removed_tracks=removed_tracks),
|
||||
)
|
||||
|
||||
@commands.command(name="bump")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_bump(self, ctx: commands.Context, index: int):
|
||||
"""Bump a track number to the top of the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("You must be in the voice channel to bump a track."),
|
||||
)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("You need the DJ role to bump tracks."),
|
||||
)
|
||||
if index > len(player.queue) or index < 1:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("Song number must be greater than 1 and within the queue limit."),
|
||||
)
|
||||
|
||||
bump_index = index - 1
|
||||
bump_song = player.queue[bump_index]
|
||||
bump_song.extras["bumped"] = True
|
||||
player.queue.insert(0, bump_song)
|
||||
removed = player.queue.pop(index)
|
||||
description = self.get_track_description(removed, self.local_folder_current_path)
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Moved track to the top of the queue."), description=description
|
||||
)
|
||||
385
redbot/cogs/audio/core/commands/equalizer.py
Normal file
385
redbot/cogs/audio/core/commands/equalizer.py
Normal file
@@ -0,0 +1,385 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import re
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.chat_formatting import box, humanize_number, pagify
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions
|
||||
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||
|
||||
from ...equalizer import Equalizer
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.equalizer")
|
||||
|
||||
|
||||
class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="eq", invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_equalizer(self, ctx: commands.Context):
|
||||
"""Equalizer management."""
|
||||
if not self._player_check(ctx):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
reactions = [
|
||||
"\N{BLACK LEFT-POINTING TRIANGLE}",
|
||||
"\N{LEFTWARDS BLACK ARROW}",
|
||||
"\N{BLACK UP-POINTING DOUBLE TRIANGLE}",
|
||||
"\N{UP-POINTING SMALL RED TRIANGLE}",
|
||||
"\N{DOWN-POINTING SMALL RED TRIANGLE}",
|
||||
"\N{BLACK DOWN-POINTING DOUBLE TRIANGLE}",
|
||||
"\N{BLACK RIGHTWARDS ARROW}",
|
||||
"\N{BLACK RIGHT-POINTING TRIANGLE}",
|
||||
"\N{BLACK CIRCLE FOR RECORD}",
|
||||
"\N{INFORMATION SOURCE}",
|
||||
]
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
eq_message = await ctx.send(box(eq.visualise(), lang="ini"))
|
||||
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await eq_message.add_reaction("\N{INFORMATION SOURCE}")
|
||||
else:
|
||||
start_adding_reactions(eq_message, reactions)
|
||||
|
||||
eq_msg_with_reacts = await ctx.fetch_message(eq_message.id)
|
||||
player.store("eq_message", eq_msg_with_reacts)
|
||||
await self._eq_interact(ctx, player, eq, eq_msg_with_reacts, 0)
|
||||
|
||||
@command_equalizer.command(name="delete", aliases=["del", "remove"])
|
||||
async def command_equalizer_delete(self, ctx: commands.Context, eq_preset: str):
|
||||
"""Delete a saved eq preset."""
|
||||
async with self.config.custom("EQUALIZER", ctx.guild.id).eq_presets() as eq_presets:
|
||||
eq_preset = eq_preset.lower()
|
||||
try:
|
||||
if eq_presets[eq_preset][
|
||||
"author"
|
||||
] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Delete Preset"),
|
||||
description=_("You are not the author of that preset setting."),
|
||||
)
|
||||
del eq_presets[eq_preset]
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Delete Preset"),
|
||||
description=_(
|
||||
"{eq_preset} is not in the eq preset list.".format(
|
||||
eq_preset=eq_preset.capitalize()
|
||||
)
|
||||
),
|
||||
)
|
||||
except TypeError:
|
||||
if await self._can_instaskip(ctx, ctx.author):
|
||||
del eq_presets[eq_preset]
|
||||
else:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Delete Preset"),
|
||||
description=_("You are not the author of that preset setting."),
|
||||
)
|
||||
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("The {preset_name} preset was deleted.".format(preset_name=eq_preset))
|
||||
)
|
||||
|
||||
@command_equalizer.command(name="list")
|
||||
async def command_equalizer_list(self, ctx: commands.Context):
|
||||
"""List saved eq presets."""
|
||||
eq_presets = await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets()
|
||||
if not eq_presets.keys():
|
||||
return await self.send_embed_msg(ctx, title=_("No saved equalizer presets."))
|
||||
|
||||
space = "\N{EN SPACE}"
|
||||
header_name = _("Preset Name")
|
||||
header_author = _("Author")
|
||||
header = box(
|
||||
"[{header_name}]{space}[{header_author}]\n".format(
|
||||
header_name=header_name, space=space * 9, header_author=header_author
|
||||
),
|
||||
lang="ini",
|
||||
)
|
||||
preset_list = ""
|
||||
for preset, bands in eq_presets.items():
|
||||
try:
|
||||
author = self.bot.get_user(bands["author"])
|
||||
except TypeError:
|
||||
author = "None"
|
||||
msg = f"{preset}{space * (22 - len(preset))}{author}\n"
|
||||
preset_list += msg
|
||||
|
||||
page_list = []
|
||||
colour = await ctx.embed_colour()
|
||||
for page in pagify(preset_list, delims=[", "], page_length=1000):
|
||||
formatted_page = box(page, lang="ini")
|
||||
embed = discord.Embed(colour=colour, description=f"{header}\n{formatted_page}")
|
||||
embed.set_footer(
|
||||
text=_("{num} preset(s)").format(num=humanize_number(len(list(eq_presets.keys()))))
|
||||
)
|
||||
page_list.append(embed)
|
||||
await menu(ctx, page_list, DEFAULT_CONTROLS)
|
||||
|
||||
@command_equalizer.command(name="load")
|
||||
async def command_equalizer_load(self, ctx: commands.Context, eq_preset: str):
|
||||
"""Load a saved eq preset."""
|
||||
eq_preset = eq_preset.lower()
|
||||
eq_presets = await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets()
|
||||
try:
|
||||
eq_values = eq_presets[eq_preset]["bands"]
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("No Preset Found"),
|
||||
description=_(
|
||||
"Preset named {eq_preset} does not exist.".format(eq_preset=eq_preset)
|
||||
),
|
||||
)
|
||||
except TypeError:
|
||||
eq_values = eq_presets[eq_preset]
|
||||
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Load Preset"),
|
||||
description=_("You need the DJ role to load equalizer presets."),
|
||||
)
|
||||
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq_values)
|
||||
await self._eq_check(ctx, player)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
message = await ctx.send(
|
||||
content=box(eq.visualise(), lang="ini"),
|
||||
embed=discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("The {eq_preset} preset was loaded.".format(eq_preset=eq_preset)),
|
||||
),
|
||||
)
|
||||
player.store("eq_message", message)
|
||||
|
||||
@command_equalizer.command(name="reset")
|
||||
async def command_equalizer_reset(self, ctx: commands.Context):
|
||||
"""Reset the eq to 0 across all bands."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Preset"),
|
||||
description=_("You need the DJ role to reset the equalizer."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
|
||||
for band in range(eq.band_count):
|
||||
eq.set_gain(band, 0.0)
|
||||
|
||||
await self._apply_gains(ctx.guild.id, eq.bands)
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
player.store("eq", eq)
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
message = await ctx.send(
|
||||
content=box(eq.visualise(), lang="ini"),
|
||||
embed=discord.Embed(
|
||||
colour=await ctx.embed_colour(), title=_("Equalizer values have been reset.")
|
||||
),
|
||||
)
|
||||
player.store("eq_message", message)
|
||||
|
||||
@command_equalizer.command(name="save")
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
async def command_equalizer_save(self, ctx: commands.Context, eq_preset: str = None):
|
||||
"""Save the current eq settings to a preset."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Save Preset"),
|
||||
description=_("You need the DJ role to save equalizer presets."),
|
||||
)
|
||||
if not eq_preset:
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Please enter a name for this equalizer preset.")
|
||||
)
|
||||
try:
|
||||
eq_name_msg = await self.bot.wait_for(
|
||||
"message",
|
||||
timeout=15.0,
|
||||
check=MessagePredicate.regex(fr"^(?!{re.escape(ctx.prefix)})", ctx),
|
||||
)
|
||||
eq_preset = eq_name_msg.content.split(" ")[0].strip('"').lower()
|
||||
except asyncio.TimeoutError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Save Preset"),
|
||||
description=_(
|
||||
"No equalizer preset name entered, try the command again later."
|
||||
),
|
||||
)
|
||||
eq_preset = eq_preset or ""
|
||||
eq_exists_msg = None
|
||||
eq_preset = eq_preset.lower().lstrip(ctx.prefix)
|
||||
eq_presets = await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets()
|
||||
eq_list = list(eq_presets.keys())
|
||||
|
||||
if len(eq_preset) > 20:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Save Preset"),
|
||||
description=_("Try the command again with a shorter name."),
|
||||
)
|
||||
if eq_preset in eq_list:
|
||||
eq_exists_msg = await self.send_embed_msg(
|
||||
ctx, title=_("Preset name already exists, do you want to replace it?")
|
||||
)
|
||||
start_adding_reactions(eq_exists_msg, ReactionPredicate.YES_OR_NO_EMOJIS)
|
||||
pred = ReactionPredicate.yes_or_no(eq_exists_msg, ctx.author)
|
||||
await self.bot.wait_for("reaction_add", check=pred)
|
||||
if not pred.result:
|
||||
await self._clear_react(eq_exists_msg)
|
||||
embed2 = discord.Embed(
|
||||
colour=await ctx.embed_colour(), title=_("Not saving preset.")
|
||||
)
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await eq_exists_msg.edit(embed=embed2)
|
||||
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
to_append = {eq_preset: {"author": ctx.author.id, "bands": eq.bands}}
|
||||
new_eq_presets = {**eq_presets, **to_append}
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets.set(new_eq_presets)
|
||||
embed3 = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Current equalizer saved to the {preset_name} preset.").format(
|
||||
preset_name=eq_preset
|
||||
),
|
||||
)
|
||||
if eq_exists_msg:
|
||||
await self._clear_react(eq_exists_msg)
|
||||
await eq_exists_msg.edit(embed=embed3)
|
||||
else:
|
||||
await self.send_embed_msg(ctx, embed=embed3)
|
||||
|
||||
@command_equalizer.command(name="set")
|
||||
async def command_equalizer_set(
|
||||
self, ctx: commands.Context, band_name_or_position, band_value: float
|
||||
):
|
||||
"""Set an eq band with a band number or name and value.
|
||||
|
||||
Band positions are 1-15 and values have a range of -0.25 to 1.0.
|
||||
Band names are 25, 40, 63, 100, 160, 250, 400, 630, 1k, 1.6k, 2.5k, 4k,
|
||||
6.3k, 10k, and 16k Hz.
|
||||
Setting a band value to -0.25 nullifies it while +0.25 is double.
|
||||
"""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Set Preset"),
|
||||
description=_("You need the DJ role to set equalizer presets."),
|
||||
)
|
||||
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
band_names = [
|
||||
"25",
|
||||
"40",
|
||||
"63",
|
||||
"100",
|
||||
"160",
|
||||
"250",
|
||||
"400",
|
||||
"630",
|
||||
"1k",
|
||||
"1.6k",
|
||||
"2.5k",
|
||||
"4k",
|
||||
"6.3k",
|
||||
"10k",
|
||||
"16k",
|
||||
]
|
||||
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
bands_num = eq.band_count
|
||||
if band_value > 1:
|
||||
band_value = 1
|
||||
elif band_value <= -0.25:
|
||||
band_value = -0.25
|
||||
else:
|
||||
band_value = round(band_value, 1)
|
||||
|
||||
try:
|
||||
band_number = int(band_name_or_position) - 1
|
||||
except ValueError:
|
||||
band_number = 1000
|
||||
|
||||
if band_number not in range(0, bands_num) and band_name_or_position not in band_names:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Band"),
|
||||
description=_(
|
||||
"Valid band numbers are 1-15 or the band names listed in "
|
||||
"the help for this command."
|
||||
),
|
||||
)
|
||||
|
||||
if band_name_or_position in band_names:
|
||||
band_pos = band_names.index(band_name_or_position)
|
||||
band_int = False
|
||||
eq.set_gain(int(band_pos), band_value)
|
||||
await self._apply_gain(ctx.guild.id, int(band_pos), band_value)
|
||||
else:
|
||||
band_int = True
|
||||
eq.set_gain(band_number, band_value)
|
||||
await self._apply_gain(ctx.guild.id, band_number, band_value)
|
||||
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
player.store("eq", eq)
|
||||
band_name = band_names[band_number] if band_int else band_name_or_position
|
||||
message = await ctx.send(
|
||||
content=box(eq.visualise(), lang="ini"),
|
||||
embed=discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Preset Modified"),
|
||||
description=_("The {band_name}Hz band has been set to {band_value}.").format(
|
||||
band_name=band_name, band_value=band_value
|
||||
),
|
||||
),
|
||||
)
|
||||
player.store("eq_message", message)
|
||||
168
redbot/cogs/audio/core/commands/llset.py
Normal file
168
redbot/cogs/audio/core/commands/llset.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import logging
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.lavalink_setup")
|
||||
|
||||
|
||||
class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="llsetup", aliases=["llset"])
|
||||
@commands.is_owner()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_llsetup(self, ctx: commands.Context):
|
||||
"""Lavalink server configuration options."""
|
||||
|
||||
@command_llsetup.command(name="external")
|
||||
async def command_llsetup_external(self, ctx: commands.Context):
|
||||
"""Toggle using external Lavalink servers."""
|
||||
external = await self.config.use_external_lavalink()
|
||||
await self.config.use_external_lavalink.set(not external)
|
||||
|
||||
if external:
|
||||
embed = discord.Embed(
|
||||
title=_("Setting Changed"),
|
||||
description=_("External Lavalink server: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not external else _("Disabled")
|
||||
),
|
||||
)
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
else:
|
||||
try:
|
||||
if self.player_manager is not None:
|
||||
await self.player_manager.shutdown()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_(
|
||||
"External Lavalink server: {true_or_false}\n"
|
||||
"For it to take effect please reload "
|
||||
"Audio (`{prefix}reload audio`)."
|
||||
).format(
|
||||
true_or_false=_("Enabled") if not external else _("Disabled"),
|
||||
prefix=ctx.prefix,
|
||||
),
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("External Lavalink server: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not external else _("Disabled")
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="host")
|
||||
async def command_llsetup_host(self, ctx: commands.Context, host: str):
|
||||
"""Set the Lavalink server host."""
|
||||
await self.config.host.set(host)
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Host set to {host}.").format(host=host),
|
||||
footer=footer,
|
||||
)
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="password")
|
||||
async def command_llsetup_password(self, ctx: commands.Context, password: str):
|
||||
"""Set the Lavalink server password."""
|
||||
await self.config.password.set(str(password))
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Server password set to {password}.").format(password=password),
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="restport")
|
||||
async def command_llsetup_restport(self, ctx: commands.Context, rest_port: int):
|
||||
"""Set the Lavalink REST server port."""
|
||||
await self.config.rest_port.set(rest_port)
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("REST port set to {port}.").format(port=rest_port),
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="wsport")
|
||||
async def command_llsetup_wsport(self, ctx: commands.Context, ws_port: int):
|
||||
"""Set the Lavalink websocket server port."""
|
||||
await self.config.ws_port.set(ws_port)
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Websocket port set to {port}.").format(port=ws_port),
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
118
redbot/cogs/audio/core/commands/localtracks.py
Normal file
118
redbot/cogs/audio/core/commands/localtracks.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import MutableMapping
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page
|
||||
|
||||
from ...audio_dataclasses import LocalPath, Query
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.local_track")
|
||||
|
||||
|
||||
class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="local")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_local(self, ctx: commands.Context):
|
||||
"""Local playback commands."""
|
||||
|
||||
@command_local.command(name="folder", aliases=["start"])
|
||||
async def command_local_folder(self, ctx: commands.Context, *, folder: str = None):
|
||||
"""Play all songs in a localtracks folder."""
|
||||
if not await self.localtracks_folder_exists(ctx):
|
||||
return
|
||||
|
||||
if not folder:
|
||||
await ctx.invoke(self.command_local_play)
|
||||
else:
|
||||
folder = folder.strip()
|
||||
_dir = LocalPath.joinpath(self.local_folder_current_path, folder)
|
||||
if not _dir.exists():
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Folder Not Found"),
|
||||
description=_("Localtracks folder named {name} does not exist.").format(
|
||||
name=folder
|
||||
),
|
||||
)
|
||||
query = Query.process_input(
|
||||
_dir, self.local_folder_current_path, search_subfolders=True
|
||||
)
|
||||
await self._local_play_all(ctx, query, from_search=False if not folder else True)
|
||||
|
||||
@command_local.command(name="play")
|
||||
async def command_local_play(self, ctx: commands.Context):
|
||||
"""Play a local track."""
|
||||
if not await self.localtracks_folder_exists(ctx):
|
||||
return
|
||||
localtracks_folders = await self.get_localtracks_folders(ctx, search_subfolders=True)
|
||||
if not localtracks_folders:
|
||||
return await self.send_embed_msg(ctx, title=_("No album folders found."))
|
||||
async with ctx.typing():
|
||||
len_folder_pages = math.ceil(len(localtracks_folders) / 5)
|
||||
folder_page_list = []
|
||||
for page_num in range(1, len_folder_pages + 1):
|
||||
embed = await self._build_search_page(ctx, localtracks_folders, page_num)
|
||||
folder_page_list.append(embed)
|
||||
|
||||
async def _local_folder_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
await self._search_button_action(ctx, localtracks_folders, emoji, page)
|
||||
return None
|
||||
|
||||
local_folder_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await menu(ctx, folder_page_list, local_folder_controls)
|
||||
|
||||
@command_local.command(name="search")
|
||||
async def command_local_search(self, ctx: commands.Context, *, search_words):
|
||||
"""Search for songs across all localtracks folders."""
|
||||
if not await self.localtracks_folder_exists(ctx):
|
||||
return
|
||||
all_tracks = await self.get_localtrack_folder_list(
|
||||
ctx,
|
||||
(
|
||||
Query.process_input(
|
||||
Path(await self.config.localpath()).absolute(),
|
||||
self.local_folder_current_path,
|
||||
search_subfolders=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
if not all_tracks:
|
||||
return await self.send_embed_msg(ctx, title=_("No album folders found."))
|
||||
async with ctx.typing():
|
||||
search_list = await self._build_local_search_list(all_tracks, search_words)
|
||||
if not search_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No matches."))
|
||||
return await ctx.invoke(self.command_search, query=search_list)
|
||||
138
redbot/cogs/audio/core/commands/miscellaneous.py
Normal file
138
redbot/cogs/audio/core/commands/miscellaneous.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import datetime
|
||||
import heapq
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.chat_formatting import humanize_number, pagify
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.miscellaneous")
|
||||
|
||||
|
||||
class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.command(name="sing")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_sing(self, ctx: commands.Context):
|
||||
"""Make Red sing one of her songs."""
|
||||
ids = (
|
||||
"zGTkAVsrfg8",
|
||||
"cGMWL8cOeAU",
|
||||
"vFrjMq4aL-g",
|
||||
"WROI5WYBU_A",
|
||||
"41tIUr_ex3g",
|
||||
"f9O2Rjn1azc",
|
||||
)
|
||||
url = f"https://www.youtube.com/watch?v={random.choice(ids)}"
|
||||
await ctx.invoke(self.command_play, query=url)
|
||||
|
||||
@commands.command(name="audiostats")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_audiostats(self, ctx: commands.Context):
|
||||
"""Audio stats."""
|
||||
server_num = len(lavalink.active_players())
|
||||
total_num = len(lavalink.all_players())
|
||||
|
||||
msg = ""
|
||||
async for p in AsyncIter(lavalink.all_players()):
|
||||
connect_start = p.fetch("connect")
|
||||
connect_dur = self.get_time_string(
|
||||
int((datetime.datetime.utcnow() - connect_start).total_seconds())
|
||||
)
|
||||
try:
|
||||
if not p.current:
|
||||
raise AttributeError
|
||||
current_title = self.get_track_description(
|
||||
p.current, self.local_folder_current_path
|
||||
)
|
||||
msg += "{} [`{}`]: {}\n".format(p.channel.guild.name, connect_dur, current_title)
|
||||
except AttributeError:
|
||||
msg += "{} [`{}`]: **{}**\n".format(
|
||||
p.channel.guild.name, connect_dur, _("Nothing playing.")
|
||||
)
|
||||
|
||||
if total_num == 0:
|
||||
return await self.send_embed_msg(ctx, title=_("Not connected anywhere."))
|
||||
servers_embed = []
|
||||
pages = 1
|
||||
for page in pagify(msg, delims=["\n"], page_length=1500):
|
||||
em = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Playing in {num}/{total} servers:").format(
|
||||
num=humanize_number(server_num), total=humanize_number(total_num)
|
||||
),
|
||||
description=page,
|
||||
)
|
||||
em.set_footer(
|
||||
text=_("Page {}/{}").format(
|
||||
humanize_number(pages), humanize_number((math.ceil(len(msg) / 1500)))
|
||||
)
|
||||
)
|
||||
pages += 1
|
||||
servers_embed.append(em)
|
||||
|
||||
await menu(ctx, servers_embed, DEFAULT_CONTROLS)
|
||||
|
||||
@commands.command(name="percent")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_percent(self, ctx: commands.Context):
|
||||
"""Queue percentage."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
queue_tracks = player.queue
|
||||
requesters = {"total": 0, "users": {}}
|
||||
|
||||
async def _usercount(req_username):
|
||||
if req_username in requesters["users"]:
|
||||
requesters["users"][req_username]["songcount"] += 1
|
||||
requesters["total"] += 1
|
||||
else:
|
||||
requesters["users"][req_username] = {}
|
||||
requesters["users"][req_username]["songcount"] = 1
|
||||
requesters["total"] += 1
|
||||
|
||||
async for track in AsyncIter(queue_tracks):
|
||||
req_username = "{}#{}".format(track.requester.name, track.requester.discriminator)
|
||||
await _usercount(req_username)
|
||||
|
||||
try:
|
||||
req_username = "{}#{}".format(
|
||||
player.current.requester.name, player.current.requester.discriminator
|
||||
)
|
||||
await _usercount(req_username)
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
async for req_username in AsyncIter(requesters["users"]):
|
||||
percentage = float(requesters["users"][req_username]["songcount"]) / float(
|
||||
requesters["total"]
|
||||
)
|
||||
requesters["users"][req_username]["percent"] = round(percentage * 100, 1)
|
||||
|
||||
top_queue_users = heapq.nlargest(
|
||||
20,
|
||||
[
|
||||
(x, requesters["users"][x][y])
|
||||
for x in requesters["users"]
|
||||
for y in requesters["users"][x]
|
||||
if y == "percent"
|
||||
],
|
||||
key=lambda x: x[1],
|
||||
)
|
||||
queue_user = ["{}: {:g}%".format(x[0], x[1]) for x in top_queue_users]
|
||||
queue_user_list = "\n".join(queue_user)
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Queued and playing tracks:"), description=queue_user_list
|
||||
)
|
||||
861
redbot/cogs/audio/core/commands/player.py
Normal file
861
redbot/cogs/audio/core/commands/player.py
Normal file
@@ -0,0 +1,861 @@
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
from typing import MutableMapping, Optional
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from discord.embeds import EmptyEmbed
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page
|
||||
|
||||
from ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
|
||||
from ...audio_logging import IS_DEBUG
|
||||
from ...errors import DatabaseError, QueryUnauthorized, SpotifyFetchError, TrackEnqueueError
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.player")
|
||||
|
||||
|
||||
class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.command(name="play")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_play(self, ctx: commands.Context, *, query: str):
|
||||
"""Play a URL or search for a track."""
|
||||
query = Query.process_input(query, self.local_folder_current_path)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
restrict = await self.config.restrict()
|
||||
if restrict and self.match_url(str(query)):
|
||||
valid_url = self.is_url_allowed(str(query))
|
||||
if not valid_url:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That URL is not allowed."),
|
||||
)
|
||||
elif not await self.is_query_allowed(self.config, ctx.guild, f"{query}", query_obj=query):
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.")
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if guild_data["dj_enabled"] and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the play command."),
|
||||
)
|
||||
if not query.valid:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("No tracks found for `{query}`.").format(
|
||||
query=query.to_string_user()
|
||||
),
|
||||
)
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
if query.is_spotify:
|
||||
return await self._get_spotify_tracks(ctx, query)
|
||||
try:
|
||||
await self._enqueue_tracks(ctx, query)
|
||||
except QueryUnauthorized as err:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=err.message
|
||||
)
|
||||
|
||||
@commands.command(name="bumpplay")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_bumpplay(
|
||||
self, ctx: commands.Context, play_now: Optional[bool] = False, *, query: str
|
||||
):
|
||||
"""Force play a URL or search for a track."""
|
||||
query = Query.process_input(query, self.local_folder_current_path)
|
||||
if not query.single_track:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("Only single tracks work with bump play."),
|
||||
)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
restrict = await self.config.restrict()
|
||||
if restrict and self.match_url(str(query)):
|
||||
valid_url = self.is_url_allowed(str(query))
|
||||
if not valid_url:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That URL is not allowed."),
|
||||
)
|
||||
elif not await self.is_query_allowed(self.config, ctx.guild, f"{query}", query_obj=query):
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.")
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if guild_data["dj_enabled"] and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the play command."),
|
||||
)
|
||||
if not query.valid:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("No tracks found for `{query}`.").format(
|
||||
query=query.to_string_user()
|
||||
),
|
||||
)
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
try:
|
||||
if query.is_spotify:
|
||||
tracks = await self._get_spotify_tracks(ctx, query)
|
||||
else:
|
||||
tracks = await self._enqueue_tracks(ctx, query, enqueue=False)
|
||||
except QueryUnauthorized as err:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=err.message
|
||||
)
|
||||
if isinstance(tracks, discord.Message):
|
||||
return
|
||||
elif not tracks:
|
||||
self.update_player_lock(ctx, False)
|
||||
title = _("Unable To Play Tracks")
|
||||
desc = _("No tracks found for `{query}`.").format(query=query.to_string_user())
|
||||
embed = discord.Embed(title=title, description=desc)
|
||||
if await self.config.use_external_lavalink() and query.is_local:
|
||||
embed.description = _(
|
||||
"Local tracks will not work "
|
||||
"if the `Lavalink.jar` cannot see the track.\n"
|
||||
"This may be due to permissions or because Lavalink.jar is being run "
|
||||
"in a different machine than the local tracks."
|
||||
)
|
||||
elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT:
|
||||
title = _("Track is not playable.")
|
||||
embed = discord.Embed(title=title)
|
||||
embed.description = _(
|
||||
"**{suffix}** is not a fully supported format and some " "tracks may not play."
|
||||
).format(suffix=query.suffix)
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
queue_dur = await self.track_remaining_duration(ctx)
|
||||
index = query.track_index
|
||||
seek = 0
|
||||
if query.start_time:
|
||||
seek = query.start_time
|
||||
single_track = (
|
||||
tracks
|
||||
if isinstance(tracks, lavalink.rest_api.Track)
|
||||
else tracks[index]
|
||||
if index
|
||||
else tracks[0]
|
||||
)
|
||||
if seek and seek > 0:
|
||||
single_track.start_timestamp = seek * 1000
|
||||
if not await self.is_query_allowed(
|
||||
self.config,
|
||||
ctx.guild,
|
||||
(
|
||||
f"{single_track.title} {single_track.author} {single_track.uri} "
|
||||
f"{str(Query.process_input(single_track, self.local_folder_current_path))}"
|
||||
),
|
||||
):
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("This track is not allowed in this server."),
|
||||
)
|
||||
elif guild_data["maxlength"] > 0:
|
||||
if self.is_track_too_long(single_track, guild_data["maxlength"]):
|
||||
single_track.requester = ctx.author
|
||||
player.queue.insert(0, single_track)
|
||||
player.maybe_shuffle()
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, single_track, ctx.author
|
||||
)
|
||||
else:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Track exceeds maximum length."),
|
||||
)
|
||||
|
||||
else:
|
||||
single_track.requester = ctx.author
|
||||
single_track.extras["bumped"] = True
|
||||
player.queue.insert(0, single_track)
|
||||
player.maybe_shuffle()
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, single_track, ctx.author
|
||||
)
|
||||
description = self.get_track_description(single_track, self.local_folder_current_path)
|
||||
footer = None
|
||||
if not play_now and not guild_data["shuffle"] and queue_dur > 0:
|
||||
footer = _("{time} until track playback: #1 in queue").format(
|
||||
time=self.format_time(queue_dur)
|
||||
)
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Track Enqueued"), description=description, footer=footer
|
||||
)
|
||||
|
||||
if not player.current:
|
||||
await player.play()
|
||||
elif play_now:
|
||||
await player.skip()
|
||||
|
||||
self.update_player_lock(ctx, False)
|
||||
|
||||
@commands.command(name="genre")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_genre(self, ctx: commands.Context):
|
||||
"""Pick a Spotify playlist from a list of categories to start playing."""
|
||||
|
||||
async def _category_search_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
output = await self._genre_search_button_action(ctx, category_list, emoji, page)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return output
|
||||
|
||||
async def _playlist_search_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
output = await self._genre_search_button_action(
|
||||
ctx, playlists_list, emoji, page, playlist=True
|
||||
)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return output
|
||||
|
||||
category_search_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
playlist_search_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
|
||||
api_data = await self._check_api_tokens()
|
||||
if any([not api_data["spotify_client_id"], not api_data["spotify_client_secret"]]):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Environment"),
|
||||
description=_(
|
||||
"The owner needs to set the Spotify client ID and Spotify client secret, "
|
||||
"before Spotify URLs or codes can be used. "
|
||||
"\nSee `{prefix}audioset spotifyapi` for instructions."
|
||||
).format(prefix=ctx.prefix),
|
||||
)
|
||||
elif not api_data["youtube_api"]:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Environment"),
|
||||
description=_(
|
||||
"The owner needs to set the YouTube API key before Spotify URLs or "
|
||||
"codes can be used.\nSee `{prefix}audioset youtubeapi` for instructions."
|
||||
).format(prefix=ctx.prefix),
|
||||
)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
if guild_data["dj_enabled"] and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the genre command."),
|
||||
)
|
||||
try:
|
||||
category_list = await self.api_interface.spotify_api.get_categories(ctx=ctx)
|
||||
except SpotifyFetchError as error:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("No categories found"),
|
||||
description=error.message.format(prefix=ctx.prefix),
|
||||
)
|
||||
if not category_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No categories found, try again later."))
|
||||
len_folder_pages = math.ceil(len(category_list) / 5)
|
||||
category_search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_folder_pages + 1)):
|
||||
embed = await self._build_genre_search_page(
|
||||
ctx, category_list, page_num, _("Categories")
|
||||
)
|
||||
category_search_page_list.append(embed)
|
||||
cat_menu_output = await menu(ctx, category_search_page_list, category_search_controls)
|
||||
if not cat_menu_output:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("No categories selected, try again later.")
|
||||
)
|
||||
category_name, category_pick = cat_menu_output
|
||||
playlists_list = await self.api_interface.spotify_api.get_playlist_from_category(
|
||||
category_pick, ctx=ctx
|
||||
)
|
||||
if not playlists_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No categories found, try again later."))
|
||||
len_folder_pages = math.ceil(len(playlists_list) / 5)
|
||||
playlists_search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_folder_pages + 1)):
|
||||
embed = await self._build_genre_search_page(
|
||||
ctx,
|
||||
playlists_list,
|
||||
page_num,
|
||||
_("Playlists for {friendly_name}").format(friendly_name=category_name),
|
||||
playlist=True,
|
||||
)
|
||||
playlists_search_page_list.append(embed)
|
||||
playlists_pick = await menu(ctx, playlists_search_page_list, playlist_search_controls)
|
||||
query = Query.process_input(playlists_pick, self.local_folder_current_path)
|
||||
if not query.valid:
|
||||
return await self.send_embed_msg(ctx, title=_("No tracks to play."))
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
if query.is_spotify:
|
||||
return await self._get_spotify_tracks(ctx, query)
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Couldn't find tracks for the selected playlist.")
|
||||
)
|
||||
|
||||
@commands.command(name="autoplay")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
@commands.mod_or_permissions(manage_guild=True)
|
||||
async def command_autoplay(self, ctx: commands.Context):
|
||||
"""Starts auto play."""
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
if guild_data["dj_enabled"] and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the autoplay command."),
|
||||
)
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
try:
|
||||
await self.api_interface.autoplay(player, self.playlist_api)
|
||||
except DatabaseError:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
await self.send_embed_msg(notify_channel, title=_("Couldn't get a valid track."))
|
||||
return
|
||||
|
||||
if not guild_data["auto_play"]:
|
||||
await ctx.invoke(self.command_audioset_autoplay_toggle)
|
||||
if not guild_data["notify"] and (
|
||||
(player.current and not player.current.extras.get("autoplay")) or not player.current
|
||||
):
|
||||
await self.send_embed_msg(ctx, title=_("Auto play started."))
|
||||
elif player.current:
|
||||
await self.send_embed_msg(ctx, title=_("Adding a track to queue."))
|
||||
|
||||
@commands.command(name="search")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_search(self, ctx: commands.Context, *, query: str):
|
||||
"""Pick a track with a search.
|
||||
|
||||
Use `[p]search list <search term>` to queue all tracks found on YouTube.
|
||||
Use `[p]search sc<search term>` will search SoundCloud instead of YouTube.
|
||||
"""
|
||||
|
||||
async def _search_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
await self._search_button_action(ctx, tracks, emoji, page)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
search_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("You must be in the voice channel to enqueue tracks."),
|
||||
)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
before_queue_length = len(player.queue)
|
||||
|
||||
if not isinstance(query, list):
|
||||
query = Query.process_input(query, self.local_folder_current_path)
|
||||
restrict = await self.config.restrict()
|
||||
if restrict and self.match_url(str(query)):
|
||||
valid_url = self.is_url_allowed(str(query))
|
||||
if not valid_url:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That URL is not allowed."),
|
||||
)
|
||||
if not await self.is_query_allowed(
|
||||
self.config, ctx.guild, f"{query}", query_obj=query
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That track is not allowed."),
|
||||
)
|
||||
if query.invoked_from == "search list" or query.invoked_from == "local folder":
|
||||
if query.invoked_from == "search list" and not query.is_local:
|
||||
try:
|
||||
result, called_api = await self.api_interface.fetch_track(
|
||||
ctx, player, query
|
||||
)
|
||||
except TrackEnqueueError:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable get a track from Lavalink at the moment, "
|
||||
"try again in a few minutes."
|
||||
),
|
||||
)
|
||||
|
||||
tracks = result.tracks
|
||||
else:
|
||||
try:
|
||||
query.search_subfolders = True
|
||||
tracks = await self.get_localtrack_folder_tracks(ctx, player, query)
|
||||
except TrackEnqueueError:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable get a track from Lavalink at the moment, "
|
||||
"try again in a few minutes."
|
||||
),
|
||||
)
|
||||
if not tracks:
|
||||
embed = discord.Embed(title=_("Nothing found."))
|
||||
if await self.config.use_external_lavalink() and query.is_local:
|
||||
embed.description = _(
|
||||
"Local tracks will not work "
|
||||
"if the `Lavalink.jar` cannot see the track.\n"
|
||||
"This may be due to permissions or because Lavalink.jar is being run "
|
||||
"in a different machine than the local tracks."
|
||||
)
|
||||
elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT:
|
||||
embed = discord.Embed(title=_("Track is not playable."))
|
||||
embed.description = _(
|
||||
"**{suffix}** is not a fully supported format and some "
|
||||
"tracks may not play."
|
||||
).format(suffix=query.suffix)
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
queue_dur = await self.queue_duration(ctx)
|
||||
queue_total_duration = self.format_time(queue_dur)
|
||||
if guild_data["dj_enabled"] and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
track_len = 0
|
||||
empty_queue = not player.queue
|
||||
async for track in AsyncIter(tracks):
|
||||
if len(player.queue) >= 10000:
|
||||
continue
|
||||
if not await self.is_query_allowed(
|
||||
self.config,
|
||||
ctx.guild,
|
||||
(
|
||||
f"{track.title} {track.author} {track.uri} "
|
||||
f"{str(Query.process_input(track, self.local_folder_current_path))}"
|
||||
),
|
||||
):
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||
continue
|
||||
elif guild_data["maxlength"] > 0:
|
||||
if self.is_track_too_long(track, guild_data["maxlength"]):
|
||||
track_len += 1
|
||||
player.add(ctx.author, track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
|
||||
)
|
||||
else:
|
||||
track_len += 1
|
||||
player.add(ctx.author, track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
|
||||
)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
player.maybe_shuffle(0 if empty_queue else 1)
|
||||
if len(tracks) > track_len:
|
||||
maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format(
|
||||
bad_tracks=(len(tracks) - track_len)
|
||||
)
|
||||
else:
|
||||
maxlength_msg = ""
|
||||
songembed = discord.Embed(
|
||||
title=_("Queued {num} track(s).{maxlength_msg}").format(
|
||||
num=track_len, maxlength_msg=maxlength_msg
|
||||
)
|
||||
)
|
||||
if not guild_data["shuffle"] and queue_dur > 0:
|
||||
if query.is_local and query.is_album:
|
||||
footer = _("folder")
|
||||
else:
|
||||
footer = _("search")
|
||||
|
||||
songembed.set_footer(
|
||||
text=_(
|
||||
"{time} until start of {type} playback: starts at #{position} in queue"
|
||||
).format(
|
||||
time=queue_total_duration,
|
||||
position=before_queue_length + 1,
|
||||
type=footer,
|
||||
)
|
||||
)
|
||||
return await self.send_embed_msg(ctx, embed=songembed)
|
||||
elif query.is_local and query.single_track:
|
||||
tracks = await self.get_localtrack_folder_list(ctx, query)
|
||||
elif query.is_local and query.is_album:
|
||||
if ctx.invoked_with == "folder":
|
||||
return await self._local_play_all(ctx, query, from_search=True)
|
||||
else:
|
||||
tracks = await self.get_localtrack_folder_list(ctx, query)
|
||||
else:
|
||||
try:
|
||||
result, called_api = await self.api_interface.fetch_track(ctx, player, query)
|
||||
except TrackEnqueueError:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable get a track from Lavalink at the moment,"
|
||||
"try again in a few minutes."
|
||||
),
|
||||
)
|
||||
tracks = result.tracks
|
||||
if not tracks:
|
||||
embed = discord.Embed(title=_("Nothing found."))
|
||||
if await self.config.use_external_lavalink() and query.is_local:
|
||||
embed.description = _(
|
||||
"Local tracks will not work "
|
||||
"if the `Lavalink.jar` cannot see the track.\n"
|
||||
"This may be due to permissions or because Lavalink.jar is being run "
|
||||
"in a different machine than the local tracks."
|
||||
)
|
||||
elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT:
|
||||
embed = discord.Embed(title=_("Track is not playable."))
|
||||
embed.description = _(
|
||||
"**{suffix}** is not a fully supported format and some "
|
||||
"tracks may not play."
|
||||
).format(suffix=query.suffix)
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
else:
|
||||
tracks = query
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
|
||||
len_search_pages = math.ceil(len(tracks) / 5)
|
||||
search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_search_pages + 1)):
|
||||
embed = await self._build_search_page(ctx, tracks, page_num)
|
||||
search_page_list.append(embed)
|
||||
|
||||
if dj_enabled and not can_skip:
|
||||
return await menu(ctx, search_page_list, DEFAULT_CONTROLS)
|
||||
|
||||
await menu(ctx, search_page_list, search_controls)
|
||||
1951
redbot/cogs/audio/core/commands/playlists.py
Normal file
1951
redbot/cogs/audio/core/commands/playlists.py
Normal file
File diff suppressed because it is too large
Load Diff
359
redbot/cogs/audio/core/commands/queue.py
Normal file
359
redbot/cogs/audio/core/commands/queue.py
Normal file
@@ -0,0 +1,359 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
from typing import MutableMapping, Optional, Union, Tuple
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import (
|
||||
DEFAULT_CONTROLS,
|
||||
close_menu,
|
||||
menu,
|
||||
next_page,
|
||||
prev_page,
|
||||
start_adding_reactions,
|
||||
)
|
||||
from redbot.core.utils.predicates import ReactionPredicate
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.queue")
|
||||
|
||||
|
||||
class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="queue", invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_queue(self, ctx: commands.Context, *, page: int = 1):
|
||||
"""List the songs in the queue."""
|
||||
|
||||
async def _queue_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
await ctx.send_help(self.command_queue)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
queue_controls = {
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
"\N{INFORMATION SOURCE}": _queue_menu,
|
||||
}
|
||||
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
if player.current and not player.queue:
|
||||
arrow = await self.draw_time(ctx)
|
||||
pos = self.format_time(player.position)
|
||||
if player.current.is_stream:
|
||||
dur = "LIVE"
|
||||
else:
|
||||
dur = self.format_time(player.current.length)
|
||||
song = self.get_track_description(player.current, self.local_folder_current_path) or ""
|
||||
song += _("\n Requested by: **{track.requester}**")
|
||||
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
||||
song = song.format(track=player.current, arrow=arrow, pos=pos, dur=dur)
|
||||
embed = discord.Embed(title=_("Now Playing"), description=song)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
if guild_data["thumbnail"] and player.current and player.current.thumbnail:
|
||||
embed.set_thumbnail(url=player.current.thumbnail)
|
||||
|
||||
shuffle = guild_data["shuffle"]
|
||||
repeat = guild_data["repeat"]
|
||||
autoplay = guild_data["auto_play"]
|
||||
text = ""
|
||||
text += (
|
||||
_("Auto-Play")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if autoplay else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Shuffle")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if shuffle else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Repeat")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if repeat else "\N{CROSS MARK}")
|
||||
)
|
||||
embed.set_footer(text=text)
|
||||
message = await self.send_embed_msg(ctx, embed=embed)
|
||||
dj_enabled = self._dj_status_cache.setdefault(ctx.guild.id, guild_data["dj_enabled"])
|
||||
vote_enabled = guild_data["vote_enabled"]
|
||||
if (
|
||||
(dj_enabled or vote_enabled)
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return
|
||||
|
||||
expected: Union[Tuple[str, ...]] = ("⏮", "⏹", "⏯", "⏭", "\N{CROSS MARK}")
|
||||
emoji = {
|
||||
"prev": "⏮",
|
||||
"stop": "⏹",
|
||||
"pause": "⏯",
|
||||
"next": "⏭",
|
||||
"close": "\N{CROSS MARK}",
|
||||
}
|
||||
if not player.queue and not autoplay:
|
||||
expected = ("⏹", "⏯", "\N{CROSS MARK}")
|
||||
if player.current:
|
||||
task: Optional[asyncio.Task] = start_adding_reactions(message, expected[:5])
|
||||
else:
|
||||
task: Optional[asyncio.Task] = None
|
||||
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=ReactionPredicate.with_emojis(expected, message, ctx.author),
|
||||
timeout=30.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._clear_react(message, emoji)
|
||||
else:
|
||||
if task is not None:
|
||||
task.cancel()
|
||||
reacts = {v: k for k, v in emoji.items()}
|
||||
react = reacts[r.emoji]
|
||||
if react == "prev":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_prev)
|
||||
elif react == "stop":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_stop)
|
||||
elif react == "pause":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_pause)
|
||||
elif react == "next":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_skip)
|
||||
elif react == "close":
|
||||
await message.delete()
|
||||
return
|
||||
elif not player.current and not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
async with ctx.typing():
|
||||
limited_queue = player.queue[:500] # TODO: Improve when Toby menu's are merged
|
||||
len_queue_pages = math.ceil(len(limited_queue) / 10)
|
||||
queue_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_queue_pages + 1)):
|
||||
embed = await self._build_queue_page(ctx, limited_queue, player, page_num)
|
||||
queue_page_list.append(embed)
|
||||
if page > len_queue_pages:
|
||||
page = len_queue_pages
|
||||
return await menu(ctx, queue_page_list, queue_controls, page=(page - 1))
|
||||
|
||||
@command_queue.command(name="clear")
|
||||
async def command_queue_clear(self, ctx: commands.Context):
|
||||
"""Clears the queue."""
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if (
|
||||
dj_enabled
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Clear Queue"),
|
||||
description=_("You need the DJ role to clear the queue."),
|
||||
)
|
||||
player.queue.clear()
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Queue Modified"), description=_("The queue has been cleared.")
|
||||
)
|
||||
|
||||
@command_queue.command(name="clean")
|
||||
async def command_queue_clean(self, ctx: commands.Context):
|
||||
"""Removes songs from the queue if the requester is not in the voice channel."""
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if (
|
||||
dj_enabled
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Clean Queue"),
|
||||
description=_("You need the DJ role to clean the queue."),
|
||||
)
|
||||
clean_tracks = []
|
||||
removed_tracks = 0
|
||||
listeners = player.channel.members
|
||||
async for track in AsyncIter(player.queue):
|
||||
if track.requester in listeners:
|
||||
clean_tracks.append(track)
|
||||
else:
|
||||
removed_tracks += 1
|
||||
player.queue = clean_tracks
|
||||
if removed_tracks == 0:
|
||||
await self.send_embed_msg(ctx, title=_("Removed 0 tracks."))
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed Tracks From The Queue"),
|
||||
description=_(
|
||||
"Removed {removed_tracks} tracks queued by members "
|
||||
"outside of the voice channel."
|
||||
).format(removed_tracks=removed_tracks),
|
||||
)
|
||||
|
||||
@command_queue.command(name="cleanself")
|
||||
async def command_queue_cleanself(self, ctx: commands.Context):
|
||||
"""Removes all tracks you requested from the queue."""
|
||||
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
clean_tracks = []
|
||||
removed_tracks = 0
|
||||
async for track in AsyncIter(player.queue):
|
||||
if track.requester != ctx.author:
|
||||
clean_tracks.append(track)
|
||||
else:
|
||||
removed_tracks += 1
|
||||
player.queue = clean_tracks
|
||||
if removed_tracks == 0:
|
||||
await self.send_embed_msg(ctx, title=_("Removed 0 tracks."))
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed Tracks From The Queue"),
|
||||
description=_(
|
||||
"Removed {removed_tracks} tracks queued by {member.display_name}."
|
||||
).format(removed_tracks=removed_tracks, member=ctx.author),
|
||||
)
|
||||
|
||||
@command_queue.command(name="search")
|
||||
async def command_queue_search(self, ctx: commands.Context, *, search_words: str):
|
||||
"""Search the queue."""
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
search_list = await self._build_queue_search_list(player.queue, search_words)
|
||||
if not search_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No matches."))
|
||||
|
||||
len_search_pages = math.ceil(len(search_list) / 10)
|
||||
search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_search_pages + 1)):
|
||||
embed = await self._build_queue_search_page(ctx, page_num, search_list)
|
||||
search_page_list.append(embed)
|
||||
await menu(ctx, search_page_list, DEFAULT_CONTROLS)
|
||||
|
||||
@command_queue.command(name="shuffle")
|
||||
@commands.cooldown(1, 30, commands.BucketType.guild)
|
||||
async def command_queue_shuffle(self, ctx: commands.Context):
|
||||
"""Shuffles the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if (
|
||||
dj_enabled
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("You need the DJ role to shuffle the queue."),
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("There's nothing in the queue."),
|
||||
)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
except KeyError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("There's nothing in the queue."),
|
||||
)
|
||||
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("There's nothing in the queue."),
|
||||
)
|
||||
|
||||
player.force_shuffle(0)
|
||||
return await self.send_embed_msg(ctx, title=_("Queue has been shuffled."))
|
||||
Reference in New Issue
Block a user