Merge V3/feature/audio into V3/develop (a.k.a. audio refactor) (#3459)

This commit is contained in:
Draper
2020-05-20 21:30:06 +01:00
committed by GitHub
parent ef76affd77
commit 8fa47cb789
53 changed files with 12372 additions and 10144 deletions

View 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"""

File diff suppressed because it is too large Load Diff

View 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
)

View 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)

View 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
),
)

View 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)

View 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
)

View 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)

File diff suppressed because it is too large Load Diff

View 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."))