mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
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:
parent
29ebf0f060
commit
2da9b502d8
@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
||||
from typing import List, MutableMapping, Optional, Union
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.utils.chat_formatting import humanize_list
|
||||
@ -74,8 +75,22 @@ class PlaylistFetchResult:
|
||||
self.tracks = json.loads(self.tracks)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueFetchResult:
|
||||
guild_id: int
|
||||
room_id: int
|
||||
track: dict = field(default_factory=lambda: {})
|
||||
track_object: lavalink.Track = None
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.track, str):
|
||||
self.track = json.loads(self.track)
|
||||
if self.track:
|
||||
self.track_object = lavalink.Track(self.track)
|
||||
|
||||
|
||||
def standardize_scope(scope: str) -> str:
|
||||
"""Convert any of the used scopes into one we are expecting"""
|
||||
"""Convert any of the used scopes into one we are expecting."""
|
||||
scope = scope.upper()
|
||||
valid_scopes = ["GLOBAL", "GUILD", "AUTHOR", "USER", "SERVER", "MEMBER", "BOT"]
|
||||
|
||||
@ -103,7 +118,7 @@ def prepare_config_scope(
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
):
|
||||
"""Return the scope used by Playlists"""
|
||||
"""Return the scope used by Playlists."""
|
||||
scope = standardize_scope(scope)
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
config_scope = [PlaylistScope.GLOBAL.value, bot.user.id]
|
||||
@ -121,7 +136,7 @@ def prepare_config_scope(
|
||||
def prepare_config_scope_for_migration23( # TODO: remove me in a future version ?
|
||||
scope, author: Union[discord.abc.User, int] = None, guild: discord.Guild = None
|
||||
):
|
||||
"""Return the scope used by Playlists"""
|
||||
"""Return the scope used by Playlists."""
|
||||
scope = standardize_scope(scope)
|
||||
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from typing import Mapping, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Mapping, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
from lavalink.rest_api import LoadResult
|
||||
@ -17,7 +19,7 @@ from ..audio_logging import IS_DEBUG, debug_exc_log
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
_API_URL = "https://redbot.app/"
|
||||
_API_URL = "https://api.redbot.app/"
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.GlobalDB")
|
||||
|
||||
@ -32,11 +34,150 @@ class GlobalCacheWrapper:
|
||||
self.session = session
|
||||
self.api_key = None
|
||||
self._handshake_token = ""
|
||||
self.can_write = False
|
||||
self._handshake_token = ""
|
||||
self.has_api_key = None
|
||||
self._token: Mapping[str, str] = {}
|
||||
self.cog = cog
|
||||
|
||||
def update_token(self, new_token: Mapping[str, str]):
|
||||
self._token = new_token
|
||||
|
||||
async def _get_api_key(
|
||||
self,
|
||||
) -> Optional[str]:
|
||||
if not self._token:
|
||||
self._token = await self.bot.get_shared_api_tokens("audiodb")
|
||||
self.api_key = self._token.get("api_key", None)
|
||||
self.has_api_key = self.cog.global_api_user.get("can_post")
|
||||
id_list = list(self.bot.owner_ids)
|
||||
self._handshake_token = "||".join(map(str, id_list))
|
||||
return self.api_key
|
||||
|
||||
async def get_call(self, query: Optional[Query] = None) -> dict:
|
||||
api_url = f"{_API_URL}api/v2/queries"
|
||||
if not self.cog.global_api_user.get("can_read"):
|
||||
return {}
|
||||
try:
|
||||
query = Query.process_input(query, self.cog.local_folder_current_path)
|
||||
if any([not query or not query.valid or query.is_spotify or query.is_local]):
|
||||
return {}
|
||||
await self._get_api_key()
|
||||
if self.api_key is None:
|
||||
return {}
|
||||
search_response = "error"
|
||||
query = query.lavalink_query
|
||||
with contextlib.suppress(aiohttp.ContentTypeError, asyncio.TimeoutError):
|
||||
async with self.session.get(
|
||||
api_url,
|
||||
timeout=aiohttp.ClientTimeout(total=await self.config.global_db_get_timeout()),
|
||||
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
|
||||
params={"query": query},
|
||||
) as r:
|
||||
search_response = await r.json(loads=json.loads)
|
||||
if IS_DEBUG and "x-process-time" in r.headers:
|
||||
log.debug(
|
||||
f"GET || Ping {r.headers.get('x-process-time')} || "
|
||||
f"Status code {r.status} || {query}"
|
||||
)
|
||||
if "tracks" not in search_response:
|
||||
return {}
|
||||
return search_response
|
||||
except Exception as err:
|
||||
debug_exc_log(log, err, f"Failed to Get query: {api_url}/{query}")
|
||||
return {}
|
||||
|
||||
async def get_spotify(self, title: str, author: Optional[str]) -> dict:
|
||||
if not self.cog.global_api_user.get("can_read"):
|
||||
return {}
|
||||
api_url = f"{_API_URL}api/v2/queries/spotify"
|
||||
try:
|
||||
search_response = "error"
|
||||
params = {"title": title, "author": author}
|
||||
await self._get_api_key()
|
||||
if self.api_key is None:
|
||||
return {}
|
||||
with contextlib.suppress(aiohttp.ContentTypeError, asyncio.TimeoutError):
|
||||
async with self.session.get(
|
||||
api_url,
|
||||
timeout=aiohttp.ClientTimeout(total=await self.config.global_db_get_timeout()),
|
||||
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
|
||||
params=params,
|
||||
) as r:
|
||||
search_response = await r.json(loads=json.loads)
|
||||
if IS_DEBUG and "x-process-time" in r.headers:
|
||||
log.debug(
|
||||
f"GET/spotify || Ping {r.headers.get('x-process-time')} || "
|
||||
f"Status code {r.status} || {title} - {author}"
|
||||
)
|
||||
if "tracks" not in search_response:
|
||||
return {}
|
||||
return search_response
|
||||
except Exception as err:
|
||||
debug_exc_log(log, err, f"Failed to Get query: {api_url}")
|
||||
return {}
|
||||
|
||||
async def post_call(self, llresponse: LoadResult, query: Optional[Query]) -> None:
|
||||
try:
|
||||
if not self.cog.global_api_user.get("can_post"):
|
||||
return
|
||||
query = Query.process_input(query, self.cog.local_folder_current_path)
|
||||
if llresponse.has_error or llresponse.load_type.value in ["NO_MATCHES", "LOAD_FAILED"]:
|
||||
return
|
||||
if query and query.valid and query.is_youtube:
|
||||
query = query.lavalink_query
|
||||
else:
|
||||
return None
|
||||
await self._get_api_key()
|
||||
if self.api_key is None:
|
||||
return None
|
||||
api_url = f"{_API_URL}api/v2/queries"
|
||||
async with self.session.post(
|
||||
api_url,
|
||||
json=llresponse._raw,
|
||||
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
|
||||
params={"query": query},
|
||||
) as r:
|
||||
await r.read()
|
||||
if IS_DEBUG and "x-process-time" in r.headers:
|
||||
log.debug(
|
||||
f"POST || Ping {r.headers.get('x-process-time')} ||"
|
||||
f" Status code {r.status} || {query}"
|
||||
)
|
||||
except Exception as err:
|
||||
debug_exc_log(log, err, f"Failed to post query: {query}")
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async def update_global(self, llresponse: LoadResult, query: Optional[Query] = None):
|
||||
await self.post_call(llresponse=llresponse, query=query)
|
||||
|
||||
async def report_invalid(self, id: str) -> None:
|
||||
if not self.cog.global_api_user.get("can_delete"):
|
||||
return
|
||||
api_url = f"{_API_URL}api/v2/queries/es/id"
|
||||
with contextlib.suppress(Exception):
|
||||
async with self.session.delete(
|
||||
api_url,
|
||||
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
|
||||
params={"id": id},
|
||||
) as r:
|
||||
await r.read()
|
||||
|
||||
async def get_perms(self):
|
||||
global_api_user = copy(self.cog.global_api_user)
|
||||
await self._get_api_key()
|
||||
is_enabled = await self.config.global_db_enabled()
|
||||
await self._get_api_key()
|
||||
if (not is_enabled) or self.api_key is None:
|
||||
return global_api_user
|
||||
with contextlib.suppress(Exception):
|
||||
async with aiohttp.ClientSession(json_serialize=json.dumps) as session:
|
||||
async with session.get(
|
||||
f"{_API_URL}api/v2/users/me",
|
||||
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
search_response = await resp.json(loads=json.loads)
|
||||
global_api_user["fetched"] = True
|
||||
global_api_user["can_read"] = search_response.get("can_read", False)
|
||||
global_api_user["can_post"] = search_response.get("can_post", False)
|
||||
global_api_user["can_delete"] = search_response.get("can_delete", False)
|
||||
return global_api_user
|
||||
|
||||
@ -1,30 +1,34 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import Callable, List, MutableMapping, Optional, TYPE_CHECKING, Tuple, Union, cast
|
||||
from typing import TYPE_CHECKING, Callable, List, MutableMapping, Optional, Tuple, Union, cast
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
import lavalink
|
||||
from lavalink.rest_api import LoadResult
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from lavalink.rest_api import LoadResult, LoadType
|
||||
from redbot.core import Config, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog, Context
|
||||
from redbot.core.i18n import Translator
|
||||
from redbot.core.utils import AsyncIter
|
||||
from redbot.core.utils.dbtools import APSWConnectionWrapper
|
||||
|
||||
from ..audio_dataclasses import Query
|
||||
from ..audio_logging import IS_DEBUG, debug_exc_log
|
||||
from ..errors import DatabaseError, SpotifyFetchError, TrackEnqueueError
|
||||
from ..utils import CacheLevel, Notifier
|
||||
from .api_utils import LavalinkCacheFetchForGlobalResult
|
||||
from .global_db import GlobalCacheWrapper
|
||||
from .local_db import LocalCacheWrapper
|
||||
from .persist_queue_wrapper import QueueInterface
|
||||
from .playlist_interface import get_playlist
|
||||
from .playlist_wrapper import PlaylistWrapper
|
||||
from .spotify import SpotifyWrapper
|
||||
@ -36,6 +40,7 @@ if TYPE_CHECKING:
|
||||
_ = Translator("Audio", __file__)
|
||||
log = logging.getLogger("red.cogs.Audio.api.AudioAPIInterface")
|
||||
_TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK"
|
||||
# TODO: Get random from global Cache
|
||||
|
||||
|
||||
class AudioAPIInterface:
|
||||
@ -60,20 +65,22 @@ class AudioAPIInterface:
|
||||
self.youtube_api: YouTubeWrapper = YouTubeWrapper(self.bot, self.config, session, self.cog)
|
||||
self.local_cache_api = LocalCacheWrapper(self.bot, self.config, self.conn, self.cog)
|
||||
self.global_cache_api = GlobalCacheWrapper(self.bot, self.config, session, self.cog)
|
||||
self.persistent_queue_api = QueueInterface(self.bot, self.config, self.conn, self.cog)
|
||||
self._session: aiohttp.ClientSession = session
|
||||
self._tasks: MutableMapping = {}
|
||||
self._lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialises the Local Cache connection"""
|
||||
"""Initialises the Local Cache connection."""
|
||||
await self.local_cache_api.lavalink.init()
|
||||
await self.persistent_queue_api.init()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Closes the Local Cache connection"""
|
||||
"""Closes the Local Cache connection."""
|
||||
self.local_cache_api.lavalink.close()
|
||||
|
||||
async def get_random_track_from_db(self) -> Optional[MutableMapping]:
|
||||
"""Get a random track from the local database and return it"""
|
||||
async def get_random_track_from_db(self, tries=0) -> Optional[MutableMapping]:
|
||||
"""Get a random track from the local database and return it."""
|
||||
track: Optional[MutableMapping] = {}
|
||||
try:
|
||||
query_data = {}
|
||||
@ -106,7 +113,7 @@ class AudioAPIInterface:
|
||||
action_type: str = None,
|
||||
data: Union[List[MutableMapping], MutableMapping] = None,
|
||||
) -> None:
|
||||
"""Separate the tasks and run them in the appropriate functions"""
|
||||
"""Separate the tasks and run them in the appropriate functions."""
|
||||
|
||||
if not data:
|
||||
return
|
||||
@ -126,9 +133,11 @@ class AudioAPIInterface:
|
||||
await self.local_cache_api.youtube.update(data)
|
||||
elif table == "spotify":
|
||||
await self.local_cache_api.spotify.update(data)
|
||||
elif action_type == "global" and isinstance(data, list):
|
||||
await asyncio.gather(*[self.global_cache_api.update_global(**d) for d in data])
|
||||
|
||||
async def run_tasks(self, ctx: Optional[commands.Context] = None, message_id=None) -> None:
|
||||
"""Run tasks for a specific context"""
|
||||
"""Run tasks for a specific context."""
|
||||
if message_id is not None:
|
||||
lock_id = message_id
|
||||
elif ctx is not None:
|
||||
@ -143,7 +152,7 @@ class AudioAPIInterface:
|
||||
try:
|
||||
tasks = self._tasks[lock_id]
|
||||
tasks = [self.route_tasks(a, tasks[a]) for a in tasks]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
await asyncio.gather(*tasks, return_exceptions=False)
|
||||
del self._tasks[lock_id]
|
||||
except Exception as exc:
|
||||
debug_exc_log(
|
||||
@ -154,7 +163,7 @@ class AudioAPIInterface:
|
||||
log.debug(f"Completed database writes for {lock_id} ({lock_author})")
|
||||
|
||||
async def run_all_pending_tasks(self) -> None:
|
||||
"""Run all pending tasks left in the cache, called on cog_unload"""
|
||||
"""Run all pending tasks left in the cache, called on cog_unload."""
|
||||
async with self._lock:
|
||||
if IS_DEBUG:
|
||||
log.debug("Running pending writes to database")
|
||||
@ -166,7 +175,7 @@ class AudioAPIInterface:
|
||||
self._tasks = {}
|
||||
coro_tasks = [self.route_tasks(a, tasks[a]) for a in tasks]
|
||||
|
||||
await asyncio.gather(*coro_tasks, return_exceptions=True)
|
||||
await asyncio.gather(*coro_tasks, return_exceptions=False)
|
||||
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed database writes")
|
||||
@ -175,7 +184,7 @@ class AudioAPIInterface:
|
||||
log.debug("Completed pending writes to database have finished")
|
||||
|
||||
def append_task(self, ctx: commands.Context, event: str, task: Tuple, _id: int = None) -> None:
|
||||
"""Add a task to the cache to be run later"""
|
||||
"""Add a task to the cache to be run later."""
|
||||
lock_id = _id or ctx.message.id
|
||||
if lock_id not in self._tasks:
|
||||
self._tasks[lock_id] = {"update": [], "insert": [], "global": []}
|
||||
@ -190,7 +199,7 @@ class AudioAPIInterface:
|
||||
skip_youtube: bool = False,
|
||||
current_cache_level: CacheLevel = CacheLevel.none(),
|
||||
) -> List[str]:
|
||||
"""Return youtube URLS for the spotify URL provided"""
|
||||
"""Return youtube URLS for the spotify URL provided."""
|
||||
youtube_urls = []
|
||||
tracks = await self.fetch_from_spotify_api(
|
||||
query_type, uri, params=None, notifier=notifier, ctx=ctx
|
||||
@ -266,7 +275,7 @@ class AudioAPIInterface:
|
||||
notifier: Optional[Notifier] = None,
|
||||
ctx: Context = None,
|
||||
) -> Union[List[MutableMapping], List[str]]:
|
||||
"""Gets track info from spotify API"""
|
||||
"""Gets track info from spotify API."""
|
||||
|
||||
if recursive is False:
|
||||
(call, params) = self.spotify_api.spotify_format_call(query_type, uri)
|
||||
@ -394,9 +403,10 @@ class AudioAPIInterface:
|
||||
lock: Callable,
|
||||
notifier: Optional[Notifier] = None,
|
||||
forced: bool = False,
|
||||
query_global: bool = False,
|
||||
query_global: bool = True,
|
||||
) -> List[lavalink.Track]:
|
||||
"""Queries the Database then falls back to Spotify and YouTube APIs then Enqueued matched tracks.
|
||||
"""Queries the Database then falls back to Spotify and YouTube APIs then Enqueued matched
|
||||
tracks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@ -423,7 +433,9 @@ class AudioAPIInterface:
|
||||
List[str]
|
||||
List of Youtube URLs.
|
||||
"""
|
||||
# globaldb_toggle = await self.config.global_db_enabled()
|
||||
await self.global_cache_api._get_api_key()
|
||||
globaldb_toggle = await self.config.global_db_enabled()
|
||||
global_entry = globaldb_toggle and query_global
|
||||
track_list: List = []
|
||||
has_not_allowed = False
|
||||
try:
|
||||
@ -485,7 +497,14 @@ class AudioAPIInterface:
|
||||
)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table")
|
||||
|
||||
should_query_global = globaldb_toggle and query_global and val is None
|
||||
if should_query_global:
|
||||
llresponse = await self.global_cache_api.get_spotify(track_name, artist_name)
|
||||
if llresponse:
|
||||
if llresponse.get("loadType") == "V2_COMPACT":
|
||||
llresponse["loadType"] = "V2_COMPAT"
|
||||
llresponse = LoadResult(llresponse)
|
||||
val = llresponse or None
|
||||
if val is None:
|
||||
val = await self.fetch_youtube_query(
|
||||
ctx, track_info, current_cache_level=current_cache_level
|
||||
@ -494,34 +513,44 @@ class AudioAPIInterface:
|
||||
task = ("update", ("youtube", {"track": track_info}))
|
||||
self.append_task(ctx, *task)
|
||||
|
||||
if llresponse is not None:
|
||||
if isinstance(llresponse, LoadResult):
|
||||
track_object = llresponse.tracks
|
||||
elif val:
|
||||
try:
|
||||
(result, called_api) = await self.fetch_track(
|
||||
ctx,
|
||||
player,
|
||||
Query.process_input(val, self.cog.local_folder_current_path),
|
||||
forced=forced,
|
||||
)
|
||||
except (RuntimeError, aiohttp.ServerDisconnectedError):
|
||||
lock(ctx, False)
|
||||
error_embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("The connection was reset while loading the playlist."),
|
||||
)
|
||||
if notifier is not None:
|
||||
await notifier.update_embed(error_embed)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
lock(ctx, False)
|
||||
error_embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Player timeout, skipping remaining tracks."),
|
||||
)
|
||||
if notifier is not None:
|
||||
await notifier.update_embed(error_embed)
|
||||
break
|
||||
result = None
|
||||
if should_query_global:
|
||||
llresponse = await self.global_cache_api.get_call(val)
|
||||
if llresponse:
|
||||
if llresponse.get("loadType") == "V2_COMPACT":
|
||||
llresponse["loadType"] = "V2_COMPAT"
|
||||
llresponse = LoadResult(llresponse)
|
||||
result = llresponse or None
|
||||
if not result:
|
||||
try:
|
||||
(result, called_api) = await self.fetch_track(
|
||||
ctx,
|
||||
player,
|
||||
Query.process_input(val, self.cog.local_folder_current_path),
|
||||
forced=forced,
|
||||
should_query_global=not should_query_global,
|
||||
)
|
||||
except (RuntimeError, aiohttp.ServerDisconnectedError):
|
||||
lock(ctx, False)
|
||||
error_embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("The connection was reset while loading the playlist."),
|
||||
)
|
||||
if notifier is not None:
|
||||
await notifier.update_embed(error_embed)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
lock(ctx, False)
|
||||
error_embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Player timeout, skipping remaining tracks."),
|
||||
)
|
||||
if notifier is not None:
|
||||
await notifier.update_embed(error_embed)
|
||||
break
|
||||
track_object = result.tracks
|
||||
else:
|
||||
track_object = []
|
||||
@ -538,7 +567,7 @@ class AudioAPIInterface:
|
||||
seconds=seconds,
|
||||
)
|
||||
|
||||
if consecutive_fails >= 10:
|
||||
if consecutive_fails >= (100 if global_entry else 10):
|
||||
error_embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Failing to get tracks, skipping remaining."),
|
||||
@ -551,13 +580,12 @@ class AudioAPIInterface:
|
||||
continue
|
||||
consecutive_fails = 0
|
||||
single_track = track_object[0]
|
||||
query = Query.process_input(single_track, self.cog.local_folder_current_path)
|
||||
if not await self.cog.is_query_allowed(
|
||||
self.config,
|
||||
ctx.guild,
|
||||
(
|
||||
f"{single_track.title} {single_track.author} {single_track.uri} "
|
||||
f"{Query.process_input(single_track, self.cog.local_folder_current_path)}"
|
||||
),
|
||||
ctx,
|
||||
f"{single_track.title} {single_track.author} {single_track.uri} {query}",
|
||||
query_obj=query,
|
||||
):
|
||||
has_not_allowed = True
|
||||
if IS_DEBUG:
|
||||
@ -570,6 +598,13 @@ class AudioAPIInterface:
|
||||
if guild_data["maxlength"] > 0:
|
||||
if self.cog.is_track_length_allowed(single_track, guild_data["maxlength"]):
|
||||
enqueued_tracks += 1
|
||||
single_track.extras.update(
|
||||
{
|
||||
"enqueue_time": int(time.time()),
|
||||
"vc": player.channel.id,
|
||||
"requester": ctx.author.id,
|
||||
}
|
||||
)
|
||||
player.add(ctx.author, single_track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue",
|
||||
@ -579,6 +614,13 @@ class AudioAPIInterface:
|
||||
)
|
||||
else:
|
||||
enqueued_tracks += 1
|
||||
single_track.extras.update(
|
||||
{
|
||||
"enqueue_time": int(time.time()),
|
||||
"vc": player.channel.id,
|
||||
"requester": ctx.author.id,
|
||||
}
|
||||
)
|
||||
player.add(ctx.author, single_track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue",
|
||||
@ -642,9 +684,7 @@ class AudioAPIInterface:
|
||||
track_info: str,
|
||||
current_cache_level: CacheLevel = CacheLevel.none(),
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Call the Youtube API and returns the youtube URL that the query matched
|
||||
"""
|
||||
"""Call the Youtube API and returns the youtube URL that the query matched."""
|
||||
track_url = await self.youtube_api.get_call(track_info)
|
||||
if CacheLevel.set_youtube().is_subset(current_cache_level) and track_url:
|
||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
@ -668,9 +708,7 @@ class AudioAPIInterface:
|
||||
async def fetch_from_youtube_api(
|
||||
self, ctx: commands.Context, track_info: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Gets an YouTube URL from for the query
|
||||
"""
|
||||
"""Gets an YouTube URL from for the query."""
|
||||
current_cache_level = CacheLevel(await self.config.cache_level())
|
||||
cache_enabled = CacheLevel.set_youtube().is_subset(current_cache_level)
|
||||
val = None
|
||||
@ -727,6 +765,7 @@ class AudioAPIInterface:
|
||||
val = None
|
||||
query = Query.process_input(query, self.cog.local_folder_current_path)
|
||||
query_string = str(query)
|
||||
globaldb_toggle = await self.config.global_db_enabled()
|
||||
valid_global_entry = False
|
||||
results = None
|
||||
called_api = False
|
||||
@ -754,7 +793,31 @@ class AudioAPIInterface:
|
||||
called_api = False
|
||||
else:
|
||||
val = None
|
||||
|
||||
if (
|
||||
globaldb_toggle
|
||||
and not val
|
||||
and should_query_global
|
||||
and not forced
|
||||
and not query.is_local
|
||||
and not query.is_spotify
|
||||
):
|
||||
valid_global_entry = False
|
||||
with contextlib.suppress(Exception):
|
||||
global_entry = await self.global_cache_api.get_call(query=query)
|
||||
if global_entry.get("loadType") == "V2_COMPACT":
|
||||
global_entry["loadType"] = "V2_COMPAT"
|
||||
results = LoadResult(global_entry)
|
||||
if results.load_type in [
|
||||
LoadType.PLAYLIST_LOADED,
|
||||
LoadType.TRACK_LOADED,
|
||||
LoadType.SEARCH_RESULT,
|
||||
LoadType.V2_COMPAT,
|
||||
]:
|
||||
valid_global_entry = True
|
||||
if valid_global_entry:
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Querying Global DB api for {query}")
|
||||
results, called_api = results, False
|
||||
if valid_global_entry:
|
||||
pass
|
||||
elif lazy is True:
|
||||
@ -769,6 +832,7 @@ class AudioAPIInterface:
|
||||
if results.has_error:
|
||||
# If cached value has an invalid entry make a new call so that it gets updated
|
||||
results, called_api = await self.fetch_track(ctx, player, query, forced=True)
|
||||
valid_global_entry = False
|
||||
else:
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Querying Lavalink api for {query_string}")
|
||||
@ -781,7 +845,19 @@ class AudioAPIInterface:
|
||||
raise TrackEnqueueError
|
||||
if results is None:
|
||||
results = LoadResult({"loadType": "LOAD_FAILED", "playlistInfo": {}, "tracks": []})
|
||||
|
||||
valid_global_entry = False
|
||||
update_global = (
|
||||
globaldb_toggle and not valid_global_entry and self.global_cache_api.has_api_key
|
||||
)
|
||||
with contextlib.suppress(Exception):
|
||||
if (
|
||||
update_global
|
||||
and not query.is_local
|
||||
and not results.has_error
|
||||
and len(results.tracks) >= 1
|
||||
):
|
||||
global_task = ("global", dict(llresponse=results, query=query))
|
||||
self.append_task(ctx, *global_task)
|
||||
if (
|
||||
cache_enabled
|
||||
and results.load_type
|
||||
@ -817,9 +893,7 @@ class AudioAPIInterface:
|
||||
return results, called_api
|
||||
|
||||
async def autoplay(self, player: lavalink.Player, playlist_api: PlaylistWrapper):
|
||||
"""
|
||||
Enqueue a random track
|
||||
"""
|
||||
"""Enqueue a random track."""
|
||||
autoplaylist = await self.config.guild(player.channel.guild).autoplaylist()
|
||||
current_cache_level = CacheLevel(await self.config.cache_level())
|
||||
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
|
||||
@ -865,19 +939,18 @@ class AudioAPIInterface:
|
||||
track = random.choice(tracks)
|
||||
query = Query.process_input(track, self.cog.local_folder_current_path)
|
||||
await asyncio.sleep(0.001)
|
||||
if not query.valid or (
|
||||
if (not query.valid) or (
|
||||
query.is_local
|
||||
and query.local_track_path is not None
|
||||
and not query.local_track_path.exists()
|
||||
):
|
||||
continue
|
||||
notify_channel = self.bot.get_channel(player.fetch("channel"))
|
||||
if not await self.cog.is_query_allowed(
|
||||
self.config,
|
||||
player.channel.guild,
|
||||
(
|
||||
f"{track.title} {track.author} {track.uri} "
|
||||
f"{str(Query.process_input(track, self.cog.local_folder_current_path))}"
|
||||
),
|
||||
notify_channel,
|
||||
f"{track.title} {track.author} {track.uri} {query}",
|
||||
query_obj=query,
|
||||
):
|
||||
if IS_DEBUG:
|
||||
log.debug(
|
||||
@ -886,11 +959,20 @@ class AudioAPIInterface:
|
||||
)
|
||||
continue
|
||||
valid = True
|
||||
|
||||
track.extras["autoplay"] = True
|
||||
track.extras.update(
|
||||
{
|
||||
"autoplay": True,
|
||||
"enqueue_time": int(time.time()),
|
||||
"vc": player.channel.id,
|
||||
"requester": player.channel.guild.me.id,
|
||||
}
|
||||
)
|
||||
player.add(player.channel.guild.me, track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me
|
||||
)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
|
||||
async def fetch_all_contribute(self) -> List[LavalinkCacheFetchForGlobalResult]:
|
||||
return await self.local_cache_api.lavalink.fetch_all_for_global()
|
||||
|
||||
@ -4,14 +4,14 @@ import datetime
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from typing import Callable, List, MutableMapping, Optional, TYPE_CHECKING, Tuple, Union
|
||||
|
||||
from redbot.core.utils import AsyncIter
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Callable, List, MutableMapping, Optional, Tuple, Union
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog
|
||||
from redbot.core.utils import AsyncIter
|
||||
from redbot.core.utils.dbtools import APSWConnectionWrapper
|
||||
|
||||
from ..audio_logging import debug_exc_log
|
||||
@ -313,7 +313,7 @@ class LavalinkTableWrapper(BaseWrapper):
|
||||
self.statement.get_random = LAVALINK_QUERY_LAST_FETCHED_RANDOM
|
||||
self.statement.get_all_global = LAVALINK_FETCH_ALL_ENTRIES_GLOBAL
|
||||
self.fetch_result = LavalinkCacheFetchResult
|
||||
self.fetch_for_global: Optional[Callable] = None
|
||||
self.fetch_for_global: Optional[Callable] = LavalinkCacheFetchForGlobalResult
|
||||
|
||||
async def fetch_one(
|
||||
self, values: MutableMapping
|
||||
|
||||
133
redbot/cogs/audio/apis/persist_queue_wrapper.py
Normal file
133
redbot/cogs/audio/apis/persist_queue_wrapper.py
Normal file
@ -0,0 +1,133 @@
|
||||
import concurrent
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
import lavalink
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog
|
||||
from redbot.core.utils import AsyncIter
|
||||
from redbot.core.utils.dbtools import APSWConnectionWrapper
|
||||
|
||||
from ..audio_logging import debug_exc_log
|
||||
from ..sql_statements import (
|
||||
PERSIST_QUEUE_BULK_PLAYED,
|
||||
PERSIST_QUEUE_CREATE_INDEX,
|
||||
PERSIST_QUEUE_CREATE_TABLE,
|
||||
PERSIST_QUEUE_DELETE_SCHEDULED,
|
||||
PERSIST_QUEUE_DROP_TABLE,
|
||||
PERSIST_QUEUE_FETCH_ALL,
|
||||
PERSIST_QUEUE_PLAYED,
|
||||
PERSIST_QUEUE_UPSERT,
|
||||
PRAGMA_FETCH_user_version,
|
||||
PRAGMA_SET_journal_mode,
|
||||
PRAGMA_SET_read_uncommitted,
|
||||
PRAGMA_SET_temp_store,
|
||||
PRAGMA_SET_user_version,
|
||||
)
|
||||
from .api_utils import QueueFetchResult
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.PersistQueueWrapper")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
|
||||
class QueueInterface:
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, conn: APSWConnectionWrapper, cog: Union["Audio", Cog]
|
||||
):
|
||||
self.bot = bot
|
||||
self.database = conn
|
||||
self.config = config
|
||||
self.cog = cog
|
||||
self.statement = SimpleNamespace()
|
||||
self.statement.pragma_temp_store = PRAGMA_SET_temp_store
|
||||
self.statement.pragma_journal_mode = PRAGMA_SET_journal_mode
|
||||
self.statement.pragma_read_uncommitted = PRAGMA_SET_read_uncommitted
|
||||
self.statement.set_user_version = PRAGMA_SET_user_version
|
||||
self.statement.get_user_version = PRAGMA_FETCH_user_version
|
||||
self.statement.create_table = PERSIST_QUEUE_CREATE_TABLE
|
||||
self.statement.create_index = PERSIST_QUEUE_CREATE_INDEX
|
||||
|
||||
self.statement.upsert = PERSIST_QUEUE_UPSERT
|
||||
self.statement.update_bulk_player = PERSIST_QUEUE_BULK_PLAYED
|
||||
self.statement.delete_scheduled = PERSIST_QUEUE_DELETE_SCHEDULED
|
||||
self.statement.drop_table = PERSIST_QUEUE_DROP_TABLE
|
||||
|
||||
self.statement.get_all = PERSIST_QUEUE_FETCH_ALL
|
||||
self.statement.get_player = PERSIST_QUEUE_PLAYED
|
||||
|
||||
async def init(self) -> None:
|
||||
"""Initialize the PersistQueue table"""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, self.statement.pragma_temp_store)
|
||||
executor.submit(self.database.cursor().execute, self.statement.pragma_journal_mode)
|
||||
executor.submit(self.database.cursor().execute, self.statement.pragma_read_uncommitted)
|
||||
executor.submit(self.database.cursor().execute, self.statement.create_table)
|
||||
executor.submit(self.database.cursor().execute, self.statement.create_index)
|
||||
|
||||
async def fetch_all(self) -> List[QueueFetchResult]:
|
||||
"""Fetch all playlists"""
|
||||
output = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
for future in concurrent.futures.as_completed(
|
||||
[
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
self.statement.get_all,
|
||||
)
|
||||
]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to complete playlist fetch from database")
|
||||
return []
|
||||
|
||||
async for index, row in AsyncIter(row_result).enumerate(start=1):
|
||||
output.append(QueueFetchResult(*row))
|
||||
return output
|
||||
|
||||
async def played(self, guild_id: int, track_id: str) -> None:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
PERSIST_QUEUE_PLAYED,
|
||||
{"guild_id": guild_id, "track_id": track_id},
|
||||
)
|
||||
|
||||
async def delete_scheduled(self):
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, PERSIST_QUEUE_DELETE_SCHEDULED)
|
||||
|
||||
async def drop(self, guild_id: int):
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
self.database.cursor().execute, PERSIST_QUEUE_BULK_PLAYED, ({"guild_id": guild_id})
|
||||
)
|
||||
|
||||
async def enqueued(self, guild_id: int, room_id: int, track: lavalink.Track):
|
||||
enqueue_time = track.extras.get("enqueue_time", 0)
|
||||
if enqueue_time == 0:
|
||||
track.extras["enqueue_time"] = int(time.time())
|
||||
track_identifier = track.track_identifier
|
||||
track = self.cog.track_to_json(track)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
PERSIST_QUEUE_UPSERT,
|
||||
{
|
||||
"guild_id": int(guild_id),
|
||||
"room_id": int(room_id),
|
||||
"played": False,
|
||||
"time": enqueue_time,
|
||||
"track": json.dumps(track),
|
||||
"track_id": track_identifier,
|
||||
},
|
||||
)
|
||||
@ -1,12 +1,13 @@
|
||||
import logging
|
||||
|
||||
from typing import List, MutableMapping, Optional, Union
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import Config, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from ..errors import NotAllowed
|
||||
from ..utils import PlaylistScope
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import concurrent
|
||||
import json
|
||||
import logging
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import List, MutableMapping, Optional
|
||||
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.utils import AsyncIter
|
||||
from redbot.core.utils.dbtools import APSWConnectionWrapper
|
||||
|
||||
from ..audio_logging import debug_exc_log
|
||||
from ..sql_statements import (
|
||||
HANDLE_DISCORD_DATA_DELETION_QUERY,
|
||||
PLAYLIST_CREATE_INDEX,
|
||||
PLAYLIST_CREATE_TABLE,
|
||||
PLAYLIST_DELETE,
|
||||
@ -27,7 +28,6 @@ from ..sql_statements import (
|
||||
PRAGMA_SET_read_uncommitted,
|
||||
PRAGMA_SET_temp_store,
|
||||
PRAGMA_SET_user_version,
|
||||
HANDLE_DISCORD_DATA_DELETION_QUERY,
|
||||
)
|
||||
from ..utils import PlaylistScope
|
||||
from .api_utils import PlaylistFetchResult
|
||||
@ -62,7 +62,7 @@ class PlaylistWrapper:
|
||||
self.statement.drop_user_playlists = HANDLE_DISCORD_DATA_DELETION_QUERY
|
||||
|
||||
async def init(self) -> None:
|
||||
"""Initialize the Playlist table"""
|
||||
"""Initialize the Playlist table."""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, self.statement.pragma_temp_store)
|
||||
executor.submit(self.database.cursor().execute, self.statement.pragma_journal_mode)
|
||||
@ -72,7 +72,7 @@ class PlaylistWrapper:
|
||||
|
||||
@staticmethod
|
||||
def get_scope_type(scope: str) -> int:
|
||||
"""Convert a scope to a numerical identifier"""
|
||||
"""Convert a scope to a numerical identifier."""
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
table = 1
|
||||
elif scope == PlaylistScope.USER.value:
|
||||
@ -82,7 +82,7 @@ class PlaylistWrapper:
|
||||
return table
|
||||
|
||||
async def fetch(self, scope: str, playlist_id: int, scope_id: int) -> PlaylistFetchResult:
|
||||
"""Fetch a single playlist"""
|
||||
"""Fetch a single playlist."""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
@ -113,7 +113,7 @@ class PlaylistWrapper:
|
||||
async def fetch_all(
|
||||
self, scope: str, scope_id: int, author_id=None
|
||||
) -> List[PlaylistFetchResult]:
|
||||
"""Fetch all playlists"""
|
||||
"""Fetch all playlists."""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
output = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
@ -160,7 +160,7 @@ class PlaylistWrapper:
|
||||
async def fetch_all_converter(
|
||||
self, scope: str, playlist_name, playlist_id
|
||||
) -> List[PlaylistFetchResult]:
|
||||
"""Fetch all playlists with the specified filter"""
|
||||
"""Fetch all playlists with the specified filter."""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
try:
|
||||
playlist_id = int(playlist_id)
|
||||
@ -195,7 +195,7 @@ class PlaylistWrapper:
|
||||
return output
|
||||
|
||||
async def delete(self, scope: str, playlist_id: int, scope_id: int):
|
||||
"""Deletes a single playlists"""
|
||||
"""Deletes a single playlists."""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
@ -205,12 +205,12 @@ class PlaylistWrapper:
|
||||
)
|
||||
|
||||
async def delete_scheduled(self):
|
||||
"""Clean up database from all deleted playlists"""
|
||||
"""Clean up database from all deleted playlists."""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, self.statement.delete_scheduled)
|
||||
|
||||
async def drop(self, scope: str):
|
||||
"""Delete all playlists in a scope"""
|
||||
"""Delete all playlists in a scope."""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
@ -220,7 +220,7 @@ class PlaylistWrapper:
|
||||
)
|
||||
|
||||
async def create_table(self):
|
||||
"""Create the playlist table"""
|
||||
"""Create the playlist table."""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, PLAYLIST_CREATE_TABLE)
|
||||
|
||||
@ -234,7 +234,7 @@ class PlaylistWrapper:
|
||||
playlist_url: Optional[str],
|
||||
tracks: List[MutableMapping],
|
||||
):
|
||||
"""Insert or update a playlist into the database"""
|
||||
"""Insert or update a playlist into the database."""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import base64
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Mapping, MutableMapping, Optional, TYPE_CHECKING, Tuple, Union
|
||||
|
||||
from typing import TYPE_CHECKING, List, Mapping, MutableMapping, Optional, Tuple, Union
|
||||
|
||||
import aiohttp
|
||||
from redbot.core.i18n import Translator
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog, Context
|
||||
from redbot.core.i18n import Translator
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from ..errors import SpotifyFetchError
|
||||
|
||||
@ -46,7 +48,7 @@ class SpotifyWrapper:
|
||||
|
||||
@staticmethod
|
||||
def spotify_format_call(query_type: str, key: str) -> Tuple[str, MutableMapping]:
|
||||
"""Format the spotify endpoint"""
|
||||
"""Format the spotify endpoint."""
|
||||
params: MutableMapping = {}
|
||||
if query_type == "album":
|
||||
query = f"{ALBUMS_ENDPOINT}/{key}/tracks"
|
||||
@ -59,7 +61,7 @@ class SpotifyWrapper:
|
||||
async def get_spotify_track_info(
|
||||
self, track_data: MutableMapping, ctx: Context
|
||||
) -> Tuple[str, ...]:
|
||||
"""Extract track info from spotify response"""
|
||||
"""Extract track info from spotify response."""
|
||||
prefer_lyrics = await self.cog.get_lyrics_status(ctx)
|
||||
track_name = track_data["name"]
|
||||
if prefer_lyrics:
|
||||
@ -75,14 +77,14 @@ class SpotifyWrapper:
|
||||
|
||||
@staticmethod
|
||||
async def is_access_token_valid(token: MutableMapping) -> bool:
|
||||
"""Check if current token is not too old"""
|
||||
"""Check if current token is not too old."""
|
||||
return (token["expires_at"] - int(time.time())) < 60
|
||||
|
||||
@staticmethod
|
||||
def make_auth_header(
|
||||
client_id: Optional[str], client_secret: Optional[str]
|
||||
) -> MutableMapping[str, Union[str, int]]:
|
||||
"""Make Authorization header for spotify token"""
|
||||
"""Make Authorization header for spotify token."""
|
||||
if client_id is None:
|
||||
client_id = ""
|
||||
if client_secret is None:
|
||||
@ -93,11 +95,11 @@ class SpotifyWrapper:
|
||||
async def get(
|
||||
self, url: str, headers: MutableMapping = None, params: MutableMapping = None
|
||||
) -> MutableMapping[str, str]:
|
||||
"""Make a GET request to the spotify API"""
|
||||
"""Make a GET request to the spotify API."""
|
||||
if params is None:
|
||||
params = {}
|
||||
async with self.session.request("GET", url, params=params, headers=headers) as r:
|
||||
data = await r.json()
|
||||
data = await r.json(loads=json.loads)
|
||||
if r.status != 200:
|
||||
log.debug(f"Issue making GET request to {url}: [{r.status}] {data}")
|
||||
return data
|
||||
@ -106,7 +108,7 @@ class SpotifyWrapper:
|
||||
self._token = new_token
|
||||
|
||||
async def get_token(self) -> None:
|
||||
"""Get the stored spotify tokens"""
|
||||
"""Get the stored spotify tokens."""
|
||||
if not self._token:
|
||||
self._token = await self.bot.get_shared_api_tokens("spotify")
|
||||
|
||||
@ -114,10 +116,17 @@ class SpotifyWrapper:
|
||||
self.client_secret = self._token.get("client_secret", "")
|
||||
|
||||
async def get_country_code(self, ctx: Context = None) -> str:
|
||||
return await self.config.guild(ctx.guild).country_code() if ctx else "US"
|
||||
return (
|
||||
(
|
||||
await self.config.user(ctx.author).country_code()
|
||||
or await self.config.guild(ctx.guild).country_code()
|
||||
)
|
||||
if ctx
|
||||
else "US"
|
||||
)
|
||||
|
||||
async def request_access_token(self) -> MutableMapping:
|
||||
"""Make a spotify call to get the auth token"""
|
||||
"""Make a spotify call to get the auth token."""
|
||||
await self.get_token()
|
||||
payload = {"grant_type": "client_credentials"}
|
||||
headers = self.make_auth_header(self.client_id, self.client_secret)
|
||||
@ -125,7 +134,7 @@ class SpotifyWrapper:
|
||||
return r
|
||||
|
||||
async def get_access_token(self) -> Optional[str]:
|
||||
"""Get the access_token"""
|
||||
"""Get the access_token."""
|
||||
if self.spotify_token and not await self.is_access_token_valid(self.spotify_token):
|
||||
return self.spotify_token["access_token"]
|
||||
token = await self.request_access_token()
|
||||
@ -142,20 +151,20 @@ class SpotifyWrapper:
|
||||
async def post(
|
||||
self, url: str, payload: MutableMapping, headers: MutableMapping = None
|
||||
) -> MutableMapping:
|
||||
"""Make a POST call to spotify"""
|
||||
"""Make a POST call to spotify."""
|
||||
async with self.session.post(url, data=payload, headers=headers) as r:
|
||||
data = await r.json()
|
||||
data = await r.json(loads=json.loads)
|
||||
if r.status != 200:
|
||||
log.debug(f"Issue making POST request to {url}: [{r.status}] {data}")
|
||||
return data
|
||||
|
||||
async def make_get_call(self, url: str, params: MutableMapping) -> MutableMapping:
|
||||
"""Make a Get call to spotify"""
|
||||
"""Make a Get call to spotify."""
|
||||
token = await self.get_access_token()
|
||||
return await self.get(url, params=params, headers={"Authorization": f"Bearer {token}"})
|
||||
|
||||
async def get_categories(self, ctx: Context = None) -> List[MutableMapping]:
|
||||
"""Get the spotify categories"""
|
||||
"""Get the spotify categories."""
|
||||
country_code = await self.get_country_code(ctx=ctx)
|
||||
params: MutableMapping = {"country": country_code} if country_code else {}
|
||||
result = await self.make_get_call(CATEGORY_ENDPOINT, params=params)
|
||||
@ -171,7 +180,7 @@ class SpotifyWrapper:
|
||||
return [{c["name"]: c["id"]} for c in categories if c]
|
||||
|
||||
async def get_playlist_from_category(self, category: str, ctx: Context = None):
|
||||
"""Get spotify playlists for the specified category"""
|
||||
"""Get spotify playlists for the specified category."""
|
||||
url = f"{CATEGORY_ENDPOINT}/{category}/playlists"
|
||||
country_code = await self.get_country_code(ctx=ctx)
|
||||
params: MutableMapping = {"country": country_code} if country_code else {}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Mapping, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from typing import TYPE_CHECKING, Mapping, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
@ -33,15 +35,17 @@ class YouTubeWrapper:
|
||||
def update_token(self, new_token: Mapping[str, str]):
|
||||
self._token = new_token
|
||||
|
||||
async def _get_api_key(self) -> str:
|
||||
"""Get the stored youtube token"""
|
||||
async def _get_api_key(
|
||||
self,
|
||||
) -> str:
|
||||
"""Get the stored youtube token."""
|
||||
if not self._token:
|
||||
self._token = await self.bot.get_shared_api_tokens("youtube")
|
||||
self.api_key = self._token.get("api_key", "")
|
||||
return self.api_key if self.api_key is not None else ""
|
||||
|
||||
async def get_call(self, query: str) -> Optional[str]:
|
||||
"""Make a Get call to youtube data api"""
|
||||
"""Make a Get call to youtube data api."""
|
||||
params = {
|
||||
"q": query,
|
||||
"part": "id",
|
||||
@ -57,7 +61,7 @@ class YouTubeWrapper:
|
||||
raise YouTubeApiError("Your YouTube Data API quota has been reached.")
|
||||
return None
|
||||
else:
|
||||
search_response = await r.json()
|
||||
search_response = await r.json(loads=json.loads)
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
return f"https://www.youtube.com/watch?v={search_result['id']['videoId']}"
|
||||
|
||||
@ -5,21 +5,23 @@ import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
|
||||
from pathlib import Path, PosixPath, WindowsPath
|
||||
from typing import (
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Final,
|
||||
Iterator,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Pattern,
|
||||
Tuple,
|
||||
Union,
|
||||
Callable,
|
||||
Pattern,
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import lavalink
|
||||
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
_RE_REMOVE_START: Final[Pattern] = re.compile(r"^(sc|list) ")
|
||||
@ -80,8 +82,8 @@ log = logging.getLogger("red.cogs.Audio.audio_dataclasses")
|
||||
class LocalPath:
|
||||
"""Local tracks class.
|
||||
|
||||
Used to handle system dir trees in a cross system manner.
|
||||
The only use of this class is for `localtracks`.
|
||||
Used to handle system dir trees in a cross system manner. The only use of this class is for
|
||||
`localtracks`.
|
||||
"""
|
||||
|
||||
_all_music_ext = _FULLY_SUPPORTED_MUSIC_EXT + _PARTIALLY_SUPPORTED_MUSIC_EXT
|
||||
@ -335,6 +337,7 @@ class Query:
|
||||
self.is_mixer: bool = kwargs.get("mixer", False)
|
||||
self.is_twitch: bool = kwargs.get("twitch", False)
|
||||
self.is_other: bool = kwargs.get("other", False)
|
||||
self.is_pornhub: bool = kwargs.get("pornhub", False)
|
||||
self.is_playlist: bool = kwargs.get("playlist", False)
|
||||
self.is_album: bool = kwargs.get("album", False)
|
||||
self.is_search: bool = kwargs.get("search", False)
|
||||
@ -350,7 +353,6 @@ class Query:
|
||||
|
||||
self.start_time: int = kwargs.get("start_time", 0)
|
||||
self.track_index: Optional[int] = kwargs.get("track_index", None)
|
||||
|
||||
if self.invoked_from == "sc search":
|
||||
self.is_youtube = False
|
||||
self.is_soundcloud = True
|
||||
@ -403,8 +405,7 @@ class Query:
|
||||
_local_folder_current_path: Path,
|
||||
**kwargs,
|
||||
) -> "Query":
|
||||
"""
|
||||
Process the input query into its type
|
||||
"""Process the input query into its type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@ -442,7 +443,7 @@ class Query:
|
||||
|
||||
@staticmethod
|
||||
def _parse(track, _local_folder_current_path: Path, **kwargs) -> MutableMapping:
|
||||
"""Parse a track into all the relevant metadata"""
|
||||
"""Parse a track into all the relevant metadata."""
|
||||
returning: MutableMapping = {}
|
||||
if (
|
||||
type(track) == type(LocalPath)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from typing import Final
|
||||
|
||||
IS_DEBUG: Final[bool] = "--debug" in sys.argv
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import argparse
|
||||
import functools
|
||||
import re
|
||||
from typing import Final, MutableMapping, Optional, Tuple, Union, Pattern
|
||||
|
||||
from typing import Final, MutableMapping, Optional, Pattern, Tuple, Union
|
||||
|
||||
import discord
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from .apis.api_utils import standardize_scope
|
||||
from .apis.playlist_interface import get_all_playlist_converter
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)):
|
||||
|
||||
@ -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."),
|
||||
)
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."
|
||||
),
|
||||
)
|
||||
|
||||
@ -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."
|
||||
),
|
||||
)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
from typing import List
|
||||
|
||||
import discord
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 []
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
35
redbot/cogs/audio/core/utilities/parsers.py
Normal file
35
redbot/cogs/audio/core/utilities/parsers.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
),
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import asyncio.subprocess # disables for # https://github.com/PyCQA/pylint/issues/1469
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import platform
|
||||
@ -9,14 +10,16 @@ import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import ClassVar, Final, List, Optional, Tuple, Pattern
|
||||
|
||||
from typing import ClassVar, Final, List, Optional, Pattern, Tuple
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
from redbot.core import data_manager
|
||||
from tqdm import tqdm
|
||||
|
||||
from .errors import LavalinkDownloadFailed
|
||||
from .utils import task_callback
|
||||
|
||||
log = logging.getLogger("red.audio.manager")
|
||||
JAR_VERSION: Final[str] = "3.3.1"
|
||||
@ -57,6 +60,7 @@ class ServerManager:
|
||||
_jvm: ClassVar[Optional[str]] = None
|
||||
_lavalink_branch: ClassVar[Optional[str]] = None
|
||||
_buildtime: ClassVar[Optional[str]] = None
|
||||
_java_exc: ClassVar[str] = "java"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.ready: asyncio.Event = asyncio.Event()
|
||||
@ -65,6 +69,10 @@ class ServerManager:
|
||||
self._monitor_task: Optional[asyncio.Task] = None
|
||||
self._shutdown: bool = False
|
||||
|
||||
@property
|
||||
def path(self) -> Optional[str]:
|
||||
return self._java_exc
|
||||
|
||||
@property
|
||||
def jvm(self) -> Optional[str]:
|
||||
return self._jvm
|
||||
@ -85,8 +93,9 @@ class ServerManager:
|
||||
def build_time(self) -> Optional[str]:
|
||||
return self._buildtime
|
||||
|
||||
async def start(self) -> None:
|
||||
async def start(self, java_path: str) -> None:
|
||||
arch_name = platform.machine()
|
||||
self._java_exc = java_path
|
||||
if arch_name in self._blacklisted_archs:
|
||||
raise asyncio.CancelledError(
|
||||
"You are attempting to run Lavalink audio on an unsupported machine architecture."
|
||||
@ -121,6 +130,7 @@ class ServerManager:
|
||||
log.warning("Timeout occurred whilst waiting for internal Lavalink server to be ready")
|
||||
|
||||
self._monitor_task = asyncio.create_task(self._monitor())
|
||||
self._monitor_task.add_done_callback(task_callback)
|
||||
|
||||
async def _get_jar_args(self) -> List[str]:
|
||||
(java_available, java_version) = await self._has_java()
|
||||
@ -128,27 +138,36 @@ class ServerManager:
|
||||
if not java_available:
|
||||
raise RuntimeError("You must install Java 11 for Lavalink to run.")
|
||||
|
||||
return ["java", "-Djdk.tls.client.protocols=TLSv1.2", "-jar", str(LAVALINK_JAR_FILE)]
|
||||
return [
|
||||
self._java_exc,
|
||||
"-Djdk.tls.client.protocols=TLSv1.2",
|
||||
"-jar",
|
||||
str(LAVALINK_JAR_FILE),
|
||||
]
|
||||
|
||||
async def _has_java(self) -> Tuple[bool, Optional[Tuple[int, int]]]:
|
||||
if self._java_available is not None:
|
||||
# Return cached value if we've checked this before
|
||||
return self._java_available, self._java_version
|
||||
java_available = shutil.which("java") is not None
|
||||
java_exec = shutil.which(self._java_exc)
|
||||
java_available = java_exec is not None
|
||||
if not java_available:
|
||||
self.java_available = False
|
||||
self.java_version = None
|
||||
else:
|
||||
self._java_version = version = await self._get_java_version()
|
||||
self._java_available = (11, 0) <= version < (12, 0)
|
||||
self._java_exc = java_exec
|
||||
return self._java_available, self._java_version
|
||||
|
||||
@staticmethod
|
||||
async def _get_java_version() -> Tuple[int, int]:
|
||||
async def _get_java_version(self) -> Tuple[int, int]:
|
||||
"""This assumes we've already checked that java exists."""
|
||||
_proc: asyncio.subprocess.Process = (
|
||||
await asyncio.create_subprocess_exec( # pylint:disable=no-member
|
||||
"java", "-version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
self._java_exc,
|
||||
"-version",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
)
|
||||
# java -version outputs to stderr
|
||||
@ -171,10 +190,7 @@ class ServerManager:
|
||||
elif short_match:
|
||||
return int(short_match["major"]), 0
|
||||
|
||||
raise RuntimeError(
|
||||
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
||||
"issue tracker."
|
||||
)
|
||||
raise RuntimeError(f"The output of `{self._java_exc} -version` was unexpected.")
|
||||
|
||||
async def _wait_for_launcher(self) -> None:
|
||||
log.debug("Waiting for Lavalink server to be ready")
|
||||
@ -202,7 +218,7 @@ class ServerManager:
|
||||
log.info("Internal Lavalink jar shutdown unexpectedly")
|
||||
if not self._has_java_error():
|
||||
log.info("Restarting internal Lavalink server")
|
||||
await self.start()
|
||||
await self.start(self._java_exc)
|
||||
else:
|
||||
log.critical(
|
||||
"Your Java is borked. Please find the hs_err_pid%d.log file"
|
||||
@ -228,7 +244,7 @@ class ServerManager:
|
||||
|
||||
async def _download_jar(self) -> None:
|
||||
log.info("Downloading Lavalink.jar...")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(json_serialize=json.dumps) as session:
|
||||
async with session.get(LAVALINK_DOWNLOAD_URL) as response:
|
||||
if response.status == 404:
|
||||
# A 404 means our LAVALINK_DOWNLOAD_URL is invalid, so likely the jar version
|
||||
|
||||
@ -54,6 +54,15 @@ __all__ = [
|
||||
"LAVALINK_QUERY_LAST_FETCHED_RANDOM",
|
||||
"LAVALINK_DELETE_OLD_ENTRIES",
|
||||
"LAVALINK_FETCH_ALL_ENTRIES_GLOBAL",
|
||||
# Persisting Queue statements
|
||||
"PERSIST_QUEUE_DROP_TABLE",
|
||||
"PERSIST_QUEUE_CREATE_TABLE",
|
||||
"PERSIST_QUEUE_CREATE_INDEX",
|
||||
"PERSIST_QUEUE_PLAYED",
|
||||
"PERSIST_QUEUE_DELETE_SCHEDULED",
|
||||
"PERSIST_QUEUE_FETCH_ALL",
|
||||
"PERSIST_QUEUE_UPSERT",
|
||||
"PERSIST_QUEUE_BULK_PLAYED",
|
||||
]
|
||||
|
||||
# PRAGMA Statements
|
||||
@ -555,3 +564,83 @@ LAVALINK_FETCH_ALL_ENTRIES_GLOBAL: Final[
|
||||
SELECT query, data
|
||||
FROM lavalink
|
||||
"""
|
||||
|
||||
# Persisting Queue statements
|
||||
PERSIST_QUEUE_DROP_TABLE: Final[
|
||||
str
|
||||
] = """
|
||||
DROP TABLE IF EXISTS persist_queue ;
|
||||
"""
|
||||
PERSIST_QUEUE_CREATE_TABLE: Final[
|
||||
str
|
||||
] = """
|
||||
CREATE TABLE IF NOT EXISTS persist_queue(
|
||||
guild_id INTEGER NOT NULL,
|
||||
room_id INTEGER NOT NULL,
|
||||
track JSON NOT NULL,
|
||||
played BOOLEAN DEFAULT false,
|
||||
track_id TEXT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
PRIMARY KEY (guild_id, room_id, track_id)
|
||||
);
|
||||
"""
|
||||
PERSIST_QUEUE_CREATE_INDEX: Final[
|
||||
str
|
||||
] = """
|
||||
CREATE INDEX IF NOT EXISTS track_index ON persist_queue (guild_id, track_id);
|
||||
"""
|
||||
PERSIST_QUEUE_PLAYED: Final[
|
||||
str
|
||||
] = """
|
||||
UPDATE persist_queue
|
||||
SET
|
||||
played = true
|
||||
WHERE
|
||||
(
|
||||
guild_id = :guild_id
|
||||
AND track_id = :track_id
|
||||
)
|
||||
;
|
||||
"""
|
||||
PERSIST_QUEUE_BULK_PLAYED: Final[
|
||||
str
|
||||
] = """
|
||||
UPDATE persist_queue
|
||||
SET
|
||||
played = true
|
||||
WHERE guild_id = :guild_id
|
||||
;
|
||||
"""
|
||||
PERSIST_QUEUE_DELETE_SCHEDULED: Final[
|
||||
str
|
||||
] = """
|
||||
DELETE
|
||||
FROM
|
||||
persist_queue
|
||||
WHERE
|
||||
played = true;
|
||||
"""
|
||||
PERSIST_QUEUE_FETCH_ALL: Final[
|
||||
str
|
||||
] = """
|
||||
SELECT
|
||||
guild_id, room_id, track
|
||||
FROM
|
||||
persist_queue
|
||||
WHERE played = false
|
||||
ORDER BY time ASC;
|
||||
"""
|
||||
PERSIST_QUEUE_UPSERT: Final[
|
||||
str
|
||||
] = """
|
||||
INSERT INTO
|
||||
persist_queue (guild_id, room_id, track, played, track_id, time)
|
||||
VALUES
|
||||
(
|
||||
:guild_id, :room_id, :track, :played, :track_id, :time
|
||||
)
|
||||
ON CONFLICT (guild_id, room_id, track_id) DO
|
||||
UPDATE
|
||||
SET
|
||||
time = excluded.time
|
||||
"""
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import time
|
||||
|
||||
from enum import Enum, unique
|
||||
from typing import MutableMapping
|
||||
|
||||
@ -6,6 +10,8 @@ import discord
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.task.callback")
|
||||
|
||||
|
||||
class CacheLevel:
|
||||
__slots__ = ("value",)
|
||||
@ -205,3 +211,9 @@ class PlaylistScope(Enum):
|
||||
@staticmethod
|
||||
def list():
|
||||
return list(map(lambda c: c.value, PlaylistScope))
|
||||
|
||||
|
||||
def task_callback(task: asyncio.Task) -> None:
|
||||
with contextlib.suppress(asyncio.CancelledError, asyncio.InvalidStateError):
|
||||
if exc := task.exception():
|
||||
log.exception(f"{task.get_name()} raised an Exception", exc_info=exc)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user