Merge pull request #4003 from Drapersniper/finally

Git Yeeeted out of here you monster
This commit is contained in:
aikaterna 2020-06-22 08:12:17 -07:00 committed by GitHub
commit 7e5009345c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
320 changed files with 0 additions and 123952 deletions

View File

@ -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()

View File

@ -1,10 +0,0 @@
from . import (
api_utils,
global_db,
interface,
local_db,
playlist_interface,
playlist_wrapper,
spotify,
youtube,
)

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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`."

View File

@ -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 ""

View File

@ -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."

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 dattente"
#: 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."

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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`를 사용하여 명령어들을 확인하세요.\""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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`."

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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."

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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`獲取說明。"

View File

@ -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)

View File

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

View File

@ -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
]

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
)

View File

@ -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

View File

@ -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."))

View File

@ -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"""

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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