Ready for public use

This commit is contained in:
Choco
2023-02-02 21:06:51 +08:00
parent a109d2338b
commit 0d1ddb399b
50 changed files with 7686 additions and 151 deletions

11
.env Example Normal file
View 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
View File

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

@@ -0,0 +1,83 @@
![Support Server](https://discordapp.com/api/guilds/811542332678996008/widget.png?style=shield)
# 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
View File

@@ -0,0 +1 @@
from .lyrics import getLyrics

161
addons/lyrics.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View 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}>"
)

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

View 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

View 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}>"
)

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