diff --git a/.env Example b/.env Example
new file mode 100644
index 0000000..5bd0292
--- /dev/null
+++ b/.env Example
@@ -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
diff --git a/.gitignore b/.gitignore
index d9005f2..8c2cfa7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4da960f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,83 @@
+
+
+# Vocard (Discord Music Bot)
+> Vocard is a simple custom Disocrd Music Bot built with Python & [discord.py](https://discordpy.readthedocs.io/en/stable/)
+
+## Requirements
+* [Python 3.8+](https://www.python.org/downloads/)
+* [Modules in requirements](https://github.com/ChocoMeow/Vocard/blob/main/requirements.txt)
+* [Lavalink Server](https://github.com/freyacodes/Lavalink)
+
+## Quick Start
+```sh
+git clone https://github.com/ChocoMeow/Vocard.git #Clone the repository
+cd Vocard #Go to the directory
+python -m pip install -r requirements.txt #Install required packages
+```
+After installing all packages, you must configure the bot before to start! [How To Configure](https://github.com/ChocoMeow/Vocard#configuration)
+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
+***Note: Make sure there are no personal files in the directory! Otherwise it will be deleted.***
diff --git a/addons/__init__.py b/addons/__init__.py
new file mode 100644
index 0000000..088e3ad
--- /dev/null
+++ b/addons/__init__.py
@@ -0,0 +1 @@
+from .lyrics import getLyrics
\ No newline at end of file
diff --git a/addons/lyrics.py b/addons/lyrics.py
new file mode 100644
index 0000000..206443c
--- /dev/null
+++ b/addons/lyrics.py
@@ -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
\ No newline at end of file
diff --git a/cogs/admin.py b/cogs/admin.py
new file mode 100644
index 0000000..c849922
--- /dev/null
+++ b/cogs/admin.py
@@ -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(
+ '' if perms.administrator else '',
+ '' if perms.manage_guild else '',
+ '' if perms.manage_channels else '',
+ '' if perms.manage_messages else ''), 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))
\ No newline at end of file
diff --git a/cogs/basic.py b/cogs/basic.py
new file mode 100644
index 0000000..1ab6839
--- /dev/null
+++ b/cogs/basic.py
@@ -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\d+:\d+|\d+:\d+:\d+) (?P.+)", 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))
\ No newline at end of file
diff --git a/cogs/effect.py b/cogs/effect.py
new file mode 100644
index 0000000..e4dd7a3
--- /dev/null
+++ b/cogs/effect.py
@@ -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 )"
+
+ 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))
\ No newline at end of file
diff --git a/cogs/nodes.py b/cogs/nodes.py
new file mode 100644
index 0000000..eec3234
--- /dev/null
+++ b/cogs/nodes.py
@@ -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))
diff --git a/cogs/playlist.py b/cogs/playlist.py
new file mode 100644
index 0000000..77b5454
--- /dev/null
+++ b/cogs/playlist.py
@@ -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))
\ No newline at end of file
diff --git a/cogs/task.py b/cogs/task.py
new file mode 100644
index 0000000..5261831
--- /dev/null
+++ b/cogs/task.py
@@ -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: ", 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))
diff --git a/function.py b/function.py
new file mode 100644
index 0000000..f002694
--- /dev/null
+++ b/function.py
@@ -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(), "🔗");
+
diff --git a/langs/CH.json b/langs/CH.json
new file mode 100644
index 0000000..c03009a
--- /dev/null
+++ b/langs/CH.json
@@ -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 將其刪除。",
+
+ "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": "抱歉,此曲目已在隊列中。"
+}
\ No newline at end of file
diff --git a/langs/DE.json b/langs/DE.json
new file mode 100644
index 0000000..3be41b2
--- /dev/null
+++ b/langs/DE.json
@@ -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 , 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 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."
+}
\ No newline at end of file
diff --git a/langs/EN.json b/langs/EN.json
new file mode 100644
index 0000000..3ccef51
--- /dev/null
+++ b/langs/EN.json
@@ -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 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 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."
+}
\ No newline at end of file
diff --git a/langs/JP.json b/langs/JP.json
new file mode 100644
index 0000000..76433cb
--- /dev/null
+++ b/langs/JP.json
@@ -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 を使用して削除してください。",
+
+ "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": "申し訳ありませんが、このトラックは既にキューに入っています。"
+}
\ No newline at end of file
diff --git a/langs/KO.json b/langs/KO.json
new file mode 100644
index 0000000..d8eccc5
--- /dev/null
+++ b/langs/KO.json
@@ -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": "죄송합니다. 이 트랙은 이미 대기열에 있습니다."
+}
\ No newline at end of file
diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json
new file mode 100644
index 0000000..4e24035
--- /dev/null
+++ b/local_langs/zh-TW.json
@@ -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": "設置"
+}
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..994ddd0
--- /dev/null
+++ b/main.py
@@ -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)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..04ddf2b
--- /dev/null
+++ b/requirements.txt
@@ -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
diff --git a/settings Example.json b/settings Example.json
new file mode 100644
index 0000000..110bd8e
--- /dev/null
+++ b/settings Example.json
@@ -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>"
+ }
+}
\ No newline at end of file
diff --git a/update.py b/update.py
new file mode 100644
index 0000000..9465da4
--- /dev/null
+++ b/update.py
@@ -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)
\ No newline at end of file
diff --git a/view/__init__.py b/view/__init__.py
new file mode 100644
index 0000000..6a1c0b8
--- /dev/null
+++ b/view/__init__.py
@@ -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
diff --git a/view/chapter.py b/view/chapter.py
new file mode 100644
index 0000000..93b2caa
--- /dev/null
+++ b/view/chapter.py
@@ -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
\ No newline at end of file
diff --git a/view/controller.py b/view/controller.py
new file mode 100644
index 0000000..ce524b5
--- /dev/null
+++ b/view/controller.py
@@ -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
\ No newline at end of file
diff --git a/view/debug.py b/view/debug.py
new file mode 100644
index 0000000..a1348c3
--- /dev/null
+++ b/view/debug.py
@@ -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()
\ No newline at end of file
diff --git a/view/help.py b/view/help.py
new file mode 100644
index 0000000..c2aa221
--- /dev/null
+++ b/view/help.py
@@ -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 ()", 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
\ No newline at end of file
diff --git a/view/inbox.py b/view/inbox.py
new file mode 100644
index 0000000..9697027
--- /dev/null
+++ b/view/inbox.py
@@ -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()
\ No newline at end of file
diff --git a/view/link.py b/view/link.py
new file mode 100644
index 0000000..9f6531d
--- /dev/null
+++ b/view/link.py
@@ -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))
\ No newline at end of file
diff --git a/view/list.py b/view/list.py
new file mode 100644
index 0000000..4210f7a
--- /dev/null
+++ b/view/list.py
@@ -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()
\ No newline at end of file
diff --git a/view/lyrics.py b/view/lyrics.py
new file mode 100644
index 0000000..2ba9338
--- /dev/null
+++ b/view/lyrics.py
@@ -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()
\ No newline at end of file
diff --git a/view/playlist.py b/view/playlist.py
new file mode 100644
index 0000000..71b1a9f
--- /dev/null
+++ b/view/playlist.py
@@ -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
\ No newline at end of file
diff --git a/view/search.py b/view/search.py
new file mode 100644
index 0000000..a8c9141
--- /dev/null
+++ b/view/search.py
@@ -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
\ No newline at end of file
diff --git a/voicelink/__init__.py b/voicelink/__init__.py
new file mode 100644
index 0000000..4cfe1d6
--- /dev/null
+++ b/voicelink/__init__.py
@@ -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 *
diff --git a/voicelink/enums.py b/voicelink/enums.py
new file mode 100644
index 0000000..8d16f4c
--- /dev/null
+++ b/voicelink/enums.py
@@ -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
\ No newline at end of file
diff --git a/voicelink/events.py b/voicelink/events.py
new file mode 100644
index 0000000..cb9b83c
--- /dev/null
+++ b/voicelink/events.py
@@ -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""
+
+
+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""
+ )
+
+
+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""
+
+
+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""
+
+
+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""
+
+
+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""
+
+
+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""
+
diff --git a/voicelink/exceptions.py b/voicelink/exceptions.py
new file mode 100644
index 0000000..b38a0ac
--- /dev/null
+++ b/voicelink/exceptions.py
@@ -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
\ No newline at end of file
diff --git a/voicelink/filters.py b/voicelink/filters.py
new file mode 100644
index 0000000..75829d0
--- /dev/null
+++ b/voicelink/filters.py
@@ -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""
+
+ @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""
+
+
+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""
+ )
+
+
+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""
+
+
+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""
+
+
+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""
+
+
+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""
+ )
+
+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" "
+ 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""
+
+
diff --git a/voicelink/objects.py b/voicelink/objects.py
new file mode 100644
index 0000000..12aada4
--- /dev/null
+++ b/voicelink/objects.py
@@ -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" 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""
+
+ @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
diff --git a/voicelink/player.py b/voicelink/player.py
new file mode 100644
index 0000000..b77caaa
--- /dev/null
+++ b/voicelink/player.py
@@ -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""
+ )
+
+ @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)
\ No newline at end of file
diff --git a/voicelink/pool.py b/voicelink/pool.py
new file mode 100644
index 0000000..d4a2b91
--- /dev/null
+++ b/voicelink/pool.py
@@ -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/(?Palbum|playlist|track|artist)/(?P[a-zA-Z0-9]+)"
+)
+
+DISCORD_MP3_URL_REGEX = re.compile(
+ r"https?://cdn.discordapp.com/attachments/(?P[0-9]+)/"
+ r"(?P[0-9]+)/(?P[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""
+ )
+
+ @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""
+
+ @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
\ No newline at end of file
diff --git a/voicelink/queue.py b/voicelink/queue.py
new file mode 100644
index 0000000..222265d
--- /dev/null
+++ b/voicelink/queue.py
@@ -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
\ No newline at end of file
diff --git a/voicelink/spotify/__init__.py b/voicelink/spotify/__init__.py
new file mode 100644
index 0000000..48611a5
--- /dev/null
+++ b/voicelink/spotify/__init__.py
@@ -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
diff --git a/voicelink/spotify/album.py b/voicelink/spotify/album.py
new file mode 100644
index 0000000..bde4a95
--- /dev/null
+++ b/voicelink/spotify/album.py
@@ -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""
+ )
diff --git a/voicelink/spotify/artist.py b/voicelink/spotify/artist.py
new file mode 100644
index 0000000..1a3dffc
--- /dev/null
+++ b/voicelink/spotify/artist.py
@@ -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""
+ )
diff --git a/voicelink/spotify/client.py b/voicelink/spotify/client.py
new file mode 100644
index 0000000..9acf771
--- /dev/null
+++ b/voicelink/spotify/client.py
@@ -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/(?Palbum|playlist|track|artist)/(?P[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()
diff --git a/voicelink/spotify/exceptions.py b/voicelink/spotify/exceptions.py
new file mode 100644
index 0000000..e421fbf
--- /dev/null
+++ b/voicelink/spotify/exceptions.py
@@ -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
diff --git a/voicelink/spotify/playlist.py b/voicelink/spotify/playlist.py
new file mode 100644
index 0000000..d82236b
--- /dev/null
+++ b/voicelink/spotify/playlist.py
@@ -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""
+ )
diff --git a/voicelink/spotify/track.py b/voicelink/spotify/track.py
new file mode 100644
index 0000000..325f920
--- /dev/null
+++ b/voicelink/spotify/track.py
@@ -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""
+ )
diff --git a/voicelink/utils.py b/voicelink/utils.py
new file mode 100644
index 0000000..0a692a0
--- /dev/null
+++ b/voicelink/utils.py
@@ -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""
+
+
+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
+