mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 01:12:33 -05:00
Compare commits
40 Commits
V3/feature
...
3.3.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d3e9fceb | ||
|
|
b3281385e9 | ||
|
|
e6947bdbf6 | ||
|
|
a6d924221d | ||
|
|
cf7db1e891 | ||
|
|
bc544d476b | ||
|
|
10976f218b | ||
|
|
a589838a41 | ||
|
|
e36d1f143d | ||
|
|
9a6f78e62e | ||
|
|
649db87a8a | ||
|
|
c6f9a78d57 | ||
|
|
bd89e4386d | ||
|
|
a962f94a58 | ||
|
|
3471011f85 | ||
|
|
186dbe6118 | ||
|
|
a59ff57c27 | ||
|
|
0cd3bede0d | ||
|
|
637fa37fad | ||
|
|
07dcf38291 | ||
|
|
ff1f7362ee | ||
|
|
d30e83b5fc | ||
|
|
14349d0649 | ||
|
|
49b19450fd | ||
|
|
5b612b8ac7 | ||
|
|
e0b922c949 | ||
|
|
60df447550 | ||
|
|
2cf7a1f80d | ||
|
|
4dd0fb97fe | ||
|
|
12bce6a560 | ||
|
|
d869410d36 | ||
|
|
31bb43ca38 | ||
|
|
07e480ff7a | ||
|
|
c251804162 | ||
|
|
ff72e415aa | ||
|
|
7d30e3de14 | ||
|
|
8b529f488b | ||
|
|
b5930155df | ||
|
|
632840384b | ||
|
|
a96e814af4 |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
* text eol=lf
|
||||
|
||||
# binary file excludsions
|
||||
*.png binary
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1,5 +1,4 @@
|
||||
# Core
|
||||
redbot/core/apis/audio/** @aikaterna @Drapersniper
|
||||
redbot/core/bank.py @palmtree5
|
||||
redbot/core/checks.py @tekulvw
|
||||
redbot/core/cli.py @tekulvw
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -700,3 +700,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
This project vendors discord.ext.menus package (https://github.com/Rapptz/discord-ext-menus) made by Danny Y. (Rapptz) which is distributed under MIT License.
|
||||
Copy of this license can be found in discord-ext-menus.LICENSE file in redbot/vendored folder of this repository.
|
||||
|
||||
8
Makefile
8
Makefile
@@ -1,12 +1,14 @@
|
||||
PYTHON ?= python3.8
|
||||
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
|
||||
# Python Code Style
|
||||
reformat:
|
||||
$(PYTHON) -m black `git ls-files "*.py"`
|
||||
$(PYTHON) -m black $(ROOT_DIR)
|
||||
stylecheck:
|
||||
$(PYTHON) -m black --check `git ls-files "*.py"`
|
||||
$(PYTHON) -m black --check $(ROOT_DIR)
|
||||
stylediff:
|
||||
$(PYTHON) -m black --check --diff `git ls-files "*.py"`
|
||||
$(PYTHON) -m black --check --diff $(ROOT_DIR)
|
||||
|
||||
# Translations
|
||||
gettext:
|
||||
|
||||
@@ -126,3 +126,6 @@ Red is named after the main character of "Transistor", a video game by
|
||||
|
||||
Artwork created by [Sinlaire](https://sinlaire.deviantart.com/) on Deviant Art for the Red Discord
|
||||
Bot Project.
|
||||
|
||||
This project vendors [discord.ext.menus](https://github.com/Rapptz/discord-ext-menus) package made by Danny Y. (Rapptz) which is distributed under MIT License.
|
||||
Copy of this license can be found in [discord-ext-menus.LICENSE](redbot/vendored/discord-ext-menus.LICENSE) file in [redbot/vendored](redbot/vendored) folder of this repository.
|
||||
|
||||
@@ -1,5 +1,150 @@
|
||||
.. 3.3.x Changelogs
|
||||
|
||||
Redbot 3.3.11 (2020-08-10)
|
||||
==========================
|
||||
|
||||
| Thanks to all these amazing people that contributed to this release:
|
||||
| :ghuser:`douglas-cpp`, :ghuser:`Drapersniper`, :ghuser:`jack1142`, :ghuser:`MeatyChunks`, :ghuser:`Vexed01`, :ghuser:`yamikaitou`
|
||||
|
||||
End-user changelog
|
||||
------------------
|
||||
|
||||
Audio
|
||||
*****
|
||||
|
||||
- Audio should now work again on all voice regions (:issue:`4162`, :issue:`4168`)
|
||||
- Removed an edge case where an unfriendly error message was sent in Audio cog (:issue:`3879`)
|
||||
|
||||
Cleanup
|
||||
*******
|
||||
|
||||
- Fixed a bug causing ``[p]cleanup`` commands to clear all messages within last 2 weeks when ``0`` is passed as the amount of messages to delete (:issue:`4114`, :issue:`4115`)
|
||||
|
||||
CustomCommands
|
||||
**************
|
||||
|
||||
- ``[p]cc show`` now sends an error message when command with the provided name couldn't be found (:issue:`4108`)
|
||||
|
||||
Downloader
|
||||
**********
|
||||
|
||||
- ``[p]findcog`` no longer fails for 3rd-party cogs without any author (:issue:`4032`, :issue:`4042`)
|
||||
- Update commands no longer crash when a different repo is added under a repo name that was once used (:issue:`4086`)
|
||||
|
||||
Permissions
|
||||
***********
|
||||
|
||||
- ``[p]permissions removeserverrule`` and ``[p]permissions removeglobalrule`` no longer error when trying to remove a rule that doesn't exist (:issue:`4028`, :issue:`4036`)
|
||||
|
||||
Warnings
|
||||
********
|
||||
|
||||
- ``[p]warn`` now sends an error message (instead of no feedback) when an unregistered reason is used by someone who doesn't have Administrator permission (:issue:`3839`, :issue:`3840`)
|
||||
|
||||
|
||||
Redbot 3.3.10 (2020-07-09)
|
||||
===================================
|
||||
|
||||
| Thanks to all these amazing people that contributed to this release:
|
||||
| :ghuser:`aikaterna`, :ghuser:`bobloy`, :ghuser:`Dav-Git`, :ghuser:`Drapersniper`, :ghuser:`Flame442`, :ghuser:`flaree`, :ghuser:`jack1142`, :ghuser:`MiniJennJenn`, :ghuser:`NeuroAssassin`, :ghuser:`thisisjvgrace`, :ghuser:`Vexed01`, :ghuser:`Injabie3`, :ghuser:`mikeshardmind`
|
||||
|
||||
End-user changelog
|
||||
------------------
|
||||
|
||||
Audio
|
||||
*****
|
||||
|
||||
- Added information about internally managed jar to ``[p]audioset info`` (:issue:`3915`)
|
||||
- Updated to Lavaplayer 1.3.50
|
||||
- Twitch playback and YouTube searching should be functioning again.
|
||||
|
||||
Core Bot
|
||||
********
|
||||
|
||||
- Fixed delayed help when ``[p]set deletedelay`` is enabled (:issue:`3884`, :issue:`3883`)
|
||||
- Bumped the Discord.py requirement from 1.3.3 to 1.3.4 (:issue:`4053`)
|
||||
- Added settings view commands for nearly all cogs. (:issue:`4041`)
|
||||
- Added more strings to be fully translatable by i18n. (:issue:`4044`)
|
||||
|
||||
Downloader
|
||||
**********
|
||||
|
||||
- Added ``[p]cog listpinned`` subcommand to see currently pinned cogs (:issue:`3974`)
|
||||
- Fixed unnecessary typing when running downloader commands (:issue:`3964`, :issue:`3948`)
|
||||
- Added embed version of ``[p]findcog`` (:issue:`3965`, :issue:`3944`)
|
||||
- Fixed ``[p]findcog`` not differentiating between core cogs and local cogs(:issue:`3969`, :issue:`3966`)
|
||||
|
||||
Filter
|
||||
******
|
||||
|
||||
- Added ``[p]filter list`` to show filtered words, and removed DMs when no subcommand was passed (:issue:`3973`)
|
||||
|
||||
Image
|
||||
*****
|
||||
|
||||
- Updated instructions for obtaining and setting the GIPHY API key (:issue:`3994`)
|
||||
|
||||
Mod
|
||||
***
|
||||
|
||||
- Added option to delete messages within the passed amount of days with ``[p]tempban`` (:issue:`3958`)
|
||||
- Added the ability to permanently ban a temporary banned user with ``[p]hackban`` (:issue:`4025`)
|
||||
- Fixed the passed reason not being used when using ``[p]tempban`` (:issue:`3958`)
|
||||
- Fixed invite being sent with ``[p]tempban`` even when no invite was set (:issue:`3991`)
|
||||
- Prevented an issue whereby the author may lock him self out of using the bot via whitelists (:issue:`3903`)
|
||||
- Reduced the number of API calls made to the storage APIs (:issue:`3910`)
|
||||
|
||||
Permissions
|
||||
***********
|
||||
|
||||
- Uploaded YAML files now accept integer commands without quotes (:issue:`3987`, :issue:`3185`)
|
||||
- Uploaded YAML files now accept command rules with empty dictionaries (:issue:`3987`, :issue:`3961`)
|
||||
|
||||
Streams
|
||||
*******
|
||||
|
||||
- Fixed streams cog sending multiple owner notifications about twitch secret not set (:issue:`3901`, :issue:`3587`)
|
||||
- Fixed old bearer tokens not being invalidated when the API key is updated (:issue:`3990`, :issue:`3917`)
|
||||
|
||||
Trivia Lists
|
||||
************
|
||||
|
||||
- Fixed URLs in ``whosthatpokemon`` (:issue:`3975`, :issue:`3023`)
|
||||
- Fixed trivia files ``leagueults`` and ``sports`` (:issue:`4026`)
|
||||
- Updated ``greekmyth`` to include more answer variations (:issue:`3970`)
|
||||
- Added new ``lotr`` trivia list (:issue:`3980`)
|
||||
- Added new ``r6seige`` trivia list (:issue:`4026`)
|
||||
|
||||
|
||||
Developer changelog
|
||||
-------------------
|
||||
|
||||
- Added the utility functions ``map``, ``find``, and ``next`` to ``AsyncIter`` (:issue:`3921`, :issue:`3887`)
|
||||
- Updated deprecation times for ``APIToken``, and loops being passed to various functions to the first minor release (represented by ``X`` in ``3.X.0``) after 2020-08-05 (:issue:`3608`)
|
||||
- Updated deprecation warnings for shared libs to reflect that they have been moved for an undefined time (:issue:`3608`)
|
||||
- Added new ``discord.com`` domain to ``INVITE_URL_RE`` common filter (:issue:`4012`)
|
||||
- Fixed incorrect role mention regex in ``MessagePredicate`` (:issue:`4030`)
|
||||
- Vendor the ``discord.ext.menus`` module (:issue:`4039`)
|
||||
|
||||
|
||||
Documentation changes
|
||||
---------------------
|
||||
|
||||
|
||||
|
||||
Miscellaneous
|
||||
-------------
|
||||
|
||||
- Improved error responses for when Modlog and Autoban on mention spam were already disabled (:issue:`3951`, :issue:`3949`)
|
||||
- Clarified that ``[p]embedset user`` only affects commands executed in DMs (:issue:`3972`, :issue:`3953`)
|
||||
- Added link to Getting Started guide if the bot was not in any guilds (:issue:`3906`)
|
||||
- Fixed exceptions being ignored or not sent to log files in special cases (:issue:`3895`)
|
||||
- Added the option of using dots in the instance name when creating your instances (:issue:`3920`)
|
||||
- Added a confirmation when using hyphens in instance names to discourage the use of them (:issue:`3920`)
|
||||
- Fixed migration owner notifications being sent even when migration was not necessary (:issue:`3911`. :issue:`3909`)
|
||||
- Fixed commands being translated where they should not be (:issue:`3938`, :issue:`3919`)
|
||||
- Fixed grammar errors and added full stopts in ``core_commands.py`` (:issue:`4023`)
|
||||
|
||||
Redbot 3.3.9 (2020-06-12)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -33,21 +33,17 @@ Tutorial
|
||||
|
||||
After making your cog, generate a :code:`messages.pot` file
|
||||
|
||||
The process of generating this will depend on the operating system
|
||||
you are using
|
||||
We recommend using redgettext - a modified version of pygettext for Red.
|
||||
You can install redgettext by running :code:`pip install redgettext` in a command prompt.
|
||||
|
||||
In a command prompt in your cog's package (where yourcog.py is),
|
||||
create a directory called "locales".
|
||||
Then do one of the following:
|
||||
|
||||
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -D -n -p locales`
|
||||
|
||||
Mac: ?
|
||||
|
||||
Linux: :code:`pygettext3 -D -n -p locales`
|
||||
|
||||
This will generate a messages.pot file with strings to be translated, including
|
||||
To generate the :code:`messages.pot` file, you will now need to run
|
||||
:code:`python -m redgettext -c [path_to_cog]`
|
||||
This file will contain all strings to be translated, including
|
||||
docstrings.
|
||||
(For advanced usage check :code:`python -m redgettext -h`)
|
||||
|
||||
You can now use a tool like `poedit
|
||||
<https://poedit.net/>`_ to translate the strings in your messages.pot file.
|
||||
|
||||
-------------
|
||||
API Reference
|
||||
|
||||
@@ -397,7 +397,7 @@ Then run the following command:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.8.3 -v
|
||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.8.5 -v
|
||||
|
||||
This may take a long time to complete, depending on your hardware. For some machines (such as
|
||||
Raspberry Pis and micro-tier VPSes), it may take over an hour; in this case, you may wish to remove
|
||||
@@ -409,7 +409,7 @@ After that is finished, run:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
pyenv global 3.8.3
|
||||
pyenv global 3.8.5
|
||||
|
||||
Pyenv is now installed and your system should be configured to run Python 3.8.
|
||||
|
||||
|
||||
@@ -29,13 +29,14 @@ Using PowerShell and Chocolatey (recommended)
|
||||
*********************************************
|
||||
|
||||
To install via PowerShell, search "powershell" in the Windows start menu,
|
||||
right-click on it and then click "Run as administrator"
|
||||
right-click on it and then click "Run as administrator".
|
||||
|
||||
Then run each of the following commands:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
choco upgrade git --params "/GitOnlyOnPath /WindowsTerminal" -y
|
||||
choco upgrade visualstudio2019-workload-vctools -y
|
||||
@@ -63,7 +64,7 @@ Manually installing dependencies
|
||||
|
||||
* `MSVC Build tools <https://www.visualstudio.com/downloads/#build-tools-for-visual-studio-2019>`_
|
||||
|
||||
* `Python 3.8.1 <https://www.python.org/downloads/>`_ - Red needs Python 3.8.1 or greater
|
||||
* `Python 3.8.1 or greater <https://www.python.org/downloads/>`_
|
||||
|
||||
.. attention:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
||||
you may run into issues when trying to run Red.
|
||||
@@ -86,7 +87,7 @@ Creating a Virtual Environment
|
||||
|
||||
.. tip::
|
||||
|
||||
If you want to learn more about virtual environments, see page: `about-venvs`
|
||||
If you want to learn more about virtual environments, see page: `about-venvs`.
|
||||
|
||||
We require installing Red into a virtual environment. Don't be scared, it's very
|
||||
straightforward.
|
||||
@@ -95,7 +96,12 @@ First, choose a directory where you would like to create your virtual environmen
|
||||
to keep it in a location which is easy to type out the path to. From now, we'll call it
|
||||
``redenv`` and it will be located in your home directory.
|
||||
|
||||
Start with opening a command prompt (open Start, search for "command prompt", then click it)
|
||||
Start with opening a command prompt (open Start, search for "command prompt", then click it).
|
||||
|
||||
.. note::
|
||||
|
||||
You shouldn't run command prompt as administrator when creating your virtual environment, or
|
||||
running Red.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -144,11 +150,6 @@ Run **one** of the following set of commands, depending on what extras you want
|
||||
python -m pip install -U pip setuptools wheel
|
||||
python -m pip install -U Red-DiscordBot[postgres]
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These commands are also used for updating Red
|
||||
|
||||
--------------------------
|
||||
Setting Up and Running Red
|
||||
--------------------------
|
||||
|
||||
@@ -17,7 +17,7 @@ Guarantees
|
||||
Anything in the ``redbot.core`` module or any of its submodules
|
||||
which is not private (even if not documented) should not break without notice.
|
||||
|
||||
Anything in the ``redbot.cogs`` module or any of it's submodules is specifically
|
||||
Anything in the ``redbot.cogs`` and ``redbot.vendored`` modules or any of their submodules is specifically
|
||||
excluded from being guaranteed.
|
||||
|
||||
Any RPC method exposed by Red may break without notice.
|
||||
|
||||
12
make.bat
12
make.bat
@@ -5,24 +5,18 @@ if [%1] == [] goto help
|
||||
REM This allows us to expand variables at execution
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
|
||||
REM This will set PYFILES as a list of tracked .py files
|
||||
set PYFILES=
|
||||
for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
|
||||
set PYFILES=!PYFILES! %%A
|
||||
)
|
||||
|
||||
goto %1
|
||||
|
||||
:reformat
|
||||
black !PYFILES!
|
||||
black "%~dp0."
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:stylecheck
|
||||
black --check !PYFILES!
|
||||
black --check "%~dp0."
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:stylediff
|
||||
black --check --diff !PYFILES!
|
||||
black --check --diff "%~dp0."
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:newenv
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
| redbot\/vendored
|
||||
)/
|
||||
'''
|
||||
|
||||
@@ -191,7 +191,7 @@ def _update_event_loop_policy():
|
||||
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
||||
|
||||
|
||||
__version__ = "3.3.10.dev1"
|
||||
__version__ = "3.3.11"
|
||||
version_info = VersionInfo.from_str(__version__)
|
||||
|
||||
# Filter fuzzywuzzy slow sequence matcher warning
|
||||
|
||||
@@ -21,7 +21,7 @@ msgstr "Я попыталась сделать что-то, в чем Discord о
|
||||
|
||||
#: redbot/cogs/admin/admin.py:22
|
||||
msgid "I can not give {role.name} to {member.display_name} because that role is higher than or equal to my highest role in the Discord hierarchy."
|
||||
msgstr "Я не могу дать {role.name} для {member.display_name}, потому что эта роль выше или равна моей самой высокой роли в иерархии Discord."
|
||||
msgstr "Невозможно выдать роль {role.name} для {member.display_name}, потому что эта роль выше или равна моей самой высокой роли в иерархии Discord."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:28
|
||||
msgid "I can not remove {role.name} from {member.display_name} because that role is higher than or equal to my highest role in the Discord hierarchy."
|
||||
@@ -172,7 +172,7 @@ msgstr "\\n Список всех доступных Собственны
|
||||
|
||||
#: redbot/cogs/admin/admin.py:407
|
||||
msgid "Available Selfroles:\\n{selfroles}"
|
||||
msgstr "Доступные Собственные роли:\\n{selfroles}"
|
||||
msgstr "Доступные собственные роли:\\n{selfroles}"
|
||||
|
||||
#: redbot/cogs/admin/admin.py:413
|
||||
#, docstring
|
||||
@@ -186,7 +186,7 @@ msgstr "\\n Добавить роль в список доступных
|
||||
|
||||
#: redbot/cogs/admin/admin.py:425
|
||||
msgid "I cannot let you add {role.name} as a selfrole because that role is higher than or equal to your highest role in the Discord hierarchy."
|
||||
msgstr "Я не могу позволить вам добавить {role.name} в качестве собственной роли, потому что эта роль выше или равна вашей самой высокой роли в иерархии Discord."
|
||||
msgstr "Нельзя добавить {role.name} в качестве собственной роли, потому что эта роль выше или равна вашей самой высшей роли в иерархии Discord."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:433
|
||||
msgid "Added."
|
||||
@@ -194,7 +194,7 @@ msgstr "Добавлено."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:436
|
||||
msgid "That role is already a selfrole."
|
||||
msgstr "Эта роль уже является собственной ролью."
|
||||
msgstr "У вас уже имеется эта роль."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:440
|
||||
#, docstring
|
||||
@@ -203,7 +203,7 @@ msgstr "\\n Удалить роль из списка доступных
|
||||
|
||||
#: redbot/cogs/admin/admin.py:447
|
||||
msgid "I cannot let you remove {role.name} from being a selfrole because that role is higher than or equal to your highest role in the Discord hierarchy."
|
||||
msgstr "Я не могу позволить вам сделать {role.name} не собственной ролью, потому что эта роль выше или равна вашей самой высокой роли в иерархии Discord."
|
||||
msgstr "Вы не можете забрать у себя роль {role.name}, потому что эта роль выше или равна вашей самой высокой роли в иерархии Discord."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:455
|
||||
msgid "Removed."
|
||||
|
||||
@@ -21,19 +21,19 @@ msgstr "Bir şey denemeye çalıştım ancak Discord izinlerim yeterli olmadı.
|
||||
|
||||
#: redbot/cogs/admin/admin.py:22
|
||||
msgid "I can not give {role.name} to {member.display_name} because that role is higher than or equal to my highest role in the Discord hierarchy."
|
||||
msgstr "{member.display_name} kullanıcısına {role.name} veremiyorum çünkü bu rol kendi rolümden daha yüksek bir pozisyonda."
|
||||
msgstr "{member.display_name} kullanıcısına {role.name} veremiyorum çünkü bu rol Discord hiyerarşisinde rolüme eşit veya daha yüksek."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:28
|
||||
msgid "I can not remove {role.name} from {member.display_name} because that role is higher than or equal to my highest role in the Discord hierarchy."
|
||||
msgstr "{member.display_name} kullanıcısından {role.name} rolünü kaldıramıyorum çünkü bu rolümden daha yüksek pozisyonda."
|
||||
msgstr "{member.display_name} kullanıcısından {role.name} rolünü kaldıramıyorum çünkü bu rol Discord hiyerarşisinde rolüme eşit veya daha yüksek."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:34
|
||||
msgid "I can not edit {role.name} because that role is higher than my or equal to highest role in the Discord hierarchy."
|
||||
msgstr "{role.name} kullanıcısının rolünü düzenleyemiyorum, çünkü bu rolümden daha yüksek pozisyonda."
|
||||
msgstr "{role.name} kullanıcısının rolünü düzenleyemiyorum, çünkü bu rol Discord hiyerarşisinde rolüme eşit veya daha yüksek."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:40
|
||||
msgid "I can not let you give {role.name} to {member.display_name} because that role is higher than or equal to your highest role in the Discord hierarchy."
|
||||
msgstr "{member.display_name} kullanıcısına {role.name} rolünü vermene müsaade edemem çünkü bu rol senin mevcut rolünden daha yüksek durumda."
|
||||
msgstr "{member.display_name} kullanıcısına {role.name} rolünü verilemedi çünkü bu rol discord hiyerarşisinde senin mevcut rolünden daha yüksek durumda."
|
||||
|
||||
#: redbot/cogs/admin/admin.py:46
|
||||
msgid "I can not let you remove {role.name} from {member.display_name} because that role is higher than or equal to your highest role in the Discord hierarchy."
|
||||
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
#: redbot/cogs/alias/alias.py:31
|
||||
#, docstring
|
||||
msgid "Create aliases for commands.\\n\\n Aliases are alternative names shortcuts for commands. They\\n can act as both a lambda (storing arguments for repeated use)\\n or as simply a shortcut to saying \\\"x y z\\\".\\n\\n When run, aliases will accept any additional arguments\\n and append them to the stored alias.\\n "
|
||||
msgstr "Создать псевдонимы для команд.\\n\\n Псевдонимы - это альтернативные сокращения имен для команд.\\n Они могут действовать как лямбда (хранение аргументов для\\n многократного использования) или просто как сокращение \\\"а б в\\\".\\n\\n При запуске псевдонимы принимают любые дополнительные\\n аргументы и добавляют их к сохраненному псевдониму.\\n "
|
||||
msgstr "Создать синонимы для команд.\\n\\n Синонимы - это альтернативные сокращения имен для команд.\\n Они могут действовать как лямбда (хранение аргументов для\\n многократного использования) или просто как сокращение \\\"а б в\\\".\\n\\n При запуске синонимы принимают любые дополнительные\\n аргументы и добавляют их к сохраненному синониму.\\n "
|
||||
|
||||
#: redbot/cogs/alias/alias.py:86
|
||||
msgid "No prefix found."
|
||||
@@ -26,7 +26,7 @@ msgstr "Префикс не найден."
|
||||
|
||||
#: redbot/cogs/alias/alias.py:116
|
||||
msgid "Aliases:\\n"
|
||||
msgstr ""
|
||||
msgstr "Синонимы:\\n"
|
||||
|
||||
#: redbot/cogs/alias/alias.py:118
|
||||
msgid "\\n\\nPage {page}/{total}"
|
||||
@@ -53,7 +53,7 @@ msgstr "Вы попытались создать новый псевдоним {
|
||||
|
||||
#: redbot/cogs/alias/alias.py:156
|
||||
msgid "You attempted to create a new alias with the name {name} but that alias already exists."
|
||||
msgstr ""
|
||||
msgstr "Синоним {name} уже существует."
|
||||
|
||||
#: redbot/cogs/alias/alias.py:167
|
||||
msgid "You attempted to create a new alias with the name {name} but that name is an invalid alias name. Alias names may not contain spaces."
|
||||
@@ -61,7 +61,7 @@ msgstr "Вы попытались создать новый псевдоним {
|
||||
|
||||
#: redbot/cogs/alias/alias.py:179 redbot/cogs/alias/alias.py:238
|
||||
msgid "You attempted to create a new alias for a command that doesn't exist."
|
||||
msgstr "Вы попытались создать новый псевдоним для не существующей команды."
|
||||
msgstr "Вы попытались создать новый синоним для не существующей команды."
|
||||
|
||||
#: redbot/cogs/alias/alias.py:193
|
||||
msgid "A new alias with the trigger `{name}` has been created."
|
||||
@@ -78,7 +78,7 @@ msgstr "Вы попытались создать новый глобальный
|
||||
|
||||
#: redbot/cogs/alias/alias.py:215
|
||||
msgid "You attempted to create a new global alias with the name {name} but that alias already exists."
|
||||
msgstr ""
|
||||
msgstr "Общий синоним {name} уже существует."
|
||||
|
||||
#: redbot/cogs/alias/alias.py:226
|
||||
msgid "You attempted to create a new global alias with the name {name} but that name is an invalid alias name. Alias names may not contain spaces."
|
||||
@@ -134,7 +134,7 @@ msgstr "Удалить существующий глобальный псевд
|
||||
|
||||
#: redbot/cogs/alias/alias.py:298
|
||||
msgid "There are no global aliases on this bot."
|
||||
msgstr "У этого бота нет глобальных псевдонимов."
|
||||
msgstr "У этого бота нет всеобщих синонимов."
|
||||
|
||||
#: redbot/cogs/alias/alias.py:312
|
||||
#, docstring
|
||||
@@ -148,7 +148,7 @@ msgstr "Список доступных глобальных псевдоним
|
||||
|
||||
#: redbot/cogs/alias/alias.py:324
|
||||
msgid "There are no global aliases."
|
||||
msgstr "Нет глобальных псевдонимов."
|
||||
msgstr "Нет всеобщих синонимов."
|
||||
|
||||
#: redbot/cogs/alias/alias_entry.py:174
|
||||
msgid "Arguments must be specified with a number."
|
||||
|
||||
9
redbot/cogs/audio/__init__.py
Normal file
9
redbot/cogs/audio/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
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()
|
||||
10
redbot/cogs/audio/apis/__init__.py
Normal file
10
redbot/cogs/audio/apis/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from . import (
|
||||
api_utils,
|
||||
global_db,
|
||||
interface,
|
||||
local_db,
|
||||
playlist_interface,
|
||||
playlist_wrapper,
|
||||
spotify,
|
||||
youtube,
|
||||
)
|
||||
140
redbot/cogs/audio/apis/api_utils.py
Normal file
140
redbot/cogs/audio/apis/api_utils.py
Normal file
@@ -0,0 +1,140 @@
|
||||
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")
|
||||
42
redbot/cogs/audio/apis/global_db.py
Normal file
42
redbot/cogs/audio/apis/global_db.py
Normal file
@@ -0,0 +1,42 @@
|
||||
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
|
||||
894
redbot/cogs/audio/apis/interface.py
Normal file
894
redbot/cogs/audio/apis/interface.py
Normal file
@@ -0,0 +1,894 @@
|
||||
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()
|
||||
372
redbot/cogs/audio/apis/local_db.py
Normal file
372
redbot/cogs/audio/apis/local_db.py
Normal file
@@ -0,0 +1,372 @@
|
||||
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)
|
||||
65
redbot/cogs/audio/apis/locales/af-ZA.po
Normal file
65
redbot/cogs/audio/apis/locales/af-ZA.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/ar-SA.po
Normal file
65
redbot/cogs/audio/apis/locales/ar-SA.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/bg-BG.po
Normal file
65
redbot/cogs/audio/apis/locales/bg-BG.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/ca-ES.po
Normal file
65
redbot/cogs/audio/apis/locales/ca-ES.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/cs-CZ.po
Normal file
65
redbot/cogs/audio/apis/locales/cs-CZ.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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`."
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/da-DK.po
Normal file
65
redbot/cogs/audio/apis/locales/da-DK.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/de-DE.po
Normal file
65
redbot/cogs/audio/apis/locales/de-DE.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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."
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/el-GR.po
Normal file
65
redbot/cogs/audio/apis/locales/el-GR.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/es-ES.po
Normal file
65
redbot/cogs/audio/apis/locales/es-ES.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/fi-FI.po
Normal file
65
redbot/cogs/audio/apis/locales/fi-FI.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/fr-FR.po
Normal file
65
redbot/cogs/audio/apis/locales/fr-FR.po
Normal file
@@ -0,0 +1,65 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: red-discordbot\n"
|
||||
"POT-Creation-Date: 2020-05-21 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: redgettext 3.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Crowdin-Project: red-discordbot\n"
|
||||
"X-Crowdin-Project-ID: 289505\n"
|
||||
"X-Crowdin-Language: fr\n"
|
||||
"X-Crowdin-File-ID: 698\n"
|
||||
"Language: fr_FR\n"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:280
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "La clé API de Spotify ou le secret client n'ont pas étés correctement définis. \\nUtilisez `{prefix}audioset spotifyapi` pour connaître la marche à suivre."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:322
|
||||
msgid "This doesn't seem to be a valid Spotify playlist/album URL or code."
|
||||
msgstr "Cela ne semble pas être une URL ou un album/playlist Spotify valide."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:443
|
||||
msgid "This doesn't seem to be a supported Spotify URL or code."
|
||||
msgstr "Cela ne semble pas être une URL ou un code Spotify pris en charge."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:509
|
||||
msgid "The connection was reset while loading the playlist."
|
||||
msgstr "La connexion a été réinitialisée lors du chargement de la playlist."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:518
|
||||
msgid "Player timeout, skipping remaining tracks."
|
||||
msgstr "Arrêt du lecteur, pistes restantes ignoré."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:542
|
||||
msgid "Failing to get tracks, skipping remaining."
|
||||
msgstr "Impossible d'obtenir les pistes, pistes ignoré."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:592
|
||||
msgid "Nothing found.\\nThe YouTube API key may be invalid or you may be rate limited on YouTube's search service.\\nCheck the YouTube API key again and follow the instructions at `{prefix}audioset youtubeapi`."
|
||||
msgstr "Rien n'a été trouvé.\\nLa clé de l'API YouTube peut être invalide ou vous pouvez être limité sur le service de recherche de YouTube.\\nVérifiez à nouveau la clé de l'API YouTube et suivez les instructions à `{prefix}audioset youtubeapi`."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:602
|
||||
msgid " {bad_tracks} tracks cannot be queued."
|
||||
msgstr " {bad_tracks} pistes ne peuvent pas être mises en attente."
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:610
|
||||
msgid "Playlist Enqueued"
|
||||
msgstr "Playlist en file d’attente"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:611
|
||||
msgid "Added {num} tracks to the queue.{maxlength_msg}"
|
||||
msgstr "Ajout de {num} pistes à la file d'attente.{maxlength_msg}"
|
||||
|
||||
#: redbot/cogs/audio/apis/interface.py:617
|
||||
msgid "{time} until start of playlist playback: starts at #{position} in queue"
|
||||
msgstr "{time} avant le début de la lecture de la playlist : commence à #{position} dans la liste"
|
||||
|
||||
#: redbot/cogs/audio/apis/spotify.py:165
|
||||
msgid "The Spotify API key or client secret has not been set properly. \\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||
msgstr "La clé API de Spotify ou le secret client n'ont pas étés correctement définis. \\nUtilisez `{prefix}audioset spotifyapi` pour connaître la marche à suivre."
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/he-IL.po
Normal file
65
redbot/cogs/audio/apis/locales/he-IL.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/hu-HU.po
Normal file
65
redbot/cogs/audio/apis/locales/hu-HU.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/id-ID.po
Normal file
65
redbot/cogs/audio/apis/locales/id-ID.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/it-IT.po
Normal file
65
redbot/cogs/audio/apis/locales/it-IT.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/ja-JP.po
Normal file
65
redbot/cogs/audio/apis/locales/ja-JP.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/ko-KR.po
Normal file
65
redbot/cogs/audio/apis/locales/ko-KR.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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`를 사용하여 명령어들을 확인하세요.\""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/nl-NL.po
Normal file
65
redbot/cogs/audio/apis/locales/nl-NL.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/no-NO.po
Normal file
65
redbot/cogs/audio/apis/locales/no-NO.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/pl-PL.po
Normal file
65
redbot/cogs/audio/apis/locales/pl-PL.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/pt-BR.po
Normal file
65
redbot/cogs/audio/apis/locales/pt-BR.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/pt-PT.po
Normal file
65
redbot/cogs/audio/apis/locales/pt-PT.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/ro-RO.po
Normal file
65
redbot/cogs/audio/apis/locales/ro-RO.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/ru-RU.po
Normal file
65
redbot/cogs/audio/apis/locales/ru-RU.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 "Не является правильным 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 может быть недействительным или вы можете оценить его в поисковой службе YouTube.\\nПроверьте YouTube 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 "API ключ Spotify или секрет клиента были установлены неправильно. \\nДля получения инструкций используйте `{prefix}audioset spotifyapi`."
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/sk-SK.po
Normal file
65
redbot/cogs/audio/apis/locales/sk-SK.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/sr-SP.po
Normal file
65
redbot/cogs/audio/apis/locales/sr-SP.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/sv-SE.po
Normal file
65
redbot/cogs/audio/apis/locales/sv-SE.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/tr-TR.po
Normal file
65
redbot/cogs/audio/apis/locales/tr-TR.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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."
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/uk-UA.po
Normal file
65
redbot/cogs/audio/apis/locales/uk-UA.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/vi-VN.po
Normal file
65
redbot/cogs/audio/apis/locales/vi-VN.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/zh-CN.po
Normal file
65
redbot/cogs/audio/apis/locales/zh-CN.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/zh-HK.po
Normal file
65
redbot/cogs/audio/apis/locales/zh-HK.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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 ""
|
||||
|
||||
65
redbot/cogs/audio/apis/locales/zh-TW.po
Normal file
65
redbot/cogs/audio/apis/locales/zh-TW.po
Normal file
@@ -0,0 +1,65 @@
|
||||
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`獲取說明。"
|
||||
|
||||
647
redbot/cogs/audio/apis/playlist_interface.py
Normal file
647
redbot/cogs/audio/apis/playlist_interface.py
Normal file
@@ -0,0 +1,647 @@
|
||||
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)
|
||||
249
redbot/cogs/audio/apis/playlist_wrapper.py
Normal file
249
redbot/cogs/audio/apis/playlist_wrapper.py
Normal file
@@ -0,0 +1,249 @@
|
||||
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),
|
||||
},
|
||||
)
|
||||
189
redbot/cogs/audio/apis/spotify.py
Normal file
189
redbot/cogs/audio/apis/spotify.py
Normal file
@@ -0,0 +1,189 @@
|
||||
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
|
||||
]
|
||||
65
redbot/cogs/audio/apis/youtube.py
Normal file
65
redbot/cogs/audio/apis/youtube.py
Normal file
@@ -0,0 +1,65 @@
|
||||
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
|
||||
683
redbot/cogs/audio/audio_dataclasses.py
Normal file
683
redbot/cogs/audio/audio_dataclasses.py
Normal file
@@ -0,0 +1,683 @@
|
||||
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()
|
||||
17
redbot/cogs/audio/audio_logging.py
Normal file
17
redbot/cogs/audio/audio_logging.py
Normal file
@@ -0,0 +1,17 @@
|
||||
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)
|
||||
541
redbot/cogs/audio/converters.py
Normal file
541
redbot/cogs/audio/converters.py
Normal file
@@ -0,0 +1,541 @@
|
||||
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
|
||||
121
redbot/cogs/audio/core/__init__.py
Normal file
121
redbot/cogs/audio/core/__init__.py
Normal file
@@ -0,0 +1,121 @@
|
||||
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)
|
||||
504
redbot/cogs/audio/core/abc.py
Normal file
504
redbot/cogs/audio/core/abc.py
Normal file
@@ -0,0 +1,504 @@
|
||||
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()
|
||||
28
redbot/cogs/audio/core/cog_utils.py
Normal file
28
redbot/cogs/audio/core/cog_utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
25
redbot/cogs/audio/core/commands/__init__.py
Normal file
25
redbot/cogs/audio/core/commands/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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"""
|
||||
1317
redbot/cogs/audio/core/commands/audioset.py
Normal file
1317
redbot/cogs/audio/core/commands/audioset.py
Normal file
File diff suppressed because it is too large
Load Diff
841
redbot/cogs/audio/core/commands/controller.py
Normal file
841
redbot/cogs/audio/core/commands/controller.py
Normal file
@@ -0,0 +1,841 @@
|
||||
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
|
||||
)
|
||||
385
redbot/cogs/audio/core/commands/equalizer.py
Normal file
385
redbot/cogs/audio/core/commands/equalizer.py
Normal file
@@ -0,0 +1,385 @@
|
||||
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)
|
||||
168
redbot/cogs/audio/core/commands/llset.py
Normal file
168
redbot/cogs/audio/core/commands/llset.py
Normal file
@@ -0,0 +1,168 @@
|
||||
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
|
||||
),
|
||||
)
|
||||
2421
redbot/cogs/audio/core/commands/locales/af-ZA.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/af-ZA.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/ar-SA.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/ar-SA.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/bg-BG.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/bg-BG.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/ca-ES.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/ca-ES.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/cs-CZ.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/cs-CZ.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/da-DK.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/da-DK.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/de-DE.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/de-DE.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/el-GR.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/el-GR.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/es-ES.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/es-ES.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/fi-FI.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/fi-FI.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/fr-FR.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/fr-FR.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/he-IL.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/he-IL.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/hu-HU.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/hu-HU.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/id-ID.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/id-ID.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/it-IT.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/it-IT.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/ja-JP.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/ja-JP.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/ko-KR.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/ko-KR.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/nl-NL.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/nl-NL.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/no-NO.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/no-NO.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/pl-PL.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/pl-PL.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/pt-BR.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/pt-BR.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/pt-PT.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/pt-PT.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/ro-RO.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/ro-RO.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/ru-RU.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/ru-RU.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/sk-SK.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/sk-SK.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/sr-SP.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/sr-SP.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/sv-SE.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/sv-SE.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/tr-TR.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/tr-TR.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/uk-UA.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/uk-UA.po
Normal file
File diff suppressed because it is too large
Load Diff
2421
redbot/cogs/audio/core/commands/locales/vi-VN.po
Normal file
2421
redbot/cogs/audio/core/commands/locales/vi-VN.po
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user