Ready for public use
This commit is contained in:
11
.env Example
Normal file
11
.env Example
Normal file
@@ -0,0 +1,11 @@
|
||||
TOKEN = YOUR_BOT_TOKEN
|
||||
CLIENT_ID = YOUR_BOT_CLIENT_ID
|
||||
BUG_REPORT_CHANNEL_ID = YOUR_DISCORD_CHANNEL_ID
|
||||
|
||||
SPOTIFY_CLIENT_ID = YOUR_SPOTIFY_CLIENT_ID
|
||||
SPOTIFY_CLIENT_SECRET = YOUR_SPOTIFY_CLIENT_SECRET
|
||||
|
||||
YOUTUBE_API_KEY = YOUR_YOUTUBE_API_KEY
|
||||
|
||||
MONGODB_URL = YOUR_MONGODB_URL
|
||||
MONGODB_NAME = YOUR_MONGODB_DB_NAME
|
||||
154
.gitignore
vendored
154
.gitignore
vendored
@@ -1,152 +1,4 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
Vocard.rar
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
*.pyc
|
||||
settings.json
|
||||
83
README.md
Normal file
83
README.md
Normal file
@@ -0,0 +1,83 @@
|
||||

|
||||
|
||||
# Vocard (Discord Music Bot)
|
||||
> Vocard is a simple custom Disocrd Music Bot built with Python & [discord.py](https://discordpy.readthedocs.io/en/stable/)
|
||||
|
||||
## Requirements
|
||||
* [Python 3.8+](https://www.python.org/downloads/)
|
||||
* [Modules in requirements](https://github.com/ChocoMeow/Vocard/blob/main/requirements.txt)
|
||||
* [Lavalink Server](https://github.com/freyacodes/Lavalink)
|
||||
|
||||
## Quick Start
|
||||
```sh
|
||||
git clone https://github.com/ChocoMeow/Vocard.git #Clone the repository
|
||||
cd Vocard #Go to the directory
|
||||
python -m pip install -r requirements.txt #Install required packages
|
||||
```
|
||||
After installing all packages, you must configure the bot before to start! [How To Configure](https://github.com/ChocoMeow/Vocard#configuration)<br />
|
||||
Start your bot with `python main.py`
|
||||
|
||||
## Configuration
|
||||
1. **Rename `.env Example` to `.env` and fill all the values**
|
||||
```sh
|
||||
TOKEN = XXXXXXXXXXXXXXXXXXXXXXXX.XXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
CLIENT_ID = 123456789012345678
|
||||
BUG_REPORT_CHANNEL_ID = 123456789012345678
|
||||
|
||||
SPOTIFY_CLIENT_ID = 0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
SPOTIFY_CLIENT_SECRET = 0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
YOUTUBE_API_KEY = AXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
MONGODB_URL = mongodb+srv://user:password@clusterURL
|
||||
MONGODB_NAME = Vocard
|
||||
```
|
||||
| Values | Description |
|
||||
| --- | --- |
|
||||
| TOKEN | Your Discord bot token [(Discord Portal)](https://discord.com/developers/applications) |
|
||||
| CLIENT_ID | Your Discord bot client id [(Discord Portal)](https://discord.com/developers/applications) |
|
||||
| BUG_REPORT_CHANNEL_ID | All the error messages will send to this text channel ***(optional)*** |
|
||||
| SPOTIFY_CLIENT_ID | Your Spoity client id [(Spotify Portal)](https://developer.spotify.com/dashboard/applications) ***(optional)*** |
|
||||
| SPOTIFY_CLIENT_SECRET | Your Spoity client sercret id [(Spotify Portal)](https://developer.spotify.com/dashboard/applications) ***(optional)*** |
|
||||
| YOUTUBE_API_KEY | Your youtube api key [(Google API)](https://cloud.google.com/apis) ***(optional)*** |
|
||||
| MONGODB_URL | Your Mongo datebase url [(Mongodb)](https://www.mongodb.com/) |
|
||||
| MONGODB_NAME | The datebase name that you created on [Mongodb](https://www.mongodb.com/) |
|
||||
|
||||
2. **Rename `settings Example.json` to `settings.json` and customize your settings**
|
||||
***(Note: Do not change any keys from `settings.json`)***
|
||||
```json
|
||||
{
|
||||
"nodes": {
|
||||
"DEFAULT": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 2333,
|
||||
"password": "password",
|
||||
"secure": false,
|
||||
"identifier": "DEFAULT"
|
||||
}
|
||||
},
|
||||
"bot_access_user": [],
|
||||
"color_code":"0xb3b3b3",
|
||||
"emoji_source_raw": {
|
||||
"youtube": "<:youtube:826661982760992778>",
|
||||
"youtube music": "<:youtube:826661982760992778>",
|
||||
"spotify": "<:spotify:826661996615172146>",
|
||||
"soundcloud": "<:soundcloud:852729280027033632>",
|
||||
"twitch": "<:twitch:852729278285086741>",
|
||||
"bandcamp": "<:bandcamp:864694003811221526>",
|
||||
"vimeo": "<:vimeo:864694001919721473>",
|
||||
"apple": "<:applemusic:994844332374884413>",
|
||||
"reddit": "<:reddit:996007566863773717>",
|
||||
"tiktok": "<:tiktok:996007689798811698>"
|
||||
}
|
||||
}
|
||||
```
|
||||
* For `nodes` you have to provide host, port, password and identifier of the [Lavalink Server](https://github.com/freyacodes/Lavalink)
|
||||
* For `bot_access_user` you can pass the [discord user id](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-). Example: `[123456789012345678]`
|
||||
* For `color_code` you must pass a [Hexadecimal color code](https://htmlcolorcodes.com/) and add `0x` before the color code. Example: `"0xb3b3b3"`
|
||||
* For `emoji_source_raw` you can change the source emoji of the track with discord emoji like `<:EMOJI_NAME:EMOJI_ID>`
|
||||
|
||||
## How to update?
|
||||
1. Run `python update --check` to check if your bot is up to date
|
||||
2. Run `python update --start` to start update your bot <br/>
|
||||
***Note: Make sure there are no personal files in the directory! Otherwise it will be deleted.***
|
||||
1
addons/__init__.py
Normal file
1
addons/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .lyrics import getLyrics
|
||||
161
addons/lyrics.py
Normal file
161
addons/lyrics.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import aiohttp, random, bs4, re
|
||||
from urllib.parse import quote
|
||||
from math import floor
|
||||
|
||||
userAgents = '''Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.11 Safari/535.19
|
||||
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11
|
||||
Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.3 Safari/532.2
|
||||
Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.0 Safari/532.2
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.220.1 Safari/532.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.6 Safari/532.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.4 Safari/532.1
|
||||
Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1
|
||||
Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.0 Safari/532.0
|
||||
Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.0 Safari/532.0
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.23 Safari/530.5
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.2 Safari/530.5
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.2 Safari/530.5
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.4
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.2; eu) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.4
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.4
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.5
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.171.0 Safari/530.4
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.170.0 Safari/530.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.169.0 Safari/530.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.168.0 Safari/530.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.164.0 Safari/530.1
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.0 (KHTML, like Gecko) Chrome/2.0.162.0 Safari/530.0
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.0 (KHTML, like Gecko) Chrome/2.0.160.0 Safari/530.0
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/528.10 (KHTML, like Gecko) Chrome/2.0.157.2 Safari/528.10
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.10 (KHTML, like Gecko) Chrome/2.0.157.2 Safari/528.10
|
||||
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/528.11 (KHTML, like Gecko) Chrome/2.0.157.0 Safari/528.11
|
||||
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.9 (KHTML, like Gecko) Chrome/2.0.157.0 Safari/528.9
|
||||
Mozilla/5.0 (Linux; U; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13
|
||||
Mozilla/5.0 (Macintosh; U; Mac OS X 10_6_1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5
|
||||
Mozilla/5.0 (Macintosh; U; Mac OS X 10_5_7; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5
|
||||
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.9 (KHTML, like Gecko) Chrome/ Safari/530.9
|
||||
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/ Safari/530.6
|
||||
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5'''
|
||||
|
||||
async def get(url):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
resp = await session.get(url=url, headers={'User-Agent': random.choice(userAgents)})
|
||||
if resp.status != 200:
|
||||
return None
|
||||
return await resp.text()
|
||||
except:
|
||||
return ""
|
||||
|
||||
async def getLyrics(title: str):
|
||||
link = await googleGet(title=title)
|
||||
if not link:
|
||||
return 0
|
||||
|
||||
page = await get(link)
|
||||
|
||||
metadata = [elm.text for elm in htmlFindAll(page)('b')]
|
||||
|
||||
if not metadata:
|
||||
return None
|
||||
|
||||
# v3.0.4: Update title and artist attributes with exact names
|
||||
title = ''.join(i for i in metadata[1][1:-1] if i not in r'<>:"/\|?*')
|
||||
|
||||
divs = [i.text for i in htmlFindAll(page)('div', {'class': None})]
|
||||
|
||||
lyrics = max(divs, key=len).strip()
|
||||
|
||||
if not lyrics:
|
||||
return print("Lyrics not found")
|
||||
|
||||
rr = re.split(r"(\[[\w\S_ ]+\:])", lyrics)
|
||||
for item in rr:
|
||||
if item == "":
|
||||
rr.remove(item)
|
||||
|
||||
count = len(rr)
|
||||
if count > 1:
|
||||
if (count % 2) != 0:
|
||||
del rr[count-1]
|
||||
return {rr[i].replace("[", "").replace(":]", ""): clearText(rr[i + 1]) for i in range(0, len(rr), 2)}
|
||||
return {"default": clearText(rr[0])}
|
||||
|
||||
async def googleGet(acc = 0.6, artist='', title=''):
|
||||
data = artist + ' ' * (title != '' and artist != '') + title
|
||||
encoded_data = quote(data.replace(' ', '+'))
|
||||
|
||||
|
||||
google_page = await get('{}{}+site%3Aazlyrics.com'.format(
|
||||
'https://duckduckgo.com/html/?q=', encoded_data))
|
||||
|
||||
try:
|
||||
results = re.findall(r'(azlyrics\.com\/lyrics\/[a-z0-9]+\/(\w+).html)', google_page)
|
||||
except:
|
||||
return None
|
||||
|
||||
if len(results):
|
||||
jaro_artist = 1.0
|
||||
jaro_title = 1.0
|
||||
|
||||
if artist:
|
||||
jaro_artist = jaro_distance(artist.replace(' ', ''), results[0][0])
|
||||
if title:
|
||||
jaro_title = jaro_distance(title.replace(' ', ''), results[0][1])
|
||||
|
||||
if jaro_artist >= acc and jaro_title >= acc:
|
||||
return 'https://www.' + results[0][0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def jaro_distance(s1, s2):
|
||||
if (s1 == s2):
|
||||
return 1.0
|
||||
|
||||
len1, len2 = len(s1), len(s2)
|
||||
max_dist = floor(max(len1, len2) / 2) - 1
|
||||
match = 0
|
||||
hash_s1, hash_s2 = [0] * len(s1), [0] * len(s2)
|
||||
|
||||
for i in range(len1):
|
||||
for j in range(max(0, i - max_dist),
|
||||
min(len2, i + max_dist + 1)):
|
||||
if (s1[i] == s2[j] and hash_s2[j] == 0):
|
||||
hash_s1[i], hash_s2[j] = 1, 1
|
||||
match += 1
|
||||
break
|
||||
|
||||
if (match == 0):
|
||||
return 0.0
|
||||
|
||||
t = 0
|
||||
point = 0
|
||||
|
||||
for i in range(len1):
|
||||
if (hash_s1[i]):
|
||||
while (hash_s2[point] == 0):
|
||||
point += 1
|
||||
|
||||
if (s1[i] != s2[point]):
|
||||
point += 1
|
||||
t += 1
|
||||
t = t//2
|
||||
|
||||
return (match/ len1 + match / len2 + (match - t + 1) / match)/ 3.0
|
||||
|
||||
def htmlFindAll(page):
|
||||
soup = bs4.BeautifulSoup(page, "html.parser")
|
||||
return soup.findAll
|
||||
|
||||
def clearText(text: str):
|
||||
if text.startswith("\n\n"):
|
||||
text = text.replace("\n\n", "", 1)
|
||||
|
||||
return text
|
||||
274
cogs/admin.py
Normal file
274
cogs/admin.py
Normal file
@@ -0,0 +1,274 @@
|
||||
import discord
|
||||
import voicelink
|
||||
import io
|
||||
import contextlib
|
||||
import textwrap
|
||||
import traceback
|
||||
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from function import (
|
||||
langs,
|
||||
lang_guilds,
|
||||
update_settings,
|
||||
get_settings,
|
||||
get_lang,
|
||||
embed_color,
|
||||
time as ctime
|
||||
)
|
||||
from view import DebugModal
|
||||
|
||||
class Admin(commands.GroupCog, name="settings"):
|
||||
def __init__(self, bot) -> None:
|
||||
self.bot = bot
|
||||
self.description = "This category is only available to admin permissions on the server."
|
||||
|
||||
def get_settings(self, interaction: discord.Interaction) -> dict:
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
settings = get_settings(interaction.guild_id)
|
||||
else:
|
||||
settings = player.settings
|
||||
|
||||
return player, settings
|
||||
|
||||
@app_commands.command(
|
||||
name = "language",
|
||||
description = "You can choose your preferred language, the bot message will change to the language you set."
|
||||
)
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.checks.cooldown(2, 60.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def language(self, interaction: discord.Interaction, language: str):
|
||||
language = language.upper()
|
||||
if language not in langs:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, "languageNotFound"))
|
||||
|
||||
player, settings = self.get_settings(interaction)
|
||||
if player:
|
||||
player.lang = language
|
||||
lang_guilds[interaction.guild_id] = language
|
||||
update_settings(interaction.guild_id, {'lang': language})
|
||||
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, 'changedLanguage').format(language))
|
||||
|
||||
@language.autocomplete('language')
|
||||
async def autocomplete_callback(self, interaction: discord.Interaction, current: str) -> list:
|
||||
if current:
|
||||
return [ app_commands.Choice(name=lang, value=lang) for lang in langs.keys() if current.upper() in lang ]
|
||||
return [ app_commands.Choice(name=lang, value=lang) for lang in langs.keys() ]
|
||||
|
||||
@app_commands.command(
|
||||
name = "dj",
|
||||
description = "Set a DJ role or remove DJ role."
|
||||
)
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.guild_only()
|
||||
async def dj(self, interaction: discord.Interaction, role: discord.Role = None):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
|
||||
if not role:
|
||||
if player:
|
||||
player.settings.pop('dj', None)
|
||||
update_settings(interaction.guild_id, {'dj':''}, mode="Unset")
|
||||
else:
|
||||
if player:
|
||||
player.settings['dj'] = role.id
|
||||
update_settings(interaction.guild_id, {'dj': role.id })
|
||||
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, 'setDJ').format(f"<@&{role.id}>" if role else "None"), allowed_mentions=discord.AllowedMentions.none())
|
||||
|
||||
@app_commands.command(
|
||||
name = "queue",
|
||||
description = "Change to another type of queue mode."
|
||||
)
|
||||
@app_commands.choices(mode= [
|
||||
app_commands.Choice(name="FairQueue", value="FairQueue"),
|
||||
app_commands.Choice(name="Queue", value="Queue")
|
||||
])
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.guild_only()
|
||||
async def queue(self, interaction: discord.Interaction, mode: str):
|
||||
player, settings = self.get_settings(interaction)
|
||||
settings["queueType"] = mode
|
||||
update_settings(interaction.guild_id, {"queueType": mode})
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, "setqueue").format(mode))
|
||||
|
||||
@app_commands.command(
|
||||
name = "247",
|
||||
description = "Toggles 24/7 mode, which disables automatic inactivity-based disconnects."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
async def playforever(self, interaction: discord.Interaction):
|
||||
player, settings = self.get_settings(interaction)
|
||||
toggle = settings.get('24/7', False)
|
||||
settings['24/7'] = not toggle
|
||||
update_settings(interaction.guild_id, {'24/7':not toggle})
|
||||
toggle = get_lang(interaction.guild_id, "enabled" if not toggle else "disabled")
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, '247').format(toggle))
|
||||
|
||||
@app_commands.command(
|
||||
name = "bypassvote",
|
||||
description = "Toggles voting system.")
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
async def bypassvote(self, interaction: discord.Interaction):
|
||||
player, settings = self.get_settings(interaction)
|
||||
toggle = settings.get('votedisable', True)
|
||||
settings['votedisable'] = not toggle
|
||||
update_settings(interaction.guild_id, {'votedisable': not toggle})
|
||||
toggle = get_lang(interaction.guild_id, "enabled" if not toggle else "disabled")
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, 'bypassVote').format(toggle))
|
||||
|
||||
@app_commands.command(
|
||||
name = "view",
|
||||
description = "Show all the bot settings in your server."
|
||||
)
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.guild_only()
|
||||
async def view(self, interaction: discord.Interaction):
|
||||
player, settings = self.get_settings(interaction)
|
||||
embed=discord.Embed(color=embed_color)
|
||||
embed.set_author(name=get_lang(interaction.guild_id, 'settingsMenu').format(interaction.guild.name), icon_url=self.bot.user.display_avatar.url)
|
||||
if interaction.guild.icon:
|
||||
embed.set_thumbnail(url=interaction.guild.icon.url)
|
||||
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'settingsTitle'), value=get_lang(interaction.guild_id, 'settingsValue').format(
|
||||
settings.get('lang', 'EN'),
|
||||
settings.get('controller', True),
|
||||
f"<@&{settings['dj']}>" if 'dj' in settings else '`None`',
|
||||
settings.get('votedisable', False),
|
||||
settings.get('24/7', False),
|
||||
settings.get('volume', 100),
|
||||
ctime(settings.get('playTime', 0) * 60 * 1000),
|
||||
inline=True)
|
||||
)
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'settingsTitle2'), value=get_lang(interaction.guild_id, 'settingsValue2').format(
|
||||
settings.get("queueType", "Queue"),
|
||||
"200",
|
||||
settings.get("duplicateTrack", True)
|
||||
)
|
||||
)
|
||||
|
||||
perms = interaction.guild.me.guild_permissions
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'settingsPermTitle'), value=get_lang(interaction.guild_id, 'settingsPermValue').format(
|
||||
'<a:Check:941206936651706378>' if perms.administrator else '<a:Cross:941206918255497237>',
|
||||
'<a:Check:941206936651706378>' if perms.manage_guild else '<a:Cross:941206918255497237>',
|
||||
'<a:Check:941206936651706378>' if perms.manage_channels else '<a:Cross:941206918255497237>',
|
||||
'<a:Check:941206936651706378>' if perms.manage_messages else '<a:Cross:941206918255497237>'), inline=False
|
||||
)
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(
|
||||
name = "volume",
|
||||
description = "Set the player's volume."
|
||||
)
|
||||
@app_commands.describe(
|
||||
value = "Input a integer."
|
||||
)
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.guild_only()
|
||||
async def volume(self, interaction: discord.Interaction, value: app_commands.Range[int, 1, 150]):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
|
||||
if player:
|
||||
player.settings['volume'] = value
|
||||
await player.set_volume(value)
|
||||
|
||||
update_settings(interaction.guild_id, {'volume':value})
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, 'setVolume').format(value))
|
||||
|
||||
@app_commands.command(
|
||||
name = "togglecontroller",
|
||||
description = "Toggles the music controller."
|
||||
)
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.guild_only()
|
||||
async def togglecontroller(self, interaction: discord.Interaction):
|
||||
player, settings = self.get_settings(interaction)
|
||||
toggle = settings.get('controller', True)
|
||||
settings['controller'] = not toggle
|
||||
if player and settings['controller'] is False:
|
||||
if player.controller:
|
||||
try:
|
||||
await player.controller.delete()
|
||||
except:
|
||||
discord.ui.View.from_message(player.controller).stop()
|
||||
|
||||
update_settings(interaction.guild_id, {'controller': not toggle})
|
||||
toggle = get_lang(interaction.guild_id, "enabled" if not toggle else "disabled")
|
||||
await interaction.response.send_message(get_lang(interaction.guild_id, 'togglecontroller').format(toggle))
|
||||
|
||||
@app_commands.command(
|
||||
name = "duplicatetrack",
|
||||
description = "Toggle Vocard to prevent duplicate songs from queuing."
|
||||
)
|
||||
@app_commands.choices(toggle = [
|
||||
app_commands.Choice(name="Enable", value="enabled"),
|
||||
app_commands.Choice(name="Disable", value="disabled")
|
||||
])
|
||||
@app_commands.checks.has_permissions(manage_guild=True)
|
||||
@app_commands.guild_only()
|
||||
async def duplicatetrack(self, interaction: discord.Interaction, toggle: str):
|
||||
player, settings = self.get_settings(interaction)
|
||||
if player:
|
||||
player.queue._duplicateTrack = False if toggle == 'enabled' else True
|
||||
|
||||
update_settings(interaction.guild_id, {'duplicateTrack': False if toggle == 'enabled' else True})
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, "toggleDuplicateTrack").format(get_lang(interaction.guild_id, toggle)))
|
||||
|
||||
@app_commands.command(
|
||||
name = "debug",
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def debug(self, interaction: discord.Interaction):
|
||||
if interaction.user.id not in [358819659581227011, 705783356767338648]:
|
||||
return await interaction.response.send_message("You are not able to use this command!")
|
||||
|
||||
def clear_code(content):
|
||||
if content.startswith("```") and content.endswith("```"):
|
||||
return "\n".join(content.split("\n")[1:])[:-3]
|
||||
else:
|
||||
return content
|
||||
|
||||
modal = DebugModal(title="Debug Panel")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
if modal.values is None:
|
||||
return
|
||||
|
||||
e = None
|
||||
|
||||
local_variables = {
|
||||
"discord": discord,
|
||||
"commands": commands,
|
||||
"voicelink": voicelink,
|
||||
"bot": self.bot,
|
||||
"interaction": interaction,
|
||||
"channel": interaction.channel,
|
||||
"author": interaction.user,
|
||||
"guild": interaction.guild,
|
||||
"message": interaction.message,
|
||||
"input": None
|
||||
}
|
||||
|
||||
code = clear_code(modal.values)
|
||||
str_obj = io.StringIO() #Retrieves a stream of data
|
||||
try:
|
||||
with contextlib.redirect_stdout(str_obj):
|
||||
exec(f"async def func():\n{textwrap.indent(code, ' ')}", local_variables)
|
||||
obj = await local_variables["func"]()
|
||||
result = f"{str_obj.getvalue()}\n-- {obj}\n"
|
||||
except Exception as e:
|
||||
errormsg = ''.join(traceback.format_exception(e, e, e.__traceback__))
|
||||
return await interaction.followup.send(f"```py\n{errormsg}```")
|
||||
|
||||
string = result.split("\n")
|
||||
text = ""
|
||||
for index, i in enumerate(string, start=1):
|
||||
text += f"{'%03d' % index} | {i}\n"
|
||||
return await interaction.followup.send(f"```{text}```")
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Admin(bot))
|
||||
856
cogs/basic.py
Normal file
856
cogs/basic.py
Normal file
@@ -0,0 +1,856 @@
|
||||
import discord
|
||||
import voicelink
|
||||
import re
|
||||
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from function import (
|
||||
time as ctime,
|
||||
formatTime,
|
||||
emoji_source,
|
||||
youtube_api_key,
|
||||
requests_api,
|
||||
get_lang,
|
||||
embed_color
|
||||
)
|
||||
|
||||
from addons import getLyrics
|
||||
from view import SearchView, ListView, LinkView, LyricsView, ChapterView, HelpView
|
||||
from validators import url
|
||||
from random import shuffle
|
||||
|
||||
async def connect_channel(interaction: discord.Interaction, channel: discord.VoiceChannel = None) -> voicelink.Player:
|
||||
try:
|
||||
channel = channel or interaction.user.voice.channel
|
||||
except:
|
||||
raise voicelink.VoicelinkException(get_lang(interaction.guild_id, 'noChannel'))
|
||||
|
||||
check = channel.permissions_for(interaction.guild.me)
|
||||
if check.connect == False or check.speak == False:
|
||||
raise voicelink.VoicelinkException(get_lang(interaction.guild_id, 'noPermission'))
|
||||
|
||||
player: voicelink.Player = await channel.connect(cls=voicelink.Player(interaction.client, channel, interaction))
|
||||
return player
|
||||
|
||||
async def nowplay(interaction: discord.Interaction, player: voicelink.Player):
|
||||
track = player.current
|
||||
if not track:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
|
||||
upnext = "\n".join(f"`{index}.` `[{track.formatLength}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate(player.queue.tracks()[:2], start=2))
|
||||
embed=discord.Embed(description=player.get_msg('nowplayingDesc').format(track.title), color=embed_color)
|
||||
embed.set_author(name=track.requester if track.requester else interaction.client, icon_url=track.requester.display_avatar.url if track.requester else interaction.client.user.display_avatar.url)
|
||||
|
||||
if upnext:
|
||||
embed.add_field(name=player.get_msg('nowplayingField'), value=upnext)
|
||||
pbar = "".join(":radio_button:" if i == round(player.position // round(track.length // 15)) else "▬" for i in range(15))
|
||||
icon = ":red_circle:" if track.is_stream else (":pause_button:" if player.is_paused else ":arrow_forward:")
|
||||
|
||||
embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatLength}]**", inline=False)
|
||||
|
||||
return await interaction.response.send_message(embed=embed, view=LinkView(player.get_msg('nowplayingLink').format(track.source), track.emoji, track.uri))
|
||||
|
||||
async def help_autocomplete(interaction: discord.Interaction, current: str) -> list:
|
||||
return [ app_commands.Choice(name=c.capitalize(), value=c) for c in interaction.client.cogs if c not in ["Nodes", "Task"] and current in c ]
|
||||
|
||||
class Basic(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
self.description = "This category is available to anyone on this server. Voting is required in certain commands."
|
||||
self.ctx_menu = app_commands.ContextMenu(
|
||||
name="play",
|
||||
callback=self._play,
|
||||
)
|
||||
self.bot.tree.add_command(self.ctx_menu)
|
||||
|
||||
async def cog_unload(self) -> None:
|
||||
self.bot.tree.remove_command(self.ctx_menu.name, type=self.ctx_menu.type)
|
||||
|
||||
@app_commands.command(
|
||||
name = "connect",
|
||||
description = "Connect to a voice channel."
|
||||
)
|
||||
@app_commands.describe(
|
||||
channel="Provide a channel to connect."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 30.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def connect(self, interaction: discord.Interaction, channel: discord.VoiceChannel = None) -> None:
|
||||
try:
|
||||
player = await connect_channel(interaction, channel)
|
||||
except discord.errors.ClientException:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, "alreadyConnected"))
|
||||
|
||||
await interaction.response.send_message(player.get_msg('connect').format(player.channel))
|
||||
|
||||
@app_commands.command(
|
||||
name = "play",
|
||||
description = "Loads your input and added it to the queue."
|
||||
)
|
||||
@app_commands.describe(
|
||||
query="Input a query or a searchable link.",
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 15.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def play(self, interaction: discord.Interaction, query: str) -> None:
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
player = await connect_channel(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
tracks = await player.get_tracks(query, requester=interaction.user)
|
||||
if not tracks:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackFound'))
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
for track in tracks.tracks:
|
||||
await player.queue.put(track)
|
||||
await interaction.response.send_message(player.get_msg('playlistLoad').format(tracks.name, len(tracks.tracks)))
|
||||
else:
|
||||
position = await player.queue.put(tracks[0])
|
||||
await interaction.response.send_message((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + ( player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].author, tracks[0].formatLength, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').format(tracks[0].title, tracks[0].author, tracks[0].formatLength)))
|
||||
except voicelink.QueueFull as e:
|
||||
await interaction.response.send_message(e)
|
||||
finally:
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
@app_commands.checks.cooldown(2, 15.0, key=lambda i: (i.guild_id))
|
||||
async def _play(self, interaction: discord.Interaction, message: discord.Message):
|
||||
query = ""
|
||||
if message.content:
|
||||
url = re.findall("http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content)
|
||||
if url:
|
||||
query = url[0]
|
||||
elif message.attachments:
|
||||
query = message.attachments[0].url
|
||||
|
||||
if not query:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, key="noPlaySource"), ephemeral=True)
|
||||
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
player = await connect_channel(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
tracks = await player.get_tracks(query, requester=interaction.user)
|
||||
if not tracks:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackFound'))
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
for track in tracks.tracks:
|
||||
await player.queue.put(track)
|
||||
await interaction.response.send_message(player.get_msg('playlistLoad').format(tracks.name, len(tracks.tracks)))
|
||||
else:
|
||||
await player.queue.put(tracks[0])
|
||||
await interaction.response.send_message((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + ( player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].author, tracks[0].formatLength, player.queue.count) if player.queue.count >= 1 and player.is_playing else player.get_msg('trackLoad').format(tracks[0].title, tracks[0].author, tracks[0].formatLength)))
|
||||
except voicelink.QueueFull as e:
|
||||
await interaction.response.send_message(e)
|
||||
finally:
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
@app_commands.command(
|
||||
name = "search",
|
||||
description = "Loads your input and added it to the queue."
|
||||
)
|
||||
@app_commands.describe(
|
||||
query="Input the name of the song.",
|
||||
platform="Select the platform you want to search."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 15.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.choices(platform = [
|
||||
app_commands.Choice(name="Youtube", value="Youtube"),
|
||||
app_commands.Choice(name="Youtube Music", value="Youtube Music"),
|
||||
app_commands.Choice(name="Spotify", value="Spotify"),
|
||||
app_commands.Choice(name="SoundCloud", value="SoundCloud"),
|
||||
app_commands.Choice(name="Apple Music", value="Apple")
|
||||
])
|
||||
@app_commands.guild_only()
|
||||
async def search(self, interaction: discord.Interaction, query: str, platform: str = "Youtube"):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
player = await connect_channel(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if url(query):
|
||||
return await interaction.response.send_message(player.get_msg('noLinkSupport'), ephemeral=True)
|
||||
if platform != 'Spotify':
|
||||
query_platform = ("ytsearch" if platform == "Youtube" else "ytmsearch" if platform == "Youtube Music" else "scsearch" if platform == "SoundCloud" else "amsearch") + f":{query}"
|
||||
tracks = await player.get_tracks(query=query_platform, requester=interaction.user)
|
||||
else:
|
||||
tracks = await player.spotifySearch(query=query, requester=interaction.user)
|
||||
if not tracks:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackFound'))
|
||||
query_track = "\n".join(f"`{index}.` `[{track.formatLength}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1))
|
||||
embed=discord.Embed(title=player.get_msg('searchTitle').format(query), description=player.get_msg('searchDesc').format(emoji_source(platform.lower()), platform, len(tracks[0:10]), query_track), color=embed_color)
|
||||
view = SearchView(tracks=tracks[0:10], lang=player.lang)
|
||||
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||||
view.response = await interaction.original_response()
|
||||
await view.wait()
|
||||
if view.values is not None:
|
||||
msg = ""
|
||||
for value in view.values:
|
||||
track = tracks[int(value.split(". ")[0]) - 1]
|
||||
await player.queue.put(track)
|
||||
msg += ((f"`{player.get_msg('live')}`" if track.is_stream else "") + ( player.get_msg('trackLoad_pos').format(track.title, track.author, track.formatLength, player.queue.count) if player.queue.count >= 1 else player.get_msg('trackLoad').format(track.title, track.author, track.formatLength)) )
|
||||
await interaction.followup.send(msg)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
@app_commands.command(
|
||||
name = "pause",
|
||||
description = "Pause the music."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 15.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def pause(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.is_paused:
|
||||
return await interaction.response.send_message(player.get_msg('pauseError'))
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
if interaction.user in player.pause_votes:
|
||||
return await interaction.response.send_message(player.get_msg('voted'), ephemeral=True)
|
||||
else:
|
||||
player.pause_votes.add(interaction.user)
|
||||
if len(player.pause_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(player.get_msg('pauseVote').format(interaction.user, len(player.pause_votes), required))
|
||||
|
||||
await player.set_pause(True)
|
||||
player.pause_votes.clear()
|
||||
await interaction.response.send_message(player.get_msg('paused').format(interaction.user))
|
||||
|
||||
@app_commands.command(
|
||||
name = "resume",
|
||||
description = "Resume the music."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 15.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def resume(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.is_paused:
|
||||
return await interaction.response.send_message(player.get_msg('resumeError'))
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
if interaction.user in player.resume_votes:
|
||||
return await interaction.response.send_message(player.get_msg('voted'), ephemeral=True)
|
||||
else:
|
||||
player.resume_votes.add(interaction.user)
|
||||
if len(player.resume_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(player.get_msg('resumeVote').format(interaction.user, len(player.resume_votes), required))
|
||||
|
||||
await player.set_pause(False)
|
||||
player.resume_votes.clear()
|
||||
await interaction.response.send_message(player.get_msg('resumed').format(interaction.user))
|
||||
|
||||
@app_commands.command(
|
||||
name = "skip",
|
||||
description = "Skips to the next song or skips to the specified song."
|
||||
)
|
||||
@app_commands.describe(
|
||||
index="Enter a index that you want to skip to."
|
||||
)
|
||||
@app_commands.checks.cooldown(3, 20.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def skip(self, interaction: discord.Interaction, index: int = 0):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.is_playing:
|
||||
return await interaction.response.send_message(player.get_msg('skipError'), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
if interaction.user == player.current.requester:
|
||||
pass
|
||||
elif interaction.user in player.skip_votes:
|
||||
return await interaction.response.send_message(player.get_msg('voted'), ephemeral=True)
|
||||
else:
|
||||
player.skip_votes.add(interaction.user)
|
||||
if len(player.skip_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(player.get_msg('skipVote').format(interaction.user, len(player.skip_votes), required))
|
||||
|
||||
if not player.node._available:
|
||||
return await interaction.response.send_message(player.get_msg('nodeReconnect'))
|
||||
|
||||
if index:
|
||||
await player.queue.skipto(index)
|
||||
|
||||
await interaction.response.send_message(player.get_msg('skipped').format(interaction.user))
|
||||
|
||||
if player.queue.repeat == "Track":
|
||||
player.queue.set_repeat("Off")
|
||||
await player.stop()
|
||||
|
||||
@app_commands.command(
|
||||
name = "back",
|
||||
description = "Skips back to the previous song or skips to the specified previous song."
|
||||
)
|
||||
@app_commands.describe(
|
||||
index="Enter a index that you want to skip back to."
|
||||
)
|
||||
@app_commands.checks.cooldown(3, 20.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def back(self, interaction: discord.Interaction, index: int = 1):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
if interaction.user in player.previous_votes:
|
||||
return await interaction.response.send_message(player.get_msg('voted'), ephemeral=True)
|
||||
else:
|
||||
player.previous_votes.add(interaction.user)
|
||||
if len(player.previous_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(player.get_msg('backVote').format(interaction.user, len(player.previous_votes), required))
|
||||
|
||||
if not player.node._available:
|
||||
return await interaction.response.send_message(player.get_msg('nodeReconnect'))
|
||||
|
||||
if not player.is_playing:
|
||||
player.queue.backto(index)
|
||||
await player.do_next()
|
||||
else:
|
||||
player.queue.backto(index + 1)
|
||||
await player.stop()
|
||||
|
||||
await interaction.response.send_message(player.get_msg('backed').format(interaction.user))
|
||||
|
||||
if player.queue.repeat == "Track":
|
||||
player.queue.set_repeat("Off")
|
||||
|
||||
@app_commands.command(
|
||||
name = "seek",
|
||||
description = "Change the player position."
|
||||
)
|
||||
@app_commands.describe(
|
||||
position="Input position. Exmaple: 1:20."
|
||||
)
|
||||
@app_commands.checks.cooldown(3, 20.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def seek(self, interaction: discord.Interaction, position: str):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_pos'), ephemeral=True)
|
||||
if player.position == 0:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
|
||||
num = formatTime(position)
|
||||
if num is None:
|
||||
return await interaction.response.send_message(player.get_msg('timeFormatError'), ephemeral=True)
|
||||
|
||||
await player.seek(num)
|
||||
await interaction.response.send_message(player.get_msg('seek').format(position))
|
||||
|
||||
@app_commands.command(
|
||||
name = "queue",
|
||||
description = "Display the players queue songs in your queue."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 40.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def queue(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.queue.is_empty:
|
||||
return await nowplay(interaction, player)
|
||||
view = ListView(player=player, author=interaction.user)
|
||||
await interaction.response.send_message(embed=view.build_embed(), view=view)
|
||||
view.response = await interaction.original_response()
|
||||
|
||||
@app_commands.command(
|
||||
name = "history",
|
||||
description = "Display the players queue songs in your history queue."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 40.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def history(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.queue.history():
|
||||
return await nowplay(interaction, player)
|
||||
|
||||
view = ListView(player=player, author=interaction.user, isQueue=False)
|
||||
await interaction.response.send_message(embed=view.build_embed(), view=view)
|
||||
view.response = await interaction.original_response()
|
||||
|
||||
@app_commands.command(
|
||||
name = "leave",
|
||||
description = "Disconnects the bot from your voice channel and chears the queue."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def leave(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
if interaction.user in player.stop_votes:
|
||||
return await interaction.response.send_message(player.get_msg('voted'), ephemeral=True)
|
||||
else:
|
||||
player.stop_votes.add(interaction.user)
|
||||
if len(player.stop_votes) >= (required := player.required(leave=True)):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(player.get_msg('leaveVote').format(interaction.user, len(player.stop_votes), required))
|
||||
|
||||
await interaction.response.send_message(player.get_msg('left').format(interaction.user))
|
||||
await player.teardown()
|
||||
|
||||
@app_commands.command(
|
||||
name = "nowplaying",
|
||||
description = "Shows details of the current track."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 10.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def nowplaying(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
await nowplay(interaction, player)
|
||||
|
||||
@app_commands.command(
|
||||
name = "loop",
|
||||
description = "Changes Loop mode."
|
||||
)
|
||||
@app_commands.describe(
|
||||
mode = "Choose a looping mode."
|
||||
)
|
||||
@app_commands.choices( mode = [
|
||||
app_commands.Choice(name='Off', value='Off'),
|
||||
app_commands.Choice(name='Track', value='Track'),
|
||||
app_commands.Choice(name='Queue', value='Queue')
|
||||
])
|
||||
@app_commands.guild_only()
|
||||
async def loop(self, interaction: discord.Interaction, mode: str):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_mode'), ephemeral=True)
|
||||
|
||||
player.queue.set_repeat(mode)
|
||||
await interaction.response.send_message(player.get_msg('repeat').format(mode))
|
||||
|
||||
@app_commands.command(
|
||||
name = "clear",
|
||||
description = "Remove all the tracks in your queue or history queue."
|
||||
)
|
||||
@app_commands.describe(
|
||||
queue = "Choose a queue that you want to clear."
|
||||
)
|
||||
@app_commands.choices( queue = [
|
||||
app_commands.Choice(name='Queue', value='Queue'),
|
||||
app_commands.Choice(name='History', value='History')
|
||||
])
|
||||
@app_commands.guild_only()
|
||||
async def clear(self, interaction: discord.Interaction, queue: str):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_queue'), ephemeral=True)
|
||||
|
||||
if queue == 'Queue':
|
||||
player.queue.clear()
|
||||
elif queue == 'History':
|
||||
player.queue.history_clear(player.is_playing)
|
||||
|
||||
await interaction.response.send_message(player.get_msg('cleared').format(queue))
|
||||
|
||||
@app_commands.command(
|
||||
name = "remove",
|
||||
description = "Removes specified track or a range of tracks from the queue."
|
||||
)
|
||||
@app_commands.describe(
|
||||
position = "Input a position from the queue to be removed.",
|
||||
position2 = "Set the range of the queue to be removed.",
|
||||
member = "Remove tracks requested by a specific member."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def remove(self, interaction: discord.Interaction, position: int, position2: int = None, member: discord.Member = None):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_queue'), ephemeral=True)
|
||||
|
||||
removedTrack = player.queue.remove(position, position2, member)
|
||||
await interaction.response.send_message(player.get_msg('removed').format(removedTrack))
|
||||
|
||||
@app_commands.command(
|
||||
name = "forward",
|
||||
description = "Forwards by a certain amount of time in the current track. The default is 10 seconds."
|
||||
)
|
||||
@app_commands.describe(
|
||||
position = "Input a amount that you to forward to. Exmaple: 1:20"
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 20.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def forward(self, interaction: discord.Interaction, position: str = "10"):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_pos'), ephemeral=True)
|
||||
|
||||
num = formatTime(position)
|
||||
if num is None:
|
||||
return await interaction.response.send_message(player.get_msg('timeFormatError'), ephemeral=True)
|
||||
|
||||
await player.seek(player.position + num)
|
||||
await interaction.response.send_message(player.get_msg('forward').format(ctime(player.position + num)))
|
||||
|
||||
@app_commands.command(
|
||||
name = "rewind",
|
||||
description = "Rewind by a certain amount of time in the current track. The default is 10 seconds."
|
||||
)
|
||||
@app_commands.describe(
|
||||
position = "Input a amount that you to rewind to. Exmaple: 1:20"
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 20.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def rewind(self, interaction: discord.Interaction, position: str = "10"):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_pos'), ephemeral=True)
|
||||
|
||||
num = formatTime(position)
|
||||
if num is None:
|
||||
return await interaction.response.send_message(player.get_msg('timeFormatError'), ephemeral=True)
|
||||
|
||||
await player.seek(player.position - num)
|
||||
await interaction.response.send_message(player.get_msg('rewind').format(ctime(player.position - num)))
|
||||
|
||||
@app_commands.command(
|
||||
name = "replay",
|
||||
description = "Reset the progress of the current song."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 20.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def replay(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_pos'), ephemeral=True)
|
||||
|
||||
await player.seek(0)
|
||||
await interaction.response.send_message(player.get_msg('replay'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "shuffle",
|
||||
description = "Randomizes the tracks in the queue."
|
||||
)
|
||||
@app_commands.checks.cooldown(1, 15.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def shuffle(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
if interaction.user in player.shuffle_votes:
|
||||
return await interaction.response.send_message(player.get_msg('voted'), ephemeral=True)
|
||||
else:
|
||||
player.shuffle_votes.add(interaction.user)
|
||||
if len(player.shuffle_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(player.get_msg('shuffleVote').format(interaction.user, len(player.skip_votes), required))
|
||||
replacement = player.queue.tracks()
|
||||
if len(replacement) < 3:
|
||||
return await interaction.response.send_message(player.get_msg('shuffleError'))
|
||||
shuffle(replacement)
|
||||
player.queue.replace("Queue", replacement)
|
||||
player.shuffle_votes.clear()
|
||||
await interaction.response.send_message(player.get_msg('shuffled'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "move",
|
||||
description = "Moves the specified song to the specified position."
|
||||
)
|
||||
@app_commands.describe(
|
||||
track = "The track to move. Example: 2",
|
||||
position = "The new position to move the track to. Exmaple: 1"
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 15.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def move(self, interaction: discord.Interaction, track: int, position: int ):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_pos'), ephemeral=True)
|
||||
|
||||
player.queue.swap(track, position)
|
||||
await interaction.response.send_message(player.get_msg('moved').format(track, position))
|
||||
|
||||
@app_commands.command(
|
||||
name = "lyrics",
|
||||
description = "Displays lyrics for the playing track."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "Searches for your query and displays the reutned lyrics.",
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 60.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def lyrics(self, interaction: discord.Interaction, name: str = None):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
|
||||
if not name:
|
||||
if not player or not player.is_playing:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noTrackPlaying'), ephemeral=True)
|
||||
name = player.current.title + " " + player.current.author
|
||||
await interaction.response.defer()
|
||||
song = await getLyrics(name)
|
||||
if not song:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'lyricsNotFound'), ephemeral=True)
|
||||
|
||||
view = LyricsView(name=name, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=interaction.user)
|
||||
await interaction.followup.send(embed=view.build_embed(), view=view)
|
||||
view.response = await interaction.original_response()
|
||||
|
||||
@app_commands.command(
|
||||
name = "swapdj",
|
||||
description = "Transfer dj to another."
|
||||
)
|
||||
@app_commands.describe(
|
||||
member = "Choose a member to transfer the dj role."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def swapdj(self, interaction: discord.Interaction, member: discord.Member):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.dj.id != interaction.user.id or player.settings.get('dj', False):
|
||||
return await interaction.response.send_message(player.get_msg('notdj').format(f"<@&{player.settings['dj']}>" if player.settings.get('dj') else player.dj.mention), ephemeral=True)
|
||||
|
||||
if player.dj.id == member.id or member.bot:
|
||||
return await interaction.response.send_message(player.get_msg('djToMe'), ephemeral=True)
|
||||
|
||||
if member not in player.channel.members:
|
||||
return await interaction.response.send_message(player.get_msg('djnotinchannel').format(member), ephemeral=True)
|
||||
|
||||
player.dj = member
|
||||
await interaction.response.send_message(player.get_msg('djswap').format(member))
|
||||
|
||||
|
||||
@app_commands.command(
|
||||
name = "chapters",
|
||||
description = "Lists all chapters of the currently playing song (if any)."
|
||||
)
|
||||
@app_commands.checks.cooldown(2, 30.0, key=lambda i: (i.guild_id))
|
||||
@app_commands.guild_only()
|
||||
async def chapters(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if not (track := player.current):
|
||||
return await interaction.response.send_message(player.get_msg('noTrackPlaying'), ephemeral=True)
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_pos'), ephemeral=True)
|
||||
|
||||
if track.source != 'youtube':
|
||||
return await interaction.response.send_message(player.get_msg('chatpersNotSupport'), ephemeral=True)
|
||||
|
||||
request_uri = "https://youtube.googleapis.com/youtube/v3/videos?part=snippet&id={videoId}&key={key}".format(videoId=track.identifier, key=youtube_api_key)
|
||||
|
||||
data = await requests_api(request_uri)
|
||||
if not data:
|
||||
return await interaction.response.send_message(player.get_msg('noChaptersFound'), ephemeral=True)
|
||||
|
||||
try:
|
||||
desc = data['items'][0]['snippet']['description']
|
||||
except KeyError:
|
||||
return await interaction.response.send_message(player.get_msg('noChaptersFound'), ephemeral=True)
|
||||
|
||||
chapters = re.findall(r"(?P<timestamp>\d+:\d+|\d+:\d+:\d+) (?P<desc>.+)", desc)
|
||||
if not chapters:
|
||||
return await interaction.response.send_message(player.get_msg('noChaptersFound'), ephemeral=True)
|
||||
|
||||
view = ChapterView(player, chapters, author=interaction.user)
|
||||
await interaction.response.send_message(view=view)
|
||||
view.response = await interaction.original_response()
|
||||
|
||||
@app_commands.command(
|
||||
name = "autoplay",
|
||||
description = "Toggles autoplay mode, it will automatically queue the best songs to play."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def autoplay(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'noPlayer'), ephemeral=True)
|
||||
|
||||
if not await player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(player.get_msg('missingPerms_autoplay'), ephemeral=True)
|
||||
|
||||
check = not player.settings.get("autoplay", False)
|
||||
player.settings['autoplay'] = check
|
||||
await interaction.response.send_message(player.get_msg('autoplay').format(player.get_msg('enabled') if check else player.get_msg('disabled')))
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
@app_commands.command(
|
||||
name = "help",
|
||||
description = "Lists all the commands in Vocard."
|
||||
)
|
||||
@app_commands.autocomplete(category=help_autocomplete)
|
||||
@app_commands.guild_only()
|
||||
async def help(self, interaction: discord.Interaction, category: str = "News") -> None:
|
||||
if category not in self.bot.cogs:
|
||||
category = "News"
|
||||
view = HelpView(self.bot, interaction.user)
|
||||
embed = view.build_embed(category)
|
||||
await interaction.response.send_message(embed=embed, view=view)
|
||||
view.response = await interaction.original_response()
|
||||
|
||||
@app_commands.command(
|
||||
name = "ping",
|
||||
description = "Test if the bot is alive, and see the delay between your commands and my response."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def ping(self, interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
embed = discord.Embed(color=embed_color)
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'pingTitle1'), value=get_lang(interaction.guild_id, 'pingfield1').format("0", "0", self.bot.latency, '😭' if self.bot.latency > 5 else ('😨' if self.bot.latency > 1 else '👌'), "St Louis, MO, United States"))
|
||||
if player:
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'pingTitle2'), value=get_lang(interaction.guild_id, 'pingfield2').format(player.node._identifier, player.ping, player.node.player_count, player.channel.rtc_region), inline=False)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Basic(bot))
|
||||
264
cogs/effect.py
Normal file
264
cogs/effect.py
Normal file
@@ -0,0 +1,264 @@
|
||||
import discord
|
||||
import voicelink
|
||||
|
||||
from function import (
|
||||
get_lang,
|
||||
)
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
async def check_access(interaction: discord.Interaction):
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
raise voicelink.VoicelinkException(get_lang(interaction.guild_id, 'noPlayer'))
|
||||
|
||||
return player
|
||||
|
||||
class Effect(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
self.description = "This category is only available to DJ on this server. (You can setdj on your server by /settings setdj <DJ ROLE>)"
|
||||
|
||||
async def effect_autocomplete(self, interaction: discord.Interaction, current: str) -> list:
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return []
|
||||
if current:
|
||||
return [ app_commands.Choice(name=effect.tag, value=effect.tag) for effect in player.filters.get_filters() if current in effect.tag]
|
||||
return [ app_commands.Choice(name=effect.tag, value=effect.tag) for effect in player.filters.get_filters() ]
|
||||
|
||||
@app_commands.command(
|
||||
name = "speed",
|
||||
description= "Sets the player's playback speed."
|
||||
)
|
||||
@app_commands.describe(
|
||||
value = "The value to set the speed to. Default is `1.0`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def speed(self, interaction: discord.Interaction, value: app_commands.Range[float, 0, 2]):
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="speed"):
|
||||
player.filters.remove_filter(filter_tag="speed")
|
||||
await player.add_filter(voicelink.Timescale(tag="speed", speed= value))
|
||||
await interaction.response.send_message(f"You set the speed to **{value}**.")
|
||||
|
||||
@app_commands.command(
|
||||
name = "karaoke",
|
||||
description= "Uses equalization to eliminate part of a band, usually targeting vocals."
|
||||
)
|
||||
@app_commands.describe(
|
||||
level = "The level of the karaoke. Default is `1.0`",
|
||||
monolevel = "The monolevel of the karaoke. Default is `1.0`",
|
||||
filterband = "The filter band of the karaoke. Default is `220.0`",
|
||||
filterwidth = "The filter band of the karaoke. Default is `100.0`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def karaoke(self, interaction: discord.Interaction, level: app_commands.Range[float, 0, 2] = 1.0, monolevel: app_commands.Range[float, 0, 2] = 1.0, filterband: app_commands.Range[float, 100, 300] = 220.0, filterwidth: app_commands.Range[float, 50, 150] = 100.0) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="karaoke"):
|
||||
player.filters.remove_filter(filter_tag="karaoke")
|
||||
await player.add_filter(voicelink.Karaoke(tag="karaoke", level=level, mono_level=monolevel, filter_band=filterband, filter_width=filterwidth))
|
||||
await interaction.response.send_message(player.get_msg('karaoke').format(level, monolevel, filterband, filterwidth))
|
||||
|
||||
@app_commands.command(
|
||||
name = "tremolo",
|
||||
description= "Uses amplification to create a shuddering effect, where the volume quickly oscillates."
|
||||
)
|
||||
@app_commands.describe(
|
||||
frequency = "The frequency of the tremolo. Default is `2.0`",
|
||||
depth = "The depth of the tremolo. Default is `0.5`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def tremolo(self, interaction: discord.Interaction, frequency: app_commands.Range[float, 0, 10] = 2.0, depth: app_commands.Range[float, 0, 1] = 0.5) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="tremolo"):
|
||||
player.filters.remove_filter(filter_tag="tremolo")
|
||||
await player.add_filter(voicelink.Tremolo(tag="tremolo", frequency=frequency, depth=depth))
|
||||
await interaction.response.send_message(player.get_msg('tremolo&vibrato').format(frequency, depth))
|
||||
|
||||
@app_commands.command(
|
||||
name = "vibrato",
|
||||
description= "Similar to tremolo. While tremolo oscillates the volume, vibrato oscillates the pitch."
|
||||
)
|
||||
@app_commands.describe(
|
||||
frequency = "The frequency of the vibrato. Default is `2.0`",
|
||||
depth = "The Depth of the vibrato. Default is `0.5`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def vibrato(self, interaction: discord.Interaction, frequency: app_commands.Range[float, 0, 14] = 2.0, depth: app_commands.Range[float, 0, 1] = 0.5) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="vibrato"):
|
||||
player.filters.remove_filter(filter_tag="vibrato")
|
||||
await player.add_filter(voicelink.Vibrato(tag="vibrato", frequency=frequency, depth=depth))
|
||||
await interaction.response.send_message(player.get_msg('tremolo&vibrato').format(frequency, depth))
|
||||
|
||||
@app_commands.command(
|
||||
name = "rotation",
|
||||
description= "Rotates the sound around the stereo channels/user headphones aka Audio Panning."
|
||||
)
|
||||
@app_commands.describe(
|
||||
hertz = "The hertz of the rotation. Default is `0.2`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def rotation(self, interaction: discord.Interaction, hertz: app_commands.Range[float, 0, 2] = 0.2) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="rotation"):
|
||||
player.filters.remove_filter(filter_tag="rotation")
|
||||
await player.add_filter(voicelink.Rotation(tag="rotation", rotation_hertz=hertz))
|
||||
await interaction.response.send_message(player.get_msg('rotation').format(hertz))
|
||||
|
||||
@app_commands.command(
|
||||
name = "distortion",
|
||||
description= "Distortion effect. It can generate some pretty unique audio effects."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def distortion(self, interaction: discord.Interaction) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="distortion"):
|
||||
player.filters.remove_filter(filter_tag="distortion")
|
||||
await player.add_filter(voicelink.Distortion(tag="distortion", sin_offset= 0.0, sin_scale= 1.0, cos_offset= 0.0, cos_scale=1.0, tan_offset= 0.0, tan_scale=1.0, offset=0.0, scale=1.0))
|
||||
await interaction.response.send_message(player.get_msg('distortion'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "lowpass",
|
||||
description= "Filter which supresses higher frequencies and allows lower frequencies to pass."
|
||||
)
|
||||
@app_commands.describe(
|
||||
smoothing = "The level of the lowPass. Default is `20.0`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def lowpass(self, interaction: discord.Interaction, smoothing: app_commands.Range[float, 10, 30] = 20.0) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="lowpass"):
|
||||
player.filters.remove_filter(filter_tag="lowpass")
|
||||
await player.add_filter(voicelink.LowPass(tag="lowpass", smoothing= smoothing))
|
||||
await interaction.response.send_message(player.get_msg('lowpass').format(smoothing))
|
||||
|
||||
@app_commands.command(
|
||||
name = "channelmix",
|
||||
description= "Filter which manually adjusts the panning of the audio."
|
||||
)
|
||||
@app_commands.describe(
|
||||
left_to_left = "Sounds from left to left. Default is `1.0`",
|
||||
right_to_right = "Sounds from right to right. Default is `1.0`",
|
||||
left_to_right = "Sounds from left to right. Default is `0.0`",
|
||||
right_to_left = "Sounds from right to left. Default is `0.0`"
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def channelmix(self, interaction: discord.Interaction, left_to_left: app_commands.Range[float, 0, 1] = 1.0, right_to_right: app_commands.Range[float, 0, 1] = 1.0, left_to_right: app_commands.Range[float, 0, 1] = 0.0, right_to_left: app_commands.Range[float, 0, 1] = 0.0) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
if player.filters.has_filter(filter_tag="channelmix"):
|
||||
player.filters.remove_filter(filter_tag="channelmix")
|
||||
await player.add_filter(voicelink.ChannelMix(tag="channelmix", left_to_left=left_to_left, right_to_right=right_to_right, left_to_right=left_to_right, right_to_left=right_to_left))
|
||||
await interaction.response.send_message(player.get_msg('channelmix').format(left_to_left, right_to_right, left_to_right, right_to_left))
|
||||
|
||||
@app_commands.command(
|
||||
name = "nightcore",
|
||||
description= "Add nightcore filter into your player."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def nightcore(self, interaction: discord.Interaction) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
await player.add_filter(voicelink.Timescale.nightcore())
|
||||
await interaction.response.send_message(player.get_msg('nightcore'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "8d",
|
||||
description= "Add 8D filter into your player."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def eightD(self, interaction: discord.Interaction) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
await player.add_filter(voicelink.Rotation.nightD())
|
||||
await interaction.response.send_message(player.get_msg('8d'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "vaporwave",
|
||||
description= "Add vaporwave filter into your player."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def vaporwave(self, interaction: discord.Interaction) -> None:
|
||||
player = await check_access(interaction)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
|
||||
await player.add_filter(voicelink.Timescale.vaporwave())
|
||||
await interaction.response.send_message(player.get_msg('vaporwave'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "cleareffect",
|
||||
description= "Clear all or specific sound effects."
|
||||
)
|
||||
@app_commands.describe(
|
||||
effect = "Remove a specific sound effects."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(effect=effect_autocomplete)
|
||||
async def cleareffect(self, interaction: discord.Interaction, effect: str = None) -> None:
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, "noPlayer"), ephemeral=True)
|
||||
|
||||
if interaction.user not in player.channel.members:
|
||||
if not interaction.user.guild_permissions.manage_guild:
|
||||
return await interaction.response.send_message(player.get_msg('notInChannel').format(interaction.user.mention, player.channel.mention), ephemeral=True)
|
||||
if effect:
|
||||
await player.remove_filter(effect)
|
||||
else:
|
||||
await player.reset_filter()
|
||||
await interaction.response.send_message(player.get_msg('cleareffect'))
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Effect(bot))
|
||||
48
cogs/nodes.py
Normal file
48
cogs/nodes.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import voicelink
|
||||
import asyncio
|
||||
import function
|
||||
|
||||
from os import getenv
|
||||
from discord.ext import commands
|
||||
|
||||
class Nodes(commands.Cog):
|
||||
"""Music Cog."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.voicelink = voicelink.NodePool()
|
||||
|
||||
bot.loop.create_task(self.start_nodes())
|
||||
|
||||
async def start_nodes(self) -> None:
|
||||
"""Connect and intiate nodes."""
|
||||
await self.bot.wait_until_ready()
|
||||
for n in function.nodes.values():
|
||||
try:
|
||||
await self.voicelink.create_node(bot=self.bot,
|
||||
spotify_client_id=getenv('SPOTIFY_CLIENT_ID'),
|
||||
spotify_client_secret=getenv('SPOTIFY_CLIENT_SECRET'),
|
||||
**n)
|
||||
except:
|
||||
print(f'Node {n["identifier"]} is not able to connect!')
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voicelink_track_end(self, player: voicelink.Player, track, _):
|
||||
await player.do_next()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voicelink_track_stuck(self, player: voicelink.Player, track, _):
|
||||
await asyncio.sleep(5)
|
||||
await player.do_next()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voicelink_track_exception(self, player: voicelink.Player, track, _):
|
||||
try:
|
||||
await player.context.send(f"{_} Please wait for 5 seconds.", delete_after=5)
|
||||
except:
|
||||
pass
|
||||
await asyncio.sleep(5)
|
||||
await player.do_next()
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Nodes(bot))
|
||||
430
cogs/playlist.py
Normal file
430
cogs/playlist.py
Normal file
@@ -0,0 +1,430 @@
|
||||
import discord
|
||||
import voicelink
|
||||
import re
|
||||
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from tldextract import extract
|
||||
from function import (
|
||||
time as ctime,
|
||||
get_playlist,
|
||||
create_account,
|
||||
checkroles,
|
||||
update_playlist,
|
||||
update_inbox,
|
||||
get_lang,
|
||||
playlist_name,
|
||||
embed_color
|
||||
)
|
||||
|
||||
from datetime import datetime
|
||||
from view import PlaylistView, InboxView
|
||||
|
||||
def assign_playlistId(existed:list) -> str:
|
||||
for i in range(200, 210):
|
||||
if str(i) not in existed:
|
||||
return str(i)
|
||||
|
||||
async def check_playlist_perms(userid:int, authorid:int, dId:str) -> dict:
|
||||
playlist = await get_playlist(authorid, 'playlist', dId)
|
||||
if not playlist or userid not in playlist['perms']['read']:
|
||||
return {}
|
||||
return playlist
|
||||
|
||||
async def check_playlist(interaction: discord.Interaction, name:str = None, full:bool = False, share:bool = True) -> dict:
|
||||
user = await get_playlist(interaction.user.id, 'playlist')
|
||||
if not user:
|
||||
return None
|
||||
await interaction.response.defer()
|
||||
if full:
|
||||
return user
|
||||
if not name:
|
||||
return {'playlist': user['200'], 'position': 1, 'id': "200"}
|
||||
|
||||
for index, data in enumerate(user, start=1):
|
||||
playlist = user[data]
|
||||
if playlist['name'].lower() == name:
|
||||
if playlist['type'] == 'share' and share:
|
||||
playlist = await check_playlist_perms(interaction.user.id, playlist['user'], playlist['referId'])
|
||||
if not playlist or interaction.user.id not in playlist['perms']['read']:
|
||||
return {'playlist': None, 'position': index, 'id': data}
|
||||
return {'playlist': playlist, 'position': index, 'id': data}
|
||||
return {'playlist': None, 'position': None, 'id': None}
|
||||
|
||||
async def search_playlist(url: str, requester: discord.Member, timeNeed=True):
|
||||
try:
|
||||
tracks = await voicelink.NodePool.get_node().get_tracks(url, requester=requester)
|
||||
tracks = {"name": tracks.name, "tracks": tracks.tracks}
|
||||
if timeNeed:
|
||||
time = 0
|
||||
for track in tracks['tracks']:
|
||||
time += track.length
|
||||
except:
|
||||
return None
|
||||
return tracks | ({'time': ctime(time)} if timeNeed else {})
|
||||
|
||||
async def connect_channel(interaction: discord.Interaction, channel: discord.VoiceChannel = None) -> voicelink.Player:
|
||||
try:
|
||||
channel = channel or interaction.user.voice.channel
|
||||
except:
|
||||
raise voicelink.VoicelinkException(get_lang(interaction.guild_id, 'noChannel'))
|
||||
|
||||
check = channel.permissions_for(interaction.guild.me)
|
||||
if check.connect == False or check.speak == False:
|
||||
raise voicelink.VoicelinkException(get_lang(interaction.guild_id, 'noPermission'))
|
||||
|
||||
player: voicelink.Player = await channel.connect(cls=voicelink.Player(interaction.client, channel, interaction))
|
||||
return player
|
||||
|
||||
class Playlist(commands.GroupCog, name="playlist"):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
self.description = "This is the Vocard playlist system. You can save your favorites and use Vocard to play on any server."
|
||||
|
||||
async def playlist_autocomplete(self, interaction: discord.Interaction, current: str) -> list:
|
||||
playlists = playlist_name.get(str(interaction.user.id), None)
|
||||
if not playlists:
|
||||
playlists_raw = await get_playlist(interaction.user.id, 'playlist')
|
||||
playlists = playlist_name[str(interaction.user.id)] = [value['name'] for value in playlists_raw.values()] if playlists_raw else []
|
||||
if current:
|
||||
return [ app_commands.Choice(name=p, value=p) for p in playlists if current in p ]
|
||||
return [ app_commands.Choice(name=p, value=p) for p in playlists ]
|
||||
|
||||
@app_commands.command(
|
||||
name = "play",
|
||||
description = "Play all songs from your favorite playlist."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "Input the name of your custom playlist",
|
||||
value = "Play the specific track from your custom playlist."
|
||||
)
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
@app_commands.guild_only()
|
||||
async def play(self, interaction: discord.Interaction, name: str = None, value: int = None) -> None:
|
||||
result = await check_playlist(interaction, name.lower() if name else None)
|
||||
if not result:
|
||||
return await create_account(interaction)
|
||||
if not result['playlist']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
rank, max_p, max_t = await checkroles(interaction.user.id)
|
||||
if result['position'] > max_p:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotAccess'), ephemeral=True)
|
||||
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
player = await connect_channel(interaction)
|
||||
|
||||
if result['playlist']['type'] == 'link':
|
||||
tracks = await search_playlist(result['playlist']['uri'], interaction.user, timeNeed=False)
|
||||
else:
|
||||
if not result['playlist']['tracks']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNoTrack').format(result['playlist']['name']), ephemeral=True)
|
||||
|
||||
playtrack = []
|
||||
for track in result['playlist']['tracks'][:max_t]:
|
||||
track['info']['length'] *= 1000
|
||||
playtrack.append(voicelink.Track(track_id=track['id'], info=track['info'], requester=interaction.user, spotify= True if extract(track['info']['uri']).domain == 'spotify' else False))
|
||||
tracks = {"name": result['playlist']['name'], "tracks": playtrack}
|
||||
|
||||
if not tracks:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNoTrack').format(result['playlist']['name']), ephemeral=True)
|
||||
|
||||
if value and 0 < value <= (len(tracks['tracks'])):
|
||||
tracks['tracks'] = [tracks['tracks'][value - 1]]
|
||||
for track in tracks['tracks']:
|
||||
await player.queue.put(track)
|
||||
|
||||
await interaction.followup.send(get_lang(interaction.guild_id, 'playlistPlay').format(result['playlist']['name'], len(tracks['tracks'][:max_t])))
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
@app_commands.command(
|
||||
name = "view",
|
||||
description = "List all your playlist and all songs in your favourite playlist."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def view(self, interaction: discord.Interaction) -> None:
|
||||
user = await check_playlist(interaction, full=True)
|
||||
if not user:
|
||||
return await create_account(interaction)
|
||||
rank, max_p, max_t = await checkroles(interaction.user.id)
|
||||
|
||||
results = []
|
||||
for index, data in enumerate(user, start=1):
|
||||
playlist = user[data]
|
||||
time = 0
|
||||
try:
|
||||
if playlist['type'] == 'link':
|
||||
tracks = await search_playlist(playlist['uri'], requester=interaction.user)
|
||||
results.append({'emoji':('🔒' if max_p < index else '🔗'), 'id': data, 'time': tracks['time'], 'name':playlist['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'type': playlist['type']})
|
||||
else:
|
||||
if share := playlist['type'] == 'share':
|
||||
playlist = await check_playlist_perms(interaction.user.id, playlist['user'], playlist['referId'])
|
||||
if not playlist:
|
||||
await update_playlist(interaction.user.id, {f"playlist.{data}":1}, mode=False)
|
||||
continue
|
||||
if playlist['type'] == 'link':
|
||||
tracks = await search_playlist(playlist['uri'], requester=interaction.user)
|
||||
results.append({'emoji':('🔒' if max_p < index else '🤝'), 'id': data, 'time': tracks['time'], 'name':user[data]['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'owner': user[data]['user'], 'type': 'share'})
|
||||
continue
|
||||
for track in playlist['tracks']:
|
||||
time += track['info']['length'] * 1000
|
||||
results.append({'emoji':('🔒' if max_p < index else ('🤝' if share else '❤️')), 'id': data, 'time': ctime(time), 'name':user[data]['name'], 'tracks': playlist['tracks'], 'perms': playlist['perms'], 'owner': user[data].get('user', None), 'type': user[data]['type']})
|
||||
except:
|
||||
results.append({'emoji': '⛔', 'id': data, 'time': '00:00', 'name': 'Error', 'tracks': [], 'type': 'error'})
|
||||
|
||||
embed=discord.Embed(title=get_lang(interaction.guild_id, 'playlistViewTitle').format(interaction.user.name),
|
||||
description='```%0s %4s %10s %10s %10s\n' % tuple(get_lang(interaction.guild_id, 'playlistViewHeaders')) + '\n'.join('%0s %3s. %10s %10s %10s'% (info['emoji'], info['id'], f"[{info['time']}]", info['name'], len(info['tracks'])) for info in results) + '```',
|
||||
color=embed_color)
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'playlistMaxP'), value=f"➥ {len(user)}/{max_p}", inline=True)
|
||||
embed.add_field(name=get_lang(interaction.guild_id, 'playlistMaxT'), value=f"➥ {max_t}", inline=True)
|
||||
embed.set_footer(text=get_lang(interaction.guild_id, 'playlistFooter'))
|
||||
|
||||
view = PlaylistView(embed, results, interaction.user)
|
||||
await interaction.followup.send(embed=embed, view=view, ephemeral=True)
|
||||
view.response = await interaction.original_response()
|
||||
|
||||
@app_commands.command(
|
||||
name = "create",
|
||||
description = "Create your custom playlist."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "Give a name to your playlist.",
|
||||
link = "Provide a playlist link if you are creating link playlist."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def create(self, interaction: discord.Interaction, name: str, link: str = None):
|
||||
if len(name) > 10:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'playlistOverText'), ephemeral=True)
|
||||
isLinkType = True if link else False
|
||||
rank, max_p, max_t = await checkroles(interaction.user.id)
|
||||
if isLinkType and rank != "Gold":
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'playlistCreateError'), ephemeral=True)
|
||||
user = await check_playlist(interaction, full=True)
|
||||
if not user:
|
||||
return await create_account(interaction)
|
||||
|
||||
if len(user) >= max_p:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'overPlaylistCreation').format(max_p), ephemeral=True)
|
||||
for data in user:
|
||||
if user[data]['name'].lower() == name.lower():
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistExists'), ephemeral=True)
|
||||
if isLinkType:
|
||||
tracks = await voicelink.NodePool.get_node().get_tracks(link, requester=interaction.user)
|
||||
if not isinstance(tracks, voicelink.Playlist):
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotInvaildUrl'), ephemeral=True)
|
||||
|
||||
playlist_name.pop(str(interaction.user.id), None)
|
||||
data = {'uri': link, 'perms': {'read': []}, 'name': name, 'type': 'link'} if isLinkType else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
|
||||
await update_playlist(interaction.user.id, { f"playlist.{assign_playlistId([data for data in user])}": data })
|
||||
await interaction.followup.send(get_lang(interaction.guild_id, 'playlistCreated'))
|
||||
|
||||
@app_commands.command(
|
||||
name = "delete",
|
||||
description = "Delete your custom playlist."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "The name of the playlist."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def delete(self, interaction: discord.Interaction, name: str):
|
||||
result = await check_playlist(interaction, name.lower(), share=False)
|
||||
if not result:
|
||||
return await create_account(interaction)
|
||||
if not result['playlist']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
if result['id'] == "200":
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistDeleteError'), ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] == 'share':
|
||||
await update_playlist(result['playlist']['user'], {f"playlist.{result['playlist']['referId']}.perms.read":interaction.user.id}, pull=True, mode=False)
|
||||
|
||||
playlist_name.pop(str(interaction.user.id), None)
|
||||
await update_playlist(interaction.user.id, {f"playlist.{result['id']}":1}, mode=False)
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistRemove').format(result['playlist']['name']))
|
||||
|
||||
@app_commands.command(
|
||||
name = "share",
|
||||
description = "Share your custom playlist with your friends."
|
||||
)
|
||||
@app_commands.describe(
|
||||
member = "The user id of your friend.",
|
||||
name = "The name of the playlist that you want to share."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def share(self, interaction: discord.Interaction, member: discord.Member, name: str):
|
||||
if member.id == interaction.user.id:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'playlistSendErrorPlayer'), ephemeral=True)
|
||||
if member.bot:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'playlistSendErrorBot'), ephemeral=True)
|
||||
result = await check_playlist(interaction, name.lower(), share=False)
|
||||
if not result:
|
||||
return await create_account(interaction)
|
||||
if not result['playlist']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] == 'share':
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistBelongs').format(result['playlist']['user']), ephemeral=True)
|
||||
if member.id in result['playlist']['perms']['read']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistShare').format(member), ephemeral=True)
|
||||
|
||||
receiver = await get_playlist(member.id)
|
||||
if not receiver:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'noPlaylistAcc').format(member))
|
||||
for mail in receiver['inbox']:
|
||||
if mail['sender'] == interaction.user.id and mail['referId'] == result['id']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistSent'), ephemeral=True)
|
||||
if len(receiver['inbox']) >= 10:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'inboxFull').format(member), ephemeral=True)
|
||||
|
||||
await update_inbox(member.id, {'sender': interaction.user.id, 'referId': result['id'], 'time': datetime.now(),'title': f'Playlist invitation from {interaction.user}', 'description': f"You are invited to use this playlist.\nPlaylist Name: {result['playlist']['name']}\nPlaylist type: {result['playlist']['type']}", 'type': 'invite'})
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'invitationSent').format(member))
|
||||
|
||||
@app_commands.command(
|
||||
name = "rename",
|
||||
description = "Rename your custom playlist."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "The name of your playlist.",
|
||||
newname = "The new name of your playlist."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def rename(self, interaction: discord.Interaction, name: str, newname: str) -> None:
|
||||
if len(newname) > 10:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'playlistOverText'), ephemeral=True)
|
||||
if name.lower() == newname.lower():
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'playlistSameName'), ephemeral=True)
|
||||
user = await check_playlist(interaction, full=True)
|
||||
if not user:
|
||||
return await create_account(interaction)
|
||||
found, id = False, 0
|
||||
for data in user:
|
||||
if user[data]['name'].lower() == name.lower():
|
||||
found, id = True, data
|
||||
if user[data]['name'].lower() == newname.lower():
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistExists'), ephemeral=True)
|
||||
|
||||
if not found:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
|
||||
playlist_name.pop(str(interaction.user.id), None)
|
||||
await update_playlist(interaction.user.id, {f'playlist.{id}.name': newname})
|
||||
await interaction.followup.send(get_lang(interaction.guild_id, 'playlistRenamed').format(name, newname))
|
||||
|
||||
@app_commands.command(
|
||||
name = "inbox",
|
||||
description = "Show your playlist invitation."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
async def inbox(self, interaction: discord.Interaction) -> None:
|
||||
user = await get_playlist(interaction.user.id)
|
||||
if user is None:
|
||||
return await create_account(interaction)
|
||||
if not user['inbox']:
|
||||
return await interaction.response.send_message(get_lang(interaction.guild_id, 'inboxNoMsg'), ephemeral=True)
|
||||
|
||||
inbox = user['inbox'].copy()
|
||||
view = InboxView(interaction.user.name, user['inbox'])
|
||||
await interaction.response.send_message(embed=view.build_embed(), view=view, ephemeral=True)
|
||||
view.response = await interaction.original_response()
|
||||
await view.wait()
|
||||
|
||||
if inbox == user['inbox']:
|
||||
return
|
||||
updateData, dId = {}, {dId for dId in user["playlist"]}
|
||||
for data in view.newplaylist[:(5 - len(user['playlist']))]:
|
||||
addId = assign_playlistId(dId)
|
||||
await update_playlist(data['sender'], {f"playlist.{data['referId']}.perms.read": interaction.user.id}, push=True)
|
||||
updateData[f'playlist.{addId}'] = {'user':data['sender'], 'referId': data['referId'], 'name': f"Share{data['time'].strftime('%M%S')}", 'type': 'share'}
|
||||
dId.add(addId)
|
||||
|
||||
playlist_name.pop(str(interaction.user.id), None)
|
||||
await update_playlist(interaction.user.id, updateData | {'inbox':view.inbox})
|
||||
|
||||
@app_commands.command(
|
||||
name = "add",
|
||||
description = "Add tracks in to your custom playlist."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "The name of the playlist.",
|
||||
query = "Input a query or a searchable link."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def add(self, interaction: discord.Interaction, name: str, query: str) -> None:
|
||||
result = await check_playlist(interaction, name.lower(), share=False)
|
||||
if not result:
|
||||
return await create_account(interaction)
|
||||
if not result['playlist']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
if result['playlist']['type'] in ['share', 'link']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotAllow'), ephemeral=True)
|
||||
rank, max_p, max_t = await checkroles(interaction.user.id)
|
||||
|
||||
if len(result['playlist']['tracks']) >= max_t:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistLimitTrack').format(max_t), ephemeral=True)
|
||||
|
||||
results = await voicelink.NodePool.get_node().get_tracks(query, requester = interaction.user)
|
||||
if not results:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'noTrackFound'))
|
||||
if isinstance(results, voicelink.Playlist):
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistPlaylistLink'), ephemeral=True)
|
||||
if results[0].is_stream:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistStream'), ephemeral=True)
|
||||
|
||||
await update_playlist(interaction.user.id, {f'playlist.{result["id"]}.tracks':{'id': results[0].track_id, 'info': {'identifier': results[0].identifier,
|
||||
'author': results[0].author,
|
||||
'length': results[0].length / 1000,
|
||||
'title': results[0].title,
|
||||
'uri': results[0].uri}}}, push=True)
|
||||
await interaction.followup.send(get_lang(interaction.guild_id, 'playlistAdded').format(results[0].title, interaction.user, result['playlist']['name']))
|
||||
|
||||
@app_commands.command(
|
||||
name = "remove",
|
||||
description = "Remove song from your favorite playlist."
|
||||
)
|
||||
@app_commands.describe(
|
||||
name = "The name of the playlist.",
|
||||
position = "Input a position from the playlist to be removed."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def remove(self, interaction: discord.Interaction, name: str, position: int):
|
||||
result = await check_playlist(interaction, name.lower(), share=False)
|
||||
if not result:
|
||||
return await create_account(interaction)
|
||||
if not result['playlist']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
if result['playlist']['type'] in ['link', 'share']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotAllow'), ephemeral=True)
|
||||
if not 0 < position <= len(result['playlist']['tracks']):
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistPositionNotFound').format(position, name))
|
||||
|
||||
await update_playlist(interaction.user.id, {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}, pull=True, mode=False)
|
||||
await interaction.followup.send(get_lang(interaction.guild_id, 'playlistRemoved').format(result['playlist']['tracks'][position - 1]['info']['title'], interaction.user, name))
|
||||
|
||||
@app_commands.command(
|
||||
name = "clear",
|
||||
description = "Remove all songs from your favorite playlist."
|
||||
)
|
||||
@app_commands.guild_only()
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def clear(self, interaction: discord.Interaction, name: str) -> None:
|
||||
result = await check_playlist(interaction, name.lower(), share=False)
|
||||
if not result:
|
||||
return await create_account(interaction)
|
||||
if not result['playlist']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotFound').format(name), ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] in ['link', 'share']:
|
||||
return await interaction.followup.send(get_lang(interaction.guild_id, 'playlistNotAllow'), ephemeral=True)
|
||||
|
||||
await update_playlist(interaction.user.id, {f'playlist.{result["id"]}.tracks': []})
|
||||
await interaction.followup.send(get_lang(interaction.guild_id, 'playlistClear').format(name))
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Playlist(bot))
|
||||
81
cogs/task.py
Normal file
81
cogs/task.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import function
|
||||
import voicelink
|
||||
import discord
|
||||
|
||||
from discord.ext import commands, tasks
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
class Task(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.player_check.start()
|
||||
self.cache_cleaner.start()
|
||||
|
||||
def cog_unload(self):
|
||||
self.player_check.cancel()
|
||||
self.cache_cleaner.cancel()
|
||||
|
||||
@tasks.loop(minutes=5.0)
|
||||
async def player_check(self):
|
||||
if not self.bot.voice_clients:
|
||||
return
|
||||
|
||||
player: voicelink.Player
|
||||
for player in self.bot.voice_clients:
|
||||
try:
|
||||
if not player.channel or not player.context or not player.guild:
|
||||
await player.teardown()
|
||||
continue
|
||||
except:
|
||||
await player.teardown()
|
||||
continue
|
||||
|
||||
members = player.channel.members
|
||||
if (not player.is_playing and player.queue.is_empty) or not any(False if member.bot or member.voice.self_deaf else True for member in members):
|
||||
if not player.settings.get('24/7', False):
|
||||
await player.teardown()
|
||||
continue
|
||||
else:
|
||||
if not player.is_paused:
|
||||
await player.set_pause(True)
|
||||
else:
|
||||
if not player.guild.me:
|
||||
await player.teardown()
|
||||
continue
|
||||
elif not player.guild.me.voice:
|
||||
await player.connect(timeout=0.0, reconnect=True)
|
||||
|
||||
try:
|
||||
if player.dj not in members:
|
||||
for m in members:
|
||||
if not m.bot:
|
||||
player.dj = m
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
@tasks.loop(hours=12.0)
|
||||
async def cache_cleaner(self):
|
||||
function.lang_guilds.clear()
|
||||
function.playlist_name.clear()
|
||||
|
||||
if function.error_log:
|
||||
report_channel = self.bot.get_channel(function.report_channel_id)
|
||||
if report_channel:
|
||||
errorText = ""
|
||||
for guildId, error in function.error_log.items():
|
||||
errorText += f"Guild ID: {guildId}\n" + "-" * 30 + "\n"
|
||||
for index, (key, value) in enumerate(error.items() , start=1):
|
||||
errorText += f"Error No: {index}, Time: {datetime.fromtimestamp(key)}\n" + value + "-" * 30 + "\n\n"
|
||||
|
||||
buffer = BytesIO(errorText.encode('utf-8'))
|
||||
tempFile = discord.File(buffer, filename='report.txt')
|
||||
try:
|
||||
await report_channel.send(content=f"Report Before: <t:{round(datetime.timestamp(datetime.now()))}:F>", file=tempFile)
|
||||
except Exception as e:
|
||||
print(f"Report could not be sent (Reason: {e})")
|
||||
function.error_log.clear()
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(Task(bot))
|
||||
218
function.py
Normal file
218
function.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import discord
|
||||
import json
|
||||
import aiohttp
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from random import choice
|
||||
from time import strptime
|
||||
from pymongo import MongoClient
|
||||
|
||||
if not os.path.exists("./.env"):
|
||||
print("Error .env file not set!")
|
||||
exit()
|
||||
|
||||
if not os.path.exists("./settings.json"):
|
||||
print("Error: Settings file not set!")
|
||||
exit()
|
||||
|
||||
#-------------- API Clients --------------
|
||||
load_dotenv() #Load .env settings
|
||||
MONGODB_NAME = os.getenv('MONGODB_NAME')
|
||||
youtube_api_key = os.getenv('YOUTUBE_API_KEY')
|
||||
mongodb = MongoClient(os.getenv('MONGODB_URL'))
|
||||
collection = mongodb[MONGODB_NAME]['Settings']
|
||||
Playlist = mongodb[MONGODB_NAME]['Playlist']
|
||||
|
||||
#--------------- Cache Var ---------------
|
||||
invite_link = "https://discord.gg/wRCgB7vBQv" #Template of invite link
|
||||
embed_color = None
|
||||
report_channel_id = int(os.getenv("BUG_REPORT_CHANNEL_ID"))
|
||||
emoji_source_raw = {} #Stores all source emoji for track
|
||||
error_log = {} #Stores error that not a Voicelink Exception
|
||||
bot_access_user = [] #Stores bot access user id
|
||||
langs = {} #Stores all the languages in ./langs
|
||||
lang_guilds = {} #Cache guild language
|
||||
local_langs = {} #Stores all the localization languages in ./local_langs
|
||||
playlist_name = {} #Cache the user's playlist name
|
||||
|
||||
#----------------- Nodes -----------------
|
||||
nodes = {}
|
||||
|
||||
#-------------- Vocard Functions --------------
|
||||
def get_settings(guildid:int):
|
||||
settings = collection.find_one({"_id":guildid})
|
||||
if not settings:
|
||||
collection.insert_one({"_id":guildid})
|
||||
return {}
|
||||
return settings
|
||||
|
||||
def update_settings(guildid:int, data, mode="Set"):
|
||||
if mode == "Set":
|
||||
collection.update_one({"_id":guildid}, {"$set":data})
|
||||
elif mode == "Delete":
|
||||
collection.update_one({"_id":guildid}, {"$unset":data})
|
||||
elif mode == "Add":
|
||||
collection.update_one({"_id":guildid}, {"$push":data})
|
||||
elif mode == "Remove":
|
||||
collection.update_one({"_id":guildid}, {"$pull":data})
|
||||
return
|
||||
|
||||
def langs_setup():
|
||||
for language in os.listdir('./langs'):
|
||||
if language.endswith('.json'):
|
||||
with open(f'./langs/{language}', encoding="utf8") as json_file:
|
||||
lang = json.load(json_file)
|
||||
|
||||
langs[language[:-5]] = lang
|
||||
|
||||
for language in os.listdir('./local_langs'):
|
||||
if language.endswith('.json'):
|
||||
with open(f'./local_langs/{language}', encoding="utf8") as json_file:
|
||||
lang = json.load(json_file)
|
||||
|
||||
local_langs[language[:-5]] = lang
|
||||
return
|
||||
|
||||
def settings_setup():
|
||||
with open('./settings.json', encoding="utf8") as json_file:
|
||||
rawSettings = json.load(json_file)
|
||||
|
||||
global nodes, embed_color, bot_access_user, emoji_source_raw
|
||||
nodes = rawSettings.get("nodes", {})
|
||||
embed_color = int(rawSettings.get("embed_color", "0xb3b3b3"), 16)
|
||||
bot_access_user = rawSettings.get("bot_access_user", [])
|
||||
emoji_source_raw = rawSettings.get("emoji_source_raw", {})
|
||||
|
||||
def get_lang(guildid:int, key:str):
|
||||
lang = lang_guilds.get(guildid)
|
||||
if not lang:
|
||||
settings = get_settings(guildid)
|
||||
lang = lang_guilds[guildid] = settings.get('lang', 'EN')
|
||||
|
||||
return langs.get(lang, langs["EN"])[key]
|
||||
|
||||
async def requests_api(url: str):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
resp = await session.get(url)
|
||||
if resp.status != 200:
|
||||
return False
|
||||
|
||||
return await resp.json(encoding="utf-8")
|
||||
|
||||
async def create_account(interaction):
|
||||
if not interaction.user:
|
||||
return
|
||||
from view import CreateView
|
||||
view = CreateView()
|
||||
embed=discord.Embed(title="Do you want to create an account on Vocard?", color=embed_color)
|
||||
embed.description = f"> Plan: `Default` | `1` Playlist | `20` tracks in each playlist\n> You are able to upgrade the plan by Type `-shop` on [Vocard support discord]({invite_link})."
|
||||
embed.add_field(name="Terms of Service:", value=" ➥ We assure you that all your data on Vocard will not be disclosed to any third party\n"
|
||||
" ➥ We will not perform any data analysis on your data\n"
|
||||
" ➥ You have the right to immediately stop the services we offer to you\n"
|
||||
" ➥ Please do not abuse our services, such as affecting other users\n"
|
||||
" ➥ Join our support server to get free extra custom playlist and extra 20 tracks per playlist\n", inline=False)
|
||||
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||||
view.response = await interaction.original_response()
|
||||
await view.wait()
|
||||
if view.value:
|
||||
try:
|
||||
Playlist.insert_one({'_id':interaction.user.id, 'playlist': {'200':{'tracks':[],'perms':{ 'read': [], 'write':[], 'remove': []},'name':'Favourite', 'type':'playlist' }},'inbox':[] })
|
||||
except:
|
||||
pass
|
||||
|
||||
async def get_playlist(userid:int, dType:str=None, dId:str=None):
|
||||
user = Playlist.find_one({"_id":userid}, {"_id": 0})
|
||||
if not user:
|
||||
return None
|
||||
if dType:
|
||||
if dId and dType == "playlist":
|
||||
return user[dType][dId] if dId in user[dType] else None
|
||||
return user[dType]
|
||||
return user
|
||||
|
||||
async def update_playlist(userid:int, data:dict=None, push=False, pull=False, mode=True):
|
||||
if mode is True:
|
||||
if push:
|
||||
return Playlist.update_one({"_id":userid}, {"$push": data})
|
||||
Playlist.update_one({"_id":userid}, {"$set": data})
|
||||
else:
|
||||
if pull:
|
||||
return Playlist.update_one({"_id":userid}, {"$pull": data})
|
||||
Playlist.update_one({"_id":userid}, {"$unset": data})
|
||||
return
|
||||
|
||||
async def update_inbox(userid:int, data:dict):
|
||||
return Playlist.update_one({"_id":userid}, {"$push":{'inbox':data}})
|
||||
|
||||
async def checkroles(userid:int):
|
||||
rank, max_p, max_t = 'Normal', 5, 500
|
||||
|
||||
return rank, max_p, max_t
|
||||
|
||||
async def similar_track(player):
|
||||
trackids = [ track.identifier for track in player.queue.history(incTrack=True) if track.source == 'youtube' ]
|
||||
randomTrack = choice(player.queue.history(incTrack=True)[-10:])
|
||||
tracks = []
|
||||
|
||||
if randomTrack.spotify:
|
||||
tracks = await player.spotifyRelatedTrack(seed_artists=randomTrack.artistId[0], seed_tracks=randomTrack.track_id)
|
||||
else:
|
||||
if randomTrack.source != 'youtube':
|
||||
return False
|
||||
|
||||
request_url = "https://youtube.googleapis.com/youtube/v3/search?part={part}&relatedToVideoId={videoId}&type={type}&videoCategoryId={videoCategoryId}&key={key}".format(
|
||||
part="snippet",
|
||||
videoId=randomTrack.identifier,
|
||||
type="video",
|
||||
videoCategoryId="10",
|
||||
key=youtube_api_key
|
||||
)
|
||||
|
||||
try:
|
||||
data = await requests_api(request_url)
|
||||
if not data:
|
||||
return False
|
||||
|
||||
for item in data['items']:
|
||||
if 'snippet' not in item:
|
||||
continue
|
||||
if item['id']['videoId'] not in trackids:
|
||||
tracks = await player.get_tracks(f"https://www.youtube.com/watch?v={item['id']['videoId']}", requester=player._bot.user)
|
||||
break
|
||||
except:
|
||||
return False
|
||||
|
||||
if tracks:
|
||||
for track in tracks:
|
||||
await player.queue.put(track)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def time(millis:int):
|
||||
seconds=(millis/1000)%60
|
||||
minutes=(millis/(1000*60))%60
|
||||
hours=(millis/(1000*60*60))%24
|
||||
if hours > 1:
|
||||
return "%02d:%02d:%02d" % (hours, minutes, seconds)
|
||||
else:
|
||||
return "%02d:%02d" % (minutes, seconds)
|
||||
|
||||
def formatTime(number:str):
|
||||
try:
|
||||
try:
|
||||
num = strptime(number, '%M:%S')
|
||||
except ValueError:
|
||||
try:
|
||||
num = strptime(number, '%S')
|
||||
except ValueError:
|
||||
num = strptime(number, '%H:%M:%S')
|
||||
except:
|
||||
return None
|
||||
|
||||
return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000
|
||||
|
||||
def emoji_source(emoji:str):
|
||||
return emoji_source_raw.get(emoji.lower(), "🔗");
|
||||
|
||||
184
langs/CH.json
Normal file
184
langs/CH.json
Normal file
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"unknownException": "⚠️ 運行命令時出錯! 請稍後重試或加入我們的不和諧服務器以獲得進一步支持。",
|
||||
"enabled": "啟用",
|
||||
"disabled": "禁用",
|
||||
|
||||
"nodeReconnect": "請在節點重新連接後再試一次。",
|
||||
"noChannel": "沒有可連接的語音通道。 請提供一個頻道或加入一個頻道。",
|
||||
"alreadyConnected": "已連接到語音通道。",
|
||||
"noPermission": "對不起! 我無權加入您的語音頻道或在您的語音頻道中發言。",
|
||||
"noPlaySource": "找不到任何可播放的資源!",
|
||||
"noPlayer": "在此服務器上未找到任何播放器。",
|
||||
"notVote": "此命令需要您的投票!輸入 `/vote` 獲取更多信息。",
|
||||
"languageNotFound": "未找到語言包,請選擇一個存在的語言包。",
|
||||
"changedLanguage": "已成功更改為`{0}`語言包。",
|
||||
"setDJ": "將 dj 設置為 {0}。",
|
||||
"setqueue": "將隊列模式設置為 `{0}`。",
|
||||
"247": "現在您有了 `{0}` 24/7 模式。",
|
||||
"bypassVote": "現在您 `{0}` 了投票系統。",
|
||||
"setVolume": "將音量設置為 `{0}`%",
|
||||
"togglecontroller": "現在您已經 `{0}` 了音樂控制器。",
|
||||
"toggleDuplicateTrack": "現在您已經 `{0}` 以防止隊列中的重複曲目。",
|
||||
"settingsMenu": "服務器設置 | {0}",
|
||||
"settingsTitle": "❤️ 基本信息:",
|
||||
"settingsValue": "語言:`{0}`\n啟用音樂控制器:`{1}`\nDJ角色:{2}\n投票繞過:`{3}`\n24/7:`{4}`\n默認音量:`{5}%`\n播放時間: `{6}`\n捐贈服務器:`{7}`",
|
||||
"settingsTitle2": "🔗 隊列信息:",
|
||||
"settingsValue2": "隊列模式: `{0}`\n最大曲目: `{1}`\n允許重複軌道: `{2}`",
|
||||
"settingsPermTitle": "✨ 權限:",
|
||||
"settingsPermValue": "{0} 管理員\n{1} 管理公會\n{2} 管理頻道\n{3} 管理消息",
|
||||
"pingTitle1": "機器人信息:",
|
||||
"pingTitle2": "播放器信息:",
|
||||
"pingfield1": "```分片 ID:{0}/{1}\n分片延遲:{2:.3f}s {3}\n地區:{4}```",
|
||||
"pingfield2": "```節點:{0} - {1:.3f}s\n播放機:{2}\n語音區域:{3}```",
|
||||
"karaoke": "您將level設置為 **{0}**,monoLevel設置為 **{1}**,filterBand設置為 **{2}**,filterWidth設置為 **{3}**",
|
||||
"tremolo&vibrato": "您將頻率設置為 **{0}** 並將深度設置為 **{1}**",
|
||||
"rotation": "您將旋轉頻率設置為 **{0}**",
|
||||
"distortion": "您已啟用失真。",
|
||||
"lowpass": "您將 lowPass 設置為 **{0}**",
|
||||
"channelmix": "您已更新 channelMix。 從左到左:**{0}**,從右到右:**{1}**,從左到右:**{2}**,從右到左:** {3}**",
|
||||
"nightcore": "您添加了夜景音效。",
|
||||
"8d": "您添加了 8D 音效。",
|
||||
"vaporwave": "您添加了蒸汽波音效。",
|
||||
"cleareffect": "音效已經清除!",
|
||||
"FilterTagAlreadyInUse": "這個音效已經在使用了! 請使用 /cleareffect <Tag> 將其刪除。",
|
||||
|
||||
"playlistViewTitle": "📜{0}的所有播放列表",
|
||||
"playlistViewHeaders": [" ", "ID:", "時間:", "姓名:", "曲目:"],
|
||||
"playlistMaxP": "最大播放列表:",
|
||||
"playlistMaxT": "最大曲目:",
|
||||
"playlistFooter": "輸入 /playlist play [playlist] 將播放列表添加到隊列中。",
|
||||
"playlistNotFound": "未找到播放列表 [`{0}`]。 鍵入 /playlist view 以查看您的所有播放列表。",
|
||||
"playlistNotAccess": "對不起!您無權訪問此播放列表!",
|
||||
"playlistNoTrack": "對不起!播放列表 [`{0}`] 中沒有曲目。",
|
||||
"playlistNotAllow": "鏈接和共享的播放列表不允許使用此命令。",
|
||||
"playlistPlay": "將帶有 `{1}` 歌曲的播放列表 [`{0}`] 添加到隊列中。",
|
||||
"playlistOverText": "對不起!播放列表的名稱不能超過 10 個字符。",
|
||||
"playlistSameName": "對不起!此名稱不能與您的新名稱相同。",
|
||||
"playlistDeleteError": "您不能刪除默認播放列表。",
|
||||
"playlistRemove": "您已刪除播放列表 [`{0}`]。",
|
||||
"playlistSendErrorPlayer": "對不起!您無法向自己發送邀請。",
|
||||
"playlistSendErrorBot": "對不起!您無法向機器人發送邀請。",
|
||||
"playlistBelongs": "對不起!此播放列表屬於 <@{0}>。",
|
||||
"playlistShare": "對不起!此播放列表已與 {0} 共享。",
|
||||
"playlistSent": "對不起!您之前已經發送過邀請。",
|
||||
"noPlaylistAcc": "{0} 沒有創建播放列表帳戶。",
|
||||
"playlistCreateError": "對不起!您無法創建此類播放列表!",
|
||||
"overPlaylistCreation": "您只能創建`{0}`個播放列表!",
|
||||
"playlistExists": "播放列表 [`{0}`] 已存在。",
|
||||
"playlistNotInvaildUrl": "請輸入有效鏈接或公開的 Spotify 或 youtube 播放列錶鍊接。",
|
||||
"playlistCreated": "您已創建`{0}`播放列表。 鍵入 /playlist view 以獲取更多信息。",
|
||||
"playlistRenamed": "您已將`{0}`重命名為`{1}`。",
|
||||
"playlistLimitTrack": "你已經達到極限了! 您只能將`{0}`歌曲添加到播放列表。",
|
||||
"playlistPlaylistLink": "您不能使用播放列表鍊接。",
|
||||
"playlistStream": "您不能將流媒體視頻添加到您的播放列表中。",
|
||||
"playlistPositionNotFound": "無法從您的播放列表 [`{1}`] 中找到位置 `{0}`!",
|
||||
"playlistRemoved": "👋 從 {1} 的播放列表 [`{2}`] 中刪除了 **{0}**。",
|
||||
"playlistClear": "您已成功清除播放列表 [`{0}`]。",
|
||||
"playlistView": "播放列表查看器",
|
||||
"playlistViewDesc": "```姓名 | ID:{0} | {1}\n總曲目:{2}\n創立者:{3}\n類型:{4}\n```",
|
||||
"playlistViewPermsValue": "📖 閱覽:✓ ✍🏽 添加:{0} 🗑️ 刪除:{1}",
|
||||
"playlistViewPermsValue2": "📖 閱覽:{0}",
|
||||
"playlistViewTrack": "曲目",
|
||||
"playlistViewPage": "頁面:{0}/{1} | 總時長:{2}",
|
||||
"inboxFull": "對不起!{0} 的收件箱已滿。",
|
||||
"inboxNoMsg": "您的收件箱中沒有郵件。",
|
||||
"invitationSent": "邀請已發送至 {0}。",
|
||||
|
||||
"notInChannel": "{0},您必須在 {1} 中才能使用語音命令。 如果您有聲音,請重新加入!",
|
||||
"noTrackPlaying": "目前沒有歌曲正在播放",
|
||||
"noTrackFound": "沒有找到該查詢的歌曲! 請提供有效的網址。",
|
||||
"noLinkSupport": "搜索命令不支持鏈接!",
|
||||
"voted": "你投票了!",
|
||||
"missingPerms_pos": "只有 DJ 或管理員可以更改曲目位置。",
|
||||
"missingPerms_mode": "只有 DJ 或管理員可以切換循環模式。",
|
||||
"missingPerms_queue": "只有 DJ 或管理員可以從隊列中刪除曲目。",
|
||||
"missingPerms_autoplay": "只有 DJ 或管理員可以啟用或禁用自動播放模式!",
|
||||
"missingPerms_function": "只有 DJ 或管理員可以使用此功能。",
|
||||
"timeFormatError": "時間格式不正確。 示例: 2:42 或 12:39:31",
|
||||
"lyricsNotFound": "找不到歌詞。 鍵入 /lyrics <歌名> <作者> 以查找歌詞。",
|
||||
"missingTrackInfo": "缺少一些曲目信息。",
|
||||
"noVoiceChannel": "未找到语音频道!",
|
||||
|
||||
"playlistAddError": "您不能將直播視頻添加到您的播放列表中!",
|
||||
"playlistAddError2": "曲目添加到播放列表時出現問題!",
|
||||
"playlistlimited": "你已經達到極限了! 您只能將 {0} 首歌曲添加到您的播放列表。",
|
||||
"playlistrepeated": "您的播放列表中已經有相同的曲目!",
|
||||
"playlistAdded": "❤️ 將 **{0}** 添加到 {1} 的播放列表 [`{2}`]!",
|
||||
|
||||
"playerAuthor": "音樂控制器 | {0}",
|
||||
"playerDesc": "**現在播放: ```[{0}]```\n鏈接: [點擊我]({1}) | 請求者: {2} | DJ: {3}**",
|
||||
"playerFooter": "隊列長度: {0} | 時長: {1} | 音量: {2}% {3}",
|
||||
"playerFooter2": "| 重複: {0}",
|
||||
"playerDropdown": "選擇要跳到的歌曲...",
|
||||
|
||||
"buttonBack": "後退",
|
||||
"buttonPause": "暫停",
|
||||
"buttonResume": "恢復",
|
||||
"buttonSkip": "跳過",
|
||||
"buttonLeave": "離開",
|
||||
|
||||
"nowplayingDesc": "**現在播放:**\n```{0}```",
|
||||
"nowplayingField": "接下來:",
|
||||
"nowplayingLink": "在 {0} 上收聽",
|
||||
|
||||
"connect": "已連接到 {0}",
|
||||
|
||||
"live": "直播",
|
||||
"playlistLoad": " 🎶 將帶有`{1}`歌曲的播放列表 **{0}** 添加到隊列中。",
|
||||
"trackLoad": "添加了 **{1}** 的 **{0}** (`{2}`) 以開始播放。\n",
|
||||
"trackLoad_pos": "將 **{1}** 的 **{0}** (`{2}`) 添加到隊列中的位置 **{3}**\n",
|
||||
|
||||
"searchTitle": "搜索查詢: {0}",
|
||||
"searchDesc": "➥ 平台: {0} **{1}**\n➥ 結果: **{2}**\n\n{3}",
|
||||
"searchWait": "選擇要添加到隊列中的歌曲。",
|
||||
"searchTimeout": "搜索超時。請稍後再試。",
|
||||
"searchSuccess": "歌曲添加到隊列中。",
|
||||
|
||||
"queueTitle": "即將到來的隊列:",
|
||||
"historyTitle": "歷史隊列:",
|
||||
"viewTitle": "音樂隊列",
|
||||
"viewDesc": "**現在播放: [點擊我]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "頁: {0}/{1} | 總時長: {2}",
|
||||
|
||||
"pauseError": "播放器已暫停。",
|
||||
"pauseVote": "{0} 已投票暫停這首歌。 [{1}/{2}]",
|
||||
"paused": "`{0}` 已暫停播放器。",
|
||||
"resumeError": "播放器沒有暫停。",
|
||||
"resumeVote": "{0} 已投票恢復這首歌。 [{1}/{2}]",
|
||||
"resumed": "`{0}` 已恢復播放器。",
|
||||
"shuffleError": "在隨機播放之前將更多歌曲添加到隊列中。",
|
||||
"shuffleVote": "{0} 已投票改組隊列。 [{1}/{2}]",
|
||||
"shuffled": "隊列被打亂了。",
|
||||
"skipError": "沒有歌曲可以跳過。",
|
||||
"skipVote": "{0} 已投票跳過這首歌。 [{1}/{2}]",
|
||||
"skipped": "`{0}` 跳過了這首歌。",
|
||||
|
||||
"backVote": "{0} 已投票跳到上一首歌曲。 [{1}/{2}]",
|
||||
"backed": "`{0}` 已跳至上一首歌曲。",
|
||||
|
||||
"leaveVote": "{0} 已投票阻止播放器。 [{1}/{2}]",
|
||||
"left": "`{0}` 已停止播放器。",
|
||||
|
||||
"seek": "將播放器設置為 **{0}**",
|
||||
"repeat": "重複模式已設置為 `{0}`",
|
||||
"cleared": "清除了所有曲目 `{0}`",
|
||||
"removed": "從隊列中刪除了 `{0}` 曲目。",
|
||||
"forward": "將播放器轉發到 **{0}**",
|
||||
"rewind": "將播放器倒回至 **{0}**",
|
||||
"replay": "重播當前歌曲。",
|
||||
"moved": "{0} 和 {1} 被交換",
|
||||
"autoplay": "現在是自動播放模式 **{0}**",
|
||||
|
||||
"notdj": "你不是 DJ。 當前 DJ 是 {0}。",
|
||||
"djToMe": "您不能將 DJ 轉移給您自己或機器人。",
|
||||
"djnotinchannel": "`{0}` 不在語音頻道中。",
|
||||
"djswap": "您已將 DJ 的角色轉移到 `{0}`。",
|
||||
|
||||
"chaptersDropdown": "選擇要跳到的章節...",
|
||||
"noChaptersFound": "沒有找到章節!",
|
||||
"chatpersNotSupport": "此命令僅支持 Youtube 視頻!",
|
||||
|
||||
"voicelinkQueueFull":"抱歉,您已達到隊列中 `{0}` 首曲目的上限!",
|
||||
"voicelinkOutofList": "請提供有效的曲目索引!",
|
||||
"voicelinkDuplicateTrack": "抱歉,此曲目已在隊列中。"
|
||||
}
|
||||
185
langs/DE.json
Normal file
185
langs/DE.json
Normal file
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"unknownException": "⚠️ Etwas falsch beim Ausführen des Befehls! Bitte versuchen Sie es später erneut oder treten Sie unserem Discord-Server bei, um weitere Unterstützung zu erhalten.",
|
||||
"enabled": "aktiviert",
|
||||
"disabled": "deaktiviert",
|
||||
|
||||
"nodeReconnect": "Bitte versuche es erneut! Nachdem sich der Knoten wieder verbunden hat.",
|
||||
"noChannel": "Kein Sprachkanal zum Verbinden. Bitte stellen Sie entweder einen zur Verfügung oder schließen Sie sich einem an.",
|
||||
"alreadyConnected": "Bereits mit einem Sprachkanal verbunden.",
|
||||
"noPermission": "Es tut uns leid! Ich bin nicht berechtigt, Ihrem Sprachkanal beizutreten oder darin zu sprechen.",
|
||||
"noPlaySource": "Kann keine abspielbaren Quellen finden!",
|
||||
"noPlayer": "Auf diesem Server wurde kein Spieler gefunden.",
|
||||
"notDonate": "Dieser Befehl erfordert, dass der Server ein Spendenserver ist!",
|
||||
"notVote": "Dieser Befehl erfordert Ihre Stimme! Geben Sie `/vote` ein, um weitere Informationen zu erhalten.",
|
||||
"languageNotFound": "Kein Sprachpaket gefunden, bitte wählen Sie ein vorhandenes Sprachpaket aus.",
|
||||
"changedLanguage": "Erfolgreich auf das Sprachpaket `{0}` geändert.",
|
||||
"setDJ": "Stellen Sie den DJ auf {0}.",
|
||||
"setqueue": "Stellen Sie den Warteschlangenmodus auf `{0}` ein.",
|
||||
"247": "Jetzt haben Sie den 24/7-Modus von `{0}`.",
|
||||
"bypassVote": "Jetzt haben Sie das Abstimmungssystem `{0}`.",
|
||||
"setVolume": "Stellen Sie die Lautstärke auf `{0}` % ein",
|
||||
"togglecontroller": "Jetzt haben Sie `{0}` den Musikcontroller.",
|
||||
"toggleDuplicateTrack": "Jetzt haben Sie `{0}`, um doppelte Tracks in der Warteschlange zu verhindern.",
|
||||
"settingsMenu": "Servereinstellungen | {0}",
|
||||
"settingsTitle": "❤️ Grundlegende Informationen:",
|
||||
"settingsValue": "Sprache: `{0}`\nMusik-Controller aktivieren: `{1}`\nDJ-Rolle: {2}\nAbstimmungsumgehung: `{3}`\nRund um die Uhr: `{4}`\nStandardvolumen: `{5} %`\nSpielzeit: `{6}`\nDonate-Server: `{7}`",
|
||||
"settingsTitle2": "🔗 Warteschlangeninformationen:",
|
||||
"settingsValue2": "Warteschlangenmodus: `{0}`\nMax Lied: `{1}`\nDoppelte Spur zulassen: `{2}`",
|
||||
"settingsPermTitle": "✨ Berechtigungen:",
|
||||
"settingsPermValue": "{0} Administrator\n{1} Gilde verwalten\n{2} Kanal verwalten\n{3} Manage_Messages",
|
||||
"pingTitle1": "Bot Info:",
|
||||
"pingTitle2": "Spielerinfo:",
|
||||
"pingfield1": "```Shard-ID: {0}/{1}\nShard-Latenz: {2:.3f} s {3}\nRegion: {4}```",
|
||||
"pingfield2": "```Knoten: {0} - {1:.3f}s\nSpieler: {2}\nSprachregion: {3}```",
|
||||
"karaoke": "Sie setzen den Pegel auf **{0}**, monoLevel auf **{1}**, filterBand auf **{2}** und filterWidth auf **{3}**",
|
||||
"tremolo&vibrato": "Sie stellen die Frequenz auf **{0}** und die Tiefe auf **{1}** ein",
|
||||
"rotation": "Sie setzen die RotationHz auf **{0}**",
|
||||
"distortion": "Sie haben die Verzerrung aktiviert.",
|
||||
"lowpass": "Sie setzen den LowPass auf **{0}**",
|
||||
"channelmix": "Sie haben den channelMix aktualisiert. Von links nach links: **{0}**, von rechts nach rechts: **{1}**, von links nach rechts: **{2}**, von rechts nach links: ** {3}**",
|
||||
"nightcore": "Sie haben Nightcore-Soundeffekte hinzugefügt.",
|
||||
"8d": "Sie haben 8D-Soundeffekte hinzugefügt.",
|
||||
"vaporwave": "Sie haben Dampfwellen-Soundeffekte hinzugefügt.",
|
||||
"cleareffect": "Die Soundeffekte wurden gelöscht!",
|
||||
"FilterTagAlreadyInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwenden Sie /cleareffect <Tag>, um es zu entfernen.",
|
||||
|
||||
"playlistViewTitle": "📜 Alle Playlists von {0}",
|
||||
"playlistViewHeaders": [" ", "ID:", "Zeit:", "Name:", "Spuren:"],
|
||||
"playlistMaxP": "Maximale Wiedergabeliste:",
|
||||
"playlistMaxT": "Max. Spuren:",
|
||||
"playlistFooter": "Geben Sie /playlist play [playlist] ein, um die Playlist in die Warteschlange einzufügen.",
|
||||
"playlistNotFound": "Wiedergabeliste [`{0}`] nicht gefunden. Geben Sie /playlist view ein, um Ihre gesamte Wiedergabeliste anzuzeigen.",
|
||||
"playlistNotAccess": "Es tut uns leid! Du bist nicht berechtigt, auf diese Playlist zuzugreifen!",
|
||||
"playlistNoTrack": "Es tut uns leid! Es gibt keine Titel in der Wiedergabeliste [`{0}`].",
|
||||
"playlistNotAllow": "Dieser Befehl ist für verknüpfte und freigegebene Wiedergabelisten nicht zulässig.",
|
||||
"playlistPlay": "Playlist [`{0}`] mit `{1}` Songs zur Warteschlange hinzugefügt.",
|
||||
"playlistOverText": "Es tut uns leid! Der Name der Playlist darf 10 Zeichen nicht überschreiten.",
|
||||
"playlistSameName": "Es tut uns leid! Dieser Name darf nicht mit Ihrem neuen Namen identisch sein.",
|
||||
"playlistDeleteError": "Sie dürfen die Standard-Wiedergabeliste nicht löschen.",
|
||||
"playlistRemove": "Sie haben die Wiedergabeliste [`{0}`] entfernt.",
|
||||
"playlistSendErrorPlayer": "Es tut uns leid! Sie können keine Einladung an sich selbst senden.",
|
||||
"playlistSendErrorBot": "Es tut uns leid! Sie können keine Einladung an einen Bot senden.",
|
||||
"playlistBelongs": "Es tut uns leid! Diese Playlist gehört <@{0}>.",
|
||||
"playlistShare": "Es tut uns leid! Diese Playlist wurde mit {0} geteilt.",
|
||||
"playlistSent": "Es tut uns leid! Sie haben bereits eine Einladung gesendet.",
|
||||
"noPlaylistAcc": "{0} hat kein Playlist-Konto erstellt.",
|
||||
"playlistCreateError": "Es tut uns leid! Sie können diese Art von Wiedergabeliste nicht erstellen!",
|
||||
"overPlaylistCreation": "Sie können nicht mehr als `{0}` Wiedergabelisten erstellen!",
|
||||
"playlistExists": "Playlist [`{0}`] existiert bereits.",
|
||||
"playlistNotInvaildUrl": "Bitte geben Sie einen gültigen Link oder öffentlichen Spotify- oder YouTube-Playlist-Link ein.",
|
||||
"playlistCreated": "Sie haben die Wiedergabeliste „{0}“ erstellt. Geben Sie /playlist view ein, um weitere Informationen zu erhalten.",
|
||||
"playlistRenamed": "Sie haben `{0}` in `{1}` umbenannt.",
|
||||
"playlistLimitTrack": "Sie haben die Grenze erreicht! Du kannst deiner Playlist nur `{0}` Songs hinzufügen.",
|
||||
"playlistPlaylistLink": "Sie dürfen keinen Playlist-Link verwenden.",
|
||||
"playlistStream": "Du darfst deiner Playlist keine Streaming-Videos hinzufügen.",
|
||||
"playlistPositionNotFound": "Position `{0}` kann nicht in Ihrer Playlist [`{1}`] gefunden werden!",
|
||||
"playlistRemoved": "👋 **{0}** aus {1}s Playlist [`{2}`] entfernt.",
|
||||
"playlistClear": "Sie haben Ihre Wiedergabeliste [`{0}`] erfolgreich gelöscht.",
|
||||
"playlistView": "Playlist-Viewer",
|
||||
"playlistViewDesc": "```Name | ID: {0} | {1}\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Lesen: ✓ ✍🏽 Schreiben: {0} 🗑️ Entfernen: {1}",
|
||||
"playlistViewPermsValue2": "📖 Lesen: {0}",
|
||||
"playlistViewTrack": "Spuren",
|
||||
"playlistViewPage": "Seite: {0}/{1} | Gesamtdauer: {2}",
|
||||
"inboxFull": "Es tut uns leid! Der Posteingang von {0} ist voll.",
|
||||
"inboxNoMsg": "Es sind keine Nachrichten in Ihrem Posteingang.",
|
||||
"invitationSent": "Einladung an {0} gesendet.",
|
||||
|
||||
"notInChannel": "{0}, Sie müssen sich in {1} befinden, um Sprachbefehle verwenden zu können. Bitte verbinden Sie sich erneut mit dem Sprachkanal, wenn Sie sich bereits dort befinden!",
|
||||
"noTrackPlaying": "Es werden gerade keine Songs abgespielt",
|
||||
"noTrackFound": "Mit dieser Suchanfrage wurden keine Songs gefunden! Bitte geben Sie eine gültige URL an.",
|
||||
"noLinkSupport": "Suchbefehl unterstützt keine Links!",
|
||||
"voted": "Sie haben abgestimmt!",
|
||||
"missingPerms_pos": "Nur der DJ oder die Administratoren dürfen die Position ändern.",
|
||||
"missingPerms_mode": "Nur der DJ oder die Administratoren können den Loop-Modus wechseln.",
|
||||
"missingPerms_queue": "Nur der DJ oder die Administratoren können Tracks aus der Warteschlange entfernen.",
|
||||
"missingPerms_autoplay": "Nur der DJ oder die Administratoren können den Autoplay-Modus aktivieren oder deaktivieren!",
|
||||
"missingPerms_function": "Nur DJ oder die Administratoren können diese Funktion verwenden.",
|
||||
"timeFormatError": "Falsches Zeitformat. Beispiele: 2:42 oder 12:39:31",
|
||||
"lyricsNotFound": "Liedtext nicht gefunden. Geben Sie /lyrics <Liedtitel> ein, um den Liedtext zu finden.",
|
||||
"missingTrackInfo": "Einige Trackinfos fehlen.",
|
||||
"noVoiceChannel": "Sprachkanal nicht gefunden!",
|
||||
|
||||
"playlistAddError": "Du darfst deiner Playlist keine Streaming-Videos hinzufügen!",
|
||||
"playlistAddError2": "Beim Hinzufügen von Titeln zur Wiedergabeliste ist ein Problem aufgetreten!",
|
||||
"playlistlimited": "Sie haben die Grenze erreicht! Du kannst deiner Playlist nur {0} Songs hinzufügen.",
|
||||
"playlistrepeated": "Es gibt bereits denselben Titel in Ihrer Playlist!",
|
||||
"playlistAdded": "❤️ **{0}** zur Playlist [`{2}`] von {1} hinzugefügt!",
|
||||
|
||||
"playerAuthor": "Musik-Controller | {0}",
|
||||
"playerDesc": "**Läuft gerade: ```[{0}]```\nLink: [Klick mich]({1}) | Anforderer: {2} | DJ: {3}**",
|
||||
"playerFooter": "Warteschlangelänge: {0} | Dauer: {1} | Lautstärke: {2}% {3}",
|
||||
"playerFooter2": "| Wiederholen: {0}",
|
||||
"playerDropdown": "Wählen Sie einen Song aus, zudem Sie springen möchten...",
|
||||
|
||||
"buttonBack": "Zurück",
|
||||
"buttonPause": "Pause",
|
||||
"buttonResume": "Fortsetzen",
|
||||
"buttonSkip": "Überspringen",
|
||||
"buttonLeave": "Verlassen",
|
||||
|
||||
"nowplayingDesc": "**Läuft gerade:**\n```{0}```",
|
||||
"nowplayingField": "Als nächstes:",
|
||||
"nowplayingLink": "Hören Sie auf {0}",
|
||||
|
||||
"connect": "Verbunden mit {0}",
|
||||
|
||||
"live": "LIVE",
|
||||
"playlistLoad": " 🎶 Playlist **{0}** mit `{1}` Songs zur Warteschlange hinzugefügt.",
|
||||
"trackLoad": "**{0}** von **{1}** (`{2}`) hinzugefügt, um mit dem Spielen zu beginnen..\n",
|
||||
"trackLoad_pos": "**{0}** von **{1}** (`{2}`) zur Warteschlange an der position **{3}** hinzugefügt\n",
|
||||
|
||||
"searchTitle": "Suchanfrage: {0}",
|
||||
"searchDesc": "➥ Plattform: {0} **{1}**\n➥ Ergebnisse: **{2}**\n\n{3}",
|
||||
"searchWait": "Wählen Sie den Song aus, den Sie in der Warteschlange hinzufügen möchten.",
|
||||
"searchTimeout": "Zeitüberschreitung bei der Suche. Bitte versuchen Sie es später erneut.",
|
||||
"searchSuccess": "Lied zur Warteschlange hinzugefügt.",
|
||||
|
||||
"queueTitle": "Bevorstehende Warteschlange:",
|
||||
"historyTitle": "Verlaufswarteschlange:",
|
||||
"viewTitle": "Musikwarteschlange",
|
||||
"viewDesc": "**Läuft gerade: [Klick mich]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Buchseite: {0}/{1} | Gesamtdauer: {2}",
|
||||
|
||||
"pauseError": "Der Player ist bereits pausiert.",
|
||||
"pauseVote": "{0} hat dafür gestimmt, den Song zu pausieren. [{1}/{2}]",
|
||||
"paused": "`{0}` hat den Player angehalten.",
|
||||
"resumeError": "Der Player wird nicht angehalten.",
|
||||
"resumeVote": "{0} hat dafür gestimmt, den Song fortzusetzen. [{1}/{2}]",
|
||||
"resumed": "`{0}` hat den Player fortgesetzt.",
|
||||
"shuffleError": "Fügen Sie vor dem Mischen weitere Songs zur Warteschlange hinzu.",
|
||||
"shuffleVote": "{0} hat dafür gestimmt, die Warteschlange zu mischen. [{1}/{2}]",
|
||||
"shuffled": "Die Warteschlange wird gemischt.",
|
||||
"skipError": "Es gibt keine Songs, zu denen man springen könnte.",
|
||||
"skipVote": "{0} hat dafür gestimmt, den Song zu überspringen. [{1}/{2}]",
|
||||
"skipped": "`{0}` hat das Lied übersprungen.",
|
||||
|
||||
"backVote": "{0} hat dafür gestimmt, zu den vorherigen Song zu überspringen. [{1}/{2}]",
|
||||
"backed": "`{0}` springt zum vorherigen Song.",
|
||||
|
||||
"leaveVote": "{0} hat dafür gestimmt, den Spieler zu stoppen. [{1}/{2}]",
|
||||
"left": "`{0}` hat den Player angehalten.",
|
||||
|
||||
"seek": "Setze den Player auf **{0}**",
|
||||
"repeat": "Der Wiederholungsmodus wurde auf `{0}` eingestellt",
|
||||
"cleared": "Alle Tracks in `{0}` gelöscht",
|
||||
"removed": "`{0}` Titel wurden aus der Warteschlange entfernt.",
|
||||
"forward": "Leite den Spieler an **{0}** weiter",
|
||||
"rewind": "Spulen Sie den Player auf **{0}** zurück",
|
||||
"replay": "Wiederholen des aktuellen Songs.",
|
||||
"moved": "{0} und {1} sind vertauscht",
|
||||
"autoplay": "Der Autoplay-Modus ist jetzt **{0}**",
|
||||
|
||||
"notdj": "Du bist kein DJ. Der aktuelle DJ ist {0}.",
|
||||
"djToMe": "Sie können den DJ nicht auf sich selbst oder einen Bot übertragen.",
|
||||
"djnotinchannel": "`{0}` ist nicht im Sprachkanal.",
|
||||
"djswap": "Sie haben die Rolle des DJs an `{0}` übertragen.",
|
||||
|
||||
"chaptersDropdown": "Wählen Sie ein Kapitel aus, zu dem Sie springen möchten ...",
|
||||
"noChaptersFound": "Es wurden keine Kapitel gefunden!",
|
||||
"chatpersNotSupport": "Dieser Befehl unterstützt nur YouTube-Videos!",
|
||||
|
||||
"voicelinkQueueFull": "Entschuldigung, Sie haben das Maximum von `{0}` Titeln in der Warteschlange erreicht!",
|
||||
"voicelinkOutofList": "Bitte geben Sie einen gültigen Titelindex an!",
|
||||
"voicelinkDuplicateTrack": "Entschuldigung, dieser Titel befindet sich bereits in der Warteschlange."
|
||||
}
|
||||
184
langs/EN.json
Normal file
184
langs/EN.json
Normal file
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"unknownException": "⚠️ Something wrong while running the command! Please try again later or join our discord server for further support.",
|
||||
"enabled": "enabled",
|
||||
"disabled": "disabled",
|
||||
|
||||
"nodeReconnect": "Please try again! After the node reconnects.",
|
||||
"noChannel": "No voice channel to connect. Please either provide one or join one.",
|
||||
"alreadyConnected": "Already connected to a voice channel.",
|
||||
"noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.",
|
||||
"noPlaySource": "Can't found any playable sources!",
|
||||
"noPlayer": "No player has found on this server.",
|
||||
"notVote": "This command requires your vote! Type `/vote` for more info.",
|
||||
"languageNotFound": "No language pack found! please select an existing language pack.",
|
||||
"changedLanguage": "Successfully changed to `{0}` language pack.",
|
||||
"setDJ": "Set the dj to {0}.",
|
||||
"setqueue": "Set queue mode to `{0}`.",
|
||||
"247": "Now you have `{0}` 24/7 mode.",
|
||||
"bypassVote": "Now you have `{0}` voting system.",
|
||||
"setVolume": "Set the volume to `{0}`%",
|
||||
"togglecontroller": "Now you have `{0}` the music controller.",
|
||||
"toggleDuplicateTrack": "Now you have `{0}` to prevent duplicate track in the queue.",
|
||||
"settingsMenu": "Server Settings | {0}",
|
||||
"settingsTitle": "❤️ Basic Info:",
|
||||
"settingsValue": "Language: `{0}`\nEnable Music Controller: `{1}`\nDJ Role: {2}\nVote Bypass: `{3}`\n24/7: `{4}`\nDefault Volume: `{5}%`\nPlay Time: `{6}`\nDonate Server: `{7}`",
|
||||
"settingsTitle2": "🔗 Queue info:",
|
||||
"settingsValue2": "Queue Mode: `{0}`\nMax Song: `{1}`\nAllow Duplicate Track: `{2}`",
|
||||
"settingsPermTitle": "✨ Permissions:",
|
||||
"settingsPermValue": "{0} Administrator\n{1} Manage_Guild\n{2} Manage_Channel\n{3} Manage_Messages",
|
||||
"pingTitle1": "Bot Info:",
|
||||
"pingTitle2": "Player Info:",
|
||||
"pingfield1": "```Shard ID: {0}/{1}\nShard Latency: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"pingfield2": "```Node: {0} - {1:.3f}s\nPlayers: {2}\nVoice Region: {3}```",
|
||||
"karaoke": "You have updated the level to **{0}**, monoLevel to **{1}**, filterBand to **{2}** and filterWidth to **{3}**",
|
||||
"tremolo&vibrato": "You updated the frequency to **{0}** and depth to **{1}**",
|
||||
"rotation": "You have updated the rotationHz to **{0}**",
|
||||
"distortion": "You have enabled distortion.",
|
||||
"lowpass": "You have updated the lowPass to **{0}**",
|
||||
"channelmix": "You have updated the channelMix. Left-to-left: **{0}**, Right-to-right: **{1}**, Left-to-right: **{2}**, Right-to-left: **{3}**",
|
||||
"nightcore": "You have added nightcore sound effects.",
|
||||
"8d": "You have added 8D sound effects.",
|
||||
"vaporwave": "You have added vaporwave sound effects.",
|
||||
"cleareffect": "The sound effects have been cleared!",
|
||||
"FilterTagAlreadyInUse": "This sound effects has already in use! Please use /cleareffect <Tag> to remove it.",
|
||||
|
||||
"playlistViewTitle": "📜 All {0}'s Playlists",
|
||||
"playlistViewHeaders": [" ", "ID:", "Time:", "Name:", "Tracks:"],
|
||||
"playlistMaxP": "Max Playlist:",
|
||||
"playlistMaxT": "Max Tracks:",
|
||||
"playlistFooter": "Type /playlist play [playlist] to add the playlist the into queue.",
|
||||
"playlistNotFound": "Playlist [`{0}`] not found. Type /playlist view to view all your playlist.",
|
||||
"playlistNotAccess": "Sorry! You are not allowed to access this playlist!",
|
||||
"playlistNoTrack": "Sorry! There are no tracks in the playlist [`{0}`].",
|
||||
"playlistNotAllow": "This command is not allowed on linked and shared playlists.",
|
||||
"playlistPlay": "Added the playlist [`{0}`] with `{1}` songs to the queue.",
|
||||
"playlistOverText": "Sorry! The name of the playlist cannot exceed 10 characters.",
|
||||
"playlistSameName": "Sorry! This name cannot be the same as your new name.",
|
||||
"playlistDeleteError": "You are not allowed to delete default playlist.",
|
||||
"playlistRemove": "You have removed playlist [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Sorry! You are not able to send invitation to yourself.",
|
||||
"playlistSendErrorBot": "Sorry! You are not able to send invitation to a bot.",
|
||||
"playlistBelongs": "Sorry! This playlist belongs to <@{0}>.",
|
||||
"playlistShare": "Sorry! This playlist has been shared with {0}.",
|
||||
"playlistSent": "Sorry! You have sent an invitation before.",
|
||||
"noPlaylistAcc": "{0} didn't create a playlist account.",
|
||||
"playlistCreateError": "Sorry! You are not able to create this type of playlist!",
|
||||
"overPlaylistCreation": "You cannot create more than `{0}` playlists!",
|
||||
"playlistExists": "Playlist [`{0}`] already exists.",
|
||||
"playlistNotInvaildUrl": "Please enter a valid link or public spotify or youtube playlist link.",
|
||||
"playlistCreated": "You have created `{0}` playlist. Type /playlist view for more info.",
|
||||
"playlistRenamed": "You have renamed `{0}` to `{1}`.",
|
||||
"playlistLimitTrack": "You have reached the limit! You can only add `{0}` songs to your playlist.",
|
||||
"playlistPlaylistLink": "You are not allowed to use playlist link.",
|
||||
"playlistStream": "You are not allowed to add streaming videos to your playlist.",
|
||||
"playlistPositionNotFound": "Can't find position `{0}` from your playlist [`{1}`]!",
|
||||
"playlistRemoved": "👋 Removed **{0}** from {1}'s playlist [`{2}`].",
|
||||
"playlistClear": "You have successfully clear your playlist [`{0}`].",
|
||||
"playlistView": "Playlist Viewer",
|
||||
"playlistViewDesc": "```Name | ID: {0} | {1}\nTotal Tracks: {2}\nOwner: {3}\nType: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Read: ✓ ✍🏽 Write: {0} 🗑️ Remove: {1}",
|
||||
"playlistViewPermsValue2": "📖 Read: {0}",
|
||||
"playlistViewTrack": "Tracks",
|
||||
"playlistViewPage": "Page: {0}/{1} | Total Duration: {2}",
|
||||
"inboxFull": "Sorry! {0}'s inbox is full.",
|
||||
"inboxNoMsg": "There are no messages in your inbox.",
|
||||
"invitationSent": "Invitation sent to {0}.",
|
||||
|
||||
"notInChannel": "{0}, you must be in {1} to use voice commands. Please rejoin if you are in voice!",
|
||||
"noTrackPlaying": "There are no songs playing right now",
|
||||
"noTrackFound": "No songs were found with that query! Please provide a valid url.",
|
||||
"noLinkSupport": "Search command does not support links!",
|
||||
"voted": "You have voted!",
|
||||
"missingPerms_pos": "Only the DJ or admins may change the position.",
|
||||
"missingPerms_mode": "Only the DJ or admins may switch loop mode.",
|
||||
"missingPerms_queue": "Only the DJ or admins may remove track from the queue.",
|
||||
"missingPerms_autoplay": "Only the DJ or admins can enable or disable autoplay mode!",
|
||||
"missingPerms_function": "Only DJ or Admin can use this function.",
|
||||
"timeFormatError": "Incorrect time format. Example: 2:42 or 12:39:31",
|
||||
"lyricsNotFound": "Lyrics not found. Type /lyrics <Song Name> <Author> to find the lyrics.",
|
||||
"missingTrackInfo": "Some track infos are missing.",
|
||||
"noVoiceChannel": "Voice Channel Not Found!",
|
||||
|
||||
"playlistAddError": "You are not allowed to add streaming videos to your playlist!",
|
||||
"playlistAddError2": "There was a problem adding tracks to the playlist!",
|
||||
"playlistlimited": "You have reached the limit! You can only add {0} songs to your playlist.",
|
||||
"playlistrepeated": "There is already the same track in your playlist!",
|
||||
"playlistAdded": "❤️ Added **{0}** into {1}'s playlist [`{2}`]!",
|
||||
|
||||
"playerAuthor": "Music Controller | {0}",
|
||||
"playerDesc": "**Now Playing: ```[{0}]```\nLink: [Click Me]({1}) | Requester: {2} | DJ: {3}**",
|
||||
"playerFooter": "Queue Length: {0} | Duration: {1} | Volume: {2}% {3}",
|
||||
"playerFooter2": "| Repeat: {0}",
|
||||
"playerDropdown": "Select a song to skip to ...",
|
||||
|
||||
"buttonBack": "Back",
|
||||
"buttonPause": "Pause",
|
||||
"buttonResume": "Resume",
|
||||
"buttonSkip": "Skip",
|
||||
"buttonLeave": "Leave",
|
||||
|
||||
"nowplayingDesc": "**Now Playing:**\n```{0}```",
|
||||
"nowplayingField": "Up Next:",
|
||||
"nowplayingLink": "Listen on {0}",
|
||||
|
||||
"connect": "Connected to {0}",
|
||||
|
||||
"live": "LIVE",
|
||||
"playlistLoad": " 🎶 Added the playlist **{0}** with `{1}` songs to the queue.",
|
||||
"trackLoad": "Added **{0}** by **{1}** (`{2}`) to begin playing.\n",
|
||||
"trackLoad_pos": "Added **{0}** by **{1}** (`{2}`) to the queue at position **{3}**\n",
|
||||
|
||||
"searchTitle": "Search Query: {0}",
|
||||
"searchDesc": "➥ Platform: {0} **{1}**\n➥ Results: **{2}**\n\n{3}",
|
||||
"searchWait": "Select the song you want to add to the queue.",
|
||||
"searchTimeout": "Search timed out. Please try again later.",
|
||||
"searchSuccess": "Song added to the queue.",
|
||||
|
||||
"queueTitle": "Upcoming Queue:",
|
||||
"historyTitle": "History Queue:",
|
||||
"viewTitle": "Music Queue",
|
||||
"viewDesc": "**Now Playing: [Click Me]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Page: {0}/{1} | Total Duration: {2}",
|
||||
|
||||
"pauseError": "The player is already paused.",
|
||||
"pauseVote": "{0} has voted to pause the song. [{1}/{2}]",
|
||||
"paused": "`{0}` has paused the player.",
|
||||
"resumeError": "The player is not paused.",
|
||||
"resumeVote": "{0} has voted to resume the song. [{1}/{2}]",
|
||||
"resumed": "`{0}` has resumed the player.",
|
||||
"shuffleError": "Add more songs to the queue before shuffling.",
|
||||
"shuffleVote": "{0} has voted to shuffle the queue. [{1}/{2}]",
|
||||
"shuffled": "The queue is shuffled.",
|
||||
"skipError": "There are no songs too skip to.",
|
||||
"skipVote": "{0} has voted to skip the song. [{1}/{2}]",
|
||||
"skipped": "`{0}` has skipped the song.",
|
||||
|
||||
"backVote": "{0} has voted to skip to the previous song. [{1}/{2}]",
|
||||
"backed": "`{0}` has skipping to previous song.",
|
||||
|
||||
"leaveVote": "{0} has voted to stop the player. [{1}/{2}]",
|
||||
"left": "`{0}` has stopped the player.",
|
||||
|
||||
"seek": "Set the player to **{0}**",
|
||||
"repeat": "The repeat mode has been set to `{0}`",
|
||||
"cleared": "Cleared all tracks in the `{0}`",
|
||||
"removed": "`{0}` tracks were removed from the queue.",
|
||||
"forward": "Forward the player to **{0}**",
|
||||
"rewind": "Rewind the player to **{0}**",
|
||||
"replay": "Replaying the current song.",
|
||||
"moved": "{0} and {1} are Swapped",
|
||||
"autoplay": "Autoplay mode is now **{0}**",
|
||||
|
||||
"notdj": "You are not DJ. The current DJ is {0}.",
|
||||
"djToMe": "You cannot transfer the DJ to yourself or a bot.",
|
||||
"djnotinchannel": "`{0}` is not in the voice channel.",
|
||||
"djswap": "You have transferred the role of dj to `{0}`.",
|
||||
|
||||
"chaptersDropdown": "Select a chapter to skip to ...",
|
||||
"noChaptersFound": "No chapters has been found!",
|
||||
"chatpersNotSupport": "This command only supports Youtube videos!",
|
||||
|
||||
"voicelinkQueueFull": "Sorry, you have reached the maximum of `{0}` tracks in the queue!",
|
||||
"voicelinkOutofList": "Please provide a valid track index!",
|
||||
"voicelinkDuplicateTrack": "Sorry, this track is already in the queue."
|
||||
}
|
||||
185
langs/JP.json
Normal file
185
langs/JP.json
Normal file
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"unknownException": "⚠️ コマンドの実行中に問題が発生しました! 後でもう一度試すか、discord サーバーに参加してさらなるサポートを受けてください。",
|
||||
"enabled": "有効",
|
||||
"disabled": "無効",
|
||||
|
||||
"nodeReconnect": "もう一度お試しください。 ノードが再接続した後。",
|
||||
"noChannel": "接続する音声チャネルがありません。いずれかを提供するか、いずれかに参加してください。",
|
||||
"alreadyConnected": "すでに音声チャネルに接続されています。",
|
||||
"noPermission": "ごめん!あなたの音声チャネルに参加したり、話したりする権限がありません。",
|
||||
"noPlaySource": "再生可能なソースが見つかりません!",
|
||||
"noPlayer": "このサーバーにはプレイヤーが見つかりません。",
|
||||
"notDonate": "このコマンドを実行するには、サーバーが寄付サーバーである必要があります。",
|
||||
"notVote": "このコマンドにはあなたの投票が必要です! 詳細については、`/vote` と入力してください。",
|
||||
"languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください",
|
||||
"changedLanguage": "`{0}` 言語パックに正常に変更されました。",
|
||||
"setDJ": "DJ を {0} に設定します。",
|
||||
"setqueue": "キュー モードを `{0}` に設定します。",
|
||||
"247": "これで `{0}` 24/7 モードになりました。",
|
||||
"bypassVote": "これで `{0}` 投票システムができました。",
|
||||
"setVolume": "音量を `{0}`% に設定",
|
||||
"togglecontroller": "これで音楽コントローラー `{0}` ができました。",
|
||||
"toggleDuplicateTrack": "これで、キュー内でトラックが重複しないように `{0}` になりました。",
|
||||
"settingsMenu": "サーバー設定 | {0}",
|
||||
"settingsTitle": "❤️ 基本情報:",
|
||||
"settingsValue": "言語: `{0}`\nミュージック コントローラを有効にする: `{1}`\nDJ ロール: {2}\n投票バイパス: `{3}`\n年中無休: `{4}`\nデフォルトのボリューム: `{5}%`\nプレイ時間: `{6}`\n寄付サーバー: `{7}`",
|
||||
"settingsTitle2": "🔗 キュー情報:",
|
||||
"settingsValue2": "キューモード: `{0}`\nマックスソング: `{1}`\n重複トラックを許可: `{2}`",
|
||||
"settingsPermTitle": "✨ パーミッション:",
|
||||
"settingsPermValue": "{0} 管理者\n{1}Manage_Guild\n{2}Manage_Channel\n{3}Manage_Messages",
|
||||
"pingTitle1": "情報について:",
|
||||
"pingTitle2": "プレイヤー情報:",
|
||||
"pingfield1": "```シャード ID: {0}/{1}\nシャードの待ち時間: {2:.3f} 秒 {3}\nリージョン: {4}```",
|
||||
"pingfield2": "ノード: {0} - {1:.3f}\nプレーヤー: {2}\n声域: {3}```",
|
||||
"karaoke": "レベルを **{0}** に、monoLevel を **{1}** に、filterBand を **{2}** に、filterWidth を **{3}** に設定します。",
|
||||
"tremolo&vibrato": "周波数を **{0}** に設定し、深さを **{1}** に設定しました",
|
||||
"rotation": "RotationHz を **{0}** に設定しました",
|
||||
"distortion": "歪みを有効にしました。",
|
||||
"lowpass": "ローパスを **{0}** に設定しました",
|
||||
"channelmix": "channelMix を更新しました。左から左: **{0}**、右から右: **{1}**、左から右: **{2}**、右から左: ** {3}**",
|
||||
"nightcore": "ナイトコア効果音を追加しました。",
|
||||
"8d": "8D 効果音を追加しました。",
|
||||
"vaporwave": "蒸気波効果音を追加しました。",
|
||||
"cleareffect": "効果音クリアしました!",
|
||||
"FilterTagAlreadyInUse": "この効果音はすでに使用されています。 /cleareffect <Tag> を使用して削除してください。",
|
||||
|
||||
"playlistViewTitle": "📜 すべての {0} のプレイリスト",
|
||||
"playlistViewHeaders": [" ", "ID:", "時間:", "名前:", "トラック:"],
|
||||
"playlistMaxP": "最大プレイリスト:",
|
||||
"playlistMaxT": "最大トラック:",
|
||||
"playlistFooter": "/playlist play [playlist] と入力して、プレイリストをキューに追加します。",
|
||||
"playlistNotFound": "プレイリスト [`{0}`] が見つかりません。 /playlist view と入力して、すべてのプレイリストを表示します。",
|
||||
"playlistNotAccess": "ごめん!このプレイリストへのアクセスは許可されていません!",
|
||||
"playlistNoTrack": "ごめん!プレイリスト [`{0}`] にトラックがありません。",
|
||||
"playlistNotAllow": "このコマンドは、リンクされた共有プレイリストでは許可されていません。",
|
||||
"playlistPlay": "`{1}` の曲を含むプレイリスト [`{0}`] をキューに追加しました。",
|
||||
"playlistOverText": "ごめん!プレイリストの名前は 10 文字を超えることはできません。",
|
||||
"playlistSameName": "ごめん!この名前を新しい名前と同じにすることはできません。",
|
||||
"playlistDeleteError": "デフォルトのプレイリストを削除することはできません。",
|
||||
"playlistRemove": "プレイリスト [`{0}`] を削除しました。",
|
||||
"playlistSendErrorPlayer": "ごめん!自分自身に招待状を送信できません。",
|
||||
"playlistSendErrorBot": "ごめん!ボットに招待状を送信できません。",
|
||||
"playlistBelongs": "ごめん!この再生リストは <@{0}> のものです。",
|
||||
"playlistShare": "ごめん!この再生リストは {0} と共有されています。",
|
||||
"playlistSent": "ごめん!以前に招待状を送信しました。",
|
||||
"noPlaylistAcc": "{0} はプレイリスト アカウントを作成しませんでした。",
|
||||
"playlistCreateError": "ごめん!このタイプのプレイリストは作成できません!",
|
||||
"overPlaylistCreation": "`{0}` 以上の再生リストを作成することはできません!",
|
||||
"playlistExists": "プレイリスト [`{0}`] は既に存在します。",
|
||||
"playlistNotInvaildUrl": "有効なリンク、または公開の Spotify または YouTube プレイリストのリンクを入力してください。",
|
||||
"playlistCreated": "`{0}` プレイリストを作成しました。詳細については、/playlist view と入力してください。",
|
||||
"playlistRenamed": "`{0}` を `{1}` に名前変更しました。",
|
||||
"playlistLimitTrack": "限界に達しました!プレイリストに追加できるのは `{0}` 曲のみです。",
|
||||
"playlistPlaylistLink": "プレイリスト リンクの使用は許可されていません。",
|
||||
"playlistStream": "ストリーミング ビデオをプレイリストに追加することはできません。",
|
||||
"playlistPositionNotFound": "プレイリスト [`{1}`] から位置 `{0}` が見つかりません!",
|
||||
"playlistRemoved": "👋 {1} のプレイリスト [`{2}`] から **{0}** を削除しました。",
|
||||
"playlistClear": "プレイリスト [`{0}`] を正常にクリアしました。",
|
||||
"playlistView": "プレイリスト ビューア",
|
||||
"playlistViewDesc": "```名前 | ID: {0} | {1}\n合計トラック数: {2}\n所有者: {3}\nタイプ: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 読み取り: ✓ ✍🏽 書き込み: {0} 🗑️ 削除: {1}",
|
||||
"playlistViewPermsValue2": "📖 読む: {0}",
|
||||
"playlistViewTrack": "トラック",
|
||||
"playlistViewPage": "ページ: {0}/{1} |合計期間: {2}",
|
||||
"inboxFull": "ごめん! {0} の受信ボックスがいっぱいです。",
|
||||
"inboxNoMsg": "受信トレイにメッセージがありません。",
|
||||
"invitationSent": "{0} に招待状を送信しました。",
|
||||
|
||||
"notInChannel": "{0}, 音声コマンドを使用するには、{1}にいる必要があります。 声が出たらまた参加してください!",
|
||||
"noTrackPlaying": "現在再生中の曲はありません",
|
||||
"noTrackFound": "そのクエリで曲が見つかりませんでした! 有効なURLを入力してください。",
|
||||
"noLinkSupport": "検索コマンドはリンクをサポートしていません!",
|
||||
"voted": "投票しました!",
|
||||
"missingPerms_pos": "DJまたは管理者のみが位置を変更できます。",
|
||||
"missingPerms_mode": "DJまたは管理者のみがループモードを切り替えることができます。",
|
||||
"missingPerms_queue": "DJまたは管理者のみがキューからトラックを削除できます。",
|
||||
"missingPerms_autoplay": "自動再生モードを有効または無効にできるのは、DJまたは管理者だけです。",
|
||||
"missingPerms_function": "この機能を使用できるのはDJまたは管理者のみです。",
|
||||
"timeFormatError": "時間形式が正しくありません。 例: 2:42または12:39:31",
|
||||
"lyricsNotFound": "歌詞が見つかりません。/lyrics <曲名> <著者> と入力して、歌詞を検索します。",
|
||||
"missingTrackInfo": "一部のトラック情報が欠落しています。",
|
||||
"noVoiceChannel": "音声チャンネルが見つかりません!",
|
||||
|
||||
"playlistAddError": "再生リストにストリーミング動画を追加することは許可されていません。",
|
||||
"playlistAddError2": "プレイリストにトラックを追加する際に問題が発生しました。",
|
||||
"playlistlimited": "限界に達しました! プレイリストに追加できるのは{0}曲のみです。",
|
||||
"playlistrepeated": "プレイリストにはすでに同じトラックがあります!",
|
||||
"playlistAdded": "❤️ {1}のプレイリスト[`{2}`]に** {0} **を追加しました!",
|
||||
|
||||
"playerAuthor": "ミュージックコントローラー | {0}",
|
||||
"playerDesc": "**再生中: ```[{0}]```\nリンク: [クリックしてください]({1}) | リクエスター: {2} | DJ: {3}**",
|
||||
"playerFooter": "キューの長さ: {0} | 間隔: {1} | 音量: {2}% {3}",
|
||||
"playerFooter2": "| 繰り返す: {0}",
|
||||
"playerDropdown": "スキップする曲を選択してください...",
|
||||
|
||||
"buttonBack": "戻る",
|
||||
"buttonPause": "休止",
|
||||
"buttonResume": "再開",
|
||||
"buttonSkip": "スキップ",
|
||||
"buttonLeave": "離れる",
|
||||
|
||||
"nowplayingDesc": "**再生中:**\n```{0}```",
|
||||
"nowplayingField": "次に:",
|
||||
"nowplayingLink": "{0}で聞く",
|
||||
|
||||
"connect": "{0}に接続",
|
||||
|
||||
"live": "生放送",
|
||||
"playlistLoad": " 🎶 `{1}`の曲を含むプレイリスト** {0} **をキューに追加しました。",
|
||||
"trackLoad": "再生を開始するには、**{1}** によって **{0}** (`{2}`) が追加されました。\n",
|
||||
"trackLoad_pos": "**{1}** によって **{0}** (`{2}`)を** {3} **の位置のキューに追加しました\n",
|
||||
|
||||
"searchTitle": "検索クエリ: {0}",
|
||||
"searchDesc": "➥ プラットホーム: {0} **{1}**\n➥ 結果: **{2}**\n\n{3}",
|
||||
"searchWait": "キューに追加する曲を選択します。",
|
||||
"searchTimeout": "検索がタイムアウトしました。後でもう一度やり直してください。",
|
||||
"searchSuccess": "曲がキューに追加されます。",
|
||||
|
||||
"queueTitle": "今後のキュー:",
|
||||
"historyTitle": "履歴キュー:",
|
||||
"viewTitle": "ミュージックキュー",
|
||||
"viewDesc": "**再生中: [クリックしてください]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "ページ: {0}/{1} | 合計期間: {2}",
|
||||
|
||||
"pauseError": "プレーヤーはすでに一時停止しています。",
|
||||
"pauseVote": "{0} は曲を一時停止することに投票しました。[{1}/{2}]",
|
||||
"paused": "`{0}` はプレーヤーを一時停止しました。",
|
||||
"resumeError": "プレーヤーは一時停止されません。",
|
||||
"resumeVote": "{0}は曲の再開に投票しました。[{1}/{2}]",
|
||||
"resumed": "`{0}`はプレーヤーを再開しました。",
|
||||
"shuffleError": "シャッフルする前に、キューに曲を追加します。",
|
||||
"shuffleVote": "{0}はキューをシャッフルすることに投票しました。[{1}/{2}]",
|
||||
"shuffled": "キューがシャッフルされます。",
|
||||
"skipError": "スキップしすぎる曲はありません。",
|
||||
"skipVote": "{0}は曲をスキップすることに投票しました。[{1}/{2}]",
|
||||
"skipped": "`{0}`は曲をスキップしました。",
|
||||
|
||||
"backVote": "{0} 前の曲にスキップすることに投票しました。[{1}/{2}]",
|
||||
"backed": "`{0}`は前の曲にスキップしています。",
|
||||
|
||||
"leaveVote": "{0}はプレーヤーを停止することに投票しました。[{1}/{2}]",
|
||||
"left": "`{0}`はプレーヤーを停止しました。",
|
||||
|
||||
"seek": "プレーヤーを **{0}** に設定します",
|
||||
"repeat": "リピートモードが `{0}`に設定されています",
|
||||
"cleared": "`{0}`のすべてのトラックをクリアしました",
|
||||
"removed": "`{0}` トラックがキューから削除されました。",
|
||||
"forward": "プレーヤーを **{0}** に転送します",
|
||||
"rewind": "プレーヤーを **{0}** に巻き戻します",
|
||||
"replay": "現在の曲を再生します。",
|
||||
"moved": "{0}と{1}は入れ替えられます",
|
||||
"autoplay": "自動再生モードが **{0}** になりました",
|
||||
|
||||
"notdj": "あなたはDJではありません。 現在のDJは{0}です。",
|
||||
"djToMe": "DJを自分やロボットに転送することはできません。",
|
||||
"djnotinchannel": "`{0}`は音声チャネルにありません。",
|
||||
"djswap": "DJの役割を `{0}`に移しました。",
|
||||
|
||||
"chaptersDropdown": "スキップするチャプターを選択してください...",
|
||||
"noChaptersFound": "チャプターが見つかりません!",
|
||||
"chatpersNotSupport": "このコマンドはYouTubeビデオのみをサポートします!",
|
||||
|
||||
"voicelinkQueueFull": "申し訳ありませんが、キューの `{0}` トラックの最大数に達しました!",
|
||||
"voicelinkOutofList": "有効なトラック インデックスを指定してください。",
|
||||
"voicelinkDuplicateTrack": "申し訳ありませんが、このトラックは既にキューに入っています。"
|
||||
}
|
||||
185
langs/KO.json
Normal file
185
langs/KO.json
Normal file
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"unknownException": "⚠️ 명령을 실행하는 동안 문제가 발생했습니다! 나중에 다시 시도하거나 추가 지원을 위해 디스코드 서버에 가입하세요.",
|
||||
"enabled": "활성화",
|
||||
"disabled": "장애가 있는",
|
||||
|
||||
"nodeReconnect": "다시 시도해 주세요! 노드가 다시 연결된 후.",
|
||||
"noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 하나를 가입하십시오.",
|
||||
"alreadyConnected": "이미 음성 채널에 연결되어 있습니다.",
|
||||
"noPermission": "죄송합니다! 나는 당신의 음성 채널에 참여하거나 말할 권한이 없습니다.",
|
||||
"noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!",
|
||||
"noPlayer": "이 서버에서 플레이어를 찾지 못했습니다.",
|
||||
"notDonate": "이 명령은 서버가 기부 서버여야 합니다!",
|
||||
"notVote": "이 명령에는 투표가 필요합니다! 자세한 정보를 보려면 `/vote`를 입력하세요.",
|
||||
"languageNotFound": "언어 팩을 찾을 수 없습니다. 기존 언어 팩을 선택하십시오.",
|
||||
"changedLanguage": "`{0}` 언어 팩으로 변경되었습니다.",
|
||||
"setDJ": "DJ를 {0}으로 설정합니다.",
|
||||
"setqueue": "대기열 모드를 `{0}`로 설정합니다.",
|
||||
"247": "이제 `{0}` 24/7 모드가 있습니다.",
|
||||
"bypassVote": "이제 `{0}` 투표 시스템이 생겼습니다.",
|
||||
"setVolume": "볼륨을 `{0}`%로 설정",
|
||||
"togglecontroller": "이제 `{0}` 음악 컨트롤러가 있습니다.",
|
||||
"toggleDuplicateTrack": "이제 대기열에서 중복 트랙을 방지하기 위해 `{0}`되었습니다.",
|
||||
"settingsMenu": "서버 설정 | {0}",
|
||||
"settingsTitle": "❤️ 기본 정보:",
|
||||
"settingsValue": "언어: `{0}`\n음악 컨트롤러 활성화: `{1}`\nDJ 역할: {2}\n투표 우회: `{3}`\n연중무휴: `{4}`\n기본 볼륨: `{5}%`\n플레이 시간: `{6}`\n서버 기부: `{7}`",
|
||||
"settingsTitle2": "🔗 대기열 정보:",
|
||||
"settingsValue2": "대기열 모드: `{0}`\n맥스송: `{1}`\n중복 트랙 허용: `{2}`",
|
||||
"settingsPermTitle": "✨ 권한:",
|
||||
"settingsPermValue": "{0} 관리자\n{1} 관리_길드\n{2} 관리_채널\n{3} 관리_메시지",
|
||||
"pingTitle1": "정보:",
|
||||
"pingTitle2": "플레이어 정보:",
|
||||
"pingfield1": "```샤드 ID: {0}/{1}\n샤드 지연 시간: {2:.3f}s {3}\n지역: {4}```",
|
||||
"pingfield2": "```노드: {0} - {1:.3f}s\n선수: {2}\n음성 지역: {3}```",
|
||||
"karaoke": "수준을 **{0}**, monoLevel을 **{1}**, filterBand를 **{2}**, filterWidth를 **{3}**으로 설정합니다.",
|
||||
"tremolo&vibrato": "빈도를 **{0}**로 설정하고 깊이를 **{1}**으로 설정합니다.",
|
||||
"rotation": "회전Hz를 **{0}**으로 설정했습니다.",
|
||||
"distortion": "왜곡을 활성화했습니다.",
|
||||
"lowpass": "저역 통과를 **{0}**으로 설정했습니다.",
|
||||
"channelmix": "channelMix를 업데이트했습니다. 왼쪽에서 왼쪽으로: **{0}**, 오른쪽에서 오른쪽으로: **{1}**, 왼쪽에서 오른쪽으로: **{2}**, 오른쪽에서 왼쪽으로: **{3}**",
|
||||
"nightcore": "나이트코어 음향 효과를 추가했습니다.",
|
||||
"8d": "8D 음향 효과를 추가했습니다.",
|
||||
"vaporwave": "증기파 음향 효과를 추가했습니다.",
|
||||
"cleareffect": "효과음이 삭제되었습니다!",
|
||||
"FilterTagAlreadyInUse": "이 음향 효과는 이미 사용 중입니다! 제거하려면 /cleareffect <태그>를 사용하세요.",
|
||||
|
||||
"playlistViewTitle": "📜 {0}님의 모든 재생목록",
|
||||
"playlistViewHeaders": [" ", "ID:", "시간:", "이름:", "트랙:"],
|
||||
"playlistMaxP": "최대 재생 목록:",
|
||||
"playlistMaxT": "최대 트랙:",
|
||||
"playlistFooter": "/playlist play [playlist]를 입력하여 재생 목록을 대기열에 추가합니다.",
|
||||
"playlistNotFound": "재생 목록 [`{0}`]을(를) 찾을 수 없습니다. 모든 재생 목록을 보려면 /playlist view를 입력하십시오.",
|
||||
"playlistNotAccess": "죄송합니다! 이 재생 목록에 액세스할 수 없습니다!",
|
||||
"playlistNoTrack": "죄송합니다! 재생목록 [`{0}`]에 트랙이 없습니다.",
|
||||
"playlistNotAllow": "이 명령은 연결 및 공유 재생 목록에서 허용되지 않습니다.",
|
||||
"playlistPlay": "대기열에 `{1}` 노래가 있는 재생목록 [`{0}`]을 추가했습니다.",
|
||||
"playlistOverText": "죄송합니다! 재생 목록 이름은 10자를 초과할 수 없습니다.",
|
||||
"playlistSameName": "죄송합니다! 이 이름은 새 이름과 같을 수 없습니다.",
|
||||
"playlistDeleteError": "기본 재생 목록을 삭제할 수 없습니다.",
|
||||
"playlistRemove": "재생목록 [`{0}`]을(를) 제거했습니다.",
|
||||
"playlistSendErrorPlayer": "죄송합니다! 자신에게 초대장을 보낼 수 없습니다.",
|
||||
"playlistSendErrorBot": "죄송합니다! 봇에 초대장을 보낼 수 없습니다.",
|
||||
"playlistBelongs": "죄송합니다! 이 재생목록은 <@{0}>에 속합니다.",
|
||||
"playlistShare": "죄송합니다! 이 재생목록은 {0}과(와) 공유되었습니다.",
|
||||
"playlistSent": "죄송합니다! 이전에 초대장을 보냈습니다.",
|
||||
"noPlaylistAcc": "{0}님은 재생목록 계정을 만들지 않았습니다.",
|
||||
"playlistCreateError": "죄송합니다! 이 유형의 재생 목록을 만들 수 없습니다!",
|
||||
"overPlaylistCreation": "재생목록은 `{0}`개 이상 만들 수 없습니다!",
|
||||
"playlistExists": "재생 목록 [`{0}`]이(가) 이미 존재합니다.",
|
||||
"playlistNotInvaildUrl": "유효한 링크나 공개 Spotify 또는 YouTube 재생 목록 링크를 입력하세요.",
|
||||
"playlistCreated": "`{0}` 재생목록을 만들었습니다. 자세한 내용은 /재생 목록 보기를 입력하세요.",
|
||||
"playlistRenamed": "`{0}`의 이름을 `{1}`로 변경했습니다.",
|
||||
"playlistLimitTrack": "한계에 도달했습니다! 재생목록에는 `{0}`곡만 추가할 수 있습니다.",
|
||||
"playlistPlaylistLink": "재생 목록 링크를 사용할 수 없습니다.",
|
||||
"playlistStream": "재생 목록에 스트리밍 비디오를 추가할 수 없습니다.",
|
||||
"playlistPositionNotFound": "재생목록 [`{1}`]에서 `{0}` 위치를 찾을 수 없습니다!",
|
||||
"playlistRemoved": "👋 {1}님의 재생목록 [`{2}`]에서 **{0}**를 제거했습니다.",
|
||||
"playlistClear": "재생목록 [`{0}`]을(를) 성공적으로 지웠습니다.",
|
||||
"playlistView": "재생 목록 뷰어",
|
||||
"playlistViewDesc": "```이름 | 아이디: {0} | {1}\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 읽기: ✓ ✍🏽 쓰기: {0} 🗑️ 제거: {1}",
|
||||
"playlistViewPermsValue2": "📖 읽기: {0}",
|
||||
"playlistViewTrack": "트랙",
|
||||
"playlistViewPage": "페이지: {0}/{1} | 총 기간: {2}",
|
||||
"inboxFull": "죄송합니다! {0}님의 받은편지함이 가득 찼습니다.",
|
||||
"inboxNoMsg": "받은 편지함에 메시지가 없습니다.",
|
||||
"invitationSent": "{0}(으)로 초대장을 보냈습니다.",
|
||||
|
||||
"notInChannel": "{0}, 음성 명령을 사용하려면 {1}에 있어야 합니다. 목소리가 좋으시면 다시 참여해주세요!",
|
||||
"noTrackPlaying": "현재 재생 중인 노래가 없습니다.",
|
||||
"noTrackFound": "해당 검색어로 검색된 노래가 없습니다! 유효한 URL을 입력하세요.",
|
||||
"noLinkSupport": "검색 명령은 링크를 지원하지 않습니다!",
|
||||
"voted": "투표하셨습니다!",
|
||||
"missingPerms_pos": "DJ 또는 관리자만 위치를 변경할 수 있습니다.",
|
||||
"missingPerms_mode": "DJ 또는 관리자만 루프 모드를 전환할 수 있습니다.",
|
||||
"missingPerms_queue": "DJ 또는 관리자만 대기열에서 트랙을 제거할 수 있습니다.",
|
||||
"missingPerms_autoplay": "DJ 또는 관리자만 자동 재생 모드를 활성화 또는 비활성화할 수 있습니다!",
|
||||
"missingPerms_function": "DJ 또는 Admin만 이 기능을 사용할 수 있습니다.",
|
||||
"timeFormatError": "잘못된 시간 형식입니다. 예: 2:42 또는 12:39:31",
|
||||
"lyricsNotFound": "가사를 찾을 수 없습니다. /lyrics <노래 이름> <작가> 을 입력하여 가사를 찾습니다.",
|
||||
"missingTrackInfo": "일부 트랙 정보가 누락되었습니다.",
|
||||
"noVoiceChannel": "음성 채널을 찾을 수 없습니다!",
|
||||
|
||||
"playlistAddError": "재생 목록에 스트리밍 비디오를 추가할 수 없습니다!",
|
||||
"playlistAddError2": "재생 목록에 트랙을 추가하는 중에 문제가 발생했습니다!",
|
||||
"playlistlimited": "한계에 도달했습니다! 재생목록에는 {0}곡만 추가할 수 있습니다.",
|
||||
"playlistrepeated": "재생 목록에 이미 같은 트랙이 있습니다!",
|
||||
"playlistAdded": "❤️ **{0}**을 {1}의 재생목록 [`{2}`]에 추가했습니다!",
|
||||
|
||||
"playerAuthor": "음악 컨트롤러 | {0}",
|
||||
"playerDesc": "**지금 재생 중: ```[{0}]```\n링크: [클릭 해주세요]({1}) | 요청자: {2} | DJ: {3}**",
|
||||
"playerFooter": "대기열 길이: {0} | 지속: {1} | 무게: {2}% {3}",
|
||||
"playerFooter2": "| 반복하다: {0}",
|
||||
"playerDropdown": "건너뛸 노래 선택 ...",
|
||||
|
||||
"buttonBack": "뒤",
|
||||
"buttonPause": "일시 중지",
|
||||
"buttonResume": "이력서",
|
||||
"buttonSkip": "거르다",
|
||||
"buttonLeave": "떠나다",
|
||||
|
||||
"nowplayingDesc": "**지금 재생 중:**\n```{0}```",
|
||||
"nowplayingField": "다음으로:",
|
||||
"nowplayingLink": "{0}에서 듣기",
|
||||
|
||||
"connect": "{0}에 연결됨",
|
||||
|
||||
"live": "라이브 스트리밍",
|
||||
"playlistLoad": " 🎶 대기열에 `{1}` 노래가 있는 재생목록 **{0}**을 추가했습니다.",
|
||||
"trackLoad": "재생을 시작하기 위해 **{1}**님이 **{0}** (`{2}`) 을(를) 추가했습니다.\n",
|
||||
"trackLoad_pos": "**{1}**님이 **{0}** (`{2}`)를 **{3}** 위치의 대기열에 추가했습니다.\n",
|
||||
|
||||
"searchTitle": "검색어: {0}",
|
||||
"searchDesc": "➥ 플랫폼: {0} **{1}**\n➥ 결과: **{2}**\n\n{3}",
|
||||
"searchWait": "대기열에 추가하려는 노래를 선택합니다.",
|
||||
"searchTimeout": "검색 시간이 초과되었습니다. 나중에 다시 시도 해주십시오.",
|
||||
"searchSuccess": "노래가 대기열에 추가되었습니다.",
|
||||
|
||||
"queueTitle": "예정 대기열:",
|
||||
"historyTitle": "기록 대기열:",
|
||||
"viewTitle": "음악 대기열",
|
||||
"viewDesc": "**지금 재생 중: [클릭 해주세요]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "페이지: {0}/{1} | 총 기간: {2}",
|
||||
|
||||
"pauseError": "플레이어가 이미 일시 중지되었습니다.",
|
||||
"pauseVote": "{0}님이 노래를 일시중지하기 위해 투표했습니다. [{1}/{2}]",
|
||||
"paused": "``{0}`이(가) 플레이어를 일시 중지했습니다.",
|
||||
"resumeError": "플레이어가 일시 중지되지 않았습니다.",
|
||||
"resumeVote": "{0}님이 노래 재개에 투표했습니다. [{1}/{2}]",
|
||||
"resumed": "`{0}`이(가) 플레이어를 재개했습니다.",
|
||||
"shuffleError": "섞기 전에 대기열에 더 많은 노래를 추가하십시오.",
|
||||
"shuffleVote": "{0}이(가) 대기열을 섞기 위해 투표했습니다. [{1}/{2}]",
|
||||
"shuffled": "대기열이 섞입니다.",
|
||||
"skipError": "건너뛸 노래가 없습니다.",
|
||||
"skipVote": "{0}님이 노래 건너뛰기에 투표했습니다. [{1}/{2}]",
|
||||
"skipped": "`{0}`이(가) 노래를 건너뛰었습니다.",
|
||||
|
||||
"backVote": "{0}이(가) 이전 노래로 건너뛰도록 투표했습니다. [{1}/{2}]",
|
||||
"backed": "`{0}`이(가) 이전 노래로 건너뛰었습니다.",
|
||||
|
||||
"leaveVote": "{0}이(가) 플레이어를 중지하는 데 투표했습니다. [{1}/{2}]",
|
||||
"left": "`{0}`이(가) 플레이어를 중지했습니다.",
|
||||
|
||||
"seek": "플레이어를 **{0}**으로 설정",
|
||||
"repeat": "반복 모드가 `{0}`(으)로 설정되었습니다.",
|
||||
"cleared": "`{0}`의 모든 트랙을 지웠습니다.",
|
||||
"removed": "`{0}` 트랙이 대기열에서 제거되었습니다.",
|
||||
"forward": "플레이어를 **{0}**로 전달",
|
||||
"rewind": "플레이어를 **{0}**으로 되감기",
|
||||
"replay": "현재 노래를 재생합니다.",
|
||||
"moved": "{0} 및 {1}이(가) 바뀌었습니다.",
|
||||
"autoplay": "자동재생 모드는 이제 **{0}**입니다.",
|
||||
|
||||
"notdj": "당신은 DJ가 아닙니다. 현재 DJ는 {0}입니다.",
|
||||
"djToMe": "DJ를 자신이나 봇에게 전송할 수 없습니다.",
|
||||
"djnotinchannel": "`{0}`은(는) 음성 채널에 없습니다.",
|
||||
"djswap": "dj의 역할을 `{0}`에게 이전했습니다.",
|
||||
|
||||
"chaptersDropdown": "건너뛸 챕터 선택...",
|
||||
"noChaptersFound": "챕터를 찾을 수 없습니다!",
|
||||
"chatpersNotSupport": "이 명령은 youtube 동영상만 지원합니다!",
|
||||
|
||||
"voicelinkQueueFull": "죄송합니다. 대기열의 최대 `{0}` 트랙에 도달했습니다!",
|
||||
"voicelinkOutofList": "유효한 트랙 색인을 제공하십시오!",
|
||||
"voicelinkDuplicateTrack": "죄송합니다. 이 트랙은 이미 대기열에 있습니다."
|
||||
}
|
||||
217
local_langs/zh-TW.json
Normal file
217
local_langs/zh-TW.json
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"language": "語言",
|
||||
"dj": "dj",
|
||||
"247": "247",
|
||||
"bypassvote": "繞過投票",
|
||||
"serversettings": "服務器設置",
|
||||
"volume": "音量",
|
||||
"togglecontroller": "切換控制器",
|
||||
"ping": "發信",
|
||||
"debug": "調試",
|
||||
"connect": "連接",
|
||||
"play": "播放",
|
||||
"search": "搜索",
|
||||
"playtop": "播放頂端",
|
||||
"pause": "暫停",
|
||||
"resume": "繼續",
|
||||
"skip": "跳過",
|
||||
"back": "退回",
|
||||
"seek": "尋找",
|
||||
"queue": "隊列",
|
||||
"history": "歷史隊列",
|
||||
"leave": "離開",
|
||||
"nowplaying": "正在播放",
|
||||
"loop": "循環",
|
||||
"clear": "清除",
|
||||
"remove": "移除",
|
||||
"forward": "向前",
|
||||
"rewind": "倒帶",
|
||||
"replay": "重播",
|
||||
"shuffle": "洗牌",
|
||||
"move": "移動",
|
||||
"lyrics": "歌詞",
|
||||
"swapdj": "交換dj",
|
||||
"chapters": "章節",
|
||||
"autoplay": "自動播放",
|
||||
"help": "幫助",
|
||||
"donate": "捐贈",
|
||||
"vote": "投票",
|
||||
"speed": "速度",
|
||||
"karaoke": "卡拉ok",
|
||||
"tremolo": "震音",
|
||||
"vibrato": "顫音",
|
||||
"rotation": "迴轉",
|
||||
"distortion": "失真",
|
||||
"lowpass": "低通",
|
||||
"channelmix": "通道混合",
|
||||
"nightcore": "夜心",
|
||||
"effectclear": "效果清晰",
|
||||
"view": "查看",
|
||||
"create": "創建",
|
||||
"delete": "刪除",
|
||||
"share": "分享",
|
||||
"rename": "改名",
|
||||
"inbox": "收件箱",
|
||||
"add": "添加",
|
||||
"You can choose your preferred language, the bot message will change to the language you set.": "您可以選擇首選語言,機器人消息將更改為您設置的語言。",
|
||||
"…": "…",
|
||||
"Set a DJ role or remove DJ role.": "設置 DJ 角色或刪除 DJ 角色。",
|
||||
"role": "角色",
|
||||
"Toggles 24/7 mode, which disables automatic inactivity-based disconnects.": "切換 24/7 模式,該模式禁用基於不活動的自動斷開連接。",
|
||||
"Toggles voting system.": "切換投票系統。",
|
||||
"Show all the bot settings in your server.": "顯示服務器中的所有機器人設置。",
|
||||
"Set the player's volume.": "設置播放器的音量。",
|
||||
"value": "價值",
|
||||
"Input a integer.": "輸入一個整數。",
|
||||
"Toggles the music controller.": "切換音樂控制器。",
|
||||
"Test if the bot is alive, and see the delay between your commands and my response.": "測試機器人是否還活著,看看你的命令和我的響應之間的延遲。",
|
||||
"Connect to a voice channel.": "連接到語音通道。",
|
||||
"channel": "渠道",
|
||||
"Provide a channel to connect.": "提供連接渠道。",
|
||||
"Loads your input and added it to the queue.": "加載您的輸入並將其添加到隊列中。",
|
||||
"query": "詢問",
|
||||
"Input a query or a searchable link.": "輸入查詢或可搜索鏈接。",
|
||||
"Input the name of the song.": "輸入歌曲名稱。",
|
||||
"platform": "平台",
|
||||
"Select the platform you want to search.": "選擇您要搜索的平台。",
|
||||
"Youtube": "YouTube",
|
||||
"Youtube Music": "YouTube 音樂",
|
||||
"SoundCloud": "聲雲",
|
||||
"Spotify": "Spotify",
|
||||
"Apple Music": "蘋果音樂",
|
||||
"Adds a song with the given url or query on the top of the queue.": "在隊列頂部添加具有給定網址或查詢的歌曲。",
|
||||
"Input the name of the song. (Without URL)": "輸入歌曲名稱。(無網址)",
|
||||
"Pause the music.": "暫停音樂。",
|
||||
"Resume the music.": "恢復音樂。",
|
||||
"Skips to the next song or skips to the specified song.": "跳到下一首歌曲或跳到指定的歌曲。",
|
||||
"index": "指數",
|
||||
"Enter a index that you want to skip to.": "輸入要跳到的索引。",
|
||||
"Skips back to the previous song or skips to the specified previous song.": "跳回上一首歌曲或跳到指定的上一首歌曲。",
|
||||
"Enter a index that you want to skip back to.": "輸入要跳回的索引。",
|
||||
"Change the player position.": "改變玩家位置。",
|
||||
"position": "位置",
|
||||
"Input position. Exmaple: 1:20.": "輸入位置。示例:1:20。",
|
||||
"Display the players queue songs in your queue.": "在您的隊列中顯示玩家隊列歌曲。",
|
||||
"Display the players queue songs in your history queue.": "在您的歷史隊列中顯示玩家隊列歌曲。",
|
||||
"Disconnects the bot from your voice channel and chears the queue.": "斷開機器人與您的語音通道的連接並監聽隊列。",
|
||||
"Shows details of the current track.": "顯示當前曲目的詳細信息。",
|
||||
"Changes Loop mode.": "更改循環模式。",
|
||||
"mode": "模式",
|
||||
"Choose a looping mode.": "選擇循環模式。",
|
||||
"Off": "關掉",
|
||||
"Track": "曲目",
|
||||
"Queue": "隊列",
|
||||
"Remove all the tracks in your queue or history queue.": "刪除隊列或歷史隊列中的所有曲目。",
|
||||
"Choose a queue that you want to clear.": "選擇要清除的隊列。",
|
||||
"History": "歷史",
|
||||
"Removes specified track or a range of tracks from the queue.": "從隊列中刪除指定的曲目或一系列曲目。",
|
||||
"Input a position from the queue to be removed.": "從隊列中輸入要刪除的位置。",
|
||||
"position2": "位置2",
|
||||
"Set the range of the queue to be removed.": "設置要刪除的隊列範圍。",
|
||||
"Forwards by a certain amount of time in the current track. The default is 10 seconds.": "在當前曲目中前進一定時間。默認值為 10 秒。",
|
||||
"Input a amount that you to forward to. Exmaple: 1:20": "輸入您要轉發的時間。示例:1:20",
|
||||
"Rewind by a certain amount of time in the current track. The default is 10 seconds.": "在當前曲目中倒退一定時間。默認值為 10 秒。",
|
||||
"Input a amount that you to rewind to. Exmaple: 1:20": "輸入您要倒回的時間。示例:1:20",
|
||||
"Reset the progress of the current song.": "重置當前歌曲的進度。",
|
||||
"Randomizes the tracks in the queue.": "隨機化隊列中的曲目。",
|
||||
"Moves the specified song to the specified position.": "將指定歌曲移動到指定位置。",
|
||||
"track": "曲目",
|
||||
"The track to move. Example: 2": "要移動的曲目。示例:2",
|
||||
"The new position to move the track to. Exmaple: 1": "要將曲目移動到的新位置。示例:1",
|
||||
"Displays lyrics for the playing track.": "顯示播放曲目的歌詞。",
|
||||
"name": "歌名",
|
||||
"Searches for your query and displays the reutned lyrics.": "搜索您的查詢並顯示 reutned 歌詞。",
|
||||
"author": "作者",
|
||||
"Seaches for lyrics by author.": "按作者搜索歌詞。",
|
||||
"Transfer dj to another.": "將 dj 轉移到另一個。",
|
||||
"member": "成員",
|
||||
"Choose a member to transfer the dj role.": "選擇一個成員來轉移 dj 角色。",
|
||||
"Lists all chapters of the currently playing song (if any).": "列出當前播放歌曲的所有章節(如果有)。",
|
||||
"Toggles autoplay mode, it will automatically queue the best songs to play.": "切換自動播放模式,它會自動排隊播放最好的歌曲。",
|
||||
"Lists all the commands in Vocard.": "列出 Vocard 中的所有命令。",
|
||||
"category": "類別",
|
||||
"Basic": "基本",
|
||||
"Tutorial": "教程",
|
||||
"DJ": "DJ",
|
||||
"Admin": "管理員",
|
||||
"Show all the donate plan on Vocard.": "在 Vocard 上顯示所有捐贈計劃。",
|
||||
"You can buy plan by voting Vocard on Top.gg.": "您可以通過在 Top.gg 上投票 Vocard 來購買計劃。",
|
||||
"Sets the player's playback speed.": "設置播放器的播放速度。",
|
||||
"The value to set the speed to. Default is `1.0`": "將速度設置為的值。默認為`1.0`",
|
||||
"Uses equalization to eliminate part of a band, usually targeting vocals.": "使用均衡來消除樂隊的一部分,通常針對人聲。",
|
||||
"level": "等級",
|
||||
"The level of the karaoke. Default is `1.0`": "卡拉OK的水平。默認為`1.0`",
|
||||
"monolevel": "單級",
|
||||
"The monolevel of the karaoke. Default is `1.0`": "卡拉OK的單聲道。默認為`1.0`",
|
||||
"filterband": "濾帶",
|
||||
"The filter band of the karaoke. Default is `220.0`": "卡拉 OK 的濾波器頻段。默認為`220.0`",
|
||||
"filterwidth": "過濾器寬度",
|
||||
"The filter band of the karaoke. Default is `100.0`": "卡拉 OK 的濾波器頻段。默認為`100.0`",
|
||||
"Uses amplification to create a shuddering effect, where the volume quickly oscillates.": "使用放大來產生顫抖的效果,音量會快速振盪。",
|
||||
"frequency": "頻率",
|
||||
"The frequency of the tremolo. Default is `2.0`": "顫音的頻率。默認為`2.0`",
|
||||
"depth": "深度",
|
||||
"The depth of the tremolo. Default is `0.5`": "顫音的深度。默認為`0.5`",
|
||||
"Similar to tremolo. While tremolo oscillates the volume, vibrato oscillates the pitch.": "類似於顫音。顫音使音量振盪,而顫音則使音高振盪。",
|
||||
"The frequency of the vibrato. Default is `2.0`": "顫音的頻率。默認為`2.0`",
|
||||
"The Depth of the vibrato. Default is `0.5`": "顫音的深度。默認為`0.5`",
|
||||
"Rotates the sound around the stereo channels/user headphones aka Audio Panning.": "圍繞立體聲通道/用戶耳機旋轉聲音,即音頻平移。",
|
||||
"hertz": "赫茲",
|
||||
"The hertz of the rotation. Default is `0.2`": "旋轉的赫茲。默認為`0.2`",
|
||||
"Distortion effect. It can generate some pretty unique audio effects.": "失真效果。它可以產生一些非常獨特的音頻效果。",
|
||||
"Filter which supresses higher frequencies and allows lower frequencies to pass.": "抑制較高頻率並允許較低頻率通過的濾波器。",
|
||||
"smoothing": "平滑",
|
||||
"The level of the lowPass. Default is `20.0`": "低通的水平。默認為`20.0`",
|
||||
"Filter which manually adjusts the panning of the audio.": "手動調整音頻平移的過濾器。",
|
||||
"left_to_left": "左到左",
|
||||
"Sounds from left to left. Default is `1.0`": "聲音從左到左。默認為`1.0`",
|
||||
"right_to_right": "右到右",
|
||||
"Sounds from right to right. Default is `1.0`": "聲音從右到右。默認為`1.0`",
|
||||
"left_to_right": "左到右",
|
||||
"Sounds from left to right. Default is `0.0`": "聲音從左到右。默認為`0.0`",
|
||||
"right_to_left": "右到左",
|
||||
"Sounds from right to left. Default is `0.0`": "聲音從右到左。默認為`0.0`",
|
||||
"Add nightcore filter into your player.": "將夜核心過濾器添加到您的播放器中。",
|
||||
"Clear all the sound effect.": "清除所有音效。",
|
||||
"playlist": "播放列表",
|
||||
"Play all songs from your favorite playlist.": "播放您喜愛的播放列表中的所有歌曲。",
|
||||
"Input the name of your custom playlist": "輸入您的自定義播放列表的名稱",
|
||||
"Play the specific track from your custom playlist.": "播放自定義播放列表中的特定曲目。",
|
||||
"List all your playlist and all songs in your favourite playlist.": "列出您喜歡的播放列表中的所有播放列表和所有歌曲。",
|
||||
"Create your custom playlist.": "創建您的自定義播放列表。",
|
||||
"Give a name to your playlist.": "為您的播放列表命名。",
|
||||
"link": "網址",
|
||||
"Provide a playlist link if you are creating link playlist.": "如果您正在創建鏈接播放列表,請提供播放列錶鍊接。",
|
||||
"Delete your custom playlist.": "刪除您的自定義播放列表。",
|
||||
"The name of the playlist.": "播放列表的名稱。",
|
||||
"Share your custom playlist with your friends.": "與您的朋友分享您的自定義播放列表。",
|
||||
"The user id of your friend.": "您朋友的用戶 ID。",
|
||||
"The name of the playlist that you want to share.": "您要共享的播放列表的名稱。",
|
||||
"Rename your custom playlist.": "重命名您的自定義播放列表。",
|
||||
"The name of your playlist.": "您的播放列表的名稱。",
|
||||
"newname": "新名字",
|
||||
"The new name of your playlist.": "播放列表的新名稱。",
|
||||
"Show your playlist invitation.": "顯示您的播放列表邀請。",
|
||||
"Add tracks in to your custom playlist.": "將曲目添加到您的自定義播放列表中。",
|
||||
"Remove song from your favorite playlist.": "從您最喜歡的播放列表中刪除歌曲。",
|
||||
"Input a position from the playlist to be removed.": "從播放列表中輸入要刪除的位置。",
|
||||
"Remove all songs from your favorite playlist.": "從您喜歡的播放列表中刪除所有歌曲。",
|
||||
"8d": "8d",
|
||||
"Add 8D filter into your player.": "在播放器中添加 8D 濾鏡。",
|
||||
"cleareffect": "清除音效",
|
||||
"Clear all or specific sound effects.": "清除所有或特定音效。",
|
||||
"effect": "音效",
|
||||
"Remove a specific sound effects.": "移除特定音效。",
|
||||
"vaporwave": "蒸汽波",
|
||||
"Add vaporwave filter into your player.": "將蒸汽波過濾器添加到您的播放器中。",
|
||||
"Change to another type of queue mode.": "更改為另一種隊列模式。",
|
||||
"FairQueue": "公平隊列",
|
||||
"Playlist": "播放列表",
|
||||
"duplicatetrack": "重複軌道",
|
||||
"toggle": "切換",
|
||||
"Enable": "啟用",
|
||||
"Disable": "停用",
|
||||
"Toggle Vocard to prevent duplicate songs from queuing.": "切換 Vocard 以防止重複歌曲排隊。",
|
||||
"Remove tracks requested by a specific member.": "刪除特定成員請求的曲目。",
|
||||
"settings": "設置"
|
||||
}
|
||||
103
main.py
Normal file
103
main.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import discord
|
||||
import sys, os, traceback, aiohttp
|
||||
import update
|
||||
|
||||
from function import (
|
||||
langs_setup,
|
||||
settings_setup,
|
||||
local_langs,
|
||||
get_lang,
|
||||
invite_link,
|
||||
error_log
|
||||
)
|
||||
from discord.ext import commands
|
||||
from dotenv import load_dotenv
|
||||
from googletrans import Translator
|
||||
from datetime import datetime
|
||||
from voicelink import VoicelinkException
|
||||
|
||||
translator = Translator()
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class Translator(discord.app_commands.Translator):
|
||||
async def load(self):
|
||||
print("Loaded Translator")
|
||||
|
||||
async def unload(self):
|
||||
print("Unload Translator")
|
||||
|
||||
async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext):
|
||||
if str(locale) in local_langs:
|
||||
text = local_langs[str(locale)].get(string.message, None)
|
||||
if not text:
|
||||
result = translator.translate(string.message, str(locale).lower())
|
||||
print(result.origin, ' -> ', result.text)
|
||||
return result.text
|
||||
return text
|
||||
return None
|
||||
|
||||
class Vocard(commands.Bot):
|
||||
|
||||
async def on_message(self, message):
|
||||
pass
|
||||
|
||||
async def setup_hook(self):
|
||||
langs_setup()
|
||||
settings_setup()
|
||||
for module in os.listdir('./cogs'):
|
||||
if module.endswith('.py'):
|
||||
try:
|
||||
await self.load_extension(f"cogs.{module[:-3]}")
|
||||
print(f"Loaded {module[:-3]}")
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
|
||||
# await bot.tree.set_translator(Translator())
|
||||
# await bot.tree.sync()
|
||||
|
||||
async def on_ready(self):
|
||||
print("------------------")
|
||||
print(f"Logging As {self.user}")
|
||||
print(f"Bot ID: {self.user.id}")
|
||||
print("------------------")
|
||||
print(f"Discord Version: {discord.__version__}")
|
||||
print(f"Python Version: {sys.version}")
|
||||
print("------------------")
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
member_cache = discord.MemberCacheFlags(
|
||||
voice=True,
|
||||
joined=False
|
||||
)
|
||||
|
||||
bot = Vocard(command_prefix="?",
|
||||
help_command=None,
|
||||
chunk_guilds_at_startup = False,
|
||||
member_cache_flags=member_cache,
|
||||
activity=discord.Activity(type=discord.ActivityType.listening,name="/help"),
|
||||
intents=intents)
|
||||
|
||||
@bot.tree.error
|
||||
async def app_command_error(interaction: discord.Interaction, error):
|
||||
error = getattr(error, 'original', error)
|
||||
if isinstance(error, (discord.errors.NotFound, aiohttp.client_exceptions.ClientOSError)):
|
||||
return
|
||||
elif isinstance(error, (discord.app_commands.CommandOnCooldown, discord.app_commands.errors.MissingPermissions)):
|
||||
pass
|
||||
elif not issubclass(error.__class__, VoicelinkException):
|
||||
error = get_lang(interaction.guild_id, "unknownException") + invite_link
|
||||
if (guildId := interaction.guild_id) not in error_log:
|
||||
error_log[guildId] = {}
|
||||
error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
|
||||
try:
|
||||
if interaction.response.is_done():
|
||||
return await interaction.followup.send(error, ephemeral=True)
|
||||
|
||||
return await interaction.response.send_message(error, ephemeral=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
update.checkVersion(withMsg = True)
|
||||
bot.run(os.getenv("TOKEN"), log_handler=None)
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
discord.py==2.1.0
|
||||
pymongo==4.1.1
|
||||
dnspython==2.2.1
|
||||
tldextract==3.2.1
|
||||
validators==0.18.2
|
||||
humanize==4.0.0
|
||||
python-dotenv==0.20.0
|
||||
beautifulsoup4==4.11.1
|
||||
24
settings Example.json
Normal file
24
settings Example.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"nodes": {
|
||||
"DEFAULT": {"host": "127.0.0.1",
|
||||
"port": 2333,
|
||||
"password": "password",
|
||||
"secure": false,
|
||||
"identifier": "DEFAULT"
|
||||
}
|
||||
},
|
||||
"bot_access_user": [],
|
||||
"color_code":"",
|
||||
"emoji_source_raw": {
|
||||
"youtube": "<:youtube:826661982760992778>",
|
||||
"youtube music": "<:youtube:826661982760992778>",
|
||||
"spotify": "<:spotify:826661996615172146>",
|
||||
"soundcloud": "<:soundcloud:852729280027033632>",
|
||||
"twitch": "<:twitch:852729278285086741>",
|
||||
"bandcamp": "<:bandcamp:864694003811221526>",
|
||||
"vimeo": "<:vimeo:864694001919721473>",
|
||||
"apple": "<:applemusic:994844332374884413>",
|
||||
"reddit": "<:reddit:996007566863773717>",
|
||||
"tiktok": "<:tiktok:996007689798811698>"
|
||||
}
|
||||
}
|
||||
55
update.py
Normal file
55
update.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import requests, zipfile, os, sys, shutil
|
||||
|
||||
root_dir = "./"
|
||||
filename = root_dir + "Vocard.zip"
|
||||
__version__ = "v2.5.0"
|
||||
|
||||
def checkVersion(withMsg = False):
|
||||
resp = requests.get("https://api.github.com/repos/ChocoMeow/Vocard/releases/latest")
|
||||
version = resp.json().get("name", __version__)
|
||||
if withMsg:
|
||||
if version == __version__:
|
||||
print(f"Your bot is up-to-date! - {version}")
|
||||
else:
|
||||
print(f"Your bot is not up-to-date! This latest version is {version} and you are currently running version {__version__}\n. Run `python update --start` to update your bot!")
|
||||
return version
|
||||
|
||||
def downloadFile(version:str = None):
|
||||
if not version:
|
||||
version = checkVersion()
|
||||
response = requests.get("https://github.com/ChocoMeow/Vocard/archive/" + version + ".zip")
|
||||
open(filename, 'wb').write(response.content)
|
||||
unZip(filename, version)
|
||||
|
||||
def unZip(path: str, version: str):
|
||||
with zipfile.ZipFile(path,"r") as f:
|
||||
f.extractall()
|
||||
|
||||
version = version.replace("v", "")
|
||||
source_dir = root_dir + "Vocard-" + version
|
||||
if os.path.exists(source_dir):
|
||||
for filename in os.listdir(root_dir):
|
||||
if filename in ["settings.json", ".env", "Vocard-" + version]:
|
||||
continue
|
||||
if os.path.isdir(os.path.join(root_dir, filename)):
|
||||
shutil.rmtree(filename)
|
||||
else:
|
||||
os.remove(filename)
|
||||
for filename in os.listdir(source_dir):
|
||||
shutil.move(os.path.join(source_dir, filename), os.path.join(root_dir, filename))
|
||||
os.rmdir(source_dir)
|
||||
|
||||
def start():
|
||||
try:
|
||||
downloadFile()
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
print("Update Successfully! Run `python main.py` to start your bot")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if "--start" in sys.argv:
|
||||
start()
|
||||
|
||||
if "--check" in sys.argv:
|
||||
checkVersion(withMsg = True)
|
||||
16
view/__init__.py
Normal file
16
view/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from discord.ext import commands
|
||||
|
||||
class ButtonOnCooldown(commands.CommandError):
|
||||
def __init__(self, retry_after: float) -> None:
|
||||
self.retry_after = retry_after
|
||||
|
||||
from .controller import InteractiveController
|
||||
from .search import SearchView
|
||||
from .help import HelpView
|
||||
from .list import ListView
|
||||
from .lyrics import LyricsView
|
||||
from .chapter import ChapterView
|
||||
from .playlist import PlaylistView, CreateView
|
||||
from .inbox import InboxView
|
||||
from .link import LinkView
|
||||
from .debug import DebugModal
|
||||
83
view/chapter.py
Normal file
83
view/chapter.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from function import formatTime
|
||||
|
||||
class Dropdown(discord.ui.Select):
|
||||
def __init__(self, player, chapters):
|
||||
|
||||
self.player = player
|
||||
self.chapters = chapters
|
||||
self.current = player.current.uri
|
||||
|
||||
options = [
|
||||
discord.SelectOption(label=f"{index}. {title[:30]}",
|
||||
description=time)
|
||||
for index, (time, title) in enumerate(self.chapters, start=1)
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
placeholder=self.player.get_msg('chaptersDropdown'),
|
||||
min_values=1, max_values=1,
|
||||
options=options,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
position = formatTime(self.chapters[int(self.values[0].split(". ")[0]) - 1][0])
|
||||
if position is None:
|
||||
return
|
||||
if not self.player.current or self.current != self.player.current.uri:
|
||||
return await self.view.stop_view()
|
||||
|
||||
await self.player.seek(position)
|
||||
|
||||
class ChapterView(discord.ui.View):
|
||||
def __init__(self, player, chapters, author):
|
||||
super().__init__(timeout=180)
|
||||
|
||||
self.player = player
|
||||
self.chapters = chapters
|
||||
self.author = author
|
||||
self.response = None
|
||||
self.add_item(self.Dropdown(player, chapters))
|
||||
|
||||
async def on_error(self, error: Exception, item, interaction) -> None:
|
||||
return
|
||||
|
||||
async def stop_view(self) -> None:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
await self.stop_view()
|
||||
|
||||
async def interaction_check(self, interaction):
|
||||
if interaction.user == self.author:
|
||||
return True
|
||||
return False
|
||||
242
view/controller.py
Normal file
242
view/controller.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from discord.ext import commands
|
||||
from . import ButtonOnCooldown
|
||||
from function import (
|
||||
get_playlist,
|
||||
update_playlist,
|
||||
create_account,
|
||||
checkroles
|
||||
)
|
||||
|
||||
def key(interaction: discord.Interaction):
|
||||
return interaction.user
|
||||
|
||||
class previous_command(discord.ui.Button):
|
||||
def __init__(self, player):
|
||||
self.player = player
|
||||
super().__init__(emoji="⏮️", label=player.get_msg('buttonBack'), style=discord.ButtonStyle.secondary, disabled=False if self.player.queue.history() or not self.player.current else True)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not await self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.previous_votes:
|
||||
return await interaction.response.send_message(self.player.get_msg("voted"), ephemeral=True)
|
||||
else:
|
||||
self.player.previous_votes.add(interaction.user)
|
||||
if len(self.player.previous_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(self.player.get_msg("backVote").format(interaction.user, len(self.player.previous_votes), required))
|
||||
|
||||
if not self.player.node._available:
|
||||
return await interaction.response.send_message(self.player.get_msg("nodeReconnect"))
|
||||
|
||||
if not self.player.is_playing:
|
||||
self.player.queue.backto(1)
|
||||
await self.player.do_next()
|
||||
else:
|
||||
self.player.queue.backto(2)
|
||||
await self.player.stop()
|
||||
|
||||
await interaction.response.send_message(self.player.get_msg("backed").format(interaction.user))
|
||||
|
||||
if self.player.queue.repeat == "Track":
|
||||
self.player.queue.set_repeat("Off")
|
||||
|
||||
class resume_command(discord.ui.Button):
|
||||
def __init__(self, player):
|
||||
self.player = player
|
||||
super().__init__(emoji="⏸️", label=player.get_msg('buttonPause'), style=discord.ButtonStyle.secondary, disabled=False if self.player.current else True)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if self.player.is_paused:
|
||||
if not await self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.resume_votes:
|
||||
return await interaction.response.send_message(self.player.get_msg("voted"), ephemeral=True)
|
||||
else:
|
||||
self.player.resume_votes.add(interaction.user)
|
||||
if len(self.player.resume_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(self.player.get_msg("resumeVote").format(interaction.user, len(self.player.resume_votes), required))
|
||||
|
||||
self.player.resume_votes.clear()
|
||||
self.emoji = "⏸️"
|
||||
self.label = self.player.get_msg("buttonPause")
|
||||
await self.player.set_pause(False)
|
||||
|
||||
else:
|
||||
if not await self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.pause_votes:
|
||||
return await interaction.response.send_message(self.player.get_msg("voted"), ephemeral=True)
|
||||
else:
|
||||
self.player.pause_votes.add(interaction.user)
|
||||
if len(self.player.pause_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(self.player.get_msg("pauseVote").format(interaction.user, len(self.player.pause_votes), required))
|
||||
|
||||
self.player.pause_votes.clear()
|
||||
self.emoji = "▶️"
|
||||
self.label = self.player.get_msg("buttonResume")
|
||||
await self.player.set_pause(True)
|
||||
await interaction.response.edit_message(view=self.view)
|
||||
|
||||
class skip_command(discord.ui.Button):
|
||||
def __init__(self, player):
|
||||
self.player = player
|
||||
super().__init__(emoji="⏭️", label=player.get_msg('buttonSkip'), style=discord.ButtonStyle.secondary)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_playing:
|
||||
return
|
||||
if not await self.player.is_privileged(interaction.user):
|
||||
if interaction.user == self.player.current.requester:
|
||||
pass
|
||||
elif interaction.user in self.player.skip_votes:
|
||||
return await interaction.response.send_message(self.player.get_msg("voted"), ephemeral=True)
|
||||
else:
|
||||
self.player.skip_votes.add(interaction.user)
|
||||
if len(self.player.skip_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(self.player.get_msg("skipVote").format(interaction.user, len(self.player.skip_votes), required))
|
||||
|
||||
if not self.player.node._available:
|
||||
return await interaction.response.send_message(self.player.get_msg("nodeReconnect"))
|
||||
|
||||
await interaction.response.send_message(self.player.get_msg("skipped").format(interaction.user))
|
||||
|
||||
if self.player.queue.repeat == "Track":
|
||||
self.player.queue.set_repeat("Off")
|
||||
await self.player.stop()
|
||||
|
||||
class stop_command(discord.ui.Button):
|
||||
def __init__(self, player):
|
||||
self.player = player
|
||||
super().__init__(emoji="⏹️", label=player.get_msg('buttonLeave'), style=discord.ButtonStyle.danger)
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not await self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.stop_votes:
|
||||
return await interaction.response.send_message(self.player.get_msg("voted"), ephemeral=True)
|
||||
else:
|
||||
self.player.stop_votes.add(interaction.user)
|
||||
if len(self.player.stop_votes) >= (required := self.player.required(leave=True)):
|
||||
pass
|
||||
else:
|
||||
return await interaction.response.send_message(self.player.get_msg("leaveVote").format(interaction.user, len(self.player.stop_votes), required))
|
||||
|
||||
await interaction.response.send_message(self.player.get_msg("left").format(interaction.user))
|
||||
await self.player.teardown()
|
||||
|
||||
class add_playlist(discord.ui.Button):
|
||||
def __init__(self, player):
|
||||
self.player = player
|
||||
super().__init__(emoji="❤️", style=discord.ButtonStyle.secondary, disabled=False if self.player.current else True)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
track = self.player.current
|
||||
if not track:
|
||||
return await interaction.response.send_message(self.player.get_msg("noTrackPlaying"))
|
||||
if track.is_stream:
|
||||
return await interaction.response.send_message(self.player.get_msg("playlistAddError"))
|
||||
user = await get_playlist(interaction.user.id, 'playlist')
|
||||
if not user:
|
||||
return await create_account(interaction)
|
||||
rank, max_p, max_t = await checkroles(interaction.user.id)
|
||||
if len(user['200']['tracks']) >= max_t:
|
||||
return await interaction.response.send_message(self.player.get_msg("playlistlimited").format(max_t), ephemeral=True)
|
||||
addtrack = {'id': track.track_id,
|
||||
'info':{'identifier': track.identifier,
|
||||
'author': track.author,
|
||||
'length': track.length / 1000,
|
||||
'title': track.title,
|
||||
'uri': track.uri}}
|
||||
if addtrack in user['200']['tracks']:
|
||||
return await interaction.response.send_message(self.player.get_msg("playlistrepeated"), ephemeral=True)
|
||||
respond = await update_playlist(interaction.user.id, {'playlist.200.tracks': addtrack}, push=True)
|
||||
if respond:
|
||||
await interaction.response.send_message(self.player.get_msg("playlistAdded").format(track.title, interaction.user.mention, user['200']['name']), ephemeral=True)
|
||||
else:
|
||||
await interaction.response.send_message(self.player.get_msg("playlistAddError2"), ephemeral=True)
|
||||
|
||||
class Dropdown(discord.ui.Select):
|
||||
def __init__(self, player):
|
||||
|
||||
self.player = player
|
||||
|
||||
options = []
|
||||
for index, track in enumerate(self.player.queue.tracks(), start=1):
|
||||
if index > 10:
|
||||
break
|
||||
options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatLength), emoji=track.emoji))
|
||||
|
||||
super().__init__(
|
||||
placeholder=player.get_msg("playerDropdown"),
|
||||
min_values=1, max_values=1,
|
||||
options=options,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not await self.player.is_privileged(interaction.user):
|
||||
return await interaction.response.send_message(self.player.get_msg("missingPerms_function"), ephemeral=True)
|
||||
|
||||
await self.player.queue.skipto(int(self.values[0].split(". ")[0]))
|
||||
await self.player.stop()
|
||||
await interaction.response.send_message(self.player.get_msg("skipped").format(interaction.user))
|
||||
|
||||
class InteractiveController(discord.ui.View):
|
||||
def __init__(self, player):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
self.player = player
|
||||
self.add_item(previous_command(player))
|
||||
self.add_item(resume_command(player))
|
||||
self.add_item(skip_command(player))
|
||||
self.add_item(stop_command(player))
|
||||
self.add_item(add_playlist(player))
|
||||
if not self.player.queue.is_empty:
|
||||
self.add_item(Dropdown(player))
|
||||
self.cooldown = commands.CooldownMapping.from_cooldown(2.0, 10.0, key)
|
||||
|
||||
async def interaction_check(self, interaction):
|
||||
if self.player.channel and interaction.user in self.player.channel.members:
|
||||
retry_after = self.cooldown.update_rate_limit(interaction)
|
||||
if retry_after:
|
||||
raise ButtonOnCooldown(retry_after)
|
||||
return True
|
||||
else:
|
||||
await interaction.response.send_message(self.player.get_msg("notInChannel").format(interaction.user.mention, self.player.channel.mention), ephemeral=True)
|
||||
return False
|
||||
|
||||
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item):
|
||||
if isinstance(error, ButtonOnCooldown):
|
||||
try:
|
||||
sec = int(error.retry_after)
|
||||
await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True)
|
||||
except:
|
||||
return
|
||||
return
|
||||
41
view/debug.py
Normal file
41
view/debug.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
class DebugModal(discord.ui.Modal):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.values = None
|
||||
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Code Runner",
|
||||
placeholder="Input Your Code",
|
||||
style=discord.TextStyle.long,
|
||||
)
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
self.values = self.children[0].value
|
||||
self.stop()
|
||||
120
view/help.py
Normal file
120
view/help.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from os import getenv
|
||||
from function import (
|
||||
embed_color,
|
||||
invite_link
|
||||
)
|
||||
|
||||
class HelpDropdown(discord.ui.Select):
|
||||
def __init__(self, category:list):
|
||||
options = [
|
||||
discord.SelectOption(emoji="🆕", label="News", description="View new updates of Vocard."),
|
||||
discord.SelectOption(emoji="🕹️", label="Tutorial", description="How to use Vocard."),
|
||||
]
|
||||
cog_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"]
|
||||
for category, emoji in zip(category, cog_emojis):
|
||||
options.append(discord.SelectOption(emoji=emoji,
|
||||
label=f"{category} Commands",
|
||||
description=f"This is {category} Category."))
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select Category!",
|
||||
min_values=1, max_values=1,
|
||||
options=options, custom_id="select"
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
embed = self.view.build_embed(self.values[0].split(" ")[0])
|
||||
await interaction.response.edit_message(embed=embed)
|
||||
|
||||
class HelpView(discord.ui.View):
|
||||
def __init__(self, bot: commands.Bot, author: discord.Member):
|
||||
super().__init__(timeout=60)
|
||||
|
||||
self.author = author
|
||||
self.bot = bot
|
||||
self.response = None
|
||||
self.categorys = [ category.capitalize() for category in bot.cogs if category not in ["Task", "Nodes"] ]
|
||||
|
||||
self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=invite_link))
|
||||
self.add_item(discord.ui.Button(label='Invite', emoji=':invite:915152589056790589', url='https://discord.com/oauth2/authorize?client_id={}&permissions=2184260928&scope=bot%20applications.commands'.format(getenv('CLIENT_ID'))))
|
||||
self.add_item(discord.ui.Button(label='Github', url='https://github.com/ChocoMeow/Vocard'))
|
||||
self.add_item(discord.ui.Button(label='Donate', emoji=':patreon:913397909024800878', url='https://www.patreon.com/Vocard'))
|
||||
self.add_item(HelpDropdown(self.categorys))
|
||||
|
||||
async def on_error(self, error, item, interaction):
|
||||
return
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
if child.custom_id == "select":
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def interaction_check(self, interaction):
|
||||
if interaction.user == self.author:
|
||||
return True
|
||||
return False
|
||||
|
||||
def build_embed(self, category: str):
|
||||
if category == "News":
|
||||
embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=embed_color)
|
||||
|
||||
embed.add_field(name=f"Available Categories: [{2 + len(self.categorys)}]",
|
||||
value="```py\n👉 News\n2. Tutorial\n{}```".format("".join(f"{i}. {c}\n" for i, c in enumerate(self.categorys, start=3))),
|
||||
inline=True)
|
||||
|
||||
update = "> Update Contents\n" \
|
||||
"➥ More text support for other languages\n" \
|
||||
"➥ Add author name in playing track msg\n" \
|
||||
"➥ Fixed some commands\n\n" \
|
||||
"[Click Me For More Info](https://discord.com/channels/811542332678996008/811909963718459392/1063728318777671741)" \
|
||||
|
||||
embed.add_field(name="📰 Latest News <t:1661168201:d> (<t:1661168201:R>)", value=update, inline=True)
|
||||
embed.add_field(name="Get Started", value="```Join a voice channel and /play {Song/URL} a song. (Names, Youtube Video Links or Playlist links or Spotify links are supported on Vocard)```", inline=False)
|
||||
|
||||
return embed
|
||||
|
||||
embed = discord.Embed(title=f"Category: {category}", color=embed_color)
|
||||
embed.add_field(name=f"Categories: [{2 + len(self.categorys)}]", value="```py\n" + "\n".join(f"👉 {c}" if c == category else f"{i}. {c}" for i, c in enumerate(['News', 'Tutorial'] + self.categorys, start=1)) + "```", inline=True)
|
||||
|
||||
if category == 'Tutorial':
|
||||
embed.description = "How can use Vocard? Some simple commands you should know now after watching this video."
|
||||
embed.set_image(url="https://cdn.discordapp.com/attachments/674788144931012638/917656288899514388/final_61aef3aa7836890135c6010c_669380.gif")
|
||||
else:
|
||||
cog = [c for _, c in self.bot.cogs.items() if _.capitalize() == category][0]
|
||||
|
||||
commands = [command for command in cog.walk_app_commands()]
|
||||
embed.description = cog.description
|
||||
embed.add_field(name=f"{category} Commands: [{len(commands)}]",
|
||||
value="```{}```".format("".join(f"/{command.qualified_name}\n" for command in commands)))
|
||||
|
||||
return embed
|
||||
96
view/inbox.py
Normal file
96
view/inbox.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from function import embed_color
|
||||
|
||||
class Select_message(discord.ui.Select):
|
||||
def __init__(self, inbox):
|
||||
options = [discord.SelectOption(label=f"{index}. {mail['title'][:50]}", description=mail['type'], emoji='✉️' if mail['type'] == 'invite' else '📢') for index, mail in enumerate(inbox, start=1) ]
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select a message to view ..",
|
||||
options=options, custom_id='select'
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
self.view.current = self.view.inbox[int(self.values[0].split(". ")[0]) - 1]
|
||||
await self.view.button_change(interaction)
|
||||
|
||||
class InboxView(discord.ui.View):
|
||||
def __init__(self, author, inbox):
|
||||
super().__init__(timeout=60)
|
||||
self.author = author
|
||||
self.inbox = inbox
|
||||
self.response = None
|
||||
self.newplaylist = []
|
||||
|
||||
self.current = None
|
||||
self.add_item(Select_message(inbox))
|
||||
|
||||
def build_embed(self):
|
||||
embed=discord.Embed(title=f"📭 All {self.author}'s Inbox",
|
||||
description=f'Max Messages: {len(self.inbox)}/10' + '```%0s %2s %20s\n' % (" ", "ID:", "Title:") + '\n'.join('%0s %2s. %35s'% ('✉️' if mail['type'] == 'invite' else '📢', index, mail['title'][:35] + "...") for index, mail in enumerate(self.inbox, start=1)) + '```',
|
||||
color=embed_color)
|
||||
if self.current:
|
||||
embed.add_field(name="Message Info:", value=f"```{self.current['description']}\nSender ID: {self.current['sender']}\nPlaylist ID: {self.current['referId']}\nInvite Time: {self.current['time'].strftime('%d-%m %H:%M:%S')}```")
|
||||
return embed
|
||||
|
||||
async def button_change(self, interaction: discord.Interaction):
|
||||
for child in self.children:
|
||||
if child.custom_id in ['accept', 'dismiss']:
|
||||
child.disabled = True if self.current is None else False
|
||||
elif child.custom_id == "select":
|
||||
child.options = [discord.SelectOption(label=f"{index}. {mail['title'][:50]}", description=mail['type'], emoji='✉️' if mail['type'] == 'invite' else '📢') for index, mail in enumerate(self.inbox, start=1) ]
|
||||
|
||||
if not self.inbox:
|
||||
await interaction.response.edit_message(embed=self.build_embed(), view=None)
|
||||
return self.stop()
|
||||
await interaction.response.edit_message(embed=self.build_embed(), view=self)
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
@discord.ui.button(label='Accept', style=discord.ButtonStyle.green, custom_id="accept", disabled=True)
|
||||
async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
self.newplaylist.append(self.current)
|
||||
self.inbox.remove(self.current)
|
||||
self.current = None
|
||||
await self.button_change(interaction)
|
||||
|
||||
@discord.ui.button(label='Dismiss', style=discord.ButtonStyle.red, custom_id="dismiss", disabled=True)
|
||||
async def dismiss_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
self.inbox.remove(self.current)
|
||||
self.current = None
|
||||
await self.button_change(interaction)
|
||||
|
||||
@discord.ui.button(label='Click Me To Save The Changes', style=discord.ButtonStyle.blurple)
|
||||
async def save_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.response.edit(view=None)
|
||||
self.stop()
|
||||
29
view/link.py
Normal file
29
view/link.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
class LinkView(discord.ui.View):
|
||||
def __init__(self, label=None, emoji=None, url=None):
|
||||
super().__init__(timeout=60)
|
||||
self.add_item(discord.ui.Button(label=label, emoji=emoji, url=url))
|
||||
108
view/list.py
Normal file
108
view/list.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from math import ceil
|
||||
from function import (
|
||||
embed_color,
|
||||
time as ctime
|
||||
)
|
||||
|
||||
class ListView(discord.ui.View):
|
||||
def __init__(self, player, author, isQueue = True):
|
||||
super().__init__(timeout=60)
|
||||
self.player = player
|
||||
|
||||
self.name = player.get_msg('queueTitle') if isQueue else player.get_msg('historyTitle')
|
||||
self.tracks = player.queue.tracks() if isQueue else player.queue.history()
|
||||
if not isQueue:
|
||||
self.tracks.reverse()
|
||||
self.author = author
|
||||
|
||||
self.page = ceil(len(self.tracks) / 7)
|
||||
self.current_page = 1
|
||||
try:
|
||||
self.time = ctime(sum([track.length for track in self.tracks]))
|
||||
except:
|
||||
self.time = "∞"
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_error(self, error, item, interaction):
|
||||
return
|
||||
|
||||
async def interaction_check(self, interaction):
|
||||
if interaction.user == self.author:
|
||||
return True
|
||||
return False
|
||||
|
||||
def build_embed(self):
|
||||
offset = self.current_page * 7
|
||||
tracks = self.tracks[(offset-7):offset]
|
||||
|
||||
embed = discord.Embed(title=self.player.get_msg('viewTitle'), color=embed_color)
|
||||
embed.description=self.player.get_msg('viewDesc').format(self.player.current.uri, f"```{self.player.current.title}```") if self.player.current else self.player.get_msg('nowplayingDesc').format("None")
|
||||
queueText = ""
|
||||
for index, track in enumerate(tracks, start=offset - 6):
|
||||
queueText += f"{track.emoji} `{index}.` `[" + (self.player.get_msg("live") if track.is_stream else ctime(track.length)) + f'`] **{track.title[:30]}** ' + (track.requester.mention if track.requester else "<@605618911471468554>") + "\n"
|
||||
|
||||
embed.add_field(name=self.name, value=queueText)
|
||||
embed.set_footer(text=self.player.get_msg('viewFooter').format(self.current_page, self.page, self.time))
|
||||
|
||||
return embed
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page < self.page:
|
||||
self.current_page += 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page != self.page:
|
||||
self.current_page = self.page
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
108
view/lyrics.py
Normal file
108
view/lyrics.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from function import embed_color
|
||||
|
||||
class LyricsDropdown(discord.ui.Select):
|
||||
def __init__(self, langs: list):
|
||||
options = [discord.SelectOption(label=lang) for lang in langs]
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select A Lyrics Translation",
|
||||
min_values=1, max_values=1,
|
||||
options=options, custom_id="selectLyricsLangs"
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
self.view.lang = self.values[0]
|
||||
self.view.current_page = 1
|
||||
self.view.pages = len(self.view.source.get(self.values[0]))
|
||||
await interaction.response.edit_message(embed=self.view.build_embed())
|
||||
|
||||
class LyricsView(discord.ui.View):
|
||||
def __init__(self, name: str, source: dict, author: discord.Member):
|
||||
super().__init__(timeout=60)
|
||||
|
||||
self.name = name
|
||||
self.source = source
|
||||
self.lang = list(source.keys())[0]
|
||||
self.author = author
|
||||
|
||||
self.response = None
|
||||
self.pages = len(self.source.get(self.lang))
|
||||
self.current_page = 1
|
||||
self.add_item(LyricsDropdown(list(source.keys())))
|
||||
|
||||
async def interaction_check(self, interaction):
|
||||
if interaction.user == self.author:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_error(self, error, item, interaction):
|
||||
return
|
||||
|
||||
def build_embed(self):
|
||||
chunk = self.source.get(self.lang)[self.current_page - 1]
|
||||
embed=discord.Embed(description=chunk, color=embed_color)
|
||||
embed.set_author(name=f"Searching Query: {self.name}", icon_url=self.author.display_avatar.url)
|
||||
embed.set_footer(text=f"Page: {self.current_page}/{self.pages}")
|
||||
return embed
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page < self.pages:
|
||||
self.current_page += 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page != self.pages:
|
||||
self.current_page = self.pages
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
181
view/playlist.py
Normal file
181
view/playlist.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from math import ceil
|
||||
from tldextract import extract
|
||||
|
||||
from function import (
|
||||
embed_color,
|
||||
invite_link,
|
||||
emoji_source,
|
||||
time as ctime,
|
||||
get_lang
|
||||
)
|
||||
|
||||
class Select_playlist(discord.ui.Select):
|
||||
def __init__(self, results):
|
||||
options = [discord.SelectOption(emoji='🌎', label='All Playlist')]
|
||||
for index, playlist in enumerate(results, start=1):
|
||||
if playlist['type'] != 'error':
|
||||
options.append(discord.SelectOption(emoji=playlist['emoji'], label=f'{index}. {playlist["name"]}', description=f"{playlist['time']} · {playlist['type']}"))
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select a playlist to view ..",
|
||||
options=options
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if self.values[0] == 'All Playlist':
|
||||
self.view.current = None
|
||||
return await interaction.response.edit_message(embed=self.view.viewEmbed)
|
||||
self.view.current = self.view.results[int(self.values[0].split(". ")[0]) - 1]
|
||||
self.view.page = ceil(len(self.view.current['tracks']) / 7)
|
||||
self.view.current_page = 1
|
||||
await interaction.response.edit_message(embed=self.view.build_embed())
|
||||
|
||||
class agree(discord.ui.Button):
|
||||
def __init__(self):
|
||||
super().__init__(label="Agree", style=discord.ButtonStyle.green)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
self.label = "Created"
|
||||
self.disabled = True
|
||||
self.style=discord.ButtonStyle.primary
|
||||
embed = discord.Embed(description="Your account has been successfully created", color=0x55e27f)
|
||||
await interaction.response.edit_message(embed=embed, view=self.view)
|
||||
self.view.value = True
|
||||
self.view.stop()
|
||||
|
||||
class PlaylistView(discord.ui.View):
|
||||
def __init__(self, viewEmbed, results, author):
|
||||
super().__init__(timeout=60)
|
||||
self.viewEmbed = viewEmbed
|
||||
self.results = results
|
||||
self.author = author
|
||||
self.guildID = author.guild.id
|
||||
self.response = None
|
||||
|
||||
self.current = None
|
||||
self.page = 0
|
||||
self.current_page = 1
|
||||
|
||||
self.add_item(Select_playlist(results))
|
||||
|
||||
async def interaction_check(self, interaction):
|
||||
if interaction.user == self.author:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def on_error(self, error, item, interaction):
|
||||
return
|
||||
|
||||
def build_embed(self):
|
||||
offset = self.current_page * 7
|
||||
tracks = self.current['tracks'][(offset-7):offset]
|
||||
|
||||
embed = discord.Embed(title=get_lang(self.guildID, 'playlistView'), color=embed_color)
|
||||
|
||||
embed.description= get_lang(self.guildID, 'playlistViewDesc').format(self.current['name'], self.current['id'], len(self.current['tracks']), self.current.get('owner', "None"), self.current['type'])
|
||||
|
||||
perms = self.current['perms']
|
||||
permsStr = get_lang(self.guildID, 'settingsPermTitle')
|
||||
if self.current['type'] == 'share':
|
||||
embed.add_field(name=permsStr, value=get_lang(self.guildID, 'playlistViewPermsValue').format('✓' if 'write' in perms and self.author.id in perms['write'] else '✘', '✓' if 'remove' in perms and self.author.id in perms['remove'] else '✘'))
|
||||
else:
|
||||
embed.add_field(name=permsStr, value=get_lang(self.guildID, 'playlistViewPermsValue2').format(', '.join(f'<@{user}>' for user in perms['read'])))
|
||||
|
||||
trackStr = get_lang(self.guildID, 'playlistViewTrack')
|
||||
if tracks:
|
||||
try:
|
||||
embed.add_field(name=trackStr, value='\n'.join(f"{emoji_source(extract(track['info']['uri']).domain)} `{index}.` `[{ctime(track['info']['length'] * 1000)}]` **{track['info']['title'][:30]}** " for index, track in enumerate(tracks, start=offset - 6)), inline=False)
|
||||
except:
|
||||
embed.add_field(name=trackStr, value='\n'.join(f"{emoji_source(extract(track.info['uri']).domain)} `{index}.` `[{ctime(track.length)}]` **{track.title[:30]}** " for index, track in enumerate(tracks, start=offset - 6)), inline=False)
|
||||
else:
|
||||
embed.add_field(name=trackStr, value=get_lang(self.guildID, 'playlistNoTrack').format(self.current['name']), inline=False)
|
||||
|
||||
embed.set_footer(text=get_lang(self.guildID, 'playlistViewPage').format(self.current_page, self.page, self.current['time']))
|
||||
|
||||
return embed
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page < self.page:
|
||||
self.current_page += 1
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page != self.page:
|
||||
self.current_page = self.page
|
||||
await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
|
||||
class CreateView(discord.ui.View):
|
||||
def __init__(self):
|
||||
super().__init__(timeout=20)
|
||||
self.value = None
|
||||
self.response = None
|
||||
self.add_item(agree())
|
||||
self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=invite_link))
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
64
view/search.py
Normal file
64
view/search.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from function import langs
|
||||
|
||||
class SearchDropdown(discord.ui.Select):
|
||||
def __init__(self, tracks, lang):
|
||||
self.tracks = tracks
|
||||
self.lang = lang
|
||||
options = []
|
||||
for index, track in enumerate(self.tracks, start=1):
|
||||
options.append(discord.SelectOption(label=f"{index}. {track.title[:50]}", description=f"{track.author[:50]} · {track.formatLength}"))
|
||||
|
||||
super().__init__(placeholder=langs[lang]['searchWait'],
|
||||
min_values=1, max_values=len(tracks),
|
||||
options=options
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
self.disabled = True
|
||||
self.placeholder = langs[self.lang]['searchSuccess']
|
||||
await interaction.response.edit_message(view=self.view)
|
||||
self.view.values = self.values
|
||||
self.view.stop()
|
||||
|
||||
class SearchView(discord.ui.View):
|
||||
def __init__(self, tracks, lang):
|
||||
super().__init__(timeout=60)
|
||||
self.response = None
|
||||
self.values = None
|
||||
self.add_item(SearchDropdown(tracks, lang))
|
||||
|
||||
async def on_error(self, error, item, interaction):
|
||||
return
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
36
voicelink/__init__.py
Normal file
36
voicelink/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
__version__ = "0.7"
|
||||
__author__ = 'Vocard Development, Choco'
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2022 (c) Vocard Development, Choco"
|
||||
|
||||
from .enums import SearchType
|
||||
from .events import *
|
||||
from .exceptions import *
|
||||
from .filters import *
|
||||
from .objects import *
|
||||
from .player import Player
|
||||
from .pool import *
|
||||
from .queue import *
|
||||
69
voicelink/enums.py
Normal file
69
voicelink/enums.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class SearchType(Enum):
|
||||
"""The enum for the different search types for Voicelink.
|
||||
This feature is exclusively for the Spotify search feature of Voicelink.
|
||||
If you are not using this feature, this class is not necessary.
|
||||
|
||||
SearchType.ytsearch searches using regular Youtube,
|
||||
which is best for all scenarios.
|
||||
|
||||
SearchType.ytmsearch searches using YouTube Music,
|
||||
which is best for getting audio-only results.
|
||||
|
||||
SearchType.scsearch searches using SoundCloud,
|
||||
which is an alternative to YouTube or YouTube Music.
|
||||
"""
|
||||
ytsearch = "ytsearch"
|
||||
ytmsearch = "ytmsearch"
|
||||
scsearch = "scsearch"
|
||||
amsearch = "amsearch"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
class NodeAlgorithm(Enum):
|
||||
"""The enum for the different node algorithms in Voicelink.
|
||||
|
||||
The enums in this class are to only differentiate different
|
||||
methods, since the actual method is handled in the
|
||||
get_best_node() method.
|
||||
|
||||
NodeAlgorithm.by_ping returns a node based on it's latency,
|
||||
preferring a node with the lowest response time
|
||||
|
||||
NodeAlgorithm.by_region returns a node based on its voice region,
|
||||
which the region is specified by the user in the method as an arg.
|
||||
This method will only work if you set a voice region when you create a node.
|
||||
"""
|
||||
|
||||
# We don't have to define anything special for these, since these just serve as flags
|
||||
by_ping = auto()
|
||||
by_region = auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
168
voicelink/events.py
Normal file
168
voicelink/events.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
from .pool import NodePool
|
||||
from discord import Client
|
||||
|
||||
|
||||
class VoicelinkEvent:
|
||||
"""The base class for all events dispatched by a node.
|
||||
Every event must be formatted within your bot's code as a listener.
|
||||
i.e: If you want to listen for when a track starts, the event would be:
|
||||
```py
|
||||
@bot.listen
|
||||
async def on_voicelink_track_start(self, event):
|
||||
```
|
||||
"""
|
||||
name = "event"
|
||||
handler_args = ()
|
||||
|
||||
def dispatch(self, bot: Client):
|
||||
bot.dispatch(f"voicelink_{self.name}", *self.handler_args)
|
||||
|
||||
|
||||
class TrackStartEvent(VoicelinkEvent):
|
||||
"""Fired when a track has successfully started.
|
||||
Returns the player associated with the event and the voicelink.Track object.
|
||||
"""
|
||||
name = "track_start"
|
||||
|
||||
def __init__(self, data: dict, player):
|
||||
self.player = player
|
||||
self.track = self.player._current
|
||||
|
||||
# on_voicelink_track_start(player, track)
|
||||
self.handler_args = self.player, self.track
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.TrackStartEvent player={self.player} track_id={self.track.track_id}>"
|
||||
|
||||
|
||||
class TrackEndEvent(VoicelinkEvent):
|
||||
"""Fired when a track has successfully ended.
|
||||
Returns the player associated with the event along with the voicelink.Track object and reason.
|
||||
"""
|
||||
name = "track_end"
|
||||
|
||||
def __init__(self, data: dict, player):
|
||||
self.player = player
|
||||
self.track = self.player._ending_track
|
||||
self.reason: str = data["reason"]
|
||||
|
||||
# on_voicelink_track_end(player, track, reason)
|
||||
self.handler_args = self.player, self.track, self.reason
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.TrackEndEvent player={self.player} track_id={self.track.track_id} "
|
||||
f"reason={self.reason}>"
|
||||
)
|
||||
|
||||
|
||||
class TrackStuckEvent(VoicelinkEvent):
|
||||
"""Fired when a track is stuck and cannot be played. Returns the player
|
||||
associated with the event along with the voicelink.Track object
|
||||
to be further parsed by the end user.
|
||||
"""
|
||||
name = "track_stuck"
|
||||
|
||||
def __init__(self, data: dict, player):
|
||||
self.player = player
|
||||
self.track = self.player._ending_track
|
||||
self.threshold: float = data["thresholdMs"]
|
||||
|
||||
# on_voicelink_track_stuck(player, track, threshold)
|
||||
self.handler_args = self.player, self.track, self.threshold
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.TrackStuckEvent player={self.player!r} track={self.track!r} " \
|
||||
f"threshold={self.threshold!r}>"
|
||||
|
||||
|
||||
class TrackExceptionEvent(VoicelinkEvent):
|
||||
"""Fired when a track error has occured.
|
||||
Returns the player associated with the event along with the error code and exception.
|
||||
"""
|
||||
name = "track_exception"
|
||||
|
||||
def __init__(self, data: dict, player):
|
||||
self.player = player
|
||||
self.track = self.player._ending_track
|
||||
if data.get('error'):
|
||||
# User is running Lavalink <= 3.3
|
||||
self.exception: str = data["error"]
|
||||
else:
|
||||
# User is running Lavalink >=3.4
|
||||
self.exception: str = data["exception"]
|
||||
|
||||
# on_voicelink_track_exception(player, track, error)
|
||||
self.handler_args = self.player, self.track, self.exception
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.TrackExceptionEvent player={self.player!r} exception={self.exception!r}>"
|
||||
|
||||
|
||||
class WebSocketClosedPayload:
|
||||
def __init__(self, data: dict):
|
||||
self.guild = NodePool.get_node().bot.get_guild(int(data["guildId"]))
|
||||
self.code: int = data["code"]
|
||||
self.reason: str = data["code"]
|
||||
self.by_remote: bool = data["byRemote"]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.WebSocketClosedPayload guild={self.guild!r} code={self.code!r} " \
|
||||
f"reason={self.reason!r} by_remote={self.by_remote!r}>"
|
||||
|
||||
|
||||
class WebSocketClosedEvent(VoicelinkEvent):
|
||||
"""Fired when a websocket connection to a node has been closed.
|
||||
Returns the reason and the error code.
|
||||
"""
|
||||
name = "websocket_closed"
|
||||
|
||||
def __init__(self, data: dict, _):
|
||||
self.payload = WebSocketClosedPayload(data)
|
||||
|
||||
# on_voicelink_websocket_closed(payload)
|
||||
self.handler_args = self.payload,
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.WebsocketClosedEvent payload={self.payload!r}>"
|
||||
|
||||
|
||||
class WebSocketOpenEvent(VoicelinkEvent):
|
||||
"""Fired when a websocket connection to a node has been initiated.
|
||||
Returns the target and the session SSRC.
|
||||
"""
|
||||
name = "websocket_open"
|
||||
|
||||
def __init__(self, data: dict, _):
|
||||
self.target: str = data["target"]
|
||||
self.ssrc: int = data["ssrc"]
|
||||
|
||||
# on_voicelink_websocket_open(target, ssrc)
|
||||
self.handler_args = self.target, self.ssrc
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.WebsocketOpenEvent target={self.target!r} ssrc={self.ssrc!r}>"
|
||||
|
||||
104
voicelink/exceptions.py
Normal file
104
voicelink/exceptions.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
class VoicelinkException(Exception):
|
||||
"""Base of all Voicelink exceptions."""
|
||||
|
||||
|
||||
class NodeException(Exception):
|
||||
"""Base exception for nodes."""
|
||||
|
||||
|
||||
class NodeCreationError(NodeException):
|
||||
"""There was a problem while creating the node."""
|
||||
|
||||
|
||||
class NodeConnectionFailure(NodeException):
|
||||
"""There was a problem while connecting to the node."""
|
||||
|
||||
|
||||
class NodeConnectionClosed(NodeException):
|
||||
"""The node's connection is closed."""
|
||||
pass
|
||||
|
||||
|
||||
class NodeNotAvailable(VoicelinkException):
|
||||
"""The node is currently unavailable."""
|
||||
pass
|
||||
|
||||
|
||||
class NoNodesAvailable(VoicelinkException):
|
||||
"""There are no nodes currently available."""
|
||||
pass
|
||||
|
||||
|
||||
class TrackInvalidPosition(VoicelinkException):
|
||||
"""An invalid position was chosen for a track."""
|
||||
pass
|
||||
|
||||
|
||||
class TrackLoadError(VoicelinkException):
|
||||
"""There was an error while loading a track."""
|
||||
pass
|
||||
|
||||
|
||||
class FilterInvalidArgument(VoicelinkException):
|
||||
"""An invalid argument was passed to a filter."""
|
||||
pass
|
||||
|
||||
class FilterTagAlreadyInUse(VoicelinkException):
|
||||
"""A filter with a tag is already in use by another filter"""
|
||||
pass
|
||||
|
||||
class FilterTagInvalid(VoicelinkException):
|
||||
"""An invalid tag was passed or Voicelink was unable to find a filter tag"""
|
||||
pass
|
||||
|
||||
class SpotifyAlbumLoadFailed(VoicelinkException):
|
||||
"""The voicelink Spotify client was unable to load an album."""
|
||||
pass
|
||||
|
||||
|
||||
class SpotifyTrackLoadFailed(VoicelinkException):
|
||||
"""The voicelink Spotify client was unable to load a track."""
|
||||
pass
|
||||
|
||||
|
||||
class SpotifyPlaylistLoadFailed(VoicelinkException):
|
||||
"""The voicelink Spotify client was unable to load a playlist."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSpotifyClientAuthorization(VoicelinkException):
|
||||
"""No Spotify client authorization was provided for track searching."""
|
||||
pass
|
||||
|
||||
|
||||
class QueueFull(VoicelinkException):
|
||||
pass
|
||||
|
||||
class OutofList(VoicelinkException):
|
||||
pass
|
||||
|
||||
class DuplicateTrack(VoicelinkException):
|
||||
pass
|
||||
446
voicelink/filters.py
Normal file
446
voicelink/filters.py
Normal file
@@ -0,0 +1,446 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import collections
|
||||
from .exceptions import FilterInvalidArgument, FilterTagAlreadyInUse, FilterTagInvalid
|
||||
|
||||
from typing import (
|
||||
List
|
||||
)
|
||||
|
||||
class Filter:
|
||||
"""
|
||||
The base class for all filters.
|
||||
You can use these filters if you have the latest Lavalink version
|
||||
installed. If you do not have the latest Lavalink version,
|
||||
these filters will not work.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.payload = None
|
||||
self.tag: str = None
|
||||
|
||||
class Filters:
|
||||
def __init__(self) -> None:
|
||||
self._filters: List[Filter] = []
|
||||
|
||||
def add_filter(self, *, filter: Filter) -> None:
|
||||
if self.has_filter(filter_tag=filter.tag):
|
||||
raise FilterTagAlreadyInUse("A filter with that tag is already in use")
|
||||
self._filters.append(filter)
|
||||
|
||||
def remove_filter(self, *, filter_tag: str) -> None:
|
||||
if not self.has_filter(filter_tag=filter_tag):
|
||||
raise FilterTagInvalid("A filter with that tag was not found.")
|
||||
|
||||
for index, filter in enumerate(self._filters):
|
||||
if filter.tag == filter_tag:
|
||||
del self._filters[index]
|
||||
|
||||
def has_filter(self, *, filter_tag: str) -> bool:
|
||||
return any(f for f in self._filters if f.tag == filter_tag)
|
||||
|
||||
def reset_filters(self) -> None:
|
||||
self._filters = []
|
||||
|
||||
def get_all_payloads(self) -> dict:
|
||||
payload = {}
|
||||
for filter in self._filters:
|
||||
payload.update(filter.payload)
|
||||
return payload
|
||||
|
||||
def get_filters(self) -> list:
|
||||
return self._filters
|
||||
|
||||
class Equalizer(Filter):
|
||||
"""
|
||||
Filter which represents a 15 band equalizer.
|
||||
You can adjust the dynamic of the sound using this filter.
|
||||
i.e: Applying a bass boost filter to emphasize the bass in a song.
|
||||
The format for the levels is: List[Tuple[int, float]]
|
||||
"""
|
||||
|
||||
def __init__(self, *, tag: str, levels: list):
|
||||
super().__init__()
|
||||
|
||||
self.eq = self._factory(levels)
|
||||
self.raw = levels
|
||||
|
||||
self.payload = {"equalizer": self.eq}
|
||||
self.tag = tag
|
||||
|
||||
def _factory(self, levels: list):
|
||||
_dict = collections.defaultdict(int)
|
||||
|
||||
_dict.update(levels)
|
||||
_dict = [{"band": i, "gain": _dict[i]} for i in range(15)]
|
||||
|
||||
return _dict
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.EqualizerFilter tag={self.tag} eq={self.eq} raw={self.raw}>"
|
||||
|
||||
@classmethod
|
||||
def flat(cls):
|
||||
"""Equalizer preset which represents a flat EQ board,
|
||||
with all levels set to their default values.
|
||||
"""
|
||||
|
||||
levels = [
|
||||
(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0), (4, 0.0),
|
||||
(5, 0.0), (6, 0.0), (7, 0.0), (8, 0.0), (9, 0.0),
|
||||
(10, 0.0), (11, 0.0), (12, 0.0), (13, 0.0), (14, 0.0)
|
||||
]
|
||||
return cls(tag="flat", levels=levels)
|
||||
|
||||
@classmethod
|
||||
def boost(cls):
|
||||
"""Equalizer preset which boosts the sound of a track,
|
||||
making it sound fun and energetic by increasing the bass
|
||||
and the highs.
|
||||
"""
|
||||
|
||||
levels = [
|
||||
(0, -0.075), (1, 0.125), (2, 0.125), (3, 0.1), (4, 0.1),
|
||||
(5, .05), (6, 0.075), (7, 0.0), (8, 0.0), (9, 0.0),
|
||||
(10, 0.0), (11, 0.0), (12, 0.125), (13, 0.15), (14, 0.05)
|
||||
]
|
||||
return cls(tag="boost", levels=levels)
|
||||
|
||||
@classmethod
|
||||
def metal(cls):
|
||||
"""Equalizer preset which increases the mids of a track,
|
||||
preferably one of the metal genre, to make it sound
|
||||
more full and concert-like.
|
||||
"""
|
||||
|
||||
levels = [
|
||||
(0, 0.0), (1, 0.1), (2, 0.1), (3, 0.15), (4, 0.13),
|
||||
(5, 0.1), (6, 0.0), (7, 0.125), (8, 0.175), (9, 0.175),
|
||||
(10, 0.125), (11, 0.125), (12, 0.1), (13, 0.075), (14, 0.0)
|
||||
]
|
||||
|
||||
return cls(tag="metal", levels=levels)
|
||||
|
||||
@classmethod
|
||||
def piano(cls):
|
||||
"""Equalizer preset which increases the mids and highs
|
||||
of a track, preferably a piano based one, to make it
|
||||
stand out.
|
||||
"""
|
||||
|
||||
levels = [
|
||||
(0, -0.25), (1, -0.25), (2, -0.125), (3, 0.0),
|
||||
(4, 0.25), (5, 0.25), (6, 0.0), (7, -0.25), (8, -0.25),
|
||||
(9, 0.0), (10, 0.0), (11, 0.5), (12, 0.25), (13, -0.025)
|
||||
]
|
||||
return cls(tag="piano", levels=levels)
|
||||
|
||||
|
||||
class Timescale(Filter):
|
||||
"""Filter which changes the speed and pitch of a track.
|
||||
You can make some very nice effects with this filter,
|
||||
i.e: a vaporwave-esque filter which slows the track down
|
||||
a certain amount to produce said effect.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
speed: float = 1.0,
|
||||
pitch: float = 1.0,
|
||||
rate: float = 1.0
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
if speed < 0:
|
||||
raise FilterInvalidArgument("Timescale speed must be more than 0.")
|
||||
if pitch < 0:
|
||||
raise FilterInvalidArgument("Timescale pitch must be more than 0.")
|
||||
if rate < 0:
|
||||
raise FilterInvalidArgument("Timescale rate must be more than 0.")
|
||||
|
||||
self.speed = speed
|
||||
self.pitch = pitch
|
||||
self.rate = rate
|
||||
self.tag = tag
|
||||
|
||||
self.payload = {"timescale": {"speed": self.speed,
|
||||
"pitch": self.pitch,
|
||||
"rate": self.rate}}
|
||||
|
||||
@classmethod
|
||||
def vaporwave(cls):
|
||||
"""Timescale preset which slows down the currently playing track,
|
||||
giving it the effect of a half-speed record/casette playing.
|
||||
|
||||
This preset will assign the tag 'vaporwave'.
|
||||
"""
|
||||
|
||||
return cls(tag="vaporwave", speed=0.8, pitch=0.8)
|
||||
|
||||
@classmethod
|
||||
def nightcore(cls):
|
||||
"""Timescale preset which speeds up the currently playing track,
|
||||
which matches up to nightcore, a genre of sped-up music
|
||||
|
||||
This preset will assign the tag 'nightcore'.
|
||||
"""
|
||||
|
||||
return cls(tag="nightcore", speed=1.25, pitch=1.3)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.TimescaleFilter tag={self.tag} speed={self.speed} pitch={self.pitch} rate={self.rate}>"
|
||||
|
||||
|
||||
class Karaoke(Filter):
|
||||
"""Filter which filters the vocal track from any song and leaves the instrumental.
|
||||
Best for karaoke as the filter implies.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
level: float = 1.0,
|
||||
mono_level: float = 1.0,
|
||||
filter_band: float = 220.0,
|
||||
filter_width: float = 100.0
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.level = level
|
||||
self.mono_level = mono_level
|
||||
self.filter_band = filter_band
|
||||
self.filter_width = filter_width
|
||||
self.tag = tag
|
||||
|
||||
self.payload = {"karaoke": {"level": self.level,
|
||||
"monoLevel": self.mono_level,
|
||||
"filterBand": self.filter_band,
|
||||
"filterWidth": self.filter_width}}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Voicelink.KaraokeFilter tag={self.tag} level={self.level} mono_level={self.mono_level} "
|
||||
f"filter_band={self.filter_band} filter_width={self.filter_width}>"
|
||||
)
|
||||
|
||||
|
||||
class Tremolo(Filter):
|
||||
"""Filter which produces a wavering tone in the music,
|
||||
causing it to sound like the music is changing in volume rapidly.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
frequency: float = 2.0,
|
||||
depth: float = 0.5
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
if frequency < 0:
|
||||
raise FilterInvalidArgument(
|
||||
"Tremolo frequency must be more than 0.")
|
||||
if depth < 0 or depth > 1:
|
||||
raise FilterInvalidArgument(
|
||||
"Tremolo depth must be between 0 and 1.")
|
||||
|
||||
self.frequency = frequency
|
||||
self.depth = depth
|
||||
self.tag = tag
|
||||
|
||||
self.payload = {"tremolo": {"frequency": self.frequency,
|
||||
"depth": self.depth}}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.TremoloFilter tag={self.tag} frequency={self.frequency} depth={self.depth}>"
|
||||
|
||||
|
||||
class Vibrato(Filter):
|
||||
"""Filter which produces a wavering tone in the music, similar to the Tremolo filter,
|
||||
but changes in pitch rather than volume.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
frequency: float = 2.0,
|
||||
depth: float = 0.5
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
if frequency < 0 or frequency > 14:
|
||||
raise FilterInvalidArgument(
|
||||
"Vibrato frequency must be between 0 and 14.")
|
||||
if depth < 0 or depth > 1:
|
||||
raise FilterInvalidArgument(
|
||||
"Vibrato depth must be between 0 and 1.")
|
||||
|
||||
self.frequency = frequency
|
||||
self.depth = depth
|
||||
self.tag = tag
|
||||
|
||||
self.payload = {"vibrato": {"frequency": self.frequency,
|
||||
"depth": self.depth}}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.VibratoFilter tag={self.tag} frequency={self.frequency} depth={self.depth}>"
|
||||
|
||||
|
||||
class Rotation(Filter):
|
||||
"""Filter which produces a stereo-like panning effect, which sounds like
|
||||
the audio is being rotated around the listener's head
|
||||
"""
|
||||
|
||||
def __init__(self, *, tag: str, rotation_hertz: float = 5):
|
||||
super().__init__()
|
||||
|
||||
self.rotation_hertz = rotation_hertz
|
||||
self.tag = tag
|
||||
self.payload = {"rotation": {"rotationHz": self.rotation_hertz}}
|
||||
|
||||
@classmethod
|
||||
def nightD(cls):
|
||||
return cls(tag="8d", rotation_hertz=0.2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.RotationFilter tag={self.tag} rotation_hertz={self.rotation_hertz}>"
|
||||
|
||||
|
||||
class ChannelMix(Filter):
|
||||
"""Filter which manually adjusts the panning of the audio, which can make
|
||||
for some cool effects when done correctly.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
left_to_left: float = 1,
|
||||
right_to_right: float = 1,
|
||||
left_to_right: float = 0,
|
||||
right_to_left: float = 0
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
if 0 > left_to_left > 1:
|
||||
raise ValueError(
|
||||
"'left_to_left' value must be more than or equal to 0 or less than or equal to 1.")
|
||||
if 0 > right_to_right > 1:
|
||||
raise ValueError(
|
||||
"'right_to_right' value must be more than or equal to 0 or less than or equal to 1.")
|
||||
if 0 > left_to_right > 1:
|
||||
raise ValueError(
|
||||
"'left_to_right' value must be more than or equal to 0 or less than or equal to 1.")
|
||||
if 0 > right_to_left > 1:
|
||||
raise ValueError(
|
||||
"'right_to_left' value must be more than or equal to 0 or less than or equal to 1.")
|
||||
|
||||
self.left_to_left = left_to_left
|
||||
self.left_to_right = left_to_right
|
||||
self.right_to_left = right_to_left
|
||||
self.right_to_right = right_to_right
|
||||
self.tag = tag
|
||||
|
||||
self.payload = {"channelMix": {"leftToLeft": self.left_to_left,
|
||||
"leftToRight": self.left_to_right,
|
||||
"rightToLeft": self.right_to_left,
|
||||
"rightToRight": self.right_to_right}
|
||||
}
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.ChannelMix tag={self.tag} left_to_left={self.left_to_left} left_to_right={self.left_to_right} "
|
||||
f"right_to_left={self.right_to_left} right_to_right={self.right_to_right}>"
|
||||
)
|
||||
|
||||
class Distortion(Filter):
|
||||
"""Filter which generates a distortion effect. Useful for certain filter implementations where
|
||||
distortion is needed.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
sin_offset: float = 0,
|
||||
sin_scale: float = 1,
|
||||
cos_offset: float = 0,
|
||||
cos_scale: float = 1,
|
||||
tan_offset: float = 0,
|
||||
tan_scale: float = 1,
|
||||
offset: float = 0,
|
||||
scale: float = 1
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.sin_offset = sin_offset
|
||||
self.sin_scale = sin_scale
|
||||
self.cos_offset = cos_offset
|
||||
self.cos_scale = cos_scale
|
||||
self.tan_offset = tan_offset
|
||||
self.tan_scale = tan_scale
|
||||
self.offset = offset
|
||||
self.scale = scale
|
||||
self.tag = tag
|
||||
|
||||
self.payload = {"distortion": {
|
||||
"sinOffset": self.sin_offset,
|
||||
"sinScale": self.sin_scale,
|
||||
"cosOffset": self.cos_offset,
|
||||
"cosScale": self.cos_scale,
|
||||
"tanOffset": self.tan_offset,
|
||||
"tanScale": self.tan_scale,
|
||||
"offset": self.offset,
|
||||
"scale": self.scale
|
||||
}}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.Distortion tag={self.tag} sin_offset={self.sin_offset} sin_scale={self.sin_scale}> "
|
||||
f"cos_offset={self.cos_offset} cos_scale={self.cos_scale} tan_offset={self.tan_offset} "
|
||||
f"tan_scale={self.tan_scale} offset={self.offset} scale={self.scale}"
|
||||
)
|
||||
|
||||
|
||||
class LowPass(Filter):
|
||||
"""Filter which supresses higher frequencies and allows lower frequencies to pass.
|
||||
You can also do this with the Equalizer filter, but this is an easier way to do it.
|
||||
"""
|
||||
|
||||
def __init__(self, *, tag: str, smoothing: float = 20):
|
||||
super().__init__()
|
||||
|
||||
self.smoothing = smoothing
|
||||
self.tag = tag
|
||||
self.payload = {"lowPass": {"smoothing": self.smoothing}}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.LowPass tag={self.tag} smoothing={self.smoothing}>"
|
||||
|
||||
|
||||
151
voicelink/objects.py
Normal file
151
voicelink/objects.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from discord import Member
|
||||
from tldextract import extract
|
||||
|
||||
from .enums import SearchType
|
||||
from function import (
|
||||
emoji_source,
|
||||
time as ctime
|
||||
)
|
||||
|
||||
YOUTUBE_REGEX = re.compile(r'(https?://)?(www\.)?youtube\.(com|nl)/watch\?v=([-\w]+)')
|
||||
class Track:
|
||||
"""The base track object. Returns critical track information needed for parsing by Lavalink.
|
||||
You can also pass in commands.Context to get a discord.py Context object in your track.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
track_id: str,
|
||||
info: dict,
|
||||
requester: Member,
|
||||
spotify: bool = False,
|
||||
search_type: SearchType = SearchType.ytsearch,
|
||||
spotify_track = None,
|
||||
):
|
||||
self.track_id = track_id
|
||||
self.info = info
|
||||
self.spotify = spotify
|
||||
if self.spotify:
|
||||
self.artistId: Optional[list] = info.get("artistId")
|
||||
|
||||
self.original: Optional[Track] = None if spotify else self
|
||||
self._search_type = SearchType.ytmsearch if spotify else search_type
|
||||
self.spotify_track = spotify_track
|
||||
|
||||
self.title = info.get("title", "Unknown")
|
||||
self.author = info.get("author", "Unknown")
|
||||
self.uri = info.get("uri", "https://discord.com/application-directory/605618911471468554")
|
||||
self.identifier = info.get("identifier")
|
||||
self.thumbnail = None
|
||||
self.source = extract(self.uri).domain
|
||||
self.emoji = emoji_source(self.source)
|
||||
|
||||
if info.get("thumbnail"):
|
||||
self.thumbnail = info.get("thumbnail")
|
||||
elif YOUTUBE_REGEX.match(self.uri):
|
||||
self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/hqdefault.jpg"
|
||||
|
||||
if self.source == "soundcloud" and "/preview/" in self.identifier:
|
||||
self.length = 30000
|
||||
else:
|
||||
self.length = info.get("length")
|
||||
|
||||
self.formatLength = ctime(self.length)
|
||||
self.requester = requester
|
||||
self.is_stream = info.get("isStream", False)
|
||||
self.is_seekable = info.get("isSeekable", True)
|
||||
self.position = info.get("position", 0)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Track):
|
||||
return False
|
||||
|
||||
return other.track_id == self.track_id
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.track title={self.title!r} uri=<{self.uri!r}> length={self.length}>"
|
||||
|
||||
|
||||
class Playlist:
|
||||
"""The base playlist object.
|
||||
Returns critical playlist information needed for parsing by Lavalink.
|
||||
You can also pass in commands.Context to get a discord.py Context object in your tracks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
playlist_info: dict,
|
||||
tracks: list,
|
||||
requester: Member = None,
|
||||
spotify: bool = False,
|
||||
spotify_playlist = None
|
||||
):
|
||||
self.playlist_info = playlist_info
|
||||
self.tracks_raw = tracks
|
||||
self.spotify = spotify
|
||||
self.name = playlist_info.get("name")
|
||||
self.spotify_playlist = spotify_playlist
|
||||
|
||||
self._thumbnail = None
|
||||
self._uri = None
|
||||
|
||||
if self.spotify:
|
||||
self.tracks = tracks
|
||||
self._thumbnail = self.spotify_playlist.image
|
||||
self._uri = self.spotify_playlist.uri
|
||||
else:
|
||||
self.tracks = [
|
||||
Track(track_id=track["track"], info=track["info"], requester=requester)
|
||||
for track in self.tracks_raw
|
||||
]
|
||||
self._thumbnail = None
|
||||
self._uri = None
|
||||
|
||||
self.track_count = len(self.tracks)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.playlist name={self.name!r} track_count={len(self.tracks)}>"
|
||||
|
||||
@property
|
||||
def uri(self) -> Optional[str]:
|
||||
"""Spotify album/playlist URI, or None if not a Spotify object."""
|
||||
return self._uri
|
||||
|
||||
@property
|
||||
def thumbnail(self) -> Optional[str]:
|
||||
"""Spotify album/playlist thumbnail, or None if not a Spotify object."""
|
||||
return self._thumbnail
|
||||
595
voicelink/player.py
Normal file
595
voicelink/player.py
Normal file
@@ -0,0 +1,595 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import time
|
||||
import function
|
||||
|
||||
from math import ceil
|
||||
from asyncio import sleep
|
||||
from view import InteractiveController
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Optional
|
||||
)
|
||||
|
||||
from discord import (
|
||||
Client,
|
||||
Guild,
|
||||
VoiceChannel,
|
||||
VoiceProtocol,
|
||||
StageChannel,
|
||||
Member,
|
||||
Embed,
|
||||
ui,
|
||||
Interaction
|
||||
)
|
||||
|
||||
from . import events
|
||||
from .enums import SearchType
|
||||
from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent
|
||||
from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, TrackLoadError, FilterTagAlreadyInUse
|
||||
from .filters import Filter, Filters
|
||||
from .objects import Track
|
||||
from .pool import Node, NodePool
|
||||
from .queue import Queue, FairQueue
|
||||
|
||||
class Player(VoiceProtocol):
|
||||
"""The base player class for Voicelink.
|
||||
In order to initiate a player, you must pass it in as a cls when you connect to a channel.
|
||||
i.e: ```py
|
||||
await ctx.author.voice.channel.connect(cls=voicelink.Player)
|
||||
```
|
||||
"""
|
||||
|
||||
def __call__(self, client: Client, channel: VoiceChannel):
|
||||
self.client: Client = client
|
||||
self.channel: VoiceChannel = channel
|
||||
|
||||
return self
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Optional[Client] = None,
|
||||
channel: Optional[VoiceChannel] = None,
|
||||
ctx: Interaction = None,
|
||||
):
|
||||
self.client = client
|
||||
self._bot = client
|
||||
self.context = ctx
|
||||
self.dj: Member = ctx.user
|
||||
self.channel = channel
|
||||
self._guild = channel.guild if channel else None
|
||||
|
||||
self.settings: dict = function.get_settings(ctx.guild.id)
|
||||
self.joinTime = round(time.time())
|
||||
self._volume = self.settings.get('volume', 100)
|
||||
self.lang = self.settings.get('lang', 'EN') if self.settings.get('lang', 'EN') in function.langs else "EN"
|
||||
self.queue = eval(self.settings.get("queueType", "Queue"))(5000, self.settings.get("duplicateTrack", True), self.get_msg)
|
||||
|
||||
self._node = NodePool.get_node()
|
||||
self._current: Track = None
|
||||
self._filters: Filters = Filters()
|
||||
self._paused = False
|
||||
self._is_connected = False
|
||||
self._ping = 0.0
|
||||
|
||||
self._position = 0
|
||||
self._last_position = 0
|
||||
self._last_update = 0
|
||||
self._ending_track: Optional[Track] = None
|
||||
|
||||
self._voice_state = {}
|
||||
|
||||
self.controller = None
|
||||
self.updating = False
|
||||
|
||||
self.pause_votes = set()
|
||||
self.resume_votes = set()
|
||||
self.skip_votes = set()
|
||||
self.previous_votes = set()
|
||||
self.shuffle_votes = set()
|
||||
self.stop_votes = set()
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Voicelink.player bot={self.bot} guildId={self.guild.id} "
|
||||
f"is_connected={self.is_connected} is_playing={self.is_playing}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def position(self) -> float:
|
||||
"""Property which returns the player's position in a track in milliseconds"""
|
||||
current = self._current.original
|
||||
|
||||
if not self.is_playing or not self._current:
|
||||
return 0
|
||||
|
||||
if self.is_paused:
|
||||
return min(self._last_position, current.length)
|
||||
|
||||
difference = (time.time() * 1000) - self._last_update
|
||||
position = self._last_position + difference
|
||||
|
||||
if position > current.length:
|
||||
return 0
|
||||
|
||||
return min(position, current.length)
|
||||
|
||||
@property
|
||||
def is_playing(self) -> bool:
|
||||
"""Property which returns whether or not the player is actively playing a track."""
|
||||
return self._is_connected and self._current is not None
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Property which returns whether or not the player is connected"""
|
||||
return self._is_connected
|
||||
|
||||
@property
|
||||
def is_paused(self) -> bool:
|
||||
"""Property which returns whether or not the player has a track which is paused or not."""
|
||||
return self._is_connected and self._paused
|
||||
|
||||
@property
|
||||
def current(self) -> Track:
|
||||
"""Property which returns the currently playing track"""
|
||||
return self._current
|
||||
|
||||
@property
|
||||
def node(self) -> Node:
|
||||
"""Property which returns the node the player is connected to"""
|
||||
return self._node
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild:
|
||||
"""Property which returns the guild associated with the player"""
|
||||
return self._guild
|
||||
|
||||
@property
|
||||
def volume(self) -> int:
|
||||
"""Property which returns the players current volume"""
|
||||
return self._volume
|
||||
|
||||
@property
|
||||
def filters(self) -> Filter:
|
||||
"""Property which returns the helper class for interacting with filters"""
|
||||
return self._filters
|
||||
|
||||
@property
|
||||
def bot(self) -> Client:
|
||||
"""Property which returns the bot associated with this player instance"""
|
||||
return self._bot
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
"""Returns a bool representing whether the player is dead or not.
|
||||
A player is considered dead if it has been destroyed and removed from stored players.
|
||||
"""
|
||||
return self.guild.id not in self._node._players
|
||||
|
||||
@property
|
||||
def ping(self) -> float:
|
||||
return round(self._ping / 1000, 2)
|
||||
|
||||
def get_msg(self, mKey: str) -> str:
|
||||
return function.langs.get(self.lang, function.langs["EN"])[mKey]
|
||||
|
||||
def required(self, leave=False):
|
||||
if self.settings.get('votedisable'):
|
||||
return 0
|
||||
|
||||
required = ceil((len(self.channel.members) - 1) / 2.5)
|
||||
if leave:
|
||||
if len(self.channel.members) == 3:
|
||||
required = 2
|
||||
|
||||
return required
|
||||
|
||||
async def _update_state(self, data: dict):
|
||||
state: dict = data.get("state")
|
||||
self._last_update = time.time() * 1000
|
||||
self._is_connected = state.get("connected")
|
||||
self._last_position = state.get("position")
|
||||
self._ping = state.get("ping")
|
||||
|
||||
async def _dispatch_voice_update(self, voice_data: Dict[str, Any]):
|
||||
if {"sessionId", "event"} != self._voice_state.keys():
|
||||
return
|
||||
|
||||
await self._node.send(
|
||||
op="voiceUpdate",
|
||||
guildId=str(self.guild.id),
|
||||
**voice_data
|
||||
)
|
||||
|
||||
async def on_voice_server_update(self, data: dict):
|
||||
self._voice_state.update({"event": data})
|
||||
await self._dispatch_voice_update(self._voice_state)
|
||||
|
||||
async def on_voice_state_update(self, data: dict):
|
||||
self._voice_state.update({"sessionId": data.get("session_id")})
|
||||
|
||||
if not (channel_id := data.get("channel_id")):
|
||||
await self.teardown()
|
||||
self._voice_state.clear()
|
||||
return
|
||||
|
||||
self.channel = self.guild.get_channel(int(channel_id))
|
||||
|
||||
if not data.get("token"):
|
||||
return
|
||||
|
||||
await self._dispatch_voice_update({**self._voice_state, "event": data})
|
||||
|
||||
async def _dispatch_event(self, data: dict):
|
||||
event_type = data.get("type")
|
||||
event: VoicelinkEvent = getattr(events, event_type)(data, self)
|
||||
|
||||
if isinstance(event, TrackEndEvent) and event.reason != "REPLACED":
|
||||
self._current = None
|
||||
|
||||
event.dispatch(self._bot)
|
||||
|
||||
if isinstance(event, TrackStartEvent):
|
||||
self._ending_track = self._current
|
||||
|
||||
async def do_next(self):
|
||||
if self.is_playing or not self.channel:
|
||||
return
|
||||
|
||||
if self._paused:
|
||||
self._paused = False
|
||||
|
||||
if not self.guild.me.voice:
|
||||
await self.connect(timeout=0.0, reconnect=True)
|
||||
|
||||
self.pause_votes.clear()
|
||||
self.resume_votes.clear()
|
||||
self.skip_votes.clear()
|
||||
self.previous_votes.clear()
|
||||
self.shuffle_votes.clear()
|
||||
self.stop_votes.clear()
|
||||
|
||||
track: Track = await self.queue.get()
|
||||
if not track:
|
||||
if self.settings.get("autoplay", False):
|
||||
if await function.similar_track(self):
|
||||
return await self.do_next()
|
||||
if self.settings.get('controller', True):
|
||||
await self.invoke_controller()
|
||||
return
|
||||
try:
|
||||
await self.play(track, start=track.position)
|
||||
except:
|
||||
await sleep(5)
|
||||
return await self.do_next()
|
||||
|
||||
if self.settings.get('controller', True):
|
||||
await self.invoke_controller()
|
||||
|
||||
async def invoke_controller(self):
|
||||
if self.updating or not self.channel:
|
||||
return
|
||||
|
||||
self.updating = True
|
||||
|
||||
if not self.controller:
|
||||
try:
|
||||
self.controller = await self.context.channel.send(embed=await self.build_embed(), view=InteractiveController(self))
|
||||
except:
|
||||
pass
|
||||
|
||||
elif not await self.is_position_fresh():
|
||||
try:
|
||||
await self.controller.delete()
|
||||
except:
|
||||
ui.View.from_message(self.controller).stop()
|
||||
try:
|
||||
self.controller = await self.context.channel.send(embed= await self.build_embed(), view=InteractiveController(self))
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
embed = await self.build_embed()
|
||||
if embed:
|
||||
try:
|
||||
await self.controller.edit(embed=embed, view=InteractiveController(self))
|
||||
except:
|
||||
pass
|
||||
|
||||
self.updating = False
|
||||
|
||||
async def build_embed(self):
|
||||
track = self.current
|
||||
|
||||
if not track:
|
||||
embed=Embed(title=self.get_msg("noTrackPlaying"), description=f"[Vote](https://top.gg/bot/605618911471468554/vote/) | [Support]({function.invite_link}) | [Invite](https://discord.com/oauth2/authorize?client_id=605618911471468554&permissions=2184260928&scope=bot%20applications.commands) | [Questionnaire](https://forms.gle/UqeeEv4GEdCq9hi3A)", color=function.embed_color)
|
||||
embed.set_image(url='https://i.imgur.com/dIFBwU7.png')
|
||||
|
||||
else:
|
||||
try:
|
||||
embed = Embed(color=function.embed_color)
|
||||
embed.set_author(name=self.get_msg("playerAuthor").format(self.channel.name), icon_url=self.client.user.avatar.url)
|
||||
embed.description = self.get_msg("playerDesc").format(track.title, track.uri, (track.requester.mention if track.requester else "<@605618911471468554>"), (f"<@&{self.settings['dj']}>" if self.settings.get('dj') else f"{self.dj.mention}"))
|
||||
embed.set_image(url=track.thumbnail if track.thumbnail else "https://cdn.discordapp.com/attachments/674788144931012638/823086668445384704/eq-dribbble.gif")
|
||||
embed.set_footer(text=self.get_msg("playerFooter").format(self.queue.count, (self.get_msg("live") if track.is_stream else function.time(track.length)), self.volume, self.get_msg("playerFooter2").format(self.queue.repeat) if self.queue.repeat != "Off" else ""))
|
||||
except:
|
||||
embed = Embed(description=self.get_msg("missingTrackInfo"), color=function.embed_color)
|
||||
return embed
|
||||
|
||||
async def is_position_fresh(self):
|
||||
try:
|
||||
async for message in self.context.channel.history(limit=5):
|
||||
if message.id == self.controller.id:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
async def teardown(self):
|
||||
timeNow = round(time.time())
|
||||
function.update_settings(self.guild.id, {"lastActice": timeNow, "playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2)})
|
||||
|
||||
try:
|
||||
await self.controller.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
await self.destroy()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def is_privileged(self, user: Member):
|
||||
if 'dj' in self.settings and self.settings['dj']:
|
||||
return user.guild_permissions.manage_guild or (self.settings['dj'] in [role.id for role in user.roles])
|
||||
return self.dj.id == user.id or user.guild_permissions.manage_guild
|
||||
|
||||
async def get_tracks(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
requester: Member,
|
||||
search_type: SearchType = SearchType.ytsearch
|
||||
):
|
||||
"""Fetches tracks from the node's REST api to parse into Lavalink.
|
||||
|
||||
If you passed in Spotify API credentials when you created the node,
|
||||
you can also pass in a Spotify URL of a playlist, album or track and it will be parsed
|
||||
accordingly.
|
||||
|
||||
You can also pass in a discord.py Context object to get a
|
||||
Context object on any track you search.
|
||||
"""
|
||||
return await self._node.get_tracks(query, requester=requester, search_type=search_type)
|
||||
|
||||
async def spotifySearch(self, query: str, *, requester: Member) -> list:
|
||||
|
||||
try:
|
||||
tracks = await self._node._spotify_client.trackSearch(query=query)
|
||||
except:
|
||||
raise TrackLoadError("Not able to find the provided Spotify entity, is it private?")
|
||||
|
||||
return [ Track(
|
||||
track_id=track.id,
|
||||
requester=requester,
|
||||
search_type=SearchType.ytsearch,
|
||||
spotify=True,
|
||||
spotify_track=track,
|
||||
info={
|
||||
"title": track.name,
|
||||
"author": track.artists,
|
||||
"length": track.length,
|
||||
"identifier": track.id,
|
||||
"artistId": track.artistId,
|
||||
"uri": track.uri,
|
||||
"isStream": False,
|
||||
"isSeekable": True,
|
||||
"position": 0,
|
||||
"thumbnail": track.image
|
||||
}
|
||||
)
|
||||
for track in tracks ]
|
||||
|
||||
async def spotifyRelatedTrack(self, seed_artists: str, seed_tracks: str):
|
||||
|
||||
tracks = await self._node._spotify_client.similar_track(seed_artists=seed_artists, seed_tracks=seed_tracks)
|
||||
|
||||
return [ Track(
|
||||
track_id=track.id,
|
||||
search_type=SearchType.ytsearch,
|
||||
spotify=True,
|
||||
spotify_track=track,
|
||||
info={
|
||||
"title": track.name,
|
||||
"author": track.artists,
|
||||
"length": track.length,
|
||||
"identifier": track.id,
|
||||
"artistId": track.artistId,
|
||||
"uri": track.uri,
|
||||
"isStream": False,
|
||||
"isSeekable": True,
|
||||
"position": 0,
|
||||
"thumbnail": track.image
|
||||
},
|
||||
requester=self.client.user
|
||||
)
|
||||
for track in tracks ]
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = True, self_mute: bool = False):
|
||||
await self.guild.change_voice_state(channel=self.channel, self_deaf=True, self_mute=self_mute)
|
||||
self._node._players[self.guild.id] = self
|
||||
self._is_connected = True
|
||||
|
||||
async def stop(self):
|
||||
"""Stops the currently playing track."""
|
||||
self._current = None
|
||||
await self._node.send(op="stop", guildId=str(self.guild.id))
|
||||
|
||||
async def disconnect(self, *, force: bool = False):
|
||||
"""Disconnects the player from voice."""
|
||||
try:
|
||||
await self.guild.change_voice_state(channel=None)
|
||||
finally:
|
||||
self.cleanup()
|
||||
self._is_connected = False
|
||||
self.channel = None
|
||||
|
||||
async def destroy(self):
|
||||
"""Disconnects and destroys the player, and runs internal cleanup."""
|
||||
|
||||
try:
|
||||
await self.disconnect()
|
||||
except:
|
||||
# 'NoneType' has no attribute '_get_voice_client_key' raised by self.cleanup() ->
|
||||
# assume we're already disconnected and cleaned up
|
||||
assert self.channel is None and not self.is_connected
|
||||
|
||||
self._node._players.pop(self.guild.id)
|
||||
await self._node.send(op="destroy", guildId=str(self.guild.id))
|
||||
|
||||
async def play(
|
||||
self,
|
||||
track: Track,
|
||||
*,
|
||||
start: int = 0,
|
||||
end: int = 0,
|
||||
ignore_if_playing: bool = False
|
||||
) -> Track:
|
||||
"""Plays a track. If a Spotify track is passed in, it will be handled accordingly."""
|
||||
if not self._node:
|
||||
return track
|
||||
|
||||
if track.spotify:
|
||||
if not track.original:
|
||||
search: Track = (await self._node.get_tracks(
|
||||
f"ytmsearch:{track.author} - {track.title}",
|
||||
requester=track.requester
|
||||
))
|
||||
|
||||
if not search:
|
||||
raise TrackLoadError("Can't not found a playable source!")
|
||||
|
||||
track.original = search[0]
|
||||
|
||||
data = {
|
||||
"op": "play",
|
||||
"guildId": str(self.guild.id),
|
||||
"track": track.original.track_id,
|
||||
"startTime": str(start),
|
||||
"noReplace": ignore_if_playing
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"op": "play",
|
||||
"guildId": str(self.guild.id),
|
||||
"track": track.track_id,
|
||||
"startTime": str(start),
|
||||
"noReplace": ignore_if_playing
|
||||
}
|
||||
|
||||
if end > 0:
|
||||
data["endTime"] = str(end)
|
||||
|
||||
await self._node.send(**data)
|
||||
self._current = track
|
||||
|
||||
if self.volume != 100:
|
||||
await self.set_volume(self.volume)
|
||||
|
||||
return self._current
|
||||
|
||||
async def seek(self, position: float) -> float:
|
||||
"""Seeks to a position in the currently playing track milliseconds"""
|
||||
if position < 0 or position > self._current.original.length:
|
||||
raise TrackInvalidPosition(
|
||||
"Seek position must be between 0 and the track length"
|
||||
)
|
||||
|
||||
await self._node.send(op="seek", guildId=str(self.guild.id), position=position)
|
||||
return self._position
|
||||
|
||||
async def set_pause(self, pause: bool) -> bool:
|
||||
"""Sets the pause state of the currently playing track."""
|
||||
await self._node.send(op="pause", guildId=str(self.guild.id), pause=pause)
|
||||
self._paused = pause
|
||||
return self._paused
|
||||
|
||||
async def set_volume(self, volume: int) -> int:
|
||||
"""Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500."""
|
||||
await self._node.send(op="volume", guildId=str(self.guild.id), volume=volume)
|
||||
self._volume = volume
|
||||
return self._volume
|
||||
|
||||
async def add_filter(self, filter: Filter, fast_apply=False) -> Filters:
|
||||
try:
|
||||
self._filters.add_filter(filter=filter)
|
||||
except FilterTagAlreadyInUse:
|
||||
raise FilterTagAlreadyInUse(self.get_msg("FilterTagAlreadyInUse"))
|
||||
payload = self._filters.get_all_payloads()
|
||||
await self._node.send(op="filters", guildId=str(self.guild.id), **payload)
|
||||
if fast_apply:
|
||||
await self.seek(self.position)
|
||||
return self._filters
|
||||
|
||||
async def remove_filter(self, filter_tag: str, fast_apply=False) -> Filters:
|
||||
self._filters.remove_filter(filter_tag=filter_tag)
|
||||
payload = self._filters.get_all_payloads()
|
||||
await self._node.send(op="filters", guildId=str(self.guild.id), **payload)
|
||||
if fast_apply:
|
||||
await self.seek(self.position)
|
||||
|
||||
return self._filters
|
||||
|
||||
async def reset_filter(self, *, fast_apply=False) -> None:
|
||||
if not self._filters:
|
||||
raise FilterInvalidArgument("You must have filters applied first in order to use this method.")
|
||||
self._filters.reset_filters()
|
||||
await self._node.send(op="filters", guildId=str(self.guild.id))
|
||||
if fast_apply:
|
||||
await self.seek(self.position)
|
||||
|
||||
async def change_node(self, identifier: str = None) -> None:
|
||||
"""Change node.
|
||||
"""
|
||||
try:
|
||||
node = NodePool.get_node(identifier=identifier)
|
||||
except:
|
||||
return await self.teardown()
|
||||
|
||||
self._node._players.pop(self.guild.id)
|
||||
self._node = node
|
||||
self._node._players[self.guild.id] = self
|
||||
|
||||
await self._dispatch_voice_update(self._voice_state)
|
||||
|
||||
if self.current:
|
||||
await self.play(self.current, start=self.position)
|
||||
self._last_update = time.time() * 1000
|
||||
|
||||
if self.is_paused:
|
||||
await self.set_pause(True)
|
||||
|
||||
if self.volume != 100:
|
||||
await self.set_volume(self.volume)
|
||||
604
voicelink/pool.py
Normal file
604
voicelink/pool.py
Normal file
@@ -0,0 +1,604 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
from urllib.parse import quote
|
||||
|
||||
import aiohttp
|
||||
from discord import Client, Member
|
||||
|
||||
|
||||
from . import (
|
||||
__version__,
|
||||
spotify,
|
||||
)
|
||||
|
||||
from .enums import SearchType, NodeAlgorithm
|
||||
from .exceptions import (
|
||||
InvalidSpotifyClientAuthorization,
|
||||
NodeConnectionFailure,
|
||||
NodeCreationError,
|
||||
NodeException,
|
||||
NodeNotAvailable,
|
||||
NoNodesAvailable,
|
||||
TrackLoadError
|
||||
)
|
||||
from .objects import Playlist, Track
|
||||
from .utils import ExponentialBackoff, NodeStats, Ping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .player import Player
|
||||
|
||||
SPOTIFY_URL_REGEX = re.compile(
|
||||
r"https?://open.spotify.com/(?P<type>album|playlist|track|artist)/(?P<id>[a-zA-Z0-9]+)"
|
||||
)
|
||||
|
||||
DISCORD_MP3_URL_REGEX = re.compile(
|
||||
r"https?://cdn.discordapp.com/attachments/(?P<channel_id>[0-9]+)/"
|
||||
r"(?P<message_id>[0-9]+)/(?P<file>[a-zA-Z0-9_.]+)+"
|
||||
)
|
||||
|
||||
URL_REGEX = re.compile(
|
||||
r"https?://(?:www\.)?.+"
|
||||
)
|
||||
|
||||
|
||||
def exception_catch_callback(task):
|
||||
if task.exception():
|
||||
return
|
||||
|
||||
class Node:
|
||||
"""The base class for a node.
|
||||
This node object represents a Lavalink node.
|
||||
To enable Spotify searching, pass in a proper Spotify Client ID and Spotify Client Secret
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pool,
|
||||
bot: Client,
|
||||
host: str,
|
||||
port: int,
|
||||
password: str,
|
||||
identifier: str,
|
||||
secure: bool = False,
|
||||
heartbeat: int = 30,
|
||||
session: Optional[aiohttp.ClientSession] = None,
|
||||
spotify_client_id: Optional[str] = None,
|
||||
spotify_client_secret: Optional[str] = None,
|
||||
resume_key: Optional[str] = None
|
||||
|
||||
):
|
||||
self._bot = bot
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._pool = pool
|
||||
self._password = password
|
||||
self._identifier = identifier
|
||||
self._heartbeat = heartbeat
|
||||
self._secure = secure
|
||||
|
||||
self._websocket_uri = f"{'wss' if self._secure else 'ws'}://{self._host}:{self._port}"
|
||||
self._rest_uri = f"{'https' if self._secure else 'http'}://{self._host}:{self._port}"
|
||||
|
||||
self._session = session or aiohttp.ClientSession()
|
||||
self._websocket: aiohttp.ClientWebSocketResponse = None
|
||||
self._task: asyncio.Task = None
|
||||
|
||||
self.resume_key = resume_key or str(os.urandom(8).hex())
|
||||
|
||||
self._connection_id = None
|
||||
self._metadata = None
|
||||
self._available = None
|
||||
|
||||
self._headers = {
|
||||
"Authorization": self._password,
|
||||
"User-Id": str(os.getenv('CLIENT_ID')),
|
||||
"Client-Name": f"Voicelink/{__version__}",
|
||||
'Resume-Key': self.resume_key
|
||||
}
|
||||
|
||||
self._players: Dict[int, Player] = {}
|
||||
|
||||
self._spotify_client_id = spotify_client_id
|
||||
self._spotify_client_secret = spotify_client_secret
|
||||
|
||||
if self._spotify_client_id and self._spotify_client_secret:
|
||||
self._spotify_client = spotify.Client(
|
||||
self._spotify_client_id, self._spotify_client_secret
|
||||
)
|
||||
|
||||
self._bot.add_listener(self._update_handler, "on_socket_response")
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Voicelink.node ws_uri={self._websocket_uri} rest_uri={self._rest_uri} "
|
||||
f"player_count={len(self._players)}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
""""Property which returns whether this node is connected or not"""
|
||||
return self._websocket is not None and not self._websocket.closed
|
||||
|
||||
|
||||
@property
|
||||
def stats(self) -> NodeStats:
|
||||
"""Property which returns the node stats."""
|
||||
return self._stats
|
||||
|
||||
@property
|
||||
def players(self) -> Dict[int, Player]:
|
||||
"""Property which returns a dict containing the guild ID and the player object."""
|
||||
return self._players
|
||||
|
||||
@property
|
||||
def bot(self) -> Client:
|
||||
"""Property which returns the discord.py client linked to this node"""
|
||||
return self._bot
|
||||
|
||||
@property
|
||||
def player_count(self) -> int:
|
||||
"""Property which returns how many players are connected to this node"""
|
||||
return len(self.players)
|
||||
|
||||
@property
|
||||
def pool(self):
|
||||
"""Property which returns the pool this node is apart of"""
|
||||
return self._pool
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
"""Property which returns the latency of the node"""
|
||||
return Ping(self._host, port=self._port).get_ping()
|
||||
|
||||
async def _update_handler(self, data: dict):
|
||||
#await self._bot.wait_until_ready()
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data["t"] == "VOICE_SERVER_UPDATE":
|
||||
guild_id = int(data["d"]["guild_id"])
|
||||
try:
|
||||
player = self._players[guild_id]
|
||||
await player.on_voice_server_update(data["d"])
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
elif data["t"] == "VOICE_STATE_UPDATE":
|
||||
if int(data["d"]["user_id"]) != self._bot.user.id:
|
||||
return
|
||||
|
||||
guild_id = int(data["d"]["guild_id"])
|
||||
try:
|
||||
player = self._players[guild_id]
|
||||
await player.on_voice_state_update(data["d"])
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
async def _listen(self):
|
||||
backoff = ExponentialBackoff(base=7)
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = await self._websocket.receive()
|
||||
except:
|
||||
break
|
||||
if msg.type == aiohttp.WSMsgType.CLOSED:
|
||||
self._available = False
|
||||
|
||||
retry = backoff.delay()
|
||||
print(f"Trying to reconnect {self._identifier} with {round(retry)}s")
|
||||
await asyncio.sleep(retry)
|
||||
if not self.is_connected:
|
||||
try:
|
||||
await self.connect()
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self._bot.loop.create_task(self._handle_payload(msg.json()))
|
||||
|
||||
async def _handle_payload(self, data: dict):
|
||||
op = data.get("op", None)
|
||||
if not op:
|
||||
return
|
||||
|
||||
if op == "ready":
|
||||
return
|
||||
|
||||
if op == "stats":
|
||||
self._stats = NodeStats(data)
|
||||
return
|
||||
|
||||
if not (player := self._players.get(int(data["guildId"]))):
|
||||
return
|
||||
|
||||
if op == "event":
|
||||
await player._dispatch_event(data)
|
||||
elif op == "playerUpdate":
|
||||
await player._update_state(data)
|
||||
|
||||
async def send(self, **data):
|
||||
if not self._available:
|
||||
raise NodeNotAvailable(
|
||||
f"The node '{self._identifier}' is unavailable."
|
||||
)
|
||||
|
||||
payload = json.dumps(data)
|
||||
if isinstance(payload, bytes):
|
||||
payload = payload.decode('utf-8')
|
||||
|
||||
await self._websocket.send_str(payload)
|
||||
|
||||
def get_player(self, guild_id: int):
|
||||
"""Takes a guild ID as a parameter. Returns a voicelink Player object."""
|
||||
return self._players.get(guild_id, None)
|
||||
|
||||
async def connect(self):
|
||||
"""Initiates a connection with a Lavalink node and adds it to the node pool."""
|
||||
|
||||
try:
|
||||
self._websocket = await self._session.ws_connect(
|
||||
self._websocket_uri, headers=self._headers, heartbeat=self._heartbeat
|
||||
)
|
||||
self._task = self._bot.loop.create_task(self._listen())
|
||||
self._available = True
|
||||
|
||||
except aiohttp.ClientConnectorError:
|
||||
raise NodeConnectionFailure(
|
||||
f"The connection to node '{self._identifier}' failed."
|
||||
)
|
||||
except aiohttp.WSServerHandshakeError:
|
||||
raise NodeConnectionFailure(
|
||||
f"The password for node '{self._identifier}' is invalid."
|
||||
)
|
||||
except aiohttp.InvalidURL:
|
||||
raise NodeConnectionFailure(
|
||||
f"The URI for node '{self._identifier}' is invalid."
|
||||
)
|
||||
|
||||
if self.is_connected:
|
||||
resume = {
|
||||
"op": "configureResuming",
|
||||
"key": f"{self.resume_key}",
|
||||
"timeout": 120
|
||||
}
|
||||
|
||||
await self.send(**resume)
|
||||
print(f"{self._identifier} is connected!")
|
||||
|
||||
if self.players:
|
||||
await self.reconnect()
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnects a connected Lavalink node and removes it from the node pool.
|
||||
This also destroys any players connected to the node.
|
||||
"""
|
||||
for player in self.players.copy().values():
|
||||
await player.teardown()
|
||||
|
||||
if self._spotify_client_id and self._spotify_client_secret:
|
||||
await self._spotify_client.close()
|
||||
|
||||
await self._websocket.close()
|
||||
del self._pool._nodes[self._identifier]
|
||||
self._available = False
|
||||
self._task.cancel()
|
||||
|
||||
async def reconnect(self):
|
||||
for player in self.players.copy().values():
|
||||
try:
|
||||
if player._voice_state:
|
||||
await player._dispatch_voice_update(player._voice_state)
|
||||
|
||||
if player.current:
|
||||
await player.play(track=player.current, start=min(player._last_position, player.current.length))
|
||||
|
||||
if player.is_paused:
|
||||
await player.set_pause(True)
|
||||
except:
|
||||
await player.teardown()
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def build_track(
|
||||
self,
|
||||
identifier: str,
|
||||
requester: Member = None
|
||||
) -> Track:
|
||||
"""
|
||||
Builds a track using a valid track identifier
|
||||
|
||||
You can also pass in a discord.py Context object to get a
|
||||
Context object on the track it builds.
|
||||
"""
|
||||
|
||||
async with self._session.get(
|
||||
f"{self._rest_uri}/decodetrack?",
|
||||
headers={"Authorization": self._password},
|
||||
params={"track": identifier}
|
||||
) as resp:
|
||||
if not resp.status == 200:
|
||||
raise TrackLoadError(
|
||||
f"Failed to build track. Check if the identifier is correct and try again."
|
||||
)
|
||||
|
||||
data: dict = await resp.json()
|
||||
return Track(track_id=identifier, info=data, requester=requester)
|
||||
|
||||
async def get_tracks(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
requester: Member,
|
||||
search_type: SearchType = SearchType.ytsearch
|
||||
):
|
||||
"""Fetches tracks from the node's REST api to parse into Lavalink.
|
||||
|
||||
If you passed in Spotify API credentials, you can also pass in a
|
||||
Spotify URL of a playlist, album or track and it will be parsed accordingly.
|
||||
|
||||
You can also pass in a discord.py Context object to get a
|
||||
Context object on any track you search.
|
||||
"""
|
||||
|
||||
if not URL_REGEX.match(query) and not re.match(r"(?:ytm?|sc)search:.", query):
|
||||
query = f"{search_type}:{query}"
|
||||
|
||||
if SPOTIFY_URL_REGEX.match(query):
|
||||
if not self._spotify_client_id and not self._spotify_client_secret:
|
||||
raise InvalidSpotifyClientAuthorization(
|
||||
"You did not provide proper Spotify client authorization credentials. "
|
||||
"If you would like to use the Spotify searching feature, "
|
||||
"please obtain Spotify API credentials here: https://developer.spotify.com/"
|
||||
)
|
||||
|
||||
try:
|
||||
spotify_results = await self._spotify_client.search(query=query)
|
||||
except:
|
||||
raise TrackLoadError("Not able to find the provided Spotify entity, is it private?")
|
||||
|
||||
if isinstance(spotify_results, spotify.Track):
|
||||
return [
|
||||
Track(
|
||||
track_id=spotify_results.id,
|
||||
requester=requester,
|
||||
search_type=search_type,
|
||||
spotify=True,
|
||||
spotify_track=spotify_results,
|
||||
info={
|
||||
"title": spotify_results.name,
|
||||
"author": spotify_results.artists,
|
||||
"length": spotify_results.length,
|
||||
"identifier": spotify_results.id,
|
||||
"uri": spotify_results.uri,
|
||||
"artistId": spotify_results.artistId,
|
||||
"isStream": False,
|
||||
"isSeekable": True,
|
||||
"position": 0,
|
||||
"thumbnail": spotify_results.image
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
tracks = [
|
||||
Track(
|
||||
track_id=track.id,
|
||||
requester=requester,
|
||||
search_type=search_type,
|
||||
spotify=True,
|
||||
spotify_track=track,
|
||||
info={
|
||||
"title": track.name,
|
||||
"author": track.artists,
|
||||
"length": track.length,
|
||||
"identifier": track.id,
|
||||
"artistId": track.artistId,
|
||||
"uri": track.uri,
|
||||
"isStream": False,
|
||||
"isSeekable": True,
|
||||
"position": 0,
|
||||
"thumbnail": track.image
|
||||
}
|
||||
) for track in spotify_results.tracks if track.uri
|
||||
]
|
||||
|
||||
return Playlist(
|
||||
playlist_info={"name": spotify_results.name, "selectedTrack": 0},
|
||||
tracks=tracks,
|
||||
requester=requester,
|
||||
spotify=True,
|
||||
spotify_playlist=spotify_results
|
||||
)
|
||||
|
||||
elif discord_url := DISCORD_MP3_URL_REGEX.match(query):
|
||||
async with self._session.get(
|
||||
url=f"{self._rest_uri}/loadtracks?identifier={quote(query)}",
|
||||
headers={"Authorization": self._password}
|
||||
) as response:
|
||||
data: dict = await response.json()
|
||||
|
||||
try:
|
||||
track: dict = data["tracks"][0]
|
||||
info: dict = track.get("info")
|
||||
except:
|
||||
raise TrackLoadError("Not able to find the provided track.")
|
||||
|
||||
return [
|
||||
Track(
|
||||
track_id=track["track"],
|
||||
info={
|
||||
"title": discord_url.group("file"),
|
||||
"author": "Unknown",
|
||||
"length": info.get("length"),
|
||||
"uri": info.get("uri"),
|
||||
"position": info.get("position"),
|
||||
"identifier": info.get("identifier")
|
||||
},
|
||||
requester=requester
|
||||
)
|
||||
]
|
||||
|
||||
else:
|
||||
async with self._session.get(
|
||||
url=f"{self._rest_uri}/loadtracks?identifier={quote(query)}",
|
||||
headers={"Authorization": self._password}
|
||||
) as response:
|
||||
data = await response.json()
|
||||
|
||||
load_type = data.get("loadType")
|
||||
|
||||
if not load_type:
|
||||
raise TrackLoadError("There was an error while trying to load this track.")
|
||||
|
||||
elif load_type == "LOAD_FAILED":
|
||||
exception = data["exception"]
|
||||
raise TrackLoadError(f"{exception['message']} [{exception['severity']}]")
|
||||
|
||||
elif load_type == "NO_MATCHES":
|
||||
return None
|
||||
|
||||
elif load_type == "PLAYLIST_LOADED":
|
||||
return Playlist(
|
||||
playlist_info=data["playlistInfo"],
|
||||
tracks=data["tracks"],
|
||||
requester=requester
|
||||
)
|
||||
|
||||
elif load_type == "SEARCH_RESULT" or load_type == "TRACK_LOADED":
|
||||
return [
|
||||
Track(
|
||||
track_id=track["track"],
|
||||
info=track["info"],
|
||||
requester=requester
|
||||
)
|
||||
for track in data["tracks"]
|
||||
]
|
||||
|
||||
|
||||
class NodePool:
|
||||
"""The base class for the node pool.
|
||||
This holds all the nodes that are to be used by the bot.
|
||||
"""
|
||||
|
||||
_nodes = {}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.NodePool node_count={self.node_count}>"
|
||||
|
||||
@property
|
||||
def nodes(self) -> Dict[str, Node]:
|
||||
"""Property which returns a dict with the node identifier and the Node object."""
|
||||
return self._nodes
|
||||
|
||||
@property
|
||||
def node_count(self):
|
||||
return len(self._nodes.values())
|
||||
|
||||
@classmethod
|
||||
def get_best_node(cls, *, algorithm: NodeAlgorithm) -> Node:
|
||||
"""Fetches the best node based on an NodeAlgorithm.
|
||||
This option is preferred if you want to choose the best node
|
||||
from a multi-node setup using either the node's latency
|
||||
or the node's voice region.
|
||||
Use NodeAlgorithm.by_ping if you want to get the best node
|
||||
based on the node's latency.
|
||||
Use NodeAlgorithm.by_region if you want to get the best node
|
||||
based on the node's voice region. This method will only work
|
||||
if you set a voice region when you create a node.
|
||||
Use NodeAlgorithm.by_players if you want to get the best node
|
||||
based on how players it has. This method will return a node with
|
||||
the least amount of players
|
||||
"""
|
||||
available_nodes = [node for node in cls._nodes.values() if node._available]
|
||||
|
||||
if not available_nodes:
|
||||
raise NoNodesAvailable("There are no nodes available.")
|
||||
|
||||
if algorithm == NodeAlgorithm.by_ping:
|
||||
tested_nodes = {node: node.latency for node in available_nodes}
|
||||
return min(tested_nodes, key=tested_nodes.get)
|
||||
|
||||
elif algorithm == NodeAlgorithm.by_players:
|
||||
tested_nodes = {node: len(node.players.keys()) for node in available_nodes}
|
||||
return min(tested_nodes, key=tested_nodes.get)
|
||||
|
||||
@classmethod
|
||||
def get_node(cls, *, identifier: str = None) -> Node:
|
||||
"""Fetches a node from the node pool using it's identifier.
|
||||
If no identifier is provided, it will choose a node at random.
|
||||
"""
|
||||
|
||||
available_nodes = { node
|
||||
for _, node in cls._nodes.items() if node.is_connected
|
||||
}
|
||||
|
||||
if identifier:
|
||||
available_nodes = { node for node in available_nodes if node._identifier == identifier }
|
||||
|
||||
if not available_nodes:
|
||||
raise NoNodesAvailable("There are no nodes available.")
|
||||
|
||||
nodes = {node: len(node.players.keys()) for node in available_nodes}
|
||||
return min(nodes, key=nodes.get)
|
||||
|
||||
@classmethod
|
||||
async def create_node(
|
||||
cls,
|
||||
*,
|
||||
bot: Client,
|
||||
host: str,
|
||||
port: str,
|
||||
password: str,
|
||||
identifier: str,
|
||||
secure: bool = False,
|
||||
heartbeat: int = 30,
|
||||
spotify_client_id: Optional[str] = None,
|
||||
spotify_client_secret: Optional[str] = None,
|
||||
session: Optional[aiohttp.ClientSession] = None,
|
||||
resume_key: Optional[str] = None,
|
||||
|
||||
) -> Node:
|
||||
"""Creates a Node object to be then added into the node pool.
|
||||
For Spotify searching capabilites, pass in valid Spotify API credentials.
|
||||
"""
|
||||
if identifier in cls._nodes.keys():
|
||||
raise NodeCreationError(f"A node with identifier '{identifier}' already exists.")
|
||||
|
||||
node = Node(
|
||||
pool=cls, bot=bot, host=host, port=port, password=password,
|
||||
identifier=identifier, secure=secure, heartbeat=heartbeat, spotify_client_id=spotify_client_id,
|
||||
session=session, spotify_client_secret=spotify_client_secret,
|
||||
resume_key=resume_key
|
||||
)
|
||||
|
||||
await node.connect()
|
||||
cls._nodes[node._identifier] = node
|
||||
return node
|
||||
199
voicelink/queue.py
Normal file
199
voicelink/queue.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
from .exceptions import QueueFull, OutofList, DuplicateTrack
|
||||
from .objects import Track
|
||||
from discord import Member
|
||||
|
||||
class Queue:
|
||||
def __init__(self, size:int, duplicateTrack: bool, get_msg):
|
||||
|
||||
self._queue = []
|
||||
self._position = 0
|
||||
self._size = size
|
||||
self._repeat = 0
|
||||
self._repeat_position = 0
|
||||
self._duplicateTrack = duplicateTrack
|
||||
self.get_msg = get_msg
|
||||
|
||||
async def get(self):
|
||||
track = None
|
||||
try:
|
||||
track = self._queue[self._position - 1 if self._repeat == 1 else self._position]
|
||||
if self._repeat != 1:
|
||||
self._position += 1
|
||||
except:
|
||||
if self._repeat == 2:
|
||||
try:
|
||||
track = self._queue[self._repeat_position]
|
||||
self._position = self._repeat_position + 1
|
||||
except IndexError:
|
||||
self._repeat = 0
|
||||
|
||||
return track
|
||||
|
||||
async def put(self, item: Track) -> int:
|
||||
if len(self._queue) >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
|
||||
if not self._duplicateTrack:
|
||||
if item.uri in [track.uri for track in self._queue]:
|
||||
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
|
||||
|
||||
self._queue.append(item)
|
||||
return self.count
|
||||
|
||||
async def put_at_front(self, item: Track):
|
||||
if len(self._queue) >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
|
||||
if not self._duplicateTrack:
|
||||
if item.uri in [track.uri for track in self._queue]:
|
||||
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
|
||||
|
||||
return self._queue.insert(self._position, item)
|
||||
|
||||
async def put_at_index(self, index: int, item: Track):
|
||||
if len(self._queue) >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
|
||||
if not self._duplicateTrack:
|
||||
if item.uri in [track.uri for track in self._queue]:
|
||||
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
|
||||
|
||||
return self._queue.insert(self._position - 1 + index, item)
|
||||
|
||||
async def skipto(self, index: int):
|
||||
if not 0 < index <= self.count:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
else:
|
||||
self._position += index - 1
|
||||
|
||||
def backto(self, index: int):
|
||||
if not self._position - index >= 0:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
else:
|
||||
self._position -= index
|
||||
|
||||
def set_repeat(self, mode:str):
|
||||
if mode == 'Track':
|
||||
self._repeat = 1
|
||||
elif mode == 'Queue':
|
||||
self._repeat = 2
|
||||
self._repeat_position = self._position - 1
|
||||
else:
|
||||
self._repeat = 0
|
||||
|
||||
def history_clear(self, is_playing: bool):
|
||||
self._queue[:self._position - 1 if is_playing else self._position] = []
|
||||
self._position = 1 if is_playing else 0
|
||||
|
||||
def clear(self):
|
||||
del self._queue[self._position:]
|
||||
|
||||
def replace(self, queuetype:str, replacement:list):
|
||||
if queuetype == "Queue":
|
||||
self.clear()
|
||||
self._queue += replacement
|
||||
elif queuetype == "History":
|
||||
self._queue[:self._position] = replacement
|
||||
|
||||
def swap(self, num1:int, num2:int):
|
||||
try:
|
||||
pos = self._position - 1
|
||||
self._queue[pos + num1], self._queue[pos + num2] = self._queue[pos + num2], self._queue[pos + num1]
|
||||
except IndexError:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
def remove(self, index:int, index2:int, member: Member = None):
|
||||
pos = self._position - 1
|
||||
if not index2:
|
||||
index2 = index
|
||||
count = 0
|
||||
try:
|
||||
for track in self._queue[pos + index: pos + index2 + 1]:
|
||||
if member:
|
||||
if track.requester == member:
|
||||
self._queue.remove(track)
|
||||
count += 1
|
||||
continue
|
||||
else:
|
||||
self._queue.remove(track)
|
||||
count += 1
|
||||
return count
|
||||
except:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
def history(self, incTrack: bool = False) -> list:
|
||||
if incTrack:
|
||||
return self._queue[:self._position]
|
||||
return self._queue[:self._position - 1]
|
||||
|
||||
def tracks(self, incTrack: bool = False):
|
||||
if incTrack:
|
||||
return self._queue[self._position - 1:]
|
||||
return self._queue[self._position:]
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
return len(self._queue[self._position:])
|
||||
|
||||
@property
|
||||
def repeat(self):
|
||||
return "Off" if self._repeat == 0 else ("Track" if self._repeat == 1 else "Queue")
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
try:
|
||||
self._queue[self._position]
|
||||
except:
|
||||
return True
|
||||
return False
|
||||
|
||||
class FairQueue(Queue):
|
||||
def __init__(self, size:int, duplicateTrack: bool, get_msg):
|
||||
super().__init__(size, duplicateTrack, get_msg)
|
||||
self._set = set()
|
||||
|
||||
async def put(self, item: Track) -> int:
|
||||
if len(self._queue) >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
|
||||
if not self._duplicateTrack:
|
||||
if item.uri in [track.uri for track in self._queue]:
|
||||
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
|
||||
|
||||
tracks = self.tracks(incTrack=True)
|
||||
lastIndex = len(tracks)
|
||||
for track in reversed(tracks):
|
||||
if track.requester == item.requester:
|
||||
break
|
||||
lastIndex -= 1
|
||||
self._set.clear()
|
||||
for track in tracks[lastIndex:]:
|
||||
if track.requester in self._set:
|
||||
break;
|
||||
lastIndex += 1
|
||||
self._set.add(track.requester)
|
||||
await self.put_at_index(lastIndex, item)
|
||||
return lastIndex
|
||||
29
voicelink/spotify/__init__.py
Normal file
29
voicelink/spotify/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2022 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
from .exceptions import InvalidSpotifyURL, SpotifyRequestException
|
||||
from .track import Track
|
||||
from .playlist import Playlist
|
||||
from .album import Album
|
||||
from .artist import Artist
|
||||
from .client import Client
|
||||
20
voicelink/spotify/album.py
Normal file
20
voicelink/spotify/album.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from .track import Track
|
||||
|
||||
|
||||
class Album:
|
||||
"""The base class for a Spotify album"""
|
||||
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.name = data.get('name', 'Unknown')
|
||||
self.artists = ", ".join(artist["name"] for artist in data.get('artists'))
|
||||
self.image = data["images"][0]["url"]
|
||||
self.tracks = [Track(track, image=self.image) for track in data["tracks"]["items"]]
|
||||
self.total_tracks = data["total_tracks"]
|
||||
self.id = data.get('id')
|
||||
self.uri = data["external_urls"]["spotify"]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.spotify.Album name={self.name} artists={self.artists} id={self.id} "
|
||||
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||
)
|
||||
20
voicelink/spotify/artist.py
Normal file
20
voicelink/spotify/artist.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from .track import Track
|
||||
|
||||
class Artist:
|
||||
"""The base class for a Spotify playlist"""
|
||||
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.tracks = [ Track(track) for track in data['tracks'] ]
|
||||
if self.tracks:
|
||||
self.image = self.tracks[0].image
|
||||
self.total_tracks = len(self.tracks)
|
||||
self.owner = self.tracks[0].artists
|
||||
self.id = self.tracks[0].artistId
|
||||
self.uri = data['tracks'][0]['album']['artists'][0]['external_urls']['spotify']
|
||||
self.name = f"Top tracks - {self.owner}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.spotify.Artist name={self.name} owner={self.owner} id={self.id} "
|
||||
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||
)
|
||||
169
voicelink/spotify/client.py
Normal file
169
voicelink/spotify/client.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2022 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
from base64 import b64encode
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .album import Album
|
||||
from .exceptions import InvalidSpotifyURL, SpotifyRequestException
|
||||
from .playlist import Playlist
|
||||
from .track import Track
|
||||
from .artist import Artist
|
||||
|
||||
GRANT_URL = "https://accounts.spotify.com/api/token"
|
||||
REQUEST_URL = "https://api.spotify.com/v1/{type}s/{id}"
|
||||
SEARCH_URL = "https://api.spotify.com/v1/search?q={query}&type={type}&limit={limit}"
|
||||
SUGGESTION_URL = "https://api.spotify.com/v1/recommendations?limit={limit}&seed_tracks={seed_tracks}"
|
||||
SPOTIFY_URL_REGEX = re.compile(
|
||||
r"https?://open.spotify.com/(?P<type>album|playlist|track|artist)/(?P<id>[a-zA-Z0-9]+)"
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
"""The base client for the Spotify module of Voicelink.
|
||||
This class will do all the heavy lifting of getting all the metadata
|
||||
for any Spotify URL you throw at it.
|
||||
"""
|
||||
|
||||
def __init__(self, client_id: str, client_secret: str) -> None:
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
self._bearer_token: str = None
|
||||
self._expiry = 0
|
||||
self._auth_token = b64encode(f"{self._client_id}:{self._client_secret}".encode())
|
||||
self._grant_headers = {"Authorization": f"Basic {self._auth_token.decode()}"}
|
||||
self._bearer_headers = None
|
||||
|
||||
async def _fetch_bearer_token(self) -> None:
|
||||
_data = {"grant_type": "client_credentials"}
|
||||
|
||||
async with self.session.post(GRANT_URL, data=_data, headers=self._grant_headers) as resp:
|
||||
if resp.status != 200:
|
||||
raise SpotifyRequestException(
|
||||
f"Error fetching bearer token: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
data: dict = await resp.json()
|
||||
|
||||
self._bearer_token = data["access_token"]
|
||||
self._expiry = time.time() + (int(data["expires_in"]) - 10)
|
||||
self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"}
|
||||
|
||||
async def trackSearch(self, query: str, track: str = "track", limit: int = 10) -> list:
|
||||
if not self._bearer_token or time.time() >= self._expiry:
|
||||
await self._fetch_bearer_token()
|
||||
|
||||
request_url = SEARCH_URL.format(query=query, type=track, limit=limit)
|
||||
|
||||
async with self.session.get(request_url, headers=self._bearer_headers) as resp:
|
||||
if resp.status != 200:
|
||||
raise SpotifyRequestException(
|
||||
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
data: dict = await resp.json()
|
||||
|
||||
return [ Track(track) for track in data['tracks']['items'] ]
|
||||
|
||||
async def similar_track(self, seed_artists: str, seed_tracks: str, limit: int = 5) -> list:
|
||||
if not self._bearer_token or time.time() >= self._expiry:
|
||||
await self._fetch_bearer_token()
|
||||
|
||||
request_url = SUGGESTION_URL.format(limit=limit, seed_tracks=seed_tracks)
|
||||
|
||||
async with self.session.get(request_url, headers=self._bearer_headers) as resp:
|
||||
if resp.status != 200:
|
||||
raise SpotifyRequestException(
|
||||
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
data: dict = await resp.json()
|
||||
|
||||
return [ Track(track) for track in data['tracks'] ]
|
||||
|
||||
async def search(self, *, query: str):
|
||||
if not self._bearer_token or time.time() >= self._expiry:
|
||||
await self._fetch_bearer_token()
|
||||
|
||||
result = SPOTIFY_URL_REGEX.match(query)
|
||||
spotify_type = result.group("type")
|
||||
spotify_id = result.group("id")
|
||||
|
||||
if not result:
|
||||
raise InvalidSpotifyURL("The Spotify link provided is not valid.")
|
||||
|
||||
request_url = REQUEST_URL.format(type=spotify_type, id=spotify_id)
|
||||
if isArtist := (spotify_type == "artist"):
|
||||
request_url += "/top-tracks?market=US"
|
||||
|
||||
async with self.session.get(request_url, headers=self._bearer_headers) as resp:
|
||||
if resp.status != 200:
|
||||
raise SpotifyRequestException(
|
||||
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
data: dict = await resp.json()
|
||||
|
||||
if spotify_type == "track":
|
||||
return Track(data)
|
||||
elif spotify_type == "album":
|
||||
return Album(data)
|
||||
elif isArtist:
|
||||
return Artist(data)
|
||||
else:
|
||||
|
||||
tracks = [
|
||||
Track(track["track"])
|
||||
for track in data["tracks"]["items"] if track["track"] is not None
|
||||
]
|
||||
|
||||
if not tracks:
|
||||
raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.")
|
||||
|
||||
next_page_url = data["tracks"]["next"]
|
||||
|
||||
while next_page_url is not None:
|
||||
async with self.session.get(next_page_url, headers=self._bearer_headers) as resp:
|
||||
if resp.status != 200:
|
||||
raise SpotifyRequestException(
|
||||
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
next_data: dict = await resp.json()
|
||||
|
||||
tracks += [
|
||||
Track(track["track"])
|
||||
for track in next_data["items"] if track["track"] is not None
|
||||
]
|
||||
next_page_url = next_data["next"]
|
||||
|
||||
return Playlist(data, tracks)
|
||||
|
||||
async def close(self):
|
||||
await self.session.close()
|
||||
8
voicelink/spotify/exceptions.py
Normal file
8
voicelink/spotify/exceptions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
class SpotifyRequestException(Exception):
|
||||
"""An error occurred when making a request to the Spotify API"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSpotifyURL(Exception):
|
||||
"""An invalid Spotify URL was passed"""
|
||||
pass
|
||||
24
voicelink/spotify/playlist.py
Normal file
24
voicelink/spotify/playlist.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from .track import Track
|
||||
from typing import List
|
||||
|
||||
|
||||
class Playlist:
|
||||
"""The base class for a Spotify playlist"""
|
||||
|
||||
def __init__(self, data: dict, tracks: List[Track]) -> None:
|
||||
self.name = data.get('name', 'Unknown')
|
||||
self.tracks = tracks
|
||||
self.owner = data["owner"]["display_name"]
|
||||
self.total_tracks = data["tracks"]["total"]
|
||||
self.id = data.get('id')
|
||||
if data.get("images") and len(data["images"]):
|
||||
self.image = data["images"][0]["url"]
|
||||
else:
|
||||
self.image = None
|
||||
self.uri = data["external_urls"]["spotify"]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.spotify.Playlist name={self.name} owner={self.owner} id={self.id} "
|
||||
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||
)
|
||||
25
voicelink/spotify/track.py
Normal file
25
voicelink/spotify/track.py
Normal file
@@ -0,0 +1,25 @@
|
||||
class Track:
|
||||
"""The base class for a Spotify Track"""
|
||||
|
||||
def __init__(self, data: dict, image = None) -> None:
|
||||
self.name = data.get('name', 'Unknown')
|
||||
self.artists = ", ".join(artist["name"] for artist in data.get('artists'))
|
||||
self.artistId = [ artist['id'] for artist in data.get('artists') ]
|
||||
self.length = data.get('duration_ms')
|
||||
self.id = data.get('id')
|
||||
|
||||
if data.get("album") and data["album"].get("images"):
|
||||
self.image = data["album"]["images"][0]["url"]
|
||||
else:
|
||||
self.image = image
|
||||
|
||||
if data["is_local"]:
|
||||
self.uri = None
|
||||
else:
|
||||
self.uri = data["external_urls"]["spotify"]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.spotify.Track name={self.name} artists={self.artists} "
|
||||
f"length={self.length} id={self.id}>"
|
||||
)
|
||||
171
voicelink/utils.py
Normal file
171
voicelink/utils.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
import socket
|
||||
from timeit import default_timer as timer
|
||||
from itertools import zip_longest
|
||||
|
||||
__all__ = [
|
||||
"ExponentialBackoff",
|
||||
"NodeStats"
|
||||
]
|
||||
|
||||
class ExponentialBackoff:
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, base: int = 1, *, integral: bool = False) -> None:
|
||||
|
||||
self._base = base
|
||||
|
||||
self._exp = 0
|
||||
self._max = 10
|
||||
self._reset_time = base * 2 ** 11
|
||||
self._last_invocation = time.monotonic()
|
||||
|
||||
rand = random.Random()
|
||||
rand.seed()
|
||||
|
||||
self._randfunc = rand.randrange if integral else rand.uniform
|
||||
|
||||
def delay(self) -> float:
|
||||
|
||||
invocation = time.monotonic()
|
||||
interval = invocation - self._last_invocation
|
||||
self._last_invocation = invocation
|
||||
|
||||
if interval > self._reset_time:
|
||||
self._exp = 0
|
||||
|
||||
self._exp = min(self._exp + 1, self._max)
|
||||
return self._randfunc(0, self._base * 2 ** self._exp)
|
||||
|
||||
|
||||
class NodeStats:
|
||||
"""The base class for the node stats object.
|
||||
Gives critical information on the node, which is updated every minute.
|
||||
"""
|
||||
|
||||
def __init__(self, data: dict) -> None:
|
||||
|
||||
memory: dict = data.get("memory")
|
||||
self.used = memory.get("used")
|
||||
self.free = memory.get("free")
|
||||
self.reservable = memory.get("reservable")
|
||||
self.allocated = memory.get("allocated")
|
||||
|
||||
cpu: dict = data.get("cpu")
|
||||
self.cpu_cores = cpu.get("cores")
|
||||
self.cpu_system_load = cpu.get("systemLoad")
|
||||
self.cpu_process_load = cpu.get("lavalinkLoad")
|
||||
|
||||
self.players_active = data.get("playingPlayers")
|
||||
self.players_total = data.get("players")
|
||||
self.uptime = data.get("uptime")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.NodeStats total_players={self.players_total!r} playing_active={self.players_active!r}>"
|
||||
|
||||
|
||||
class Ping:
|
||||
# Thanks to https://github.com/zhengxiaowai/tcping for the nice ping impl
|
||||
def __init__(self, host, port, timeout=5):
|
||||
self.timer = self.Timer()
|
||||
|
||||
self._successed = 0
|
||||
self._failed = 0
|
||||
self._conn_time = None
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._timeout = timeout
|
||||
|
||||
class Socket(object):
|
||||
def __init__(self, family, type_, timeout):
|
||||
s = socket.socket(family, type_)
|
||||
s.settimeout(timeout)
|
||||
self._s = s
|
||||
|
||||
def connect(self, host, port):
|
||||
self._s.connect((host, int(port)))
|
||||
|
||||
def shutdown(self):
|
||||
self._s.shutdown(socket.SHUT_RD)
|
||||
|
||||
def close(self):
|
||||
self._s.close()
|
||||
|
||||
|
||||
class Timer(object):
|
||||
def __init__(self):
|
||||
self._start = 0
|
||||
self._stop = 0
|
||||
|
||||
def start(self):
|
||||
self._start = timer()
|
||||
|
||||
def stop(self):
|
||||
self._stop = timer()
|
||||
|
||||
def cost(self, funcs, args):
|
||||
self.start()
|
||||
for func, arg in zip_longest(funcs, args):
|
||||
if arg:
|
||||
func(*arg)
|
||||
else:
|
||||
func()
|
||||
|
||||
self.stop()
|
||||
return self._stop - self._start
|
||||
|
||||
def _create_socket(self, family, type_):
|
||||
return self.Socket(family, type_, self._timeout)
|
||||
|
||||
def get_ping(self):
|
||||
s = self._create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
cost_time = self.timer.cost(
|
||||
(s.connect, s.shutdown),
|
||||
((self._host, self._port), None))
|
||||
s_runtime = 1000 * (cost_time)
|
||||
|
||||
return s_runtime
|
||||
|
||||
Reference in New Issue
Block a user