Audio Cog - v2.3.0 (#4446)

* First commit - Bring everything from dev cog minus NSFW support

* Add a toggle for auto deafen

* Add a one off Send to Owners

* aaaaaaa

* Update this to ensure `get_perms` is not called if the API is disabled

* Apply suggestions from code review

Co-authored-by: Vuks <51289041+Vuks69@users.noreply.github.com>

* silence any errors here (in case API is down so it doesnt affect audio)

* update the message to tell the mto join the Official Red server.

* remove useless sutff, and change dj check order to ensure bot doesnt join VC for non DJ's

* ffs

* Update redbot/cogs/audio/core/tasks/startup.py

Co-authored-by: Twentysix <Twentysix26@users.noreply.github.com>

* Aikas Review

* Add #3995 in here

* update

* *sigh*

* lock behind owner

* to help with debugging

* Revert "to help with debugging"

This reverts commit 8cbf17be

* resolve last review

Co-authored-by: Vuks <51289041+Vuks69@users.noreply.github.com>
Co-authored-by: Twentysix <Twentysix26@users.noreply.github.com>
This commit is contained in:
Draper
2020-10-12 19:39:39 +01:00
committed by GitHub
parent 29ebf0f060
commit 2da9b502d8
41 changed files with 1553 additions and 331 deletions

View File

@@ -1,8 +1,11 @@
import asyncio
import json
from collections import Counter
from typing import Mapping
import aiohttp
import discord
from redbot.core import Config
from redbot.core.bot import Red
@@ -49,6 +52,7 @@ class Audio(
self._disconnected_players = {}
self._daily_playlist_cache = {}
self._daily_global_playlist_cache = {}
self._persist_queue_cache = {}
self._dj_status_cache = {}
self._dj_role_cache = {}
self.skip_votes = {}
@@ -58,30 +62,47 @@ class Audio(
self.player_automated_timer_task = None
self.cog_cleaned_up = False
self.lavalink_connection_aborted = False
self.permission_cache = discord.Permissions(
embed_links=True,
read_messages=True,
send_messages=True,
read_message_history=True,
add_reactions=True,
)
self.session = aiohttp.ClientSession()
self.session = aiohttp.ClientSession(json_serialize=json.dumps)
self.cog_ready_event = asyncio.Event()
self.cog_init_task = None
self.global_api_user = {
"fetched": False,
"can_read": False,
"can_post": False,
"can_delete": False,
}
default_global = dict(
schema_version=1,
owner_notification=0,
cache_level=0,
cache_age=365,
daily_playlists=False,
global_db_enabled=False,
global_db_get_timeout=5, # Here as a placeholder in case we want to enable the command
global_db_get_timeout=5,
status=False,
use_external_lavalink=False,
restrict=True,
localpath=str(cog_data_path(raw_name="Audio")),
url_keyword_blacklist=[],
url_keyword_whitelist=[],
java_exc_path="java",
**self._default_lavalink_settings,
)
default_guild = dict(
auto_play=False,
auto_deafen=True,
autoplaylist={"enabled": False, "id": None, "name": None, "scope": None},
persist_queue=True,
disconnect=False,
dj_enabled=False,
dj_role=None,
@@ -119,3 +140,4 @@ class Audio(
self.config.register_custom(PlaylistScope.USER.value, **_playlist)
self.config.register_guild(**default_guild)
self.config.register_global(**default_global)
self.config.register_user(country_code=None)

View File

@@ -1,10 +1,11 @@
from __future__ import annotations
import asyncio
from abc import ABC, abstractmethod
from collections import Counter
from pathlib import Path
from typing import Any, List, Mapping, MutableMapping, Optional, Tuple, Union, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple, Union
import aiohttp
import discord
@@ -25,8 +26,7 @@ if TYPE_CHECKING:
class MixinMeta(ABC):
"""
Base class for well behaved type hint detection with composite class.
"""Base class for well behaved type hint detection with composite class.
Basically, to keep developers sane when not all attributes are defined in each mixin.
"""
@@ -44,10 +44,12 @@ class MixinMeta(ABC):
play_lock: MutableMapping[int, bool]
_daily_playlist_cache: MutableMapping[int, bool]
_daily_global_playlist_cache: MutableMapping[int, bool]
_persist_queue_cache: MutableMapping[int, bool]
_dj_status_cache: MutableMapping[int, Optional[bool]]
_dj_role_cache: MutableMapping[int, Optional[int]]
_error_timer: MutableMapping[int, float]
_disconnected_players: MutableMapping[int, bool]
global_api_user: MutableMapping[str, Any]
cog_cleaned_up: bool
lavalink_connection_aborted: bool
@@ -60,6 +62,7 @@ class MixinMeta(ABC):
cog_ready_event: asyncio.Event
_default_lavalink_settings: Mapping
permission_cache = discord.Permissions
@abstractmethod
async def command_llsetup(self, ctx: commands.Context):
@@ -74,7 +77,7 @@ class MixinMeta(ABC):
raise NotImplementedError()
@abstractmethod
def get_active_player_count(self) -> Tuple[str, int]:
async def get_active_player_count(self) -> Tuple[str, int]:
raise NotImplementedError()
@abstractmethod
@@ -160,16 +163,20 @@ class MixinMeta(ABC):
@abstractmethod
async def is_query_allowed(
self, config: Config, guild: discord.Guild, query: str, query_obj: "Query" = None
self,
config: Config,
ctx_or_channel: Optional[Union[Context, discord.TextChannel]],
query: str,
query_obj: Query,
) -> bool:
raise NotImplementedError()
@abstractmethod
def is_track_length_allowed(self, track: lavalink.Track, maxlength: int) -> bool:
def is_track_length_allowed(self, track: Union[lavalink.Track, int], maxlength: int) -> bool:
raise NotImplementedError()
@abstractmethod
def get_track_description(
async def get_track_description(
self,
track: Union[lavalink.rest_api.Track, "Query"],
local_folder_current_path: Path,
@@ -178,7 +185,7 @@ class MixinMeta(ABC):
raise NotImplementedError()
@abstractmethod
def get_track_description_unformatted(
async def get_track_description_unformatted(
self, track: Union[lavalink.rest_api.Track, "Query"], local_folder_current_path: Path
) -> Optional[str]:
raise NotImplementedError()
@@ -495,6 +502,10 @@ class MixinMeta(ABC):
async def get_lyrics_status(self, ctx: Context) -> bool:
raise NotImplementedError()
@abstractmethod
async def restore_players(self) -> bool:
raise NotImplementedError()
@abstractmethod
async def command_skip(self, ctx: commands.Context, skip_to_track: int = None):
raise NotImplementedError()
@@ -502,3 +513,10 @@ class MixinMeta(ABC):
@abstractmethod
async def command_prev(self, ctx: commands.Context):
raise NotImplementedError()
@abstractmethod
async def icyparser(self, url: str) -> Optional[str]:
raise NotImplementedError()
async def self_deafen(self, player: lavalink.Player) -> None:
raise NotImplementedError()

View File

@@ -8,15 +8,49 @@ from redbot.core.i18n import Translator
from ..converters import get_lazy_converter, get_playlist_converter
__version__ = VersionInfo.from_json({"major": 2, "minor": 0, "micro": 0, "releaselevel": "final"})
__version__ = VersionInfo.from_json({"major": 2, "minor": 3, "micro": 0, "releaselevel": "final"})
__author__ = ["aikaterna", "Draper"]
_ = Translator("Audio", Path(__file__).parent)
_SCHEMA_VERSION: Final[int] = 3
_OWNER_NOTIFICATION: Final[int] = 1
LazyGreedyConverter = get_lazy_converter("--")
PlaylistConverter = get_playlist_converter()
HUMANIZED_PERM = {
"create_instant_invite": "Create Instant Invite",
"kick_members": "Kick Members",
"ban_members": "Ban Members",
"administrator": "Administrator",
"manage_channels": "Manage Channels",
"manage_guild": "Manage Server",
"add_reactions": "Add Reactions",
"view_audit_log": "View Audit Log",
"priority_speaker": "Priority Speaker",
"stream": "Go Live",
"read_messages": "Read Text Channels & See Voice Channels",
"send_messages": "Send Messages",
"send_tts_messages": "Send TTS Messages",
"manage_messages": "Manage Messages",
"embed_links": "Embed Links",
"attach_files": "Attach Files",
"read_message_history": "Read Message History",
"mention_everyone": "Mention @everyone, @here, and All Roles",
"external_emojis": "Use External Emojis",
"view_guild_insights": "View Server Insights",
"connect": "Connect",
"speak": "Speak",
"mute_members": "Mute Members",
"deafen_members": "Deafen Members",
"move_members": "Move Members",
"use_voice_activation": "Use Voice Activity",
"change_nickname": "Change Nickname",
"manage_nicknames": "Manage Nicknames",
"manage_roles": "Manage Roles",
"manage_webhooks": "Manage Webhooks",
"manage_emojis": "Manage Emojis",
}
class CompositeMetaClass(type(commands.Cog), type(ABC)):

View File

@@ -1,6 +1,7 @@
import asyncio
import contextlib
import logging
from typing import Union
import discord
@@ -888,6 +889,21 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
),
)
@command_audioset.command(name="autodeafen")
@commands.guild_only()
@commands.mod_or_permissions(manage_guild=True)
async def command_audioset_auto_deafen(self, ctx: commands.Context):
"""Toggle whether the bot will be auto deafened upon joining the voice channel."""
auto_deafen = await self.config.guild(ctx.guild).auto_deafen()
await self.config.guild(ctx.guild).auto_deafen.set(not auto_deafen)
await self.send_embed_msg(
ctx,
title=_("Setting Changed"),
description=_("Auto Deafen: {true_or_false}.").format(
true_or_false=_("Enabled") if not auto_deafen else _("Disabled")
),
)
@command_audioset.command(name="restrict")
@commands.is_owner()
@commands.guild_only()
@@ -951,6 +967,9 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
bumpped_shuffle = _("Enabled") if data["shuffle_bumped"] else _("Disabled")
song_notify = _("Enabled") if data["notify"] else _("Disabled")
song_status = _("Enabled") if global_data["status"] else _("Disabled")
persist_queue = _("Enabled") if data["persist_queue"] else _("Disabled")
auto_deafen = _("Enabled") if data["auto_deafen"] else _("Disabled")
countrycode = data["country_code"]
spotify_cache = CacheLevel.set_spotify()
@@ -992,7 +1011,9 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
"Shuffle bumped: [{bumpped_shuffle}]\n"
"Song notify msgs: [{notify}]\n"
"Songs as status: [{status}]\n"
"Persist queue: [{persist_queue}]\n"
"Spotify search: [{countrycode}]\n"
"Auto-Deafen: [{auto_deafen}]\n"
).format(
countrycode=countrycode,
repeat=song_repeat,
@@ -1000,6 +1021,8 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
notify=song_notify,
status=song_status,
bumpped_shuffle=bumpped_shuffle,
persist_queue=persist_queue,
auto_deafen=auto_deafen,
)
if thumbnail:
msg += _("Thumbnails: [{0}]\n").format(
@@ -1050,16 +1073,22 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
+ _("Local Spotify cache: [{spotify_status}]\n")
+ _("Local Youtube cache: [{youtube_status}]\n")
+ _("Local Lavalink cache: [{lavalink_status}]\n")
# + _("Global cache status: [{global_cache}]\n")
# + _("Global timeout: [{num_seconds}]\n")
+ _("Global cache status: [{global_cache}]\n")
+ _("Global timeout: [{num_seconds}]\n")
).format(
max_age=str(await self.config.cache_age()) + " " + _("days"),
spotify_status=_("Enabled") if has_spotify_cache else _("Disabled"),
youtube_status=_("Enabled") if has_youtube_cache else _("Disabled"),
lavalink_status=_("Enabled") if has_lavalink_cache else _("Disabled"),
# global_cache=_("Enabled") if global_data["global_db_enabled"] else _("Disabled"),
# num_seconds=self.get_time_string(global_data["global_db_get_timeout"]),
global_cache=_("Enabled") if global_data["global_db_enabled"] else _("Disabled"),
num_seconds=self.get_time_string(global_data["global_db_get_timeout"]),
)
msg += (
"\n---"
+ _("User Settings")
+ "--- \n"
+ _("Spotify search: [{country_code}]\n")
).format(country_code=await self.config.user(ctx.author).country_code())
msg += (
"\n---"
@@ -1075,19 +1104,21 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
if global_data["use_external_lavalink"]
else _("Disabled"),
)
if not global_data["use_external_lavalink"] and self.player_manager.ll_build:
if is_owner and not global_data["use_external_lavalink"] and self.player_manager.ll_build:
msg += _(
"Lavalink build: [{llbuild}]\n"
"Lavalink branch: [{llbranch}]\n"
"Release date: [{build_time}]\n"
"Lavaplayer version: [{lavaplayer}]\n"
"Java version: [{jvm}]\n"
"Java Executable: [{jv_exec}]\n"
).format(
build_time=self.player_manager.build_time,
llbuild=self.player_manager.ll_build,
llbranch=self.player_manager.ll_branch,
lavaplayer=self.player_manager.lavaplayer,
jvm=self.player_manager.jvm,
jv_exec=self.player_manager.path,
)
if is_owner:
msg += _("Localtracks path: [{localpath}]\n").format(**global_data)
@@ -1212,6 +1243,28 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
await self.config.guild(ctx.guild).country_code.set(country)
@command_audioset.command(name="mycountrycode")
@commands.guild_only()
async def command_audioset_countrycode_user(self, ctx: commands.Context, country: str):
"""Set the country code for Spotify searches."""
if len(country) != 2:
return await self.send_embed_msg(
ctx,
title=_("Invalid Country Code"),
description=_(
"Please use an official [ISO 3166-1 alpha-2]"
"(https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) code."
),
)
country = country.upper()
await self.send_embed_msg(
ctx,
title=_("Setting Changed"),
description=_("Country Code set to {country}.").format(country=country),
)
await self.config.user(ctx.author).country_code.set(country)
@command_audioset.command(name="cache")
@commands.is_owner()
async def command_audioset_cache(self, ctx: commands.Context, *, level: int = None):
@@ -1315,3 +1368,73 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
msg += _("I've set the cache age to {age} days").format(age=age)
await self.config.cache_age.set(age)
await self.send_embed_msg(ctx, title=_("Setting Changed"), description=msg)
@commands.is_owner()
@command_audioset.group(name="globalapi")
async def command_audioset_audiodb(self, ctx: commands.Context):
"""Change globalapi settings."""
@command_audioset_audiodb.command(name="toggle")
async def command_audioset_audiodb_toggle(self, ctx: commands.Context):
"""Toggle the server settings.
Default is ON
"""
state = await self.config.global_db_enabled()
await self.config.global_db_enabled.set(not state)
if not state: # Ensure a call is made if the API is enabled to update user perms
self.global_api_user = await self.api_interface.global_cache_api.get_perms()
await ctx.send(
_("Global DB is {status}").format(status=_("enabled") if not state else _("disabled"))
)
@command_audioset_audiodb.command(name="timeout")
async def command_audioset_audiodb_timeout(
self, ctx: commands.Context, timeout: Union[float, int]
):
"""Set GET request timeout.
Example: 0.1 = 100ms 1 = 1 second
"""
await self.config.global_db_get_timeout.set(timeout)
await ctx.send(_("Request timeout set to {time} second(s)").format(time=timeout))
@command_audioset.command(name="persistqueue")
@commands.admin()
async def command_audioset_persist_queue(self, ctx: commands.Context):
"""Toggle persistent queues.
Persistent queues allows the current queue to be restored when the queue closes.
"""
persist_cache = self._persist_queue_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).persist_queue()
)
await self.config.guild(ctx.guild).persist_queue.set(not persist_cache)
self._persist_queue_cache[ctx.guild.id] = not persist_cache
await self.send_embed_msg(
ctx,
title=_("Setting Changed"),
description=_("Persisting queues: {true_or_false}.").format(
true_or_false=_("Enabled") if not persist_cache else _("Disabled")
),
)
@command_audioset.command(name="restart")
@commands.is_owner()
async def command_audioset_restart(self, ctx: commands.Context):
"""Restarts the lavalink connection."""
async with ctx.typing():
lavalink.unregister_event_listener(self.lavalink_event_handler)
await lavalink.close()
if self.player_manager is not None:
await self.player_manager.shutdown()
self.lavalink_restart_connect()
lavalink.register_event_listener(self.lavalink_event_handler)
await self.restore_players()
await self.send_embed_msg(
ctx,
title=_("Restarting Lavalink"),
description=_("It can take a couple of minutes for Lavalink to fully start up."),
)

View File

@@ -2,13 +2,15 @@ import asyncio
import contextlib
import datetime
import logging
from typing import Optional, Tuple, Union
import time
from typing import Optional, Union
import discord
import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import commands
from redbot.core.utils import AsyncIter
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
@@ -67,6 +69,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
await player.stop()
await player.disconnect()
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
@commands.command(name="now")
@commands.guild_only()
@@ -91,7 +94,10 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
dur = "LIVE"
else:
dur = self.format_time(player.current.length)
song = self.get_track_description(player.current, self.local_folder_current_path) or ""
song = (
await 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)
@@ -206,7 +212,9 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
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)
description = await self.get_track_description(
player.current, self.local_folder_current_path
)
if player.current and not player.paused:
await player.pause()
@@ -262,6 +270,13 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
)
else:
track = player.fetch("prev_song")
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
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)
@@ -269,7 +284,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
player.queue.insert(0, bump_song)
player.queue.pop(queue_len)
await player.skip()
description = self.get_track_description(
description = await self.get_track_description(
player.current, self.local_folder_current_path
)
embed = discord.Embed(title=_("Replaying Track"), description=description)
@@ -406,8 +421,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
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`.
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()
@@ -581,6 +596,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
player.store("requester", None)
await player.stop()
await self.send_embed_msg(ctx, title=_("Stopping..."))
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
@commands.command(name="summon")
@commands.guild_only()
@@ -626,12 +642,14 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
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)
await self.self_deafen(player)
except AttributeError:
ctx.command.reset_cooldown(ctx)
return await self.send_embed_msg(
@@ -774,7 +792,12 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
)
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.api_interface.persistent_queue_api.played(
ctx.guild.id, removed.extras.get("enqueue_time")
)
removed_title = await self.get_track_description(
removed, self.local_folder_current_path
)
await self.send_embed_msg(
ctx,
title=_("Removed track from queue"),
@@ -787,6 +810,9 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
if track.uri != index_or_url:
clean_tracks.append(track)
else:
await self.api_interface.persistent_queue_api.played(
ctx.guild.id, track.extras.get("enqueue_time")
)
removed_tracks += 1
player.queue = clean_tracks
if removed_tracks == 0:
@@ -841,7 +867,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
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)
description = await 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

@@ -1,4 +1,5 @@
import logging
from pathlib import Path
import discord
@@ -18,6 +19,73 @@ class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
async def command_llsetup(self, ctx: commands.Context):
"""Lavalink server configuration options."""
@command_llsetup.command(name="java")
async def command_llsetup_java(self, ctx: commands.Context, *, java_path: str = None):
"""Change your Java executable path
Enter nothing to reset to default.
"""
external = await self.config.use_external_lavalink()
if external:
return await self.send_embed_msg(
ctx,
title=_("Invalid Environment"),
description=_(
"You cannot changed the Java executable path of "
"external Lavalink instances from the Audio Cog."
),
)
if java_path is None:
await self.config.java_exc_path.clear()
await self.send_embed_msg(
ctx,
title=_("Java Executable Reset"),
description=_("Audio will now use `java` to run your Lavalink.jar"),
)
else:
exc = Path(java_path)
exc_absolute = exc.absolute()
if not exc.exists() or not exc.is_file():
return await self.send_embed_msg(
ctx,
title=_("Invalid Environment"),
description=_("`{java_path}` is not a valid executable").format(
java_path=exc_absolute
),
)
await self.config.java_exc_path.set(exc_absolute)
await self.send_embed_msg(
ctx,
title=_("Java Executable Changed"),
description=_("Audio will now use `{exc}` to run your Lavalink.jar").format(
exc=exc_absolute
),
)
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=_(
"For it to take effect please reload " "Audio (`{prefix}reload audio`)."
).format(
prefix=ctx.prefix,
),
)
else:
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="external")
async def command_llsetup_external(self, ctx: commands.Context):
"""Toggle using external Lavalink servers."""

View File

@@ -6,9 +6,9 @@ import random
import discord
import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
@@ -52,7 +52,7 @@ class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass):
try:
if not p.current:
raise AttributeError
current_title = self.get_track_description(
current_title = await self.get_track_description(
p.current, self.local_folder_current_path
)
msg += "{} [`{}`]: {}\n".format(p.channel.guild.name, connect_dur, current_title)

View File

@@ -2,20 +2,27 @@ import contextlib
import datetime
import logging
import math
from typing import MutableMapping, Optional
import time
from typing import MutableMapping
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core.utils import AsyncIter
from discord.embeds import EmptyEmbed
from redbot.core import commands
from redbot.core.commands import UserInputOptional
from redbot.core.utils import AsyncIter
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 ...errors import (
DatabaseError,
QueryUnauthorized,
SpotifyFetchError,
TrackEnqueueError,
)
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _
@@ -39,10 +46,17 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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):
elif not await self.is_query_allowed(self.config, ctx, f"{query}", query_obj=query):
return await self.send_embed_msg(
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.")
)
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."),
)
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed")
@@ -64,6 +78,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
return await self.send_embed_msg(
ctx,
@@ -76,15 +91,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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)
@@ -143,10 +150,17 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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):
elif not await self.is_query_allowed(self.config, ctx, f"{query}", query_obj=query):
return await self.send_embed_msg(
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.")
)
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."),
)
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed")
@@ -168,6 +182,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
return await self.send_embed_msg(
ctx,
@@ -180,15 +195,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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)
@@ -258,13 +265,12 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
)
if seek and seek > 0:
single_track.start_timestamp = seek * 1000
query = Query.process_input(single_track, self.local_folder_current_path)
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))}"
),
ctx,
f"{single_track.title} {single_track.author} {single_track.uri} " f"{str(query)}",
query_obj=query,
):
if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
@@ -277,6 +283,13 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(single_track, guild_data["maxlength"]):
single_track.requester = ctx.author
single_track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.queue.insert(0, single_track)
player.maybe_shuffle()
self.bot.dispatch(
@@ -293,12 +306,21 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
else:
single_track.requester = ctx.author
single_track.extras["bumped"] = True
single_track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
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)
description = await 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(
@@ -395,6 +417,12 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
).format(prefix=ctx.prefix),
)
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."),
)
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed")
@@ -416,6 +444,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
return await self.send_embed_msg(
ctx,
@@ -428,12 +457,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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)
@@ -509,6 +532,13 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.mod_or_permissions(manage_guild=True)
async def command_autoplay(self, ctx: commands.Context):
"""Starts auto play."""
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."),
)
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed")
@@ -530,6 +560,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
return await self.send_embed_msg(
ctx,
@@ -542,13 +573,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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)
@@ -583,7 +607,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, try again in a few "
"I'm unable to get a track from Lavalink at the moment, try again in a few "
"minutes."
),
)
@@ -603,10 +627,15 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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.
Use `[p]search list <search term>` to queue all tracks found on YouTube. Use `[p]search sc
<search term>` to search on SoundCloud instead of YouTube.
"""
if not isinstance(query, (str, Query)):
raise RuntimeError(
f"Expected 'query' to be a string or Query object but received: {type(query)} - this is an unexpected argument type, please report it."
)
async def _search_menu(
ctx: commands.Context,
pages: list,
@@ -654,6 +683,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
return await self.send_embed_msg(
ctx,
@@ -693,9 +723,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
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
):
if not await self.is_query_allowed(self.config, ctx, f"{query}", query_obj=query):
return await self.send_embed_msg(
ctx,
title=_("Unable To Play Tracks"),
@@ -713,7 +741,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, "
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
)
@@ -729,7 +757,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, "
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
)
@@ -762,13 +790,12 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
async for track in AsyncIter(tracks):
if len(player.queue) >= 10000:
continue
query = Query.process_input(track, self.local_folder_current_path)
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))}"
),
ctx,
f"{track.title} {track.author} {track.uri} " f"{str(query)}",
query_obj=query,
):
if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
@@ -776,12 +803,26 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(track, guild_data["maxlength"]):
track_len += 1
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, track)
self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
)
else:
track_len += 1
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, track)
self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
@@ -832,7 +873,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment,"
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
)

View File

@@ -4,22 +4,24 @@ import logging
import math
import os
import tarfile
import time
from io import BytesIO
from typing import Optional, cast
from typing import cast
import discord
import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import commands
from redbot.core.commands import UserInputOptional
from redbot.core.data_manager import cog_data_path
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import bold, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from redbot.core.utils.predicates import MessagePredicate
from ...apis.api_utils import FakePlaylist
from ...apis.playlist_interface import create_playlist, delete_playlist, get_all_playlist, Playlist
from ...apis.playlist_interface import Playlist, create_playlist, delete_playlist, get_all_playlist
from ...audio_dataclasses import LocalPath, Query
from ...audio_logging import IS_DEBUG, debug_exc_log
from ...converters import ComplexScopeParser, ScopeParser
@@ -48,7 +50,6 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
**User**:
Visible to all bot users, if --author is passed.
Editable by bot owner and creator.
"""
@command_playlist.command(
@@ -1368,7 +1369,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
else:
return await self.send_embed_msg(
ctx,
title=_("Playlist Couldn't Be Created"),
title=_("Playlist Couldn't be created"),
description=_("Unable to create your playlist."),
)
@@ -1472,13 +1473,12 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
async for track in AsyncIter(tracks):
if len(player.queue) >= 10000:
continue
query = Query.process_input(track, self.local_folder_current_path)
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))}"
),
ctx,
f"{track.title} {track.author} {track.uri} " f"{str(query)}",
query_obj=query,
):
if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
@@ -1492,7 +1492,13 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
continue
if maxlength > 0 and not self.is_track_length_allowed(track, maxlength):
continue
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(author_obj, track)
self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
@@ -1800,7 +1806,9 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
)
try:
async with self.session.request("GET", file_url) as r:
uploaded_playlist = await r.json(content_type="text/plain", encoding="utf-8")
uploaded_playlist = await r.json(
content_type="text/plain", encoding="utf-8", loads=json.loads
)
except UnicodeDecodeError:
return await self.send_embed_msg(ctx, title=_("Not a valid playlist file."))
@@ -1862,7 +1870,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, try again in a few "
"I'm unable to get a track from Lavalink at the moment, try again in a few "
"minutes."
),
)

View File

@@ -3,13 +3,14 @@ import contextlib
import datetime
import logging
import math
from typing import MutableMapping, Optional, Union, Tuple
from typing import MutableMapping, Optional
import discord
import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import (
DEFAULT_CONTROLS,
close_menu,
@@ -66,7 +67,10 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
dur = "LIVE"
else:
dur = self.format_time(player.current.length)
song = self.get_track_description(player.current, self.local_folder_current_path) or ""
song = (
await 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)
@@ -186,6 +190,10 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Clear Queue"),
description=_("You need the DJ role to clear the queue."),
)
async for track in AsyncIter(player.queue):
await self.api_interface.persistent_queue_api.played(
ctx.guild.id, track.extras.get("enqueue_time")
)
player.queue.clear()
await self.send_embed_msg(
ctx, title=_("Queue Modified"), description=_("The queue has been cleared.")
@@ -220,6 +228,9 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
if track.requester in listeners:
clean_tracks.append(track)
else:
await self.api_interface.persistent_queue_api.played(
ctx.guild.id, track.extras.get("enqueue_time")
)
removed_tracks += 1
player.queue = clean_tracks
if removed_tracks == 0:
@@ -252,6 +263,9 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
clean_tracks.append(track)
else:
removed_tracks += 1
await self.api_interface.persistent_queue_api.played(
ctx.guild.id, track.extras.get("enqueue_time")
)
player.queue = clean_tracks
if removed_tracks == 0:
await self.send_embed_msg(ctx, title=_("Removed 0 tracks."))
@@ -325,6 +339,7 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
ctx.command.reset_cooldown(ctx)
return await self.send_embed_msg(

View File

@@ -2,6 +2,7 @@ import asyncio
import datetime
import logging
import time
from typing import Optional
import discord
@@ -130,6 +131,13 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
)
except Exception as err:
debug_exc_log(log, err, f"Failed to delete global daily playlist ID: {too_old_id}")
persist_cache = self._persist_queue_cache.setdefault(
guild.id, await self.config.guild(guild).persist_queue()
)
if persist_cache:
await self.api_interface.persistent_queue_api.played(
guild_id=guild.id, track_id=track_identifier
)
@commands.Cog.listener()
async def on_red_audio_queue_end(
@@ -141,6 +149,21 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
await self.api_interface.local_cache_api.youtube.clean_up_old_entries()
await asyncio.sleep(5)
await self.playlist_api.delete_scheduled()
await self.api_interface.persistent_queue_api.drop(guild.id)
await asyncio.sleep(5)
await self.api_interface.persistent_queue_api.delete_scheduled()
@commands.Cog.listener()
async def on_red_audio_track_enqueue(self, guild: discord.Guild, track, requester):
if not (track and guild):
return
persist_cache = self._persist_queue_cache.setdefault(
guild.id, await self.config.guild(guild).persist_queue()
)
if persist_cache:
await self.api_interface.persistent_queue_api.enqueued(
guild_id=guild.id, room_id=track.extras["vc"], track=track
)
@commands.Cog.listener()
async def on_red_audio_track_end(
@@ -152,3 +175,6 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
await self.api_interface.local_cache_api.youtube.clean_up_old_entries()
await asyncio.sleep(5)
await self.playlist_api.delete_scheduled()
await self.api_interface.persistent_queue_api.drop(guild.id)
await asyncio.sleep(5)
await self.api_interface.persistent_queue_api.delete_scheduled()

View File

@@ -1,19 +1,24 @@
import asyncio
import contextlib
import logging
import re
from collections import OrderedDict
from pathlib import Path
from typing import Final, Pattern
import discord
import lavalink
from aiohttp import ClientConnectorError
from discord.ext.commands import CheckFailure
from redbot.core import commands
from redbot.core.utils.chat_formatting import box, humanize_list
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _
from ...audio_logging import debug_exc_log
from ...errors import TrackEnqueueError
from ..abc import MixinMeta
from ..cog_utils import HUMANIZED_PERM, CompositeMetaClass, _
log = logging.getLogger("red.cogs.Audio.cog.Events.dpy")
@@ -39,11 +44,55 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
raise RuntimeError(
"Not running audio command due to invalid machine architecture for Lavalink."
)
# with contextlib.suppress(Exception):
# player = lavalink.get_player(ctx.guild.id)
# notify_channel = player.fetch("channel")
# if not notify_channel:
# player.store("channel", ctx.channel.id)
current_perms = ctx.channel.permissions_for(ctx.me)
surpass_ignore = (
isinstance(ctx.channel, discord.abc.PrivateChannel)
or await ctx.bot.is_owner(ctx.author)
or await ctx.bot.is_admin(ctx.author)
)
guild = ctx.guild
if guild and not current_perms.is_superset(self.permission_cache):
current_perms_set = set(iter(current_perms))
expected_perms_set = set(iter(self.permission_cache))
diff = expected_perms_set - current_perms_set
missing_perms = dict((i for i in diff if i[-1] is not False))
missing_perms = OrderedDict(sorted(missing_perms.items()))
missing_permissions = missing_perms.keys()
log.debug(
"Missing the following perms in %d, Owner ID: %d: %s",
ctx.guild.id,
ctx.guild.owner.id,
humanize_list(list(missing_permissions)),
)
if not surpass_ignore:
text = _(
"I'm missing permissions in this server, "
"Please address this as soon as possible.\n\n"
"Expected Permissions:\n"
)
for perm, value in missing_perms.items():
text += "{perm}: [{status}]\n".format(
status=_("Enabled") if value else _("Disabled"),
perm=HUMANIZED_PERM.get(perm),
)
text = text.strip()
if current_perms.send_messages and current_perms.read_messages:
await ctx.send(box(text=text, lang="ini"))
else:
log.info(
"Missing write permission in %d, Owner ID: %d",
ctx.guild.id,
ctx.guild.owner.id,
)
raise CheckFailure(message=text)
with contextlib.suppress(Exception):
player = lavalink.get_player(ctx.guild.id)
notify_channel = player.fetch("channel")
if not notify_channel:
player.store("channel", ctx.channel.id)
self._daily_global_playlist_cache.setdefault(
self.bot.user.id, await self.config.daily_playlists()
)
@@ -51,12 +100,16 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
self.local_folder_current_path = Path(await self.config.localpath())
if not ctx.guild:
return
dj_enabled = self._dj_status_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
)
self._daily_playlist_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).daily_playlists()
)
self._persist_queue_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).persist_queue()
)
if dj_enabled:
dj_role = self._dj_role_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_role()
@@ -78,12 +131,16 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
if isinstance(error, commands.ArgParserFailure):
handled = True
msg = _("`{user_input}` is not a valid value for `{command}`").format(
user_input=error.user_input, command=error.cmd
user_input=error.user_input,
command=error.cmd,
)
if error.custom_help_msg:
msg += f"\n{error.custom_help_msg}"
await self.send_embed_msg(
ctx, title=_("Unable To Parse Argument"), description=msg, error=True
ctx,
title=_("Unable To Parse Argument"),
description=msg,
error=True,
)
if error.send_cmd_help:
await ctx.send_help()
@@ -101,7 +158,10 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
)
else:
await self.send_embed_msg(
ctx, title=_("Invalid Argument"), description=error.args[0], error=True
ctx,
title=_("Invalid Argument"),
description=error.args[0],
error=True,
)
else:
await ctx.send_help()
@@ -137,6 +197,17 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
error=True,
)
debug_exc_log(log, error, "This is a handled error")
elif isinstance(error, discord.errors.HTTPException):
handled = True
await self.send_embed_msg(
ctx,
title=_("There was an issue communicating with Discord."),
description=_("This error has been reported to the bot owner."),
error=True,
)
log.exception(
"This is not handled in the core Audio cog, please report it.", exc_info=error
)
if not isinstance(
error,
(
@@ -186,3 +257,13 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
self.skip_votes[before.channel.guild].remove(member.id)
except (ValueError, KeyError, AttributeError):
pass
channel = self.rgetattr(member, "voice.channel", None)
bot_voice_state = self.rgetattr(member, "guild.me.voice.self_deaf", None)
if channel and bot_voice_state is False:
try:
player = lavalink.get_player(channel.guild.id)
except (KeyError, AttributeError):
pass
else:
if player.channel.id == channel.id:
await self.self_deafen(player)

View File

@@ -18,6 +18,8 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
) -> None:
current_track = player.current
current_channel = player.channel
if not current_channel:
return
guild = self.rgetattr(current_channel, "guild", None)
if await self.bot.cog_disabled_in_guild(self, guild):
await player.stop()
@@ -31,12 +33,15 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
current_length = self.rgetattr(current_track, "length", None)
current_thumbnail = self.rgetattr(current_track, "thumbnail", None)
current_extras = self.rgetattr(current_track, "extras", {})
current_id = self.rgetattr(current_track, "_info", {}).get("identifier")
guild_data = await self.config.guild(guild).all()
repeat = guild_data["repeat"]
notify = guild_data["notify"]
disconnect = guild_data["disconnect"]
autoplay = guild_data["auto_play"]
description = self.get_track_description(current_track, self.local_folder_current_path)
description = await self.get_track_description(
current_track, self.local_folder_current_path
)
status = await self.config.status()
log.debug(f"Received a new lavalink event for {guild_id}: {event_type}: {extra}")
prev_song: lavalink.Track = player.fetch("prev_song")
@@ -51,12 +56,18 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
player.store("playing_song", current_track)
player.store("requester", current_requester)
self.bot.dispatch("red_audio_track_start", guild, current_track, current_requester)
if guild_id and current_track:
await self.api_interface.persistent_queue_api.played(
guild_id=guild_id, track_id=current_track.track_identifier
)
if event_type == lavalink.LavalinkEvents.TRACK_END:
prev_requester = player.fetch("prev_requester")
self.bot.dispatch("red_audio_track_end", guild, prev_song, prev_requester)
if event_type == lavalink.LavalinkEvents.QUEUE_END:
prev_requester = player.fetch("prev_requester")
self.bot.dispatch("red_audio_queue_end", guild, prev_song, prev_requester)
if guild_id:
await self.api_interface.persistent_queue_api.drop(guild_id)
if (
autoplay
and not player.queue
@@ -82,7 +93,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
notify_channel,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, try again in a few "
"I'm unable to get a track from Lavalink at the moment, try again in a few "
"minutes."
),
)
@@ -127,13 +138,13 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
)
player.store("notify_message", notify_message)
if event_type == lavalink.LavalinkEvents.TRACK_START and status:
player_check = self.get_active_player_count()
player_check = await self.get_active_player_count()
await self.update_bot_presence(*player_check)
if event_type == lavalink.LavalinkEvents.TRACK_END and status:
await asyncio.sleep(1)
if not player.is_playing:
player_check = self.get_active_player_count()
player_check = await self.get_active_player_count()
await self.update_bot_presence(*player_check)
if event_type == lavalink.LavalinkEvents.QUEUE_END:
@@ -146,7 +157,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
self.bot.dispatch("red_audio_audio_disconnect", guild)
await player.disconnect()
if status:
player_check = self.get_active_player_count()
player_check = await self.get_active_player_count()
await self.update_bot_presence(*player_check)
if event_type in [
@@ -209,5 +220,9 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
colour=await self.bot.get_embed_color(message_channel),
description="{}\n{}".format(extra.replace("\n", ""), description),
)
if current_id:
asyncio.create_task(
self.api_interface.global_cache_api.report_invalid(current_id)
)
await message_channel.send(embed=embed)
await player.skip()

View File

@@ -23,7 +23,9 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
max_retries = 5
retry_count = 0
while retry_count < max_retries:
external = await self.config.use_external_lavalink()
configs = await self.config.all()
external = configs["use_external_lavalink"]
java_exec = configs["java_exc_path"]
if external is False:
settings = self._default_lavalink_settings
host = settings["host"]
@@ -34,7 +36,7 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
await self.player_manager.shutdown()
self.player_manager = ServerManager()
try:
await self.player_manager.start()
await self.player_manager.start(java_exec)
except LavalinkDownloadFailed as exc:
await asyncio.sleep(1)
if exc.should_retry:
@@ -66,11 +68,10 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
else:
break
else:
config_data = await self.config.all()
host = config_data["host"]
password = config_data["password"]
rest_port = config_data["rest_port"]
ws_port = config_data["ws_port"]
host = configs["host"]
password = configs["password"]
rest_port = configs["rest_port"]
ws_port = configs["ws_port"]
break
else:
log.critical(

View File

@@ -1,9 +1,11 @@
import asyncio
import logging
import time
from typing import Dict
import lavalink
from redbot.core.utils import AsyncIter
from ...audio_logging import debug_exc_log
@@ -48,6 +50,7 @@ class PlayerTasks(MixinMeta, metaclass=CompositeMetaClass):
stop_times.pop(sid)
try:
player = lavalink.get_player(sid)
await self.api_interface.persistent_queue_api.drop(sid)
await player.stop()
await player.disconnect()
except Exception as err:

View File

@@ -1,14 +1,22 @@
import asyncio
import datetime
import itertools
import logging
from typing import Optional
import lavalink
from redbot.core.data_manager import cog_data_path
from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replaced
from redbot.core.utils.dbtools import APSWConnectionWrapper
from ...apis.interface import AudioAPIInterface
from ...apis.playlist_wrapper import PlaylistWrapper
from ...audio_logging import debug_exc_log
from ...utils import task_callback
from ..abc import MixinMeta
from ..cog_utils import _SCHEMA_VERSION, CompositeMetaClass
from ..cog_utils import _, _OWNER_NOTIFICATION, _SCHEMA_VERSION, CompositeMetaClass
log = logging.getLogger("red.cogs.Audio.cog.Tasks.startup")
@@ -19,11 +27,13 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
# If it waits for ready in startup, we cause a deadlock during initial load
# as initial load happens before the bot can ever be ready.
self.cog_init_task = self.bot.loop.create_task(self.initialize())
self.cog_init_task.add_done_callback(task_callback)
async def initialize(self) -> None:
await self.bot.wait_until_red_ready()
# Unlike most cases, we want the cache to exit before migration.
try:
await self.maybe_message_all_owners()
self.db_conn = APSWConnectionWrapper(
str(cog_data_path(self.bot.get_cog("Audio")) / "Audio.db")
)
@@ -33,17 +43,109 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
self.playlist_api = PlaylistWrapper(self.bot, self.config, self.db_conn)
await self.playlist_api.init()
await self.api_interface.initialize()
self.global_api_user = await self.api_interface.global_cache_api.get_perms()
await self.data_schema_migration(
from_version=await self.config.schema_version(), to_version=_SCHEMA_VERSION
)
await self.playlist_api.delete_scheduled()
await self.api_interface.persistent_queue_api.delete_scheduled()
self.lavalink_restart_connect()
self.player_automated_timer_task = self.bot.loop.create_task(
self.player_automated_timer()
)
self.player_automated_timer_task.add_done_callback(task_callback)
lavalink.register_event_listener(self.lavalink_event_handler)
await self.restore_players()
except Exception as err:
log.exception("Audio failed to start up, please report this issue.", exc_info=err)
raise err
self.cog_ready_event.set()
async def restore_players(self):
tries = 0
tracks_to_restore = await self.api_interface.persistent_queue_api.fetch_all()
for guild_id, track_data in itertools.groupby(tracks_to_restore, key=lambda x: x.guild_id):
await asyncio.sleep(0)
try:
player: Optional[lavalink.Player]
track_data = list(track_data)
guild = self.bot.get_guild(guild_id)
persist_cache = self._persist_queue_cache.setdefault(
guild_id, await self.config.guild(guild).persist_queue()
)
if not persist_cache:
await self.api_interface.persistent_queue_api.drop(guild_id)
continue
if self.lavalink_connection_aborted:
player = None
else:
try:
player = lavalink.get_player(guild_id)
except IndexError:
player = None
except KeyError:
player = None
vc = 0
if player is None:
while tries < 25 and vc is not None:
try:
vc = guild.get_channel(track_data[-1].room_id)
await lavalink.connect(vc)
player = lavalink.get_player(guild.id)
player.store("connect", datetime.datetime.utcnow())
player.store("guild", guild_id)
await self.self_deafen(player)
break
except IndexError:
await asyncio.sleep(5)
tries += 1
except Exception as exc:
debug_exc_log(log, exc, "Failed to restore music voice channel")
if vc is None:
break
if tries >= 25 or guild is None or vc is None:
await self.api_interface.persistent_queue_api.drop(guild_id)
continue
shuffle = await self.config.guild(guild).shuffle()
repeat = await self.config.guild(guild).repeat()
volume = await self.config.guild(guild).volume()
shuffle_bumped = await self.config.guild(guild).shuffle_bumped()
player.repeat = repeat
player.shuffle = shuffle
player.shuffle_bumped = shuffle_bumped
if player.volume != volume:
await player.set_volume(volume)
for track in track_data:
track = track.track_object
player.add(guild.get_member(track.extras.get("requester")) or guild.me, track)
player.maybe_shuffle()
await player.play()
except Exception as err:
debug_exc_log(log, err, f"Error restoring player in {guild_id}")
await self.api_interface.persistent_queue_api.drop(guild_id)
async def maybe_message_all_owners(self):
current_notification = await self.config.owner_notification()
if current_notification == _OWNER_NOTIFICATION:
return
if current_notification < 1 <= _OWNER_NOTIFICATION:
msg = _(
"""Hello, this message brings you an important update regarding the core Audio cog:
Starting from Audio v2.3.0+ you can take advantage of the **Global Audio API**, a new service offered by the Cog-Creators organization that allows your bot to greatly reduce the amount of requests done to YouTube / Spotify. This reduces the likelihood of YouTube rate-limiting your bot for making requests too often.
See `[p]help audioset globalapi` for more information.
Access to this service is disabled by default and **requires you to explicitly opt-in** to start using it.
An access token is **required** to use this API. To obtain this token you may join <https://discord.gg/red> and run `?audioapi register` in the #testing channel.
Note: by using this service you accept that your bot's IP address will be disclosed to the Cog-Creators organization and used only for the purpose of providing the Global API service.
On a related note, it is highly recommended that you enable your local cache if you haven't yet.
To do so, run `[p]audioset cache 5`. This cache, which stores only metadata, will make repeated audio requests faster and further reduce the likelihood of YouTube rate-limiting your bot. Since it's only metadata the required disk space for this cache is expected to be negligible."""
)
await send_to_owners_with_prefix_replaced(self.bot, msg)
await self.config.owner_notification.set(1)

View File

@@ -3,6 +3,7 @@ from .equalizer import EqualizerUtilities
from .formatting import FormattingUtilities
from .local_tracks import LocalTrackUtilities
from .miscellaneous import MiscellaneousUtilities
from .parsers import ParsingUtilities
from .player import PlayerUtilities
from .playlists import PlaylistUtilities
from .queue import QueueUtilities
@@ -18,6 +19,7 @@ class Utilities(
PlaylistUtilities,
QueueUtilities,
ValidationUtilities,
ParsingUtilities,
metaclass=CompositeMetaClass,
):
"""Class joining all utility subclasses"""

View File

@@ -1,6 +1,7 @@
import asyncio
import contextlib
import logging
from typing import List
import discord

View File

@@ -2,14 +2,16 @@ import datetime
import logging
import math
import re
import time
from typing import List, Optional
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core.utils import AsyncIter
from discord.embeds import EmptyEmbed
from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box, escape
from ...audio_dataclasses import LocalPath, Query
@@ -98,6 +100,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError:
return await self.send_embed_msg(ctx, title=_("Connect to a voice channel first."))
except IndexError:
@@ -128,7 +131,9 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
except IndexError:
search_choice = tracks[-1]
if not hasattr(search_choice, "is_local") and getattr(search_choice, "uri", None):
description = self.get_track_description(search_choice, self.local_folder_current_path)
description = await self.get_track_description(
search_choice, self.local_folder_current_path
)
else:
search_choice = Query.process_input(search_choice, self.local_folder_current_path)
if search_choice.is_local:
@@ -148,14 +153,12 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
queue_dur = await self.queue_duration(ctx)
queue_total_duration = self.format_time(queue_dur)
before_queue_length = len(player.queue)
query = Query.process_input(search_choice, self.local_folder_current_path)
if not await self.is_query_allowed(
self.config,
ctx.guild,
(
f"{search_choice.title} {search_choice.author} {search_choice.uri} "
f"{str(Query.process_input(search_choice, self.local_folder_current_path))}"
),
ctx,
f"{search_choice.title} {search_choice.author} {search_choice.uri} " f"{str(query)}",
query_obj=query,
):
if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
@@ -166,6 +169,13 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(search_choice, guild_data["maxlength"]):
search_choice.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, search_choice)
player.maybe_shuffle()
self.bot.dispatch(
@@ -174,6 +184,13 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
else:
return await self.send_embed_msg(ctx, title=_("Track exceeds maximum length."))
else:
search_choice.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, search_choice)
player.maybe_shuffle()
self.bot.dispatch(
@@ -191,9 +208,11 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
await player.play()
return await self.send_embed_msg(ctx, embed=songembed)
def _format_search_options(self, search_choice):
async def _format_search_options(self, search_choice):
query = Query.process_input(search_choice, self.local_folder_current_path)
description = self.get_track_description(search_choice, self.local_folder_current_path)
description = await self.get_track_description(
search_choice, self.local_folder_current_path
)
return description, query
async def _build_search_page(
@@ -259,10 +278,10 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
)
return embed
def get_track_description(
async def get_track_description(
self, track, local_folder_current_path, shorten=False
) -> Optional[str]:
"""Get the user facing formatted track name"""
"""Get the user facing formatted track name."""
string = None
if track and getattr(track, "uri", None):
query = Query.process_input(track.uri, local_folder_current_path)
@@ -299,7 +318,13 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
string = "{}...".format((string[:40]).rstrip(" "))
string = f'**{escape(f"{string}", formatting=True)}**'
else:
if track.author.lower() not in track.title.lower():
if track.is_stream:
icy = await self.icyparser(track.uri)
if icy:
title = icy
else:
title = f"{track.title} - {track.author}"
elif track.author.lower() not in track.title.lower():
title = f"{track.title} - {track.author}"
else:
title = track.title
@@ -315,8 +340,10 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
string = f'**{escape(f"{string}", formatting=True)}**'
return string
def get_track_description_unformatted(self, track, local_folder_current_path) -> Optional[str]:
"""Get the user facing unformatted track name"""
async def get_track_description_unformatted(
self, track, local_folder_current_path
) -> Optional[str]:
"""Get the user facing unformatted track name."""
if track and hasattr(track, "uri"):
query = Query.process_input(track.uri, local_folder_current_path)
if query.is_local or "localtracks/" in track.uri:
@@ -332,7 +359,13 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
else:
return query.to_string_user()
else:
if track.author.lower() not in track.title.lower():
if track.is_stream:
icy = await self.icyparser(track.uri)
if icy:
title = icy
else:
title = f"{track.title} - {track.author}"
elif track.author.lower() not in track.title.lower():
title = f"{track.title} - {track.author}"
else:
title = track.title
@@ -342,7 +375,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
return None
def format_playlist_picker_data(self, pid, pname, ptracks, pauthor, scope) -> str:
"""Format the values into a pretified codeblock"""
"""Format the values into a prettified codeblock."""
author = self.bot.get_user(pauthor) or pauthor or _("Unknown")
line = _(
" - Name: <{pname}>\n"
@@ -359,9 +392,9 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
player = lavalink.get_player(ctx.guild.id)
paused = player.paused
pos = player.position
dur = player.current.length
dur = getattr(player.current, "length", player.position or 1)
sections = 12
loc_time = round((pos / dur) * sections)
loc_time = round((pos / dur if dur != 0 else pos) * sections)
bar = "\N{BOX DRAWINGS HEAVY HORIZONTAL}"
seek = "\N{RADIO BUTTON}"
if paused:

View File

@@ -1,16 +1,17 @@
import contextlib
import logging
from pathlib import Path
from typing import List, Union
import lavalink
from fuzzywuzzy import process
from redbot.core.utils import AsyncIter
from redbot.core import commands
from redbot.core.utils import AsyncIter
from ...errors import TrackEnqueueError
from ...audio_dataclasses import LocalPath, Query
from ...errors import TrackEnqueueError
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _
@@ -32,7 +33,7 @@ class LocalTrackUtilities(MixinMeta, metaclass=CompositeMetaClass):
)
async def get_localtrack_folder_list(self, ctx: commands.Context, query: Query) -> List[Query]:
"""Return a list of folders per the provided query"""
"""Return a list of folders per the provided query."""
if not await self.localtracks_folder_exists(ctx):
return []
query = Query.process_input(query, self.local_folder_current_path)
@@ -49,7 +50,7 @@ class LocalTrackUtilities(MixinMeta, metaclass=CompositeMetaClass):
async def get_localtrack_folder_tracks(
self, ctx, player: lavalink.player_manager.Player, query: Query
) -> List[lavalink.rest_api.Track]:
"""Return a list of tracks per the provided query"""
"""Return a list of tracks per the provided query."""
if not await self.localtracks_folder_exists(ctx) or self.api_interface is None:
return []

View File

@@ -5,21 +5,22 @@ import functools
import json
import logging
import re
from typing import Any, Final, MutableMapping, Union, cast, Mapping, Pattern
from typing import Any, Final, Mapping, MutableMapping, Pattern, Union, cast
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core.utils import AsyncIter
from discord.embeds import EmptyEmbed
from redbot.core import bank, commands
from redbot.core.commands import Context
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _, _SCHEMA_VERSION
from ...apis.playlist_interface import get_all_playlist_for_migration23
from ...utils import PlaylistScope
from ...utils import PlaylistScope, task_callback
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _
log = logging.getLogger("red.cogs.Audio.cog.Utilities.miscellaneous")
@@ -32,7 +33,9 @@ class MiscellaneousUtilities(MixinMeta, metaclass=CompositeMetaClass):
self, message: discord.Message, emoji: MutableMapping = None
) -> asyncio.Task:
"""Non blocking version of clear_react."""
return self.bot.loop.create_task(self.clear_react(message, emoji))
task = self.bot.loop.create_task(self.clear_react(message, emoji))
task.add_done_callback(task_callback)
return task
async def maybe_charge_requester(self, ctx: commands.Context, jukebox_price: int) -> bool:
jukebox = await self.config.guild(ctx.guild).jukebox()

View File

@@ -0,0 +1,35 @@
import logging
import re
import struct
from typing import Final, Optional
import aiohttp
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass
log = logging.getLogger("red.cogs.Audio.cog.Utilities.Parsing")
STREAM_TITLE: Final[re.Pattern] = re.compile(br"StreamTitle='([^']*)';")
class ParsingUtilities(MixinMeta, metaclass=CompositeMetaClass):
async def icyparser(self, url: str) -> Optional[str]:
try:
async with self.session.get(url, headers={"Icy-MetaData": "1"}) as resp:
metaint = int(resp.headers["icy-metaint"])
for _ in range(5):
await resp.content.readexactly(metaint)
metadata_length = struct.unpack("B", await resp.content.readexactly(1))[0] * 16
metadata = await resp.content.readexactly(metadata_length)
m = re.search(STREAM_TITLE, metadata.rstrip(b"\0"))
if m:
title = m.group(1)
if title:
title = title.decode("utf-8", errors="replace")
return title
else:
return None
except (KeyError, aiohttp.ClientConnectionError, aiohttp.ClientResponseError):
return None

View File

@@ -1,14 +1,15 @@
import logging
import time
from typing import List, Optional, Tuple, Union
import aiohttp
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core.utils import AsyncIter
from discord.embeds import EmptyEmbed
from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import bold, escape
from ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
@@ -42,7 +43,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
self._error_timer[guild] = now
return self._error_counter[guild] >= 5
def get_active_player_count(self) -> Tuple[Optional[str], int]:
async def get_active_player_count(self) -> Tuple[Optional[str], int]:
try:
current = next(
(
@@ -52,7 +53,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
),
None,
)
get_single_title = self.get_track_description_unformatted(
get_single_title = await self.get_track_description_unformatted(
current, self.local_folder_current_path
)
playing_servers = len(lavalink.active_players())
@@ -149,7 +150,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
elif autoplay and not player.queue:
embed = discord.Embed(
title=_("Track Skipped"),
description=self.get_track_description(
description=await self.get_track_description(
player.current, self.local_folder_current_path
),
)
@@ -184,7 +185,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
else:
embed = discord.Embed(
title=_("Track Skipped"),
description=self.get_track_description(
description=await self.get_track_description(
player.current, self.local_folder_current_path
),
)
@@ -208,6 +209,17 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
except (IndexError, KeyError):
return False
async def self_deafen(self, player: lavalink.Player) -> None:
guild_id = self.rgetattr(player, "channel.guild.id", None)
if not guild_id:
return
if not await self.config.guild_from_id(guild_id).auto_deafen():
return
channel_id = player.channel.id
node = player.manager.node
voice_ws = node.get_voice_ws(guild_id)
await voice_ws.voice_state(guild_id, channel_id, self_deaf=True)
async def _get_spotify_tracks(
self, ctx: commands.Context, query: Query, forced: bool = False
) -> Union[discord.Message, List[lavalink.Track], lavalink.Track]:
@@ -285,7 +297,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, "
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
)
@@ -354,9 +366,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
playlist_url = None
seek = 0
if type(query) is not list:
if not await self.is_query_allowed(
self.config, ctx.guild, f"{query}", query_obj=query
):
if not await self.is_query_allowed(self.config, ctx, f"{query}", query_obj=query):
raise QueryUnauthorized(
_("{query} is not an allowed query.").format(query=query.to_string_user())
)
@@ -373,7 +383,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, "
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
)
@@ -423,13 +433,12 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
async for track in AsyncIter(tracks):
if len(player.queue) >= 10000:
continue
query = Query.process_input(track, self.local_folder_current_path)
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))}"
),
ctx,
f"{track.title} {track.author} {track.uri} " f"{str(query)}",
query_obj=query,
):
if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
@@ -437,6 +446,13 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(track, guild_data["maxlength"]):
track_len += 1
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, track)
self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
@@ -444,6 +460,13 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
else:
track_len += 1
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, track)
self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
@@ -499,13 +522,15 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
)
if seek and seek > 0:
single_track.start_timestamp = seek * 1000
query = Query.process_input(single_track, self.local_folder_current_path)
if not await self.is_query_allowed(
self.config,
ctx.guild,
ctx,
(
f"{single_track.title} {single_track.author} {single_track.uri} "
f"{str(Query.process_input(single_track, self.local_folder_current_path))}"
f"{str(query)}"
),
query_obj=query,
):
if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
@@ -515,6 +540,13 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
)
elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(single_track, guild_data["maxlength"]):
single_track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, single_track)
player.maybe_shuffle()
self.bot.dispatch(
@@ -530,6 +562,13 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
)
else:
single_track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, single_track)
player.maybe_shuffle()
self.bot.dispatch(
@@ -542,7 +581,9 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
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=title, description=desc)
description = self.get_track_description(single_track, self.local_folder_current_path)
description = await self.get_track_description(
single_track, self.local_folder_current_path
)
embed = discord.Embed(title=_("Track Enqueued"), description=description)
if not guild_data["shuffle"] and queue_dur > 0:
embed.set_footer(
@@ -588,6 +629,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
lock=self.update_player_lock,
notifier=notifier,
forced=forced,
query_global=await self.config.global_db_enabled(),
)
except SpotifyFetchError as error:
self.update_player_lock(ctx, False)
@@ -602,7 +644,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment,"
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
error=True,
@@ -657,6 +699,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
and len(player.queue) == 0
):
await player.move_to(user_channel)
await self.self_deafen(player)
return True
else:
return False

View File

@@ -4,14 +4,15 @@ import datetime
import json
import logging
import math
from typing import List, MutableMapping, Optional, Tuple, Union
import discord
import lavalink
from discord.embeds import EmptyEmbed
from redbot.core.utils import AsyncIter
from discord.embeds import EmptyEmbed
from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate
@@ -408,7 +409,7 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, "
"I'm unable to get a track from Lavalink at the moment, "
"try again in a few minutes."
),
)
@@ -513,6 +514,7 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except IndexError:
await self.send_embed_msg(
ctx,
@@ -593,7 +595,7 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, try again in a few "
"I'm unable to get a track from Lavalink at the moment, try again in a few "
"minutes."
),
)
@@ -619,7 +621,7 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Unable to Get Track"),
description=_(
"I'm unable get a track from Lavalink at the moment, try again in a few "
"I'm unable to get a track from Lavalink at the moment, try again in a few "
"minutes."
),
)

View File

@@ -1,13 +1,14 @@
import logging
import math
from typing import List, Tuple
import discord
import lavalink
from fuzzywuzzy import process
from redbot.core.utils import AsyncIter
from fuzzywuzzy import process
from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number
from ...audio_dataclasses import LocalPath, Query
@@ -46,7 +47,7 @@ class QueueUtilities(MixinMeta, metaclass=CompositeMetaClass):
dur = self.format_time(player.current.length)
query = Query.process_input(player.current, self.local_folder_current_path)
current_track_description = self.get_track_description(
current_track_description = await self.get_track_description(
player.current, self.local_folder_current_path
)
if query.is_stream:
@@ -65,7 +66,7 @@ class QueueUtilities(MixinMeta, metaclass=CompositeMetaClass):
):
req_user = track.requester
track_idx = i + 1
track_description = self.get_track_description(
track_description = await self.get_track_description(
track, self.local_folder_current_path, shorten=True
)
queue_list += f"`{track_idx}.` {track_description}, "
@@ -76,6 +77,7 @@ class QueueUtilities(MixinMeta, metaclass=CompositeMetaClass):
title=_("Queue for __{guild_name}__").format(guild_name=ctx.guild.name),
description=queue_list,
)
if await self.config.guild(ctx.guild).thumbnail() and player.current.thumbnail:
embed.set_thumbnail(url=player.current.thumbnail)
queue_dur = await self.queue_duration(ctx)

View File

@@ -1,11 +1,13 @@
import logging
import re
from typing import Final, List, Set, Pattern
from typing import Final, List, Optional, Pattern, Set, Union
from urllib.parse import urlparse
import discord
from redbot.core import Config
from redbot.core.commands import Context
from ...audio_dataclasses import Query
from ..abc import MixinMeta
@@ -54,11 +56,21 @@ class ValidationUtilities(MixinMeta, metaclass=CompositeMetaClass):
return not (channel.user_limit == 0 or channel.user_limit > len(channel.members))
async def is_query_allowed(
self, config: Config, guild: discord.Guild, query: str, query_obj: Query = None
self,
config: Config,
ctx_or_channel: Optional[Union[Context, discord.TextChannel]],
query: str,
query_obj: Query,
) -> bool:
"""Checks if the query is allowed in this server or globally"""
query = query.lower().strip()
"""Checks if the query is allowed in this server or globally."""
if ctx_or_channel:
guild = ctx_or_channel.guild
channel = (
ctx_or_channel.channel if isinstance(ctx_or_channel, Context) else ctx_or_channel
)
query = query.lower().strip()
else:
guild = None
if query_obj is not None:
query = query_obj.lavalink_query.replace("ytsearch:", "youtubesearch").replace(
"scsearch:", "soundcloudsearch"