mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
Merge pull request #4003 from Drapersniper/finally
Git Yeeeted out of here you monster
This commit is contained in:
commit
7e5009345c
@ -1,9 +0,0 @@
|
||||
from redbot.core.bot import Red
|
||||
|
||||
from .core import Audio
|
||||
|
||||
|
||||
def setup(bot: Red):
|
||||
cog = Audio(bot)
|
||||
bot.add_cog(cog)
|
||||
cog.start_up_task()
|
||||
@ -1,10 +0,0 @@
|
||||
from . import (
|
||||
api_utils,
|
||||
global_db,
|
||||
interface,
|
||||
local_db,
|
||||
playlist_interface,
|
||||
playlist_wrapper,
|
||||
spotify,
|
||||
youtube,
|
||||
)
|
||||
@ -1,140 +0,0 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, MutableMapping, Optional, Union
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.utils.chat_formatting import humanize_list
|
||||
|
||||
from ..errors import InvalidPlaylistScope, MissingAuthor, MissingGuild
|
||||
from ..utils import PlaylistScope
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.utils")
|
||||
|
||||
|
||||
@dataclass
|
||||
class YouTubeCacheFetchResult:
|
||||
query: Optional[str]
|
||||
last_updated: int
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.last_updated, int):
|
||||
self.updated_on: datetime.datetime = datetime.datetime.fromtimestamp(self.last_updated)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpotifyCacheFetchResult:
|
||||
query: Optional[str]
|
||||
last_updated: int
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.last_updated, int):
|
||||
self.updated_on: datetime.datetime = datetime.datetime.fromtimestamp(self.last_updated)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LavalinkCacheFetchResult:
|
||||
query: Optional[MutableMapping]
|
||||
last_updated: int
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.last_updated, int):
|
||||
self.updated_on: datetime.datetime = datetime.datetime.fromtimestamp(self.last_updated)
|
||||
|
||||
if isinstance(self.query, str):
|
||||
self.query = json.loads(self.query)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LavalinkCacheFetchForGlobalResult:
|
||||
query: str
|
||||
data: MutableMapping
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.data, str):
|
||||
self.data_string = str(self.data)
|
||||
self.data = json.loads(self.data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlaylistFetchResult:
|
||||
playlist_id: int
|
||||
playlist_name: str
|
||||
scope_id: int
|
||||
author_id: int
|
||||
playlist_url: Optional[str] = None
|
||||
tracks: List[MutableMapping] = field(default_factory=lambda: [])
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.tracks, str):
|
||||
self.tracks = json.loads(self.tracks)
|
||||
|
||||
|
||||
def standardize_scope(scope: str) -> str:
|
||||
"""Convert any of the used scopes into one we are expecting"""
|
||||
scope = scope.upper()
|
||||
valid_scopes = ["GLOBAL", "GUILD", "AUTHOR", "USER", "SERVER", "MEMBER", "BOT"]
|
||||
|
||||
if scope in PlaylistScope.list():
|
||||
return scope
|
||||
elif scope not in valid_scopes:
|
||||
raise InvalidPlaylistScope(
|
||||
f'"{scope}" is not a valid playlist scope.'
|
||||
f" Scope needs to be one of the following: {humanize_list(valid_scopes)}"
|
||||
)
|
||||
|
||||
if scope in ["GLOBAL", "BOT"]:
|
||||
scope = PlaylistScope.GLOBAL.value
|
||||
elif scope in ["GUILD", "SERVER"]:
|
||||
scope = PlaylistScope.GUILD.value
|
||||
elif scope in ["USER", "MEMBER", "AUTHOR"]:
|
||||
scope = PlaylistScope.USER.value
|
||||
|
||||
return scope
|
||||
|
||||
|
||||
def prepare_config_scope(
|
||||
bot: Red,
|
||||
scope,
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
):
|
||||
"""Return the scope used by Playlists"""
|
||||
scope = standardize_scope(scope)
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
config_scope = [PlaylistScope.GLOBAL.value, bot.user.id]
|
||||
elif scope == PlaylistScope.USER.value:
|
||||
if author is None:
|
||||
raise MissingAuthor("Invalid author for user scope.")
|
||||
config_scope = [PlaylistScope.USER.value, int(getattr(author, "id", author))]
|
||||
else:
|
||||
if guild is None:
|
||||
raise MissingGuild("Invalid guild for guild scope.")
|
||||
config_scope = [PlaylistScope.GUILD.value, int(getattr(guild, "id", guild))]
|
||||
return 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"""
|
||||
scope = standardize_scope(scope)
|
||||
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
config_scope = [PlaylistScope.GLOBAL.value]
|
||||
elif scope == PlaylistScope.USER.value:
|
||||
if author is None:
|
||||
raise MissingAuthor("Invalid author for user scope.")
|
||||
config_scope = [PlaylistScope.USER.value, str(getattr(author, "id", author))]
|
||||
else:
|
||||
if guild is None:
|
||||
raise MissingGuild("Invalid guild for guild scope.")
|
||||
config_scope = [PlaylistScope.GUILD.value, str(getattr(guild, "id", guild))]
|
||||
return config_scope
|
||||
|
||||
|
||||
FakePlaylist = namedtuple("Playlist", "author scope")
|
||||
@ -1,42 +0,0 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import urllib.parse
|
||||
from typing import Mapping, Optional, TYPE_CHECKING, Union
|
||||
|
||||
import aiohttp
|
||||
from lavalink.rest_api import LoadResult
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog
|
||||
|
||||
from ..audio_dataclasses import Query
|
||||
from ..audio_logging import IS_DEBUG, debug_exc_log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
_API_URL = "https://redbot.app/"
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.GlobalDB")
|
||||
|
||||
|
||||
class GlobalCacheWrapper:
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, session: aiohttp.ClientSession, cog: Union["Audio", Cog]
|
||||
):
|
||||
# Place Holder for the Global Cache PR
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
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
|
||||
@ -1,894 +0,0 @@
|
||||
import asyncio
|
||||
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
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
import lavalink
|
||||
from lavalink.rest_api import LoadResult
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
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.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 .global_db import GlobalCacheWrapper
|
||||
from .local_db import LocalCacheWrapper
|
||||
from .playlist_interface import get_playlist
|
||||
from .playlist_wrapper import PlaylistWrapper
|
||||
from .spotify import SpotifyWrapper
|
||||
from .youtube import YouTubeWrapper
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
_ = Translator("Audio", __file__)
|
||||
log = logging.getLogger("red.cogs.Audio.api.AudioAPIInterface")
|
||||
_TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK"
|
||||
|
||||
|
||||
class AudioAPIInterface:
|
||||
"""Handles music queries.
|
||||
|
||||
Always tries the Local cache first, then Global cache before making API calls.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: Red,
|
||||
config: Config,
|
||||
session: aiohttp.ClientSession,
|
||||
conn: APSWConnectionWrapper,
|
||||
cog: Union["Audio", Cog],
|
||||
):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.conn = conn
|
||||
self.cog = cog
|
||||
self.spotify_api: SpotifyWrapper = SpotifyWrapper(self.bot, self.config, session, self.cog)
|
||||
self.youtube_api: YouTubeWrapper = YouTubeWrapper(self.bot, self.config, session, self.cog)
|
||||
self.local_cache_api = LocalCacheWrapper(self.bot, self.config, self.conn, self.cog)
|
||||
self.global_cache_api = GlobalCacheWrapper(self.bot, self.config, session, 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"""
|
||||
await self.local_cache_api.lavalink.init()
|
||||
|
||||
def close(self) -> None:
|
||||
"""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"""
|
||||
track: Optional[MutableMapping] = {}
|
||||
try:
|
||||
query_data = {}
|
||||
date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7)
|
||||
date_timestamp = int(date.timestamp())
|
||||
query_data["day"] = date_timestamp
|
||||
max_age = await self.config.cache_age()
|
||||
maxage = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(
|
||||
days=max_age
|
||||
)
|
||||
maxage_int = int(time.mktime(maxage.timetuple()))
|
||||
query_data["maxage"] = maxage_int
|
||||
track = await self.local_cache_api.lavalink.fetch_random(query_data)
|
||||
if track is not None:
|
||||
if track.get("loadType") == "V2_COMPACT":
|
||||
track["loadType"] = "V2_COMPAT"
|
||||
results = LoadResult(track)
|
||||
track = random.choice(list(results.tracks))
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to fetch a random track from database")
|
||||
track = {}
|
||||
|
||||
if not track:
|
||||
return None
|
||||
|
||||
return track
|
||||
|
||||
async def route_tasks(
|
||||
self, action_type: str = None, data: Union[List[MutableMapping], MutableMapping] = None,
|
||||
) -> None:
|
||||
"""Separate the tasks and run them in the appropriate functions"""
|
||||
|
||||
if not data:
|
||||
return
|
||||
if action_type == "insert" and isinstance(data, list):
|
||||
for table, d in data:
|
||||
if table == "lavalink":
|
||||
await self.local_cache_api.lavalink.insert(d)
|
||||
elif table == "youtube":
|
||||
await self.local_cache_api.youtube.insert(d)
|
||||
elif table == "spotify":
|
||||
await self.local_cache_api.spotify.insert(d)
|
||||
elif action_type == "update" and isinstance(data, dict):
|
||||
for table, d in data:
|
||||
if table == "lavalink":
|
||||
await self.local_cache_api.lavalink.update(data)
|
||||
elif table == "youtube":
|
||||
await self.local_cache_api.youtube.update(data)
|
||||
elif table == "spotify":
|
||||
await self.local_cache_api.spotify.update(data)
|
||||
|
||||
async def run_tasks(self, ctx: Optional[commands.Context] = None, message_id=None) -> None:
|
||||
"""Run tasks for a specific context"""
|
||||
if message_id is not None:
|
||||
lock_id = message_id
|
||||
elif ctx is not None:
|
||||
lock_id = ctx.message.id
|
||||
else:
|
||||
return
|
||||
lock_author = ctx.author if ctx else None
|
||||
async with self._lock:
|
||||
if lock_id in self._tasks:
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Running database writes for {lock_id} ({lock_author})")
|
||||
try:
|
||||
tasks = self._tasks[lock_id]
|
||||
tasks = [self.route_tasks(a, tasks[a]) for a in tasks]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
del self._tasks[lock_id]
|
||||
except Exception as exc:
|
||||
debug_exc_log(
|
||||
log, exc, f"Failed database writes for {lock_id} ({lock_author})"
|
||||
)
|
||||
else:
|
||||
if IS_DEBUG:
|
||||
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"""
|
||||
async with self._lock:
|
||||
if IS_DEBUG:
|
||||
log.debug("Running pending writes to database")
|
||||
try:
|
||||
tasks: MutableMapping = {"update": [], "insert": [], "global": []}
|
||||
async for k, task in AsyncIter(self._tasks.items()):
|
||||
async for t, args in AsyncIter(task.items()):
|
||||
tasks[t].append(args)
|
||||
self._tasks = {}
|
||||
coro_tasks = [self.route_tasks(a, tasks[a]) for a in tasks]
|
||||
|
||||
await asyncio.gather(*coro_tasks, return_exceptions=True)
|
||||
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed database writes")
|
||||
else:
|
||||
if IS_DEBUG:
|
||||
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"""
|
||||
lock_id = _id or ctx.message.id
|
||||
if lock_id not in self._tasks:
|
||||
self._tasks[lock_id] = {"update": [], "insert": [], "global": []}
|
||||
self._tasks[lock_id][event].append(task)
|
||||
|
||||
async def fetch_spotify_query(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
query_type: str,
|
||||
uri: str,
|
||||
notifier: Optional[Notifier],
|
||||
skip_youtube: bool = False,
|
||||
current_cache_level: CacheLevel = CacheLevel.none(),
|
||||
) -> List[str]:
|
||||
"""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
|
||||
)
|
||||
total_tracks = len(tracks)
|
||||
database_entries = []
|
||||
track_count = 0
|
||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
|
||||
async for track in AsyncIter(tracks):
|
||||
if isinstance(track, str):
|
||||
break
|
||||
elif isinstance(track, dict) and track.get("error", {}).get("message") == "invalid id":
|
||||
continue
|
||||
(
|
||||
song_url,
|
||||
track_info,
|
||||
uri,
|
||||
artist_name,
|
||||
track_name,
|
||||
_id,
|
||||
_type,
|
||||
) = await self.spotify_api.get_spotify_track_info(track, ctx)
|
||||
|
||||
database_entries.append(
|
||||
{
|
||||
"id": _id,
|
||||
"type": _type,
|
||||
"uri": uri,
|
||||
"track_name": track_name,
|
||||
"artist_name": artist_name,
|
||||
"song_url": song_url,
|
||||
"track_info": track_info,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
)
|
||||
if skip_youtube is False:
|
||||
val = None
|
||||
if youtube_cache:
|
||||
try:
|
||||
(val, last_update) = await self.local_cache_api.youtube.fetch_one(
|
||||
{"track": track_info}
|
||||
)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table")
|
||||
|
||||
if val is None:
|
||||
val = await self.fetch_youtube_query(
|
||||
ctx, track_info, current_cache_level=current_cache_level
|
||||
)
|
||||
if youtube_cache and val:
|
||||
task = ("update", ("youtube", {"track": track_info}))
|
||||
self.append_task(ctx, *task)
|
||||
if val:
|
||||
youtube_urls.append(val)
|
||||
else:
|
||||
youtube_urls.append(track_info)
|
||||
track_count += 1
|
||||
if notifier is not None and ((track_count % 2 == 0) or (track_count == total_tracks)):
|
||||
await notifier.notify_user(current=track_count, total=total_tracks, key="youtube")
|
||||
if CacheLevel.set_spotify().is_subset(current_cache_level):
|
||||
task = ("insert", ("spotify", database_entries))
|
||||
self.append_task(ctx, *task)
|
||||
return youtube_urls
|
||||
|
||||
async def fetch_from_spotify_api(
|
||||
self,
|
||||
query_type: str,
|
||||
uri: str,
|
||||
recursive: Union[str, bool] = False,
|
||||
params: MutableMapping = None,
|
||||
notifier: Optional[Notifier] = None,
|
||||
ctx: Context = None,
|
||||
) -> Union[List[MutableMapping], List[str]]:
|
||||
"""Gets track info from spotify API"""
|
||||
|
||||
if recursive is False:
|
||||
(call, params) = self.spotify_api.spotify_format_call(query_type, uri)
|
||||
results = await self.spotify_api.make_get_call(call, params)
|
||||
else:
|
||||
if isinstance(recursive, str):
|
||||
results = await self.spotify_api.make_get_call(recursive, params)
|
||||
else:
|
||||
results = {}
|
||||
try:
|
||||
if results["error"]["status"] == 401 and not recursive:
|
||||
raise SpotifyFetchError(
|
||||
_(
|
||||
"The Spotify API key or client secret has not been set properly. "
|
||||
"\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
)
|
||||
)
|
||||
elif recursive:
|
||||
return {"next": None}
|
||||
except KeyError:
|
||||
pass
|
||||
if recursive:
|
||||
return results
|
||||
tracks = []
|
||||
track_count = 0
|
||||
total_tracks = results.get("tracks", results).get("total", 1)
|
||||
while True:
|
||||
new_tracks: List = []
|
||||
if query_type == "track":
|
||||
new_tracks = results
|
||||
tracks.append(new_tracks)
|
||||
elif query_type == "album":
|
||||
tracks_raw = results.get("tracks", results).get("items", [])
|
||||
if tracks_raw:
|
||||
new_tracks = tracks_raw
|
||||
tracks.extend(new_tracks)
|
||||
else:
|
||||
tracks_raw = results.get("tracks", results).get("items", [])
|
||||
if tracks_raw:
|
||||
new_tracks = [k["track"] for k in tracks_raw if k.get("track")]
|
||||
tracks.extend(new_tracks)
|
||||
track_count += len(new_tracks)
|
||||
if notifier:
|
||||
await notifier.notify_user(current=track_count, total=total_tracks, key="spotify")
|
||||
try:
|
||||
if results.get("next") is not None:
|
||||
results = await self.fetch_from_spotify_api(
|
||||
query_type, uri, results["next"], params, notifier=notifier
|
||||
)
|
||||
continue
|
||||
else:
|
||||
break
|
||||
except KeyError:
|
||||
raise SpotifyFetchError(
|
||||
_("This doesn't seem to be a valid Spotify playlist/album URL or code.")
|
||||
)
|
||||
return tracks
|
||||
|
||||
async def spotify_query(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
query_type: str,
|
||||
uri: str,
|
||||
skip_youtube: bool = False,
|
||||
notifier: Optional[Notifier] = None,
|
||||
) -> List[str]:
|
||||
"""Queries the Database then falls back to Spotify and YouTube APIs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: commands.Context
|
||||
The context this method is being called under.
|
||||
query_type : str
|
||||
Type of query to perform (Pl
|
||||
uri: str
|
||||
Spotify URL ID.
|
||||
skip_youtube:bool
|
||||
Whether or not to skip YouTube API Calls.
|
||||
notifier: Notifier
|
||||
A Notifier object to handle the user UI notifications while tracks are loaded.
|
||||
Returns
|
||||
-------
|
||||
List[str]
|
||||
List of Youtube URLs.
|
||||
"""
|
||||
current_cache_level = CacheLevel(await self.config.cache_level())
|
||||
cache_enabled = CacheLevel.set_spotify().is_subset(current_cache_level)
|
||||
if query_type == "track" and cache_enabled:
|
||||
try:
|
||||
(val, last_update) = await self.local_cache_api.spotify.fetch_one(
|
||||
{"uri": f"spotify:track:{uri}"}
|
||||
)
|
||||
except Exception as exc:
|
||||
debug_exc_log(
|
||||
log, exc, f"Failed to fetch 'spotify:track:{uri}' from Spotify table"
|
||||
)
|
||||
val = None
|
||||
else:
|
||||
val = None
|
||||
youtube_urls = []
|
||||
if val is None:
|
||||
urls = await self.fetch_spotify_query(
|
||||
ctx,
|
||||
query_type,
|
||||
uri,
|
||||
notifier,
|
||||
skip_youtube,
|
||||
current_cache_level=current_cache_level,
|
||||
)
|
||||
youtube_urls.extend(urls)
|
||||
else:
|
||||
if query_type == "track" and cache_enabled:
|
||||
task = ("update", ("spotify", {"uri": f"spotify:track:{uri}"}))
|
||||
self.append_task(ctx, *task)
|
||||
youtube_urls.append(val)
|
||||
return youtube_urls
|
||||
|
||||
async def spotify_enqueue(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
query_type: str,
|
||||
uri: str,
|
||||
enqueue: bool,
|
||||
player: lavalink.Player,
|
||||
lock: Callable,
|
||||
notifier: Optional[Notifier] = None,
|
||||
forced: bool = False,
|
||||
query_global: bool = False,
|
||||
) -> List[lavalink.Track]:
|
||||
"""Queries the Database then falls back to Spotify and YouTube APIs then Enqueued matched tracks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: commands.Context
|
||||
The context this method is being called under.
|
||||
query_type : str
|
||||
Type of query to perform (Pl
|
||||
uri: str
|
||||
Spotify URL ID.
|
||||
enqueue:bool
|
||||
Whether or not to enqueue the tracks
|
||||
player: lavalink.Player
|
||||
The current Player.
|
||||
notifier: Notifier
|
||||
A Notifier object to handle the user UI notifications while tracks are loaded.
|
||||
lock: Callable
|
||||
A callable handling the Track enqueue lock while spotify tracks are being added.
|
||||
query_global: bool
|
||||
Whether or not to query the global API.
|
||||
forced: bool
|
||||
Ignore Cache and make a fetch from API.
|
||||
Returns
|
||||
-------
|
||||
List[str]
|
||||
List of Youtube URLs.
|
||||
"""
|
||||
# globaldb_toggle = await self.config.global_db_enabled()
|
||||
track_list: List = []
|
||||
has_not_allowed = False
|
||||
try:
|
||||
current_cache_level = CacheLevel(await self.config.cache_level())
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
enqueued_tracks = 0
|
||||
consecutive_fails = 0
|
||||
queue_dur = await self.cog.queue_duration(ctx)
|
||||
queue_total_duration = self.cog.format_time(queue_dur)
|
||||
before_queue_length = len(player.queue)
|
||||
tracks_from_spotify = await self.fetch_from_spotify_api(
|
||||
query_type, uri, params=None, notifier=notifier
|
||||
)
|
||||
total_tracks = len(tracks_from_spotify)
|
||||
if total_tracks < 1 and notifier is not None:
|
||||
lock(ctx, False)
|
||||
embed3 = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("This doesn't seem to be a supported Spotify URL or code."),
|
||||
)
|
||||
await notifier.update_embed(embed3)
|
||||
|
||||
return track_list
|
||||
database_entries = []
|
||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
|
||||
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
|
||||
spotify_cache = CacheLevel.set_spotify().is_subset(current_cache_level)
|
||||
async for track_count, track in AsyncIter(tracks_from_spotify).enumerate(start=1):
|
||||
(
|
||||
song_url,
|
||||
track_info,
|
||||
uri,
|
||||
artist_name,
|
||||
track_name,
|
||||
_id,
|
||||
_type,
|
||||
) = await self.spotify_api.get_spotify_track_info(track, ctx)
|
||||
|
||||
database_entries.append(
|
||||
{
|
||||
"id": _id,
|
||||
"type": _type,
|
||||
"uri": uri,
|
||||
"track_name": track_name,
|
||||
"artist_name": artist_name,
|
||||
"song_url": song_url,
|
||||
"track_info": track_info,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
)
|
||||
val = None
|
||||
llresponse = None
|
||||
if youtube_cache:
|
||||
try:
|
||||
(val, last_updated) = await self.local_cache_api.youtube.fetch_one(
|
||||
{"track": track_info}
|
||||
)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table")
|
||||
|
||||
if val is None:
|
||||
val = await self.fetch_youtube_query(
|
||||
ctx, track_info, current_cache_level=current_cache_level
|
||||
)
|
||||
if youtube_cache and val and llresponse is None:
|
||||
task = ("update", ("youtube", {"track": track_info}))
|
||||
self.append_task(ctx, *task)
|
||||
|
||||
if llresponse is not None:
|
||||
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
|
||||
track_object = result.tracks
|
||||
else:
|
||||
track_object = []
|
||||
if (track_count % 2 == 0) or (track_count == total_tracks):
|
||||
key = "lavalink"
|
||||
seconds = "???"
|
||||
second_key = None
|
||||
if notifier is not None:
|
||||
await notifier.notify_user(
|
||||
current=track_count,
|
||||
total=total_tracks,
|
||||
key=key,
|
||||
seconds_key=second_key,
|
||||
seconds=seconds,
|
||||
)
|
||||
|
||||
if consecutive_fails >= 10:
|
||||
error_embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Failing to get tracks, skipping remaining."),
|
||||
)
|
||||
if notifier is not None:
|
||||
await notifier.update_embed(error_embed)
|
||||
break
|
||||
if not track_object:
|
||||
consecutive_fails += 1
|
||||
continue
|
||||
consecutive_fails = 0
|
||||
single_track = track_object[0]
|
||||
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)}"
|
||||
),
|
||||
):
|
||||
has_not_allowed = True
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||
continue
|
||||
track_list.append(single_track)
|
||||
if enqueue:
|
||||
if len(player.queue) >= 10000:
|
||||
continue
|
||||
if guild_data["maxlength"] > 0:
|
||||
if self.cog.is_track_length_allowed(single_track, guild_data["maxlength"]):
|
||||
enqueued_tracks += 1
|
||||
player.add(ctx.author, single_track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue",
|
||||
player.channel.guild,
|
||||
single_track,
|
||||
ctx.author,
|
||||
)
|
||||
else:
|
||||
enqueued_tracks += 1
|
||||
player.add(ctx.author, single_track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue",
|
||||
player.channel.guild,
|
||||
single_track,
|
||||
ctx.author,
|
||||
)
|
||||
|
||||
if not player.current:
|
||||
await player.play()
|
||||
if not track_list and not has_not_allowed:
|
||||
raise SpotifyFetchError(
|
||||
message=_(
|
||||
"Nothing found.\nThe YouTube API key may be invalid "
|
||||
"or you may be rate limited on YouTube's search service.\n"
|
||||
"Check the YouTube API key again and follow the instructions "
|
||||
"at `{prefix}audioset youtubeapi`."
|
||||
)
|
||||
)
|
||||
player.maybe_shuffle()
|
||||
if enqueue and tracks_from_spotify:
|
||||
if total_tracks > enqueued_tracks:
|
||||
maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format(
|
||||
bad_tracks=(total_tracks - enqueued_tracks)
|
||||
)
|
||||
else:
|
||||
maxlength_msg = ""
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Playlist Enqueued"),
|
||||
description=_("Added {num} tracks to the queue.{maxlength_msg}").format(
|
||||
num=enqueued_tracks, maxlength_msg=maxlength_msg
|
||||
),
|
||||
)
|
||||
if not guild_data["shuffle"] and queue_dur > 0:
|
||||
embed.set_footer(
|
||||
text=_(
|
||||
"{time} until start of playlist"
|
||||
" playback: starts at #{position} in queue"
|
||||
).format(time=queue_total_duration, position=before_queue_length + 1)
|
||||
)
|
||||
|
||||
if notifier is not None:
|
||||
await notifier.update_embed(embed)
|
||||
lock(ctx, False)
|
||||
|
||||
if spotify_cache:
|
||||
task = ("insert", ("spotify", database_entries))
|
||||
self.append_task(ctx, *task)
|
||||
except Exception as exc:
|
||||
lock(ctx, False)
|
||||
raise exc
|
||||
finally:
|
||||
lock(ctx, False)
|
||||
return track_list
|
||||
|
||||
async def fetch_youtube_query(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
track_info: str,
|
||||
current_cache_level: CacheLevel = CacheLevel.none(),
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
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())
|
||||
task = (
|
||||
"insert",
|
||||
(
|
||||
"youtube",
|
||||
[
|
||||
{
|
||||
"track_info": track_info,
|
||||
"track_url": track_url,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
self.append_task(ctx, *task)
|
||||
return track_url
|
||||
|
||||
async def fetch_from_youtube_api(
|
||||
self, ctx: commands.Context, track_info: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
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
|
||||
if cache_enabled:
|
||||
try:
|
||||
(val, update) = await self.local_cache_api.youtube.fetch_one({"track": track_info})
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table")
|
||||
if val is None:
|
||||
youtube_url = await self.fetch_youtube_query(
|
||||
ctx, track_info, current_cache_level=current_cache_level
|
||||
)
|
||||
else:
|
||||
if cache_enabled:
|
||||
task = ("update", ("youtube", {"track": track_info}))
|
||||
self.append_task(ctx, *task)
|
||||
youtube_url = val
|
||||
return youtube_url
|
||||
|
||||
async def fetch_track(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
player: lavalink.Player,
|
||||
query: Query,
|
||||
forced: bool = False,
|
||||
lazy: bool = False,
|
||||
should_query_global: bool = True,
|
||||
) -> Tuple[LoadResult, bool]:
|
||||
"""A replacement for :code:`lavalink.Player.load_tracks`. This will try to get a valid
|
||||
cached entry first if not found or if in valid it will then call the lavalink API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: commands.Context
|
||||
The context this method is being called under.
|
||||
player : lavalink.Player
|
||||
The player who's requesting the query.
|
||||
query: audio_dataclasses.Query
|
||||
The Query object for the query in question.
|
||||
forced:bool
|
||||
Whether or not to skip cache and call API first.
|
||||
lazy:bool
|
||||
If set to True, it will not call the api if a track is not found.
|
||||
should_query_global:bool
|
||||
If the method should query the global database.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[lavalink.LoadResult, bool]
|
||||
Tuple with the Load result and whether or not the API was called.
|
||||
"""
|
||||
current_cache_level = CacheLevel(await self.config.cache_level())
|
||||
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
|
||||
val = None
|
||||
query = Query.process_input(query, self.cog.local_folder_current_path)
|
||||
query_string = str(query)
|
||||
valid_global_entry = False
|
||||
results = None
|
||||
called_api = False
|
||||
prefer_lyrics = await self.cog.get_lyrics_status(ctx)
|
||||
if prefer_lyrics and query.is_youtube and query.is_search:
|
||||
query_string = f"{query} - lyrics"
|
||||
if cache_enabled and not forced and not query.is_local:
|
||||
try:
|
||||
(val, last_updated) = await self.local_cache_api.lavalink.fetch_one(
|
||||
{"query": query_string}
|
||||
)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, f"Failed to fetch '{query_string}' from Lavalink table")
|
||||
|
||||
if val and isinstance(val, dict):
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Updating Local Database with {query_string}")
|
||||
task = ("update", ("lavalink", {"query": query_string}))
|
||||
self.append_task(ctx, *task)
|
||||
else:
|
||||
val = None
|
||||
|
||||
if val and not forced and isinstance(val, dict):
|
||||
valid_global_entry = False
|
||||
called_api = False
|
||||
else:
|
||||
val = None
|
||||
|
||||
if valid_global_entry:
|
||||
pass
|
||||
elif lazy is True:
|
||||
called_api = False
|
||||
elif val and not forced and isinstance(val, dict):
|
||||
data = val
|
||||
data["query"] = query_string
|
||||
if data.get("loadType") == "V2_COMPACT":
|
||||
data["loadType"] = "V2_COMPAT"
|
||||
results = LoadResult(data)
|
||||
called_api = False
|
||||
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)
|
||||
else:
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Querying Lavalink api for {query_string}")
|
||||
called_api = True
|
||||
try:
|
||||
results = await player.load_tracks(query_string)
|
||||
except KeyError:
|
||||
results = None
|
||||
except RuntimeError:
|
||||
raise TrackEnqueueError
|
||||
if results is None:
|
||||
results = LoadResult({"loadType": "LOAD_FAILED", "playlistInfo": {}, "tracks": []})
|
||||
|
||||
if (
|
||||
cache_enabled
|
||||
and results.load_type
|
||||
and not results.has_error
|
||||
and not query.is_local
|
||||
and results.tracks
|
||||
):
|
||||
try:
|
||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
data = json.dumps(results._raw)
|
||||
if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]):
|
||||
task = (
|
||||
"insert",
|
||||
(
|
||||
"lavalink",
|
||||
[
|
||||
{
|
||||
"query": query_string,
|
||||
"data": data,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
self.append_task(ctx, *task)
|
||||
except Exception as exc:
|
||||
debug_exc_log(
|
||||
log,
|
||||
exc,
|
||||
f"Failed to enqueue write task for '{query_string}' to Lavalink table",
|
||||
)
|
||||
return results, called_api
|
||||
|
||||
async def autoplay(self, player: lavalink.Player, playlist_api: PlaylistWrapper):
|
||||
"""
|
||||
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)
|
||||
playlist = None
|
||||
tracks = None
|
||||
if autoplaylist["enabled"]:
|
||||
try:
|
||||
playlist = await get_playlist(
|
||||
autoplaylist["id"],
|
||||
autoplaylist["scope"],
|
||||
self.bot,
|
||||
playlist_api,
|
||||
player.channel.guild,
|
||||
player.channel.guild.me,
|
||||
)
|
||||
tracks = playlist.tracks_obj
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to fetch playlist for autoplay")
|
||||
|
||||
if not tracks or not getattr(playlist, "tracks", None):
|
||||
if cache_enabled:
|
||||
track = await self.get_random_track_from_db()
|
||||
tracks = [] if not track else [track]
|
||||
if not tracks:
|
||||
ctx = namedtuple("Context", "message guild cog")
|
||||
(results, called_api) = await self.fetch_track(
|
||||
cast(
|
||||
commands.Context, ctx(player.channel.guild, player.channel.guild, self.cog)
|
||||
),
|
||||
player,
|
||||
Query.process_input(_TOP_100_US, self.cog.local_folder_current_path),
|
||||
)
|
||||
tracks = list(results.tracks)
|
||||
if tracks:
|
||||
multiple = len(tracks) > 1
|
||||
valid = not multiple
|
||||
tries = len(tracks)
|
||||
track = tracks[0]
|
||||
while valid is False and multiple:
|
||||
tries -= 1
|
||||
if tries <= 0:
|
||||
raise DatabaseError("No valid entry found")
|
||||
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 (
|
||||
query.is_local
|
||||
and query.local_track_path is not None
|
||||
and not query.local_track_path.exists()
|
||||
):
|
||||
continue
|
||||
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))}"
|
||||
),
|
||||
):
|
||||
if IS_DEBUG:
|
||||
log.debug(
|
||||
"Query is not allowed in "
|
||||
f"{player.channel.guild} ({player.channel.guild.id})"
|
||||
)
|
||||
continue
|
||||
valid = True
|
||||
|
||||
track.extras["autoplay"] = True
|
||||
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()
|
||||
@ -1,372 +0,0 @@
|
||||
import concurrent
|
||||
import contextlib
|
||||
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 redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog
|
||||
from redbot.core.utils.dbtools import APSWConnectionWrapper
|
||||
|
||||
from ..audio_logging import debug_exc_log
|
||||
from ..sql_statements import (
|
||||
LAVALINK_CREATE_INDEX,
|
||||
LAVALINK_CREATE_TABLE,
|
||||
LAVALINK_DELETE_OLD_ENTRIES,
|
||||
LAVALINK_FETCH_ALL_ENTRIES_GLOBAL,
|
||||
LAVALINK_QUERY,
|
||||
LAVALINK_QUERY_ALL,
|
||||
LAVALINK_QUERY_LAST_FETCHED_RANDOM,
|
||||
LAVALINK_UPDATE,
|
||||
LAVALINK_UPSERT,
|
||||
SPOTIFY_CREATE_INDEX,
|
||||
SPOTIFY_CREATE_TABLE,
|
||||
SPOTIFY_DELETE_OLD_ENTRIES,
|
||||
SPOTIFY_QUERY,
|
||||
SPOTIFY_QUERY_ALL,
|
||||
SPOTIFY_QUERY_LAST_FETCHED_RANDOM,
|
||||
SPOTIFY_UPDATE,
|
||||
SPOTIFY_UPSERT,
|
||||
YOUTUBE_CREATE_INDEX,
|
||||
YOUTUBE_CREATE_TABLE,
|
||||
YOUTUBE_DELETE_OLD_ENTRIES,
|
||||
YOUTUBE_QUERY,
|
||||
YOUTUBE_QUERY_ALL,
|
||||
YOUTUBE_QUERY_LAST_FETCHED_RANDOM,
|
||||
YOUTUBE_UPDATE,
|
||||
YOUTUBE_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 (
|
||||
LavalinkCacheFetchForGlobalResult,
|
||||
LavalinkCacheFetchResult,
|
||||
SpotifyCacheFetchResult,
|
||||
YouTubeCacheFetchResult,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.LocalDB")
|
||||
|
||||
_SCHEMA_VERSION = 3
|
||||
|
||||
|
||||
class BaseWrapper:
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, conn: APSWConnectionWrapper, cog: Union["Audio", Cog]
|
||||
):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.database = conn
|
||||
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.fetch_result: Optional[Callable] = None
|
||||
self.cog = cog
|
||||
|
||||
async def init(self) -> None:
|
||||
"""Initialize the local cache"""
|
||||
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.maybe_migrate)
|
||||
executor.submit(self.database.cursor().execute, LAVALINK_CREATE_TABLE)
|
||||
executor.submit(self.database.cursor().execute, LAVALINK_CREATE_INDEX)
|
||||
executor.submit(self.database.cursor().execute, YOUTUBE_CREATE_TABLE)
|
||||
executor.submit(self.database.cursor().execute, YOUTUBE_CREATE_INDEX)
|
||||
executor.submit(self.database.cursor().execute, SPOTIFY_CREATE_TABLE)
|
||||
executor.submit(self.database.cursor().execute, SPOTIFY_CREATE_INDEX)
|
||||
await self.clean_up_old_entries()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the connection with the local cache"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.database.close()
|
||||
|
||||
async def clean_up_old_entries(self) -> None:
|
||||
"""Delete entries older than x in the local cache tables"""
|
||||
max_age = await self.config.cache_age()
|
||||
maxage = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=max_age)
|
||||
maxage_int = int(time.mktime(maxage.timetuple()))
|
||||
values = {"maxage": maxage_int}
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, LAVALINK_DELETE_OLD_ENTRIES, values)
|
||||
executor.submit(self.database.cursor().execute, YOUTUBE_DELETE_OLD_ENTRIES, values)
|
||||
executor.submit(self.database.cursor().execute, SPOTIFY_DELETE_OLD_ENTRIES, values)
|
||||
|
||||
def maybe_migrate(self) -> None:
|
||||
"""Maybe migrate Database schema for the local cache"""
|
||||
current_version = 0
|
||||
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_user_version)]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
current_version = row_result.fetchone()
|
||||
break
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed fetch from database")
|
||||
if isinstance(current_version, tuple):
|
||||
current_version = current_version[0]
|
||||
if current_version == _SCHEMA_VERSION:
|
||||
return
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
self.statement.set_user_version,
|
||||
{"version": _SCHEMA_VERSION},
|
||||
)
|
||||
|
||||
async def insert(self, values: List[MutableMapping]) -> None:
|
||||
"""Insert an entry into the local cache"""
|
||||
try:
|
||||
with self.database.transaction() as transaction:
|
||||
transaction.executemany(self.statement.upsert, values)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Error during table insert")
|
||||
|
||||
async def update(self, values: MutableMapping) -> None:
|
||||
"""Update an entry of the local cache"""
|
||||
|
||||
try:
|
||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
values["last_fetched"] = time_now
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, self.statement.update, values)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Error during table update")
|
||||
|
||||
async def _fetch_one(
|
||||
self, values: MutableMapping
|
||||
) -> Optional[
|
||||
Union[LavalinkCacheFetchResult, SpotifyCacheFetchResult, YouTubeCacheFetchResult]
|
||||
]:
|
||||
"""Get an entry from the local cache"""
|
||||
max_age = await self.config.cache_age()
|
||||
maxage = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=max_age)
|
||||
maxage_int = int(time.mktime(maxage.timetuple()))
|
||||
values.update({"maxage": maxage_int})
|
||||
row = None
|
||||
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_one, values)]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
row = row_result.fetchone()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed fetch from database")
|
||||
if not row:
|
||||
return None
|
||||
if self.fetch_result is None:
|
||||
return None
|
||||
return self.fetch_result(*row)
|
||||
|
||||
async def _fetch_all(
|
||||
self, values: MutableMapping
|
||||
) -> List[Union[LavalinkCacheFetchResult, SpotifyCacheFetchResult, YouTubeCacheFetchResult]]:
|
||||
"""Get all entries from the local cache"""
|
||||
output = []
|
||||
row_result = []
|
||||
if self.fetch_result is None:
|
||||
return []
|
||||
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, values)]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed fetch from database")
|
||||
async for row in AsyncIter(row_result):
|
||||
output.append(self.fetch_result(*row))
|
||||
return output
|
||||
|
||||
async def _fetch_random(
|
||||
self, values: MutableMapping
|
||||
) -> Optional[
|
||||
Union[LavalinkCacheFetchResult, SpotifyCacheFetchResult, YouTubeCacheFetchResult]
|
||||
]:
|
||||
"""Get a random entry from the local cache"""
|
||||
row = None
|
||||
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_random, values
|
||||
)
|
||||
]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
rows = row_result.fetchall()
|
||||
if rows:
|
||||
row = random.choice(rows)
|
||||
else:
|
||||
row = None
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed random fetch from database")
|
||||
if not row:
|
||||
return None
|
||||
if self.fetch_result is None:
|
||||
return None
|
||||
return self.fetch_result(*row)
|
||||
|
||||
|
||||
class YouTubeTableWrapper(BaseWrapper):
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, conn: APSWConnectionWrapper, cog: Union["Audio", Cog]
|
||||
):
|
||||
super().__init__(bot, config, conn, cog)
|
||||
self.statement.upsert = YOUTUBE_UPSERT
|
||||
self.statement.update = YOUTUBE_UPDATE
|
||||
self.statement.get_one = YOUTUBE_QUERY
|
||||
self.statement.get_all = YOUTUBE_QUERY_ALL
|
||||
self.statement.get_random = YOUTUBE_QUERY_LAST_FETCHED_RANDOM
|
||||
self.fetch_result = YouTubeCacheFetchResult
|
||||
|
||||
async def fetch_one(
|
||||
self, values: MutableMapping
|
||||
) -> Tuple[Optional[str], Optional[datetime.datetime]]:
|
||||
"""Get an entry from the Youtube table"""
|
||||
result = await self._fetch_one(values)
|
||||
if not result or not isinstance(result.query, str):
|
||||
return None, None
|
||||
return result.query, result.updated_on
|
||||
|
||||
async def fetch_all(self, values: MutableMapping) -> List[YouTubeCacheFetchResult]:
|
||||
"""Get all entries from the Youtube table"""
|
||||
result = await self._fetch_all(values)
|
||||
if result and isinstance(result[0], YouTubeCacheFetchResult):
|
||||
return result
|
||||
return []
|
||||
|
||||
async def fetch_random(self, values: MutableMapping) -> Optional[str]:
|
||||
"""Get a random entry from the Youtube table"""
|
||||
result = await self._fetch_random(values)
|
||||
if not result or not isinstance(result.query, str):
|
||||
return None
|
||||
return result.query
|
||||
|
||||
|
||||
class SpotifyTableWrapper(BaseWrapper):
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, conn: APSWConnectionWrapper, cog: Union["Audio", Cog]
|
||||
):
|
||||
super().__init__(bot, config, conn, cog)
|
||||
self.statement.upsert = SPOTIFY_UPSERT
|
||||
self.statement.update = SPOTIFY_UPDATE
|
||||
self.statement.get_one = SPOTIFY_QUERY
|
||||
self.statement.get_all = SPOTIFY_QUERY_ALL
|
||||
self.statement.get_random = SPOTIFY_QUERY_LAST_FETCHED_RANDOM
|
||||
self.fetch_result = SpotifyCacheFetchResult
|
||||
|
||||
async def fetch_one(
|
||||
self, values: MutableMapping
|
||||
) -> Tuple[Optional[str], Optional[datetime.datetime]]:
|
||||
"""Get an entry from the Spotify table"""
|
||||
result = await self._fetch_one(values)
|
||||
if not result or not isinstance(result.query, str):
|
||||
return None, None
|
||||
return result.query, result.updated_on
|
||||
|
||||
async def fetch_all(self, values: MutableMapping) -> List[SpotifyCacheFetchResult]:
|
||||
"""Get all entries from the Spotify table"""
|
||||
result = await self._fetch_all(values)
|
||||
if result and isinstance(result[0], SpotifyCacheFetchResult):
|
||||
return result
|
||||
return []
|
||||
|
||||
async def fetch_random(self, values: MutableMapping) -> Optional[str]:
|
||||
"""Get a random entry from the Spotify table"""
|
||||
result = await self._fetch_random(values)
|
||||
if not result or not isinstance(result.query, str):
|
||||
return None
|
||||
return result.query
|
||||
|
||||
|
||||
class LavalinkTableWrapper(BaseWrapper):
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, conn: APSWConnectionWrapper, cog: Union["Audio", Cog]
|
||||
):
|
||||
super().__init__(bot, config, conn, cog)
|
||||
self.statement.upsert = LAVALINK_UPSERT
|
||||
self.statement.update = LAVALINK_UPDATE
|
||||
self.statement.get_one = LAVALINK_QUERY
|
||||
self.statement.get_all = LAVALINK_QUERY_ALL
|
||||
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
|
||||
|
||||
async def fetch_one(
|
||||
self, values: MutableMapping
|
||||
) -> Tuple[Optional[MutableMapping], Optional[datetime.datetime]]:
|
||||
"""Get an entry from the Lavalink table"""
|
||||
result = await self._fetch_one(values)
|
||||
if not result or not isinstance(result.query, dict):
|
||||
return None, None
|
||||
return result.query, result.updated_on
|
||||
|
||||
async def fetch_all(self, values: MutableMapping) -> List[LavalinkCacheFetchResult]:
|
||||
"""Get all entries from the Lavalink table"""
|
||||
result = await self._fetch_all(values)
|
||||
if result and isinstance(result[0], LavalinkCacheFetchResult):
|
||||
return result
|
||||
return []
|
||||
|
||||
async def fetch_random(self, values: MutableMapping) -> Optional[MutableMapping]:
|
||||
"""Get a random entry from the Lavalink table"""
|
||||
result = await self._fetch_random(values)
|
||||
if not result or not isinstance(result.query, dict):
|
||||
return None
|
||||
return result.query
|
||||
|
||||
async def fetch_all_for_global(self) -> List[LavalinkCacheFetchForGlobalResult]:
|
||||
"""Get all entries from the Lavalink table"""
|
||||
output: List[LavalinkCacheFetchForGlobalResult] = []
|
||||
row_result = []
|
||||
if self.fetch_for_global is None:
|
||||
return []
|
||||
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_global)]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed fetch from database")
|
||||
async for row in AsyncIter(row_result):
|
||||
output.append(self.fetch_for_global(*row))
|
||||
return output
|
||||
|
||||
|
||||
class LocalCacheWrapper:
|
||||
"""Wraps all table apis into 1 object representing the local cache"""
|
||||
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, conn: APSWConnectionWrapper, cog: Union["Audio", Cog]
|
||||
):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.database = conn
|
||||
self.cog = cog
|
||||
self.lavalink: LavalinkTableWrapper = LavalinkTableWrapper(bot, config, conn, self.cog)
|
||||
self.spotify: SpotifyTableWrapper = SpotifyTableWrapper(bot, config, conn, self.cog)
|
||||
self.youtube: YouTubeTableWrapper = YouTubeTableWrapper(bot, config, conn, self.cog)
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Afrikaans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: af\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: af_ZA\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Arabic\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ar\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: ar_SA\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: bg\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: bg_BG\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ca\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: ca_ES\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: cs\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: cs_CZ\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API klíč nebo klientský tajný klíč nebyl správně nastaven. \\nPro pokyny použijte `{prefix}audioset spotifyapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "Nezdá se, že by to byla platná adresa Spotify playlistu/alba nebo kód."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Pravděpodobně se nejedná o podporovaný Spotify odkaz nebo kód."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "Připojení bylo obnoveno při načítání seznamu skladeb."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Vypršel časový limit přehrávače, přeskakuji zbývající skladby."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Nepodařilo se získat skladby, zbývá přeskakování."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Nic nenalezeno.\\nYouTube API klíč může být neplatný nebo může být omezen na YouTube's vyhledávací službu.\\nPodívejte se znovu na YouTube API klíč a postupujte podle instrukcí na `{prefix}audioset youtubeapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr " {bad_tracks} skladby nemůžou být zařazeny do fronty."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Playlist zařazen do fronty"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "Přidáno {num} skladeb do fronty.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} do začátku přehrávání playlistu: je na #{position} pozici ve frontě"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API klíč nebo klientský tajný klíč nebyl správně nastaven. \\nPro pokyny použijte `{prefix}audioset spotifyapi`."
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: da\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: da_DK\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: de\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: de_DE\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Der Spotify API Key oder das Client secret wurden nicht richtig eingestellt.\\n Benutze `{prefix}audioset spotifyapi` für eine Anleitung."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "Dies scheint keine gültige Spotify-Playlist/Album-URL oder Spotify-Code zu sein."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Dies scheint keine unterstützte Spotify-URL oder Spotify-Code zu sein."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "Die Verbindung wurde zurückgesetzt beim Laden der Playlist."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Audioplayer-Timeout. Verbleibende Titel werden übersprungen."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Fehler beim laden der Tracks. Verbleibende Tracks werden übersprungen."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Nichts Gefunden.\\n Der Youtube API Key könnte falsch sein oder du überschreitest das Rate Limit der Youtube Suche.\\n Kontrollieren den Youtube API Key nocheinmal und dann folge der Anleitung bei `{prefix}audioset youtubeapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr " {bad_tracks} Tracks können nicht zur Warteschlange hinzugefügt werden."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Wiedergabeliste eingereiht"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "Es wurden {num} Tracks zu der Playlist hinzugefügt.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} bis zum Start der Wiedergabeliste: beginnt bei #{position} in der Warteschlange"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Der Spotify API Key oder dar Client secret wurden nicht richtig eingestellt.\\n Benutze `{prefix}audioset spotifyapi` für eine Anleitung."
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Greek\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: el\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: el_GR\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: es-ES\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: es_ES\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Lista de reproducción en cola"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Finnish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: fi\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: fi_FI\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: fr\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: fr_FR\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "La clé API de Spotify ou le secret client n'ont pas étés correctement définis. \\nUtilisez `{prefix}audioset spotifyapi` pour connaître la marche à suivre."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "Cela ne semble pas être une URL ou un album/playlist Spotify valide."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Cela ne semble pas être une URL ou un code Spotify pris en charge."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "La connexion a été réinitialisée lors du chargement de la playlist."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Arrêt du lecteur, pistes restantes ignoré."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Impossible d'obtenir les pistes, pistes ignoré."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Rien n'a été trouvé.\\nLa clé de l'API YouTube peut être invalide ou vous pouvez être limité sur le service de recherche de YouTube.\\nVérifiez à nouveau la clé de l'API YouTube et suivez les instructions à `{prefix}audioset youtubeapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr " {bad_tracks} pistes ne peuvent pas être mises en attente."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Playlist en file d’attente"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "Ajout de {num} pistes à la file d'attente.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} avant le début de la lecture de la playlist : commence à #{position} dans la liste"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "La clé API de Spotify ou le secret client n'ont pas étés correctement définis. \\nUtilisez `{prefix}audioset spotifyapi` pour connaître la marche à suivre."
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hebrew\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: he\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: he_IL\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: hu\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: hu_HU\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Indonesian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: id\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: id_ID\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: it\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: it_IT\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ja\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: ja_JP\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Korean\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ko\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: ko_KR\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API 키 또는 client secret이 올바르게 설정되지 않았습니다. \\n`{prefix}audioset spotifyapi`을 사용하여 지침을 확인하세요."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "유효한 Spotify 재생 목록, 앨범 URL 또는 코드가 아닌것 같습니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "지원되는 Spotify URL 또는 코드가 아닌 것 같습니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "재생 목록을 로드하는 동안 연결이 재설정되었습니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "플래이어 시간이 초과되었습니다. 남은 트랙들을 건너 뜁니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "트랙을 추가하지 못해 나머지는 건너 뜁니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "검색 결과가 없습니다.\\n 유튜브 API키가 유효하지 않거나 유튜브 검색 서비스 제한이 걸렸을 수도 있습니다. \\n유튜브 API 키를 다시 확인하고 `{prefix}audioset youtubeapi`.에서 지침을 확인하세요."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr " {bad_tracks} 을 대기열에 추가할 수 없습니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "대기중인 재생 목록"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "대기열에 {num} 개의 트랙이 추가되었습니다. {maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "재생 목록 재생이 시작될 때까지 {time} 남았습니다.: 대기열의 #{position} 에 시작합니다."
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "You deleted the translation \"Spotify API 키 또는 client secret이 올바르게 설정되지 않았습니다. \\n`{prefix}audioset spotifyapi`를 사용하여 명령어들을 확인하세요.\""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: nl\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: nl_NL\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Afspeellijst toegevoegd"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} tot het begin van het afspelen van de afspeellijst: begint bij #{position} in de wachtrij"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Norwegian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: no\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: no_NO\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: pl\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: pl_PL\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: pt-BR\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: pt_BR\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Isto não parece ser uma URL ou código do Spotify válido."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "A conexão foi redefinida durante o carregamento da lista de reprodução."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Tempo limite do reprodutor atingido; saltando as faixas restantes."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Falha ao obter as faixas; saltando as faixas restantes."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Nada encontrado.\\nA chave de API do YouTube pode ser inválida ou você pode estar sendo limitado pelas cotas do serviço de busca do YouTube.\\nVerifique a chave de API do YouTube novamente e siga as instruções em `{prefix}audioset youtubeapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Lista de reprodução enfileirada"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "{num} faixas enfileiradas.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} até o início da reprodução da lista: começa na posição #{position} da fila"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: pt-PT\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: pt_PT\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Romanian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ro\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: ro_RO\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ru\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: ru_RU\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "API ключ Spotify или секрет клиента были установлены неправильно. \\nДля получения инструкций используйте `{prefix}audioset spotifyapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Это не похоже на поддерживаемый Spotify URL или код."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "Соединение было сброшено при загрузке плейлиста."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Тайм-аут проигрывателя, пропуск оставшихся треков."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Не удалось получить треки, пропускаю оставшиеся треки."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Ничего не найдено.\\nКлюч YouTube API может быть недействительным или вы можете оценить его в поисковой службе YouTube.\\nПроверьте YouTube API еще раз и следуйте инструкциям в `{prefix}audioset youtubeapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Плейлист поставлен в очередь"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "{num} треков добавлено в очередь.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} до начала воспроизведения плейлиста: начинается с #{position} в очереди"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "API ключ Spotify или секрет клиента были установлены неправильно. \\nДля получения инструкций используйте `{prefix}audioset spotifyapi`."
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovak\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: sk\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: sk_SK\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Serbian (Cyrillic)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: sr\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: sr_SP\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: sv-SE\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: sv_SE\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: tr\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: tr_TR\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API veya client secret'ı düzgün bir şekilde ayarlanmamış. \\n `{prefix}audioset spotifyapi` komutundan bilgi alabilirsiniz."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "Bu geçerli bir Spotify çalma listesi / albüm URL'si veya Kodu gibi görünmüyor."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Bu geçerli bir Spotify URL'si ya da kodu gibi gözükmüyor."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "Playlist yüklenirken bağlantı yenilendi."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Oynatıcı zaman aşımına uğradı, kalan parçalar atlanıyor."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Parça alınamıyor, atlanıyor."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Hiçbir şey bulunamadı.\\nYouTube API keyi yanlış ya da API kullanımınız sınırlandırılmış.\\nYouTube API keyinizi kontrol edin ve `{prefix}audioset youtubeapi`'de ki yönlendirmeleri takip edin."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr " {bad_tracks} parçalar sıraya alınamaz."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Playlist sıraya alındı"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "{num} adet şarkı sıraya eklendi.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "Playlistin başlamasına {time} süre var: #{position} sırasında başlar"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API veya client secret'ı düzgün bir şekilde ayarlanmamış. \\n `{prefix}audioset spotifyapi` komutundan bilgi alabilirsiniz."
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: uk\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: uk_UA\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Vietnamese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: vi\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: vi_VN\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: zh-CN\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: zh_CN\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Traditional, Hong Kong\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: zh-HK\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: zh_HK\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr ""
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Traditional\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: zh-TW\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: zh_TW\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API key或client secret未正確設置。\\n請使用`{prefix} audioset spotifyapi`獲取說明。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "這似乎不是有效的Spotify播放列表/專輯URL或代碼。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "這似乎不是支持的Spotify URL或代碼。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "加載播放列表時重置了連接。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "播放器超時,跳過剩餘歌曲。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "無法取得歌曲,跳過剩餘的歌曲。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "找不到任何內容。\\n您的YouTube API key可能是無效的,或者您在YouTube的搜索服務上受到速率限制。\\n請檢查YouTube API key,然後按照`{prefix}audioset youtubeapi`中的說明進行操作。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr "{bad_tracks}首歌曲加載失敗。"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "已加入播放清單"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "已將{num}首歌曲添加到播放清單中。{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time}後播放: 在播放清單的#{position}首之後"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "Spotify API key或client secret未正確設置。\\n請使用`{prefix} audioset spotifyapi`獲取說明。"
|
||||
|
||||
@ -1,647 +0,0 @@
|
||||
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 ..errors import NotAllowed
|
||||
from ..utils import PlaylistScope
|
||||
from .api_utils import PlaylistFetchResult, prepare_config_scope, standardize_scope
|
||||
from .playlist_wrapper import PlaylistWrapper
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.PlaylistsInterface")
|
||||
|
||||
|
||||
class Playlist:
|
||||
"""A single playlist."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
author: int,
|
||||
playlist_id: int,
|
||||
name: str,
|
||||
playlist_url: Optional[str] = None,
|
||||
tracks: Optional[List[MutableMapping]] = None,
|
||||
guild: Union[discord.Guild, int, None] = None,
|
||||
):
|
||||
self.bot = bot
|
||||
self.guild = guild
|
||||
self.scope = standardize_scope(scope)
|
||||
self.config_scope = prepare_config_scope(self.bot, self.scope, author, guild)
|
||||
self.scope_id = self.config_scope[-1]
|
||||
self.author = author
|
||||
self.author_id = getattr(self.author, "id", self.author)
|
||||
self.guild_id = (
|
||||
getattr(guild, "id", guild) if self.scope == PlaylistScope.GLOBAL.value else None
|
||||
)
|
||||
self.id = playlist_id
|
||||
self.name = name
|
||||
self.url = playlist_url
|
||||
self.tracks = tracks or []
|
||||
self.tracks_obj = [lavalink.Track(data=track) for track in self.tracks]
|
||||
self.playlist_api = playlist_api
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"Playlist(name={self.name}, id={self.id}, scope={self.scope}, "
|
||||
f"scope_id={self.scope_id}, author={self.author_id}, "
|
||||
f"tracks={len(self.tracks)}, url={self.url})"
|
||||
)
|
||||
|
||||
async def edit(self, data: MutableMapping):
|
||||
"""
|
||||
Edits a Playlist.
|
||||
Parameters
|
||||
----------
|
||||
data: dict
|
||||
The attributes to change.
|
||||
"""
|
||||
# Disallow ID editing
|
||||
if "id" in data:
|
||||
raise NotAllowed("Playlist ID cannot be edited.")
|
||||
|
||||
for item in list(data.keys()):
|
||||
setattr(self, item, data[item])
|
||||
await self.save()
|
||||
return self
|
||||
|
||||
async def save(self):
|
||||
"""Saves a Playlist."""
|
||||
scope, scope_id = self.config_scope
|
||||
await self.playlist_api.upsert(
|
||||
scope,
|
||||
playlist_id=int(self.id),
|
||||
playlist_name=self.name,
|
||||
scope_id=scope_id,
|
||||
author_id=self.author_id,
|
||||
playlist_url=self.url,
|
||||
tracks=self.tracks,
|
||||
)
|
||||
|
||||
def to_json(self) -> MutableMapping:
|
||||
"""Transform the object to a dict.
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The playlist in the form of a dict.
|
||||
"""
|
||||
data = dict(
|
||||
id=self.id,
|
||||
author=self.author_id,
|
||||
guild=self.guild_id,
|
||||
name=self.name,
|
||||
playlist_url=self.url,
|
||||
tracks=self.tracks,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
async def from_json(
|
||||
cls,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
playlist_number: int,
|
||||
data: PlaylistFetchResult,
|
||||
**kwargs,
|
||||
) -> "Playlist":
|
||||
"""Get a Playlist object from the provided information.
|
||||
Parameters
|
||||
----------
|
||||
bot: Red
|
||||
The bot's instance. Needed to get the target user.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
scope:str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
playlist_number: int
|
||||
The playlist's number.
|
||||
data: PlaylistFetchResult
|
||||
The PlaylistFetchResult representation of the playlist to be gotten.
|
||||
**kwargs
|
||||
Extra attributes for the Playlist instance which override values
|
||||
in the data dict. These should be complete objects and not
|
||||
IDs, where possible.
|
||||
Returns
|
||||
-------
|
||||
Playlist
|
||||
The playlist object for the requested playlist.
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
guild = data.scope_id if scope == PlaylistScope.GUILD.value else kwargs.get("guild")
|
||||
author = data.author_id
|
||||
playlist_id = data.playlist_id or playlist_number
|
||||
name = data.playlist_name
|
||||
playlist_url = data.playlist_url
|
||||
tracks = data.tracks
|
||||
|
||||
return cls(
|
||||
bot=bot,
|
||||
playlist_api=playlist_api,
|
||||
guild=guild,
|
||||
scope=scope,
|
||||
author=author,
|
||||
playlist_id=playlist_id,
|
||||
name=name,
|
||||
playlist_url=playlist_url,
|
||||
tracks=tracks,
|
||||
)
|
||||
|
||||
|
||||
class PlaylistCompat23:
|
||||
"""A single playlist, migrating from Schema 2 to Schema 3"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
author: int,
|
||||
playlist_id: int,
|
||||
name: str,
|
||||
playlist_url: Optional[str] = None,
|
||||
tracks: Optional[List[MutableMapping]] = None,
|
||||
guild: Union[discord.Guild, int, None] = None,
|
||||
):
|
||||
|
||||
self.bot = bot
|
||||
self.guild = guild
|
||||
self.scope = standardize_scope(scope)
|
||||
self.author = author
|
||||
self.id = playlist_id
|
||||
self.name = name
|
||||
self.url = playlist_url
|
||||
self.tracks = tracks or []
|
||||
|
||||
self.playlist_api = playlist_api
|
||||
|
||||
@classmethod
|
||||
async def from_json(
|
||||
cls,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
playlist_number: int,
|
||||
data: MutableMapping,
|
||||
**kwargs,
|
||||
) -> "PlaylistCompat23":
|
||||
"""Get a Playlist object from the provided information.
|
||||
Parameters
|
||||
----------
|
||||
bot: Red
|
||||
The Bot instance.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
scope:str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
playlist_number: int
|
||||
The playlist's number.
|
||||
data: MutableMapping
|
||||
The JSON representation of the playlist to be gotten.
|
||||
**kwargs
|
||||
Extra attributes for the Playlist instance which override values
|
||||
in the data dict. These should be complete objects and not
|
||||
IDs, where possible.
|
||||
Returns
|
||||
-------
|
||||
Playlist
|
||||
The playlist object for the requested playlist.
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
guild = data.get("guild") or kwargs.get("guild")
|
||||
author: int = data.get("author") or 0
|
||||
playlist_id = data.get("id") or playlist_number
|
||||
name = data.get("name", "Unnamed")
|
||||
playlist_url = data.get("playlist_url", None)
|
||||
tracks = data.get("tracks", [])
|
||||
|
||||
return cls(
|
||||
bot=bot,
|
||||
playlist_api=playlist_api,
|
||||
guild=guild,
|
||||
scope=scope,
|
||||
author=author,
|
||||
playlist_id=playlist_id,
|
||||
name=name,
|
||||
playlist_url=playlist_url,
|
||||
tracks=tracks,
|
||||
)
|
||||
|
||||
async def save(self):
|
||||
"""Saves a Playlist to SQL."""
|
||||
scope, scope_id = prepare_config_scope(self.bot, self.scope, self.author, self.guild)
|
||||
await self.playlist_api.upsert(
|
||||
scope,
|
||||
playlist_id=int(self.id),
|
||||
playlist_name=self.name,
|
||||
scope_id=scope_id,
|
||||
author_id=self.author,
|
||||
playlist_url=self.url,
|
||||
tracks=self.tracks,
|
||||
)
|
||||
|
||||
|
||||
async def get_all_playlist_for_migration23(
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
config: Config,
|
||||
scope: str,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
) -> List[PlaylistCompat23]:
|
||||
"""
|
||||
Gets all playlist for the specified scope.
|
||||
Parameters
|
||||
----------
|
||||
bot: Red
|
||||
The Bot instance.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
config: Config
|
||||
The Audio cog Config instance.
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
guild: discord.Guild
|
||||
The guild to get the playlist from if scope is GUILDPLAYLIST.
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
A list of all playlists for the specified scope
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
playlists = await config.custom(scope).all()
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
return [
|
||||
await PlaylistCompat23.from_json(
|
||||
bot,
|
||||
playlist_api,
|
||||
scope,
|
||||
playlist_number,
|
||||
playlist_data,
|
||||
guild=guild,
|
||||
author=int(playlist_data.get("author", 0)),
|
||||
)
|
||||
async for playlist_number, playlist_data in AsyncIter(playlists.items())
|
||||
]
|
||||
elif scope == PlaylistScope.USER.value:
|
||||
return [
|
||||
await PlaylistCompat23.from_json(
|
||||
bot,
|
||||
playlist_api,
|
||||
scope,
|
||||
playlist_number,
|
||||
playlist_data,
|
||||
guild=guild,
|
||||
author=int(user_id),
|
||||
)
|
||||
async for user_id, scopedata in AsyncIter(playlists.items())
|
||||
async for playlist_number, playlist_data in AsyncIter(scopedata.items())
|
||||
]
|
||||
else:
|
||||
return [
|
||||
await PlaylistCompat23.from_json(
|
||||
bot,
|
||||
playlist_api,
|
||||
scope,
|
||||
playlist_number,
|
||||
playlist_data,
|
||||
guild=int(guild_id),
|
||||
author=int(playlist_data.get("author", 0)),
|
||||
)
|
||||
async for guild_id, scopedata in AsyncIter(playlists.items())
|
||||
async for playlist_number, playlist_data in AsyncIter(scopedata.items())
|
||||
]
|
||||
|
||||
|
||||
async def get_playlist(
|
||||
playlist_number: int,
|
||||
scope: str,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
) -> Playlist:
|
||||
"""
|
||||
Gets the playlist with the associated playlist number.
|
||||
Parameters
|
||||
----------
|
||||
playlist_number: int
|
||||
The playlist number for the playlist to get.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
guild: discord.Guild
|
||||
The guild to get the playlist from if scope is GUILDPLAYLIST.
|
||||
author: int
|
||||
The ID of the user to get the playlist from if scope is USERPLAYLIST.
|
||||
bot: Red
|
||||
The bot's instance.
|
||||
Returns
|
||||
-------
|
||||
Playlist
|
||||
The playlist associated with the playlist number.
|
||||
Raises
|
||||
------
|
||||
`RuntimeError`
|
||||
If there is no playlist for the specified number.
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
scope_standard, scope_id = prepare_config_scope(bot, scope, author, guild)
|
||||
playlist_data = await playlist_api.fetch(scope_standard, playlist_number, scope_id)
|
||||
|
||||
if not (playlist_data and playlist_data.playlist_id):
|
||||
raise RuntimeError(f"That playlist does not exist for the following scope: {scope}")
|
||||
return await Playlist.from_json(
|
||||
bot,
|
||||
playlist_api,
|
||||
scope_standard,
|
||||
playlist_number,
|
||||
playlist_data,
|
||||
guild=guild,
|
||||
author=author,
|
||||
)
|
||||
|
||||
|
||||
async def get_all_playlist(
|
||||
scope: str,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
specified_user: bool = False,
|
||||
) -> List[Playlist]:
|
||||
"""
|
||||
Gets all playlist for the specified scope.
|
||||
Parameters
|
||||
----------
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
guild: discord.Guild
|
||||
The guild to get the playlist from if scope is GUILDPLAYLIST.
|
||||
author: int
|
||||
The ID of the user to get the playlist from if scope is USERPLAYLIST.
|
||||
bot: Red
|
||||
The bot's instance
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
specified_user:bool
|
||||
Whether or not user ID was passed as an argparse.
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
A list of all playlists for the specified scope
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
scope_standard, scope_id = prepare_config_scope(bot, scope, author, guild)
|
||||
|
||||
if specified_user:
|
||||
user_id = getattr(author, "id", author)
|
||||
playlists = await playlist_api.fetch_all(scope_standard, scope_id, author_id=user_id)
|
||||
else:
|
||||
playlists = await playlist_api.fetch_all(scope_standard, scope_id)
|
||||
|
||||
playlist_list = []
|
||||
async for playlist in AsyncIter(playlists):
|
||||
playlist_list.append(
|
||||
await Playlist.from_json(
|
||||
bot,
|
||||
playlist_api,
|
||||
scope,
|
||||
playlist.playlist_id,
|
||||
playlist,
|
||||
guild=guild,
|
||||
author=author,
|
||||
)
|
||||
)
|
||||
return playlist_list
|
||||
|
||||
|
||||
async def get_all_playlist_converter(
|
||||
scope: str,
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
arg: str,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
) -> List[Playlist]:
|
||||
"""
|
||||
Gets all playlist for the specified scope.
|
||||
Parameters
|
||||
----------
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
guild: discord.Guild
|
||||
The guild to get the playlist from if scope is GUILDPLAYLIST.
|
||||
author: int
|
||||
The ID of the user to get the playlist from if scope is USERPLAYLIST.
|
||||
bot: Red
|
||||
The bot's instance
|
||||
arg:str
|
||||
The value to lookup.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
A list of all playlists for the specified scope
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
scope_standard, scope_id = prepare_config_scope(bot, scope, author, guild)
|
||||
playlists = await playlist_api.fetch_all_converter(
|
||||
scope_standard, playlist_name=arg, playlist_id=arg
|
||||
)
|
||||
playlist_list = []
|
||||
async for playlist in AsyncIter(playlists):
|
||||
playlist_list.append(
|
||||
await Playlist.from_json(
|
||||
bot,
|
||||
playlist_api,
|
||||
scope,
|
||||
playlist.playlist_id,
|
||||
playlist,
|
||||
guild=guild,
|
||||
author=author,
|
||||
)
|
||||
)
|
||||
return playlist_list
|
||||
|
||||
|
||||
async def create_playlist(
|
||||
ctx: commands.Context,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
playlist_name: str,
|
||||
playlist_url: Optional[str] = None,
|
||||
tracks: Optional[List[MutableMapping]] = None,
|
||||
author: Optional[discord.User] = None,
|
||||
guild: Optional[discord.Guild] = None,
|
||||
) -> Optional[Playlist]:
|
||||
"""Creates a new Playlist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: commands.Context
|
||||
The context in which the play list is being created.
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
playlist_name: str
|
||||
The name of the new playlist.
|
||||
playlist_url:str
|
||||
the url of the new playlist.
|
||||
tracks: List[MutableMapping]
|
||||
A list of tracks to add to the playlist.
|
||||
author: discord.User
|
||||
The Author of the playlist.
|
||||
If provided it will create a playlist under this user.
|
||||
This is only required when creating a playlist in User scope.
|
||||
guild: discord.Guild
|
||||
The guild to create this playlist under.
|
||||
This is only used when creating a playlist in the Guild scope
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
|
||||
playlist = Playlist(
|
||||
ctx.bot,
|
||||
playlist_api,
|
||||
scope,
|
||||
author.id if author else None,
|
||||
ctx.message.id,
|
||||
playlist_name,
|
||||
playlist_url,
|
||||
tracks,
|
||||
guild or ctx.guild,
|
||||
)
|
||||
await playlist.save()
|
||||
return playlist
|
||||
|
||||
|
||||
async def reset_playlist(
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
guild: Union[discord.Guild, int] = None,
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
) -> None:
|
||||
"""Wipes all playlists for the specified scope.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bot: Red
|
||||
The bot's instance
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
guild: discord.Guild
|
||||
The guild to get the playlist from if scope is GUILDPLAYLIST.
|
||||
author: int
|
||||
The ID of the user to get the playlist from if scope is USERPLAYLIST.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
scope, scope_id = prepare_config_scope(bot, scope, author, guild)
|
||||
await playlist_api.drop(scope)
|
||||
await playlist_api.create_table()
|
||||
|
||||
|
||||
async def delete_playlist(
|
||||
bot: Red,
|
||||
playlist_api: PlaylistWrapper,
|
||||
scope: str,
|
||||
playlist_id: Union[str, int],
|
||||
guild: discord.Guild,
|
||||
author: Union[discord.abc.User, int] = None,
|
||||
) -> None:
|
||||
"""Deletes the specified playlist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bot: Red
|
||||
The bot's instance
|
||||
scope: str
|
||||
The custom config scope. One of 'GLOBALPLAYLIST', 'GUILDPLAYLIST' or 'USERPLAYLIST'.
|
||||
playlist_id: Union[str, int]
|
||||
The ID of the playlist.
|
||||
guild: discord.Guild
|
||||
The guild to get the playlist from if scope is GUILDPLAYLIST.
|
||||
author: int
|
||||
The ID of the user to get the playlist from if scope is USERPLAYLIST.
|
||||
playlist_api: PlaylistWrapper
|
||||
The Playlist API interface.
|
||||
|
||||
Raises
|
||||
------
|
||||
`InvalidPlaylistScope`
|
||||
Passing a scope that is not supported.
|
||||
`MissingGuild`
|
||||
Trying to access the Guild scope without a guild.
|
||||
`MissingAuthor`
|
||||
Trying to access the User scope without an user id.
|
||||
"""
|
||||
scope, scope_id = prepare_config_scope(bot, scope, author, guild)
|
||||
await playlist_api.delete(scope, int(playlist_id), scope_id)
|
||||
@ -1,249 +0,0 @@
|
||||
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.dbtools import APSWConnectionWrapper
|
||||
|
||||
from ..audio_logging import debug_exc_log
|
||||
from ..sql_statements import (
|
||||
PLAYLIST_CREATE_INDEX,
|
||||
PLAYLIST_CREATE_TABLE,
|
||||
PLAYLIST_DELETE,
|
||||
PLAYLIST_DELETE_SCHEDULED,
|
||||
PLAYLIST_DELETE_SCOPE,
|
||||
PLAYLIST_FETCH,
|
||||
PLAYLIST_FETCH_ALL,
|
||||
PLAYLIST_FETCH_ALL_CONVERTER,
|
||||
PLAYLIST_FETCH_ALL_WITH_FILTER,
|
||||
PLAYLIST_UPSERT,
|
||||
PRAGMA_FETCH_user_version,
|
||||
PRAGMA_SET_journal_mode,
|
||||
PRAGMA_SET_read_uncommitted,
|
||||
PRAGMA_SET_temp_store,
|
||||
PRAGMA_SET_user_version,
|
||||
)
|
||||
from ..utils import PlaylistScope
|
||||
from .api_utils import PlaylistFetchResult
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.Playlists")
|
||||
|
||||
|
||||
class PlaylistWrapper:
|
||||
def __init__(self, bot: Red, config: Config, conn: APSWConnectionWrapper):
|
||||
self.bot = bot
|
||||
self.database = conn
|
||||
self.config = config
|
||||
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 = PLAYLIST_CREATE_TABLE
|
||||
self.statement.create_index = PLAYLIST_CREATE_INDEX
|
||||
|
||||
self.statement.upsert = PLAYLIST_UPSERT
|
||||
self.statement.delete = PLAYLIST_DELETE
|
||||
self.statement.delete_scope = PLAYLIST_DELETE_SCOPE
|
||||
self.statement.delete_scheduled = PLAYLIST_DELETE_SCHEDULED
|
||||
|
||||
self.statement.get_one = PLAYLIST_FETCH
|
||||
self.statement.get_all = PLAYLIST_FETCH_ALL
|
||||
self.statement.get_all_with_filter = PLAYLIST_FETCH_ALL_WITH_FILTER
|
||||
self.statement.get_all_converter = PLAYLIST_FETCH_ALL_CONVERTER
|
||||
|
||||
async def init(self) -> None:
|
||||
"""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)
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def get_scope_type(scope: str) -> int:
|
||||
"""Convert a scope to a numerical identifier"""
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
table = 1
|
||||
elif scope == PlaylistScope.USER.value:
|
||||
table = 3
|
||||
else:
|
||||
table = 2
|
||||
return table
|
||||
|
||||
async def fetch(self, scope: str, playlist_id: int, scope_id: int) -> PlaylistFetchResult:
|
||||
"""Fetch a single playlist"""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
|
||||
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_one,
|
||||
(
|
||||
{
|
||||
"playlist_id": playlist_id,
|
||||
"scope_id": scope_id,
|
||||
"scope_type": scope_type,
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed playlist fetch from database")
|
||||
row = row_result.fetchone()
|
||||
if row:
|
||||
row = PlaylistFetchResult(*row)
|
||||
return row
|
||||
|
||||
async def fetch_all(
|
||||
self, scope: str, scope_id: int, author_id=None
|
||||
) -> List[PlaylistFetchResult]:
|
||||
"""Fetch all playlists"""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
output = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
if author_id is not None:
|
||||
for future in concurrent.futures.as_completed(
|
||||
[
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
self.statement.get_all_with_filter,
|
||||
(
|
||||
{
|
||||
"scope_type": scope_type,
|
||||
"scope_id": scope_id,
|
||||
"author_id": author_id,
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed playlist fetch from database")
|
||||
return []
|
||||
else:
|
||||
for future in concurrent.futures.as_completed(
|
||||
[
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
self.statement.get_all,
|
||||
({"scope_type": scope_type, "scope_id": scope_id}),
|
||||
)
|
||||
]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed playlist fetch from database")
|
||||
return []
|
||||
async for row in AsyncIter(row_result):
|
||||
output.append(PlaylistFetchResult(*row))
|
||||
return output
|
||||
|
||||
async def fetch_all_converter(
|
||||
self, scope: str, playlist_name, playlist_id
|
||||
) -> List[PlaylistFetchResult]:
|
||||
"""Fetch all playlists with the specified filter"""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
try:
|
||||
playlist_id = int(playlist_id)
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed converting playlist_id to int")
|
||||
playlist_id = -1
|
||||
|
||||
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_converter,
|
||||
(
|
||||
{
|
||||
"scope_type": scope_type,
|
||||
"playlist_name": playlist_name,
|
||||
"playlist_id": playlist_id,
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
):
|
||||
try:
|
||||
row_result = future.result()
|
||||
except Exception as exc:
|
||||
debug_exc_log(log, exc, "Failed to completed fetch from database")
|
||||
|
||||
async for row in AsyncIter(row_result):
|
||||
output.append(PlaylistFetchResult(*row))
|
||||
return output
|
||||
|
||||
async def delete(self, scope: str, playlist_id: int, scope_id: int):
|
||||
"""Deletes a single playlists"""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
self.statement.delete,
|
||||
({"playlist_id": playlist_id, "scope_id": scope_id, "scope_type": scope_type}),
|
||||
)
|
||||
|
||||
async def delete_scheduled(self):
|
||||
"""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"""
|
||||
scope_type = self.get_scope_type(scope)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(
|
||||
self.database.cursor().execute,
|
||||
self.statement.delete_scope,
|
||||
({"scope_type": scope_type}),
|
||||
)
|
||||
|
||||
async def create_table(self):
|
||||
"""Create the playlist table"""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.submit(self.database.cursor().execute, PLAYLIST_CREATE_TABLE)
|
||||
|
||||
async def upsert(
|
||||
self,
|
||||
scope: str,
|
||||
playlist_id: int,
|
||||
playlist_name: str,
|
||||
scope_id: int,
|
||||
author_id: int,
|
||||
playlist_url: Optional[str],
|
||||
tracks: List[MutableMapping],
|
||||
):
|
||||
"""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(
|
||||
self.database.cursor().execute,
|
||||
self.statement.upsert,
|
||||
{
|
||||
"scope_type": str(scope_type),
|
||||
"playlist_id": int(playlist_id),
|
||||
"playlist_name": str(playlist_name),
|
||||
"scope_id": int(scope_id),
|
||||
"author_id": int(author_id),
|
||||
"playlist_url": playlist_url,
|
||||
"tracks": json.dumps(tracks),
|
||||
},
|
||||
)
|
||||
@ -1,189 +0,0 @@
|
||||
import base64
|
||||
import contextlib
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Mapping, MutableMapping, Optional, TYPE_CHECKING, 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 ..errors import SpotifyFetchError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
_ = Translator("Audio", __file__)
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.Spotify")
|
||||
|
||||
|
||||
CATEGORY_ENDPOINT = "https://api.spotify.com/v1/browse/categories"
|
||||
TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token"
|
||||
ALBUMS_ENDPOINT = "https://api.spotify.com/v1/albums"
|
||||
TRACKS_ENDPOINT = "https://api.spotify.com/v1/tracks"
|
||||
PLAYLISTS_ENDPOINT = "https://api.spotify.com/v1/playlists"
|
||||
|
||||
|
||||
class SpotifyWrapper:
|
||||
"""Wrapper for the Spotify API."""
|
||||
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, session: aiohttp.ClientSession, cog: Union["Audio", Cog]
|
||||
):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.session = session
|
||||
self.spotify_token: Optional[MutableMapping] = None
|
||||
self.client_id: Optional[str] = None
|
||||
self.client_secret: Optional[str] = None
|
||||
self._token: Mapping[str, str] = {}
|
||||
self.cog = cog
|
||||
|
||||
@staticmethod
|
||||
def spotify_format_call(query_type: str, key: str) -> Tuple[str, MutableMapping]:
|
||||
"""Format the spotify endpoint"""
|
||||
params: MutableMapping = {}
|
||||
if query_type == "album":
|
||||
query = f"{ALBUMS_ENDPOINT}/{key}/tracks"
|
||||
elif query_type == "track":
|
||||
query = f"{TRACKS_ENDPOINT}/{key}"
|
||||
else:
|
||||
query = f"{PLAYLISTS_ENDPOINT}/{key}/tracks"
|
||||
return query, params
|
||||
|
||||
async def get_spotify_track_info(
|
||||
self, track_data: MutableMapping, ctx: Context
|
||||
) -> Tuple[str, ...]:
|
||||
"""Extract track info from spotify response"""
|
||||
prefer_lyrics = await self.cog.get_lyrics_status(ctx)
|
||||
track_name = track_data["name"]
|
||||
if prefer_lyrics:
|
||||
track_name = f"{track_name} - lyrics"
|
||||
artist_name = track_data["artists"][0]["name"]
|
||||
track_info = f"{track_name} {artist_name}"
|
||||
song_url = track_data.get("external_urls", {}).get("spotify")
|
||||
uri = track_data["uri"]
|
||||
_id = track_data["id"]
|
||||
_type = track_data["type"]
|
||||
|
||||
return song_url, track_info, uri, artist_name, track_name, _id, _type
|
||||
|
||||
@staticmethod
|
||||
async def is_access_token_valid(token: MutableMapping) -> bool:
|
||||
"""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"""
|
||||
if client_id is None:
|
||||
client_id = ""
|
||||
if client_secret is None:
|
||||
client_secret = ""
|
||||
auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode("ascii"))
|
||||
return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
|
||||
|
||||
async def get(
|
||||
self, url: str, headers: MutableMapping = None, params: MutableMapping = None
|
||||
) -> MutableMapping[str, str]:
|
||||
"""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()
|
||||
if r.status != 200:
|
||||
log.debug(f"Issue making GET request to {url}: [{r.status}] {data}")
|
||||
return data
|
||||
|
||||
def update_token(self, new_token: Mapping[str, str]):
|
||||
self._token = new_token
|
||||
|
||||
async def get_token(self) -> None:
|
||||
"""Get the stored spotify tokens"""
|
||||
if not self._token:
|
||||
self._token = await self.bot.get_shared_api_tokens("spotify")
|
||||
|
||||
self.client_id = self._token.get("client_id", "")
|
||||
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"
|
||||
|
||||
async def request_access_token(self) -> MutableMapping:
|
||||
"""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)
|
||||
r = await self.post(TOKEN_ENDPOINT, payload=payload, headers=headers)
|
||||
return r
|
||||
|
||||
async def get_access_token(self) -> Optional[str]:
|
||||
"""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()
|
||||
if token is None:
|
||||
log.debug("Requested a token from Spotify, did not end up getting one.")
|
||||
try:
|
||||
token["expires_at"] = int(time.time()) + int(token["expires_in"])
|
||||
except KeyError:
|
||||
return None
|
||||
self.spotify_token = token
|
||||
log.debug(f"Created a new access token for Spotify: {token}")
|
||||
return self.spotify_token["access_token"]
|
||||
|
||||
async def post(
|
||||
self, url: str, payload: MutableMapping, headers: MutableMapping = None
|
||||
) -> MutableMapping:
|
||||
"""Make a POST call to spotify"""
|
||||
async with self.session.post(url, data=payload, headers=headers) as r:
|
||||
data = await r.json()
|
||||
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"""
|
||||
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"""
|
||||
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)
|
||||
with contextlib.suppress(KeyError):
|
||||
if result["error"]["status"] == 401:
|
||||
raise SpotifyFetchError(
|
||||
message=_(
|
||||
"The Spotify API key or client secret has not been set properly. "
|
||||
"\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
)
|
||||
)
|
||||
categories = result.get("categories", {}).get("items", [])
|
||||
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"""
|
||||
url = f"{CATEGORY_ENDPOINT}/{category}/playlists"
|
||||
country_code = await self.get_country_code(ctx=ctx)
|
||||
params: MutableMapping = {"country": country_code} if country_code else {}
|
||||
result = await self.make_get_call(url, params=params)
|
||||
playlists = result.get("playlists", {}).get("items", [])
|
||||
return [
|
||||
{
|
||||
"name": c["name"],
|
||||
"uri": c["uri"],
|
||||
"url": c.get("external_urls", {}).get("spotify"),
|
||||
"tracks": c.get("tracks", {}).get("total", "Unknown"),
|
||||
}
|
||||
async for c in AsyncIter(playlists)
|
||||
if c
|
||||
]
|
||||
@ -1,65 +0,0 @@
|
||||
import logging
|
||||
from typing import Mapping, Optional, TYPE_CHECKING, Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog
|
||||
|
||||
from ..errors import YouTubeApiError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Audio
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.api.YouTube")
|
||||
|
||||
SEARCH_ENDPOINT = "https://www.googleapis.com/youtube/v3/search"
|
||||
|
||||
|
||||
class YouTubeWrapper:
|
||||
"""Wrapper for the YouTube Data API."""
|
||||
|
||||
def __init__(
|
||||
self, bot: Red, config: Config, session: aiohttp.ClientSession, cog: Union["Audio", Cog]
|
||||
):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.session = session
|
||||
self.api_key: Optional[str] = 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,) -> 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"""
|
||||
params = {
|
||||
"q": query,
|
||||
"part": "id",
|
||||
"key": await self._get_api_key(),
|
||||
"maxResults": 1,
|
||||
"type": "video",
|
||||
}
|
||||
async with self.session.request("GET", SEARCH_ENDPOINT, params=params) as r:
|
||||
if r.status in [400, 404]:
|
||||
return None
|
||||
elif r.status in [403, 429]:
|
||||
if r.reason == "quotaExceeded":
|
||||
raise YouTubeApiError("Your YouTube Data API quota has been reached.")
|
||||
return None
|
||||
else:
|
||||
search_response = await r.json()
|
||||
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']}"
|
||||
|
||||
return None
|
||||
@ -1,683 +0,0 @@
|
||||
import contextlib
|
||||
import glob
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from pathlib import Path, PosixPath, WindowsPath
|
||||
from typing import (
|
||||
AsyncIterator,
|
||||
Final,
|
||||
Iterator,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
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) ")
|
||||
_RE_YOUTUBE_TIMESTAMP: Final[Pattern] = re.compile(r"[&|?]t=(\d+)s?")
|
||||
_RE_YOUTUBE_INDEX: Final[Pattern] = re.compile(r"&index=(\d+)")
|
||||
_RE_SPOTIFY_URL: Final[Pattern] = re.compile(r"(http[s]?://)?(open.spotify.com)/")
|
||||
_RE_SPOTIFY_TIMESTAMP: Final[Pattern] = re.compile(r"#(\d+):(\d+)")
|
||||
_RE_SOUNDCLOUD_TIMESTAMP: Final[Pattern] = re.compile(r"#t=(\d+):(\d+)s?")
|
||||
_RE_TWITCH_TIMESTAMP: Final[Pattern] = re.compile(r"\?t=(\d+)h(\d+)m(\d+)s")
|
||||
_PATH_SEPS: Final[Tuple[str, str]] = (posixpath.sep, ntpath.sep)
|
||||
|
||||
_FULLY_SUPPORTED_MUSIC_EXT: Final[Tuple[str, ...]] = (".mp3", ".flac", ".ogg")
|
||||
_PARTIALLY_SUPPORTED_MUSIC_EXT: Tuple[str, ...] = (
|
||||
".m3u",
|
||||
".m4a",
|
||||
".aac",
|
||||
".ra",
|
||||
".wav",
|
||||
".opus",
|
||||
".wma",
|
||||
".ts",
|
||||
".au",
|
||||
# These do not work
|
||||
# ".mid",
|
||||
# ".mka",
|
||||
# ".amr",
|
||||
# ".aiff",
|
||||
# ".ac3",
|
||||
# ".voc",
|
||||
# ".dsf",
|
||||
)
|
||||
_PARTIALLY_SUPPORTED_VIDEO_EXT: Tuple[str, ...] = (
|
||||
".mp4",
|
||||
".mov",
|
||||
".flv",
|
||||
".webm",
|
||||
".mkv",
|
||||
".wmv",
|
||||
".3gp",
|
||||
".m4v",
|
||||
".mk3d", # https://github.com/Devoxin/lavaplayer
|
||||
".mka", # https://github.com/Devoxin/lavaplayer
|
||||
".mks", # https://github.com/Devoxin/lavaplayer
|
||||
# These do not work
|
||||
# ".vob",
|
||||
# ".mts",
|
||||
# ".avi",
|
||||
# ".mpg",
|
||||
# ".mpeg",
|
||||
# ".swf",
|
||||
)
|
||||
_PARTIALLY_SUPPORTED_MUSIC_EXT += _PARTIALLY_SUPPORTED_VIDEO_EXT
|
||||
|
||||
|
||||
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`.
|
||||
"""
|
||||
|
||||
_all_music_ext = _FULLY_SUPPORTED_MUSIC_EXT + _PARTIALLY_SUPPORTED_MUSIC_EXT
|
||||
|
||||
def __init__(self, path, localtrack_folder, **kwargs):
|
||||
self._localtrack_folder = localtrack_folder
|
||||
self._path = path
|
||||
if isinstance(path, (Path, WindowsPath, PosixPath, LocalPath)):
|
||||
path = str(path.absolute())
|
||||
elif path is not None:
|
||||
path = str(path)
|
||||
|
||||
self.cwd = Path.cwd()
|
||||
_lt_folder = Path(self._localtrack_folder) if self._localtrack_folder else self.cwd
|
||||
_path = Path(path) if path else self.cwd
|
||||
if _lt_folder.parts[-1].lower() == "localtracks" and not kwargs.get("forced"):
|
||||
self.localtrack_folder = _lt_folder
|
||||
elif kwargs.get("forced"):
|
||||
if _path.parts[-1].lower() == "localtracks":
|
||||
self.localtrack_folder = _path
|
||||
else:
|
||||
self.localtrack_folder = _path / "localtracks"
|
||||
else:
|
||||
self.localtrack_folder = _lt_folder / "localtracks"
|
||||
|
||||
try:
|
||||
_path = Path(path)
|
||||
_path.relative_to(self.localtrack_folder)
|
||||
self.path = _path
|
||||
except (ValueError, TypeError):
|
||||
for sep in _PATH_SEPS:
|
||||
if path and path.startswith(f"localtracks{sep}{sep}"):
|
||||
path = path.replace(f"localtracks{sep}{sep}", "", 1)
|
||||
elif path and path.startswith(f"localtracks{sep}"):
|
||||
path = path.replace(f"localtracks{sep}", "", 1)
|
||||
self.path = self.localtrack_folder.joinpath(path) if path else self.localtrack_folder
|
||||
|
||||
try:
|
||||
if self.path.is_file():
|
||||
parent = self.path.parent
|
||||
else:
|
||||
parent = self.path
|
||||
self.parent = Path(parent)
|
||||
except OSError:
|
||||
self.parent = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return str(self.path.name)
|
||||
|
||||
@property
|
||||
def suffix(self):
|
||||
return str(self.path.suffix)
|
||||
|
||||
def is_dir(self):
|
||||
try:
|
||||
return self.path.is_dir()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def exists(self):
|
||||
try:
|
||||
return self.path.exists()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def is_file(self):
|
||||
try:
|
||||
return self.path.is_file()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def absolute(self):
|
||||
try:
|
||||
return self.path.absolute()
|
||||
except OSError:
|
||||
return self._path
|
||||
|
||||
@classmethod
|
||||
def joinpath(cls, localpath, *args):
|
||||
modified = cls(None, localpath)
|
||||
modified.path = modified.path.joinpath(*args)
|
||||
return modified
|
||||
|
||||
def rglob(self, pattern, folder=False) -> Iterator[str]:
|
||||
if folder:
|
||||
return glob.iglob(f"{glob.escape(self.path)}{os.sep}**{os.sep}", recursive=True)
|
||||
else:
|
||||
return glob.iglob(
|
||||
f"{glob.escape(self.path)}{os.sep}**{os.sep}*{pattern}", recursive=True
|
||||
)
|
||||
|
||||
def glob(self, pattern, folder=False) -> Iterator[str]:
|
||||
if folder:
|
||||
return glob.iglob(f"{glob.escape(self.path)}{os.sep}*{os.sep}", recursive=False)
|
||||
else:
|
||||
return glob.iglob(f"{glob.escape(self.path)}{os.sep}*{pattern}", recursive=False)
|
||||
|
||||
async def _multiglob(self, pattern: str, folder: bool, method: Callable):
|
||||
async for rp in AsyncIter(method(pattern)):
|
||||
rp_local = LocalPath(rp, self._localtrack_folder)
|
||||
if (
|
||||
(folder and rp_local.is_dir() and rp_local.exists())
|
||||
or (not folder and rp_local.suffix in self._all_music_ext and rp_local.is_file())
|
||||
and rp_local.exists()
|
||||
):
|
||||
yield rp_local
|
||||
|
||||
async def multiglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
|
||||
async for p in AsyncIter(patterns):
|
||||
async for path in self._multiglob(p, folder, self.glob):
|
||||
yield path
|
||||
|
||||
async def multirglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
|
||||
async for p in AsyncIter(patterns):
|
||||
async for path in self._multiglob(p, folder, self.rglob):
|
||||
yield path
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def to_string(self):
|
||||
try:
|
||||
return str(self.path.absolute())
|
||||
except OSError:
|
||||
return str(self._path)
|
||||
|
||||
def to_string_user(self, arg: str = None):
|
||||
string = str(self.absolute()).replace(
|
||||
(str(self.localtrack_folder.absolute()) + os.sep) if arg is None else arg, ""
|
||||
)
|
||||
chunked = False
|
||||
while len(string) > 145 and os.sep in string:
|
||||
string = string.split(os.sep, 1)[-1]
|
||||
chunked = True
|
||||
|
||||
if chunked:
|
||||
string = f"...{os.sep}{string}"
|
||||
return string
|
||||
|
||||
async def tracks_in_tree(self):
|
||||
tracks = []
|
||||
async for track in self.multirglob(*[f"{ext}" for ext in self._all_music_ext]):
|
||||
with contextlib.suppress(ValueError):
|
||||
if track.path.parent != self.localtrack_folder and track.path.relative_to(
|
||||
self.path
|
||||
):
|
||||
tracks.append(Query.process_input(track, self._localtrack_folder))
|
||||
return sorted(tracks, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
async def subfolders_in_tree(self):
|
||||
return_folders = []
|
||||
async for f in self.multirglob("", folder=True):
|
||||
with contextlib.suppress(ValueError):
|
||||
if (
|
||||
f not in return_folders
|
||||
and f.is_dir()
|
||||
and f.path != self.localtrack_folder
|
||||
and f.path.relative_to(self.path)
|
||||
):
|
||||
return_folders.append(f)
|
||||
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
async def tracks_in_folder(self):
|
||||
tracks = []
|
||||
async for track in self.multiglob(*[f"{ext}" for ext in self._all_music_ext]):
|
||||
with contextlib.suppress(ValueError):
|
||||
if track.path.parent != self.localtrack_folder and track.path.relative_to(
|
||||
self.path
|
||||
):
|
||||
tracks.append(Query.process_input(track, self._localtrack_folder))
|
||||
return sorted(tracks, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
async def subfolders(self):
|
||||
return_folders = []
|
||||
async for f in self.multiglob("", folder=True):
|
||||
with contextlib.suppress(ValueError):
|
||||
if (
|
||||
f not in return_folders
|
||||
and f.path != self.localtrack_folder
|
||||
and f.path.relative_to(self.path)
|
||||
):
|
||||
return_folders.append(f)
|
||||
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts == other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts == other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
return self._hash
|
||||
except AttributeError:
|
||||
self._hash = hash(tuple(self.path._cparts))
|
||||
return self._hash
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts < other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts < other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other):
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts <= other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts <= other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other):
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts > other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts > other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other):
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts >= other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts >= other._cpart
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class Query:
|
||||
"""Query data class.
|
||||
|
||||
Use: Query.process_input(query, localtrack_folder) to generate the Query object.
|
||||
"""
|
||||
|
||||
def __init__(self, query: Union[LocalPath, str], local_folder_current_path: Path, **kwargs):
|
||||
query = kwargs.get("queryforced", query)
|
||||
self._raw: Union[LocalPath, str] = query
|
||||
self._local_folder_current_path = local_folder_current_path
|
||||
_localtrack: LocalPath = LocalPath(query, local_folder_current_path)
|
||||
|
||||
self.valid: bool = query != "InvalidQueryPlaceHolderName"
|
||||
self.is_local: bool = kwargs.get("local", False)
|
||||
self.is_spotify: bool = kwargs.get("spotify", False)
|
||||
self.is_youtube: bool = kwargs.get("youtube", False)
|
||||
self.is_soundcloud: bool = kwargs.get("soundcloud", False)
|
||||
self.is_bandcamp: bool = kwargs.get("bandcamp", False)
|
||||
self.is_vimeo: bool = kwargs.get("vimeo", False)
|
||||
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_playlist: bool = kwargs.get("playlist", False)
|
||||
self.is_album: bool = kwargs.get("album", False)
|
||||
self.is_search: bool = kwargs.get("search", False)
|
||||
self.is_stream: bool = kwargs.get("stream", False)
|
||||
self.single_track: bool = kwargs.get("single", False)
|
||||
self.id: Optional[str] = kwargs.get("id", None)
|
||||
self.invoked_from: Optional[str] = kwargs.get("invoked_from", None)
|
||||
self.local_name: Optional[str] = kwargs.get("name", None)
|
||||
self.search_subfolders: bool = kwargs.get("search_subfolders", False)
|
||||
self.spotify_uri: Optional[str] = kwargs.get("uri", None)
|
||||
self.uri: Optional[str] = kwargs.get("url", None)
|
||||
self.is_url: bool = kwargs.get("is_url", False)
|
||||
|
||||
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
|
||||
|
||||
if (_localtrack.is_file() or _localtrack.is_dir()) and _localtrack.exists():
|
||||
self.local_track_path: Optional[LocalPath] = _localtrack
|
||||
self.track: str = str(_localtrack.absolute())
|
||||
self.is_local: bool = True
|
||||
self.uri = self.track
|
||||
else:
|
||||
self.local_track_path: Optional[LocalPath] = None
|
||||
self.track: str = str(query)
|
||||
|
||||
self.lavalink_query: str = self._get_query()
|
||||
|
||||
if self.is_playlist or self.is_album:
|
||||
self.single_track = False
|
||||
self._hash = hash(
|
||||
(
|
||||
self.valid,
|
||||
self.is_local,
|
||||
self.is_spotify,
|
||||
self.is_youtube,
|
||||
self.is_soundcloud,
|
||||
self.is_bandcamp,
|
||||
self.is_vimeo,
|
||||
self.is_mixer,
|
||||
self.is_twitch,
|
||||
self.is_other,
|
||||
self.is_playlist,
|
||||
self.is_album,
|
||||
self.is_search,
|
||||
self.is_stream,
|
||||
self.single_track,
|
||||
self.id,
|
||||
self.spotify_uri,
|
||||
self.start_time,
|
||||
self.track_index,
|
||||
self.uri,
|
||||
)
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.lavalink_query)
|
||||
|
||||
@classmethod
|
||||
def process_input(
|
||||
cls,
|
||||
query: Union[LocalPath, lavalink.Track, "Query", str],
|
||||
_local_folder_current_path: Path,
|
||||
**kwargs,
|
||||
) -> "Query":
|
||||
"""
|
||||
Process the input query into its type
|
||||
|
||||
Parameters
|
||||
----------
|
||||
query : Union[Query, LocalPath, lavalink.Track, str]
|
||||
The query string or LocalPath object.
|
||||
_local_folder_current_path: Path
|
||||
The Current Local Track folder
|
||||
Returns
|
||||
-------
|
||||
Query
|
||||
Returns a parsed Query object.
|
||||
"""
|
||||
if not query:
|
||||
query = "InvalidQueryPlaceHolderName"
|
||||
possible_values = {}
|
||||
|
||||
if isinstance(query, str):
|
||||
query = query.strip("<>")
|
||||
while "ytsearch:" in query:
|
||||
query = query.replace("ytsearch:", "")
|
||||
while "scsearch:" in query:
|
||||
query = query.replace("scsearch:", "")
|
||||
|
||||
elif isinstance(query, Query):
|
||||
for key, val in kwargs.items():
|
||||
setattr(query, key, val)
|
||||
return query
|
||||
elif isinstance(query, lavalink.Track):
|
||||
possible_values["stream"] = query.is_stream
|
||||
query = query.uri
|
||||
|
||||
possible_values.update(dict(**kwargs))
|
||||
possible_values.update(cls._parse(query, _local_folder_current_path, **kwargs))
|
||||
return cls(query, _local_folder_current_path, **possible_values)
|
||||
|
||||
@staticmethod
|
||||
def _parse(track, _local_folder_current_path: Path, **kwargs) -> MutableMapping:
|
||||
"""Parse a track into all the relevant metadata"""
|
||||
returning: MutableMapping = {}
|
||||
if (
|
||||
type(track) == type(LocalPath)
|
||||
and (track.is_file() or track.is_dir())
|
||||
and track.exists()
|
||||
):
|
||||
returning["local"] = True
|
||||
returning["name"] = track.name
|
||||
if track.is_file():
|
||||
returning["single"] = True
|
||||
elif track.is_dir():
|
||||
returning["album"] = True
|
||||
else:
|
||||
track = str(track)
|
||||
if track.startswith("spotify:"):
|
||||
returning["spotify"] = True
|
||||
if ":playlist:" in track:
|
||||
returning["playlist"] = True
|
||||
elif ":album:" in track:
|
||||
returning["album"] = True
|
||||
elif ":track:" in track:
|
||||
returning["single"] = True
|
||||
_id = track.split(":", 2)[-1]
|
||||
_id = _id.split("?")[0]
|
||||
returning["id"] = _id
|
||||
if "#" in _id:
|
||||
match = re.search(_RE_SPOTIFY_TIMESTAMP, track)
|
||||
if match:
|
||||
returning["start_time"] = (int(match.group(1)) * 60) + int(match.group(2))
|
||||
returning["uri"] = track
|
||||
return returning
|
||||
if track.startswith("sc ") or track.startswith("list "):
|
||||
if track.startswith("sc "):
|
||||
returning["invoked_from"] = "sc search"
|
||||
returning["soundcloud"] = True
|
||||
elif track.startswith("list "):
|
||||
returning["invoked_from"] = "search list"
|
||||
track = _RE_REMOVE_START.sub("", track, 1)
|
||||
returning["queryforced"] = track
|
||||
|
||||
_localtrack = LocalPath(track, _local_folder_current_path)
|
||||
if _localtrack.exists():
|
||||
if _localtrack.is_file():
|
||||
returning["local"] = True
|
||||
returning["single"] = True
|
||||
returning["name"] = _localtrack.name
|
||||
return returning
|
||||
elif _localtrack.is_dir():
|
||||
returning["album"] = True
|
||||
returning["local"] = True
|
||||
returning["name"] = _localtrack.name
|
||||
return returning
|
||||
try:
|
||||
query_url = urlparse(track)
|
||||
if all([query_url.scheme, query_url.netloc, query_url.path]):
|
||||
returning["url"] = track
|
||||
returning["is_url"] = True
|
||||
url_domain = ".".join(query_url.netloc.split(".")[-2:])
|
||||
if not query_url.netloc:
|
||||
url_domain = ".".join(query_url.path.split("/")[0].split(".")[-2:])
|
||||
if url_domain in ["youtube.com", "youtu.be"]:
|
||||
returning["youtube"] = True
|
||||
_has_index = "&index=" in track
|
||||
if "&t=" in track or "?t=" in track:
|
||||
match = re.search(_RE_YOUTUBE_TIMESTAMP, track)
|
||||
if match:
|
||||
returning["start_time"] = int(match.group(1))
|
||||
if _has_index:
|
||||
match = re.search(_RE_YOUTUBE_INDEX, track)
|
||||
if match:
|
||||
returning["track_index"] = int(match.group(1)) - 1
|
||||
if all(k in track for k in ["&list=", "watch?"]):
|
||||
returning["track_index"] = 0
|
||||
returning["playlist"] = True
|
||||
returning["single"] = False
|
||||
elif all(x in track for x in ["playlist?"]):
|
||||
returning["playlist"] = not _has_index
|
||||
returning["single"] = _has_index
|
||||
elif any(k in track for k in ["list="]):
|
||||
returning["track_index"] = 0
|
||||
returning["playlist"] = True
|
||||
returning["single"] = False
|
||||
else:
|
||||
returning["single"] = True
|
||||
elif url_domain == "spotify.com":
|
||||
returning["spotify"] = True
|
||||
if "/playlist/" in track:
|
||||
returning["playlist"] = True
|
||||
elif "/album/" in track:
|
||||
returning["album"] = True
|
||||
elif "/track/" in track:
|
||||
returning["single"] = True
|
||||
val = re.sub(_RE_SPOTIFY_URL, "", track).replace("/", ":")
|
||||
if "user:" in val:
|
||||
val = val.split(":", 2)[-1]
|
||||
_id = val.split(":", 1)[-1]
|
||||
_id = _id.split("?")[0]
|
||||
|
||||
if "#" in _id:
|
||||
_id = _id.split("#")[0]
|
||||
match = re.search(_RE_SPOTIFY_TIMESTAMP, track)
|
||||
if match:
|
||||
returning["start_time"] = (int(match.group(1)) * 60) + int(
|
||||
match.group(2)
|
||||
)
|
||||
|
||||
returning["id"] = _id
|
||||
returning["uri"] = f"spotify:{val}"
|
||||
elif url_domain == "soundcloud.com":
|
||||
returning["soundcloud"] = True
|
||||
if "#t=" in track:
|
||||
match = re.search(_RE_SOUNDCLOUD_TIMESTAMP, track)
|
||||
if match:
|
||||
returning["start_time"] = (int(match.group(1)) * 60) + int(
|
||||
match.group(2)
|
||||
)
|
||||
if "/sets/" in track:
|
||||
if "?in=" in track:
|
||||
returning["single"] = True
|
||||
else:
|
||||
returning["playlist"] = True
|
||||
else:
|
||||
returning["single"] = True
|
||||
elif url_domain == "bandcamp.com":
|
||||
returning["bandcamp"] = True
|
||||
if "/album/" in track:
|
||||
returning["album"] = True
|
||||
else:
|
||||
returning["single"] = True
|
||||
elif url_domain == "vimeo.com":
|
||||
returning["vimeo"] = True
|
||||
elif url_domain in ["mixer.com", "beam.pro"]:
|
||||
returning["mixer"] = True
|
||||
elif url_domain == "twitch.tv":
|
||||
returning["twitch"] = True
|
||||
if "?t=" in track:
|
||||
match = re.search(_RE_TWITCH_TIMESTAMP, track)
|
||||
if match:
|
||||
returning["start_time"] = (
|
||||
(int(match.group(1)) * 60 * 60)
|
||||
+ (int(match.group(2)) * 60)
|
||||
+ int(match.group(3))
|
||||
)
|
||||
|
||||
if not any(x in track for x in ["/clip/", "/videos/"]):
|
||||
returning["stream"] = True
|
||||
else:
|
||||
returning["other"] = True
|
||||
returning["single"] = True
|
||||
else:
|
||||
if kwargs.get("soundcloud", False):
|
||||
returning["soundcloud"] = True
|
||||
else:
|
||||
returning["youtube"] = True
|
||||
returning["search"] = True
|
||||
returning["single"] = True
|
||||
except Exception:
|
||||
returning["search"] = True
|
||||
returning["youtube"] = True
|
||||
returning["single"] = True
|
||||
return returning
|
||||
|
||||
def _get_query(self):
|
||||
if self.is_local:
|
||||
return self.local_track_path.to_string()
|
||||
elif self.is_spotify:
|
||||
return self.spotify_uri
|
||||
elif self.is_search and self.is_youtube:
|
||||
return f"ytsearch:{self.track}"
|
||||
elif self.is_search and self.is_soundcloud:
|
||||
return f"scsearch:{self.track}"
|
||||
return self.track
|
||||
|
||||
def to_string_user(self):
|
||||
if self.is_local:
|
||||
return str(self.local_track_path.to_string_user())
|
||||
return str(self._raw)
|
||||
|
||||
@property
|
||||
def suffix(self):
|
||||
if self.is_local:
|
||||
return self.local_track_path.suffix
|
||||
return None
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Query):
|
||||
return NotImplemented
|
||||
return self.to_string_user() == other.to_string_user()
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
return self._hash
|
||||
except AttributeError:
|
||||
self._hash = hash(
|
||||
(
|
||||
self.valid,
|
||||
self.is_local,
|
||||
self.is_spotify,
|
||||
self.is_youtube,
|
||||
self.is_soundcloud,
|
||||
self.is_bandcamp,
|
||||
self.is_vimeo,
|
||||
self.is_mixer,
|
||||
self.is_twitch,
|
||||
self.is_other,
|
||||
self.is_playlist,
|
||||
self.is_album,
|
||||
self.is_search,
|
||||
self.is_stream,
|
||||
self.single_track,
|
||||
self.id,
|
||||
self.spotify_uri,
|
||||
self.start_time,
|
||||
self.track_index,
|
||||
self.uri,
|
||||
)
|
||||
)
|
||||
return self._hash
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Query):
|
||||
return NotImplemented
|
||||
return self.to_string_user() < other.to_string_user()
|
||||
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, Query):
|
||||
return NotImplemented
|
||||
return self.to_string_user() <= other.to_string_user()
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, Query):
|
||||
return NotImplemented
|
||||
return self.to_string_user() > other.to_string_user()
|
||||
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, Query):
|
||||
return NotImplemented
|
||||
return self.to_string_user() >= other.to_string_user()
|
||||
@ -1,17 +0,0 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
IS_DEBUG: Final[bool] = "--debug" in sys.argv
|
||||
|
||||
|
||||
def is_debug() -> bool:
|
||||
return IS_DEBUG
|
||||
|
||||
|
||||
def debug_exc_log(lg: logging.Logger, exc: Exception, msg: str = None) -> None:
|
||||
"""Logs an exception if logging is set to DEBUG level"""
|
||||
if lg.getEffectiveLevel() <= logging.DEBUG:
|
||||
if msg is None:
|
||||
msg = f"{exc}"
|
||||
lg.exception(msg, exc_info=exc)
|
||||
@ -1,541 +0,0 @@
|
||||
import argparse
|
||||
import functools
|
||||
import re
|
||||
from typing import Final, MutableMapping, Optional, Tuple, Union, Pattern
|
||||
|
||||
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 .apis.api_utils import standardize_scope
|
||||
from .apis.playlist_interface import get_all_playlist_converter
|
||||
from .errors import NoMatchesFound, TooManyMatches
|
||||
from .utils import PlaylistScope
|
||||
|
||||
_ = Translator("Audio", __file__)
|
||||
|
||||
__all__ = [
|
||||
"ComplexScopeParser",
|
||||
"PlaylistConverter",
|
||||
"ScopeParser",
|
||||
"LazyGreedyConverter",
|
||||
"standardize_scope",
|
||||
"get_lazy_converter",
|
||||
"get_playlist_converter",
|
||||
]
|
||||
|
||||
T_ = _
|
||||
_ = lambda s: s
|
||||
|
||||
_SCOPE_HELP: Final[str] = _(
|
||||
"""
|
||||
Scope must be a valid version of one of the following:
|
||||
Global
|
||||
Guild
|
||||
User
|
||||
"""
|
||||
)
|
||||
_USER_HELP: Final[str] = _(
|
||||
"""
|
||||
Author must be a valid version of one of the following:
|
||||
User ID
|
||||
User Mention
|
||||
User Name#123
|
||||
"""
|
||||
)
|
||||
_GUILD_HELP: Final[str] = _(
|
||||
"""
|
||||
Guild must be a valid version of one of the following:
|
||||
Guild ID
|
||||
Exact guild name
|
||||
"""
|
||||
)
|
||||
|
||||
_ = T_
|
||||
|
||||
MENTION_RE: Final[Pattern] = re.compile(r"^<?(?:(?:@[!&]?)?|#)(\d{15,21})>?$")
|
||||
|
||||
|
||||
def _match_id(arg: str) -> Optional[int]:
|
||||
m = MENTION_RE.match(arg)
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
return None
|
||||
|
||||
|
||||
async def global_unique_guild_finder(ctx: commands.Context, arg: str) -> discord.Guild:
|
||||
bot: Red = ctx.bot
|
||||
_id = _match_id(arg)
|
||||
|
||||
if _id is not None:
|
||||
guild: discord.Guild = bot.get_guild(_id)
|
||||
if guild is not None:
|
||||
return guild
|
||||
|
||||
maybe_matches = []
|
||||
async for obj in AsyncIter(bot.guilds):
|
||||
if obj.name == arg or str(obj) == arg:
|
||||
maybe_matches.append(obj)
|
||||
|
||||
if not maybe_matches:
|
||||
raise NoMatchesFound(
|
||||
_(
|
||||
'"{arg}" was not found. It must be the ID or '
|
||||
"complete name of a server which the bot can see."
|
||||
).format(arg=arg)
|
||||
)
|
||||
elif len(maybe_matches) == 1:
|
||||
return maybe_matches[0]
|
||||
else:
|
||||
raise TooManyMatches(
|
||||
_(
|
||||
'"{arg}" does not refer to a unique server. '
|
||||
"Please use the ID for the server you're trying to specify."
|
||||
).format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
async def global_unique_user_finder(
|
||||
ctx: commands.Context, arg: str, guild: discord.guild = None
|
||||
) -> discord.abc.User:
|
||||
bot: Red = ctx.bot
|
||||
guild = guild or ctx.guild
|
||||
_id = _match_id(arg)
|
||||
|
||||
if _id is not None:
|
||||
user: discord.User = bot.get_user(_id)
|
||||
if user is not None:
|
||||
return user
|
||||
|
||||
maybe_matches = []
|
||||
async for user in AsyncIter(bot.users).filter(lambda u: u.name == arg or f"{u}" == arg):
|
||||
maybe_matches.append(user)
|
||||
|
||||
if guild is not None:
|
||||
async for member in AsyncIter(guild.members).filter(
|
||||
lambda m: m.nick == arg and not any(obj.id == m.id for obj in maybe_matches)
|
||||
):
|
||||
maybe_matches.append(member)
|
||||
|
||||
if not maybe_matches:
|
||||
raise NoMatchesFound(
|
||||
_(
|
||||
'"{arg}" was not found. It must be the ID or name or '
|
||||
"mention a user which the bot can see."
|
||||
).format(arg=arg)
|
||||
)
|
||||
elif len(maybe_matches) == 1:
|
||||
return maybe_matches[0]
|
||||
else:
|
||||
raise TooManyMatches(
|
||||
_(
|
||||
'"{arg}" does not refer to a unique server. '
|
||||
"Please use the ID for the server you're trying to specify."
|
||||
).format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class PlaylistConverter(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> MutableMapping:
|
||||
"""Get playlist for all scopes that match the argument user provided"""
|
||||
cog = ctx.cog
|
||||
user_matches = []
|
||||
guild_matches = []
|
||||
global_matches = []
|
||||
if cog:
|
||||
global_matches = await get_all_playlist_converter(
|
||||
PlaylistScope.GLOBAL.value,
|
||||
ctx.bot,
|
||||
cog.playlist_api,
|
||||
arg,
|
||||
guild=ctx.guild,
|
||||
author=ctx.author,
|
||||
)
|
||||
guild_matches = await get_all_playlist_converter(
|
||||
PlaylistScope.GUILD.value,
|
||||
ctx.bot,
|
||||
cog.playlist_api,
|
||||
arg,
|
||||
guild=ctx.guild,
|
||||
author=ctx.author,
|
||||
)
|
||||
user_matches = await get_all_playlist_converter(
|
||||
PlaylistScope.USER.value,
|
||||
ctx.bot,
|
||||
cog.playlist_api,
|
||||
arg,
|
||||
guild=ctx.guild,
|
||||
author=ctx.author,
|
||||
)
|
||||
if not user_matches and not guild_matches and not global_matches:
|
||||
raise commands.BadArgument(_("Could not match '{}' to a playlist.").format(arg))
|
||||
return {
|
||||
PlaylistScope.GLOBAL.value: global_matches,
|
||||
PlaylistScope.GUILD.value: guild_matches,
|
||||
PlaylistScope.USER.value: user_matches,
|
||||
"all": [*global_matches, *guild_matches, *user_matches],
|
||||
"arg": arg,
|
||||
}
|
||||
|
||||
|
||||
class NoExitParser(argparse.ArgumentParser):
|
||||
def error(self, message):
|
||||
raise commands.BadArgument()
|
||||
|
||||
|
||||
class ScopeParser(commands.Converter):
|
||||
async def convert(
|
||||
self, ctx: commands.Context, argument: str
|
||||
) -> Tuple[Optional[str], discord.User, Optional[discord.Guild], bool]:
|
||||
|
||||
target_scope: Optional[str] = None
|
||||
target_user: Optional[Union[discord.Member, discord.User]] = None
|
||||
target_guild: Optional[discord.Guild] = None
|
||||
specified_user = False
|
||||
|
||||
argument = argument.replace("—", "--")
|
||||
|
||||
command, *arguments = argument.split(" -- ")
|
||||
if arguments:
|
||||
argument = " -- ".join(arguments)
|
||||
else:
|
||||
command = ""
|
||||
|
||||
parser = NoExitParser(description="Playlist Scope Parsing.", add_help=False)
|
||||
parser.add_argument("--scope", nargs="*", dest="scope", default=[])
|
||||
parser.add_argument("--guild", nargs="*", dest="guild", default=[])
|
||||
parser.add_argument("--server", nargs="*", dest="guild", default=[])
|
||||
parser.add_argument("--author", nargs="*", dest="author", default=[])
|
||||
parser.add_argument("--user", nargs="*", dest="author", default=[])
|
||||
parser.add_argument("--member", nargs="*", dest="author", default=[])
|
||||
|
||||
if not command:
|
||||
parser.add_argument("command", nargs="*")
|
||||
|
||||
try:
|
||||
vals = vars(parser.parse_args(argument.split()))
|
||||
except Exception as exc:
|
||||
raise commands.BadArgument() from exc
|
||||
|
||||
if vals["scope"]:
|
||||
scope_raw = " ".join(vals["scope"]).strip()
|
||||
scope = scope_raw.upper().strip()
|
||||
valid_scopes = PlaylistScope.list() + [
|
||||
"GLOBAL",
|
||||
"GUILD",
|
||||
"AUTHOR",
|
||||
"USER",
|
||||
"SERVER",
|
||||
"MEMBER",
|
||||
"BOT",
|
||||
]
|
||||
if scope not in valid_scopes:
|
||||
raise commands.ArgParserFailure("--scope", scope_raw, custom_help=_(_SCOPE_HELP))
|
||||
target_scope = standardize_scope(scope)
|
||||
elif "--scope" in argument and not vals["scope"]:
|
||||
raise commands.ArgParserFailure("--scope", _("Nothing"), custom_help=_(_SCOPE_HELP))
|
||||
|
||||
is_owner = await ctx.bot.is_owner(ctx.author)
|
||||
guild = vals.get("guild", None) or vals.get("server", None)
|
||||
if is_owner and guild:
|
||||
server_error = ""
|
||||
target_guild = None
|
||||
guild_raw = " ".join(guild).strip()
|
||||
try:
|
||||
target_guild = await global_unique_guild_finder(ctx, guild_raw)
|
||||
except TooManyMatches as err:
|
||||
server_error = f"{err}\n"
|
||||
except NoMatchesFound as err:
|
||||
server_error = f"{err}\n"
|
||||
if target_guild is None:
|
||||
raise commands.ArgParserFailure(
|
||||
"--guild", guild_raw, custom_help=f"{server_error}{_(_GUILD_HELP)}"
|
||||
)
|
||||
|
||||
elif not is_owner and (guild or any(x in argument for x in ["--guild", "--server"])):
|
||||
raise commands.BadArgument(_("You cannot use `--guild`"))
|
||||
elif any(x in argument for x in ["--guild", "--server"]):
|
||||
raise commands.ArgParserFailure("--guild", _("Nothing"), custom_help=_(_GUILD_HELP))
|
||||
|
||||
author = vals.get("author", None) or vals.get("user", None) or vals.get("member", None)
|
||||
if author:
|
||||
user_error = ""
|
||||
target_user = None
|
||||
user_raw = " ".join(author).strip()
|
||||
try:
|
||||
target_user = await global_unique_user_finder(ctx, user_raw, guild=target_guild)
|
||||
specified_user = True
|
||||
except TooManyMatches as err:
|
||||
user_error = f"{err}\n"
|
||||
except NoMatchesFound as err:
|
||||
user_error = f"{err}\n"
|
||||
|
||||
if target_user is None:
|
||||
raise commands.ArgParserFailure(
|
||||
"--author", user_raw, custom_help=f"{user_error}{_(_USER_HELP)}"
|
||||
)
|
||||
elif any(x in argument for x in ["--author", "--user", "--member"]):
|
||||
raise commands.ArgParserFailure("--scope", _("Nothing"), custom_help=_(_USER_HELP))
|
||||
|
||||
target_scope: Optional[str] = target_scope or None
|
||||
target_user: Union[discord.Member, discord.User] = target_user or ctx.author
|
||||
target_guild: discord.Guild = target_guild or ctx.guild
|
||||
|
||||
return target_scope, target_user, target_guild, specified_user
|
||||
|
||||
|
||||
class ComplexScopeParser(commands.Converter):
|
||||
async def convert(
|
||||
self, ctx: commands.Context, argument: str
|
||||
) -> Tuple[
|
||||
str,
|
||||
discord.User,
|
||||
Optional[discord.Guild],
|
||||
bool,
|
||||
str,
|
||||
discord.User,
|
||||
Optional[discord.Guild],
|
||||
bool,
|
||||
]:
|
||||
|
||||
target_scope: Optional[str] = None
|
||||
target_user: Optional[Union[discord.Member, discord.User]] = None
|
||||
target_guild: Optional[discord.Guild] = None
|
||||
specified_target_user = False
|
||||
|
||||
source_scope: Optional[str] = None
|
||||
source_user: Optional[Union[discord.Member, discord.User]] = None
|
||||
source_guild: Optional[discord.Guild] = None
|
||||
specified_source_user = False
|
||||
|
||||
argument = argument.replace("—", "--")
|
||||
|
||||
command, *arguments = argument.split(" -- ")
|
||||
if arguments:
|
||||
argument = " -- ".join(arguments)
|
||||
else:
|
||||
command = ""
|
||||
|
||||
parser = NoExitParser(description="Playlist Scope Parsing.", add_help=False)
|
||||
|
||||
parser.add_argument("--to-scope", nargs="*", dest="to_scope", default=[])
|
||||
parser.add_argument("--to-guild", nargs="*", dest="to_guild", default=[])
|
||||
parser.add_argument("--to-server", nargs="*", dest="to_server", default=[])
|
||||
parser.add_argument("--to-author", nargs="*", dest="to_author", default=[])
|
||||
parser.add_argument("--to-user", nargs="*", dest="to_user", default=[])
|
||||
parser.add_argument("--to-member", nargs="*", dest="to_member", default=[])
|
||||
|
||||
parser.add_argument("--from-scope", nargs="*", dest="from_scope", default=[])
|
||||
parser.add_argument("--from-guild", nargs="*", dest="from_guild", default=[])
|
||||
parser.add_argument("--from-server", nargs="*", dest="from_server", default=[])
|
||||
parser.add_argument("--from-author", nargs="*", dest="from_author", default=[])
|
||||
parser.add_argument("--from-user", nargs="*", dest="from_user", default=[])
|
||||
parser.add_argument("--from-member", nargs="*", dest="from_member", default=[])
|
||||
|
||||
if not command:
|
||||
parser.add_argument("command", nargs="*")
|
||||
|
||||
try:
|
||||
vals = vars(parser.parse_args(argument.split()))
|
||||
except Exception as exc:
|
||||
raise commands.BadArgument() from exc
|
||||
|
||||
is_owner = await ctx.bot.is_owner(ctx.author)
|
||||
valid_scopes = PlaylistScope.list() + [
|
||||
"GLOBAL",
|
||||
"GUILD",
|
||||
"AUTHOR",
|
||||
"USER",
|
||||
"SERVER",
|
||||
"MEMBER",
|
||||
"BOT",
|
||||
]
|
||||
|
||||
if vals["to_scope"]:
|
||||
to_scope_raw = " ".join(vals["to_scope"]).strip()
|
||||
to_scope = to_scope_raw.upper().strip()
|
||||
if to_scope not in valid_scopes:
|
||||
raise commands.ArgParserFailure(
|
||||
"--to-scope", to_scope_raw, custom_help=_SCOPE_HELP
|
||||
)
|
||||
target_scope = standardize_scope(to_scope)
|
||||
elif "--to-scope" in argument and not vals["to_scope"]:
|
||||
raise commands.ArgParserFailure("--to-scope", _("Nothing"), custom_help=_(_SCOPE_HELP))
|
||||
|
||||
if vals["from_scope"]:
|
||||
from_scope_raw = " ".join(vals["from_scope"]).strip()
|
||||
from_scope = from_scope_raw.upper().strip()
|
||||
|
||||
if from_scope not in valid_scopes:
|
||||
raise commands.ArgParserFailure(
|
||||
"--from-scope", from_scope_raw, custom_help=_SCOPE_HELP
|
||||
)
|
||||
source_scope = standardize_scope(from_scope)
|
||||
elif "--from-scope" in argument and not vals["to_scope"]:
|
||||
raise commands.ArgParserFailure("--to-scope", _("Nothing"), custom_help=_(_SCOPE_HELP))
|
||||
|
||||
to_guild = vals.get("to_guild", None) or vals.get("to_server", None)
|
||||
if is_owner and to_guild:
|
||||
target_server_error = ""
|
||||
target_guild = None
|
||||
to_guild_raw = " ".join(to_guild).strip()
|
||||
try:
|
||||
target_guild = await global_unique_guild_finder(ctx, to_guild_raw)
|
||||
except TooManyMatches as err:
|
||||
target_server_error = f"{err}\n"
|
||||
except NoMatchesFound as err:
|
||||
target_server_error = f"{err}\n"
|
||||
if target_guild is None:
|
||||
raise commands.ArgParserFailure(
|
||||
"--to-guild",
|
||||
to_guild_raw,
|
||||
custom_help=f"{target_server_error}{_(_GUILD_HELP)}",
|
||||
)
|
||||
elif not is_owner and (
|
||||
to_guild or any(x in argument for x in ["--to-guild", "--to-server"])
|
||||
):
|
||||
raise commands.BadArgument(_("You cannot use `--to-server`"))
|
||||
elif any(x in argument for x in ["--to-guild", "--to-server"]):
|
||||
raise commands.ArgParserFailure(
|
||||
"--to-server", _("Nothing"), custom_help=_(_GUILD_HELP)
|
||||
)
|
||||
|
||||
from_guild = vals.get("from_guild", None) or vals.get("from_server", None)
|
||||
if is_owner and from_guild:
|
||||
source_server_error = ""
|
||||
source_guild = None
|
||||
from_guild_raw = " ".join(from_guild).strip()
|
||||
try:
|
||||
source_guild = await global_unique_guild_finder(ctx, from_guild_raw)
|
||||
except TooManyMatches as err:
|
||||
source_server_error = f"{err}\n"
|
||||
except NoMatchesFound as err:
|
||||
source_server_error = f"{err}\n"
|
||||
if source_guild is None:
|
||||
raise commands.ArgParserFailure(
|
||||
"--from-guild",
|
||||
from_guild_raw,
|
||||
custom_help=f"{source_server_error}{_(_GUILD_HELP)}",
|
||||
)
|
||||
elif not is_owner and (
|
||||
from_guild or any(x in argument for x in ["--from-guild", "--from-server"])
|
||||
):
|
||||
raise commands.BadArgument(_("You cannot use `--from-server`"))
|
||||
elif any(x in argument for x in ["--from-guild", "--from-server"]):
|
||||
raise commands.ArgParserFailure(
|
||||
"--from-server", _("Nothing"), custom_help=_(_GUILD_HELP)
|
||||
)
|
||||
|
||||
to_author = (
|
||||
vals.get("to_author", None) or vals.get("to_user", None) or vals.get("to_member", None)
|
||||
)
|
||||
if to_author:
|
||||
target_user_error = ""
|
||||
target_user = None
|
||||
to_user_raw = " ".join(to_author).strip()
|
||||
try:
|
||||
target_user = await global_unique_user_finder(ctx, to_user_raw, guild=target_guild)
|
||||
specified_target_user = True
|
||||
except TooManyMatches as err:
|
||||
target_user_error = f"{err}\n"
|
||||
except NoMatchesFound as err:
|
||||
target_user_error = f"{err}\n"
|
||||
if target_user is None:
|
||||
raise commands.ArgParserFailure(
|
||||
"--to-author", to_user_raw, custom_help=f"{target_user_error}{_(_USER_HELP)}"
|
||||
)
|
||||
elif any(x in argument for x in ["--to-author", "--to-user", "--to-member"]):
|
||||
raise commands.ArgParserFailure("--to-user", _("Nothing"), custom_help=_(_USER_HELP))
|
||||
|
||||
from_author = (
|
||||
vals.get("from_author", None)
|
||||
or vals.get("from_user", None)
|
||||
or vals.get("from_member", None)
|
||||
)
|
||||
if from_author:
|
||||
source_user_error = ""
|
||||
source_user = None
|
||||
from_user_raw = " ".join(from_author).strip()
|
||||
try:
|
||||
source_user = await global_unique_user_finder(
|
||||
ctx, from_user_raw, guild=target_guild
|
||||
)
|
||||
specified_target_user = True
|
||||
except TooManyMatches as err:
|
||||
source_user_error = f"{err}\n"
|
||||
except NoMatchesFound as err:
|
||||
source_user_error = f"{err}\n"
|
||||
if source_user is None:
|
||||
raise commands.ArgParserFailure(
|
||||
"--from-author",
|
||||
from_user_raw,
|
||||
custom_help=f"{source_user_error}{_(_USER_HELP)}",
|
||||
)
|
||||
elif any(x in argument for x in ["--from-author", "--from-user", "--from-member"]):
|
||||
raise commands.ArgParserFailure("--from-user", _("Nothing"), custom_help=_(_USER_HELP))
|
||||
|
||||
target_scope = target_scope or PlaylistScope.GUILD.value
|
||||
target_user = target_user or ctx.author
|
||||
target_guild = target_guild or ctx.guild
|
||||
|
||||
source_scope = source_scope or PlaylistScope.GUILD.value
|
||||
source_user = source_user or ctx.author
|
||||
source_guild = source_guild or ctx.guild
|
||||
|
||||
return (
|
||||
source_scope,
|
||||
source_user,
|
||||
source_guild,
|
||||
specified_source_user,
|
||||
target_scope,
|
||||
target_user,
|
||||
target_guild,
|
||||
specified_target_user,
|
||||
)
|
||||
|
||||
|
||||
class LazyGreedyConverter(commands.Converter):
|
||||
def __init__(self, splitter: str):
|
||||
self.splitter_Value = splitter
|
||||
|
||||
async def convert(self, ctx: commands.Context, argument: str) -> str:
|
||||
full_message = ctx.message.content.partition(f" {argument} ")
|
||||
if len(full_message) == 1:
|
||||
full_message = (
|
||||
(argument if argument not in full_message else "") + " " + full_message[0]
|
||||
)
|
||||
elif len(full_message) > 1:
|
||||
full_message = (
|
||||
(argument if argument not in full_message else "") + " " + full_message[-1]
|
||||
)
|
||||
greedy_output = (" " + full_message.replace("—", "--")).partition(
|
||||
f" {self.splitter_Value}"
|
||||
)[0]
|
||||
return f"{greedy_output}".strip()
|
||||
|
||||
|
||||
def get_lazy_converter(splitter: str) -> type:
|
||||
"""Returns a typechecking safe `LazyGreedyConverter` suitable for use with discord.py."""
|
||||
|
||||
class PartialMeta(type(LazyGreedyConverter)):
|
||||
__call__ = functools.partialmethod(type(LazyGreedyConverter).__call__, splitter)
|
||||
|
||||
class ValidatedConverter(LazyGreedyConverter, metaclass=PartialMeta):
|
||||
pass
|
||||
|
||||
return ValidatedConverter
|
||||
|
||||
|
||||
def get_playlist_converter() -> type:
|
||||
"""Returns a typechecking safe `PlaylistConverter` suitable for use with discord.py."""
|
||||
|
||||
class PartialMeta(type(PlaylistConverter)):
|
||||
__call__ = functools.partialmethod(type(PlaylistConverter).__call__)
|
||||
|
||||
class ValidatedConverter(PlaylistConverter, metaclass=PartialMeta):
|
||||
pass
|
||||
|
||||
return ValidatedConverter
|
||||
@ -1,121 +0,0 @@
|
||||
import asyncio
|
||||
from collections import Counter
|
||||
from typing import Mapping
|
||||
|
||||
import aiohttp
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Cog
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
from redbot.core.i18n import cog_i18n
|
||||
|
||||
from ..utils import PlaylistScope
|
||||
from . import abc, cog_utils, commands, events, tasks, utilities
|
||||
from .cog_utils import CompositeMetaClass, _
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Audio(
|
||||
commands.Commands,
|
||||
events.Events,
|
||||
tasks.Tasks,
|
||||
utilities.Utilities,
|
||||
Cog,
|
||||
metaclass=CompositeMetaClass,
|
||||
):
|
||||
"""Play audio through voice channels."""
|
||||
|
||||
_default_lavalink_settings = {
|
||||
"host": "localhost",
|
||||
"rest_port": 2333,
|
||||
"ws_port": 2333,
|
||||
"password": "youshallnotpass",
|
||||
}
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, 2711759130, force_registration=True)
|
||||
|
||||
self.api_interface = None
|
||||
self.player_manager = None
|
||||
self.playlist_api = None
|
||||
self.local_folder_current_path = None
|
||||
self.db_conn = None
|
||||
|
||||
self._error_counter = Counter()
|
||||
self._error_timer = {}
|
||||
self._disconnected_players = {}
|
||||
self._daily_playlist_cache = {}
|
||||
self._daily_global_playlist_cache = {}
|
||||
self._dj_status_cache = {}
|
||||
self._dj_role_cache = {}
|
||||
self.skip_votes = {}
|
||||
self.play_lock = {}
|
||||
|
||||
self.lavalink_connect_task = None
|
||||
self.player_automated_timer_task = None
|
||||
self.cog_cleaned_up = False
|
||||
self.lavalink_connection_aborted = False
|
||||
|
||||
self.session = aiohttp.ClientSession()
|
||||
self.cog_ready_event = asyncio.Event()
|
||||
self.cog_init_task = None
|
||||
|
||||
default_global = dict(
|
||||
schema_version=1,
|
||||
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
|
||||
status=False,
|
||||
use_external_lavalink=False,
|
||||
restrict=True,
|
||||
localpath=str(cog_data_path(raw_name="Audio")),
|
||||
url_keyword_blacklist=[],
|
||||
url_keyword_whitelist=[],
|
||||
**self._default_lavalink_settings,
|
||||
)
|
||||
|
||||
default_guild = dict(
|
||||
auto_play=False,
|
||||
autoplaylist={"enabled": False, "id": None, "name": None, "scope": None},
|
||||
disconnect=False,
|
||||
dj_enabled=False,
|
||||
dj_role=None,
|
||||
daily_playlists=False,
|
||||
emptydc_enabled=False,
|
||||
emptydc_timer=0,
|
||||
emptypause_enabled=False,
|
||||
emptypause_timer=0,
|
||||
jukebox=False,
|
||||
jukebox_price=0,
|
||||
maxlength=0,
|
||||
notify=False,
|
||||
prefer_lyrics=False,
|
||||
repeat=False,
|
||||
shuffle=False,
|
||||
shuffle_bumped=True,
|
||||
thumbnail=False,
|
||||
volume=100,
|
||||
vote_enabled=False,
|
||||
vote_percent=0,
|
||||
room_lock=None,
|
||||
url_keyword_blacklist=[],
|
||||
url_keyword_whitelist=[],
|
||||
country_code="US",
|
||||
)
|
||||
_playlist: Mapping = dict(id=None, author=None, name=None, playlist_url=None, tracks=[])
|
||||
|
||||
self.config.init_custom("EQUALIZER", 1)
|
||||
self.config.register_custom("EQUALIZER", eq_bands=[], eq_presets={})
|
||||
self.config.init_custom(PlaylistScope.GLOBAL.value, 1)
|
||||
self.config.register_custom(PlaylistScope.GLOBAL.value, **_playlist)
|
||||
self.config.init_custom(PlaylistScope.GUILD.value, 2)
|
||||
self.config.register_custom(PlaylistScope.GUILD.value, **_playlist)
|
||||
self.config.init_custom(PlaylistScope.USER.value, 2)
|
||||
self.config.register_custom(PlaylistScope.USER.value, **_playlist)
|
||||
self.config.register_guild(**default_guild)
|
||||
self.config.register_global(**default_global)
|
||||
@ -1,504 +0,0 @@
|
||||
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
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from redbot.core import Config, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import Context
|
||||
from redbot.core.utils.dbtools import APSWConnectionWrapper
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..apis.interface import AudioAPIInterface
|
||||
from ..apis.playlist_interface import Playlist
|
||||
from ..apis.playlist_wrapper import PlaylistWrapper
|
||||
from ..audio_dataclasses import LocalPath, Query
|
||||
from ..equalizer import Equalizer
|
||||
from ..manager import ServerManager
|
||||
|
||||
|
||||
class MixinMeta(ABC):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
bot: Red
|
||||
config: Config
|
||||
api_interface: Optional["AudioAPIInterface"]
|
||||
player_manager: Optional["ServerManager"]
|
||||
playlist_api: Optional["PlaylistWrapper"]
|
||||
local_folder_current_path: Optional[Path]
|
||||
db_conn: Optional[APSWConnectionWrapper]
|
||||
session: aiohttp.ClientSession
|
||||
|
||||
skip_votes: MutableMapping[discord.Guild, List[discord.Member]]
|
||||
play_lock: MutableMapping[int, bool]
|
||||
_daily_playlist_cache: MutableMapping[int, bool]
|
||||
_daily_global_playlist_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]
|
||||
|
||||
cog_cleaned_up: bool
|
||||
lavalink_connection_aborted: bool
|
||||
|
||||
_error_counter: Counter
|
||||
|
||||
lavalink_connect_task: Optional[asyncio.Task]
|
||||
player_automated_timer_task: Optional[asyncio.Task]
|
||||
cog_init_task: Optional[asyncio.Task]
|
||||
cog_ready_event: asyncio.Event
|
||||
|
||||
_default_lavalink_settings: Mapping
|
||||
|
||||
@abstractmethod
|
||||
async def command_llsetup(self, ctx: commands.Context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def maybe_reset_error_counter(self, player: lavalink.Player) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def update_bot_presence(self, track: lavalink.Track, playing_servers: int) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_active_player_count(self) -> Tuple[str, int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def increase_error_counter(self, player: lavalink.Player) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _close_database(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def maybe_run_pending_db_tasks(self, ctx: commands.Context) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def update_player_lock(self, ctx: commands.Context, true_or_false: bool) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def data_schema_migration(self, from_version: int, to_version: int) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def lavalink_restart_connect(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def lavalink_attempt_connect(self, timeout: int = 50) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def player_automated_timer(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def lavalink_event_handler(
|
||||
self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _clear_react(
|
||||
self, message: discord.Message, emoji: MutableMapping = None
|
||||
) -> asyncio.Task:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def remove_react(
|
||||
self,
|
||||
message: discord.Message,
|
||||
react_emoji: Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str],
|
||||
react_user: discord.abc.User,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_equalizer(self, ctx: commands.Context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _eq_msg_clear(self, eq_message: discord.Message) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def _player_check(self, ctx: commands.Context) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def maybe_charge_requester(self, ctx: commands.Context, jukebox_price: int) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _can_instaskip(self, ctx: commands.Context, member: discord.Member) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_search(self, ctx: commands.Context, *, query: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def is_query_allowed(
|
||||
self, config: Config, guild: discord.Guild, query: str, query_obj: "Query" = None
|
||||
) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def is_track_length_allowed(self, track: lavalink.Track, maxlength: int) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_track_description(
|
||||
self,
|
||||
track: Union[lavalink.rest_api.Track, "Query"],
|
||||
local_folder_current_path: Path,
|
||||
shorten: bool = False,
|
||||
) -> Optional[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_track_description_unformatted(
|
||||
self, track: Union[lavalink.rest_api.Track, "Query"], local_folder_current_path: Path
|
||||
) -> Optional[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def humanize_scope(
|
||||
self, scope: str, ctx: Union[discord.Guild, discord.abc.User, str] = None, the: bool = None
|
||||
) -> Optional[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def draw_time(self, ctx) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def rsetattr(self, obj, attr, val) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def rgetattr(self, obj, attr, *args) -> Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _check_api_tokens(self) -> MutableMapping:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def send_embed_msg(
|
||||
self, ctx: commands.Context, author: Mapping[str, str] = None, **kwargs
|
||||
) -> discord.Message:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def update_external_status(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_track_json(
|
||||
self,
|
||||
player: lavalink.Player,
|
||||
position: Union[int, str] = None,
|
||||
other_track: lavalink.Track = None,
|
||||
) -> MutableMapping:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def track_to_json(self, track: lavalink.Track) -> MutableMapping:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def time_convert(self, length: Union[int, str]) -> int:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def queue_duration(self, ctx: commands.Context) -> int:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def track_remaining_duration(self, ctx: commands.Context) -> int:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_time_string(self, seconds: int) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def set_player_settings(self, ctx: commands.Context) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def get_playlist_match(
|
||||
self,
|
||||
context: commands.Context,
|
||||
matches: MutableMapping,
|
||||
scope: str,
|
||||
author: discord.User,
|
||||
guild: discord.Guild,
|
||||
specified_user: bool = False,
|
||||
) -> Tuple[Optional["Playlist"], str, str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def is_requester_alone(self, ctx: commands.Context) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def is_requester(self, ctx: commands.Context, member: discord.Member) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _skip_action(self, ctx: commands.Context, skip_to_track: int = None) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def is_vc_full(self, channel: discord.VoiceChannel) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _has_dj_role(self, ctx: commands.Context, member: discord.Member) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def match_url(self, url: str) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _playlist_check(self, ctx: commands.Context) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def can_manage_playlist(
|
||||
self, scope: str, playlist: "Playlist", ctx: commands.Context, user, guild
|
||||
) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _maybe_update_playlist(
|
||||
self, ctx: commands.Context, player: lavalink.player_manager.Player, playlist: "Playlist"
|
||||
) -> Tuple[List[lavalink.Track], List[lavalink.Track], "Playlist"]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def is_url_allowed(self, url: str) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _eq_check(self, ctx: commands.Context, player: lavalink.Player) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _enqueue_tracks(
|
||||
self, ctx: commands.Context, query: Union["Query", list], enqueue: bool = True
|
||||
) -> Union[discord.Message, List[lavalink.Track], lavalink.Track]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _eq_interact(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
player: lavalink.Player,
|
||||
eq: "Equalizer",
|
||||
message: discord.Message,
|
||||
selected: int,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _apply_gains(self, guild_id: int, gains: List[float]) -> None:
|
||||
NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _apply_gain(self, guild_id: int, band: int, gain: float) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _get_spotify_tracks(
|
||||
self, ctx: commands.Context, query: "Query", forced: bool = False
|
||||
) -> Union[discord.Message, List[lavalink.Track], lavalink.Track]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _genre_search_button_action(
|
||||
self, ctx: commands.Context, options: List, emoji: str, page: int, playlist: bool = False
|
||||
) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_genre_search_page(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
tracks: List,
|
||||
page_num: int,
|
||||
title: str,
|
||||
playlist: bool = False,
|
||||
) -> discord.Embed:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_audioset_autoplay_toggle(self, ctx: commands.Context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _search_button_action(
|
||||
self, ctx: commands.Context, tracks: List, emoji: str, page: int
|
||||
):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def get_localtrack_folder_tracks(
|
||||
self, ctx, player: lavalink.player_manager.Player, query: "Query"
|
||||
) -> List[lavalink.rest_api.Track]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def get_localtrack_folder_list(
|
||||
self, ctx: commands.Context, query: "Query"
|
||||
) -> List["Query"]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _local_play_all(
|
||||
self, ctx: commands.Context, query: "Query", from_search: bool = False
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_search_page(
|
||||
self, ctx: commands.Context, tracks: List, page_num: int
|
||||
) -> discord.Embed:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_play(self, ctx: commands.Context, *, query: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def localtracks_folder_exists(self, ctx: commands.Context) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def get_localtracks_folders(
|
||||
self, ctx: commands.Context, search_subfolders: bool = False
|
||||
) -> List[Union[Path, "LocalPath"]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_local_search_list(
|
||||
self, to_search: List["Query"], search_words: str
|
||||
) -> List[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_stop(self, ctx: commands.Context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_queue_page(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
queue: list,
|
||||
player: lavalink.player_manager.Player,
|
||||
page_num: int,
|
||||
) -> discord.Embed:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_pause(self, ctx: commands.Context):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_queue_search_list(
|
||||
self, queue_list: List[lavalink.Track], search_words: str
|
||||
) -> List[Tuple[int, str]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_queue_search_page(
|
||||
self, ctx: commands.Context, page_num: int, search_list: List[Tuple[int, str]]
|
||||
) -> discord.Embed:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_playlist_tracks(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
player: lavalink.player_manager.Player,
|
||||
query: "Query",
|
||||
skip_cache: bool = False,
|
||||
) -> Union[discord.Message, None, List[MutableMapping]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _build_playlist_list_page(
|
||||
self, ctx: commands.Context, page_num: int, abc_names: List, scope: Optional[str]
|
||||
) -> discord.Embed:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def match_yt_playlist(self, url: str) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _load_v3_playlist(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
scope: str,
|
||||
uploaded_playlist_name: str,
|
||||
uploaded_playlist_url: str,
|
||||
track_list: List,
|
||||
author: Union[discord.User, discord.Member],
|
||||
guild: Union[discord.Guild],
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def _load_v2_playlist(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
uploaded_track_list,
|
||||
player: lavalink.player_manager.Player,
|
||||
playlist_url: str,
|
||||
uploaded_playlist_name: str,
|
||||
scope: str,
|
||||
author: Union[discord.User, discord.Member],
|
||||
guild: Union[discord.Guild],
|
||||
):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def format_time(self, time: int) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def get_lyrics_status(self, ctx: Context) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_skip(self, ctx: commands.Context, skip_to_track: int = None):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
async def command_prev(self, ctx: commands.Context):
|
||||
raise NotImplementedError()
|
||||
@ -1,28 +0,0 @@
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from redbot import VersionInfo
|
||||
from redbot.core import commands
|
||||
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"})
|
||||
|
||||
__author__ = ["aikaterna", "Draper"]
|
||||
|
||||
_ = Translator("Audio", Path(__file__).parent)
|
||||
_SCHEMA_VERSION: Final[int] = 3
|
||||
|
||||
LazyGreedyConverter = get_lazy_converter("--")
|
||||
PlaylistConverter = get_playlist_converter()
|
||||
|
||||
|
||||
class CompositeMetaClass(type(commands.Cog), type(ABC)):
|
||||
"""
|
||||
This allows the metaclass used for proper type detection to
|
||||
coexist with discord.py's metaclass
|
||||
"""
|
||||
|
||||
pass
|
||||
@ -1,25 +0,0 @@
|
||||
from ..cog_utils import CompositeMetaClass
|
||||
from .audioset import AudioSetCommands
|
||||
from .controller import PlayerControllerCommands
|
||||
from .equalizer import EqualizerCommands
|
||||
from .llset import LavalinkSetupCommands
|
||||
from .localtracks import LocalTrackCommands
|
||||
from .miscellaneous import MiscellaneousCommands
|
||||
from .player import PlayerCommands
|
||||
from .playlists import PlaylistCommands
|
||||
from .queue import QueueCommands
|
||||
|
||||
|
||||
class Commands(
|
||||
AudioSetCommands,
|
||||
PlayerControllerCommands,
|
||||
EqualizerCommands,
|
||||
LavalinkSetupCommands,
|
||||
LocalTrackCommands,
|
||||
MiscellaneousCommands,
|
||||
PlayerCommands,
|
||||
PlaylistCommands,
|
||||
QueueCommands,
|
||||
metaclass=CompositeMetaClass,
|
||||
):
|
||||
"""Class joining all command subclasses"""
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,841 +0,0 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.chat_formatting import humanize_number
|
||||
from redbot.core.utils.menus import start_adding_reactions
|
||||
from redbot.core.utils.predicates import ReactionPredicate
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.player_controller")
|
||||
|
||||
|
||||
class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.command(name="disconnect")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_disconnect(self, ctx: commands.Context):
|
||||
"""Disconnect from the voice channel."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
else:
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (
|
||||
(vote_enabled or (vote_enabled and dj_enabled))
|
||||
and not can_skip
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Disconnect"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Disconnect"),
|
||||
description=_("You need the DJ role to disconnect."),
|
||||
)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Disconnect"),
|
||||
description=_("You need the DJ role to disconnect."),
|
||||
)
|
||||
|
||||
await self.send_embed_msg(ctx, title=_("Disconnecting..."))
|
||||
self.bot.dispatch("red_audio_audio_disconnect", ctx.guild)
|
||||
self.update_player_lock(ctx, False)
|
||||
eq = player.fetch("eq")
|
||||
player.queue = []
|
||||
player.store("playing_song", None)
|
||||
if eq:
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
await player.stop()
|
||||
await player.disconnect()
|
||||
|
||||
@commands.command(name="now")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_now(self, ctx: commands.Context):
|
||||
"""Now playing."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
expected: Union[Tuple[str, ...]] = ("⏮", "⏹", "⏯", "⏭", "\N{CROSS MARK}")
|
||||
emoji = {"prev": "⏮", "stop": "⏹", "pause": "⏯", "next": "⏭", "close": "\N{CROSS MARK}"}
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if player.current:
|
||||
arrow = await self.draw_time(ctx)
|
||||
pos = self.format_time(player.position)
|
||||
if player.current.is_stream:
|
||||
dur = "LIVE"
|
||||
else:
|
||||
dur = self.format_time(player.current.length)
|
||||
song = self.get_track_description(player.current, self.local_folder_current_path) or ""
|
||||
song += _("\n Requested by: **{track.requester}**")
|
||||
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
||||
song = song.format(track=player.current, arrow=arrow, pos=pos, dur=dur)
|
||||
else:
|
||||
song = _("Nothing.")
|
||||
|
||||
if player.fetch("np_message") is not None:
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await player.fetch("np_message").delete()
|
||||
embed = discord.Embed(title=_("Now Playing"), description=song)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
|
||||
if guild_data["thumbnail"] and player.current and player.current.thumbnail:
|
||||
embed.set_thumbnail(url=player.current.thumbnail)
|
||||
shuffle = guild_data["shuffle"]
|
||||
repeat = guild_data["repeat"]
|
||||
autoplay = guild_data["auto_play"]
|
||||
text = ""
|
||||
text += (
|
||||
_("Auto-Play")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if autoplay else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Shuffle")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if shuffle else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Repeat")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if repeat else "\N{CROSS MARK}")
|
||||
)
|
||||
|
||||
message = await self.send_embed_msg(ctx, embed=embed, footer=text)
|
||||
|
||||
player.store("np_message", message)
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
if (
|
||||
(dj_enabled or vote_enabled)
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return
|
||||
|
||||
if not player.queue and not autoplay:
|
||||
expected = ("⏹", "⏯", "\N{CROSS MARK}")
|
||||
task: Optional[asyncio.Task]
|
||||
if player.current:
|
||||
task = start_adding_reactions(message, expected[:5])
|
||||
else:
|
||||
task = None
|
||||
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=ReactionPredicate.with_emojis(expected, message, ctx.author),
|
||||
timeout=30.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._clear_react(message, emoji)
|
||||
else:
|
||||
if task is not None:
|
||||
task.cancel()
|
||||
reacts = {v: k for k, v in emoji.items()}
|
||||
react = reacts[r.emoji]
|
||||
if react == "prev":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_prev)
|
||||
elif react == "stop":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_stop)
|
||||
elif react == "pause":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_pause)
|
||||
elif react == "next":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_skip)
|
||||
elif react == "close":
|
||||
await message.delete()
|
||||
|
||||
@commands.command(name="pause")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_pause(self, ctx: commands.Context):
|
||||
"""Pause or resume a playing track."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Manage Tracks"),
|
||||
description=_("You must be in the voice channel to pause or resume."),
|
||||
)
|
||||
if dj_enabled and not can_skip and not await self.is_requester_alone(ctx):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Manage Tracks"),
|
||||
description=_("You need the DJ role to pause or resume tracks."),
|
||||
)
|
||||
|
||||
if not player.current:
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
description = self.get_track_description(player.current, self.local_folder_current_path)
|
||||
|
||||
if player.current and not player.paused:
|
||||
await player.pause()
|
||||
return await self.send_embed_msg(ctx, title=_("Track Paused"), description=description)
|
||||
if player.current and player.paused:
|
||||
await player.pause(False)
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Track Resumed"), description=description
|
||||
)
|
||||
|
||||
await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
@commands.command(name="prev")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_prev(self, ctx: commands.Context):
|
||||
"""Skip to the start of the previously played track."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("You must be in the voice channel to skip the track."),
|
||||
)
|
||||
if (vote_enabled or (vote_enabled and dj_enabled)) and not can_skip and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not (can_skip or is_requester) and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_(
|
||||
"You need the DJ role or be the track requester "
|
||||
"to enqueue the previous song tracks."
|
||||
),
|
||||
)
|
||||
|
||||
if player.fetch("prev_song") is None:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("No previous track.")
|
||||
)
|
||||
else:
|
||||
track = player.fetch("prev_song")
|
||||
player.add(player.fetch("prev_requester"), track)
|
||||
self.bot.dispatch("red_audio_track_enqueue", player.channel.guild, track, ctx.author)
|
||||
queue_len = len(player.queue)
|
||||
bump_song = player.queue[-1]
|
||||
player.queue.insert(0, bump_song)
|
||||
player.queue.pop(queue_len)
|
||||
await player.skip()
|
||||
description = self.get_track_description(
|
||||
player.current, self.local_folder_current_path
|
||||
)
|
||||
embed = discord.Embed(title=_("Replaying Track"), description=description)
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
|
||||
@commands.command(name="seek")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_seek(self, ctx: commands.Context, seconds: Union[int, str]):
|
||||
"""Seek ahead or behind on a track by seconds or a to a specific time.
|
||||
|
||||
Accepts seconds or a value formatted like 00:00:00 (`hh:mm:ss`) or 00:00 (`mm:ss`).
|
||||
"""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("You must be in the voice channel to use seek."),
|
||||
)
|
||||
|
||||
if vote_enabled and not can_skip and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
|
||||
if dj_enabled and not (can_skip or is_requester) and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("You need the DJ role or be the track requester to use seek."),
|
||||
)
|
||||
|
||||
if player.current:
|
||||
if player.current.is_stream:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Seek Tracks"), description=_("Can't seek on a stream.")
|
||||
)
|
||||
else:
|
||||
try:
|
||||
int(seconds)
|
||||
abs_position = False
|
||||
except ValueError:
|
||||
abs_position = True
|
||||
seconds = self.time_convert(seconds)
|
||||
if seconds == 0:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Seek Tracks"),
|
||||
description=_("Invalid input for the time to seek."),
|
||||
)
|
||||
if not abs_position:
|
||||
time_sec = int(seconds) * 1000
|
||||
seek = player.position + time_sec
|
||||
if seek <= 0:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Moved {num_seconds}s to 00:00:00").format(
|
||||
num_seconds=seconds
|
||||
),
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Moved {num_seconds}s to {time}").format(
|
||||
num_seconds=seconds, time=self.format_time(seek)
|
||||
),
|
||||
)
|
||||
await player.seek(seek)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Moved to {time}").format(time=self.format_time(seconds * 1000)),
|
||||
)
|
||||
await player.seek(seconds * 1000)
|
||||
else:
|
||||
await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
@commands.group(name="shuffle", autohelp=False)
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_shuffle(self, ctx: commands.Context):
|
||||
"""Toggle shuffle."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You need the DJ role to toggle shuffle."),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You must be in the voice channel to toggle shuffle."),
|
||||
)
|
||||
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
await self.config.guild(ctx.guild).shuffle.set(not shuffle)
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Shuffle tracks: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not shuffle else _("Disabled")
|
||||
),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
@command_shuffle.command(name="bumped")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_shuffle_bumpped(self, ctx: commands.Context):
|
||||
"""Toggle bumped track shuffle.
|
||||
|
||||
Set this to disabled if you wish to avoid bumped songs being shuffled.
|
||||
This takes priority over `[p]shuffle`.
|
||||
"""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You need the DJ role to toggle shuffle."),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Shuffle"),
|
||||
description=_("You must be in the voice channel to toggle shuffle."),
|
||||
)
|
||||
|
||||
bumped = await self.config.guild(ctx.guild).shuffle_bumped()
|
||||
await self.config.guild(ctx.guild).shuffle_bumped.set(not bumped)
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Shuffle bumped tracks: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not bumped else _("Disabled")
|
||||
),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
@commands.command(name="skip")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_skip(self, ctx: commands.Context, skip_to_track: int = None):
|
||||
"""Skip to the next track, or to a given track number."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("You must be in the voice channel to skip the music."),
|
||||
)
|
||||
if not player.current:
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
if dj_enabled and not vote_enabled:
|
||||
if not (can_skip or is_requester) and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_(
|
||||
"You need the DJ role or be the track requester to skip tracks."
|
||||
),
|
||||
)
|
||||
if (
|
||||
is_requester
|
||||
and not can_skip
|
||||
and isinstance(skip_to_track, int)
|
||||
and skip_to_track > 1
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_("You can only skip the current track."),
|
||||
)
|
||||
|
||||
if vote_enabled:
|
||||
if not can_skip:
|
||||
if skip_to_track is not None:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Skip Tracks"),
|
||||
description=_(
|
||||
"Can't skip to a specific track in vote mode without the DJ role."
|
||||
),
|
||||
)
|
||||
if ctx.author.id in self.skip_votes[ctx.message.guild]:
|
||||
self.skip_votes[ctx.message.guild].remove(ctx.author.id)
|
||||
reply = _("I removed your vote to skip.")
|
||||
else:
|
||||
self.skip_votes[ctx.message.guild].append(ctx.author.id)
|
||||
reply = _("You voted to skip.")
|
||||
|
||||
num_votes = len(self.skip_votes[ctx.message.guild])
|
||||
vote_mods = []
|
||||
for member in player.channel.members:
|
||||
can_skip = await self._can_instaskip(ctx, member)
|
||||
if can_skip:
|
||||
vote_mods.append(member)
|
||||
num_members = len(player.channel.members) - len(vote_mods)
|
||||
vote = int(100 * num_votes / num_members)
|
||||
percent = await self.config.guild(ctx.guild).vote_percent()
|
||||
if vote >= percent:
|
||||
self.skip_votes[ctx.message.guild] = []
|
||||
await self.send_embed_msg(ctx, title=_("Vote threshold met."))
|
||||
return await self._skip_action(ctx)
|
||||
else:
|
||||
reply += _(
|
||||
" Votes: {num_votes}/{num_members}"
|
||||
" ({cur_percent}% out of {required_percent}% needed)"
|
||||
).format(
|
||||
num_votes=humanize_number(num_votes),
|
||||
num_members=humanize_number(num_members),
|
||||
cur_percent=vote,
|
||||
required_percent=percent,
|
||||
)
|
||||
return await self.send_embed_msg(ctx, title=reply)
|
||||
else:
|
||||
return await self._skip_action(ctx, skip_to_track)
|
||||
else:
|
||||
return await self._skip_action(ctx, skip_to_track)
|
||||
|
||||
@commands.command(name="stop")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_stop(self, ctx: commands.Context):
|
||||
"""Stop playback and clear the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Stop Player"),
|
||||
description=_("You must be in the voice channel to stop the music."),
|
||||
)
|
||||
if (vote_enabled or (vote_enabled and dj_enabled)) and not can_skip and not is_alone:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Stop Player"),
|
||||
description=_("There are other people listening - vote to skip instead."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Stop Player"),
|
||||
description=_("You need the DJ role to stop the music."),
|
||||
)
|
||||
if (
|
||||
player.is_playing
|
||||
or (not player.is_playing and player.paused)
|
||||
or player.queue
|
||||
or getattr(player.current, "extras", {}).get("autoplay")
|
||||
):
|
||||
eq = player.fetch("eq")
|
||||
if eq:
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
player.queue = []
|
||||
player.store("playing_song", None)
|
||||
player.store("prev_requester", None)
|
||||
player.store("prev_song", None)
|
||||
player.store("requester", None)
|
||||
await player.stop()
|
||||
await self.send_embed_msg(ctx, title=_("Stopping..."))
|
||||
|
||||
@commands.command(name="summon")
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_summon(self, ctx: commands.Context):
|
||||
"""Summon the bot to a voice channel."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
is_alone = await self.is_requester_alone(ctx)
|
||||
is_requester = await self.is_requester(ctx, ctx.author)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (vote_enabled or (vote_enabled and dj_enabled)) and not can_skip and not is_alone:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("There are other people listening."),
|
||||
)
|
||||
if dj_enabled and not vote_enabled and not (can_skip or is_requester) and not is_alone:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("You need the DJ role to summon the bot."),
|
||||
)
|
||||
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
else:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ctx.author.voice.channel == player.channel:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return
|
||||
await player.move_to(ctx.author.voice.channel)
|
||||
except AttributeError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Join Voice Channel"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
|
||||
@commands.command(name="volume")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_volume(self, ctx: commands.Context, vol: int = None):
|
||||
"""Set the volume, 1% - 150%."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if not vol:
|
||||
vol = await self.config.guild(ctx.guild).volume()
|
||||
embed = discord.Embed(title=_("Current Volume:"), description=str(vol) + "%")
|
||||
if not self._player_check(ctx):
|
||||
embed.set_footer(text=_("Nothing playing."))
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
if self._player_check(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Change Volume"),
|
||||
description=_("You must be in the voice channel to change the volume."),
|
||||
)
|
||||
if dj_enabled and not can_skip and not await self._has_dj_role(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Change Volume"),
|
||||
description=_("You need the DJ role to change the volume."),
|
||||
)
|
||||
if vol < 0:
|
||||
vol = 0
|
||||
if vol > 150:
|
||||
vol = 150
|
||||
await self.config.guild(ctx.guild).volume.set(vol)
|
||||
if self._player_check(ctx):
|
||||
await lavalink.get_player(ctx.guild.id).set_volume(vol)
|
||||
else:
|
||||
await self.config.guild(ctx.guild).volume.set(vol)
|
||||
if self._player_check(ctx):
|
||||
await lavalink.get_player(ctx.guild.id).set_volume(vol)
|
||||
embed = discord.Embed(title=_("Volume:"), description=str(vol) + "%")
|
||||
if not self._player_check(ctx):
|
||||
embed.set_footer(text=_("Nothing playing."))
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
|
||||
@commands.command(name="repeat")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_repeat(self, ctx: commands.Context):
|
||||
"""Toggle repeat."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if dj_enabled and not can_skip and not await self._has_dj_role(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Repeat"),
|
||||
description=_("You need the DJ role to toggle repeat."),
|
||||
)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Toggle Repeat"),
|
||||
description=_("You must be in the voice channel to toggle repeat."),
|
||||
)
|
||||
|
||||
autoplay = await self.config.guild(ctx.guild).auto_play()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
msg = ""
|
||||
msg += _("Repeat tracks: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not repeat else _("Disabled")
|
||||
)
|
||||
await self.config.guild(ctx.guild).repeat.set(not repeat)
|
||||
if repeat is not True and autoplay is True:
|
||||
msg += _("\nAuto-play has been disabled.")
|
||||
await self.config.guild(ctx.guild).auto_play.set(False)
|
||||
|
||||
embed = discord.Embed(title=_("Setting Changed"), description=msg)
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
if self._player_check(ctx):
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
@commands.command(name="remove")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_remove(self, ctx: commands.Context, index_or_url: Union[int, str]):
|
||||
"""Remove a specific track number from the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing queued."))
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_("You need the DJ role to remove tracks."),
|
||||
)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_("You must be in the voice channel to manage the queue."),
|
||||
)
|
||||
if isinstance(index_or_url, int):
|
||||
if index_or_url > len(player.queue) or index_or_url < 1:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_(
|
||||
"Song number must be greater than 1 and within the queue limit."
|
||||
),
|
||||
)
|
||||
index_or_url -= 1
|
||||
removed = player.queue.pop(index_or_url)
|
||||
removed_title = self.get_track_description(removed, self.local_folder_current_path)
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed track from queue"),
|
||||
description=_("Removed {track} from the queue.").format(track=removed_title),
|
||||
)
|
||||
else:
|
||||
clean_tracks = []
|
||||
removed_tracks = 0
|
||||
async for track in AsyncIter(player.queue):
|
||||
if track.uri != index_or_url:
|
||||
clean_tracks.append(track)
|
||||
else:
|
||||
removed_tracks += 1
|
||||
player.queue = clean_tracks
|
||||
if removed_tracks == 0:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Queue"),
|
||||
description=_("Removed 0 tracks, nothing matches the URL provided."),
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed track from queue"),
|
||||
description=_(
|
||||
"Removed {removed_tracks} tracks from queue "
|
||||
"which matched the URL provided."
|
||||
).format(removed_tracks=removed_tracks),
|
||||
)
|
||||
|
||||
@commands.command(name="bump")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_bump(self, ctx: commands.Context, index: int):
|
||||
"""Bump a track number to the top of the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("You must be in the voice channel to bump a track."),
|
||||
)
|
||||
if dj_enabled and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("You need the DJ role to bump tracks."),
|
||||
)
|
||||
if index > len(player.queue) or index < 1:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("Song number must be greater than 1 and within the queue limit."),
|
||||
)
|
||||
|
||||
bump_index = index - 1
|
||||
bump_song = player.queue[bump_index]
|
||||
bump_song.extras["bumped"] = True
|
||||
player.queue.insert(0, bump_song)
|
||||
removed = player.queue.pop(index)
|
||||
description = self.get_track_description(removed, self.local_folder_current_path)
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Moved track to the top of the queue."), description=description
|
||||
)
|
||||
@ -1,385 +0,0 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import re
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.chat_formatting import box, humanize_number, pagify
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions
|
||||
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||
|
||||
from ...equalizer import Equalizer
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.equalizer")
|
||||
|
||||
|
||||
class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="eq", invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_equalizer(self, ctx: commands.Context):
|
||||
"""Equalizer management."""
|
||||
if not self._player_check(ctx):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
reactions = [
|
||||
"\N{BLACK LEFT-POINTING TRIANGLE}",
|
||||
"\N{LEFTWARDS BLACK ARROW}",
|
||||
"\N{BLACK UP-POINTING DOUBLE TRIANGLE}",
|
||||
"\N{UP-POINTING SMALL RED TRIANGLE}",
|
||||
"\N{DOWN-POINTING SMALL RED TRIANGLE}",
|
||||
"\N{BLACK DOWN-POINTING DOUBLE TRIANGLE}",
|
||||
"\N{BLACK RIGHTWARDS ARROW}",
|
||||
"\N{BLACK RIGHT-POINTING TRIANGLE}",
|
||||
"\N{BLACK CIRCLE FOR RECORD}",
|
||||
"\N{INFORMATION SOURCE}",
|
||||
]
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
eq_message = await ctx.send(box(eq.visualise(), lang="ini"))
|
||||
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await eq_message.add_reaction("\N{INFORMATION SOURCE}")
|
||||
else:
|
||||
start_adding_reactions(eq_message, reactions)
|
||||
|
||||
eq_msg_with_reacts = await ctx.fetch_message(eq_message.id)
|
||||
player.store("eq_message", eq_msg_with_reacts)
|
||||
await self._eq_interact(ctx, player, eq, eq_msg_with_reacts, 0)
|
||||
|
||||
@command_equalizer.command(name="delete", aliases=["del", "remove"])
|
||||
async def command_equalizer_delete(self, ctx: commands.Context, eq_preset: str):
|
||||
"""Delete a saved eq preset."""
|
||||
async with self.config.custom("EQUALIZER", ctx.guild.id).eq_presets() as eq_presets:
|
||||
eq_preset = eq_preset.lower()
|
||||
try:
|
||||
if eq_presets[eq_preset][
|
||||
"author"
|
||||
] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Delete Preset"),
|
||||
description=_("You are not the author of that preset setting."),
|
||||
)
|
||||
del eq_presets[eq_preset]
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Delete Preset"),
|
||||
description=_(
|
||||
"{eq_preset} is not in the eq preset list.".format(
|
||||
eq_preset=eq_preset.capitalize()
|
||||
)
|
||||
),
|
||||
)
|
||||
except TypeError:
|
||||
if await self._can_instaskip(ctx, ctx.author):
|
||||
del eq_presets[eq_preset]
|
||||
else:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Delete Preset"),
|
||||
description=_("You are not the author of that preset setting."),
|
||||
)
|
||||
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("The {preset_name} preset was deleted.".format(preset_name=eq_preset))
|
||||
)
|
||||
|
||||
@command_equalizer.command(name="list")
|
||||
async def command_equalizer_list(self, ctx: commands.Context):
|
||||
"""List saved eq presets."""
|
||||
eq_presets = await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets()
|
||||
if not eq_presets.keys():
|
||||
return await self.send_embed_msg(ctx, title=_("No saved equalizer presets."))
|
||||
|
||||
space = "\N{EN SPACE}"
|
||||
header_name = _("Preset Name")
|
||||
header_author = _("Author")
|
||||
header = box(
|
||||
"[{header_name}]{space}[{header_author}]\n".format(
|
||||
header_name=header_name, space=space * 9, header_author=header_author
|
||||
),
|
||||
lang="ini",
|
||||
)
|
||||
preset_list = ""
|
||||
for preset, bands in eq_presets.items():
|
||||
try:
|
||||
author = self.bot.get_user(bands["author"])
|
||||
except TypeError:
|
||||
author = "None"
|
||||
msg = f"{preset}{space * (22 - len(preset))}{author}\n"
|
||||
preset_list += msg
|
||||
|
||||
page_list = []
|
||||
colour = await ctx.embed_colour()
|
||||
for page in pagify(preset_list, delims=[", "], page_length=1000):
|
||||
formatted_page = box(page, lang="ini")
|
||||
embed = discord.Embed(colour=colour, description=f"{header}\n{formatted_page}")
|
||||
embed.set_footer(
|
||||
text=_("{num} preset(s)").format(num=humanize_number(len(list(eq_presets.keys()))))
|
||||
)
|
||||
page_list.append(embed)
|
||||
await menu(ctx, page_list, DEFAULT_CONTROLS)
|
||||
|
||||
@command_equalizer.command(name="load")
|
||||
async def command_equalizer_load(self, ctx: commands.Context, eq_preset: str):
|
||||
"""Load a saved eq preset."""
|
||||
eq_preset = eq_preset.lower()
|
||||
eq_presets = await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets()
|
||||
try:
|
||||
eq_values = eq_presets[eq_preset]["bands"]
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("No Preset Found"),
|
||||
description=_(
|
||||
"Preset named {eq_preset} does not exist.".format(eq_preset=eq_preset)
|
||||
),
|
||||
)
|
||||
except TypeError:
|
||||
eq_values = eq_presets[eq_preset]
|
||||
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Load Preset"),
|
||||
description=_("You need the DJ role to load equalizer presets."),
|
||||
)
|
||||
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq_values)
|
||||
await self._eq_check(ctx, player)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
message = await ctx.send(
|
||||
content=box(eq.visualise(), lang="ini"),
|
||||
embed=discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("The {eq_preset} preset was loaded.".format(eq_preset=eq_preset)),
|
||||
),
|
||||
)
|
||||
player.store("eq_message", message)
|
||||
|
||||
@command_equalizer.command(name="reset")
|
||||
async def command_equalizer_reset(self, ctx: commands.Context):
|
||||
"""Reset the eq to 0 across all bands."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Modify Preset"),
|
||||
description=_("You need the DJ role to reset the equalizer."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
|
||||
for band in range(eq.band_count):
|
||||
eq.set_gain(band, 0.0)
|
||||
|
||||
await self._apply_gains(ctx.guild.id, eq.bands)
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
player.store("eq", eq)
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
message = await ctx.send(
|
||||
content=box(eq.visualise(), lang="ini"),
|
||||
embed=discord.Embed(
|
||||
colour=await ctx.embed_colour(), title=_("Equalizer values have been reset.")
|
||||
),
|
||||
)
|
||||
player.store("eq_message", message)
|
||||
|
||||
@command_equalizer.command(name="save")
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
async def command_equalizer_save(self, ctx: commands.Context, eq_preset: str = None):
|
||||
"""Save the current eq settings to a preset."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Save Preset"),
|
||||
description=_("You need the DJ role to save equalizer presets."),
|
||||
)
|
||||
if not eq_preset:
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Please enter a name for this equalizer preset.")
|
||||
)
|
||||
try:
|
||||
eq_name_msg = await self.bot.wait_for(
|
||||
"message",
|
||||
timeout=15.0,
|
||||
check=MessagePredicate.regex(fr"^(?!{re.escape(ctx.prefix)})", ctx),
|
||||
)
|
||||
eq_preset = eq_name_msg.content.split(" ")[0].strip('"').lower()
|
||||
except asyncio.TimeoutError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Save Preset"),
|
||||
description=_(
|
||||
"No equalizer preset name entered, try the command again later."
|
||||
),
|
||||
)
|
||||
eq_preset = eq_preset or ""
|
||||
eq_exists_msg = None
|
||||
eq_preset = eq_preset.lower().lstrip(ctx.prefix)
|
||||
eq_presets = await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets()
|
||||
eq_list = list(eq_presets.keys())
|
||||
|
||||
if len(eq_preset) > 20:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Save Preset"),
|
||||
description=_("Try the command again with a shorter name."),
|
||||
)
|
||||
if eq_preset in eq_list:
|
||||
eq_exists_msg = await self.send_embed_msg(
|
||||
ctx, title=_("Preset name already exists, do you want to replace it?")
|
||||
)
|
||||
start_adding_reactions(eq_exists_msg, ReactionPredicate.YES_OR_NO_EMOJIS)
|
||||
pred = ReactionPredicate.yes_or_no(eq_exists_msg, ctx.author)
|
||||
await self.bot.wait_for("reaction_add", check=pred)
|
||||
if not pred.result:
|
||||
await self._clear_react(eq_exists_msg)
|
||||
embed2 = discord.Embed(
|
||||
colour=await ctx.embed_colour(), title=_("Not saving preset.")
|
||||
)
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await eq_exists_msg.edit(embed=embed2)
|
||||
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
to_append = {eq_preset: {"author": ctx.author.id, "bands": eq.bands}}
|
||||
new_eq_presets = {**eq_presets, **to_append}
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_presets.set(new_eq_presets)
|
||||
embed3 = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Current equalizer saved to the {preset_name} preset.").format(
|
||||
preset_name=eq_preset
|
||||
),
|
||||
)
|
||||
if eq_exists_msg:
|
||||
await self._clear_react(eq_exists_msg)
|
||||
await eq_exists_msg.edit(embed=embed3)
|
||||
else:
|
||||
await self.send_embed_msg(ctx, embed=embed3)
|
||||
|
||||
@command_equalizer.command(name="set")
|
||||
async def command_equalizer_set(
|
||||
self, ctx: commands.Context, band_name_or_position, band_value: float
|
||||
):
|
||||
"""Set an eq band with a band number or name and value.
|
||||
|
||||
Band positions are 1-15 and values have a range of -0.25 to 1.0.
|
||||
Band names are 25, 40, 63, 100, 160, 250, 400, 630, 1k, 1.6k, 2.5k, 4k,
|
||||
6.3k, 10k, and 16k Hz.
|
||||
Setting a band value to -0.25 nullifies it while +0.25 is double.
|
||||
"""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Set Preset"),
|
||||
description=_("You need the DJ role to set equalizer presets."),
|
||||
)
|
||||
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
band_names = [
|
||||
"25",
|
||||
"40",
|
||||
"63",
|
||||
"100",
|
||||
"160",
|
||||
"250",
|
||||
"400",
|
||||
"630",
|
||||
"1k",
|
||||
"1.6k",
|
||||
"2.5k",
|
||||
"4k",
|
||||
"6.3k",
|
||||
"10k",
|
||||
"16k",
|
||||
]
|
||||
|
||||
eq = player.fetch("eq", Equalizer())
|
||||
bands_num = eq.band_count
|
||||
if band_value > 1:
|
||||
band_value = 1
|
||||
elif band_value <= -0.25:
|
||||
band_value = -0.25
|
||||
else:
|
||||
band_value = round(band_value, 1)
|
||||
|
||||
try:
|
||||
band_number = int(band_name_or_position) - 1
|
||||
except ValueError:
|
||||
band_number = 1000
|
||||
|
||||
if band_number not in range(0, bands_num) and band_name_or_position not in band_names:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Band"),
|
||||
description=_(
|
||||
"Valid band numbers are 1-15 or the band names listed in "
|
||||
"the help for this command."
|
||||
),
|
||||
)
|
||||
|
||||
if band_name_or_position in band_names:
|
||||
band_pos = band_names.index(band_name_or_position)
|
||||
band_int = False
|
||||
eq.set_gain(int(band_pos), band_value)
|
||||
await self._apply_gain(ctx.guild.id, int(band_pos), band_value)
|
||||
else:
|
||||
band_int = True
|
||||
eq.set_gain(band_number, band_value)
|
||||
await self._apply_gain(ctx.guild.id, band_number, band_value)
|
||||
|
||||
await self._eq_msg_clear(player.fetch("eq_message"))
|
||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||
player.store("eq", eq)
|
||||
band_name = band_names[band_number] if band_int else band_name_or_position
|
||||
message = await ctx.send(
|
||||
content=box(eq.visualise(), lang="ini"),
|
||||
embed=discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Preset Modified"),
|
||||
description=_("The {band_name}Hz band has been set to {band_value}.").format(
|
||||
band_name=band_name, band_value=band_value
|
||||
),
|
||||
),
|
||||
)
|
||||
player.store("eq_message", message)
|
||||
@ -1,168 +0,0 @@
|
||||
import logging
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.lavalink_setup")
|
||||
|
||||
|
||||
class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="llsetup", aliases=["llset"])
|
||||
@commands.is_owner()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_llsetup(self, ctx: commands.Context):
|
||||
"""Lavalink server configuration options."""
|
||||
|
||||
@command_llsetup.command(name="external")
|
||||
async def command_llsetup_external(self, ctx: commands.Context):
|
||||
"""Toggle using external Lavalink servers."""
|
||||
external = await self.config.use_external_lavalink()
|
||||
await self.config.use_external_lavalink.set(not external)
|
||||
|
||||
if external:
|
||||
embed = discord.Embed(
|
||||
title=_("Setting Changed"),
|
||||
description=_("External Lavalink server: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not external else _("Disabled")
|
||||
),
|
||||
)
|
||||
await self.send_embed_msg(ctx, embed=embed)
|
||||
else:
|
||||
try:
|
||||
if self.player_manager is not None:
|
||||
await self.player_manager.shutdown()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_(
|
||||
"External Lavalink server: {true_or_false}\n"
|
||||
"For it to take effect please reload "
|
||||
"Audio (`{prefix}reload audio`)."
|
||||
).format(
|
||||
true_or_false=_("Enabled") if not external else _("Disabled"),
|
||||
prefix=ctx.prefix,
|
||||
),
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("External Lavalink server: {true_or_false}.").format(
|
||||
true_or_false=_("Enabled") if not external else _("Disabled")
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="host")
|
||||
async def command_llsetup_host(self, ctx: commands.Context, host: str):
|
||||
"""Set the Lavalink server host."""
|
||||
await self.config.host.set(host)
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Host set to {host}.").format(host=host),
|
||||
footer=footer,
|
||||
)
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="password")
|
||||
async def command_llsetup_password(self, ctx: commands.Context, password: str):
|
||||
"""Set the Lavalink server password."""
|
||||
await self.config.password.set(str(password))
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Server password set to {password}.").format(password=password),
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="restport")
|
||||
async def command_llsetup_restport(self, ctx: commands.Context, rest_port: int):
|
||||
"""Set the Lavalink REST server port."""
|
||||
await self.config.rest_port.set(rest_port)
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("REST port set to {port}.").format(port=rest_port),
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
|
||||
@command_llsetup.command(name="wsport")
|
||||
async def command_llsetup_wsport(self, ctx: commands.Context, ws_port: int):
|
||||
"""Set the Lavalink websocket server port."""
|
||||
await self.config.ws_port.set(ws_port)
|
||||
footer = None
|
||||
if await self.update_external_status():
|
||||
footer = _("External Lavalink server set to True.")
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Setting Changed"),
|
||||
description=_("Websocket port set to {port}.").format(port=ws_port),
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
try:
|
||||
self.lavalink_restart_connect()
|
||||
except ProcessLookupError:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Failed To Shutdown Lavalink"),
|
||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
||||
prefix=ctx.prefix
|
||||
),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,118 +0,0 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import MutableMapping
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page
|
||||
|
||||
from ...audio_dataclasses import LocalPath, Query
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.local_track")
|
||||
|
||||
|
||||
class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="local")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_local(self, ctx: commands.Context):
|
||||
"""Local playback commands."""
|
||||
|
||||
@command_local.command(name="folder", aliases=["start"])
|
||||
async def command_local_folder(self, ctx: commands.Context, *, folder: str = None):
|
||||
"""Play all songs in a localtracks folder."""
|
||||
if not await self.localtracks_folder_exists(ctx):
|
||||
return
|
||||
|
||||
if not folder:
|
||||
await ctx.invoke(self.command_local_play)
|
||||
else:
|
||||
folder = folder.strip()
|
||||
_dir = LocalPath.joinpath(self.local_folder_current_path, folder)
|
||||
if not _dir.exists():
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Folder Not Found"),
|
||||
description=_("Localtracks folder named {name} does not exist.").format(
|
||||
name=folder
|
||||
),
|
||||
)
|
||||
query = Query.process_input(
|
||||
_dir, self.local_folder_current_path, search_subfolders=True
|
||||
)
|
||||
await self._local_play_all(ctx, query, from_search=False if not folder else True)
|
||||
|
||||
@command_local.command(name="play")
|
||||
async def command_local_play(self, ctx: commands.Context):
|
||||
"""Play a local track."""
|
||||
if not await self.localtracks_folder_exists(ctx):
|
||||
return
|
||||
localtracks_folders = await self.get_localtracks_folders(ctx, search_subfolders=True)
|
||||
if not localtracks_folders:
|
||||
return await self.send_embed_msg(ctx, title=_("No album folders found."))
|
||||
async with ctx.typing():
|
||||
len_folder_pages = math.ceil(len(localtracks_folders) / 5)
|
||||
folder_page_list = []
|
||||
for page_num in range(1, len_folder_pages + 1):
|
||||
embed = await self._build_search_page(ctx, localtracks_folders, page_num)
|
||||
folder_page_list.append(embed)
|
||||
|
||||
async def _local_folder_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
await self._search_button_action(ctx, localtracks_folders, emoji, page)
|
||||
return None
|
||||
|
||||
local_folder_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _local_folder_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await menu(ctx, folder_page_list, local_folder_controls)
|
||||
|
||||
@command_local.command(name="search")
|
||||
async def command_local_search(self, ctx: commands.Context, *, search_words):
|
||||
"""Search for songs across all localtracks folders."""
|
||||
if not await self.localtracks_folder_exists(ctx):
|
||||
return
|
||||
all_tracks = await self.get_localtrack_folder_list(
|
||||
ctx,
|
||||
(
|
||||
Query.process_input(
|
||||
Path(await self.config.localpath()).absolute(),
|
||||
self.local_folder_current_path,
|
||||
search_subfolders=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
if not all_tracks:
|
||||
return await self.send_embed_msg(ctx, title=_("No album folders found."))
|
||||
async with ctx.typing():
|
||||
search_list = await self._build_local_search_list(all_tracks, search_words)
|
||||
if not search_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No matches."))
|
||||
return await ctx.invoke(self.command_search, query=search_list)
|
||||
@ -1,138 +0,0 @@
|
||||
import datetime
|
||||
import heapq
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.chat_formatting import humanize_number, pagify
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.miscellaneous")
|
||||
|
||||
|
||||
class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.command(name="sing")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_sing(self, ctx: commands.Context):
|
||||
"""Make Red sing one of her songs."""
|
||||
ids = (
|
||||
"zGTkAVsrfg8",
|
||||
"cGMWL8cOeAU",
|
||||
"vFrjMq4aL-g",
|
||||
"WROI5WYBU_A",
|
||||
"41tIUr_ex3g",
|
||||
"f9O2Rjn1azc",
|
||||
)
|
||||
url = f"https://www.youtube.com/watch?v={random.choice(ids)}"
|
||||
await ctx.invoke(self.command_play, query=url)
|
||||
|
||||
@commands.command(name="audiostats")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_audiostats(self, ctx: commands.Context):
|
||||
"""Audio stats."""
|
||||
server_num = len(lavalink.active_players())
|
||||
total_num = len(lavalink.all_players())
|
||||
|
||||
msg = ""
|
||||
async for p in AsyncIter(lavalink.all_players()):
|
||||
connect_start = p.fetch("connect")
|
||||
connect_dur = self.get_time_string(
|
||||
int((datetime.datetime.utcnow() - connect_start).total_seconds())
|
||||
)
|
||||
try:
|
||||
if not p.current:
|
||||
raise AttributeError
|
||||
current_title = self.get_track_description(
|
||||
p.current, self.local_folder_current_path
|
||||
)
|
||||
msg += "{} [`{}`]: {}\n".format(p.channel.guild.name, connect_dur, current_title)
|
||||
except AttributeError:
|
||||
msg += "{} [`{}`]: **{}**\n".format(
|
||||
p.channel.guild.name, connect_dur, _("Nothing playing.")
|
||||
)
|
||||
|
||||
if total_num == 0:
|
||||
return await self.send_embed_msg(ctx, title=_("Not connected anywhere."))
|
||||
servers_embed = []
|
||||
pages = 1
|
||||
for page in pagify(msg, delims=["\n"], page_length=1500):
|
||||
em = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Playing in {num}/{total} servers:").format(
|
||||
num=humanize_number(server_num), total=humanize_number(total_num)
|
||||
),
|
||||
description=page,
|
||||
)
|
||||
em.set_footer(
|
||||
text=_("Page {}/{}").format(
|
||||
humanize_number(pages), humanize_number((math.ceil(len(msg) / 1500)))
|
||||
)
|
||||
)
|
||||
pages += 1
|
||||
servers_embed.append(em)
|
||||
|
||||
await menu(ctx, servers_embed, DEFAULT_CONTROLS)
|
||||
|
||||
@commands.command(name="percent")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_percent(self, ctx: commands.Context):
|
||||
"""Queue percentage."""
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
queue_tracks = player.queue
|
||||
requesters = {"total": 0, "users": {}}
|
||||
|
||||
async def _usercount(req_username):
|
||||
if req_username in requesters["users"]:
|
||||
requesters["users"][req_username]["songcount"] += 1
|
||||
requesters["total"] += 1
|
||||
else:
|
||||
requesters["users"][req_username] = {}
|
||||
requesters["users"][req_username]["songcount"] = 1
|
||||
requesters["total"] += 1
|
||||
|
||||
async for track in AsyncIter(queue_tracks):
|
||||
req_username = "{}#{}".format(track.requester.name, track.requester.discriminator)
|
||||
await _usercount(req_username)
|
||||
|
||||
try:
|
||||
req_username = "{}#{}".format(
|
||||
player.current.requester.name, player.current.requester.discriminator
|
||||
)
|
||||
await _usercount(req_username)
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
async for req_username in AsyncIter(requesters["users"]):
|
||||
percentage = float(requesters["users"][req_username]["songcount"]) / float(
|
||||
requesters["total"]
|
||||
)
|
||||
requesters["users"][req_username]["percent"] = round(percentage * 100, 1)
|
||||
|
||||
top_queue_users = heapq.nlargest(
|
||||
20,
|
||||
[
|
||||
(x, requesters["users"][x][y])
|
||||
for x in requesters["users"]
|
||||
for y in requesters["users"][x]
|
||||
if y == "percent"
|
||||
],
|
||||
key=lambda x: x[1],
|
||||
)
|
||||
queue_user = ["{}: {:g}%".format(x[0], x[1]) for x in top_queue_users]
|
||||
queue_user_list = "\n".join(queue_user)
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Queued and playing tracks:"), description=queue_user_list
|
||||
)
|
||||
@ -1,861 +0,0 @@
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
from typing import MutableMapping, Optional
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from discord.embeds import EmptyEmbed
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page
|
||||
|
||||
from ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
|
||||
from ...audio_logging import IS_DEBUG
|
||||
from ...errors import DatabaseError, QueryUnauthorized, SpotifyFetchError, TrackEnqueueError
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.player")
|
||||
|
||||
|
||||
class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.command(name="play")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_play(self, ctx: commands.Context, *, query: str):
|
||||
"""Play a URL or search for a track."""
|
||||
query = Query.process_input(query, self.local_folder_current_path)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
restrict = await self.config.restrict()
|
||||
if restrict and self.match_url(str(query)):
|
||||
valid_url = self.is_url_allowed(str(query))
|
||||
if not valid_url:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That URL is not allowed."),
|
||||
)
|
||||
elif not await self.is_query_allowed(self.config, ctx.guild, f"{query}", query_obj=query):
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.")
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if guild_data["dj_enabled"] and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the play command."),
|
||||
)
|
||||
if not query.valid:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("No tracks found for `{query}`.").format(
|
||||
query=query.to_string_user()
|
||||
),
|
||||
)
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
if query.is_spotify:
|
||||
return await self._get_spotify_tracks(ctx, query)
|
||||
try:
|
||||
await self._enqueue_tracks(ctx, query)
|
||||
except QueryUnauthorized as err:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=err.message
|
||||
)
|
||||
|
||||
@commands.command(name="bumpplay")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_bumpplay(
|
||||
self, ctx: commands.Context, play_now: Optional[bool] = False, *, query: str
|
||||
):
|
||||
"""Force play a URL or search for a track."""
|
||||
query = Query.process_input(query, self.local_folder_current_path)
|
||||
if not query.single_track:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Bump Track"),
|
||||
description=_("Only single tracks work with bump play."),
|
||||
)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
restrict = await self.config.restrict()
|
||||
if restrict and self.match_url(str(query)):
|
||||
valid_url = self.is_url_allowed(str(query))
|
||||
if not valid_url:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That URL is not allowed."),
|
||||
)
|
||||
elif not await self.is_query_allowed(self.config, ctx.guild, f"{query}", query_obj=query):
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("That track is not allowed.")
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if guild_data["dj_enabled"] and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the play command."),
|
||||
)
|
||||
if not query.valid:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("No tracks found for `{query}`.").format(
|
||||
query=query.to_string_user()
|
||||
),
|
||||
)
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
try:
|
||||
if query.is_spotify:
|
||||
tracks = await self._get_spotify_tracks(ctx, query)
|
||||
else:
|
||||
tracks = await self._enqueue_tracks(ctx, query, enqueue=False)
|
||||
except QueryUnauthorized as err:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=err.message
|
||||
)
|
||||
if isinstance(tracks, discord.Message):
|
||||
return
|
||||
elif not tracks:
|
||||
self.update_player_lock(ctx, False)
|
||||
title = _("Unable To Play Tracks")
|
||||
desc = _("No tracks found for `{query}`.").format(query=query.to_string_user())
|
||||
embed = discord.Embed(title=title, description=desc)
|
||||
if await self.config.use_external_lavalink() and query.is_local:
|
||||
embed.description = _(
|
||||
"Local tracks will not work "
|
||||
"if the `Lavalink.jar` cannot see the track.\n"
|
||||
"This may be due to permissions or because Lavalink.jar is being run "
|
||||
"in a different machine than the local tracks."
|
||||
)
|
||||
elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT:
|
||||
title = _("Track is not playable.")
|
||||
embed = discord.Embed(title=title)
|
||||
embed.description = _(
|
||||
"**{suffix}** is not a fully supported format and some " "tracks may not play."
|
||||
).format(suffix=query.suffix)
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
queue_dur = await self.track_remaining_duration(ctx)
|
||||
index = query.track_index
|
||||
seek = 0
|
||||
if query.start_time:
|
||||
seek = query.start_time
|
||||
single_track = (
|
||||
tracks
|
||||
if isinstance(tracks, lavalink.rest_api.Track)
|
||||
else tracks[index]
|
||||
if index
|
||||
else tracks[0]
|
||||
)
|
||||
if seek and seek > 0:
|
||||
single_track.start_timestamp = seek * 1000
|
||||
if not await self.is_query_allowed(
|
||||
self.config,
|
||||
ctx.guild,
|
||||
(
|
||||
f"{single_track.title} {single_track.author} {single_track.uri} "
|
||||
f"{str(Query.process_input(single_track, self.local_folder_current_path))}"
|
||||
),
|
||||
):
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("This track is not allowed in this server."),
|
||||
)
|
||||
elif guild_data["maxlength"] > 0:
|
||||
if self.is_track_length_allowed(single_track, guild_data["maxlength"]):
|
||||
single_track.requester = ctx.author
|
||||
player.queue.insert(0, single_track)
|
||||
player.maybe_shuffle()
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, single_track, ctx.author
|
||||
)
|
||||
else:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Track exceeds maximum length."),
|
||||
)
|
||||
|
||||
else:
|
||||
single_track.requester = ctx.author
|
||||
single_track.extras["bumped"] = True
|
||||
player.queue.insert(0, single_track)
|
||||
player.maybe_shuffle()
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, single_track, ctx.author
|
||||
)
|
||||
description = self.get_track_description(single_track, self.local_folder_current_path)
|
||||
footer = None
|
||||
if not play_now and not guild_data["shuffle"] and queue_dur > 0:
|
||||
footer = _("{time} until track playback: #1 in queue").format(
|
||||
time=self.format_time(queue_dur)
|
||||
)
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Track Enqueued"), description=description, footer=footer
|
||||
)
|
||||
|
||||
if not player.current:
|
||||
await player.play()
|
||||
elif play_now:
|
||||
await player.skip()
|
||||
|
||||
self.update_player_lock(ctx, False)
|
||||
|
||||
@commands.command(name="genre")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
async def command_genre(self, ctx: commands.Context):
|
||||
"""Pick a Spotify playlist from a list of categories to start playing."""
|
||||
|
||||
async def _category_search_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
output = await self._genre_search_button_action(ctx, category_list, emoji, page)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return output
|
||||
|
||||
async def _playlist_search_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
output = await self._genre_search_button_action(
|
||||
ctx, playlists_list, emoji, page, playlist=True
|
||||
)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return output
|
||||
|
||||
category_search_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _category_search_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
playlist_search_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _playlist_search_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
|
||||
api_data = await self._check_api_tokens()
|
||||
if any([not api_data["spotify_client_id"], not api_data["spotify_client_secret"]]):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Environment"),
|
||||
description=_(
|
||||
"The owner needs to set the Spotify client ID and Spotify client secret, "
|
||||
"before Spotify URLs or codes can be used. "
|
||||
"\nSee `{prefix}audioset spotifyapi` for instructions."
|
||||
).format(prefix=ctx.prefix),
|
||||
)
|
||||
elif not api_data["youtube_api"]:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Environment"),
|
||||
description=_(
|
||||
"The owner needs to set the YouTube API key before Spotify URLs or "
|
||||
"codes can be used.\nSee `{prefix}audioset youtubeapi` for instructions."
|
||||
).format(prefix=ctx.prefix),
|
||||
)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
if guild_data["dj_enabled"] and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the genre command."),
|
||||
)
|
||||
try:
|
||||
category_list = await self.api_interface.spotify_api.get_categories(ctx=ctx)
|
||||
except SpotifyFetchError as error:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("No categories found"),
|
||||
description=error.message.format(prefix=ctx.prefix),
|
||||
)
|
||||
if not category_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No categories found, try again later."))
|
||||
len_folder_pages = math.ceil(len(category_list) / 5)
|
||||
category_search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_folder_pages + 1)):
|
||||
embed = await self._build_genre_search_page(
|
||||
ctx, category_list, page_num, _("Categories")
|
||||
)
|
||||
category_search_page_list.append(embed)
|
||||
cat_menu_output = await menu(ctx, category_search_page_list, category_search_controls)
|
||||
if not cat_menu_output:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("No categories selected, try again later.")
|
||||
)
|
||||
category_name, category_pick = cat_menu_output
|
||||
playlists_list = await self.api_interface.spotify_api.get_playlist_from_category(
|
||||
category_pick, ctx=ctx
|
||||
)
|
||||
if not playlists_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No categories found, try again later."))
|
||||
len_folder_pages = math.ceil(len(playlists_list) / 5)
|
||||
playlists_search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_folder_pages + 1)):
|
||||
embed = await self._build_genre_search_page(
|
||||
ctx,
|
||||
playlists_list,
|
||||
page_num,
|
||||
_("Playlists for {friendly_name}").format(friendly_name=category_name),
|
||||
playlist=True,
|
||||
)
|
||||
playlists_search_page_list.append(embed)
|
||||
playlists_pick = await menu(ctx, playlists_search_page_list, playlist_search_controls)
|
||||
query = Query.process_input(playlists_pick, self.local_folder_current_path)
|
||||
if not query.valid:
|
||||
return await self.send_embed_msg(ctx, title=_("No tracks to play."))
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
if query.is_spotify:
|
||||
return await self._get_spotify_tracks(ctx, query)
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Couldn't find tracks for the selected playlist.")
|
||||
)
|
||||
|
||||
@commands.command(name="autoplay")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True)
|
||||
@commands.mod_or_permissions(manage_guild=True)
|
||||
async def command_autoplay(self, ctx: commands.Context):
|
||||
"""Starts auto play."""
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
if guild_data["dj_enabled"] and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
if (
|
||||
not ctx.author.voice or ctx.author.voice.channel != player.channel
|
||||
) and not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You must be in the voice channel to use the autoplay command."),
|
||||
)
|
||||
if len(player.queue) >= 10000:
|
||||
return await self.send_embed_msg(
|
||||
ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.")
|
||||
)
|
||||
if not await self.maybe_charge_requester(ctx, guild_data["jukebox_price"]):
|
||||
return
|
||||
try:
|
||||
await self.api_interface.autoplay(player, self.playlist_api)
|
||||
except DatabaseError:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
await self.send_embed_msg(notify_channel, title=_("Couldn't get a valid track."))
|
||||
return
|
||||
|
||||
if not guild_data["auto_play"]:
|
||||
await ctx.invoke(self.command_audioset_autoplay_toggle)
|
||||
if not guild_data["notify"] and (
|
||||
(player.current and not player.current.extras.get("autoplay")) or not player.current
|
||||
):
|
||||
await self.send_embed_msg(ctx, title=_("Auto play started."))
|
||||
elif player.current:
|
||||
await self.send_embed_msg(ctx, title=_("Adding a track to queue."))
|
||||
|
||||
@commands.command(name="search")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_search(self, ctx: commands.Context, *, query: str):
|
||||
"""Pick a track with a search.
|
||||
|
||||
Use `[p]search list <search term>` to queue all tracks found on YouTube.
|
||||
Use `[p]search sc <search term>` will search SoundCloud instead of YouTube.
|
||||
"""
|
||||
|
||||
async def _search_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
await self._search_button_action(ctx, tracks, emoji, page)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
search_controls = {
|
||||
"\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT TWO}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT THREE}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT FOUR}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{DIGIT FIVE}\N{COMBINING ENCLOSING KEYCAP}": _search_menu,
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
}
|
||||
|
||||
if not self._player_check(ctx):
|
||||
if self.lavalink_connection_aborted:
|
||||
msg = _("Connection to Lavalink has failed")
|
||||
desc = EmptyEmbed
|
||||
if await self.bot.is_owner(ctx.author):
|
||||
desc = _("Please check your console or logs for details.")
|
||||
return await self.send_embed_msg(ctx, title=msg, description=desc)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
player.store("channel", ctx.channel.id)
|
||||
player.store("guild", ctx.guild.id)
|
||||
can_skip = await self._can_instaskip(ctx, ctx.author)
|
||||
if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Search For Tracks"),
|
||||
description=_("You must be in the voice channel to enqueue tracks."),
|
||||
)
|
||||
await self._eq_check(ctx, player)
|
||||
await self.set_player_settings(ctx)
|
||||
|
||||
before_queue_length = len(player.queue)
|
||||
|
||||
if not isinstance(query, list):
|
||||
query = Query.process_input(query, self.local_folder_current_path)
|
||||
restrict = await self.config.restrict()
|
||||
if restrict and self.match_url(str(query)):
|
||||
valid_url = self.is_url_allowed(str(query))
|
||||
if not valid_url:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That URL is not allowed."),
|
||||
)
|
||||
if not await self.is_query_allowed(
|
||||
self.config, ctx.guild, f"{query}", query_obj=query
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("That track is not allowed."),
|
||||
)
|
||||
if query.invoked_from == "search list" or query.invoked_from == "local folder":
|
||||
if query.invoked_from == "search list" and not query.is_local:
|
||||
try:
|
||||
result, called_api = await self.api_interface.fetch_track(
|
||||
ctx, player, query
|
||||
)
|
||||
except TrackEnqueueError:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable get a track from Lavalink at the moment, "
|
||||
"try again in a few minutes."
|
||||
),
|
||||
)
|
||||
|
||||
tracks = result.tracks
|
||||
else:
|
||||
try:
|
||||
query.search_subfolders = True
|
||||
tracks = await self.get_localtrack_folder_tracks(ctx, player, query)
|
||||
except TrackEnqueueError:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable get a track from Lavalink at the moment, "
|
||||
"try again in a few minutes."
|
||||
),
|
||||
)
|
||||
if not tracks:
|
||||
embed = discord.Embed(title=_("Nothing found."))
|
||||
if await self.config.use_external_lavalink() and query.is_local:
|
||||
embed.description = _(
|
||||
"Local tracks will not work "
|
||||
"if the `Lavalink.jar` cannot see the track.\n"
|
||||
"This may be due to permissions or because Lavalink.jar is being run "
|
||||
"in a different machine than the local tracks."
|
||||
)
|
||||
elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT:
|
||||
embed = discord.Embed(title=_("Track is not playable."))
|
||||
embed.description = _(
|
||||
"**{suffix}** is not a fully supported format and some "
|
||||
"tracks may not play."
|
||||
).format(suffix=query.suffix)
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
queue_dur = await self.queue_duration(ctx)
|
||||
queue_total_duration = self.format_time(queue_dur)
|
||||
if guild_data["dj_enabled"] and not can_skip:
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Play Tracks"),
|
||||
description=_("You need the DJ role to queue tracks."),
|
||||
)
|
||||
track_len = 0
|
||||
empty_queue = not player.queue
|
||||
async for track in AsyncIter(tracks):
|
||||
if len(player.queue) >= 10000:
|
||||
continue
|
||||
if not await self.is_query_allowed(
|
||||
self.config,
|
||||
ctx.guild,
|
||||
(
|
||||
f"{track.title} {track.author} {track.uri} "
|
||||
f"{str(Query.process_input(track, self.local_folder_current_path))}"
|
||||
),
|
||||
):
|
||||
if IS_DEBUG:
|
||||
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
||||
continue
|
||||
elif guild_data["maxlength"] > 0:
|
||||
if self.is_track_length_allowed(track, guild_data["maxlength"]):
|
||||
track_len += 1
|
||||
player.add(ctx.author, track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
|
||||
)
|
||||
else:
|
||||
track_len += 1
|
||||
player.add(ctx.author, track)
|
||||
self.bot.dispatch(
|
||||
"red_audio_track_enqueue", player.channel.guild, track, ctx.author
|
||||
)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
player.maybe_shuffle(0 if empty_queue else 1)
|
||||
if len(tracks) > track_len:
|
||||
maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format(
|
||||
bad_tracks=(len(tracks) - track_len)
|
||||
)
|
||||
else:
|
||||
maxlength_msg = ""
|
||||
songembed = discord.Embed(
|
||||
title=_("Queued {num} track(s).{maxlength_msg}").format(
|
||||
num=track_len, maxlength_msg=maxlength_msg
|
||||
)
|
||||
)
|
||||
if not guild_data["shuffle"] and queue_dur > 0:
|
||||
if query.is_local and query.is_album:
|
||||
footer = _("folder")
|
||||
else:
|
||||
footer = _("search")
|
||||
|
||||
songembed.set_footer(
|
||||
text=_(
|
||||
"{time} until start of {type} playback: starts at #{position} in queue"
|
||||
).format(
|
||||
time=queue_total_duration,
|
||||
position=before_queue_length + 1,
|
||||
type=footer,
|
||||
)
|
||||
)
|
||||
return await self.send_embed_msg(ctx, embed=songembed)
|
||||
elif query.is_local and query.single_track:
|
||||
tracks = await self.get_localtrack_folder_list(ctx, query)
|
||||
elif query.is_local and query.is_album:
|
||||
if ctx.invoked_with == "folder":
|
||||
return await self._local_play_all(ctx, query, from_search=True)
|
||||
else:
|
||||
tracks = await self.get_localtrack_folder_list(ctx, query)
|
||||
else:
|
||||
try:
|
||||
result, called_api = await self.api_interface.fetch_track(ctx, player, query)
|
||||
except TrackEnqueueError:
|
||||
self.update_player_lock(ctx, False)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable get a track from Lavalink at the moment,"
|
||||
"try again in a few minutes."
|
||||
),
|
||||
)
|
||||
tracks = result.tracks
|
||||
if not tracks:
|
||||
embed = discord.Embed(title=_("Nothing found."))
|
||||
if await self.config.use_external_lavalink() and query.is_local:
|
||||
embed.description = _(
|
||||
"Local tracks will not work "
|
||||
"if the `Lavalink.jar` cannot see the track.\n"
|
||||
"This may be due to permissions or because Lavalink.jar is being run "
|
||||
"in a different machine than the local tracks."
|
||||
)
|
||||
elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT:
|
||||
embed = discord.Embed(title=_("Track is not playable."))
|
||||
embed.description = _(
|
||||
"**{suffix}** is not a fully supported format and some "
|
||||
"tracks may not play."
|
||||
).format(suffix=query.suffix)
|
||||
return await self.send_embed_msg(ctx, embed=embed)
|
||||
else:
|
||||
tracks = query
|
||||
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
|
||||
len_search_pages = math.ceil(len(tracks) / 5)
|
||||
search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_search_pages + 1)):
|
||||
embed = await self._build_search_page(ctx, tracks, page_num)
|
||||
search_page_list.append(embed)
|
||||
|
||||
if dj_enabled and not can_skip:
|
||||
return await menu(ctx, search_page_list, DEFAULT_CONTROLS)
|
||||
|
||||
await menu(ctx, search_page_list, search_controls)
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,359 +0,0 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
from typing import MutableMapping, Optional, Union, Tuple
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core.utils import AsyncIter
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import (
|
||||
DEFAULT_CONTROLS,
|
||||
close_menu,
|
||||
menu,
|
||||
next_page,
|
||||
prev_page,
|
||||
start_adding_reactions,
|
||||
)
|
||||
from redbot.core.utils.predicates import ReactionPredicate
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Commands.queue")
|
||||
|
||||
|
||||
class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.group(name="queue", invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
|
||||
async def command_queue(self, ctx: commands.Context, *, page: int = 1):
|
||||
"""List the songs in the queue."""
|
||||
|
||||
async def _queue_menu(
|
||||
ctx: commands.Context,
|
||||
pages: list,
|
||||
controls: MutableMapping,
|
||||
message: discord.Message,
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
await ctx.send_help(self.command_queue)
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
queue_controls = {
|
||||
"\N{LEFTWARDS BLACK ARROW}": prev_page,
|
||||
"\N{CROSS MARK}": close_menu,
|
||||
"\N{BLACK RIGHTWARDS ARROW}": next_page,
|
||||
"\N{INFORMATION SOURCE}": _queue_menu,
|
||||
}
|
||||
|
||||
if not self._player_check(ctx):
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
if player.current and not player.queue:
|
||||
arrow = await self.draw_time(ctx)
|
||||
pos = self.format_time(player.position)
|
||||
if player.current.is_stream:
|
||||
dur = "LIVE"
|
||||
else:
|
||||
dur = self.format_time(player.current.length)
|
||||
song = self.get_track_description(player.current, self.local_folder_current_path) or ""
|
||||
song += _("\n Requested by: **{track.requester}**")
|
||||
song += "\n\n{arrow}`{pos}`/`{dur}`"
|
||||
song = song.format(track=player.current, arrow=arrow, pos=pos, dur=dur)
|
||||
embed = discord.Embed(title=_("Now Playing"), description=song)
|
||||
guild_data = await self.config.guild(ctx.guild).all()
|
||||
if guild_data["thumbnail"] and player.current and player.current.thumbnail:
|
||||
embed.set_thumbnail(url=player.current.thumbnail)
|
||||
|
||||
shuffle = guild_data["shuffle"]
|
||||
repeat = guild_data["repeat"]
|
||||
autoplay = guild_data["auto_play"]
|
||||
text = ""
|
||||
text += (
|
||||
_("Auto-Play")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if autoplay else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Shuffle")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if shuffle else "\N{CROSS MARK}")
|
||||
)
|
||||
text += (
|
||||
(" | " if text else "")
|
||||
+ _("Repeat")
|
||||
+ ": "
|
||||
+ ("\N{WHITE HEAVY CHECK MARK}" if repeat else "\N{CROSS MARK}")
|
||||
)
|
||||
embed.set_footer(text=text)
|
||||
message = await self.send_embed_msg(ctx, embed=embed)
|
||||
dj_enabled = self._dj_status_cache.setdefault(ctx.guild.id, guild_data["dj_enabled"])
|
||||
vote_enabled = guild_data["vote_enabled"]
|
||||
if (
|
||||
(dj_enabled or vote_enabled)
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return
|
||||
|
||||
expected: Union[Tuple[str, ...]] = ("⏮", "⏹", "⏯", "⏭", "\N{CROSS MARK}")
|
||||
emoji = {
|
||||
"prev": "⏮",
|
||||
"stop": "⏹",
|
||||
"pause": "⏯",
|
||||
"next": "⏭",
|
||||
"close": "\N{CROSS MARK}",
|
||||
}
|
||||
if not player.queue and not autoplay:
|
||||
expected = ("⏹", "⏯", "\N{CROSS MARK}")
|
||||
if player.current:
|
||||
task: Optional[asyncio.Task] = start_adding_reactions(message, expected[:5])
|
||||
else:
|
||||
task: Optional[asyncio.Task] = None
|
||||
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=ReactionPredicate.with_emojis(expected, message, ctx.author),
|
||||
timeout=30.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._clear_react(message, emoji)
|
||||
else:
|
||||
if task is not None:
|
||||
task.cancel()
|
||||
reacts = {v: k for k, v in emoji.items()}
|
||||
react = reacts[r.emoji]
|
||||
if react == "prev":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_prev)
|
||||
elif react == "stop":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_stop)
|
||||
elif react == "pause":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_pause)
|
||||
elif react == "next":
|
||||
await self._clear_react(message, emoji)
|
||||
await ctx.invoke(self.command_skip)
|
||||
elif react == "close":
|
||||
await message.delete()
|
||||
return
|
||||
elif not player.current and not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
async with ctx.typing():
|
||||
limited_queue = player.queue[:500] # TODO: Improve when Toby menu's are merged
|
||||
len_queue_pages = math.ceil(len(limited_queue) / 10)
|
||||
queue_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_queue_pages + 1)):
|
||||
embed = await self._build_queue_page(ctx, limited_queue, player, page_num)
|
||||
queue_page_list.append(embed)
|
||||
if page > len_queue_pages:
|
||||
page = len_queue_pages
|
||||
return await menu(ctx, queue_page_list, queue_controls, page=(page - 1))
|
||||
|
||||
@command_queue.command(name="clear")
|
||||
async def command_queue_clear(self, ctx: commands.Context):
|
||||
"""Clears the queue."""
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if (
|
||||
dj_enabled
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Clear Queue"),
|
||||
description=_("You need the DJ role to clear the queue."),
|
||||
)
|
||||
player.queue.clear()
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Queue Modified"), description=_("The queue has been cleared.")
|
||||
)
|
||||
|
||||
@command_queue.command(name="clean")
|
||||
async def command_queue_clean(self, ctx: commands.Context):
|
||||
"""Removes songs from the queue if the requester is not in the voice channel."""
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if (
|
||||
dj_enabled
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Clean Queue"),
|
||||
description=_("You need the DJ role to clean the queue."),
|
||||
)
|
||||
clean_tracks = []
|
||||
removed_tracks = 0
|
||||
listeners = player.channel.members
|
||||
async for track in AsyncIter(player.queue):
|
||||
if track.requester in listeners:
|
||||
clean_tracks.append(track)
|
||||
else:
|
||||
removed_tracks += 1
|
||||
player.queue = clean_tracks
|
||||
if removed_tracks == 0:
|
||||
await self.send_embed_msg(ctx, title=_("Removed 0 tracks."))
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed Tracks From The Queue"),
|
||||
description=_(
|
||||
"Removed {removed_tracks} tracks queued by members "
|
||||
"outside of the voice channel."
|
||||
).format(removed_tracks=removed_tracks),
|
||||
)
|
||||
|
||||
@command_queue.command(name="cleanself")
|
||||
async def command_queue_cleanself(self, ctx: commands.Context):
|
||||
"""Removes all tracks you requested from the queue."""
|
||||
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
clean_tracks = []
|
||||
removed_tracks = 0
|
||||
async for track in AsyncIter(player.queue):
|
||||
if track.requester != ctx.author:
|
||||
clean_tracks.append(track)
|
||||
else:
|
||||
removed_tracks += 1
|
||||
player.queue = clean_tracks
|
||||
if removed_tracks == 0:
|
||||
await self.send_embed_msg(ctx, title=_("Removed 0 tracks."))
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Removed Tracks From The Queue"),
|
||||
description=_(
|
||||
"Removed {removed_tracks} tracks queued by {member.display_name}."
|
||||
).format(removed_tracks=removed_tracks, member=ctx.author),
|
||||
)
|
||||
|
||||
@command_queue.command(name="search")
|
||||
async def command_queue_search(self, ctx: commands.Context, *, search_words: str):
|
||||
"""Search the queue."""
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
except KeyError:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
return await self.send_embed_msg(ctx, title=_("There's nothing in the queue."))
|
||||
|
||||
search_list = await self._build_queue_search_list(player.queue, search_words)
|
||||
if not search_list:
|
||||
return await self.send_embed_msg(ctx, title=_("No matches."))
|
||||
|
||||
len_search_pages = math.ceil(len(search_list) / 10)
|
||||
search_page_list = []
|
||||
async for page_num in AsyncIter(range(1, len_search_pages + 1)):
|
||||
embed = await self._build_queue_search_page(ctx, page_num, search_list)
|
||||
search_page_list.append(embed)
|
||||
await menu(ctx, search_page_list, DEFAULT_CONTROLS)
|
||||
|
||||
@command_queue.command(name="shuffle")
|
||||
@commands.cooldown(1, 30, commands.BucketType.guild)
|
||||
async def command_queue_shuffle(self, ctx: commands.Context):
|
||||
"""Shuffles the queue."""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
)
|
||||
if (
|
||||
dj_enabled
|
||||
and not await self._can_instaskip(ctx, ctx.author)
|
||||
and not await self.is_requester_alone(ctx)
|
||||
):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("You need the DJ role to shuffle the queue."),
|
||||
)
|
||||
if not self._player_check(ctx):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("There's nothing in the queue."),
|
||||
)
|
||||
try:
|
||||
if (
|
||||
not ctx.author.voice.channel.permissions_for(ctx.me).connect
|
||||
or not ctx.author.voice.channel.permissions_for(ctx.me).move_members
|
||||
and self.is_vc_full(ctx.author.voice.channel)
|
||||
):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("I don't have permission to connect to your channel."),
|
||||
)
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store("connect", datetime.datetime.utcnow())
|
||||
except AttributeError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("Connect to a voice channel first."),
|
||||
)
|
||||
except IndexError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("Connection to Lavalink has not yet been established."),
|
||||
)
|
||||
except KeyError:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("There's nothing in the queue."),
|
||||
)
|
||||
|
||||
if not self._player_check(ctx) or not player.queue:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
return await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable To Shuffle Queue"),
|
||||
description=_("There's nothing in the queue."),
|
||||
)
|
||||
|
||||
player.force_shuffle(0)
|
||||
return await self.send_embed_msg(ctx, title=_("Queue has been shuffled."))
|
||||
@ -1,13 +0,0 @@
|
||||
import logging
|
||||
|
||||
from ..cog_utils import CompositeMetaClass
|
||||
from .cog import AudioEvents
|
||||
from .dpy import DpyEvents
|
||||
from .lavalink import LavalinkEvents
|
||||
from .red import RedEvents
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Events")
|
||||
|
||||
|
||||
class Events(AudioEvents, DpyEvents, LavalinkEvents, RedEvents, metaclass=CompositeMetaClass):
|
||||
"""Class joining all event subclasses"""
|
||||
@ -1,147 +0,0 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
from ...apis.playlist_interface import Playlist, delete_playlist, get_playlist
|
||||
from ...audio_logging import debug_exc_log
|
||||
from ...utils import PlaylistScope
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Events.audio")
|
||||
|
||||
|
||||
class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
|
||||
@commands.Cog.listener()
|
||||
async def on_red_audio_track_start(
|
||||
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
|
||||
):
|
||||
if not (track and guild):
|
||||
return
|
||||
track_identifier = track.track_identifier
|
||||
if self.playlist_api is not None:
|
||||
daily_cache = self._daily_playlist_cache.setdefault(
|
||||
guild.id, await self.config.guild(guild).daily_playlists()
|
||||
)
|
||||
global_daily_playlists = self._daily_global_playlist_cache.setdefault(
|
||||
self.bot.user.id, await self.config.daily_playlists()
|
||||
)
|
||||
today = datetime.date.today()
|
||||
midnight = datetime.datetime.combine(today, datetime.datetime.min.time())
|
||||
today_id = int(time.mktime(today.timetuple()))
|
||||
track = self.track_to_json(track)
|
||||
if daily_cache:
|
||||
name = f"Daily playlist - {today}"
|
||||
playlist: Optional[Playlist]
|
||||
try:
|
||||
playlist = await get_playlist(
|
||||
playlist_api=self.playlist_api,
|
||||
playlist_number=today_id,
|
||||
scope=PlaylistScope.GUILD.value,
|
||||
bot=self.bot,
|
||||
guild=guild,
|
||||
author=self.bot.user,
|
||||
)
|
||||
except RuntimeError:
|
||||
playlist = None
|
||||
|
||||
if playlist:
|
||||
tracks = playlist.tracks
|
||||
tracks.append(track)
|
||||
await playlist.edit({"tracks": tracks})
|
||||
else:
|
||||
playlist = Playlist(
|
||||
bot=self.bot,
|
||||
scope=PlaylistScope.GUILD.value,
|
||||
author=self.bot.user.id,
|
||||
playlist_id=today_id,
|
||||
name=name,
|
||||
playlist_url=None,
|
||||
tracks=[track],
|
||||
guild=guild,
|
||||
playlist_api=self.playlist_api,
|
||||
)
|
||||
await playlist.save()
|
||||
if global_daily_playlists:
|
||||
global_name = f"Global Daily playlist - {today}"
|
||||
try:
|
||||
playlist = await get_playlist(
|
||||
playlist_number=today_id,
|
||||
scope=PlaylistScope.GLOBAL.value,
|
||||
bot=self.bot,
|
||||
guild=guild,
|
||||
author=self.bot.user,
|
||||
playlist_api=self.playlist_api,
|
||||
)
|
||||
except RuntimeError:
|
||||
playlist = None
|
||||
if playlist:
|
||||
tracks = playlist.tracks
|
||||
tracks.append(track)
|
||||
await playlist.edit({"tracks": tracks})
|
||||
else:
|
||||
playlist = Playlist(
|
||||
bot=self.bot,
|
||||
scope=PlaylistScope.GLOBAL.value,
|
||||
author=self.bot.user.id,
|
||||
playlist_id=today_id,
|
||||
name=global_name,
|
||||
playlist_url=None,
|
||||
tracks=[track],
|
||||
guild=guild,
|
||||
playlist_api=self.playlist_api,
|
||||
)
|
||||
await playlist.save()
|
||||
too_old = midnight - datetime.timedelta(days=8)
|
||||
too_old_id = int(time.mktime(too_old.timetuple()))
|
||||
try:
|
||||
await delete_playlist(
|
||||
scope=PlaylistScope.GUILD.value,
|
||||
playlist_id=too_old_id,
|
||||
guild=guild,
|
||||
author=self.bot.user,
|
||||
playlist_api=self.playlist_api,
|
||||
bot=self.bot,
|
||||
)
|
||||
except Exception as err:
|
||||
debug_exc_log(log, err, f"Failed to delete daily playlist ID: {too_old_id}")
|
||||
try:
|
||||
await delete_playlist(
|
||||
scope=PlaylistScope.GLOBAL.value,
|
||||
playlist_id=too_old_id,
|
||||
guild=guild,
|
||||
author=self.bot.user,
|
||||
playlist_api=self.playlist_api,
|
||||
bot=self.bot,
|
||||
)
|
||||
except Exception as err:
|
||||
debug_exc_log(log, err, f"Failed to delete global daily playlist ID: {too_old_id}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_red_audio_queue_end(
|
||||
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
|
||||
):
|
||||
if not (track and guild):
|
||||
return
|
||||
if self.api_interface is not None and self.playlist_api is not None:
|
||||
await self.api_interface.local_cache_api.youtube.clean_up_old_entries()
|
||||
await asyncio.sleep(5)
|
||||
await self.playlist_api.delete_scheduled()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_red_audio_track_end(
|
||||
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
|
||||
):
|
||||
if not (track and guild):
|
||||
return
|
||||
if self.api_interface is not None and self.playlist_api is not None:
|
||||
await self.api_interface.local_cache_api.youtube.clean_up_old_entries()
|
||||
await asyncio.sleep(5)
|
||||
await self.playlist_api.delete_scheduled()
|
||||
@ -1,186 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Final, Pattern
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from aiohttp import ClientConnectorError
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
from ...audio_logging import debug_exc_log
|
||||
from ...errors import TrackEnqueueError
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Events.dpy")
|
||||
|
||||
RE_CONVERSION: Final[Pattern] = re.compile('Converting to "(.*)" failed for parameter "(.*)".')
|
||||
|
||||
|
||||
class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
|
||||
async def cog_before_invoke(self, ctx: commands.Context) -> None:
|
||||
await self.cog_ready_event.wait()
|
||||
# check for unsupported arch
|
||||
# Check on this needs refactoring at a later date
|
||||
# so that we have a better way to handle the tasks
|
||||
if self.command_llsetup in [ctx.command, ctx.command.root_parent]:
|
||||
pass
|
||||
|
||||
elif self.lavalink_connect_task and self.lavalink_connect_task.cancelled():
|
||||
await ctx.send(
|
||||
_(
|
||||
"You have attempted to run Audio's Lavalink server on an unsupported"
|
||||
" architecture. Only settings related commands will be available."
|
||||
)
|
||||
)
|
||||
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)
|
||||
self._daily_global_playlist_cache.setdefault(
|
||||
self.bot.user.id, await self.config.daily_playlists()
|
||||
)
|
||||
if self.local_folder_current_path is None:
|
||||
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()
|
||||
)
|
||||
if dj_enabled:
|
||||
dj_role = self._dj_role_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_role()
|
||||
)
|
||||
dj_role_obj = ctx.guild.get_role(dj_role)
|
||||
if not dj_role_obj:
|
||||
await self.config.guild(ctx.guild).dj_enabled.set(None)
|
||||
self._dj_status_cache[ctx.guild.id] = None
|
||||
await self.config.guild(ctx.guild).dj_role.set(None)
|
||||
self._dj_role_cache[ctx.guild.id] = None
|
||||
await self.send_embed_msg(ctx, title=_("No DJ role found. Disabling DJ mode."))
|
||||
|
||||
async def cog_after_invoke(self, ctx: commands.Context) -> None:
|
||||
await self.maybe_run_pending_db_tasks(ctx)
|
||||
|
||||
async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
||||
error = getattr(error, "original", error)
|
||||
handled = False
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
if error.send_cmd_help:
|
||||
await ctx.send_help()
|
||||
elif isinstance(error, commands.ConversionFailure):
|
||||
handled = True
|
||||
if error.args:
|
||||
if match := RE_CONVERSION.search(error.args[0]):
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Argument"),
|
||||
description=_(
|
||||
"The argument you gave for `{}` is not valid: I was expecting a `{}`."
|
||||
).format(match.group(2), match.group(1)),
|
||||
error=True,
|
||||
)
|
||||
else:
|
||||
await self.send_embed_msg(
|
||||
ctx, title=_("Invalid Argument"), description=error.args[0], error=True,
|
||||
)
|
||||
else:
|
||||
await ctx.send_help()
|
||||
elif isinstance(error, (IndexError, ClientConnectorError)) and any(
|
||||
e in str(error).lower() for e in ["no nodes found.", "cannot connect to host"]
|
||||
):
|
||||
handled = True
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Invalid Environment"),
|
||||
description=_("Connection to Lavalink has been lost."),
|
||||
error=True,
|
||||
)
|
||||
debug_exc_log(log, error, "This is a handled error")
|
||||
elif isinstance(error, KeyError) and "such player for that guild" in str(error):
|
||||
handled = True
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("No Player Available"),
|
||||
description=_("The bot is not connected to a voice channel."),
|
||||
error=True,
|
||||
)
|
||||
debug_exc_log(log, error, "This is a handled error")
|
||||
elif isinstance(error, (TrackEnqueueError, asyncio.exceptions.TimeoutError)):
|
||||
handled = True
|
||||
await self.send_embed_msg(
|
||||
ctx,
|
||||
title=_("Unable to Get Track"),
|
||||
description=_(
|
||||
"I'm unable to get a track from Lavalink at the moment, "
|
||||
"try again in a few minutes."
|
||||
),
|
||||
error=True,
|
||||
)
|
||||
debug_exc_log(log, error, "This is a handled error")
|
||||
if not isinstance(
|
||||
error,
|
||||
(
|
||||
commands.CheckFailure,
|
||||
commands.UserInputError,
|
||||
commands.DisabledCommand,
|
||||
commands.CommandOnCooldown,
|
||||
commands.MaxConcurrencyReached,
|
||||
),
|
||||
):
|
||||
self.update_player_lock(ctx, False)
|
||||
if self.api_interface is not None:
|
||||
await self.api_interface.run_tasks(ctx)
|
||||
if not handled:
|
||||
await self.bot.on_command_error(ctx, error, unhandled_by_cog=True)
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
if not self.cog_cleaned_up:
|
||||
self.bot.dispatch("red_audio_unload", self)
|
||||
self.session.detach()
|
||||
self.bot.loop.create_task(self._close_database())
|
||||
if self.player_automated_timer_task:
|
||||
self.player_automated_timer_task.cancel()
|
||||
|
||||
if self.lavalink_connect_task:
|
||||
self.lavalink_connect_task.cancel()
|
||||
|
||||
if self.cog_init_task:
|
||||
self.cog_init_task.cancel()
|
||||
|
||||
lavalink.unregister_event_listener(self.lavalink_event_handler)
|
||||
self.bot.loop.create_task(lavalink.close())
|
||||
if self.player_manager is not None:
|
||||
self.bot.loop.create_task(self.player_manager.shutdown())
|
||||
|
||||
self.cog_cleaned_up = True
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voice_state_update(
|
||||
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
||||
) -> None:
|
||||
await self.cog_ready_event.wait()
|
||||
if after.channel != before.channel:
|
||||
try:
|
||||
self.skip_votes[before.channel.guild].remove(member.id)
|
||||
except (ValueError, KeyError, AttributeError):
|
||||
pass
|
||||
@ -1,192 +0,0 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from ...errors import DatabaseError
|
||||
from ..abc import MixinMeta
|
||||
from ..cog_utils import CompositeMetaClass, _
|
||||
|
||||
log = logging.getLogger("red.cogs.Audio.cog.Events.lavalink")
|
||||
|
||||
|
||||
class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
|
||||
async def lavalink_event_handler(
|
||||
self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra
|
||||
) -> None:
|
||||
current_track = player.current
|
||||
current_channel = player.channel
|
||||
guild = self.rgetattr(current_channel, "guild", None)
|
||||
guild_id = self.rgetattr(guild, "id", None)
|
||||
current_requester = self.rgetattr(current_track, "requester", None)
|
||||
current_stream = self.rgetattr(current_track, "is_stream", None)
|
||||
current_length = self.rgetattr(current_track, "length", None)
|
||||
current_thumbnail = self.rgetattr(current_track, "thumbnail", None)
|
||||
current_extras = self.rgetattr(current_track, "extras", {})
|
||||
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)
|
||||
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")
|
||||
await self.maybe_reset_error_counter(player)
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_START:
|
||||
self.skip_votes[guild] = []
|
||||
playing_song = player.fetch("playing_song")
|
||||
requester = player.fetch("requester")
|
||||
player.store("prev_song", playing_song)
|
||||
player.store("prev_requester", requester)
|
||||
player.store("playing_song", current_track)
|
||||
player.store("requester", current_requester)
|
||||
self.bot.dispatch("red_audio_track_start", guild, current_track, current_requester)
|
||||
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 (
|
||||
autoplay
|
||||
and not player.queue
|
||||
and player.fetch("playing_song") is not None
|
||||
and self.playlist_api is not None
|
||||
and self.api_interface is not None
|
||||
):
|
||||
try:
|
||||
await self.api_interface.autoplay(player, self.playlist_api)
|
||||
except DatabaseError:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
await self.send_embed_msg(
|
||||
notify_channel, title=_("Couldn't get a valid track.")
|
||||
)
|
||||
return
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_START and notify:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
if player.fetch("notify_message") is not None:
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
await player.fetch("notify_message").delete()
|
||||
|
||||
if (
|
||||
autoplay
|
||||
and current_extras.get("autoplay")
|
||||
and (
|
||||
prev_song is None
|
||||
or (hasattr(prev_song, "extras") and not prev_song.extras.get("autoplay"))
|
||||
)
|
||||
):
|
||||
await self.send_embed_msg(notify_channel, title=_("Auto Play started."))
|
||||
|
||||
if not description:
|
||||
return
|
||||
if current_stream:
|
||||
dur = "LIVE"
|
||||
else:
|
||||
dur = self.format_time(current_length)
|
||||
|
||||
thumb = None
|
||||
if await self.config.guild(guild).thumbnail() and current_thumbnail:
|
||||
thumb = current_thumbnail
|
||||
|
||||
notify_message = await self.send_embed_msg(
|
||||
notify_channel,
|
||||
title=_("Now Playing"),
|
||||
description=description,
|
||||
footer=_("Track length: {length} | Requested by: {user}").format(
|
||||
length=dur, user=current_requester
|
||||
),
|
||||
thumbnail=thumb,
|
||||
)
|
||||
player.store("notify_message", notify_message)
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_START and status:
|
||||
player_check = 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()
|
||||
await self.update_bot_presence(*player_check)
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END:
|
||||
if not autoplay:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel and notify:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
await self.send_embed_msg(notify_channel, title=_("Queue ended."))
|
||||
if disconnect:
|
||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||
await player.disconnect()
|
||||
if status:
|
||||
player_check = self.get_active_player_count()
|
||||
await self.update_bot_presence(*player_check)
|
||||
|
||||
if event_type in [
|
||||
lavalink.LavalinkEvents.TRACK_EXCEPTION,
|
||||
lavalink.LavalinkEvents.TRACK_STUCK,
|
||||
]:
|
||||
message_channel = player.fetch("channel")
|
||||
while True:
|
||||
if current_track in player.queue:
|
||||
player.queue.remove(current_track)
|
||||
else:
|
||||
break
|
||||
if repeat:
|
||||
player.current = None
|
||||
if not guild_id:
|
||||
return
|
||||
self._error_counter.setdefault(guild_id, 0)
|
||||
if guild_id not in self._error_counter:
|
||||
self._error_counter[guild_id] = 0
|
||||
early_exit = await self.increase_error_counter(player)
|
||||
if early_exit:
|
||||
self._disconnected_players[guild_id] = True
|
||||
self.play_lock[guild_id] = False
|
||||
eq = player.fetch("eq")
|
||||
player.queue = []
|
||||
player.store("playing_song", None)
|
||||
if eq:
|
||||
await self.config.custom("EQUALIZER", guild_id).eq_bands.set(eq.bands)
|
||||
await player.stop()
|
||||
await player.disconnect()
|
||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||
if message_channel:
|
||||
message_channel = self.bot.get_channel(message_channel)
|
||||
if early_exit:
|
||||
embed = discord.Embed(
|
||||
colour=await self.bot.get_embed_color(message_channel),
|
||||
title=_("Multiple Errors Detected"),
|
||||
description=_(
|
||||
"Closing the audio player "
|
||||
"due to multiple errors being detected. "
|
||||
"If this persists, please inform the bot owner "
|
||||
"as the Audio cog may be temporally unavailable."
|
||||
),
|
||||
)
|
||||
await message_channel.send(embed=embed)
|
||||
return
|
||||
else:
|
||||
description = description or ""
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_STUCK:
|
||||
embed = discord.Embed(
|
||||
colour=await self.bot.get_embed_color(message_channel),
|
||||
title=_("Track Stuck"),
|
||||
description="{}".format(description),
|
||||
)
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
title=_("Track Error"),
|
||||
colour=await self.bot.get_embed_color(message_channel),
|
||||
description="{}\n{}".format(extra.replace("\n", ""), description),
|
||||
)
|
||||
await message_channel.send(embed=embed)
|
||||
await player.skip()
|
||||
@ -1,102 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-06-18 12:13+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Afrikaans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: af\n"
|
||||
"X-Crowdin-File-ID: 670\n"
|
||||
"Language: af_ZA\n"
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:34
|
||||
msgid "You have attempted to run Audio's Lavalink server on an unsupported architecture. Only settings related commands will be available."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:70
|
||||
msgid "No DJ role found. Disabling DJ mode."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:80
|
||||
msgid "`{user_input}` is not a valid value for `{command}`"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:86
|
||||
msgid "Unable To Parse Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:96
|
||||
#: redbot/cogs/audio/core/events/dpy.py:104
|
||||
msgid "Invalid Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:97
|
||||
msgid "The argument you gave for `{}` is not valid: I was expecting a `{}`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:114
|
||||
msgid "Invalid Environment"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:115
|
||||
msgid "Connection to Lavalink has been lost."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:123
|
||||
msgid "No Player Available"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:124
|
||||
msgid "The bot is not connected to a voice channel."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:132
|
||||
msgid "Unable to Get Track"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:133
|
||||
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:68
|
||||
msgid "Couldn't get a valid track."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:87
|
||||
msgid "Auto Play started."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:102
|
||||
msgid "Now Playing"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:104
|
||||
msgid "Track length: {length} | Requested by: {user}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:125
|
||||
msgid "Queue ended."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:167
|
||||
msgid "Multiple Errors Detected"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:168
|
||||
msgid "Closing the audio player due to multiple errors being detected. If this persists, please inform the bot owner as the Audio cog may be temporally unavailable."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:182
|
||||
msgid "Track Stuck"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:187
|
||||
msgid "Track Error"
|
||||
msgstr ""
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-06-18 12:13+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Arabic\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ar\n"
|
||||
"X-Crowdin-File-ID: 670\n"
|
||||
"Language: ar_SA\n"
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:34
|
||||
msgid "You have attempted to run Audio's Lavalink server on an unsupported architecture. Only settings related commands will be available."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:70
|
||||
msgid "No DJ role found. Disabling DJ mode."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:80
|
||||
msgid "`{user_input}` is not a valid value for `{command}`"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:86
|
||||
msgid "Unable To Parse Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:96
|
||||
#: redbot/cogs/audio/core/events/dpy.py:104
|
||||
msgid "Invalid Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:97
|
||||
msgid "The argument you gave for `{}` is not valid: I was expecting a `{}`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:114
|
||||
msgid "Invalid Environment"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:115
|
||||
msgid "Connection to Lavalink has been lost."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:123
|
||||
msgid "No Player Available"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:124
|
||||
msgid "The bot is not connected to a voice channel."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:132
|
||||
msgid "Unable to Get Track"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:133
|
||||
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:68
|
||||
msgid "Couldn't get a valid track."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:87
|
||||
msgid "Auto Play started."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:102
|
||||
msgid "Now Playing"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:104
|
||||
msgid "Track length: {length} | Requested by: {user}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:125
|
||||
msgid "Queue ended."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:167
|
||||
msgid "Multiple Errors Detected"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:168
|
||||
msgid "Closing the audio player due to multiple errors being detected. If this persists, please inform the bot owner as the Audio cog may be temporally unavailable."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:182
|
||||
msgid "Track Stuck"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:187
|
||||
msgid "Track Error"
|
||||
msgstr ""
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-06-18 12:13+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: bg\n"
|
||||
"X-Crowdin-File-ID: 670\n"
|
||||
"Language: bg_BG\n"
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:34
|
||||
msgid "You have attempted to run Audio's Lavalink server on an unsupported architecture. Only settings related commands will be available."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:70
|
||||
msgid "No DJ role found. Disabling DJ mode."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:80
|
||||
msgid "`{user_input}` is not a valid value for `{command}`"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:86
|
||||
msgid "Unable To Parse Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:96
|
||||
#: redbot/cogs/audio/core/events/dpy.py:104
|
||||
msgid "Invalid Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:97
|
||||
msgid "The argument you gave for `{}` is not valid: I was expecting a `{}`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:114
|
||||
msgid "Invalid Environment"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:115
|
||||
msgid "Connection to Lavalink has been lost."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:123
|
||||
msgid "No Player Available"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:124
|
||||
msgid "The bot is not connected to a voice channel."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:132
|
||||
msgid "Unable to Get Track"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:133
|
||||
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:68
|
||||
msgid "Couldn't get a valid track."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:87
|
||||
msgid "Auto Play started."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:102
|
||||
msgid "Now Playing"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:104
|
||||
msgid "Track length: {length} | Requested by: {user}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:125
|
||||
msgid "Queue ended."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:167
|
||||
msgid "Multiple Errors Detected"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:168
|
||||
msgid "Closing the audio player due to multiple errors being detected. If this persists, please inform the bot owner as the Audio cog may be temporally unavailable."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:182
|
||||
msgid "Track Stuck"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:187
|
||||
msgid "Track Error"
|
||||
msgstr ""
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-06-18 12:13+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: ca\n"
|
||||
"X-Crowdin-File-ID: 670\n"
|
||||
"Language: ca_ES\n"
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:34
|
||||
msgid "You have attempted to run Audio's Lavalink server on an unsupported architecture. Only settings related commands will be available."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:70
|
||||
msgid "No DJ role found. Disabling DJ mode."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:80
|
||||
msgid "`{user_input}` is not a valid value for `{command}`"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:86
|
||||
msgid "Unable To Parse Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:96
|
||||
#: redbot/cogs/audio/core/events/dpy.py:104
|
||||
msgid "Invalid Argument"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:97
|
||||
msgid "The argument you gave for `{}` is not valid: I was expecting a `{}`."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:114
|
||||
msgid "Invalid Environment"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:115
|
||||
msgid "Connection to Lavalink has been lost."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:123
|
||||
msgid "No Player Available"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:124
|
||||
msgid "The bot is not connected to a voice channel."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:132
|
||||
msgid "Unable to Get Track"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/dpy.py:133
|
||||
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:68
|
||||
msgid "Couldn't get a valid track."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:87
|
||||
msgid "Auto Play started."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:102
|
||||
msgid "Now Playing"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:104
|
||||
msgid "Track length: {length} | Requested by: {user}"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:125
|
||||
msgid "Queue ended."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:167
|
||||
msgid "Multiple Errors Detected"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:168
|
||||
msgid "Closing the audio player due to multiple errors being detected. If this persists, please inform the bot owner as the Audio cog may be temporally unavailable."
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:182
|
||||
msgid "Track Stuck"
|
||||
msgstr ""
|
||||
|
||||
#: redbot/cogs/audio/core/events/lavalink.py:187
|
||||
msgid "Track Error"
|
||||
msgstr ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user