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
import discord
import lavalink
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import humanize_list
@ -74,8 +75,22 @@ class PlaylistFetchResult:
self.tracks = json.loads(self.tracks)
@dataclass
class QueueFetchResult:
guild_id: int
room_id: int
track: dict = field(default_factory=lambda: {})
track_object: lavalink.Track = None
def __post_init__(self):
if isinstance(self.track, str):
self.track = json.loads(self.track)
if self.track:
self.track_object = lavalink.Track(self.track)
def standardize_scope(scope: str) -> str:
"""Convert any of the used scopes into one we are expecting"""
"""Convert any of the used scopes into one we are expecting."""
scope = scope.upper()
valid_scopes = ["GLOBAL", "GUILD", "AUTHOR", "USER", "SERVER", "MEMBER", "BOT"]
@ -103,7 +118,7 @@ def prepare_config_scope(
author: Union[discord.abc.User, int] = None,
guild: Union[discord.Guild, int] = None,
):
"""Return the scope used by Playlists"""
"""Return the scope used by Playlists."""
scope = standardize_scope(scope)
if scope == PlaylistScope.GLOBAL.value:
config_scope = [PlaylistScope.GLOBAL.value, bot.user.id]
@ -121,7 +136,7 @@ def prepare_config_scope(
def prepare_config_scope_for_migration23( # TODO: remove me in a future version ?
scope, author: Union[discord.abc.User, int] = None, guild: discord.Guild = None
):
"""Return the scope used by Playlists"""
"""Return the scope used by Playlists."""
scope = standardize_scope(scope)
if scope == PlaylistScope.GLOBAL.value:

View File

@ -1,8 +1,10 @@
import asyncio
import contextlib
import json
import logging
import urllib.parse
from typing import Mapping, Optional, TYPE_CHECKING, Union
from copy import copy
from typing import TYPE_CHECKING, Mapping, Optional, Union
import aiohttp
from lavalink.rest_api import LoadResult
@ -17,7 +19,7 @@ from ..audio_logging import IS_DEBUG, debug_exc_log
if TYPE_CHECKING:
from .. import Audio
_API_URL = "https://redbot.app/"
_API_URL = "https://api.redbot.app/"
log = logging.getLogger("red.cogs.Audio.api.GlobalDB")
@ -32,11 +34,150 @@ class GlobalCacheWrapper:
self.session = session
self.api_key = None
self._handshake_token = ""
self.can_write = False
self._handshake_token = ""
self.has_api_key = None
self._token: Mapping[str, str] = {}
self.cog = cog
def update_token(self, new_token: Mapping[str, str]):
self._token = new_token
async def _get_api_key(
self,
) -> Optional[str]:
if not self._token:
self._token = await self.bot.get_shared_api_tokens("audiodb")
self.api_key = self._token.get("api_key", None)
self.has_api_key = self.cog.global_api_user.get("can_post")
id_list = list(self.bot.owner_ids)
self._handshake_token = "||".join(map(str, id_list))
return self.api_key
async def get_call(self, query: Optional[Query] = None) -> dict:
api_url = f"{_API_URL}api/v2/queries"
if not self.cog.global_api_user.get("can_read"):
return {}
try:
query = Query.process_input(query, self.cog.local_folder_current_path)
if any([not query or not query.valid or query.is_spotify or query.is_local]):
return {}
await self._get_api_key()
if self.api_key is None:
return {}
search_response = "error"
query = query.lavalink_query
with contextlib.suppress(aiohttp.ContentTypeError, asyncio.TimeoutError):
async with self.session.get(
api_url,
timeout=aiohttp.ClientTimeout(total=await self.config.global_db_get_timeout()),
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
params={"query": query},
) as r:
search_response = await r.json(loads=json.loads)
if IS_DEBUG and "x-process-time" in r.headers:
log.debug(
f"GET || Ping {r.headers.get('x-process-time')} || "
f"Status code {r.status} || {query}"
)
if "tracks" not in search_response:
return {}
return search_response
except Exception as err:
debug_exc_log(log, err, f"Failed to Get query: {api_url}/{query}")
return {}
async def get_spotify(self, title: str, author: Optional[str]) -> dict:
if not self.cog.global_api_user.get("can_read"):
return {}
api_url = f"{_API_URL}api/v2/queries/spotify"
try:
search_response = "error"
params = {"title": title, "author": author}
await self._get_api_key()
if self.api_key is None:
return {}
with contextlib.suppress(aiohttp.ContentTypeError, asyncio.TimeoutError):
async with self.session.get(
api_url,
timeout=aiohttp.ClientTimeout(total=await self.config.global_db_get_timeout()),
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
params=params,
) as r:
search_response = await r.json(loads=json.loads)
if IS_DEBUG and "x-process-time" in r.headers:
log.debug(
f"GET/spotify || Ping {r.headers.get('x-process-time')} || "
f"Status code {r.status} || {title} - {author}"
)
if "tracks" not in search_response:
return {}
return search_response
except Exception as err:
debug_exc_log(log, err, f"Failed to Get query: {api_url}")
return {}
async def post_call(self, llresponse: LoadResult, query: Optional[Query]) -> None:
try:
if not self.cog.global_api_user.get("can_post"):
return
query = Query.process_input(query, self.cog.local_folder_current_path)
if llresponse.has_error or llresponse.load_type.value in ["NO_MATCHES", "LOAD_FAILED"]:
return
if query and query.valid and query.is_youtube:
query = query.lavalink_query
else:
return None
await self._get_api_key()
if self.api_key is None:
return None
api_url = f"{_API_URL}api/v2/queries"
async with self.session.post(
api_url,
json=llresponse._raw,
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
params={"query": query},
) as r:
await r.read()
if IS_DEBUG and "x-process-time" in r.headers:
log.debug(
f"POST || Ping {r.headers.get('x-process-time')} ||"
f" Status code {r.status} || {query}"
)
except Exception as err:
debug_exc_log(log, err, f"Failed to post query: {query}")
await asyncio.sleep(0)
async def update_global(self, llresponse: LoadResult, query: Optional[Query] = None):
await self.post_call(llresponse=llresponse, query=query)
async def report_invalid(self, id: str) -> None:
if not self.cog.global_api_user.get("can_delete"):
return
api_url = f"{_API_URL}api/v2/queries/es/id"
with contextlib.suppress(Exception):
async with self.session.delete(
api_url,
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
params={"id": id},
) as r:
await r.read()
async def get_perms(self):
global_api_user = copy(self.cog.global_api_user)
await self._get_api_key()
is_enabled = await self.config.global_db_enabled()
await self._get_api_key()
if (not is_enabled) or self.api_key is None:
return global_api_user
with contextlib.suppress(Exception):
async with aiohttp.ClientSession(json_serialize=json.dumps) as session:
async with session.get(
f"{_API_URL}api/v2/users/me",
headers={"Authorization": self.api_key, "X-Token": self._handshake_token},
) as resp:
if resp.status == 200:
search_response = await resp.json(loads=json.loads)
global_api_user["fetched"] = True
global_api_user["can_read"] = search_response.get("can_read", False)
global_api_user["can_post"] = search_response.get("can_post", False)
global_api_user["can_delete"] = search_response.get("can_delete", False)
return global_api_user

View File

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

View File

@ -4,14 +4,14 @@ import datetime
import logging
import random
import time
from types import SimpleNamespace
from typing import Callable, List, MutableMapping, Optional, TYPE_CHECKING, Tuple, Union
from redbot.core.utils import AsyncIter
from types import SimpleNamespace
from typing import TYPE_CHECKING, Callable, List, MutableMapping, Optional, Tuple, Union
from redbot.core import Config
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper
from ..audio_logging import debug_exc_log
@ -313,7 +313,7 @@ class LavalinkTableWrapper(BaseWrapper):
self.statement.get_random = LAVALINK_QUERY_LAST_FETCHED_RANDOM
self.statement.get_all_global = LAVALINK_FETCH_ALL_ENTRIES_GLOBAL
self.fetch_result = LavalinkCacheFetchResult
self.fetch_for_global: Optional[Callable] = None
self.fetch_for_global: Optional[Callable] = LavalinkCacheFetchForGlobalResult
async def fetch_one(
self, values: MutableMapping

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
from typing import List, MutableMapping, Optional, Union
import discord
import lavalink
from redbot.core.utils import AsyncIter
from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.utils import AsyncIter
from ..errors import NotAllowed
from ..utils import PlaylistScope

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,6 +54,15 @@ __all__ = [
"LAVALINK_QUERY_LAST_FETCHED_RANDOM",
"LAVALINK_DELETE_OLD_ENTRIES",
"LAVALINK_FETCH_ALL_ENTRIES_GLOBAL",
# Persisting Queue statements
"PERSIST_QUEUE_DROP_TABLE",
"PERSIST_QUEUE_CREATE_TABLE",
"PERSIST_QUEUE_CREATE_INDEX",
"PERSIST_QUEUE_PLAYED",
"PERSIST_QUEUE_DELETE_SCHEDULED",
"PERSIST_QUEUE_FETCH_ALL",
"PERSIST_QUEUE_UPSERT",
"PERSIST_QUEUE_BULK_PLAYED",
]
# PRAGMA Statements
@ -555,3 +564,83 @@ LAVALINK_FETCH_ALL_ENTRIES_GLOBAL: Final[
SELECT query, data
FROM lavalink
"""
# Persisting Queue statements
PERSIST_QUEUE_DROP_TABLE: Final[
str
] = """
DROP TABLE IF EXISTS persist_queue ;
"""
PERSIST_QUEUE_CREATE_TABLE: Final[
str
] = """
CREATE TABLE IF NOT EXISTS persist_queue(
guild_id INTEGER NOT NULL,
room_id INTEGER NOT NULL,
track JSON NOT NULL,
played BOOLEAN DEFAULT false,
track_id TEXT NOT NULL,
time INTEGER NOT NULL,
PRIMARY KEY (guild_id, room_id, track_id)
);
"""
PERSIST_QUEUE_CREATE_INDEX: Final[
str
] = """
CREATE INDEX IF NOT EXISTS track_index ON persist_queue (guild_id, track_id);
"""
PERSIST_QUEUE_PLAYED: Final[
str
] = """
UPDATE persist_queue
SET
played = true
WHERE
(
guild_id = :guild_id
AND track_id = :track_id
)
;
"""
PERSIST_QUEUE_BULK_PLAYED: Final[
str
] = """
UPDATE persist_queue
SET
played = true
WHERE guild_id = :guild_id
;
"""
PERSIST_QUEUE_DELETE_SCHEDULED: Final[
str
] = """
DELETE
FROM
persist_queue
WHERE
played = true;
"""
PERSIST_QUEUE_FETCH_ALL: Final[
str
] = """
SELECT
guild_id, room_id, track
FROM
persist_queue
WHERE played = false
ORDER BY time ASC;
"""
PERSIST_QUEUE_UPSERT: Final[
str
] = """
INSERT INTO
persist_queue (guild_id, room_id, track, played, track_id, time)
VALUES
(
:guild_id, :room_id, :track, :played, :track_id, :time
)
ON CONFLICT (guild_id, room_id, track_id) DO
UPDATE
SET
time = excluded.time
"""

View File

@ -1,4 +1,8 @@
import asyncio
import contextlib
import logging
import time
from enum import Enum, unique
from typing import MutableMapping
@ -6,6 +10,8 @@ import discord
from redbot.core import commands
log = logging.getLogger("red.cogs.Audio.task.callback")
class CacheLevel:
__slots__ = ("value",)
@ -205,3 +211,9 @@ class PlaylistScope(Enum):
@staticmethod
def list():
return list(map(lambda c: c.value, PlaylistScope))
def task_callback(task: asyncio.Task) -> None:
with contextlib.suppress(asyncio.CancelledError, asyncio.InvalidStateError):
if exc := task.exception():
log.exception(f"{task.get_name()} raised an Exception", exc_info=exc)