Audio Cog - v2.3.0 (#4446)

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

* Add a toggle for auto deafen

* Add a one off Send to Owners

* aaaaaaa

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

* Apply suggestions from code review

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

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

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

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

* ffs

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

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

* Aikas Review

* Add #3995 in here

* update

* *sigh*

* lock behind owner

* to help with debugging

* Revert "to help with debugging"

This reverts commit 8cbf17be

* resolve last review

Co-authored-by: Vuks <51289041+Vuks69@users.noreply.github.com>
Co-authored-by: Twentysix <Twentysix26@users.noreply.github.com>
This commit is contained in:
Draper 2020-10-12 19:39:39 +01:00 committed by GitHub
parent 29ebf0f060
commit 2da9b502d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1553 additions and 331 deletions

View File

@ -6,6 +6,7 @@ from dataclasses import dataclass, field
from typing import List, MutableMapping, Optional, Union from typing import List, MutableMapping, Optional, Union
import discord import discord
import lavalink
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import humanize_list from redbot.core.utils.chat_formatting import humanize_list
@ -74,8 +75,22 @@ class PlaylistFetchResult:
self.tracks = json.loads(self.tracks) 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: 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() scope = scope.upper()
valid_scopes = ["GLOBAL", "GUILD", "AUTHOR", "USER", "SERVER", "MEMBER", "BOT"] valid_scopes = ["GLOBAL", "GUILD", "AUTHOR", "USER", "SERVER", "MEMBER", "BOT"]
@ -103,7 +118,7 @@ def prepare_config_scope(
author: Union[discord.abc.User, int] = None, author: Union[discord.abc.User, int] = None,
guild: Union[discord.Guild, int] = None, guild: Union[discord.Guild, int] = None,
): ):
"""Return the scope used by Playlists""" """Return the scope used by Playlists."""
scope = standardize_scope(scope) scope = standardize_scope(scope)
if scope == PlaylistScope.GLOBAL.value: if scope == PlaylistScope.GLOBAL.value:
config_scope = [PlaylistScope.GLOBAL.value, bot.user.id] 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 ? 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 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) scope = standardize_scope(scope)
if scope == PlaylistScope.GLOBAL.value: if scope == PlaylistScope.GLOBAL.value:

View File

@ -1,8 +1,10 @@
import asyncio import asyncio
import contextlib import contextlib
import json
import logging 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 import aiohttp
from lavalink.rest_api import LoadResult from lavalink.rest_api import LoadResult
@ -17,7 +19,7 @@ from ..audio_logging import IS_DEBUG, debug_exc_log
if TYPE_CHECKING: if TYPE_CHECKING:
from .. import Audio from .. import Audio
_API_URL = "https://redbot.app/" _API_URL = "https://api.redbot.app/"
log = logging.getLogger("red.cogs.Audio.api.GlobalDB") log = logging.getLogger("red.cogs.Audio.api.GlobalDB")
@ -32,11 +34,150 @@ class GlobalCacheWrapper:
self.session = session self.session = session
self.api_key = None self.api_key = None
self._handshake_token = "" self._handshake_token = ""
self.can_write = False
self._handshake_token = ""
self.has_api_key = None self.has_api_key = None
self._token: Mapping[str, str] = {} self._token: Mapping[str, str] = {}
self.cog = cog self.cog = cog
def update_token(self, new_token: Mapping[str, str]): def update_token(self, new_token: Mapping[str, str]):
self._token = new_token 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

View File

@ -1,30 +1,34 @@
import asyncio import asyncio
import contextlib
import datetime import datetime
import json import json
import logging import logging
import random import random
import time import time
from collections import namedtuple 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 aiohttp
import discord import discord
import lavalink 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 import Config, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.commands import Cog, Context from redbot.core.commands import Cog, Context
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper from redbot.core.utils.dbtools import APSWConnectionWrapper
from ..audio_dataclasses import Query from ..audio_dataclasses import Query
from ..audio_logging import IS_DEBUG, debug_exc_log from ..audio_logging import IS_DEBUG, debug_exc_log
from ..errors import DatabaseError, SpotifyFetchError, TrackEnqueueError from ..errors import DatabaseError, SpotifyFetchError, TrackEnqueueError
from ..utils import CacheLevel, Notifier from ..utils import CacheLevel, Notifier
from .api_utils import LavalinkCacheFetchForGlobalResult
from .global_db import GlobalCacheWrapper from .global_db import GlobalCacheWrapper
from .local_db import LocalCacheWrapper from .local_db import LocalCacheWrapper
from .persist_queue_wrapper import QueueInterface
from .playlist_interface import get_playlist from .playlist_interface import get_playlist
from .playlist_wrapper import PlaylistWrapper from .playlist_wrapper import PlaylistWrapper
from .spotify import SpotifyWrapper from .spotify import SpotifyWrapper
@ -36,6 +40,7 @@ if TYPE_CHECKING:
_ = Translator("Audio", __file__) _ = Translator("Audio", __file__)
log = logging.getLogger("red.cogs.Audio.api.AudioAPIInterface") log = logging.getLogger("red.cogs.Audio.api.AudioAPIInterface")
_TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK" _TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK"
# TODO: Get random from global Cache
class AudioAPIInterface: class AudioAPIInterface:
@ -60,20 +65,22 @@ class AudioAPIInterface:
self.youtube_api: YouTubeWrapper = YouTubeWrapper(self.bot, self.config, session, self.cog) 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.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.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._session: aiohttp.ClientSession = session
self._tasks: MutableMapping = {} self._tasks: MutableMapping = {}
self._lock: asyncio.Lock = asyncio.Lock() self._lock: asyncio.Lock = asyncio.Lock()
async def initialize(self) -> None: async def initialize(self) -> None:
"""Initialises the Local Cache connection""" """Initialises the Local Cache connection."""
await self.local_cache_api.lavalink.init() await self.local_cache_api.lavalink.init()
await self.persistent_queue_api.init()
def close(self) -> None: def close(self) -> None:
"""Closes the Local Cache connection""" """Closes the Local Cache connection."""
self.local_cache_api.lavalink.close() self.local_cache_api.lavalink.close()
async def get_random_track_from_db(self) -> Optional[MutableMapping]: async def get_random_track_from_db(self, tries=0) -> Optional[MutableMapping]:
"""Get a random track from the local database and return it""" """Get a random track from the local database and return it."""
track: Optional[MutableMapping] = {} track: Optional[MutableMapping] = {}
try: try:
query_data = {} query_data = {}
@ -106,7 +113,7 @@ class AudioAPIInterface:
action_type: str = None, action_type: str = None,
data: Union[List[MutableMapping], MutableMapping] = None, data: Union[List[MutableMapping], MutableMapping] = None,
) -> 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: if not data:
return return
@ -126,9 +133,11 @@ class AudioAPIInterface:
await self.local_cache_api.youtube.update(data) await self.local_cache_api.youtube.update(data)
elif table == "spotify": elif table == "spotify":
await self.local_cache_api.spotify.update(data) 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: 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: if message_id is not None:
lock_id = message_id lock_id = message_id
elif ctx is not None: elif ctx is not None:
@ -143,7 +152,7 @@ class AudioAPIInterface:
try: try:
tasks = self._tasks[lock_id] tasks = self._tasks[lock_id]
tasks = [self.route_tasks(a, tasks[a]) for a in tasks] 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] del self._tasks[lock_id]
except Exception as exc: except Exception as exc:
debug_exc_log( debug_exc_log(
@ -154,7 +163,7 @@ class AudioAPIInterface:
log.debug(f"Completed database writes for {lock_id} ({lock_author})") log.debug(f"Completed database writes for {lock_id} ({lock_author})")
async def run_all_pending_tasks(self) -> None: 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: async with self._lock:
if IS_DEBUG: if IS_DEBUG:
log.debug("Running pending writes to database") log.debug("Running pending writes to database")
@ -166,7 +175,7 @@ class AudioAPIInterface:
self._tasks = {} self._tasks = {}
coro_tasks = [self.route_tasks(a, tasks[a]) for a in 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: except Exception as exc:
debug_exc_log(log, exc, "Failed database writes") debug_exc_log(log, exc, "Failed database writes")
@ -175,7 +184,7 @@ class AudioAPIInterface:
log.debug("Completed pending writes to database have finished") log.debug("Completed pending writes to database have finished")
def append_task(self, ctx: commands.Context, event: str, task: Tuple, _id: int = None) -> None: 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 lock_id = _id or ctx.message.id
if lock_id not in self._tasks: if lock_id not in self._tasks:
self._tasks[lock_id] = {"update": [], "insert": [], "global": []} self._tasks[lock_id] = {"update": [], "insert": [], "global": []}
@ -190,7 +199,7 @@ class AudioAPIInterface:
skip_youtube: bool = False, skip_youtube: bool = False,
current_cache_level: CacheLevel = CacheLevel.none(), current_cache_level: CacheLevel = CacheLevel.none(),
) -> List[str]: ) -> List[str]:
"""Return youtube URLS for the spotify URL provided""" """Return youtube URLS for the spotify URL provided."""
youtube_urls = [] youtube_urls = []
tracks = await self.fetch_from_spotify_api( tracks = await self.fetch_from_spotify_api(
query_type, uri, params=None, notifier=notifier, ctx=ctx query_type, uri, params=None, notifier=notifier, ctx=ctx
@ -266,7 +275,7 @@ class AudioAPIInterface:
notifier: Optional[Notifier] = None, notifier: Optional[Notifier] = None,
ctx: Context = None, ctx: Context = None,
) -> Union[List[MutableMapping], List[str]]: ) -> Union[List[MutableMapping], List[str]]:
"""Gets track info from spotify API""" """Gets track info from spotify API."""
if recursive is False: if recursive is False:
(call, params) = self.spotify_api.spotify_format_call(query_type, uri) (call, params) = self.spotify_api.spotify_format_call(query_type, uri)
@ -394,9 +403,10 @@ class AudioAPIInterface:
lock: Callable, lock: Callable,
notifier: Optional[Notifier] = None, notifier: Optional[Notifier] = None,
forced: bool = False, forced: bool = False,
query_global: bool = False, query_global: bool = True,
) -> List[lavalink.Track]: ) -> 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 Parameters
---------- ----------
@ -423,7 +433,9 @@ class AudioAPIInterface:
List[str] List[str]
List of Youtube URLs. 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 = [] track_list: List = []
has_not_allowed = False has_not_allowed = False
try: try:
@ -485,7 +497,14 @@ class AudioAPIInterface:
) )
except Exception as exc: except Exception as exc:
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table") 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: if val is None:
val = await self.fetch_youtube_query( val = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level ctx, track_info, current_cache_level=current_cache_level
@ -494,34 +513,44 @@ class AudioAPIInterface:
task = ("update", ("youtube", {"track": track_info})) task = ("update", ("youtube", {"track": track_info}))
self.append_task(ctx, *task) self.append_task(ctx, *task)
if llresponse is not None: if isinstance(llresponse, LoadResult):
track_object = llresponse.tracks track_object = llresponse.tracks
elif val: elif val:
try: result = None
(result, called_api) = await self.fetch_track( if should_query_global:
ctx, llresponse = await self.global_cache_api.get_call(val)
player, if llresponse:
Query.process_input(val, self.cog.local_folder_current_path), if llresponse.get("loadType") == "V2_COMPACT":
forced=forced, llresponse["loadType"] = "V2_COMPAT"
) llresponse = LoadResult(llresponse)
except (RuntimeError, aiohttp.ServerDisconnectedError): result = llresponse or None
lock(ctx, False) if not result:
error_embed = discord.Embed( try:
colour=await ctx.embed_colour(), (result, called_api) = await self.fetch_track(
title=_("The connection was reset while loading the playlist."), ctx,
) player,
if notifier is not None: Query.process_input(val, self.cog.local_folder_current_path),
await notifier.update_embed(error_embed) forced=forced,
break should_query_global=not should_query_global,
except asyncio.TimeoutError: )
lock(ctx, False) except (RuntimeError, aiohttp.ServerDisconnectedError):
error_embed = discord.Embed( lock(ctx, False)
colour=await ctx.embed_colour(), error_embed = discord.Embed(
title=_("Player timeout, skipping remaining tracks."), 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) if notifier is not None:
break 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 track_object = result.tracks
else: else:
track_object = [] track_object = []
@ -538,7 +567,7 @@ class AudioAPIInterface:
seconds=seconds, seconds=seconds,
) )
if consecutive_fails >= 10: if consecutive_fails >= (100 if global_entry else 10):
error_embed = discord.Embed( error_embed = discord.Embed(
colour=await ctx.embed_colour(), colour=await ctx.embed_colour(),
title=_("Failing to get tracks, skipping remaining."), title=_("Failing to get tracks, skipping remaining."),
@ -551,13 +580,12 @@ class AudioAPIInterface:
continue continue
consecutive_fails = 0 consecutive_fails = 0
single_track = track_object[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( if not await self.cog.is_query_allowed(
self.config, self.config,
ctx.guild, ctx,
( f"{single_track.title} {single_track.author} {single_track.uri} {query}",
f"{single_track.title} {single_track.author} {single_track.uri} " query_obj=query,
f"{Query.process_input(single_track, self.cog.local_folder_current_path)}"
),
): ):
has_not_allowed = True has_not_allowed = True
if IS_DEBUG: if IS_DEBUG:
@ -570,6 +598,13 @@ class AudioAPIInterface:
if guild_data["maxlength"] > 0: if guild_data["maxlength"] > 0:
if self.cog.is_track_length_allowed(single_track, guild_data["maxlength"]): if self.cog.is_track_length_allowed(single_track, guild_data["maxlength"]):
enqueued_tracks += 1 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) player.add(ctx.author, single_track)
self.bot.dispatch( self.bot.dispatch(
"red_audio_track_enqueue", "red_audio_track_enqueue",
@ -579,6 +614,13 @@ class AudioAPIInterface:
) )
else: else:
enqueued_tracks += 1 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) player.add(ctx.author, single_track)
self.bot.dispatch( self.bot.dispatch(
"red_audio_track_enqueue", "red_audio_track_enqueue",
@ -642,9 +684,7 @@ class AudioAPIInterface:
track_info: str, track_info: str,
current_cache_level: CacheLevel = CacheLevel.none(), current_cache_level: CacheLevel = CacheLevel.none(),
) -> Optional[str]: ) -> 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) track_url = await self.youtube_api.get_call(track_info)
if CacheLevel.set_youtube().is_subset(current_cache_level) and track_url: if CacheLevel.set_youtube().is_subset(current_cache_level) and track_url:
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
@ -668,9 +708,7 @@ class AudioAPIInterface:
async def fetch_from_youtube_api( async def fetch_from_youtube_api(
self, ctx: commands.Context, track_info: str self, ctx: commands.Context, track_info: str
) -> Optional[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()) current_cache_level = CacheLevel(await self.config.cache_level())
cache_enabled = CacheLevel.set_youtube().is_subset(current_cache_level) cache_enabled = CacheLevel.set_youtube().is_subset(current_cache_level)
val = None val = None
@ -727,6 +765,7 @@ class AudioAPIInterface:
val = None val = None
query = Query.process_input(query, self.cog.local_folder_current_path) query = Query.process_input(query, self.cog.local_folder_current_path)
query_string = str(query) query_string = str(query)
globaldb_toggle = await self.config.global_db_enabled()
valid_global_entry = False valid_global_entry = False
results = None results = None
called_api = False called_api = False
@ -754,7 +793,31 @@ class AudioAPIInterface:
called_api = False called_api = False
else: else:
val = None 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: if valid_global_entry:
pass pass
elif lazy is True: elif lazy is True:
@ -769,6 +832,7 @@ class AudioAPIInterface:
if results.has_error: if results.has_error:
# If cached value has an invalid entry make a new call so that it gets updated # 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) results, called_api = await self.fetch_track(ctx, player, query, forced=True)
valid_global_entry = False
else: else:
if IS_DEBUG: if IS_DEBUG:
log.debug(f"Querying Lavalink api for {query_string}") log.debug(f"Querying Lavalink api for {query_string}")
@ -781,7 +845,19 @@ class AudioAPIInterface:
raise TrackEnqueueError raise TrackEnqueueError
if results is None: if results is None:
results = LoadResult({"loadType": "LOAD_FAILED", "playlistInfo": {}, "tracks": []}) 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 ( if (
cache_enabled cache_enabled
and results.load_type and results.load_type
@ -817,9 +893,7 @@ class AudioAPIInterface:
return results, called_api return results, called_api
async def autoplay(self, player: lavalink.Player, playlist_api: PlaylistWrapper): 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() autoplaylist = await self.config.guild(player.channel.guild).autoplaylist()
current_cache_level = CacheLevel(await self.config.cache_level()) current_cache_level = CacheLevel(await self.config.cache_level())
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level) cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
@ -865,19 +939,18 @@ class AudioAPIInterface:
track = random.choice(tracks) track = random.choice(tracks)
query = Query.process_input(track, self.cog.local_folder_current_path) query = Query.process_input(track, self.cog.local_folder_current_path)
await asyncio.sleep(0.001) await asyncio.sleep(0.001)
if not query.valid or ( if (not query.valid) or (
query.is_local query.is_local
and query.local_track_path is not None and query.local_track_path is not None
and not query.local_track_path.exists() and not query.local_track_path.exists()
): ):
continue continue
notify_channel = self.bot.get_channel(player.fetch("channel"))
if not await self.cog.is_query_allowed( if not await self.cog.is_query_allowed(
self.config, self.config,
player.channel.guild, notify_channel,
( f"{track.title} {track.author} {track.uri} {query}",
f"{track.title} {track.author} {track.uri} " query_obj=query,
f"{str(Query.process_input(track, self.cog.local_folder_current_path))}"
),
): ):
if IS_DEBUG: if IS_DEBUG:
log.debug( log.debug(
@ -886,11 +959,20 @@ class AudioAPIInterface:
) )
continue continue
valid = True valid = True
track.extras.update(
track.extras["autoplay"] = True {
"autoplay": True,
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": player.channel.guild.me.id,
}
)
player.add(player.channel.guild.me, track) player.add(player.channel.guild.me, track)
self.bot.dispatch( self.bot.dispatch(
"red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me "red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me
) )
if not player.current: if not player.current:
await player.play() await player.play()
async def fetch_all_contribute(self) -> List[LavalinkCacheFetchForGlobalResult]:
return await self.local_cache_api.lavalink.fetch_all_for_global()

View File

@ -4,14 +4,14 @@ import datetime
import logging import logging
import random import random
import time 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 import Config
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.commands import Cog from redbot.core.commands import Cog
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper from redbot.core.utils.dbtools import APSWConnectionWrapper
from ..audio_logging import debug_exc_log 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_random = LAVALINK_QUERY_LAST_FETCHED_RANDOM
self.statement.get_all_global = LAVALINK_FETCH_ALL_ENTRIES_GLOBAL self.statement.get_all_global = LAVALINK_FETCH_ALL_ENTRIES_GLOBAL
self.fetch_result = LavalinkCacheFetchResult self.fetch_result = LavalinkCacheFetchResult
self.fetch_for_global: Optional[Callable] = None self.fetch_for_global: Optional[Callable] = LavalinkCacheFetchForGlobalResult
async def fetch_one( async def fetch_one(
self, values: MutableMapping self, values: MutableMapping

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

View File

@ -1,12 +1,13 @@
import logging import logging
from typing import List, MutableMapping, Optional, Union from typing import List, MutableMapping, Optional, Union
import discord import discord
import lavalink import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import Config, commands from redbot.core import Config, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils import AsyncIter
from ..errors import NotAllowed from ..errors import NotAllowed
from ..utils import PlaylistScope from ..utils import PlaylistScope

View File

@ -1,17 +1,18 @@
import concurrent import concurrent
import json import json
import logging import logging
from types import SimpleNamespace from types import SimpleNamespace
from typing import List, MutableMapping, Optional from typing import List, MutableMapping, Optional
from redbot.core.utils import AsyncIter
from redbot.core import Config from redbot.core import Config
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper from redbot.core.utils.dbtools import APSWConnectionWrapper
from ..audio_logging import debug_exc_log from ..audio_logging import debug_exc_log
from ..sql_statements import ( from ..sql_statements import (
HANDLE_DISCORD_DATA_DELETION_QUERY,
PLAYLIST_CREATE_INDEX, PLAYLIST_CREATE_INDEX,
PLAYLIST_CREATE_TABLE, PLAYLIST_CREATE_TABLE,
PLAYLIST_DELETE, PLAYLIST_DELETE,
@ -27,7 +28,6 @@ from ..sql_statements import (
PRAGMA_SET_read_uncommitted, PRAGMA_SET_read_uncommitted,
PRAGMA_SET_temp_store, PRAGMA_SET_temp_store,
PRAGMA_SET_user_version, PRAGMA_SET_user_version,
HANDLE_DISCORD_DATA_DELETION_QUERY,
) )
from ..utils import PlaylistScope from ..utils import PlaylistScope
from .api_utils import PlaylistFetchResult from .api_utils import PlaylistFetchResult
@ -62,7 +62,7 @@ class PlaylistWrapper:
self.statement.drop_user_playlists = HANDLE_DISCORD_DATA_DELETION_QUERY self.statement.drop_user_playlists = HANDLE_DISCORD_DATA_DELETION_QUERY
async def init(self) -> None: async def init(self) -> None:
"""Initialize the Playlist table""" """Initialize the Playlist table."""
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: 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_temp_store)
executor.submit(self.database.cursor().execute, self.statement.pragma_journal_mode) executor.submit(self.database.cursor().execute, self.statement.pragma_journal_mode)
@ -72,7 +72,7 @@ class PlaylistWrapper:
@staticmethod @staticmethod
def get_scope_type(scope: str) -> int: 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: if scope == PlaylistScope.GLOBAL.value:
table = 1 table = 1
elif scope == PlaylistScope.USER.value: elif scope == PlaylistScope.USER.value:
@ -82,7 +82,7 @@ class PlaylistWrapper:
return table return table
async def fetch(self, scope: str, playlist_id: int, scope_id: int) -> PlaylistFetchResult: 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) scope_type = self.get_scope_type(scope)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
@ -113,7 +113,7 @@ class PlaylistWrapper:
async def fetch_all( async def fetch_all(
self, scope: str, scope_id: int, author_id=None self, scope: str, scope_id: int, author_id=None
) -> List[PlaylistFetchResult]: ) -> List[PlaylistFetchResult]:
"""Fetch all playlists""" """Fetch all playlists."""
scope_type = self.get_scope_type(scope) scope_type = self.get_scope_type(scope)
output = [] output = []
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
@ -160,7 +160,7 @@ class PlaylistWrapper:
async def fetch_all_converter( async def fetch_all_converter(
self, scope: str, playlist_name, playlist_id self, scope: str, playlist_name, playlist_id
) -> List[PlaylistFetchResult]: ) -> List[PlaylistFetchResult]:
"""Fetch all playlists with the specified filter""" """Fetch all playlists with the specified filter."""
scope_type = self.get_scope_type(scope) scope_type = self.get_scope_type(scope)
try: try:
playlist_id = int(playlist_id) playlist_id = int(playlist_id)
@ -195,7 +195,7 @@ class PlaylistWrapper:
return output return output
async def delete(self, scope: str, playlist_id: int, scope_id: int): 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) scope_type = self.get_scope_type(scope)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
executor.submit( executor.submit(
@ -205,12 +205,12 @@ class PlaylistWrapper:
) )
async def delete_scheduled(self): 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: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
executor.submit(self.database.cursor().execute, self.statement.delete_scheduled) executor.submit(self.database.cursor().execute, self.statement.delete_scheduled)
async def drop(self, scope: str): 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) scope_type = self.get_scope_type(scope)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
executor.submit( executor.submit(
@ -220,7 +220,7 @@ class PlaylistWrapper:
) )
async def create_table(self): async def create_table(self):
"""Create the playlist table""" """Create the playlist table."""
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
executor.submit(self.database.cursor().execute, PLAYLIST_CREATE_TABLE) executor.submit(self.database.cursor().execute, PLAYLIST_CREATE_TABLE)
@ -234,7 +234,7 @@ class PlaylistWrapper:
playlist_url: Optional[str], playlist_url: Optional[str],
tracks: List[MutableMapping], 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) scope_type = self.get_scope_type(scope)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
executor.submit( executor.submit(

View File

@ -1,16 +1,18 @@
import base64 import base64
import contextlib import contextlib
import json
import logging import logging
import time 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 import aiohttp
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core import Config from redbot.core import Config
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.commands import Cog, Context from redbot.core.commands import Cog, Context
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from ..errors import SpotifyFetchError from ..errors import SpotifyFetchError
@ -46,7 +48,7 @@ class SpotifyWrapper:
@staticmethod @staticmethod
def spotify_format_call(query_type: str, key: str) -> Tuple[str, MutableMapping]: def spotify_format_call(query_type: str, key: str) -> Tuple[str, MutableMapping]:
"""Format the spotify endpoint""" """Format the spotify endpoint."""
params: MutableMapping = {} params: MutableMapping = {}
if query_type == "album": if query_type == "album":
query = f"{ALBUMS_ENDPOINT}/{key}/tracks" query = f"{ALBUMS_ENDPOINT}/{key}/tracks"
@ -59,7 +61,7 @@ class SpotifyWrapper:
async def get_spotify_track_info( async def get_spotify_track_info(
self, track_data: MutableMapping, ctx: Context self, track_data: MutableMapping, ctx: Context
) -> Tuple[str, ...]: ) -> Tuple[str, ...]:
"""Extract track info from spotify response""" """Extract track info from spotify response."""
prefer_lyrics = await self.cog.get_lyrics_status(ctx) prefer_lyrics = await self.cog.get_lyrics_status(ctx)
track_name = track_data["name"] track_name = track_data["name"]
if prefer_lyrics: if prefer_lyrics:
@ -75,14 +77,14 @@ class SpotifyWrapper:
@staticmethod @staticmethod
async def is_access_token_valid(token: MutableMapping) -> bool: 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 return (token["expires_at"] - int(time.time())) < 60
@staticmethod @staticmethod
def make_auth_header( def make_auth_header(
client_id: Optional[str], client_secret: Optional[str] client_id: Optional[str], client_secret: Optional[str]
) -> MutableMapping[str, Union[str, int]]: ) -> MutableMapping[str, Union[str, int]]:
"""Make Authorization header for spotify token""" """Make Authorization header for spotify token."""
if client_id is None: if client_id is None:
client_id = "" client_id = ""
if client_secret is None: if client_secret is None:
@ -93,11 +95,11 @@ class SpotifyWrapper:
async def get( async def get(
self, url: str, headers: MutableMapping = None, params: MutableMapping = None self, url: str, headers: MutableMapping = None, params: MutableMapping = None
) -> MutableMapping[str, str]: ) -> MutableMapping[str, str]:
"""Make a GET request to the spotify API""" """Make a GET request to the spotify API."""
if params is None: if params is None:
params = {} params = {}
async with self.session.request("GET", url, params=params, headers=headers) as r: 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: if r.status != 200:
log.debug(f"Issue making GET request to {url}: [{r.status}] {data}") log.debug(f"Issue making GET request to {url}: [{r.status}] {data}")
return data return data
@ -106,7 +108,7 @@ class SpotifyWrapper:
self._token = new_token self._token = new_token
async def get_token(self) -> None: async def get_token(self) -> None:
"""Get the stored spotify tokens""" """Get the stored spotify tokens."""
if not self._token: if not self._token:
self._token = await self.bot.get_shared_api_tokens("spotify") self._token = await self.bot.get_shared_api_tokens("spotify")
@ -114,10 +116,17 @@ class SpotifyWrapper:
self.client_secret = self._token.get("client_secret", "") self.client_secret = self._token.get("client_secret", "")
async def get_country_code(self, ctx: Context = None) -> str: 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: 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() await self.get_token()
payload = {"grant_type": "client_credentials"} payload = {"grant_type": "client_credentials"}
headers = self.make_auth_header(self.client_id, self.client_secret) headers = self.make_auth_header(self.client_id, self.client_secret)
@ -125,7 +134,7 @@ class SpotifyWrapper:
return r return r
async def get_access_token(self) -> Optional[str]: 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): if self.spotify_token and not await self.is_access_token_valid(self.spotify_token):
return self.spotify_token["access_token"] return self.spotify_token["access_token"]
token = await self.request_access_token() token = await self.request_access_token()
@ -142,20 +151,20 @@ class SpotifyWrapper:
async def post( async def post(
self, url: str, payload: MutableMapping, headers: MutableMapping = None self, url: str, payload: MutableMapping, headers: MutableMapping = None
) -> MutableMapping: ) -> 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: 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: if r.status != 200:
log.debug(f"Issue making POST request to {url}: [{r.status}] {data}") log.debug(f"Issue making POST request to {url}: [{r.status}] {data}")
return data return data
async def make_get_call(self, url: str, params: MutableMapping) -> MutableMapping: 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() token = await self.get_access_token()
return await self.get(url, params=params, headers={"Authorization": f"Bearer {token}"}) return await self.get(url, params=params, headers={"Authorization": f"Bearer {token}"})
async def get_categories(self, ctx: Context = None) -> List[MutableMapping]: 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) country_code = await self.get_country_code(ctx=ctx)
params: MutableMapping = {"country": country_code} if country_code else {} params: MutableMapping = {"country": country_code} if country_code else {}
result = await self.make_get_call(CATEGORY_ENDPOINT, params=params) 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] return [{c["name"]: c["id"]} for c in categories if c]
async def get_playlist_from_category(self, category: str, ctx: Context = None): 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" url = f"{CATEGORY_ENDPOINT}/{category}/playlists"
country_code = await self.get_country_code(ctx=ctx) country_code = await self.get_country_code(ctx=ctx)
params: MutableMapping = {"country": country_code} if country_code else {} params: MutableMapping = {"country": country_code} if country_code else {}

View File

@ -1,5 +1,7 @@
import json
import logging import logging
from typing import Mapping, Optional, TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Mapping, Optional, Union
import aiohttp import aiohttp
@ -33,15 +35,17 @@ class YouTubeWrapper:
def update_token(self, new_token: Mapping[str, str]): def update_token(self, new_token: Mapping[str, str]):
self._token = new_token self._token = new_token
async def _get_api_key(self) -> str: async def _get_api_key(
"""Get the stored youtube token""" self,
) -> str:
"""Get the stored youtube token."""
if not self._token: if not self._token:
self._token = await self.bot.get_shared_api_tokens("youtube") self._token = await self.bot.get_shared_api_tokens("youtube")
self.api_key = self._token.get("api_key", "") self.api_key = self._token.get("api_key", "")
return self.api_key if self.api_key is not None else "" return self.api_key if self.api_key is not None else ""
async def get_call(self, query: str) -> Optional[str]: 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 = { params = {
"q": query, "q": query,
"part": "id", "part": "id",
@ -57,7 +61,7 @@ class YouTubeWrapper:
raise YouTubeApiError("Your YouTube Data API quota has been reached.") raise YouTubeApiError("Your YouTube Data API quota has been reached.")
return None return None
else: else:
search_response = await r.json() search_response = await r.json(loads=json.loads)
for search_result in search_response.get("items", []): for search_result in search_response.get("items", []):
if search_result["id"]["kind"] == "youtube#video": if search_result["id"]["kind"] == "youtube#video":
return f"https://www.youtube.com/watch?v={search_result['id']['videoId']}" return f"https://www.youtube.com/watch?v={search_result['id']['videoId']}"

View File

@ -5,21 +5,23 @@ import ntpath
import os import os
import posixpath import posixpath
import re import re
from pathlib import Path, PosixPath, WindowsPath from pathlib import Path, PosixPath, WindowsPath
from typing import ( from typing import (
AsyncIterator, AsyncIterator,
Callable,
Final, Final,
Iterator, Iterator,
MutableMapping, MutableMapping,
Optional, Optional,
Pattern,
Tuple, Tuple,
Union, Union,
Callable,
Pattern,
) )
from urllib.parse import urlparse from urllib.parse import urlparse
import lavalink import lavalink
from redbot.core.utils import AsyncIter from redbot.core.utils import AsyncIter
_RE_REMOVE_START: Final[Pattern] = re.compile(r"^(sc|list) ") _RE_REMOVE_START: Final[Pattern] = re.compile(r"^(sc|list) ")
@ -80,8 +82,8 @@ log = logging.getLogger("red.cogs.Audio.audio_dataclasses")
class LocalPath: class LocalPath:
"""Local tracks class. """Local tracks class.
Used to handle system dir trees in a cross system manner. Used to handle system dir trees in a cross system manner. The only use of this class is for
The only use of this class is for `localtracks`. `localtracks`.
""" """
_all_music_ext = _FULLY_SUPPORTED_MUSIC_EXT + _PARTIALLY_SUPPORTED_MUSIC_EXT _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_mixer: bool = kwargs.get("mixer", False)
self.is_twitch: bool = kwargs.get("twitch", False) self.is_twitch: bool = kwargs.get("twitch", False)
self.is_other: bool = kwargs.get("other", 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_playlist: bool = kwargs.get("playlist", False)
self.is_album: bool = kwargs.get("album", False) self.is_album: bool = kwargs.get("album", False)
self.is_search: bool = kwargs.get("search", 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.start_time: int = kwargs.get("start_time", 0)
self.track_index: Optional[int] = kwargs.get("track_index", None) self.track_index: Optional[int] = kwargs.get("track_index", None)
if self.invoked_from == "sc search": if self.invoked_from == "sc search":
self.is_youtube = False self.is_youtube = False
self.is_soundcloud = True self.is_soundcloud = True
@ -403,8 +405,7 @@ class Query:
_local_folder_current_path: Path, _local_folder_current_path: Path,
**kwargs, **kwargs,
) -> "Query": ) -> "Query":
""" """Process the input query into its type.
Process the input query into its type
Parameters Parameters
---------- ----------
@ -442,7 +443,7 @@ class Query:
@staticmethod @staticmethod
def _parse(track, _local_folder_current_path: Path, **kwargs) -> MutableMapping: 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 = {} returning: MutableMapping = {}
if ( if (
type(track) == type(LocalPath) type(track) == type(LocalPath)

View File

@ -1,5 +1,6 @@
import logging import logging
import sys import sys
from typing import Final from typing import Final
IS_DEBUG: Final[bool] = "--debug" in sys.argv IS_DEBUG: Final[bool] = "--debug" in sys.argv

View File

@ -1,14 +1,15 @@
import argparse import argparse
import functools import functools
import re import re
from typing import Final, MutableMapping, Optional, Tuple, Union, Pattern
from typing import Final, MutableMapping, Optional, Pattern, Tuple, Union
import discord import discord
from redbot.core.utils import AsyncIter
from redbot.core import commands from redbot.core import commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from .apis.api_utils import standardize_scope from .apis.api_utils import standardize_scope
from .apis.playlist_interface import get_all_playlist_converter from .apis.playlist_interface import get_all_playlist_converter

View File

@ -1,8 +1,11 @@
import asyncio import asyncio
import json
from collections import Counter from collections import Counter
from typing import Mapping from typing import Mapping
import aiohttp import aiohttp
import discord
from redbot.core import Config from redbot.core import Config
from redbot.core.bot import Red from redbot.core.bot import Red
@ -49,6 +52,7 @@ class Audio(
self._disconnected_players = {} self._disconnected_players = {}
self._daily_playlist_cache = {} self._daily_playlist_cache = {}
self._daily_global_playlist_cache = {} self._daily_global_playlist_cache = {}
self._persist_queue_cache = {}
self._dj_status_cache = {} self._dj_status_cache = {}
self._dj_role_cache = {} self._dj_role_cache = {}
self.skip_votes = {} self.skip_votes = {}
@ -58,30 +62,47 @@ class Audio(
self.player_automated_timer_task = None self.player_automated_timer_task = None
self.cog_cleaned_up = False self.cog_cleaned_up = False
self.lavalink_connection_aborted = 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_ready_event = asyncio.Event()
self.cog_init_task = None self.cog_init_task = None
self.global_api_user = {
"fetched": False,
"can_read": False,
"can_post": False,
"can_delete": False,
}
default_global = dict( default_global = dict(
schema_version=1, schema_version=1,
owner_notification=0,
cache_level=0, cache_level=0,
cache_age=365, cache_age=365,
daily_playlists=False, daily_playlists=False,
global_db_enabled=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, status=False,
use_external_lavalink=False, use_external_lavalink=False,
restrict=True, restrict=True,
localpath=str(cog_data_path(raw_name="Audio")), localpath=str(cog_data_path(raw_name="Audio")),
url_keyword_blacklist=[], url_keyword_blacklist=[],
url_keyword_whitelist=[], url_keyword_whitelist=[],
java_exc_path="java",
**self._default_lavalink_settings, **self._default_lavalink_settings,
) )
default_guild = dict( default_guild = dict(
auto_play=False, auto_play=False,
auto_deafen=True,
autoplaylist={"enabled": False, "id": None, "name": None, "scope": None}, autoplaylist={"enabled": False, "id": None, "name": None, "scope": None},
persist_queue=True,
disconnect=False, disconnect=False,
dj_enabled=False, dj_enabled=False,
dj_role=None, dj_role=None,
@ -119,3 +140,4 @@ class Audio(
self.config.register_custom(PlaylistScope.USER.value, **_playlist) self.config.register_custom(PlaylistScope.USER.value, **_playlist)
self.config.register_guild(**default_guild) self.config.register_guild(**default_guild)
self.config.register_global(**default_global) self.config.register_global(**default_global)
self.config.register_user(country_code=None)

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import Counter from collections import Counter
from pathlib import Path 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 aiohttp
import discord import discord
@ -25,8 +26,7 @@ if TYPE_CHECKING:
class MixinMeta(ABC): 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. 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] play_lock: MutableMapping[int, bool]
_daily_playlist_cache: MutableMapping[int, bool] _daily_playlist_cache: MutableMapping[int, bool]
_daily_global_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_status_cache: MutableMapping[int, Optional[bool]]
_dj_role_cache: MutableMapping[int, Optional[int]] _dj_role_cache: MutableMapping[int, Optional[int]]
_error_timer: MutableMapping[int, float] _error_timer: MutableMapping[int, float]
_disconnected_players: MutableMapping[int, bool] _disconnected_players: MutableMapping[int, bool]
global_api_user: MutableMapping[str, Any]
cog_cleaned_up: bool cog_cleaned_up: bool
lavalink_connection_aborted: bool lavalink_connection_aborted: bool
@ -60,6 +62,7 @@ class MixinMeta(ABC):
cog_ready_event: asyncio.Event cog_ready_event: asyncio.Event
_default_lavalink_settings: Mapping _default_lavalink_settings: Mapping
permission_cache = discord.Permissions
@abstractmethod @abstractmethod
async def command_llsetup(self, ctx: commands.Context): async def command_llsetup(self, ctx: commands.Context):
@ -74,7 +77,7 @@ class MixinMeta(ABC):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def get_active_player_count(self) -> Tuple[str, int]: async def get_active_player_count(self) -> Tuple[str, int]:
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
@ -160,16 +163,20 @@ class MixinMeta(ABC):
@abstractmethod @abstractmethod
async def is_query_allowed( 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: ) -> bool:
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @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() raise NotImplementedError()
@abstractmethod @abstractmethod
def get_track_description( async def get_track_description(
self, self,
track: Union[lavalink.rest_api.Track, "Query"], track: Union[lavalink.rest_api.Track, "Query"],
local_folder_current_path: Path, local_folder_current_path: Path,
@ -178,7 +185,7 @@ class MixinMeta(ABC):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @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 self, track: Union[lavalink.rest_api.Track, "Query"], local_folder_current_path: Path
) -> Optional[str]: ) -> Optional[str]:
raise NotImplementedError() raise NotImplementedError()
@ -495,6 +502,10 @@ class MixinMeta(ABC):
async def get_lyrics_status(self, ctx: Context) -> bool: async def get_lyrics_status(self, ctx: Context) -> bool:
raise NotImplementedError() raise NotImplementedError()
@abstractmethod
async def restore_players(self) -> bool:
raise NotImplementedError()
@abstractmethod @abstractmethod
async def command_skip(self, ctx: commands.Context, skip_to_track: int = None): async def command_skip(self, ctx: commands.Context, skip_to_track: int = None):
raise NotImplementedError() raise NotImplementedError()
@ -502,3 +513,10 @@ class MixinMeta(ABC):
@abstractmethod @abstractmethod
async def command_prev(self, ctx: commands.Context): async def command_prev(self, ctx: commands.Context):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod
async def icyparser(self, url: str) -> Optional[str]:
raise NotImplementedError()
async def self_deafen(self, player: lavalink.Player) -> None:
raise NotImplementedError()

View File

@ -8,15 +8,49 @@ from redbot.core.i18n import Translator
from ..converters import get_lazy_converter, get_playlist_converter 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"] __author__ = ["aikaterna", "Draper"]
_ = Translator("Audio", Path(__file__).parent) _ = Translator("Audio", Path(__file__).parent)
_SCHEMA_VERSION: Final[int] = 3 _SCHEMA_VERSION: Final[int] = 3
_OWNER_NOTIFICATION: Final[int] = 1
LazyGreedyConverter = get_lazy_converter("--") LazyGreedyConverter = get_lazy_converter("--")
PlaylistConverter = get_playlist_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)): class CompositeMetaClass(type(commands.Cog), type(ABC)):

View File

@ -1,6 +1,7 @@
import asyncio import asyncio
import contextlib import contextlib
import logging import logging
from typing import Union from typing import Union
import discord 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") @command_audioset.command(name="restrict")
@commands.is_owner() @commands.is_owner()
@commands.guild_only() @commands.guild_only()
@ -951,6 +967,9 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
bumpped_shuffle = _("Enabled") if data["shuffle_bumped"] else _("Disabled") bumpped_shuffle = _("Enabled") if data["shuffle_bumped"] else _("Disabled")
song_notify = _("Enabled") if data["notify"] else _("Disabled") song_notify = _("Enabled") if data["notify"] else _("Disabled")
song_status = _("Enabled") if global_data["status"] 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"] countrycode = data["country_code"]
spotify_cache = CacheLevel.set_spotify() spotify_cache = CacheLevel.set_spotify()
@ -992,7 +1011,9 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
"Shuffle bumped: [{bumpped_shuffle}]\n" "Shuffle bumped: [{bumpped_shuffle}]\n"
"Song notify msgs: [{notify}]\n" "Song notify msgs: [{notify}]\n"
"Songs as status: [{status}]\n" "Songs as status: [{status}]\n"
"Persist queue: [{persist_queue}]\n"
"Spotify search: [{countrycode}]\n" "Spotify search: [{countrycode}]\n"
"Auto-Deafen: [{auto_deafen}]\n"
).format( ).format(
countrycode=countrycode, countrycode=countrycode,
repeat=song_repeat, repeat=song_repeat,
@ -1000,6 +1021,8 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
notify=song_notify, notify=song_notify,
status=song_status, status=song_status,
bumpped_shuffle=bumpped_shuffle, bumpped_shuffle=bumpped_shuffle,
persist_queue=persist_queue,
auto_deafen=auto_deafen,
) )
if thumbnail: if thumbnail:
msg += _("Thumbnails: [{0}]\n").format( msg += _("Thumbnails: [{0}]\n").format(
@ -1050,16 +1073,22 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
+ _("Local Spotify cache: [{spotify_status}]\n") + _("Local Spotify cache: [{spotify_status}]\n")
+ _("Local Youtube cache: [{youtube_status}]\n") + _("Local Youtube cache: [{youtube_status}]\n")
+ _("Local Lavalink cache: [{lavalink_status}]\n") + _("Local Lavalink cache: [{lavalink_status}]\n")
# + _("Global cache status: [{global_cache}]\n") + _("Global cache status: [{global_cache}]\n")
# + _("Global timeout: [{num_seconds}]\n") + _("Global timeout: [{num_seconds}]\n")
).format( ).format(
max_age=str(await self.config.cache_age()) + " " + _("days"), max_age=str(await self.config.cache_age()) + " " + _("days"),
spotify_status=_("Enabled") if has_spotify_cache else _("Disabled"), spotify_status=_("Enabled") if has_spotify_cache else _("Disabled"),
youtube_status=_("Enabled") if has_youtube_cache else _("Disabled"), youtube_status=_("Enabled") if has_youtube_cache else _("Disabled"),
lavalink_status=_("Enabled") if has_lavalink_cache else _("Disabled"), lavalink_status=_("Enabled") if has_lavalink_cache else _("Disabled"),
# global_cache=_("Enabled") if global_data["global_db_enabled"] 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"]), 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 += ( msg += (
"\n---" "\n---"
@ -1075,19 +1104,21 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
if global_data["use_external_lavalink"] if global_data["use_external_lavalink"]
else _("Disabled"), 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 += _( msg += _(
"Lavalink build: [{llbuild}]\n" "Lavalink build: [{llbuild}]\n"
"Lavalink branch: [{llbranch}]\n" "Lavalink branch: [{llbranch}]\n"
"Release date: [{build_time}]\n" "Release date: [{build_time}]\n"
"Lavaplayer version: [{lavaplayer}]\n" "Lavaplayer version: [{lavaplayer}]\n"
"Java version: [{jvm}]\n" "Java version: [{jvm}]\n"
"Java Executable: [{jv_exec}]\n"
).format( ).format(
build_time=self.player_manager.build_time, build_time=self.player_manager.build_time,
llbuild=self.player_manager.ll_build, llbuild=self.player_manager.ll_build,
llbranch=self.player_manager.ll_branch, llbranch=self.player_manager.ll_branch,
lavaplayer=self.player_manager.lavaplayer, lavaplayer=self.player_manager.lavaplayer,
jvm=self.player_manager.jvm, jvm=self.player_manager.jvm,
jv_exec=self.player_manager.path,
) )
if is_owner: if is_owner:
msg += _("Localtracks path: [{localpath}]\n").format(**global_data) 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) 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") @command_audioset.command(name="cache")
@commands.is_owner() @commands.is_owner()
async def command_audioset_cache(self, ctx: commands.Context, *, level: int = None): 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) msg += _("I've set the cache age to {age} days").format(age=age)
await self.config.cache_age.set(age) await self.config.cache_age.set(age)
await self.send_embed_msg(ctx, title=_("Setting Changed"), description=msg) await self.send_embed_msg(ctx, title=_("Setting Changed"), description=msg)
@commands.is_owner()
@command_audioset.group(name="globalapi")
async def command_audioset_audiodb(self, ctx: commands.Context):
"""Change globalapi settings."""
@command_audioset_audiodb.command(name="toggle")
async def command_audioset_audiodb_toggle(self, ctx: commands.Context):
"""Toggle the server settings.
Default is ON
"""
state = await self.config.global_db_enabled()
await self.config.global_db_enabled.set(not state)
if not state: # Ensure a call is made if the API is enabled to update user perms
self.global_api_user = await self.api_interface.global_cache_api.get_perms()
await ctx.send(
_("Global DB is {status}").format(status=_("enabled") if not state else _("disabled"))
)
@command_audioset_audiodb.command(name="timeout")
async def command_audioset_audiodb_timeout(
self, ctx: commands.Context, timeout: Union[float, int]
):
"""Set GET request timeout.
Example: 0.1 = 100ms 1 = 1 second
"""
await self.config.global_db_get_timeout.set(timeout)
await ctx.send(_("Request timeout set to {time} second(s)").format(time=timeout))
@command_audioset.command(name="persistqueue")
@commands.admin()
async def command_audioset_persist_queue(self, ctx: commands.Context):
"""Toggle persistent queues.
Persistent queues allows the current queue to be restored when the queue closes.
"""
persist_cache = self._persist_queue_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).persist_queue()
)
await self.config.guild(ctx.guild).persist_queue.set(not persist_cache)
self._persist_queue_cache[ctx.guild.id] = not persist_cache
await self.send_embed_msg(
ctx,
title=_("Setting Changed"),
description=_("Persisting queues: {true_or_false}.").format(
true_or_false=_("Enabled") if not persist_cache else _("Disabled")
),
)
@command_audioset.command(name="restart")
@commands.is_owner()
async def command_audioset_restart(self, ctx: commands.Context):
"""Restarts the lavalink connection."""
async with ctx.typing():
lavalink.unregister_event_listener(self.lavalink_event_handler)
await lavalink.close()
if self.player_manager is not None:
await self.player_manager.shutdown()
self.lavalink_restart_connect()
lavalink.register_event_listener(self.lavalink_event_handler)
await self.restore_players()
await self.send_embed_msg(
ctx,
title=_("Restarting Lavalink"),
description=_("It can take a couple of minutes for Lavalink to fully start up."),
)

View File

@ -2,13 +2,15 @@ import asyncio
import contextlib import contextlib
import datetime import datetime
import logging import logging
from typing import Optional, Tuple, Union import time
from typing import Optional, Union
import discord import discord
import lavalink import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import commands from redbot.core import commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number from redbot.core.utils.chat_formatting import humanize_number
from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate 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 self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
await player.stop() await player.stop()
await player.disconnect() await player.disconnect()
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
@commands.command(name="now") @commands.command(name="now")
@commands.guild_only() @commands.guild_only()
@ -91,7 +94,10 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
dur = "LIVE" dur = "LIVE"
else: else:
dur = self.format_time(player.current.length) 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 Requested by: **{track.requester}**")
song += "\n\n{arrow}`{pos}`/`{dur}`" song += "\n\n{arrow}`{pos}`/`{dur}`"
song = song.format(track=player.current, arrow=arrow, pos=pos, dur=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: if not player.current:
return await self.send_embed_msg(ctx, title=_("Nothing playing.")) 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: if player.current and not player.paused:
await player.pause() await player.pause()
@ -262,6 +270,13 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
) )
else: else:
track = player.fetch("prev_song") 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) player.add(player.fetch("prev_requester"), track)
self.bot.dispatch("red_audio_track_enqueue", player.channel.guild, track, ctx.author) self.bot.dispatch("red_audio_track_enqueue", player.channel.guild, track, ctx.author)
queue_len = len(player.queue) queue_len = len(player.queue)
@ -269,7 +284,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
player.queue.insert(0, bump_song) player.queue.insert(0, bump_song)
player.queue.pop(queue_len) player.queue.pop(queue_len)
await player.skip() await player.skip()
description = self.get_track_description( description = await self.get_track_description(
player.current, self.local_folder_current_path player.current, self.local_folder_current_path
) )
embed = discord.Embed(title=_("Replaying Track"), description=description) 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): async def command_shuffle_bumpped(self, ctx: commands.Context):
"""Toggle bumped track shuffle. """Toggle bumped track shuffle.
Set this to disabled if you wish to avoid bumped songs being shuffled. Set this to disabled if you wish to avoid bumped songs being shuffled. This takes priority
This takes priority over `[p]shuffle`. over `[p]shuffle`.
""" """
dj_enabled = self._dj_status_cache.setdefault( dj_enabled = self._dj_status_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled() ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
@ -581,6 +596,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
player.store("requester", None) player.store("requester", None)
await player.stop() await player.stop()
await self.send_embed_msg(ctx, title=_("Stopping...")) await self.send_embed_msg(ctx, title=_("Stopping..."))
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
@commands.command(name="summon") @commands.command(name="summon")
@commands.guild_only() @commands.guild_only()
@ -626,12 +642,14 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
else: else:
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ctx.author.voice.channel == player.channel: if ctx.author.voice.channel == player.channel:
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
return return
await player.move_to(ctx.author.voice.channel) await player.move_to(ctx.author.voice.channel)
await self.self_deafen(player)
except AttributeError: except AttributeError:
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
return await self.send_embed_msg( return await self.send_embed_msg(
@ -774,7 +792,12 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
) )
index_or_url -= 1 index_or_url -= 1
removed = player.queue.pop(index_or_url) 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( await self.send_embed_msg(
ctx, ctx,
title=_("Removed track from queue"), title=_("Removed track from queue"),
@ -787,6 +810,9 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
if track.uri != index_or_url: if track.uri != index_or_url:
clean_tracks.append(track) clean_tracks.append(track)
else: else:
await self.api_interface.persistent_queue_api.played(
ctx.guild.id, track.extras.get("enqueue_time")
)
removed_tracks += 1 removed_tracks += 1
player.queue = clean_tracks player.queue = clean_tracks
if removed_tracks == 0: if removed_tracks == 0:
@ -841,7 +867,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
bump_song.extras["bumped"] = True bump_song.extras["bumped"] = True
player.queue.insert(0, bump_song) player.queue.insert(0, bump_song)
removed = player.queue.pop(index) 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( await self.send_embed_msg(
ctx, title=_("Moved track to the top of the queue."), description=description ctx, title=_("Moved track to the top of the queue."), description=description
) )

View File

@ -1,4 +1,5 @@
import logging import logging
from pathlib import Path
import discord import discord
@ -18,6 +19,73 @@ class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
async def command_llsetup(self, ctx: commands.Context): async def command_llsetup(self, ctx: commands.Context):
"""Lavalink server configuration options.""" """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") @command_llsetup.command(name="external")
async def command_llsetup_external(self, ctx: commands.Context): async def command_llsetup_external(self, ctx: commands.Context):
"""Toggle using external Lavalink servers.""" """Toggle using external Lavalink servers."""

View File

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

View File

@ -2,20 +2,27 @@ import contextlib
import datetime import datetime
import logging import logging
import math import math
from typing import MutableMapping, Optional import time
from typing import MutableMapping
import discord import discord
import lavalink 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 import commands
from redbot.core.commands import UserInputOptional 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 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_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
from ...audio_logging import IS_DEBUG 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 ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _ from ..cog_utils import CompositeMetaClass, _
@ -39,10 +46,17 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("That URL is not allowed."), 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( return await self.send_embed_msg(
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.") 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 not self._player_check(ctx):
if self.lavalink_connection_aborted: if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed") msg = _("Connection to Lavalink has failed")
@ -64,6 +78,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError: except AttributeError:
return await self.send_embed_msg( return await self.send_embed_msg(
ctx, ctx,
@ -76,15 +91,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("Connection to Lavalink has not yet been established."), 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 = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id) player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id) player.store("guild", ctx.guild.id)
await self._eq_check(ctx, player) await self._eq_check(ctx, player)
@ -143,10 +150,17 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("That URL is not allowed."), 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( return await self.send_embed_msg(
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.") 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 not self._player_check(ctx):
if self.lavalink_connection_aborted: if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed") msg = _("Connection to Lavalink has failed")
@ -168,6 +182,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError: except AttributeError:
return await self.send_embed_msg( return await self.send_embed_msg(
ctx, ctx,
@ -180,15 +195,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("Connection to Lavalink has not yet been established."), 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 = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id) player.store("channel", ctx.channel.id)
player.store("guild", ctx.guild.id) player.store("guild", ctx.guild.id)
await self._eq_check(ctx, player) await self._eq_check(ctx, player)
@ -258,13 +265,12 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
) )
if seek and seek > 0: if seek and seek > 0:
single_track.start_timestamp = seek * 1000 single_track.start_timestamp = seek * 1000
query = Query.process_input(single_track, self.local_folder_current_path)
if not await self.is_query_allowed( if not await self.is_query_allowed(
self.config, self.config,
ctx.guild, ctx,
( f"{single_track.title} {single_track.author} {single_track.uri} " f"{str(query)}",
f"{single_track.title} {single_track.author} {single_track.uri} " query_obj=query,
f"{str(Query.process_input(single_track, self.local_folder_current_path))}"
),
): ):
if IS_DEBUG: if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") 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: elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(single_track, guild_data["maxlength"]): if self.is_track_length_allowed(single_track, guild_data["maxlength"]):
single_track.requester = ctx.author 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.queue.insert(0, single_track)
player.maybe_shuffle() player.maybe_shuffle()
self.bot.dispatch( self.bot.dispatch(
@ -293,12 +306,21 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
else: else:
single_track.requester = ctx.author single_track.requester = ctx.author
single_track.extras["bumped"] = True 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.queue.insert(0, single_track)
player.maybe_shuffle() player.maybe_shuffle()
self.bot.dispatch( self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, single_track, ctx.author "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 footer = None
if not play_now and not guild_data["shuffle"] and queue_dur > 0: if not play_now and not guild_data["shuffle"] and queue_dur > 0:
footer = _("{time} until track playback: #1 in queue").format( footer = _("{time} until track playback: #1 in queue").format(
@ -395,6 +417,12 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
).format(prefix=ctx.prefix), ).format(prefix=ctx.prefix),
) )
guild_data = await self.config.guild(ctx.guild).all() 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 not self._player_check(ctx):
if self.lavalink_connection_aborted: if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed") msg = _("Connection to Lavalink has failed")
@ -416,6 +444,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError: except AttributeError:
return await self.send_embed_msg( return await self.send_embed_msg(
ctx, ctx,
@ -428,12 +457,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("Connection to Lavalink has not yet been established."), 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 = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id) player.store("channel", ctx.channel.id)
@ -509,6 +532,13 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.mod_or_permissions(manage_guild=True) @commands.mod_or_permissions(manage_guild=True)
async def command_autoplay(self, ctx: commands.Context): async def command_autoplay(self, ctx: commands.Context):
"""Starts auto play.""" """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 not self._player_check(ctx):
if self.lavalink_connection_aborted: if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed") msg = _("Connection to Lavalink has failed")
@ -530,6 +560,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError: except AttributeError:
return await self.send_embed_msg( return await self.send_embed_msg(
ctx, ctx,
@ -542,13 +573,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("Connection to Lavalink has not yet been established."), 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 = lavalink.get_player(ctx.guild.id)
player.store("channel", ctx.channel.id) player.store("channel", ctx.channel.id)
@ -583,7 +607,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx, ctx,
title=_("Unable to Get Track"), title=_("Unable to Get Track"),
description=_( 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." "minutes."
), ),
) )
@ -603,10 +627,15 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
async def command_search(self, ctx: commands.Context, *, query: str): async def command_search(self, ctx: commands.Context, *, query: str):
"""Pick a track with a search. """Pick a track with a search.
Use `[p]search list <search term>` to queue all tracks found on YouTube. Use `[p]search list <search term>` to queue all tracks found on YouTube. Use `[p]search sc
Use `[p]search sc <search term>` will search SoundCloud instead of YouTube. <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( async def _search_menu(
ctx: commands.Context, ctx: commands.Context,
pages: list, pages: list,
@ -654,6 +683,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
await self.self_deafen(player)
except AttributeError: except AttributeError:
return await self.send_embed_msg( return await self.send_embed_msg(
ctx, ctx,
@ -693,9 +723,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
description=_("That URL is not allowed."), description=_("That URL is not allowed."),
) )
if not await self.is_query_allowed( if not await self.is_query_allowed(self.config, ctx, f"{query}", query_obj=query):
self.config, ctx.guild, f"{query}", query_obj=query
):
return await self.send_embed_msg( return await self.send_embed_msg(
ctx, ctx,
title=_("Unable To Play Tracks"), title=_("Unable To Play Tracks"),
@ -713,7 +741,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx, ctx,
title=_("Unable to Get Track"), title=_("Unable to Get Track"),
description=_( 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." "try again in a few minutes."
), ),
) )
@ -729,7 +757,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx, ctx,
title=_("Unable to Get Track"), title=_("Unable to Get Track"),
description=_( 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." "try again in a few minutes."
), ),
) )
@ -762,13 +790,12 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
async for track in AsyncIter(tracks): async for track in AsyncIter(tracks):
if len(player.queue) >= 10000: if len(player.queue) >= 10000:
continue continue
query = Query.process_input(track, self.local_folder_current_path)
if not await self.is_query_allowed( if not await self.is_query_allowed(
self.config, self.config,
ctx.guild, ctx,
( f"{track.title} {track.author} {track.uri} " f"{str(query)}",
f"{track.title} {track.author} {track.uri} " query_obj=query,
f"{str(Query.process_input(track, self.local_folder_current_path))}"
),
): ):
if IS_DEBUG: if IS_DEBUG:
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") 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: elif guild_data["maxlength"] > 0:
if self.is_track_length_allowed(track, guild_data["maxlength"]): if self.is_track_length_allowed(track, guild_data["maxlength"]):
track_len += 1 track_len += 1
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, track) player.add(ctx.author, track)
self.bot.dispatch( self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author "red_audio_track_enqueue", player.channel.guild, track, ctx.author
) )
else: else:
track_len += 1 track_len += 1
track.extras.update(
{
"enqueue_time": int(time.time()),
"vc": player.channel.id,
"requester": ctx.author.id,
}
)
player.add(ctx.author, track) player.add(ctx.author, track)
self.bot.dispatch( self.bot.dispatch(
"red_audio_track_enqueue", player.channel.guild, track, ctx.author "red_audio_track_enqueue", player.channel.guild, track, ctx.author
@ -832,7 +873,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx, ctx,
title=_("Unable to Get Track"), title=_("Unable to Get Track"),
description=_( 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." "try again in a few minutes."
), ),
) )

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import asyncio
import datetime import datetime
import logging import logging
import time import time
from typing import Optional from typing import Optional
import discord import discord
@ -130,6 +131,13 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
) )
except Exception as err: except Exception as err:
debug_exc_log(log, err, f"Failed to delete global daily playlist ID: {too_old_id}") 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() @commands.Cog.listener()
async def on_red_audio_queue_end( 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 self.api_interface.local_cache_api.youtube.clean_up_old_entries()
await asyncio.sleep(5) await asyncio.sleep(5)
await self.playlist_api.delete_scheduled() 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() @commands.Cog.listener()
async def on_red_audio_track_end( 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 self.api_interface.local_cache_api.youtube.clean_up_old_entries()
await asyncio.sleep(5) await asyncio.sleep(5)
await self.playlist_api.delete_scheduled() await self.playlist_api.delete_scheduled()
await self.api_interface.persistent_queue_api.drop(guild.id)
await asyncio.sleep(5)
await self.api_interface.persistent_queue_api.delete_scheduled()

View File

@ -1,19 +1,24 @@
import asyncio import asyncio
import contextlib
import logging import logging
import re import re
from collections import OrderedDict
from pathlib import Path from pathlib import Path
from typing import Final, Pattern from typing import Final, Pattern
import discord import discord
import lavalink import lavalink
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
from discord.ext.commands import CheckFailure
from redbot.core import commands 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 ...audio_logging import debug_exc_log
from ...errors import TrackEnqueueError from ...errors import TrackEnqueueError
from ..abc import MixinMeta
from ..cog_utils import HUMANIZED_PERM, CompositeMetaClass, _
log = logging.getLogger("red.cogs.Audio.cog.Events.dpy") log = logging.getLogger("red.cogs.Audio.cog.Events.dpy")
@ -39,11 +44,55 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
raise RuntimeError( raise RuntimeError(
"Not running audio command due to invalid machine architecture for Lavalink." "Not running audio command due to invalid machine architecture for Lavalink."
) )
# with contextlib.suppress(Exception):
# player = lavalink.get_player(ctx.guild.id) current_perms = ctx.channel.permissions_for(ctx.me)
# notify_channel = player.fetch("channel") surpass_ignore = (
# if not notify_channel: isinstance(ctx.channel, discord.abc.PrivateChannel)
# player.store("channel", ctx.channel.id) 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._daily_global_playlist_cache.setdefault(
self.bot.user.id, await self.config.daily_playlists() 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()) self.local_folder_current_path = Path(await self.config.localpath())
if not ctx.guild: if not ctx.guild:
return return
dj_enabled = self._dj_status_cache.setdefault( dj_enabled = self._dj_status_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled() ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
) )
self._daily_playlist_cache.setdefault( self._daily_playlist_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).daily_playlists() 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: if dj_enabled:
dj_role = self._dj_role_cache.setdefault( dj_role = self._dj_role_cache.setdefault(
ctx.guild.id, await self.config.guild(ctx.guild).dj_role() 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): if isinstance(error, commands.ArgParserFailure):
handled = True handled = True
msg = _("`{user_input}` is not a valid value for `{command}`").format( 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: if error.custom_help_msg:
msg += f"\n{error.custom_help_msg}" msg += f"\n{error.custom_help_msg}"
await self.send_embed_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: if error.send_cmd_help:
await ctx.send_help() await ctx.send_help()
@ -101,7 +158,10 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
) )
else: else:
await self.send_embed_msg( 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: else:
await ctx.send_help() await ctx.send_help()
@ -137,6 +197,17 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
error=True, error=True,
) )
debug_exc_log(log, error, "This is a handled error") 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( if not isinstance(
error, error,
( (
@ -186,3 +257,13 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
self.skip_votes[before.channel.guild].remove(member.id) self.skip_votes[before.channel.guild].remove(member.id)
except (ValueError, KeyError, AttributeError): except (ValueError, KeyError, AttributeError):
pass pass
channel = self.rgetattr(member, "voice.channel", None)
bot_voice_state = self.rgetattr(member, "guild.me.voice.self_deaf", None)
if channel and bot_voice_state is False:
try:
player = lavalink.get_player(channel.guild.id)
except (KeyError, AttributeError):
pass
else:
if player.channel.id == channel.id:
await self.self_deafen(player)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,17 @@
import contextlib import contextlib
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Union from typing import List, Union
import lavalink import lavalink
from fuzzywuzzy import process from fuzzywuzzy import process
from redbot.core.utils import AsyncIter
from redbot.core import commands from redbot.core import commands
from redbot.core.utils import AsyncIter
from ...errors import TrackEnqueueError
from ...audio_dataclasses import LocalPath, Query from ...audio_dataclasses import LocalPath, Query
from ...errors import TrackEnqueueError
from ..abc import MixinMeta from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass, _ 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]: 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): if not await self.localtracks_folder_exists(ctx):
return [] return []
query = Query.process_input(query, self.local_folder_current_path) 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( async def get_localtrack_folder_tracks(
self, ctx, player: lavalink.player_manager.Player, query: Query self, ctx, player: lavalink.player_manager.Player, query: Query
) -> List[lavalink.rest_api.Track]: ) -> 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: if not await self.localtracks_folder_exists(ctx) or self.api_interface is None:
return [] return []

View File

@ -5,21 +5,22 @@ import functools
import json import json
import logging import logging
import re 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 discord
import lavalink 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 import bank, commands
from redbot.core.commands import Context from redbot.core.commands import Context
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number 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 ...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") 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 self, message: discord.Message, emoji: MutableMapping = None
) -> asyncio.Task: ) -> asyncio.Task:
"""Non blocking version of clear_react.""" """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: async def maybe_charge_requester(self, ctx: commands.Context, jukebox_price: int) -> bool:
jukebox = await self.config.guild(ctx.guild).jukebox() jukebox = await self.config.guild(ctx.guild).jukebox()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,13 @@
import logging import logging
import re import re
from typing import Final, List, Set, Pattern
from typing import Final, List, Optional, Pattern, Set, Union
from urllib.parse import urlparse from urllib.parse import urlparse
import discord import discord
from redbot.core import Config from redbot.core import Config
from redbot.core.commands import Context
from ...audio_dataclasses import Query from ...audio_dataclasses import Query
from ..abc import MixinMeta 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)) return not (channel.user_limit == 0 or channel.user_limit > len(channel.members))
async def is_query_allowed( 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: ) -> bool:
"""Checks if the query is allowed in this server or globally""" """Checks if the query is allowed in this server or globally."""
if ctx_or_channel:
query = query.lower().strip() 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: if query_obj is not None:
query = query_obj.lavalink_query.replace("ytsearch:", "youtubesearch").replace( query = query_obj.lavalink_query.replace("ytsearch:", "youtubesearch").replace(
"scsearch:", "soundcloudsearch" "scsearch:", "soundcloudsearch"

View File

@ -1,6 +1,7 @@
import asyncio import asyncio
import asyncio.subprocess # disables for # https://github.com/PyCQA/pylint/issues/1469 import asyncio.subprocess # disables for # https://github.com/PyCQA/pylint/issues/1469
import itertools import itertools
import json
import logging import logging
import pathlib import pathlib
import platform import platform
@ -9,14 +10,16 @@ import shutil
import sys import sys
import tempfile import tempfile
import time import time
from typing import ClassVar, Final, List, Optional, Tuple, Pattern
from typing import ClassVar, Final, List, Optional, Pattern, Tuple
import aiohttp import aiohttp
from tqdm import tqdm
from redbot.core import data_manager from redbot.core import data_manager
from tqdm import tqdm
from .errors import LavalinkDownloadFailed from .errors import LavalinkDownloadFailed
from .utils import task_callback
log = logging.getLogger("red.audio.manager") log = logging.getLogger("red.audio.manager")
JAR_VERSION: Final[str] = "3.3.1" JAR_VERSION: Final[str] = "3.3.1"
@ -57,6 +60,7 @@ class ServerManager:
_jvm: ClassVar[Optional[str]] = None _jvm: ClassVar[Optional[str]] = None
_lavalink_branch: ClassVar[Optional[str]] = None _lavalink_branch: ClassVar[Optional[str]] = None
_buildtime: ClassVar[Optional[str]] = None _buildtime: ClassVar[Optional[str]] = None
_java_exc: ClassVar[str] = "java"
def __init__(self) -> None: def __init__(self) -> None:
self.ready: asyncio.Event = asyncio.Event() self.ready: asyncio.Event = asyncio.Event()
@ -65,6 +69,10 @@ class ServerManager:
self._monitor_task: Optional[asyncio.Task] = None self._monitor_task: Optional[asyncio.Task] = None
self._shutdown: bool = False self._shutdown: bool = False
@property
def path(self) -> Optional[str]:
return self._java_exc
@property @property
def jvm(self) -> Optional[str]: def jvm(self) -> Optional[str]:
return self._jvm return self._jvm
@ -85,8 +93,9 @@ class ServerManager:
def build_time(self) -> Optional[str]: def build_time(self) -> Optional[str]:
return self._buildtime return self._buildtime
async def start(self) -> None: async def start(self, java_path: str) -> None:
arch_name = platform.machine() arch_name = platform.machine()
self._java_exc = java_path
if arch_name in self._blacklisted_archs: if arch_name in self._blacklisted_archs:
raise asyncio.CancelledError( raise asyncio.CancelledError(
"You are attempting to run Lavalink audio on an unsupported machine architecture." "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") log.warning("Timeout occurred whilst waiting for internal Lavalink server to be ready")
self._monitor_task = asyncio.create_task(self._monitor()) self._monitor_task = asyncio.create_task(self._monitor())
self._monitor_task.add_done_callback(task_callback)
async def _get_jar_args(self) -> List[str]: async def _get_jar_args(self) -> List[str]:
(java_available, java_version) = await self._has_java() (java_available, java_version) = await self._has_java()
@ -128,27 +138,36 @@ class ServerManager:
if not java_available: if not java_available:
raise RuntimeError("You must install Java 11 for Lavalink to run.") 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]]]: async def _has_java(self) -> Tuple[bool, Optional[Tuple[int, int]]]:
if self._java_available is not None: if self._java_available is not None:
# Return cached value if we've checked this before # Return cached value if we've checked this before
return self._java_available, self._java_version 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: if not java_available:
self.java_available = False self.java_available = False
self.java_version = None self.java_version = None
else: else:
self._java_version = version = await self._get_java_version() self._java_version = version = await self._get_java_version()
self._java_available = (11, 0) <= version < (12, 0) self._java_available = (11, 0) <= version < (12, 0)
self._java_exc = java_exec
return self._java_available, self._java_version return self._java_available, self._java_version
@staticmethod async def _get_java_version(self) -> Tuple[int, int]:
async def _get_java_version() -> Tuple[int, int]:
"""This assumes we've already checked that java exists.""" """This assumes we've already checked that java exists."""
_proc: asyncio.subprocess.Process = ( _proc: asyncio.subprocess.Process = (
await asyncio.create_subprocess_exec( # pylint:disable=no-member 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 # java -version outputs to stderr
@ -171,10 +190,7 @@ class ServerManager:
elif short_match: elif short_match:
return int(short_match["major"]), 0 return int(short_match["major"]), 0
raise RuntimeError( raise RuntimeError(f"The output of `{self._java_exc} -version` was unexpected.")
"The output of `java -version` was unexpected. Please report this issue on Red's "
"issue tracker."
)
async def _wait_for_launcher(self) -> None: async def _wait_for_launcher(self) -> None:
log.debug("Waiting for Lavalink server to be ready") log.debug("Waiting for Lavalink server to be ready")
@ -202,7 +218,7 @@ class ServerManager:
log.info("Internal Lavalink jar shutdown unexpectedly") log.info("Internal Lavalink jar shutdown unexpectedly")
if not self._has_java_error(): if not self._has_java_error():
log.info("Restarting internal Lavalink server") log.info("Restarting internal Lavalink server")
await self.start() await self.start(self._java_exc)
else: else:
log.critical( log.critical(
"Your Java is borked. Please find the hs_err_pid%d.log file" "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: async def _download_jar(self) -> None:
log.info("Downloading Lavalink.jar...") 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: async with session.get(LAVALINK_DOWNLOAD_URL) as response:
if response.status == 404: if response.status == 404:
# A 404 means our LAVALINK_DOWNLOAD_URL is invalid, so likely the jar version # A 404 means our LAVALINK_DOWNLOAD_URL is invalid, so likely the jar version

View File

@ -54,6 +54,15 @@ __all__ = [
"LAVALINK_QUERY_LAST_FETCHED_RANDOM", "LAVALINK_QUERY_LAST_FETCHED_RANDOM",
"LAVALINK_DELETE_OLD_ENTRIES", "LAVALINK_DELETE_OLD_ENTRIES",
"LAVALINK_FETCH_ALL_ENTRIES_GLOBAL", "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 # PRAGMA Statements
@ -555,3 +564,83 @@ LAVALINK_FETCH_ALL_ENTRIES_GLOBAL: Final[
SELECT query, data SELECT query, data
FROM lavalink 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
"""

View File

@ -1,4 +1,8 @@
import asyncio
import contextlib
import logging
import time import time
from enum import Enum, unique from enum import Enum, unique
from typing import MutableMapping from typing import MutableMapping
@ -6,6 +10,8 @@ import discord
from redbot.core import commands from redbot.core import commands
log = logging.getLogger("red.cogs.Audio.task.callback")
class CacheLevel: class CacheLevel:
__slots__ = ("value",) __slots__ = ("value",)
@ -205,3 +211,9 @@ class PlaylistScope(Enum):
@staticmethod @staticmethod
def list(): def list():
return list(map(lambda c: c.value, PlaylistScope)) 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)