Vocard v2.6.9 Update: A lot new features, bug fixes, and code clean-up (#34)
* Update docker-compose.yml No dotenv file needed. All environment values are now defined in docker-compose * Update RU.json * Updated docker-compose * Update README.md * Resolved issue where playback didn’t start after user joined channel 247 * Added logging system * Updated logging system * Update player.py * Added 24/7 required intent * Update main.py * Added logs folder * Updated logging system * Updated logging system * Added some typehint * Removed error_log * Renamed a valuable * Removed unnecessary value * Fixed some bugs * Added bot status settings * Added autocomplete in playtop * Added a new emoji in Loop button * Optimized code * Update requirements.txt * Fixed autoplay with empty queue This commit addresses the issue where autoplay was being activated even when the history queue had no tracks. The problem has been resolved to ensure smooth user experience * Enhanced Loop Control Button Functionality * Updated the loop button in controller * Fixed activity update Resolved the issue where activity updates were not reflected when the value remained the same. * Added sync function in debug view * Optimized code * Added version in the debug * Added nodes panel in debug view * Merged .env into settings.json * Rewritten ipc_client * Separated the dashboard from the project * Removed some necessary requirements * Remove support for replit * Update README.md * Update settings Example.json * Added get_recommendations into node class * Saved log file when updating * Rewritten some method in ipc_client * Written swap and remove method in queue * Added remove_track and swap_track method in player * Update methods.py * Added secure protocol in ipc client * Fixed some bugs * Added some method in ipc cilent * Update methods.py * Update methods.py * Added playlist method * Optimized Spotify client code * Added Category class in Spotify client * Added settings method in ipc client * Fixed some bugs * Send track requester_id in initPlayer * Fixed some bugs * Fixed some bugs * Optimized some code * Dump discord.py * Added start and end position for each track * Update methods.py * Fixed a bug * Fixed some bugs * Fixed IPC client creation issue * Rewrote filters * Fixed some bugs * Added filter event in ipc method * Added filter selector in music controller * Added inbox method * Optimized some code * Updated zh-TW local_lang * Fixed some bugs * Delete supervisord.conf * Fixed some bugs * Updated wording in IPC client * Update pool.py * Fixed no song suggestions when spotify_client is None * Fixed connection problem from the dashboard * Fixed some bugs * Added voice status (#33) * Updated the placeholder name for clarity * Added voice status * Dump bot version * Added global template for voice status * Added new placeholder * Updated settings view * Updated version tag * Improved codes * Fixed some bugs * Supported clear queue endpoint in ipc * Update methods.py * Update README.md * Remove PLACEHOLDERS.MD --------- Co-authored-by: Azarath7 <158289825+Azarath7@users.noreply.github.com>
This commit is contained in:
18
.env Example
18
.env Example
@@ -1,18 +0,0 @@
|
||||
TOKEN = YOUR_BOT_TOKEN
|
||||
CLIENT_ID = YOUR_BOT_CLIENT_ID
|
||||
CLIENT_SECRET_ID = YOUR_BOT_CLIENT_SECRET_ID
|
||||
|
||||
BUG_REPORT_CHANNEL_ID = YOUR_DISCORD_CHANNEL_ID
|
||||
|
||||
SPOTIFY_CLIENT_ID = YOUR_SPOTIFY_CLIENT_ID
|
||||
SPOTIFY_CLIENT_SECRET = YOUR_SPOTIFY_CLIENT_SECRET
|
||||
|
||||
GENIUS_TOKEN = YOUR_GENIUS_TOKEN
|
||||
|
||||
MONGODB_URL = YOUR_MONGODB_URL
|
||||
MONGODB_NAME = YOUR_MONGODB_DB_NAME
|
||||
|
||||
DASHBOARD_HOST = 0.0.0.0
|
||||
DASHBOARD_PORT = 5555
|
||||
DASHBOARD_SECRET_KEY = DASHBOARD_SECRET_KEY
|
||||
DASHBOARD_SECURE = False
|
||||
179
PLACEHOLDERS.md
179
PLACEHOLDERS.md
@@ -1,179 +0,0 @@
|
||||
# Bot Activity (Activity are updated every 10 minutes)
|
||||
> Usage: {"action": "@@value@@"}
|
||||
|
||||
Valuables
|
||||
|
||||
| Value Name | Description |
|
||||
| --- | --- |
|
||||
| guilds | Number of guilds in the bot |
|
||||
| users | Number of users in the voice channel. |
|
||||
| players | Number of players playing. |
|
||||
| nodes | Number of nodes are connected. |
|
||||
|
||||
Actions
|
||||
|
||||
| Action Name | Description |
|
||||
| --- | --- |
|
||||
| play | Playing status. |
|
||||
| listen | Listening status. |
|
||||
| watch | Watching status. |
|
||||
| stream | Streaming status. |
|
||||
|
||||
## Examples
|
||||
Change the value of `activity` in `settings.json`.
|
||||
|
||||
1. Example 1
|
||||
<img width="200" alt="image" src="https://user-images.githubusercontent.com/94597336/232210610-fb2b8dba-736e-4230-b315-c52b1cf37a22.png">
|
||||
|
||||
```json
|
||||
"activity":[
|
||||
{"listen": "/help"}
|
||||
]
|
||||
```
|
||||
|
||||
2. Example 2
|
||||
<img width="200" alt="image" src="https://user-images.githubusercontent.com/94597336/232210639-255025e2-f55b-422d-a8f8-01cdaeec1e8d.png">
|
||||
|
||||
```json
|
||||
"activity":[
|
||||
{"watch": "@@players@@ players"}
|
||||
]
|
||||
```
|
||||
|
||||
3. Example 3 (Multiple activities)
|
||||
|
||||
```json
|
||||
"activity":[
|
||||
{"listen": "/help"},
|
||||
{"watch": "@@players@@ players"}
|
||||
]
|
||||
```
|
||||
|
||||
# Controller Embeds
|
||||
> Usage: @@value@@
|
||||
|
||||
Valuables
|
||||
|
||||
| Value Name | Description |
|
||||
| --- | --- |
|
||||
| channel_name | The name of the channel the bot is playing on. |
|
||||
| track_name | The name of the track currently playing. |
|
||||
| track_url | The url of the track currently playing. |
|
||||
| track_author | The name of the author of the track currently playing. |
|
||||
| track_duration | The length of the track currently playing. |
|
||||
| track_thumbnail | The thumbnail of the track currently playing. |
|
||||
| track_color | The color associated with the source of the currently playing track. For example, YouTube tracks are indicated in red. This color can be customized in the settings.json file |
|
||||
| requester | The mention name of the requester of the currently playing track. |
|
||||
| requester_name | The name of the requester of the currently playing track. |
|
||||
| requester_avatar | The avatar url of the requester of the currently playing track. |
|
||||
| queue_length | Number of queue lengths |
|
||||
| volume | Music volume. |
|
||||
| dj | DJ role. `(It can be a user or role)` |
|
||||
| loop_mode | Current repeat mode. |
|
||||
| default_embed_color | Default embed color. `(color_code in settings.json)` |
|
||||
| bot_icon | The avatar of the bot. |
|
||||
| server_invite_link | The invite url of the support server |
|
||||
| invite_link | The invite url of the bot. |
|
||||
|
||||
Expression
|
||||
> {{ EXPRESSION ?? TRUE // FALSE }}
|
||||
|
||||
## Examples
|
||||
Change the value of `default_controller` in `settings.json`.
|
||||
|
||||
1. Example 1
|
||||
<img width="450" alt="image" src="https://user-images.githubusercontent.com/94597336/232209745-e55de4e5-e4f2-47ce-ab78-69e9263f321a.png">
|
||||
<img width="450" alt="image" src="https://user-images.githubusercontent.com/94597336/232209778-e30f2f92-a179-4eb6-90d9-865cb1903699.png">
|
||||
|
||||
```json
|
||||
"default_controller": {
|
||||
"embeds": {
|
||||
"active": {
|
||||
"description": "**Now Playing: ```[@@track_name@@]```\nLink: [Click Me](@@track_url@@) | Requester: @@requester@@ | DJ: @@dj@@**",
|
||||
"footer": {
|
||||
"text": "Queue Length: @@queue_length@@ | Duration: @@track_duration@@ | Volume: @@volume@@% {{loop_mode != 'Off' ?? | Repeat: @@loop_mode@@}}"
|
||||
},
|
||||
"image": "@@track_thumbnail@@",
|
||||
"author": {
|
||||
"name": "Music Controller | @@channel_name@@",
|
||||
"icon_url": "@@bot_icon@@"
|
||||
},
|
||||
"color": "@@track_color@@"
|
||||
},
|
||||
"inactive": {
|
||||
"title": {
|
||||
"name": "There are no songs playing right now"
|
||||
},
|
||||
"description": "[Support](@@server_invite_link@@) | [Invite](@@invite_link@@) | [Questionnaire](https://forms.gle/Qm8vjBfg2kp13YGD7)",
|
||||
"image": "https://i.imgur.com/dIFBwU7.png",
|
||||
"color": "@@default_embed_color@@"
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
# Controller Buttons
|
||||
> Usage: "button" or {"button": "color"}
|
||||
|
||||
Buttons
|
||||
|
||||
| Button Name | Description |
|
||||
| --- | --- |
|
||||
| Back | Skips back to the previous song. |
|
||||
| Resume | Resume or pause the music. |
|
||||
| Skip | Skips to the next song. |
|
||||
| Stop | Disconnects the bot from your voice channel and chears the queue. |
|
||||
| Loop | Changes Loop mode. `[Off, Track, Queue]` |
|
||||
| Add | Add the playing track in to your default custom playlist. |
|
||||
| VolumeUp | Increase player volume by 20%. |
|
||||
| VolumeDown | Decrease player volume by 20%. |
|
||||
| VolumeMute | Mute or unmute the player. |
|
||||
| Autoplay | Enable or disable autoplay mode. |
|
||||
| Shuffle | Randomizes the tracks in the queue. |
|
||||
| Forward | Forward 30 seconds in the current track. |
|
||||
| Rewind | Rewind 30 seconds in the current track. |
|
||||
| Tracks | If there are tracks in the queue, a drop-down list will be appear. Up to 10 tracks. `(This will take one row)`|
|
||||
|
||||
Colors
|
||||
|
||||
| Color Name | Description |
|
||||
| --- | --- |
|
||||
| Grey | Color the button grey. |
|
||||
| Red | Color the button red. |
|
||||
| Blue | Color the button blue. |
|
||||
| Green | Color the button green. |
|
||||
|
||||
## Examples
|
||||
Change the value of `default_buttons` in `default_controller`.
|
||||
|
||||
1. Example 1
|
||||
<img width="459" alt="image" src="https://user-images.githubusercontent.com/94597336/221099779-d458e274-6052-4265-afb2-b232de1b1fd4.png">
|
||||
|
||||
```json
|
||||
"default_buttons": [
|
||||
["back", "resume", "skip", {"stop": "red"}, "add"],
|
||||
["tracks"]
|
||||
]
|
||||
```
|
||||
|
||||
2. Example 2
|
||||
<img width="480" alt="image" src="https://user-images.githubusercontent.com/94597336/221099004-9913ee28-5079-488a-b880-902c7ab7ce38.png">
|
||||
|
||||
```json
|
||||
"default_buttons": [
|
||||
["back", "resume", "skip", {"stop": "red"}, {"add": "green"}],
|
||||
[{"loop": "green"}, {"volumeup": "blue"}, {"volumedown": "blue"}, {"volumemute": "red"}],
|
||||
["tracks"]
|
||||
]
|
||||
```
|
||||
|
||||
3. Example 3
|
||||
<img width="480" alt="image" src="https://user-images.githubusercontent.com/94597336/230248758-14ff7e1b-d6db-49f8-94a6-c55fd02f13df.png">
|
||||
|
||||
```json
|
||||
"default_buttons": [
|
||||
["autoplay", "shuffle", {"loop": "green"}, "add"],
|
||||
["back", "resume", "skip", {"stop": "red"}],
|
||||
["volumeup", "volumedown", {"mute": "red"}]
|
||||
]
|
||||
```
|
||||
73
README.md
73
README.md
@@ -2,62 +2,35 @@
|
||||
<img src="https://img.shields.io/discord/811542332678996008?color=7289DA&label=Support&logo=discord&style=for-the-badge" alt="Discord">
|
||||
</a>
|
||||
|
||||
# Vocard (Discord Music Bot)
|
||||
> Vocard is a simple custom Disocrd Music Bot built with Python & [discord.py](https://discordpy.readthedocs.io/en/stable/) <br>
|
||||
Demo: [Discord Bot Demo](https://discord.com/api/oauth2/authorize?client_id=890399639008866355&permissions=36708608&scope=bot%20applications.commands),
|
||||
[Dashboard Demo](https://vocard.xyz)
|
||||
# Vocard Bot
|
||||
Vocard is a highly customizable Discord music bot, designed to deliver a user-friendly experience. It offers support for a wide range of streaming platforms including Youtube, Soundcloud, Spotify, Twitch, and more.
|
||||
|
||||
# Host for you?
|
||||
<a href="https://www.patreon.com/Vocard">
|
||||
<img src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dendel%26type%3Dpatrons&style=for-the-badge" alt="Patreon">
|
||||
</a>
|
||||
## Features
|
||||
* Fast song loading
|
||||
* Works with slash and message commands
|
||||
* Lightweight design
|
||||
* Smooth playback
|
||||
* Clean and nice interface
|
||||
* Supports many music platforms (YouTube, SoundCloud, etc.)
|
||||
* Built-in playlist support
|
||||
* Fully customizable settings
|
||||
* Lyrics support
|
||||
* Various sound effects
|
||||
* Multiple languages available
|
||||
* Easy to update
|
||||
* Supports docker
|
||||
* Premium dashboard (in beta)
|
||||
|
||||
# Tutorial
|
||||
Click on the image below to watch the tutorial on Youtube.
|
||||
|
||||
[](https://www.youtube.com/watch?v=f_Z0RLRZzWw)
|
||||
|
||||
## Screenshot
|
||||
### Discord Bot
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766331-dfa7d360-18d7-4014-ac6a-4fca55907d99.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766379-c824512e-a6f7-4ca5-9342-cc8e78d52491.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766408-37d733f7-c849-4cbd-9e17-0cd5800affb3.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766416-22ae3d91-40d9-44c0-bde1-9d40bd54c3af.png">
|
||||
|
||||
### Dashboard
|
||||
<img src="https://github.com/ChocoMeow/Vocard/assets/94597336/53f31f9f-57c5-452c-8317-114125ddbf03">
|
||||
<img src="https://github.com/ChocoMeow/Vocard/assets/94597336/b2acd87a-e910-4247-8d5a-418f3782f63f">
|
||||

|
||||
|
||||
## Requirements
|
||||
* [Python 3.10+](https://www.python.org/downloads/)
|
||||
* [Python 3.11+](https://www.python.org/downloads/)
|
||||
* [Lavalink Server (Requires 4.0.0+)](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://docs.vocard.xyz/configuration/)<br />
|
||||
Start your bot with `python main.py`
|
||||
## Setup
|
||||
Please see the [Setup Page](https://docs.vocard.xyz) in the docs to run this bot yourself!
|
||||
|
||||
## Configuration
|
||||
1. Rename `.env Example` to `.env` and [customize your env](https://docs.vocard.xyz/configuration/#settings-up-env).
|
||||
2. Rename `settings Example.json` to `settings.json` and [customize your settings](https://docs.vocard.xyz/configuration/#settings-up-settingsjson).
|
||||
3. Now, you can start your bot with `python main.py`.
|
||||
## Need Help?
|
||||
Join the [Vocard Support Discord](https://discord.gg/wRCgB7vBQv) for help or questions.
|
||||
|
||||
## How to update? (For Windows and Linux)
|
||||
***Note: Make sure there are no personal files in the directory! Otherwise it will be deleted.***
|
||||
```sh
|
||||
# Check the current version
|
||||
python update.py -c
|
||||
|
||||
# Install the latest version
|
||||
python update.py -l
|
||||
|
||||
# Install the specified version
|
||||
python update.py -v VERSION
|
||||
|
||||
# Install the beta version
|
||||
python update.py -b
|
||||
```
|
||||
@@ -1,3 +1,3 @@
|
||||
from .lyrics import lyricsPlatform
|
||||
from .placeholders import Placeholders
|
||||
from .settings import Settings, TOKENS
|
||||
from .settings import Settings
|
||||
@@ -85,7 +85,7 @@ class A_ZLyrics(LyricsPlatform):
|
||||
lyrics = max(divs, key=len).strip()
|
||||
|
||||
if not lyrics:
|
||||
return print("Lyrics not found")
|
||||
return None
|
||||
|
||||
lyrics_parts = re.split(r"(\[[\w\S_ ]+\:])", lyrics)
|
||||
lyrics_parts = [item for item in lyrics_parts if item != ""]
|
||||
@@ -173,7 +173,7 @@ class A_ZLyrics(LyricsPlatform):
|
||||
class Genius(LyricsPlatform):
|
||||
def __init__(self) -> None:
|
||||
self.module = import_module("lyricsgenius")
|
||||
self.genius = self.module.Genius(func.tokens.genius_token)
|
||||
self.genius = self.module.Genius(func.settings.genius_token)
|
||||
|
||||
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
|
||||
song = self.genius.search_song(title=title, artist=artist)
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from typing import (
|
||||
Dict,
|
||||
List,
|
||||
Any,
|
||||
Union
|
||||
)
|
||||
|
||||
class Settings:
|
||||
def __init__(self, settings: dict) -> None:
|
||||
def __init__(self, settings: Dict) -> None:
|
||||
self.token: str = settings.get("token")
|
||||
self.client_id: int = int(settings.get("client_id", 0))
|
||||
self.spotify_client_id: str = settings.get("spotify_client_id")
|
||||
self.spotify_client_secret: str = settings.get("spotify_client_secret")
|
||||
self.genius_token: str = settings.get("genius_token")
|
||||
self.mongodb_url: str = settings.get("mongodb_url")
|
||||
self.mongodb_name: str = settings.get("mongodb_name")
|
||||
|
||||
self.invite_link: str = "https://discord.gg/wRCgB7vBQv"
|
||||
self.nodes: dict = settings.get("nodes", {})
|
||||
self.nodes: Dict[str, Dict[str, Union[str, int, bool]]] = settings.get("nodes", {})
|
||||
self.max_queue: int = settings.get("default_max_queue", 1000)
|
||||
self.bot_prefix: str = settings.get("prefix", "")
|
||||
self.activity: dict = settings.get("activity", [{"listen": "/help"}])
|
||||
self.activity: List[Dict[str, str]] = settings.get("activity", [{"listen": "/help"}])
|
||||
self.logging: Dict[Union[str, Dict[str, Union[str, bool]]]] = settings.get("logging", {})
|
||||
self.embed_color: str = int(settings.get("embed_color", "0xb3b3b3"), 16)
|
||||
self.bot_access_user: list = settings.get("bot_access_user", [])
|
||||
self.sources_settings: dict = settings.get("sources_settings", {})
|
||||
self.cooldowns_settings: dict = settings.get("cooldowns", {})
|
||||
self.aliases_settings: dict = settings.get("aliases", {})
|
||||
self.controller: dict = settings.get("default_controller", {})
|
||||
self.bot_access_user: List[int] = settings.get("bot_access_user", [])
|
||||
self.sources_settings: Dict[Dict[str, str]] = settings.get("sources_settings", {})
|
||||
self.cooldowns_settings: Dict[str, List[int]] = settings.get("cooldowns", {})
|
||||
self.aliases_settings: Dict[str, List[str]] = settings.get("aliases", {})
|
||||
self.controller: Dict[str, Dict[str, Any]] = settings.get("default_controller", {})
|
||||
self.voice_status_template: str = settings.get("default_voice_status_template", "")
|
||||
self.lyrics_platform: str = settings.get("lyrics_platform", "A_ZLyrics").lower()
|
||||
self.ipc_server: dict = settings.get("ipc_server", {})
|
||||
self.version: str = settings.get("version", "")
|
||||
|
||||
class TOKENS:
|
||||
def __init__(self) -> None:
|
||||
load_dotenv()
|
||||
|
||||
self.token = os.getenv("TOKEN")
|
||||
self.client_id = os.getenv("CLIENT_ID")
|
||||
self.client_secret_id = os.getenv("CLIENT_SECRET_ID")
|
||||
self.secret_key = os.getenv("DASHBOARD_SECRET_KEY")
|
||||
self.bug_report_channel_id = int(os.getenv("BUG_REPORT_CHANNEL_ID"))
|
||||
self.spotify_client_id = os.getenv("SPOTIFY_CLIENT_ID")
|
||||
self.spotify_client_secret = os.getenv("SPOTIFY_CLIENT_SECRET")
|
||||
self.genius_token = os.getenv("GENIUS_TOKEN")
|
||||
self.mongodb_url = os.getenv("MONGODB_URL")
|
||||
self.mongodb_name = os.getenv("MONGODB_NAME")
|
||||
self.ipc_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {})
|
||||
self.version: str = settings.get("version", "")
|
||||
194
cogs/basic.py
194
cogs/basic.py
@@ -30,13 +30,14 @@ from function import (
|
||||
settings,
|
||||
send,
|
||||
time as ctime,
|
||||
formatTime,
|
||||
format_time,
|
||||
get_source,
|
||||
get_user,
|
||||
get_lang,
|
||||
truncate_string,
|
||||
cooldown_check,
|
||||
get_aliases
|
||||
get_aliases,
|
||||
logger
|
||||
)
|
||||
|
||||
from addons import lyricsPlatform
|
||||
@@ -91,22 +92,25 @@ class Basic(commands.Cog):
|
||||
return [app_commands.Choice(name=c.capitalize(), value=c) for c in self.bot.cogs if c not in ["Nodes", "Task"] and current in c]
|
||||
|
||||
async def play_autocomplete(self, interaction: discord.Interaction, current: str) -> list:
|
||||
if voicelink.pool.URL_REGEX.match(current): return
|
||||
if voicelink.pool.URL_REGEX.match(current): return []
|
||||
|
||||
history: dict[str, str] = {}
|
||||
for track_id in reversed(await get_user(interaction.user.id, "history")):
|
||||
track_dict = voicelink.decode(track_id)
|
||||
history[track_dict["identifier"]] = track_dict
|
||||
|
||||
history_tracks = [app_commands.Choice(name=truncate_string(f"🕒 {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values()][:25]
|
||||
history_tracks = [app_commands.Choice(name=truncate_string(f"🕒 {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25]
|
||||
if not current:
|
||||
return history_tracks
|
||||
|
||||
node = voicelink.NodePool.get_node()
|
||||
if node and node.spotify_client:
|
||||
tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user)
|
||||
return history_tracks[:5] + [app_commands.Choice(name=f"🎵 {track.author} - {track.title}", value=f"{track.author} - {track.title}") for track in tracks]
|
||||
|
||||
try:
|
||||
tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user)
|
||||
return history_tracks[:5] + [app_commands.Choice(name=truncate_string(f"🎵 {track.author} - {track.title}", 100), value=truncate_string(f"{track.author} - {track.title}", 100)) for track in tracks]
|
||||
except voicelink.TrackLoadError:
|
||||
return history_tracks
|
||||
|
||||
@commands.hybrid_command(name="connect", aliases=get_aliases("connect"))
|
||||
@app_commands.describe(channel="Provide a channel to connect.")
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -120,10 +124,14 @@ class Basic(commands.Cog):
|
||||
await send(ctx, 'connect', player.channel)
|
||||
|
||||
@commands.hybrid_command(name="play", aliases=get_aliases("play"))
|
||||
@app_commands.describe(query="Input a query or a searchable link.")
|
||||
@app_commands.describe(
|
||||
query="Input a query or a searchable link.",
|
||||
start="Specify a time you would like to start, e.g. 1:00",
|
||||
end="Specify a time you would like to end, e.g. 4:00"
|
||||
)
|
||||
@app_commands.autocomplete(query=play_autocomplete)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def play(self, ctx: commands.Context, *, query: str) -> None:
|
||||
async def play(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0") -> None:
|
||||
"Loads your input and added it to the queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
@@ -132,16 +140,19 @@ class Basic(commands.Cog):
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
if ctx.interaction:
|
||||
await ctx.interaction.response.defer()
|
||||
|
||||
tracks = await player.get_tracks(query, requester=ctx.author)
|
||||
if not tracks:
|
||||
return await send(ctx, "noTrackFound")
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
index = await player.add_track(tracks.tracks)
|
||||
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end))
|
||||
await send(ctx, "playlistLoad", tracks.name, index)
|
||||
else:
|
||||
position = await player.add_track(tracks[0])
|
||||
position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end))
|
||||
texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
|
||||
await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
|
||||
except voicelink.QueueFull as e:
|
||||
@@ -172,6 +183,7 @@ class Basic(commands.Cog):
|
||||
if not player.is_user_join(interaction.user):
|
||||
return await send(interaction, "notInChannel", interaction.user.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
await interaction.response.defer()
|
||||
tracks = await player.get_tracks(query, requester=interaction.user)
|
||||
if not tracks:
|
||||
return await send(interaction, "noTrackFound")
|
||||
@@ -183,12 +195,9 @@ class Basic(commands.Cog):
|
||||
else:
|
||||
position = await player.add_track(tracks[0])
|
||||
texts = await get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad")
|
||||
await interaction.response.send_message((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
|
||||
await interaction.followup.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
|
||||
except voicelink.QueueFull as e:
|
||||
await interaction.response.send_message(e)
|
||||
|
||||
except Exception as e:
|
||||
return await interaction.response.send_message(e, ephemeral=True)
|
||||
await interaction.followup.send(e)
|
||||
|
||||
finally:
|
||||
if not player.is_playing:
|
||||
@@ -248,9 +257,14 @@ class Basic(commands.Cog):
|
||||
await player.do_next()
|
||||
|
||||
@commands.hybrid_command(name="playtop", aliases=get_aliases("playtop"))
|
||||
@app_commands.describe(query="Input a query or a searchable link.")
|
||||
@app_commands.describe(
|
||||
query="Input a query or a searchable link.",
|
||||
start="Specify a time you would like to start, e.g. 1:00",
|
||||
end="Specify a time you would like to end, e.g. 4:00"
|
||||
)
|
||||
@app_commands.autocomplete(query=play_autocomplete)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def playtop(self, ctx: commands.Context, *, query: str):
|
||||
async def playtop(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0"):
|
||||
"Adds a song with the given url or query on the top of the queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
@@ -258,17 +272,20 @@ class Basic(commands.Cog):
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
|
||||
if ctx.interaction:
|
||||
await ctx.interaction.response.defer()
|
||||
|
||||
tracks = await player.get_tracks(query, requester=ctx.author)
|
||||
if not tracks:
|
||||
return await send(ctx, "noTrackFound")
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
index = await player.add_track(tracks.tracks, at_font=True)
|
||||
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end), at_front=True)
|
||||
await send(ctx, "playlistLoad", tracks.name, index)
|
||||
else:
|
||||
position = await player.add_track(tracks[0], at_font=True)
|
||||
position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
|
||||
texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
|
||||
await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False)
|
||||
|
||||
@@ -280,9 +297,13 @@ class Basic(commands.Cog):
|
||||
await player.do_next()
|
||||
|
||||
@commands.hybrid_command(name="forceplay", aliases=get_aliases("forceplay"))
|
||||
@app_commands.describe(query="Input a query or a searchable link.")
|
||||
@app_commands.describe(
|
||||
query="Input a query or a searchable link.",
|
||||
start="Specify a time you would like to start, e.g. 1:00",
|
||||
end="Specify a time you would like to end, e.g. 4:00"
|
||||
)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def forceplay(self, ctx: commands.Context, query: str):
|
||||
async def forceplay(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0"):
|
||||
"Enforce playback using the given URL or query."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
@@ -290,6 +311,9 @@ class Basic(commands.Cog):
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPerms_function", ephemeral=True)
|
||||
|
||||
if ctx.interaction:
|
||||
await ctx.interaction.response.defer()
|
||||
|
||||
tracks = await player.get_tracks(query, requester=ctx.author)
|
||||
if not tracks:
|
||||
@@ -297,11 +321,11 @@ class Basic(commands.Cog):
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
index = await player.add_track(tracks.tracks, at_font=True)
|
||||
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end), at_front=True)
|
||||
await send(ctx, "playlistLoad", tracks.name, index)
|
||||
else:
|
||||
texts = await get_lang(ctx.guild.id, "live", "trackLoad")
|
||||
await player.add_track(tracks[0], at_font=True)
|
||||
await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
|
||||
await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length), allowed_mentions=False)
|
||||
|
||||
except voicelink.QueueFull as e:
|
||||
@@ -311,7 +335,7 @@ class Basic(commands.Cog):
|
||||
if player.queue._repeat.mode == voicelink.LoopType.track:
|
||||
await player.set_repeat(voicelink.LoopType.off.name)
|
||||
|
||||
await player.stop() if player.is_playing else await player.do_next()
|
||||
await player.stop() if player.is_playing else await player.do_next()
|
||||
|
||||
@commands.hybrid_command(name="pause", aliases=get_aliases("pause"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -327,15 +351,12 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.pause_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
else:
|
||||
player.pause_votes.add(ctx.author)
|
||||
if len(player.pause_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await send(ctx, "pauseVote", ctx.author, len(player.pause_votes), required)
|
||||
|
||||
player.pause_votes.add(ctx.author)
|
||||
if len(player.pause_votes) < (required := player.required()):
|
||||
return await send(ctx, "pauseVote", ctx.author, len(player.pause_votes), required)
|
||||
|
||||
await player.set_pause(True, ctx.author)
|
||||
player.pause_votes.clear()
|
||||
await send(ctx, "paused", ctx.author)
|
||||
|
||||
@commands.hybrid_command(name="resume", aliases=get_aliases("resume"))
|
||||
@@ -352,15 +373,12 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.resume_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
else:
|
||||
player.resume_votes.add(ctx.author)
|
||||
if len(player.resume_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await send(ctx, "resumeVote", ctx.author, len(player.resume_votes), required)
|
||||
|
||||
player.resume_votes.add(ctx.author)
|
||||
if len(player.resume_votes) < (required := player.required()):
|
||||
return await send(ctx, "resumeVote", ctx.author, len(player.resume_votes), required)
|
||||
|
||||
await player.set_pause(False, ctx.author)
|
||||
player.resume_votes.clear()
|
||||
await send(ctx, "resumed", ctx.author)
|
||||
|
||||
@commands.hybrid_command(name="skip", aliases=get_aliases("skip"))
|
||||
@@ -372,6 +390,9 @@ class Basic(commands.Cog):
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
|
||||
if not player.node._available:
|
||||
return await send(ctx, "nodeReconnect")
|
||||
|
||||
if not player.is_playing:
|
||||
return await send(ctx, "skipError", ephemeral=True)
|
||||
|
||||
@@ -382,19 +403,13 @@ class Basic(commands.Cog):
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
else:
|
||||
player.skip_votes.add(ctx.author)
|
||||
if len(player.skip_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
if len(player.skip_votes) < (required := player.required()):
|
||||
return await send(ctx, "skipVote", ctx.author, len(player.skip_votes), required)
|
||||
|
||||
if not player.node._available:
|
||||
return await send(ctx, "nodeReconnect")
|
||||
|
||||
if index:
|
||||
player.queue.skipto(index)
|
||||
|
||||
await send(ctx, "skipped", ctx.author)
|
||||
|
||||
if player.queue._repeat.mode == voicelink.LoopType.track:
|
||||
await player.set_repeat(voicelink.LoopType.off.name)
|
||||
|
||||
@@ -409,18 +424,16 @@ class Basic(commands.Cog):
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
|
||||
if not player.node._available:
|
||||
return await send(ctx, "nodeReconnectode")
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.previous_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
else:
|
||||
player.previous_votes.add(ctx.author)
|
||||
if len(player.previous_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await send(ctx, "backVote", ctx.author, len(player.previous_votes), required)
|
||||
|
||||
if not player.node._available:
|
||||
return await send(ctx, "nnodeReconnectode")
|
||||
|
||||
player.previous_votes.add(ctx.author)
|
||||
if len(player.previous_votes) < (required := player.required()):
|
||||
return await send(ctx, "backVote", ctx.author, len(player.previous_votes), required)
|
||||
|
||||
if not player.is_playing:
|
||||
player.queue.backto(index)
|
||||
@@ -430,7 +443,6 @@ class Basic(commands.Cog):
|
||||
await player.stop()
|
||||
|
||||
await send(ctx, "backed", ctx.author)
|
||||
|
||||
if player.queue._repeat.mode == voicelink.LoopType.track:
|
||||
await player.set_repeat(voicelink.LoopType.off.name)
|
||||
|
||||
@@ -449,8 +461,7 @@ class Basic(commands.Cog):
|
||||
if not player.current or player.position == 0:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
|
||||
num = formatTime(position)
|
||||
if num is None:
|
||||
if not (num := format_time(position)):
|
||||
return await send(ctx, "timeFormatError", ephemeral=True)
|
||||
|
||||
await player.seek(num, ctx.author)
|
||||
@@ -530,7 +541,7 @@ class Basic(commands.Cog):
|
||||
track_ids = bytes.split(b"\n")[-1]
|
||||
track_ids = track_ids.decode().split(",")
|
||||
|
||||
tracks = (voicelink.Track(track_id=track_id, info=voicelink.decode(track_id), requester=ctx.author) for track_id in track_ids)
|
||||
tracks = [voicelink.Track(track_id=track_id, info=voicelink.decode(track_id), requester=ctx.author) for track_id in track_ids]
|
||||
if not tracks:
|
||||
return await send(ctx, "noTrackFound")
|
||||
|
||||
@@ -540,8 +551,9 @@ class Basic(commands.Cog):
|
||||
except voicelink.QueueFull as e:
|
||||
return await ctx.send(e, ephemeral=True)
|
||||
|
||||
except:
|
||||
return await send(ctx, "decodeError", ephemeral=True)
|
||||
except Exception as e:
|
||||
logger.error("error", exc_info=e)
|
||||
raise e
|
||||
|
||||
finally:
|
||||
if not player.is_playing:
|
||||
@@ -615,7 +627,7 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPerms_mode", ephemeral=True)
|
||||
|
||||
await player.set_repeat(mode)
|
||||
await player.set_repeat(mode, ctx.author)
|
||||
await send(ctx, "repeat", mode.capitalize())
|
||||
|
||||
@commands.hybrid_command(name="clear", aliases=get_aliases("clear"))
|
||||
@@ -634,13 +646,7 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPerms_queue", ephemeral=True)
|
||||
|
||||
queue = queue.lower()
|
||||
if queue == 'history':
|
||||
player.queue.history_clear(player.is_playing)
|
||||
else:
|
||||
queue = "queue"
|
||||
player.queue.clear()
|
||||
|
||||
await player.clear_queue(queue, ctx.author)
|
||||
await send(ctx, "cleared", queue.capitalize())
|
||||
|
||||
@commands.hybrid_command(name="remove", aliases=get_aliases("remove"))
|
||||
@@ -659,17 +665,8 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPerms_queue", ephemeral=True)
|
||||
|
||||
removedTrack = player.queue.remove(position1, position2, member=member)
|
||||
|
||||
if player.is_ipc_connected and removedTrack:
|
||||
await player.send_ws({
|
||||
"op": "removeTrack",
|
||||
"positions": [track["position"] for track in removedTrack],
|
||||
"track_ids": [track["track"].track_id for track in removedTrack],
|
||||
"current_queue_position": player.queue._position
|
||||
}, requester=ctx.author)
|
||||
|
||||
await send(ctx, "removed", len(removedTrack))
|
||||
removed_tracks = await player.remove_track(position1, position2, remove_target=member, requester=ctx.author)
|
||||
await send(ctx, "removed", len(removed_tracks.keys()))
|
||||
|
||||
@commands.hybrid_command(name="forward", aliases=get_aliases("forward"))
|
||||
@app_commands.describe(position="Input a amount that you to forward to. Exmaple: 1:20")
|
||||
@@ -685,9 +682,8 @@ class Basic(commands.Cog):
|
||||
|
||||
if not player.current:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
|
||||
num = formatTime(position)
|
||||
if num is None:
|
||||
|
||||
if not (num := format_time(position)):
|
||||
return await send(ctx, "timeFormatError", ephemeral=True)
|
||||
|
||||
await player.seek(int(player.position + num))
|
||||
@@ -708,8 +704,7 @@ class Basic(commands.Cog):
|
||||
if not player.current:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
|
||||
num = formatTime(position)
|
||||
if num is None:
|
||||
if not (num := format_time(position)):
|
||||
return await send(ctx, "timeFormatError", ephemeral=True)
|
||||
|
||||
await player.seek(int(player.position - num))
|
||||
@@ -743,12 +738,10 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.shuffle_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
else:
|
||||
player.shuffle_votes.add(ctx.author)
|
||||
if len(player.shuffle_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return await send(ctx, "shuffleVote", ctx.author, len(player.shuffle_votes), required)
|
||||
|
||||
player.shuffle_votes.add(ctx.author)
|
||||
if len(player.shuffle_votes) < (required := player.required()):
|
||||
return await send(ctx, "shuffleVote", ctx.author, len(player.shuffle_votes), required)
|
||||
|
||||
await player.shuffle("queue", ctx.author)
|
||||
await send(ctx, "shuffled")
|
||||
@@ -768,12 +761,7 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPerms_pos", ephemeral=True)
|
||||
|
||||
track1, track2 = player.queue.swap(position1, position2)
|
||||
await player.send_ws({
|
||||
"op": "swapTrack",
|
||||
"position1": {"index": position1, "track_id": track1.track_id},
|
||||
"position2": {"index": position2, "track_id": track2.track_id}
|
||||
}, requester=ctx.author)
|
||||
track1, track2 = await player.swap_track(position1, position2, ctx.author)
|
||||
await send(ctx, "swapped", track1.title, track2.title)
|
||||
|
||||
@commands.hybrid_command(name="move", aliases=get_aliases("move"))
|
||||
@@ -791,12 +779,7 @@ class Basic(commands.Cog):
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPerms_pos", ephemeral=True)
|
||||
|
||||
moved_track = player.queue.move(target, to)
|
||||
await player.send_ws({
|
||||
"op": "moveTrack",
|
||||
"position": {"index": target, "track_id": moved_track.track_id},
|
||||
"newPosition": {"index": to}
|
||||
}, requester=ctx.author)
|
||||
moved_track = await player.move_track(target, to, ctx.author)
|
||||
await send(ctx, "moved", moved_track, to)
|
||||
|
||||
@commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics"))
|
||||
@@ -861,6 +844,9 @@ class Basic(commands.Cog):
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
if player.is_ipc_connected:
|
||||
await player.send_ws({"op": "toggleAutoplay", "status": check})
|
||||
|
||||
@commands.hybrid_command(name="help", aliases=get_aliases("help"))
|
||||
@app_commands.autocomplete(category=help_autocomplete)
|
||||
|
||||
@@ -69,8 +69,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 ctx.send(f"You set the speed to **{value}**.")
|
||||
|
||||
effect = voicelink.Timescale(tag="speed", speed=value)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="karaoke", aliases=get_aliases("karaoke"))
|
||||
@app_commands.describe(
|
||||
@@ -86,8 +88,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "karaoke", level, monolevel, filterband, filterwidth)
|
||||
|
||||
effect = voicelink.Karaoke(tag="karaoke", level=level, mono_level=monolevel, filter_band=filterband, filter_width=filterwidth)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="tremolo", aliases=get_aliases("tremolo"))
|
||||
@app_commands.describe(
|
||||
@@ -101,8 +105,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "tremolo&vibrato", frequency, depth)
|
||||
|
||||
effect = voicelink.Tremolo(tag="tremolo", frequency=frequency, depth=depth)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="vibrato", aliases=get_aliases("vibrato"))
|
||||
@app_commands.describe(
|
||||
@@ -116,8 +122,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "tremolo&vibrato", frequency, depth)
|
||||
|
||||
effect = voicelink.Vibrato(tag="vibrato", frequency=frequency, depth=depth)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="rotation", aliases=get_aliases("rotation"))
|
||||
@app_commands.describe(hertz="The hertz of the rotation. Default is `0.2`")
|
||||
@@ -128,8 +136,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "rotation", hertz)
|
||||
|
||||
effect = voicelink.Rotation(tag="rotation", rotation_hertz=hertz)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="distortion", aliases=get_aliases("distortion"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -139,8 +149,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "distortion")
|
||||
|
||||
effect = 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 player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="lowpass", aliases=get_aliases("lowpass"))
|
||||
@app_commands.describe(smoothing="The level of the lowPass. Default is `20.0`")
|
||||
@@ -151,8 +163,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "lowpass", smoothing)
|
||||
|
||||
effect = voicelink.LowPass(tag="lowpass", smoothing=smoothing)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="channelmix", aliases=get_aliases("channelmix"))
|
||||
@app_commands.describe(
|
||||
@@ -168,8 +182,10 @@ class Effect(commands.Cog):
|
||||
|
||||
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 send(ctx, "channelmix", left_to_left, right_to_right, left_to_right, right_to_left)
|
||||
|
||||
effect = 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 player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="nightcore", aliases=get_aliases("nightcore"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -177,8 +193,9 @@ class Effect(commands.Cog):
|
||||
"Add nightcore filter into your player."
|
||||
player = await check_access(ctx)
|
||||
|
||||
await player.add_filter(voicelink.Timescale.nightcore())
|
||||
await send(ctx, "nightcore")
|
||||
effect = voicelink.Timescale.nightcore()
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="8d", aliases=get_aliases("8d"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -186,8 +203,9 @@ class Effect(commands.Cog):
|
||||
"Add 8D filter into your player."
|
||||
player = await check_access(ctx)
|
||||
|
||||
await player.add_filter(voicelink.Rotation.nightD())
|
||||
await send(ctx, "8d")
|
||||
effect = voicelink.Rotation.nightD()
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="vaporwave", aliases=get_aliases("vaporwave"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -195,8 +213,9 @@ class Effect(commands.Cog):
|
||||
"Add vaporwave filter into your player."
|
||||
player = await check_access(ctx)
|
||||
|
||||
await player.add_filter(voicelink.Timescale.vaporwave())
|
||||
await send(ctx, "vaporwave")
|
||||
effect = voicelink.Timescale.vaporwave()
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="cleareffect", aliases=get_aliases("cleareffect"))
|
||||
@app_commands.describe(effect="Remove a specific sound effects.")
|
||||
@@ -211,7 +230,7 @@ class Effect(commands.Cog):
|
||||
else:
|
||||
await player.reset_filter()
|
||||
|
||||
await send(ctx, "cleareffect")
|
||||
await send(ctx, "clearEffect")
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Effect(bot))
|
||||
|
||||
@@ -44,12 +44,13 @@ class Listeners(commands.Cog):
|
||||
try:
|
||||
await self.voicelink.create_node(
|
||||
bot=self.bot,
|
||||
spotify_client_id=func.tokens.spotify_client_id,
|
||||
spotify_client_secret=func.tokens.spotify_client_secret,
|
||||
spotify_client_id=func.settings.spotify_client_id,
|
||||
spotify_client_secret=func.settings.spotify_client_secret,
|
||||
logger=func.logger,
|
||||
**n
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Node {n["identifier"]} is not able to connect! - Reason: {e}')
|
||||
func.logger.error(f'Node {n["identifier"]} is not able to connect! - Reason: {e}')
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voicelink_track_end(self, player: voicelink.Player, track, _):
|
||||
@@ -79,8 +80,7 @@ class Listeners(commands.Cog):
|
||||
player: voicelink.Player = member.guild.voice_client
|
||||
if not player:
|
||||
return
|
||||
|
||||
guild = member.guild.id
|
||||
|
||||
is_joined = True
|
||||
|
||||
if not before.channel and after.channel:
|
||||
@@ -94,16 +94,20 @@ class Listeners(commands.Cog):
|
||||
if after.channel.id != player.channel.id:
|
||||
is_joined = False
|
||||
|
||||
if player.is_ipc_connected:
|
||||
if is_joined and player.settings.get("24/7", False):
|
||||
if player.is_paused and len([m for m in player.channel.members if not m.bot]) == 1:
|
||||
await player.set_pause(False, member)
|
||||
|
||||
if self.bot.ipc._is_connected:
|
||||
await self.bot.ipc.send({
|
||||
"op": "updateGuild",
|
||||
"user": {
|
||||
"user_id": member.id,
|
||||
"user_id": str(member.id),
|
||||
"avatar_url": member.display_avatar.url,
|
||||
"name": member.name,
|
||||
},
|
||||
"channel_name": member.voice.channel.name if is_joined else "",
|
||||
"guild_id": guild,
|
||||
"guild_id": str(member.guild.id),
|
||||
"is_joined": is_joined
|
||||
})
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord, voicelink
|
||||
import discord, voicelink, time
|
||||
|
||||
from io import StringIO
|
||||
from discord import app_commands
|
||||
@@ -35,13 +35,13 @@ from function import (
|
||||
get_lang,
|
||||
settings,
|
||||
get_aliases,
|
||||
cooldown_check
|
||||
cooldown_check,
|
||||
logger
|
||||
)
|
||||
|
||||
from datetime import datetime
|
||||
from views import PlaylistView, InboxView, HelpView
|
||||
|
||||
def assign_playlistId(existed: list) -> str:
|
||||
def assign_playlist_id(existed: list) -> str:
|
||||
for i in range(200, 210):
|
||||
if str(i) not in existed:
|
||||
return str(i)
|
||||
@@ -195,18 +195,18 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
text = await get_lang(ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter")
|
||||
embed = discord.Embed(
|
||||
title=text[0].format(ctx.author.display_name),
|
||||
description='```prolog\n %4s %10s %10s %10s\n' % tuple(text[1].split(",")),
|
||||
description='```prolog\n %4s %10s %12s %10s\n' % tuple(text[1].split(",")),
|
||||
color=settings.embed_color
|
||||
)
|
||||
|
||||
for index in range(max_p):
|
||||
try:
|
||||
info = results[index]
|
||||
track_info = (info['emoji'], info['id'], f"[{info['time']}]", info['name'], f"{len(info['tracks'])}/{max_t}")
|
||||
track_info = (info['emoji'], info['id'], f"[{info['time']}]", info['name'], f"{len(info['tracks'])}")
|
||||
except IndexError:
|
||||
track_info = ("🎵", "-"*3, "[--:--]", "-"*6, f"-/{max_t}")
|
||||
track_info = ("🎵", "-"*3, "[--:--]", "-"*6, f"-")
|
||||
|
||||
embed.description += '%0s %3s. %10s %10s %10s\n' % track_info
|
||||
embed.description += '%0s %3s. %10s %12s %10s\n' % track_info
|
||||
|
||||
embed.description += "```"
|
||||
embed.set_footer(text=text[2])
|
||||
@@ -240,7 +240,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
return await send(ctx, "playlistNotInvaildUrl", ephemeral=True)
|
||||
|
||||
data = {'uri': link, 'perms': {'read': []}, 'name': name, 'type': 'link'} if link else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
|
||||
await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlistId([data for data in user])}": data}})
|
||||
await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
|
||||
await send(ctx, "playlistCreated", name)
|
||||
|
||||
@playlist.command(name="delete", aliases=get_aliases("delete"))
|
||||
@@ -297,7 +297,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
{"$push": {"inbox": {
|
||||
'sender': ctx.author.id,
|
||||
'referId': result['id'],
|
||||
'time': datetime.now(),
|
||||
'time': time.time(),
|
||||
'title': f'Playlist invitation from {ctx.author}',
|
||||
'description': f"You are invited to use this playlist.\nPlaylist Name: {result['playlist']['name']}\nPlaylist type: {result['playlist']['type']}",
|
||||
'type': 'invite'
|
||||
@@ -352,11 +352,12 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
|
||||
update_data, dId = {}, {dId for dId in user["playlist"]}
|
||||
for data in view.newplaylist[:(max_p - len(user['playlist']))]:
|
||||
addId = assign_playlistId(dId)
|
||||
addId = assign_playlist_id(dId)
|
||||
await update_user(data['sender'], {"$push": {f"playlist.{data['referId']}.perms.read": ctx.author.id}})
|
||||
update_data[f'playlist.{addId}'] = {
|
||||
'user': data['sender'], 'referId': data['referId'],
|
||||
'name': f"Share{data['time'].strftime('%M%S')}", 'type': 'share'
|
||||
'name': f"Share{time.strftime('%M%S', time.gmtime(int(data['time'])))}",
|
||||
'type': 'share'
|
||||
}
|
||||
update_data["inbox"] = view.inbox
|
||||
dId.add(addId)
|
||||
@@ -501,11 +502,12 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
track_ids = track_ids.decode().split(",")
|
||||
|
||||
data = {'tracks': track_ids, 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
|
||||
await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlistId([data for data in user])}": data}})
|
||||
await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
|
||||
await send(ctx, 'playlistCreated', name)
|
||||
|
||||
except:
|
||||
return await send(ctx, "decodeError", ephemeral=True)
|
||||
except Exception as e:
|
||||
logger.error("Decode Error", exc_info=e)
|
||||
raise e
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Playlists(bot))
|
||||
@@ -26,7 +26,6 @@ import voicelink
|
||||
import psutil
|
||||
import function as func
|
||||
|
||||
from typing import Tuple
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from function import (
|
||||
@@ -37,16 +36,14 @@ from function import (
|
||||
get_lang,
|
||||
time as ctime,
|
||||
get_aliases,
|
||||
cooldown_check
|
||||
cooldown_check,
|
||||
format_bytes
|
||||
)
|
||||
|
||||
from views import DebugView, HelpView, EmbedBuilderView
|
||||
|
||||
def formatBytes(bytes: int, unit: bool = False):
|
||||
if bytes <= 1_000_000_000:
|
||||
return f"{bytes / (1024 ** 2):.1f}" + ("MB" if unit else "")
|
||||
|
||||
else:
|
||||
return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "")
|
||||
def status_icon(status: bool) -> str:
|
||||
return "✅" if status else "❌"
|
||||
|
||||
class Settings(commands.Cog, name="settings"):
|
||||
def __init__(self, bot) -> None:
|
||||
@@ -137,17 +134,18 @@ class Settings(commands.Cog, name="settings"):
|
||||
"Show all the bot settings in your server."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
|
||||
texts = await get_lang(ctx.guild.id, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsPermTitle", "settingsPermValue")
|
||||
texts = await get_lang(ctx.guild.id, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsTitle3", "settingsPermTitle", "settingsPermValue")
|
||||
embed = discord.Embed(color=func.settings.embed_color)
|
||||
embed.set_author(name=texts[0].format(ctx.guild.name), icon_url=self.bot.user.display_avatar.url)
|
||||
if ctx.guild.icon:
|
||||
embed.set_thumbnail(url=ctx.guild.icon.url)
|
||||
|
||||
dj_role = ctx.guild.get_role(settings.get('dj', 0))
|
||||
embed.add_field(name=texts[1], value=texts[2].format(
|
||||
settings.get('prefix', func.settings.bot_prefix) or "None",
|
||||
settings.get('prefix', func.settings.bot_prefix) or 'None',
|
||||
settings.get('lang', 'EN'),
|
||||
settings.get('controller', True),
|
||||
f"<@&{settings['dj']}>" if 'dj' in settings else '`None`',
|
||||
dj_role.name if dj_role else 'None',
|
||||
settings.get('votedisable', False),
|
||||
settings.get('24/7', False),
|
||||
settings.get('volume', 100),
|
||||
@@ -160,12 +158,17 @@ class Settings(commands.Cog, name="settings"):
|
||||
settings.get("duplicateTrack", True)
|
||||
))
|
||||
|
||||
if stage_template := settings.get("stage_announce_template"):
|
||||
embed.add_field(name=texts[5], value=f"```{stage_template}```", inline=False)
|
||||
|
||||
perms = ctx.guild.me.guild_permissions
|
||||
embed.add_field(name=texts[5], value=texts[6].format(
|
||||
'<a:Check:941206936651706378>' if perms.administrator else '<a:Cross:941206918255497237>',
|
||||
'<a:Check:941206936651706378>' if perms.manage_guild else '<a:Cross:941206918255497237>',
|
||||
'<a:Check:941206936651706378>' if perms.manage_channels else '<a:Cross:941206918255497237>',
|
||||
'<a:Check:941206936651706378>' if perms.manage_messages else '<a:Cross:941206918255497237>'), inline=False
|
||||
embed.add_field(name=texts[6], value=texts[7].format(
|
||||
status_icon(perms.administrator),
|
||||
status_icon(perms.manage_guild),
|
||||
status_icon(perms.manage_channels),
|
||||
status_icon(perms.manage_messages)
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@@ -236,25 +239,34 @@ class Settings(commands.Cog, name="settings"):
|
||||
await update_settings(ctx.guild.id, {"$set": {'controller_msg': toggle}})
|
||||
await send(ctx, 'toggleControllerMsg', await get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
|
||||
|
||||
@settings.command(name="stageannounce", aliases=get_aliases("stageannounce"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def stageannounce(self, ctx: commands.Context, template: str = None):
|
||||
"""Customize the channel topic template"""
|
||||
await update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}})
|
||||
await send(ctx, "SetStageAnnounceTemplate")
|
||||
|
||||
@app_commands.command(name="debug")
|
||||
async def debug(self, interaction: discord.Interaction):
|
||||
if interaction.user.id not in func.settings.bot_access_user:
|
||||
return await interaction.response.send_message("You are not able to use this command!")
|
||||
|
||||
memory = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage('/')
|
||||
disk = psutil.disk_usage(func.ROOT_DIR)
|
||||
|
||||
available_memory, total_memory = memory.available, memory.total
|
||||
used_disk_space, total_disk_space = disk.used, disk.total
|
||||
embed = discord.Embed(title="📄 Debug Panel", color=func.settings.embed_color)
|
||||
embed.description = "```== System Info ==\n" \
|
||||
f"• CPU: {psutil.cpu_freq().current}Mhz ({psutil.cpu_percent()}%)\n" \
|
||||
f"• RAM: {formatBytes(total_memory - available_memory)}/{formatBytes(total_memory, True)} ({memory.percent}%)\n" \
|
||||
f"• DISK: {formatBytes(total_disk_space - used_disk_space)}/{formatBytes(total_disk_space, True)} ({disk.percent}%)```"
|
||||
f"• RAM: {format_bytes(total_memory - available_memory)}/{format_bytes(total_memory, True)} ({memory.percent}%)\n" \
|
||||
f"• DISK: {format_bytes(total_disk_space - used_disk_space)}/{format_bytes(total_disk_space, True)} ({disk.percent}%)```"
|
||||
|
||||
embed.add_field(
|
||||
name="🤖 Bot Information",
|
||||
value=f"```• LATENCY: {self.bot.latency:.2f}ms\n" \
|
||||
value=f"```• VERSION: {func.settings.version}\n" \
|
||||
f"• LATENCY: {self.bot.latency:.2f}ms\n" \
|
||||
f"• GUILDS: {len(self.bot.guilds)}\n" \
|
||||
f"• USERS: {sum([guild.member_count for guild in self.bot.guilds])}\n" \
|
||||
f"• PLAYERS: {len(self.bot.voice_clients)}```",
|
||||
@@ -263,17 +275,23 @@ class Settings(commands.Cog, name="settings"):
|
||||
|
||||
node: voicelink.Node
|
||||
for name, node in voicelink.NodePool._nodes.items():
|
||||
total_memory = node.stats.used + node.stats.free
|
||||
embed.add_field(
|
||||
name=f"{name} Node - " + ("🟢 Connected" if node._available else "🔴 Disconnected"),
|
||||
value=f"```• ADDRESS: {node._host}:{node._port}\n" \
|
||||
f"• PLAYERS: {len(node._players)}\n" \
|
||||
f"• CPU: {node.stats.cpu_process_load:.1f}%\n" \
|
||||
f"• RAM: {formatBytes(node.stats.free)}/{formatBytes(total_memory, True)} ({(node.stats.free/total_memory) * 100:.1f}%)\n"
|
||||
f"• LATENCY: {node.latency:.2f}ms\n" \
|
||||
f"• UPTIME: {func.time(node.stats.uptime)}```",
|
||||
inline=True
|
||||
)
|
||||
if node._available:
|
||||
total_memory = node.stats.used + node.stats.free
|
||||
embed.add_field(
|
||||
name=f"{name} Node - 🟢 Connected",
|
||||
value=f"```• ADDRESS: {node._host}:{node._port}\n" \
|
||||
f"• PLAYERS: {len(node._players)}\n" \
|
||||
f"• CPU: {node.stats.cpu_process_load:.1f}%\n" \
|
||||
f"• RAM: {format_bytes(node.stats.free)}/{format_bytes(total_memory, True)} ({(node.stats.free/total_memory) * 100:.1f}%)\n"
|
||||
f"• LATENCY: {node.latency:.2f}ms\n" \
|
||||
f"• UPTIME: {func.time(node.stats.uptime)}```"
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=f"{name} Node - 🔴 Disconnected",
|
||||
value=f"```• ADDRESS: {node._host}:{node._port}\n" \
|
||||
f"• PLAYERS: {len(node._players)}\nNo extra data is available for display```",
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed, view=DebugView(self.bot), ephemeral=True)
|
||||
|
||||
|
||||
71
cogs/task.py
71
cogs/task.py
@@ -26,7 +26,6 @@ import discord
|
||||
import function as func
|
||||
|
||||
from discord.ext import commands, tasks
|
||||
from datetime import datetime
|
||||
from addons import Placeholders
|
||||
|
||||
class Task(commands.Cog):
|
||||
@@ -36,12 +35,6 @@ class Task(commands.Cog):
|
||||
self.player_check.start()
|
||||
self.cache_cleaner.start()
|
||||
|
||||
self.act_type = {
|
||||
"play": discord.ActivityType.playing,
|
||||
"listen": discord.ActivityType.listening,
|
||||
"watch": discord.ActivityType.watching,
|
||||
"stream": discord.ActivityType.streaming
|
||||
}
|
||||
self.current_act = 0
|
||||
self.placeholder = Placeholders(bot)
|
||||
|
||||
@@ -56,18 +49,21 @@ class Task(commands.Cog):
|
||||
|
||||
try:
|
||||
act_data = func.settings.activity[(self.current_act + 1) % len(func.settings.activity) - 1]
|
||||
|
||||
act_original = self.bot.activity
|
||||
act_type = self.act_type.get(list(act_data.keys())[0].lower(), discord.ActivityType.playing)
|
||||
act_name = self.placeholder.replace(list(act_data.values())[0])
|
||||
act_type = getattr(discord.ActivityType, act_data.get("type", "").lower(), discord.ActivityType.playing)
|
||||
act_name = self.placeholder.replace(act_data.get("name", ""))
|
||||
|
||||
status_type = getattr(discord.Status, act_data.get("status", "").lower(), None)
|
||||
|
||||
if act_original.type != act_type or act_original.name != act_name:
|
||||
new_act = discord.Activity(type=act_type, name=act_name)
|
||||
await self.bot.change_presence(activity=new_act)
|
||||
self.bot.activity = discord.Activity(type=act_type, name=act_name)
|
||||
await self.bot.change_presence(activity=self.bot.activity, status=status_type)
|
||||
self.current_act = (self.current_act + 1) % len(func.settings.activity)
|
||||
|
||||
except:
|
||||
pass
|
||||
func.logger.info(f"Changed the bot status to {act_name}")
|
||||
|
||||
except Exception as e:
|
||||
func.logger.error("Error occurred while changing the bot status!", exc_info=e)
|
||||
|
||||
@tasks.loop(minutes=5.0)
|
||||
async def player_check(self):
|
||||
@@ -84,44 +80,35 @@ class Task(commands.Cog):
|
||||
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:
|
||||
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)
|
||||
|
||||
if player.dj not in members:
|
||||
for m in members:
|
||||
if not m.bot:
|
||||
player.dj = m
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
func.logger.error("Error occurred while checking the player!", exc_info=e)
|
||||
|
||||
@tasks.loop(hours=12.0)
|
||||
async def cache_cleaner(self):
|
||||
func.SETTINGS_BUFFER.clear()
|
||||
func.USERS_BUFFER.clear()
|
||||
|
||||
errorFile = func.gen_report()
|
||||
if errorFile:
|
||||
report_channel = self.bot.get_channel(func.report_channel_id)
|
||||
if report_channel:
|
||||
try:
|
||||
await report_channel.send(content=f"Report Before: <t:{round(datetime.timestamp(datetime.now()))}:F>", file=errorFile)
|
||||
except Exception as e:
|
||||
print(f"Report could not be sent (Reason: {e})")
|
||||
func.ERROR_LOGS.clear()
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(Task(bot))
|
||||
await bot.add_cog(Task(bot))
|
||||
@@ -9,24 +9,25 @@ services:
|
||||
- _JAVA_OPTIONS=-Xmx1G
|
||||
- SERVER_PORT=2333
|
||||
- LAVALINK_SERVER_PASSWORD=youshallnotpass
|
||||
healthcheck:
|
||||
test: 'curl -H "Authorization: youshallnotpass" -s http://localhost:2333/version'
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
volumes:
|
||||
## Use "./" if you want to create a mount from your current directory (where docker-compose.yml is located)
|
||||
## Having access to files INSIDE the container is complicated. Mount function is used to have access to certain container files or folders.
|
||||
## Read more: https://docs.docker.com/storage/bind-mounts/
|
||||
- ./application.yml:/opt/Lavalink/application.yml
|
||||
networks:
|
||||
- local
|
||||
expose:
|
||||
- "2333"
|
||||
|
||||
# # You can selfhost MongoDB. Just uncomment lines starting with single "#" below.
|
||||
# mongo:
|
||||
# image: mongo:latest
|
||||
# container_name: mongo
|
||||
# restart: unless-stopped
|
||||
# volumes:
|
||||
# # Use "./" if you want to create a mount from your current directory (where docker-compose.yml is located)
|
||||
# # Having access to files INSIDE the container is complicated. Mount function is used to have access to certain container files or folders.
|
||||
# # Read more: https://docs.docker.com/storage/bind-mounts/
|
||||
# - ./data/mongo/db:/data/db
|
||||
# - ./data/mongo/conf:/data/configdb
|
||||
# environment:
|
||||
@@ -39,21 +40,19 @@ services:
|
||||
# command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"]
|
||||
|
||||
vocard:
|
||||
image: vocard
|
||||
container_name: vocard
|
||||
volumes:
|
||||
## Use "./" if you want to create a mount from your current directory (where docker-compose.yml is located)
|
||||
## Having access to files INSIDE the container is complicated. Mount function is used to have access to certain container files or folders.
|
||||
## Read more: https://docs.docker.com/storage/bind-mounts/
|
||||
- ./settings.json:/app/settings.json
|
||||
build:
|
||||
dockerfile: ./Dockerfile
|
||||
volumes:
|
||||
- ./settings.json:/app/settings.json
|
||||
- ./.env:/app/.env
|
||||
- ./logs/supervisor/:/var/log
|
||||
depends_on:
|
||||
lavalink:
|
||||
condition: service_healthy
|
||||
condition: service_started
|
||||
# mongo:
|
||||
# condition: service_started
|
||||
ports:
|
||||
- "5555:5555"
|
||||
networks:
|
||||
- local
|
||||
|
||||
|
||||
75
function.py
75
function.py
@@ -1,11 +1,15 @@
|
||||
import discord, json, os, copy
|
||||
import discord, json, os, copy, logging
|
||||
|
||||
from discord.ext import commands
|
||||
from datetime import datetime
|
||||
from time import strptime
|
||||
from io import BytesIO
|
||||
from typing import Optional, Union, Dict, Any
|
||||
from addons import Settings, TOKENS
|
||||
from addons import Settings
|
||||
|
||||
from typing import (
|
||||
Optional,
|
||||
Union,
|
||||
Dict,
|
||||
Any
|
||||
)
|
||||
|
||||
from motor.motor_asyncio import (
|
||||
AsyncIOMotorClient,
|
||||
@@ -18,20 +22,19 @@ if not os.path.exists(os.path.join(ROOT_DIR, "settings.json")):
|
||||
raise Exception("Settings file not set!")
|
||||
|
||||
#--------------- Cache Var ---------------
|
||||
tokens: TOKENS = TOKENS()
|
||||
settings: Settings
|
||||
logger: logging.Logger = logging.getLogger("vocard")
|
||||
|
||||
MONGO_DB: AsyncIOMotorClient
|
||||
SETTINGS_DB: AsyncIOMotorCollection
|
||||
USERS_DB: AsyncIOMotorCollection
|
||||
|
||||
ERROR_LOGS: dict[int, dict[int, str]] = {} #Stores error that not a Voicelink Exception
|
||||
LANGS: dict[str, dict[str, str]] = {} #Stores all the languages in ./langs
|
||||
LOCAL_LANGS: dict[str, dict[str, str]] = {} #Stores all the localization languages in ./local_langs
|
||||
SETTINGS_BUFFER: dict[int, dict[str, Any]] = {} #Cache guild language
|
||||
USERS_BUFFER: dict[str, dict] = {}
|
||||
|
||||
USERS_BASE: dict[str, Any] = {
|
||||
USER_BASE: dict[str, Any] = {
|
||||
'playlist': {
|
||||
'200': {
|
||||
'tracks':[],
|
||||
@@ -75,16 +78,20 @@ def langs_setup() -> None:
|
||||
|
||||
return
|
||||
|
||||
def time(millis:int) -> str:
|
||||
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)
|
||||
def time(millis: int) -> str:
|
||||
seconds = (millis // 1000) % 60
|
||||
minutes = (millis // (1000 * 60)) % 60
|
||||
hours = (millis // (1000 * 60 * 60)) % 24
|
||||
days = millis // (1000 * 60 * 60 * 24)
|
||||
|
||||
if days > 0:
|
||||
return "%d days, %02d:%02d:%02d" % (days, hours, minutes, seconds)
|
||||
elif hours > 0:
|
||||
return "%d:%02d:%02d" % (hours, minutes, seconds)
|
||||
else:
|
||||
return "%02d:%02d" % (minutes, seconds)
|
||||
|
||||
def formatTime(number:str) -> Optional[int]:
|
||||
def format_time(number:str) -> int:
|
||||
try:
|
||||
try:
|
||||
num = strptime(number, '%M:%S')
|
||||
@@ -94,7 +101,7 @@ def formatTime(number:str) -> Optional[int]:
|
||||
except ValueError:
|
||||
num = strptime(number, '%H:%M:%S')
|
||||
except:
|
||||
return None
|
||||
return 0
|
||||
|
||||
return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000
|
||||
|
||||
@@ -102,21 +109,6 @@ def get_source(source: str, type: str) -> str:
|
||||
source_settings: dict = settings.sources_settings.get(source.lower(), {})
|
||||
return source_settings.get(type, ("🔗" if type == "emoji" else settings.embed_color))
|
||||
|
||||
def gen_report() -> Optional[discord.File]:
|
||||
if ERROR_LOGS:
|
||||
errorText = ""
|
||||
for guild_id, error in ERROR_LOGS.items():
|
||||
errorText += f"Guild ID: {guild_id}\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'))
|
||||
file = discord.File(buffer, filename='report.txt')
|
||||
buffer.close()
|
||||
|
||||
return file
|
||||
return None
|
||||
|
||||
def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]:
|
||||
if ctx.author.id in settings.bot_access_user:
|
||||
return None
|
||||
@@ -144,6 +136,13 @@ def get_lang_non_async(guild_id: int, *keys) -> Union[list[str], str]:
|
||||
return LANGS.get(lang, {}).get(keys[0], "Language pack not found!")
|
||||
return [LANGS.get(lang, {}).get(key, "Language pack not found!") for key in keys]
|
||||
|
||||
def format_bytes(bytes: int, unit: bool = False):
|
||||
if bytes <= 1_000_000_000:
|
||||
return f"{bytes / (1024 ** 2):.1f}" + ("MB" if unit else "")
|
||||
|
||||
else:
|
||||
return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "")
|
||||
|
||||
async def get_lang(guild_id:int, *keys) -> Union[list[str], str]:
|
||||
settings = await get_settings(guild_id)
|
||||
lang = settings.get("lang", "EN")
|
||||
@@ -158,7 +157,15 @@ async def send(ctx: Union[commands.Context, discord.Interaction], key: str, *par
|
||||
text = await get_lang(ctx.guild.id, key)
|
||||
text = text.format(*params)
|
||||
|
||||
send_func = ctx.send if isinstance(ctx, commands.Context) else (ctx.followup.send if ctx.response.is_done() else ctx.response.send_message)
|
||||
if isinstance(ctx, commands.Context):
|
||||
send_func = ctx.send
|
||||
else:
|
||||
if not ctx.response.is_done():
|
||||
send_func = ctx.response.send_message
|
||||
|
||||
else:
|
||||
return await ctx.followup.send(text, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS)
|
||||
|
||||
return await send_func(text, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS)
|
||||
|
||||
async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool:
|
||||
@@ -225,13 +232,13 @@ async def get_user(user_id: int, d_type: Optional[str] = None, need_copy: bool =
|
||||
if not user:
|
||||
user = await USERS_DB.find_one({"_id": user_id})
|
||||
if not user:
|
||||
user = {"_id": user_id, **USERS_BASE}
|
||||
user = {"_id": user_id, **USER_BASE}
|
||||
await USERS_DB.insert_one(user)
|
||||
|
||||
USERS_BUFFER[user_id] = user
|
||||
|
||||
if d_type:
|
||||
user = user.setdefault(d_type, copy.deepcopy(USERS_BASE.get(d_type)))
|
||||
user = user.setdefault(d_type, copy.deepcopy(USER_BASE.get(d_type)))
|
||||
|
||||
return copy.deepcopy(user) if need_copy else user
|
||||
|
||||
|
||||
1
ipc/__init__.py
Normal file
1
ipc/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .client import IPCClient
|
||||
110
ipc/client.py
Normal file
110
ipc/client.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import function as func
|
||||
|
||||
from discord.ext import commands
|
||||
from typing import Optional
|
||||
|
||||
from .methods import process_methods
|
||||
|
||||
class IPCClient:
|
||||
def __init__(
|
||||
self,
|
||||
bot: commands.Bot,
|
||||
host: str,
|
||||
port: int,
|
||||
password: str,
|
||||
heartbeat: int = 30,
|
||||
secure: bool = False,
|
||||
*arg,
|
||||
**kwargs
|
||||
) -> None:
|
||||
|
||||
self._bot: commands.Bot = bot
|
||||
self._host: str = host
|
||||
self._port: int = port
|
||||
self._password: str = password
|
||||
self._heartbeat: int = heartbeat
|
||||
self._is_secure: bool = secure
|
||||
self._is_connected: bool = False
|
||||
self._is_connecting: bool = False
|
||||
self._logger: logging.Logger = logging.getLogger("ipc_client")
|
||||
|
||||
self._websocket_url: str = f"{'wss' if self._is_secure else 'ws'}://{self._host}:{self._port}/ws_bot"
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
||||
self._heanders = {
|
||||
"Authorization": self._password,
|
||||
"User-Id": str(bot.user.id),
|
||||
"Client-Version": func.settings.version
|
||||
}
|
||||
|
||||
async def _listen(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
msg = await self._websocket.receive()
|
||||
self._logger.debug(f"Received Message: {msg}")
|
||||
except:
|
||||
break
|
||||
|
||||
if msg.type in [aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED]:
|
||||
self._is_connected = False
|
||||
self._logger.info("Connection closed. Trying to reconnect in 10s.")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
if not self._is_connected:
|
||||
try:
|
||||
await self.connect()
|
||||
except Exception as e:
|
||||
self._logger.error("Reconnection failed.")
|
||||
else:
|
||||
self._bot.loop.create_task(process_methods(self, self._bot, msg.json()))
|
||||
|
||||
async def send(self, data: dict):
|
||||
if self.is_connected:
|
||||
self._logger.debug(f"Send Message: {data}")
|
||||
await self._websocket.send_json(data)
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
if not self._session:
|
||||
self._session = aiohttp.ClientSession()
|
||||
|
||||
if self._is_connecting or self._is_connected:
|
||||
return
|
||||
|
||||
self._is_connecting = True
|
||||
self._websocket = await self._session.ws_connect(
|
||||
self._websocket_url, headers=self._heanders, heartbeat=self._heartbeat
|
||||
)
|
||||
|
||||
self._task = self._bot.loop.create_task(self._listen())
|
||||
self._is_connected = True
|
||||
|
||||
self._logger.info("Connected to dashboard!")
|
||||
|
||||
except aiohttp.ClientConnectorError:
|
||||
raise Exception("Connection failed.")
|
||||
|
||||
except aiohttp.WSServerHandshakeError as e:
|
||||
self._logger.error("Access forbidden: Missing bot ID, version mismatch, or invalid password.")
|
||||
|
||||
except Exception as e:
|
||||
self._logger.error("Error occurred while connecting to dashboard.", exc_info=e)
|
||||
|
||||
finally:
|
||||
self._is_connecting = False
|
||||
|
||||
return self
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
self._is_connected = False
|
||||
self._task.cancel()
|
||||
self._logger.info("Disconnected to dashboard!")
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._is_connected
|
||||
787
ipc/methods.py
Normal file
787
ipc/methods.py
Normal file
@@ -0,0 +1,787 @@
|
||||
import time, re
|
||||
import function as func
|
||||
|
||||
from typing import List, Dict, Union, Optional
|
||||
|
||||
from discord import User, Member, VoiceChannel
|
||||
from discord.ext import commands
|
||||
from voicelink import Player, Track, Playlist, NodePool, decode, LoopType, Filters
|
||||
from addons import lyricsPlatform
|
||||
|
||||
RATELIMIT_COUNTER: Dict[int, Dict[str, float]] = {}
|
||||
SCOPES = {
|
||||
"prefix": str,
|
||||
"lang": str,
|
||||
"queueType": str,
|
||||
"dj": int,
|
||||
"controller": bool,
|
||||
"24/7": bool,
|
||||
"votedisable": bool,
|
||||
"duplicateTrack": bool,
|
||||
"default_controller": dict,
|
||||
"stage_announce_template": str
|
||||
}
|
||||
|
||||
class TempCtx():
|
||||
def __init__(self, author: Member, channel: VoiceChannel) -> None:
|
||||
self.author = author
|
||||
self.channel = channel
|
||||
self.guild = channel.guild
|
||||
|
||||
class SystemMethod:
|
||||
def __init__(self, function: callable, *, credit: int = 1):
|
||||
self.function: callable = function
|
||||
self.params: List[str] = ["bot", "data"]
|
||||
self.credit: int = credit
|
||||
|
||||
class PlayerMethod(SystemMethod):
|
||||
def __init__(self, function, *, credit: int = 1, auto_connect: bool = False):
|
||||
super().__init__(function, credit=credit)
|
||||
self.params: List[str] = ["player", "member", "data"]
|
||||
self.auto_connect: bool = auto_connect
|
||||
|
||||
def require_permission(only_admin: bool = False):
|
||||
def decorator(func) -> callable:
|
||||
async def wrapper(player: Player, member: Member, dict: Dict) -> Optional[Dict]:
|
||||
if only_admin and not member.guild_permissions.manage_guild:
|
||||
return error_msg("Only the admins may use this funciton!", user_id=member.id)
|
||||
if not player.is_privileged(member):
|
||||
return error_msg("Only the DJ or admins may use this funciton!", user_id=member.id)
|
||||
return await func(player, member, dict)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def error_msg(msg: str, *, user_id: int = None, guild_id: int = None, level: str = "info") -> Dict:
|
||||
payload = {"op": "errorMsg", "level": level, "msg": msg}
|
||||
if user_id:
|
||||
payload["user_id"] = str(user_id)
|
||||
if guild_id:
|
||||
payload["guild_id"] = str(guild_id)
|
||||
|
||||
return payload
|
||||
|
||||
async def connect_channel(member: Member, bot: commands.Bot) -> Player:
|
||||
if not member.voice:
|
||||
return
|
||||
|
||||
channel = member.voice.channel
|
||||
try:
|
||||
settings = await func.get_settings(channel.guild.id)
|
||||
player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings))
|
||||
await player.send_ws({"op": "createPlayer", "member_ids": [str(member.id) for member in channel.members]})
|
||||
return player
|
||||
except:
|
||||
return
|
||||
|
||||
async def initBot(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("user_id"))
|
||||
user = bot.get_user(user_id)
|
||||
if not user:
|
||||
user = await bot.fetch_user(user_id)
|
||||
|
||||
if user:
|
||||
return {
|
||||
"op": "initBot",
|
||||
"user_id": str(user_id),
|
||||
"bot_name": bot.user.display_name,
|
||||
"bot_avatar": bot.user.display_avatar.url,
|
||||
"bot_id": str(bot.user.id)
|
||||
}
|
||||
|
||||
async def initUser(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("user_id"))
|
||||
data = await func.get_user(user_id)
|
||||
|
||||
for mail in data.get("inbox"):
|
||||
sender = bot.get_user(mail.get("sender"))
|
||||
if not sender:
|
||||
sender = await bot.fetch_user(mail.get("sender"))
|
||||
|
||||
if not sender:
|
||||
data.get("inbox").remove(mail)
|
||||
|
||||
mail["sender"] = {"avatar_url": sender.display_avatar.url, "name": sender.display_name, "id": str(sender.id)}
|
||||
|
||||
return {
|
||||
"op": "initUser",
|
||||
"user_id": str(user_id),
|
||||
"data": data
|
||||
}
|
||||
|
||||
async def initPlayer(player: Player, member: Member, data: Dict) -> Dict:
|
||||
player._ipc_connection = True
|
||||
available_filters = []
|
||||
for name, filter_cls in Filters.get_available_filters().items():
|
||||
filter = filter_cls()
|
||||
available_filters.append({"tag": name, "scope": filter.scope, "payload": filter.payload})
|
||||
|
||||
return {
|
||||
"op": "initPlayer",
|
||||
"guild_id": str(player.guild.id),
|
||||
"user_id": str(data.get("user_id")),
|
||||
"users": [{
|
||||
"user_id": str(member.id),
|
||||
"avatar_url": member.display_avatar.url,
|
||||
"name": member.name
|
||||
} for member in player.channel.members ],
|
||||
"tracks": [ {"track_id": track.track_id, "requester_id": str(track.requester.id)} for track in player.queue._queue ],
|
||||
"repeat_mode": player.queue.repeat.lower(),
|
||||
"channel_name": player.channel.name,
|
||||
"current_queue_position": player.queue._position + (0 if player.is_playing else 1),
|
||||
"current_position": 0 or player.position if player.is_playing else 0,
|
||||
"is_playing": player.is_playing,
|
||||
"is_paused": player.is_paused,
|
||||
"is_dj": player.is_privileged(member, check_user_join=False),
|
||||
"autoplay": player.settings.get("autoplay", False),
|
||||
"volume": player.volume,
|
||||
"filters": [{"tag": filter.tag, "scope": filter.scope, "payload": filter.payload} for filter in player.filters.get_filters()],
|
||||
"available_filters": available_filters
|
||||
}
|
||||
|
||||
async def closeConnection(bot: commands.Bot, data: Dict) -> None:
|
||||
guild_id = int(data.get("guild_id"))
|
||||
guild = bot.get_guild(guild_id)
|
||||
player: Player = guild.voice_client
|
||||
if player:
|
||||
player._ipc_connection = False
|
||||
|
||||
async def getRecommendation(bot: commands.Bot, data: Dict) -> None:
|
||||
node = NodePool.get_node()
|
||||
if not node:
|
||||
return
|
||||
|
||||
track_data = decode(track_id := data.get("track_id"))
|
||||
track = Track(track_id=track_id, info=track_data, requester=bot.user)
|
||||
tracks: List[Track] = await node.get_recommendations(track, limit=60)
|
||||
|
||||
return {
|
||||
"op": "getRecommendation",
|
||||
"user_id": str(data.get("user_id")),
|
||||
"callback": data.get("callback"),
|
||||
"tracks": [track.track_id for track in tracks] if tracks else []
|
||||
}
|
||||
|
||||
async def skipTo(player: Player, member: Member, data: Dict) -> None:
|
||||
if not player.is_privileged(member):
|
||||
if player.current and member == player.current.requester:
|
||||
pass
|
||||
|
||||
elif member in player.skip_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
|
||||
else:
|
||||
player.skip_votes.add(member)
|
||||
if len(player.skip_votes) < (required := player.required()):
|
||||
return error_msg(player.get_msg('skipVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
index = data.get("index", 1)
|
||||
if index > 1:
|
||||
player.queue.skipto(index)
|
||||
|
||||
if player.queue._repeat.mode == LoopType.track:
|
||||
await player.set_repeat(LoopType.off.name)
|
||||
await player.stop()
|
||||
|
||||
async def backTo(player: Player, member: Member, data: Dict) -> None:
|
||||
if not player.is_privileged(member):
|
||||
if player.current and member == player.current.requester:
|
||||
pass
|
||||
|
||||
elif member in player.skip_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
|
||||
else:
|
||||
player.skip_votes.add(member)
|
||||
if len(player.skip_votes) < (required := player.required()):
|
||||
return error_msg(player.get_msg('backVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
index = data.get("index", 1)
|
||||
if not player.is_playing:
|
||||
player.queue.backto(index)
|
||||
await player.do_next()
|
||||
else:
|
||||
player.queue.backto(index + 1)
|
||||
await player.stop()
|
||||
|
||||
@require_permission()
|
||||
async def moveTrack(player: Player, member: Member, data: Dict) -> None:
|
||||
index = data.get("index")
|
||||
new_index = data.get("newIndex")
|
||||
if index == new_index:
|
||||
return
|
||||
|
||||
await player.move_track(index, new_index, member)
|
||||
|
||||
async def addTracks(player: Player, member: Member, data: Dict) -> None:
|
||||
_type = data.get("type", "addToQueue")
|
||||
tracks = [Track(
|
||||
track_id=track_id,
|
||||
info=decode(track_id),
|
||||
requester=member
|
||||
) for track_id in data.get("tracks", [])]
|
||||
|
||||
if _type == "addToQueue":
|
||||
await player.add_track(tracks)
|
||||
|
||||
elif _type == "forcePlay":
|
||||
await player.add_track(tracks, at_front=True)
|
||||
if player.is_playing:
|
||||
return await player.stop()
|
||||
|
||||
elif _type == "addNext":
|
||||
await player.add_track(tracks, at_front=True)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
async def getTracks(bot: commands.Bot, data: Dict) -> Dict:
|
||||
query = data.get("query", None)
|
||||
|
||||
if query:
|
||||
payload = {"op": "getTracks", "user_id": data.get("user_id"), "callback": data.get("callback")}
|
||||
tracks = await NodePool.get_node().get_tracks(query=query, requester=None)
|
||||
if not tracks:
|
||||
return payload
|
||||
|
||||
payload["tracks"] = [ track.track_id for track in (tracks.tracks if isinstance(tracks, Playlist) else tracks ) ]
|
||||
return payload
|
||||
|
||||
async def shuffleTrack(player: Player, member: Member, data: Dict) -> None:
|
||||
if not player.is_privileged(member):
|
||||
if member in player.shuffle_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
|
||||
player.shuffle_votes.add(member)
|
||||
if len(player.shuffle_votes) < (required := player.required()):
|
||||
return error_msg(player.get_msg('shuffleVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
await player.shuffle(data.get("type", "queue"), member)
|
||||
|
||||
@require_permission()
|
||||
async def repeatTrack(player: Player, member: Member, data: Dict) -> None:
|
||||
await player.set_repeat(requester=member)
|
||||
|
||||
@require_permission()
|
||||
async def removeTrack(player: Player, member: Member, data: Dict) -> None:
|
||||
index, index2 = data.get("index"), data.get("index2")
|
||||
await player.remove_track(index, index2, requester=member)
|
||||
|
||||
@require_permission()
|
||||
async def clearQueue(player: Player, member: Member, data: Dict) -> None:
|
||||
queue_type = data.get("queue_type", "").lower()
|
||||
await player.clear_queue(queue_type, member)
|
||||
|
||||
@require_permission(only_admin=True)
|
||||
async def updateVolume(player: Player, member: Member, data: Dict) -> None:
|
||||
volume = data.get("volume", 100)
|
||||
await func.update_settings(player.guild.id, {"$set": {"volume": volume}})
|
||||
await player.set_volume(volume=volume, requester=member)
|
||||
|
||||
async def updatePause(player: Player, member: Member, data: Dict) -> None:
|
||||
pause = data.get("pause", True)
|
||||
if not player.is_privileged(member):
|
||||
if pause:
|
||||
if member in player.pause_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
|
||||
player.pause_votes.add(member)
|
||||
if len(player.pause_votes) < (required := player.required()):
|
||||
return error_msg(player.get_msg('pauseVote').format(member, len(player.pause_votes), required), guild_id=player.guild.id)
|
||||
|
||||
else:
|
||||
if member in player.resume_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
|
||||
player.resume_votes.add(member)
|
||||
if len(player.resume_votes) < (required := player.required()):
|
||||
return error_msg(player.get_msg('resumeVote').format(member, len(player.resume_votes), required), guild_id=player.guild.id)
|
||||
|
||||
await player.set_pause(pause, member)
|
||||
|
||||
@require_permission()
|
||||
async def updatePosition(player: Player, member: Member, data: Dict) -> None:
|
||||
position = data.get("position");
|
||||
await player.seek(position, member);
|
||||
|
||||
async def toggleAutoplay(player: Player, member: Member, data: Dict) -> Dict:
|
||||
if not player.is_privileged(member):
|
||||
return error_msg(player.get_msg('missingPerms_autoplay'))
|
||||
|
||||
check = data.get("status", False)
|
||||
player.settings['autoplay'] = check
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
return {
|
||||
"op": "toggleAutoplay",
|
||||
"status": check,
|
||||
"guild_id": player.guild.id,
|
||||
"requester_id": str(member.id)
|
||||
}
|
||||
|
||||
@require_permission()
|
||||
async def updateFilter(player: Player, member: Member, data: Dict) -> None:
|
||||
updateType = data.get("type", "add")
|
||||
filter_tag = data.get("tag")
|
||||
|
||||
if updateType == "add":
|
||||
available_filters = Filters.get_available_filters()
|
||||
filter_cls = available_filters.get(filter_tag)
|
||||
if not filter_cls:
|
||||
return
|
||||
|
||||
payload = {}
|
||||
if "payload" in data:
|
||||
payload = data.get("payload").get(list(data.get("payload").keys()[0]), {})
|
||||
if player.filters.has_filter(filter_tag=filter_tag):
|
||||
player.filters.remove_filter(filter_tag=filter_tag)
|
||||
await player.add_filter(filter=filter_cls(**payload), requester=member)
|
||||
|
||||
elif updateType == "remove":
|
||||
await player.remove_filter(filter_tag=filter_tag, requester=member)
|
||||
|
||||
else:
|
||||
await player.reset_filter(requester=member)
|
||||
|
||||
async def _loadPlaylist(playlist: Dict) -> Optional[List[Track]]:
|
||||
if playlist.get("type") == "link":
|
||||
tracks: List[Track]= await NodePool.get_node().get_tracks(playlist.get("uri"), requester=None)
|
||||
if tracks:
|
||||
return [track.track_id for track in (tracks.tracks if isinstance(tracks, Playlist) else tracks)]
|
||||
else:
|
||||
return playlist.get("tracks", [])
|
||||
|
||||
def _assign_playlist_id(existed: list) -> str:
|
||||
for i in range(200, 210):
|
||||
if str(i) not in existed:
|
||||
return str(i)
|
||||
|
||||
async def _getPlaylist(user_id: int, playlist_id: str) -> Dict:
|
||||
playlists = await func.get_user(user_id, "playlist")
|
||||
playlist = playlists.get(playlist_id)
|
||||
if not playlist:
|
||||
return
|
||||
|
||||
if playlist["type"] == "share":
|
||||
target_user = await func.get_user(playlist["user"], "playlist")
|
||||
target_playlist = target_user.get(playlist["referId"])
|
||||
if target_playlist and user_id in target_playlist.get("perms", {}).get("read", []):
|
||||
playlist["tracks"] = await _loadPlaylist(target_playlist)
|
||||
else:
|
||||
playlist["tracks"] = await _loadPlaylist(playlist)
|
||||
|
||||
return playlist
|
||||
|
||||
async def getPlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("user_id"))
|
||||
playlist_id = str(data.get("playlist_id"))
|
||||
|
||||
payload = {"op": "loadPlaylist", "playlist_id": playlist_id, "user_id": str(user_id)}
|
||||
playlist = await _getPlaylist(user_id, playlist_id)
|
||||
payload["tracks"] = playlist["tracks"] if playlist else []
|
||||
|
||||
return payload
|
||||
|
||||
async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("user_id"))
|
||||
playlist_id = str(data.get("playlist_id"))
|
||||
_type = data.get("type")
|
||||
|
||||
rank, max_p, max_t = func.check_roles()
|
||||
if _type == "createPlaylist":
|
||||
name, playlist_url = data.get("name"), data.get("playlist_url")
|
||||
if not name:
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"You must enter name for this field!",
|
||||
"field": "create-playlist-name",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
playlist = await func.get_user(user_id, "playlist")
|
||||
if len(list(playlist.keys())) >= max_p:
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"You cannot create more than '{max_p}' playlists!",
|
||||
"field": "create-playlist-name",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
for playlist_data in playlist.values():
|
||||
if playlist_data['name'].lower() == name.lower():
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"Playlist '{name}' already exists.",
|
||||
"field": "create-playlist-name",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
if playlist_url:
|
||||
tracks = await NodePool.get_node().get_tracks(playlist_url, requester=None)
|
||||
if not isinstance(tracks, Playlist):
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"Please enter a valid link or public spotify or youtube playlist link.",
|
||||
"field": "create-playlist-url",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
assgined_playlist_id = _assign_playlist_id(list(playlist.keys()))
|
||||
data = {'uri': playlist_url, 'perms': {'read': []}, 'name': name, 'type': 'link'} if playlist_url else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
|
||||
await func.update_user(user_id, {"$set": {f"playlist.{assgined_playlist_id}": data}})
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "created",
|
||||
"playlist_id": assgined_playlist_id,
|
||||
"msg": f"You have created '{name}' playlist.",
|
||||
"user_id": str(user_id),
|
||||
"data": data
|
||||
}
|
||||
|
||||
elif _type == "removePlaylist":
|
||||
playlist = await _getPlaylist(user_id, playlist_id)
|
||||
if playlist:
|
||||
if playlist['type'] == 'share':
|
||||
await func.update_user(playlist['user'], {"$pull": {f"playlist.{playlist['referId']}.perms.read": user_id}})
|
||||
|
||||
await func.update_user(user_id, {"$unset": {f"playlist.{playlist_id}": 1}})
|
||||
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "deleted",
|
||||
"playlist_id": playlist_id,
|
||||
"msg": f"You have removed playlist '{playlist['name']}'",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
elif _type == "renamePlaylist":
|
||||
name = data.get("name")
|
||||
if not name:
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"You must enter name for this field!",
|
||||
"field": "rename-playlist-name",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
playlist = await func.get_user(user_id, "playlist")
|
||||
for data in playlist.values():
|
||||
if data['name'].lower() == name.lower():
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"Playlist '{data['name']}' already exists.",
|
||||
"field": "rename-playlist-name",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
await func.update_user(user_id, {"$set": {f'playlist.{playlist_id}.name': name}})
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "renamed",
|
||||
"name": name,
|
||||
"playlist_id": playlist_id,
|
||||
"msg": f"You have renamed the playlist to '{name}'.",
|
||||
"field": "rename-playlist-name",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
elif _type == "addTrack":
|
||||
track_id = data.get("track_id")
|
||||
if not track_id:
|
||||
return error_msg("No track ID could be located.", user_id=user_id, level='error')
|
||||
|
||||
playlist = await _getPlaylist(user_id, playlist_id)
|
||||
if playlist['type'] in ['share', 'link']:
|
||||
return error_msg("You cannot add songs to a linked playlist through Vocard.", user_id=user_id, level='error')
|
||||
|
||||
rank, max_p, max_t = func.check_roles()
|
||||
if len(playlist['tracks']) >= max_t:
|
||||
return error_msg(f"You have reached the limit! You can only add {max_t} songs to your playlist.", user_id=user_id)
|
||||
|
||||
decoded_track = Track(track_id=track_id, info=decode(track_id), requester=None)
|
||||
if decoded_track.is_stream:
|
||||
return error_msg("You are not allowed to add streaming videos to your playlist.", user_id=user_id)
|
||||
|
||||
await func.update_user(user_id, {"$push": {f'playlist.{playlist_id}.tracks': track_id}})
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "addTrack",
|
||||
"playlist_id": playlist_id,
|
||||
"track_id": track_id,
|
||||
"msg": f"Added {decoded_track.title} into '{playlist['name']}' playlist.",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
elif _type == "removeTrack":
|
||||
track_id, track_position = data.get("track_id"), data.get("track_position", 0)
|
||||
if not track_id:
|
||||
return error_msg("No track ID could be located.", user_id=user_id, level='error')
|
||||
|
||||
playlist = await _getPlaylist(user_id, playlist_id)
|
||||
if playlist['type'] in ['share', 'link']:
|
||||
return error_msg("You cannot remove songs from a linked playlist through Vocard.", user_id=user_id, level='error')
|
||||
|
||||
if not 0 <= track_position < len(playlist['tracks']):
|
||||
return error_msg("Cannot find the position from your playlist.", user_id=user_id, level="error")
|
||||
|
||||
if playlist['tracks'][track_position] != track_id:
|
||||
return error_msg("Something wrong while removing the track from your playlist.", user_id=user_id, level='error')
|
||||
|
||||
await func.update_user(user_id, {"$pull": {f'playlist.{playlist_id}.tracks': playlist['tracks'][track_position]}})
|
||||
|
||||
decoded_track = decode(playlist['tracks'][track_position])
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "removeTrack",
|
||||
"playlist_id": playlist_id,
|
||||
"track_position": track_position,
|
||||
"track_id": track_id,
|
||||
"msg": f"Removed '{decoded_track['title']}' from '{playlist['name']}' playlist.",
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
elif _type == "updateInbox":
|
||||
user = await func.get_user(user_id)
|
||||
is_accpet = data.get("accept", False)
|
||||
|
||||
if is_accpet and len(list(user.get("playlist").keys())) >= max_p:
|
||||
return error_msg(f"You cannot create more than '{max_p}' playlists!", user_id=user_id, level = "error")
|
||||
|
||||
info = data.get("refer_id", "").split("-")
|
||||
sender_id, refer_id = info[0], info[1]
|
||||
inbox = user.get("inbox")
|
||||
|
||||
payload = {"op": "updatePlaylist", "status": "updateInbox", "user_id": str(user_id), "accpet": is_accpet, "sender_id": sender_id, "refer_id": refer_id}
|
||||
for index, mail in enumerate(inbox.copy()):
|
||||
if not (str(mail.get("sender")) == sender_id and mail.get("referId") == refer_id):
|
||||
continue
|
||||
|
||||
del inbox[index]
|
||||
if is_accpet:
|
||||
share_playlists = await func.get_user(mail["sender"], "playlist")
|
||||
if refer_id not in share_playlists:
|
||||
return error_msg("The shared playlist couldn’t be found. It’s possible that the user has already deleted it.", user_id=user_id)
|
||||
|
||||
assgined_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys()))
|
||||
playlist_name = f"Share{time.strftime('%M%S', time.gmtime(int(mail['time'])))}"
|
||||
share_playlist = share_playlists.get(refer_id)
|
||||
share_playlist.update({
|
||||
"name": playlist_name,
|
||||
"type": "share"
|
||||
})
|
||||
await func.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}})
|
||||
await func.update_user(user_id, {"$set": {
|
||||
f'playlist.{assgined_playlist_id}': {
|
||||
'user': mail['sender'], 'referId': mail['referId'],
|
||||
'name': playlist_name,
|
||||
'type': 'share'
|
||||
},
|
||||
"inbox": inbox
|
||||
}})
|
||||
|
||||
payload.update({
|
||||
"playlist_id": assgined_playlist_id,
|
||||
"msg": f"You have created '{playlist_name}' playlist.",
|
||||
"data": share_playlist,
|
||||
})
|
||||
|
||||
await func.update_user(user_id, {"$set": {"inbox": inbox}})
|
||||
return payload
|
||||
|
||||
async def getMutualGuilds(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("user_id"))
|
||||
|
||||
payload = {"op": "getMutualGuilds", "mutualGuilds": {}, "inviteGuilds": {}, "user_id": str(user_id)}
|
||||
for guild_id, guild_info in data.get("guilds", {}).items():
|
||||
if guild := bot.get_guild(int(guild_id)):
|
||||
payload["mutualGuilds"][guild_id] = {
|
||||
**guild_info,
|
||||
"member_count": guild.member_count
|
||||
}
|
||||
else:
|
||||
payload["inviteGuilds"][guild_id] = {**guild_info}
|
||||
|
||||
return payload
|
||||
|
||||
async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("user_id"))
|
||||
guild_id = int(data.get("guild_id"))
|
||||
|
||||
guild = bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return error_msg("Vocard don't have access to requested guild.", user_id=user_id, level="error")
|
||||
|
||||
member = guild.get_member(user_id)
|
||||
if not member:
|
||||
return error_msg("You are not in the requested guild.", user_id=user_id, level="error")
|
||||
|
||||
if not member.guild_permissions.manage_guild:
|
||||
return error_msg("You don't have permission to access the settings.", user_id=user_id, level='error')
|
||||
|
||||
settings = await func.get_settings(guild_id)
|
||||
if "dj" in settings:
|
||||
role = guild.get_role(settings["dj"])
|
||||
if role:
|
||||
settings["dj"] = role.name
|
||||
|
||||
return {
|
||||
"op": "getSettings",
|
||||
"settings": settings,
|
||||
"options": {
|
||||
"languages": list(func.LANGS.keys()),
|
||||
"queue_modes": ["Queue", "FairQueue"],
|
||||
"roles": [role.name for role in guild.roles]
|
||||
},
|
||||
"guild": {
|
||||
"avatar": guild.icon.url if guild.icon else None,
|
||||
"name": guild.name,
|
||||
"id": str(guild_id)
|
||||
},
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
|
||||
async def getLyrics(bot: commands.Bot, data: Dict) -> Dict:
|
||||
title, artist, platform = data.get("title", ""), data.get("artist", ""), data.get("platform", "")
|
||||
if not platform or platform not in lyricsPlatform:
|
||||
platform = func.settings.lyrics_platform
|
||||
|
||||
song: dict[str, str] = await lyricsPlatform.get(platform)().get_lyrics(title, artist)
|
||||
payload = {
|
||||
"op": "getLyrics",
|
||||
"user_id": data.get("user_id"),
|
||||
"lyrics": {_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()} if song else {},
|
||||
"callback": data.get("callback")
|
||||
}
|
||||
|
||||
return payload
|
||||
|
||||
async def updateSettings(bot: commands.Bot, data: Dict) -> None:
|
||||
user_id = int(data.get("user_id"))
|
||||
guild_id = int(data.get("guild_id"))
|
||||
|
||||
guild = bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return error_msg("Vocard don't have access to required guild.", user_id=user_id, level="error")
|
||||
|
||||
member = guild.get_member(user_id)
|
||||
if not member:
|
||||
return error_msg("You are not in the required guild.", user_id=user_id, level="error")
|
||||
|
||||
if not member.guild_permissions.manage_guild:
|
||||
return error_msg("You don't have permission to change the settings.", user_id=user_id, level='error')
|
||||
|
||||
data = data.get("settings", {})
|
||||
if "dj" in data:
|
||||
for role in guild.roles:
|
||||
if role.name.lower() == data["dj"]:
|
||||
data["dj"] = role.id
|
||||
break
|
||||
|
||||
for key, value in data.copy().items():
|
||||
if key not in SCOPES or not isinstance(value, SCOPES[key]):
|
||||
del data[key]
|
||||
|
||||
await func.update_settings(guild.id, {"$set": data})
|
||||
|
||||
METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = {
|
||||
"initBot": SystemMethod(initBot, credit=0),
|
||||
"initUser": SystemMethod(initUser, credit=2),
|
||||
"getPlaylist": SystemMethod(getPlaylist),
|
||||
"updatePlaylist": SystemMethod(updatePlaylist, credit=2),
|
||||
"getMutualGuilds": SystemMethod(getMutualGuilds),
|
||||
"getSettings": SystemMethod(getSettings),
|
||||
"getLyrics": SystemMethod(getLyrics),
|
||||
"updateSettings": SystemMethod(updateSettings),
|
||||
"getRecommendation": SystemMethod(getRecommendation, credit=5),
|
||||
"closeConnection": SystemMethod(closeConnection, credit=0),
|
||||
"getTracks": SystemMethod(getTracks, credit=5),
|
||||
"initPlayer": PlayerMethod(initPlayer),
|
||||
"skipTo": PlayerMethod(skipTo),
|
||||
"backTo": PlayerMethod(backTo),
|
||||
"moveTrack": PlayerMethod(moveTrack),
|
||||
"addTracks": PlayerMethod(addTracks, auto_connect=True),
|
||||
"shuffleTrack": PlayerMethod(shuffleTrack, credit=3),
|
||||
"repeatTrack": PlayerMethod(repeatTrack),
|
||||
"removeTrack": PlayerMethod(removeTrack),
|
||||
"clearQueue": PlayerMethod(clearQueue),
|
||||
"updateVolume": PlayerMethod(updateVolume, credit=2),
|
||||
"updatePause": PlayerMethod(updatePause),
|
||||
"updatePosition": PlayerMethod(updatePosition),
|
||||
"toggleAutoplay": PlayerMethod(toggleAutoplay),
|
||||
"updateFilter": PlayerMethod(updateFilter),
|
||||
}
|
||||
|
||||
async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None:
|
||||
op: str = data.get("op", "")
|
||||
method = METHODS.get(op)
|
||||
if not method or not (user_id := data.get("user_id")):
|
||||
return
|
||||
|
||||
user_id = int(user_id)
|
||||
if user_id not in RATELIMIT_COUNTER or (time.time() - RATELIMIT_COUNTER[user_id]["time"]) >= 300:
|
||||
RATELIMIT_COUNTER[user_id] = {"time": time.time(), "count": 0}
|
||||
|
||||
else:
|
||||
if RATELIMIT_COUNTER[user_id]["count"] >= 100:
|
||||
return await ipc_client.send({"op": "rateLimited", "user_id": str(user_id)})
|
||||
RATELIMIT_COUNTER[user_id]["count"] += method.credit
|
||||
|
||||
try:
|
||||
env: Dict = {"bot": bot, "data": data}
|
||||
args: List = []
|
||||
|
||||
params = method.params
|
||||
if not (type(method) == SystemMethod):
|
||||
if guild_id := data.get("guild_id"):
|
||||
if (guild := bot.get_guild(int(guild_id))):
|
||||
env["guild"] = guild
|
||||
|
||||
else:
|
||||
user: User = bot.get_user(user_id)
|
||||
if not user:
|
||||
return
|
||||
|
||||
for guild in user.mutual_guilds:
|
||||
member = guild.get_member(user_id)
|
||||
if member.voice and member.voice.channel:
|
||||
env["guild"] = guild
|
||||
env["member"] = member
|
||||
break
|
||||
|
||||
if "member" in params and "member" not in env:
|
||||
if not (guild := env.get("guild")) or not (member := guild.get_member(user_id)):
|
||||
return
|
||||
env["member"] = member
|
||||
|
||||
if "player" in params:
|
||||
if not (guild := env.get("guild")) or not (player := guild.voice_client):
|
||||
if not method.auto_connect or not (member := env.get("member")):
|
||||
return
|
||||
player = await connect_channel(member, bot)
|
||||
|
||||
if not player or player.channel.id != member.voice.channel.id:
|
||||
return
|
||||
|
||||
env["player"] = player
|
||||
|
||||
for param in params:
|
||||
args.append(env.get(param))
|
||||
|
||||
if resp := await method.function(*args):
|
||||
await ipc_client.send(resp)
|
||||
|
||||
except Exception as e:
|
||||
payload = {
|
||||
"op": "errorMsg",
|
||||
"level": "error",
|
||||
"msg": str(e),
|
||||
"user_id": str(user_id)
|
||||
}
|
||||
await ipc_client.send(payload)
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "現在您已從音樂控制器 `{0}` 消息。",
|
||||
"settingsMenu": "伺服器設置 | {0}",
|
||||
"settingsTitle": "❤️ 基本資訊:",
|
||||
"settingsValue": "前綴:`{0}`\n語言:`{1}`\n啟用音樂控制器:`{2}`\nDJ 角色:{3}\n投票繞過:`{4}`\n24/7:`{5}`\n默認音量:`{6}%`\n播放時間:`{7}`",
|
||||
"settingsValue": "```前綴:{0}\n語言:{1}\n音樂控制器:{2}\nDJ 角色:@{3}\n投票繞過:{4}\n24/7:{5}\n默認音量:{6}%\n播放時間:{7}```",
|
||||
"settingsTitle2": "🔗 隊列資訊:",
|
||||
"settingsValue2": "隊列模式:`{0}`\n最大歌曲數:`{1}`\n允許重複曲目:`{2}`",
|
||||
"settingsValue2": "```隊列模式:{0}\n最大歌曲數:{1}\n允許重複曲目:{2}```",
|
||||
"settingsTitle3": "🎤 語音狀態資訊:",
|
||||
"settingsPermTitle": "✨ 權限:",
|
||||
"settingsPermValue": "{0} 管理員\n{1} 管理伺服器\n{2} 管理頻道\n{3} 管理訊息",
|
||||
"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}**,單聲道等級到 **{1}**,濾波頻帶到 **{2}**,濾波寬度到 **{3}**",
|
||||
"tremolo&vibrato": "你已更新頻率到 {0},深度到 {1}",
|
||||
"rotation": "你已更新旋轉 Hz 到 {0}",
|
||||
"distortion": "你已啟用失真。",
|
||||
"lowpass": "你已更新低通濾波器到 {0}",
|
||||
"channelmix": "你已更新聲道混音。左左: {0},右右: {1},左右: {2},右左: {3}",
|
||||
"nightcore": "你已加入夜蒼聲音效果。",
|
||||
"8d": "你已加入 8D 聲音效果。",
|
||||
"vaporwave": "你已加入 vaporwave 聲音效果。",
|
||||
"cleareffect": "聲音效果已清除!",
|
||||
"addEffect": "套用音效`{0}`濾鏡。",
|
||||
"clearEffect": "聲音效果已清除!",
|
||||
"FilterTagAlreadyInUse": "此聲音效果已在使用中!請使用 /cleareffect <Tag> 移除它。",
|
||||
|
||||
"playlistViewTitle": "📜 所有 {0} 的播放清單",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ 已將 **{0}** 添加到 {1} 的播放清單中 [`{2}`]!",
|
||||
|
||||
"playerDropdown": "選擇要跳轉到的音軌...",
|
||||
"playerFilter": "選擇要套用的篩選器...",
|
||||
|
||||
"buttonBack": "返回",
|
||||
"buttonPause": "暫停",
|
||||
@@ -187,5 +181,10 @@
|
||||
"voicelinkOutofList": "請提供有效的歌曲索引!",
|
||||
"voicelinkDuplicateTrack": "抱歉,此歌曲已在隊列中。",
|
||||
|
||||
"deocdeError": "解碼文件時出現問題!"
|
||||
"deocdeError": "解碼文件時出現問題!",
|
||||
"invalidStartTime": "無效的開始時間! 時間必須在 `00:00` 和 `{0}` 之間。",
|
||||
"invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。",
|
||||
"invalidTimeOrder": "結束時間不能小於或等於開始時間。",
|
||||
|
||||
"SetStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。"
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "Sie haben jetzt `{0}` Nachrichten vom Musik-Controller.",
|
||||
"settingsMenu": "Servereinstellungen | {0}",
|
||||
"settingsTitle": "❤️ Grundlegende Informationen:",
|
||||
"settingsValue": "Präfix: `{0}`\nSprache: `{1}`\nMusik-Controller aktivieren: `{2}`\nDJ-Rolle: {3}\nAbstimmungsumgehung: `{4}`\nRund um die Uhr: `{5}`\nStandardvolumen: `{6} %`\nSpielzeit: `{7}`",
|
||||
"settingsValue": "```Präfix: {0}`\nSprache: {1}\nMusik-Controller: {2}\nDJ-Rolle: @{3}\nAbstimmungsumgehung: {4}\nRund um die Uhr: {5}\nStandardvolumen: {6}%\nSpielzeit: {7}```",
|
||||
"settingsTitle2": "🔗 Warteschlangeninformationen:",
|
||||
"settingsValue2": "Warteschlangenmodus: `{0}`\nMax Lied: `{1}`\nDoppelte Spur zulassen: `{2}`",
|
||||
"settingsValue2": "```Warteschlangenmodus: {0}\nMax Lied: {1}\nDoppelte Spur zulassen: {2}```",
|
||||
"settingsTitle3": "🎤 Sprachstatusinfo:",
|
||||
"settingsPermTitle": "✨ Berechtigungen:",
|
||||
"settingsPermValue": "{0} Administrator\n{1} Gilde verwalten\n{2} Kanal verwalten\n{3} Manage_Messages",
|
||||
"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!",
|
||||
"addEffect": "Wende den Effekt `{0}` Filter an.",
|
||||
"clearEffect": "Die Soundeffekte wurden gelöscht!",
|
||||
"FilterTagAlreadyInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwenden Sie /cleareffect <Tag>, um sie zu entfernen.",
|
||||
|
||||
"playlistViewTitle": "📜 Alle Playlists von {0}",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ Hinzugefügt **{0}** in {1}'s Wiedergabeliste [`{2}`]!",
|
||||
|
||||
"playerDropdown": "Wählen Sie einen Song aus, um zu überspringen ...",
|
||||
"playerFilter": "Wählen Sie einen Filter aus, um ihn anzuwenden ...",
|
||||
|
||||
"buttonBack": "Zurück",
|
||||
"buttonPause": "Pause",
|
||||
@@ -187,5 +181,10 @@
|
||||
"voicelinkOutofList": "Bitte gib einen gültigen Track-Index an!",
|
||||
"voicelinkDuplicateTrack": "Entschuldigung, dieser Track ist bereits in der Warteschlange.",
|
||||
|
||||
"deocdeError": "Beim Dekodieren der Datei ist etwas schief gelaufen!"
|
||||
"deocdeError": "Beim Dekodieren der Datei ist etwas schief gelaufen!",
|
||||
"invalidStartTime": "Ungültiger Startzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.",
|
||||
"invalidEndTime": "Ungültiger Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.",
|
||||
"invalidTimeOrder": "Der Endzeit darf nicht kleiner oder gleich dem Startzeit sein.",
|
||||
|
||||
"SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Sie sich gerade befinden, gemäß Ihrer Vorlage benannt. Sie sollten in wenigen Sekunden eine Aktualisierung sehen."
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "Now you have `{0}` messages from the music controller.",
|
||||
"settingsMenu": "Server Settings | {0}",
|
||||
"settingsTitle": "❤️ Basic Info:",
|
||||
"settingsValue": "Prefix: `{0}`\nLanguage: `{1}`\nEnable Music Controller: `{2}`\nDJ Role: {3}\nVote Bypass: `{4}`\n24/7: `{5}`\nDefault Volume: `{6}%`\nPlay Time: `{7}`",
|
||||
"settingsValue": "```Prefix: {0}\nLanguage: {1}\nMusic Controller: {2}\nDJ Role: @{3}\nVote Bypass: {4}\n24/7: {5}\nDefault Volume: {6}%\nPlay Time: {7}```",
|
||||
"settingsTitle2": "🔗 Queue info:",
|
||||
"settingsValue2": "Queue Mode: `{0}`\nMax Song: `{1}`\nAllow Duplicate Track: `{2}`",
|
||||
"settingsValue2": "```Queue Mode: {0}\nMax Song: {1}\nDuplicate Track: {2}```",
|
||||
"settingsTitle3": "🎤 Voice Status Info:",
|
||||
"settingsPermTitle": "✨ Permissions:",
|
||||
"settingsPermValue": "{0} Administrator\n{1} Manage_Guild\n{2} Manage_Channel\n{3} Manage_Messages",
|
||||
"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!",
|
||||
"addEffect": "Apply the effect `{0}` filter.",
|
||||
"clearEffect": "The sound effects have been cleared!",
|
||||
"FilterTagAlreadyInUse": "This sound effects has already in use! Please use /cleareffect <Tag> to remove it.",
|
||||
|
||||
"playlistViewTitle": "📜 All {0}'s Playlists",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ Added **{0}** into {1}'s playlist [`{2}`]!",
|
||||
|
||||
"playerDropdown": "Select a song to skip to ...",
|
||||
"playerFilter": "Select a filter to apply ...",
|
||||
|
||||
"buttonBack": "Back",
|
||||
"buttonPause": "Pause",
|
||||
@@ -187,5 +181,10 @@
|
||||
"voicelinkOutofList": "Please provide a valid track index!",
|
||||
"voicelinkDuplicateTrack": "Sorry, this track is already in the queue.",
|
||||
|
||||
"deocdeError": "Something went wrong while decoding the file!"
|
||||
"deocdeError": "Something went wrong while decoding the file!",
|
||||
"invalidStartTime": "Invalid start time, it must be between `00:00` and `{0}`",
|
||||
"invalidEndTime": "Invalid end time, it must be between `00:00` and `{0}`",
|
||||
"invalidTimeOrder": "End time cannot be less than or equal to start time",
|
||||
|
||||
"SetStageAnnounceTemplate": "Done! From now on, voice status like the one you're in now will be named according to your template. You should see it update in a few seconds."
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "Ahora tiene `{0}` mensajes del controlador de música.",
|
||||
"settingsMenu": "Configuración del servidor | {0}",
|
||||
"settingsTitle": "❤️ Información básica:",
|
||||
"settingsValue": "Prefijo: `{0}`\nIdioma: `{1}`\nHabilitar controlador de música: `{2}`\nRol de DJ: {3}\nBypass de votación: `{4}`\n24/7: `{5}`\nVolumen predeterminado: `{6}%`\nTiempo de reproducción: `{7}`",
|
||||
"settingsValue": "```Prefijo: {0}\nIdioma: {1}\nHabilitar controlador de música: {2}\nRol de DJ: @{3}\nBypass de votación: {4}\n24/7: {5}\nVolumen predeterminado: {6}%\nTiempo de reproducción: {7}```",
|
||||
"settingsTitle2": "🔗 Información de la cola:",
|
||||
"settingsValue2": "Modo de cola: `{0}`\nMáximo de canciones: `{1}`\nPermitir pistas duplicadas: `{2}`",
|
||||
"settingsValue2": "```Modo de cola: {0}\nMáximo de canciones: {1}\nPermitir pistas duplicadas: {2}```",
|
||||
"settingsTitle3": "🎤 Información del estado de voz:",
|
||||
"settingsPermTitle": "✨ Permisos:",
|
||||
"settingsPermValue": "{0} Administrador\n{1} Administrar servidor\n{2} Administrar canal\n{3} Administrar mensajes",
|
||||
"settingsPermValue": "```{0} Administrador\n{1} Administrar servidor\n{2} Administrar canal\n{3} Administrar mensajes```",
|
||||
"pingTitle1": "Información del bot:",
|
||||
"pingTitle2": "Información del reproductor:",
|
||||
"pingfield1": "```ID de fragmento: {0}/{1}\nLatencia de fragmento: {2:.3f}s {3}\nRegión: {4}```",
|
||||
"pingfield2": "```Nodo: {0} - {1:.3f}s\nReproductores: {2}\nRegión de voz: {3}```",
|
||||
"karaoke": "Ha actualizado el nivel a **{0}**, monoLevel a **{1}**, filterBand a **{2}** y filterWidth a **{3}**.",
|
||||
"tremolo&vibrato": "Ha actualizado la frecuencia a **{0}** y la profundidad a **{1}**.",
|
||||
"rotation": "Ha actualizado la rotación Hz a **{0}**.",
|
||||
"distortion": "Ha habilitado la distorsión.",
|
||||
"lowpass": "Ha actualizado el paso bajo a **{0}**",
|
||||
"channelmix": "Ha actualizado la mezcla de canales. De izquierda a izquierda: **{0}**, de derecha a derecha: **{1}**, de izquierda a derecha: **{2}**, de derecha a izquierda: **{3}**.",
|
||||
"nightcore": "Ha agregado efectos de sonido nightcore.",
|
||||
"8d": "Ha agregado efectos de sonido 8D.",
|
||||
"vaporwave": "Ha agregado efectos de sonido vaporwave.",
|
||||
"cleareffect": "¡Los efectos de sonido se han borrado!",
|
||||
"addEffect": "plica el efecto `{0}` filtro.",
|
||||
"clearEffect": "¡Los efectos de sonido se han borrado!",
|
||||
"FilterTagAlreadyInUse": "¡Este efecto de sonido ya está en uso! Utilice /cleareffect <Tag> para eliminarlo.",
|
||||
|
||||
"playlistViewTitle": "📜 Todas las listas de reproducción de {0}",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ Agregado **{0}** a la lista de reproducción de {1} [`{2}`]!",
|
||||
|
||||
"playerDropdown": "Seleccione una canción para saltar a ...",
|
||||
"playerFilter": "Seleccione un filtro para aplicar ...",
|
||||
|
||||
"buttonBack": "Atrás",
|
||||
"buttonPause": "Pausa",
|
||||
@@ -187,5 +181,10 @@
|
||||
"voicelinkOutofList": "¡Proporcione un índice de pista válido!",
|
||||
"voicelinkDuplicateTrack": "Lo siento, esta canción ya está en la cola.",
|
||||
|
||||
"deocdeError": "¡Algo salió mal al decodificar el archivo!"
|
||||
"deocdeError": "¡Algo salió mal al decodificar el archivo!",
|
||||
"invalidStartTime": "Tiempo de inicio inválido! El tiempo debe estar entre `00:00` y `{0}`.",
|
||||
"invalidEndTime": "Tiempo de finalización inválido! El tiempo debe estar entre `00:00` y `{0}`.",
|
||||
"invalidTimeOrder": "El tiempo final no puede ser menor o igual que el tiempo de inicio.",
|
||||
|
||||
"SetStageAnnounceTemplate": "¡Hecho! A partir de ahora, el estado de voz como el que tienes ahora se nombrará según tu plantilla. Deberías verlo actualizarse en unos segundos."
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "現在、音楽コントローラーから `{0}` 件のメッセージがあります。",
|
||||
"settingsMenu": "サーバー設定| {0}",
|
||||
"settingsTitle": "❤️ 基本情報:",
|
||||
"settingsValue": "プレフィックス:「{0}」\n言語:「{1}」\n音楽コントローラーを有効にする:「{2}」\nDJロール:{3}\n投票バイパス:「{4}」\n24/7:「{5}」\nデフォルトボリューム:「{6}%」\n再生時間:「{7}」",
|
||||
"settingsValue": "```プレフィックス:{0}\n言語:{1}\n音楽コントローラーを有効にする:{2}\nDJロール:@{3}\n投票バイパス:{4}\n24/7:{5}\nデフォルトボリューム:{6}%\n再生時間:{7}```",
|
||||
"settingsTitle2": "🔗 キュー情報:",
|
||||
"settingsValue2": "キューモード:「{0}」\n最大曲数:「{1}」\n重複トラックを許可する:「{2}」",
|
||||
"settingsValue2": "```キューモード:{0}\n最大曲数:{1}\n重複トラックを許可する:{2}```",
|
||||
"settingsTitle3": "🎤 音声ステータス情報:",
|
||||
"settingsPermTitle": "✨ 権限:",
|
||||
"settingsPermValue": "{0} 管理者\n{1} ギルド管理\n{2} チャンネル管理\n{3} メッセージ管理",
|
||||
"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}**のレベル、**{1}**のモノレベル、**{2}**のフィルターバンド、および**{3}**のフィルター幅を更新しました。",
|
||||
"tremolo&vibrato": "周波数を**{0}**、深度を**{1}**に更新しました。",
|
||||
"rotation": "回転Hzを**{0}**に更新しました。",
|
||||
"distortion": "歪みを有効にしました。",
|
||||
"lowpass": "lowPassを**{0}**に更新しました。",
|
||||
"channelmix": "チャンネルミックスを更新しました。左から左へ:**{0}**、右から右へ:**{1}**、左から右へ:**{2}**、右から左へ:**{3}**",
|
||||
"nightcore": "ナイトコアサウンドエフェクトを追加しました。",
|
||||
"8d": "8Dサウンドエフェクトを追加しました。",
|
||||
"vaporwave": "ヴェイパーウェーブサウンドエフェクトを追加しました。",
|
||||
"cleareffect": "効果音がクリアされました!",
|
||||
"addEffect": "`{0}` フィルターを適用します。",
|
||||
"clearEffect": "効果音がクリアされました!",
|
||||
"FilterTagAlreadyInUse": "このサウンドエフェクトはすでに使用されています!削除するには/cleareffect <Tag>を使用してください。",
|
||||
|
||||
"playlistViewTitle": "📜 {0}のすべてのプレイリスト",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ **{0}**を{1}のプレイリスト[`{2}`]に追加しました!",
|
||||
|
||||
"playerDropdown": "スキップする曲を選択してください...",
|
||||
"playerFilter": "適用するフィルターを選択してください...",
|
||||
|
||||
"buttonBack": "戻る",
|
||||
"buttonPause": "一時停止",
|
||||
@@ -187,5 +181,10 @@
|
||||
"voicelinkOutofList": "有効なトラックインデックスを指定してください!",
|
||||
"voicelinkDuplicateTrack": "申し訳ありませんが、このトラックは既にキューに存在します。",
|
||||
|
||||
"deocdeError": "ファイルのデコード中に問題が発生しました!"
|
||||
"deocdeError": "ファイルのデコード中に問題が発生しました!",
|
||||
"invalidStartTime": "無効な開始時間!時間は `00:00` と `{0}` の間に設定する必要があります。",
|
||||
"invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。",
|
||||
"invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。",
|
||||
|
||||
"SetStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。"
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "이제 음악 컨트롤러로부터 `{0}` 메시지가 있습니다.",
|
||||
"settingsMenu": "서버 설정 | {0}",
|
||||
"settingsTitle": "❤️ 기본 정보:",
|
||||
"settingsValue": "접두사: `{0}`\n언어: `{1}`\n음악 컨트롤러 사용: `{2}`\nDJ 역할: {3}\n투표 우회: `{4}`\n24/7 모드: `{5}`\n기본 볼륨: `{6}%`\n재생 시간: `{7}`",
|
||||
"settingsValue": "```접두사: {0}\n언어: {1}\n음악 컨트롤러 사용: {2}\nDJ 역할: @{3}\n투표 우회: {4}\n24/7 모드: {5}\n기본 볼륨: {6}%\n재생 시간: {7}```",
|
||||
"settingsTitle2": "🔗 대기열 정보:",
|
||||
"settingsValue2": "큐 모드: `{0}`\n최대 노래: `{1}`\n중복된 트랙 허용: `{2}`",
|
||||
"settingsValue2": "```큐 모드: {0}\n최대 노래: {1}\n중복된 트랙 허용: {2}```",
|
||||
"settingsTitle3": "🎤 음성 상태 정보:",
|
||||
"settingsPermTitle": "✨ 권한:",
|
||||
"settingsPermValue": "{0} 관리자\n{1} 서버 관리\n{2} 채널 관리\n{3} 메시지 관리",
|
||||
"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}** 레벨, 모노 레벨 **{1}**, 필터 밴드 **{2}**, 필터 너비 **{3}**으로 업데이트되었습니다.",
|
||||
"tremolo&vibrato": "주파수를 **{0}**으로, 깊이를 **{1}**으로 업데이트했습니다.",
|
||||
"rotation": "회전 주파수를 **{0}**으로 업데이트했습니다.",
|
||||
"distortion": "왜곡이 활성화되었습니다.",
|
||||
"lowpass": "LowPass를 **{0}**으로 업데이트했습니다.",
|
||||
"channelmix": "채널 믹스를 업데이트했습니다. 왼쪽-왼쪽: **{0}**, 오른쪽-오른쪽: **{1}**, 왼쪽-오른쪽: **{2}**, 오른쪽-왼쪽: **{3}**",
|
||||
"nightcore": "나이트코어 효과를 추가했습니다.",
|
||||
"8d": "8D 효과를 추가했습니다.",
|
||||
"vaporwave": "Vaporwave 효과를 추가했습니다.",
|
||||
"cleareffect": "효과가 삭제되었습니다!",
|
||||
"addEffect": "`{0}` 필터를 적용하세요.",
|
||||
"clearEffect": "효과가 삭제되었습니다!",
|
||||
"FilterTagAlreadyInUse": "이 필터는 이미 사용 중입니다! 삭제하려면 /cleareffect <Tag>를 사용하십시오.",
|
||||
|
||||
"playlistViewTitle": "📜 {0}의 모든 재생 목록",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ **{0}**을(를) {1}의 재생목록 [`{2}`] 에 추가했습니다!",
|
||||
|
||||
"playerDropdown": "건너뛰기 할 노래를 선택하세요...",
|
||||
"playerFilter": "적용할 필터를 선택하세요...",
|
||||
|
||||
"buttonBack": "이전",
|
||||
"buttonPause": "일시정지",
|
||||
@@ -187,5 +181,10 @@
|
||||
"voicelinkOutofList": "유효한 트랙 인덱스를 제공해주세요!",
|
||||
"voicelinkDuplicateTrack": "죄송합니다. 이 트랙은 이미 큐에 있습니다.",
|
||||
|
||||
"deocdeError": "파일 디코딩 중 문제가 발생했습니다!"
|
||||
"deocdeError": "파일 디코딩 중 문제가 발생했습니다!",
|
||||
"invalidStartTime": "효력 없는 시작 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.",
|
||||
"invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.",
|
||||
"invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다.",
|
||||
|
||||
"SetStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다."
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "Теперь у вас `{0}` сообщения от контроллера музыки.",
|
||||
"settingsMenu": "Настройки сервера | {0}",
|
||||
"settingsTitle": "❤️ Основная информация:",
|
||||
"settingsValue": "Префикс: `{0}`\nЯзык: `{1}`\nВключить контроллер музыки: `{2}`\nDJ роль: {3}\nОбход голосования: `{4}`\n24/7: `{5}`\nГромкость по умолчанию: `{6}%`\nВремя проигрывания: `{7}`",
|
||||
"settingsValue": "```Префикс: {0}\nЯзык: {1}\nВключить контроллер музыки: {2}\nDJ роль: @{3}\nОбход голосования: {4}\n24/7: {5}\nГромкость по умолчанию: {6}%\nВремя проигрывания: {7}```",
|
||||
"settingsTitle2": "🔗 Информация об очереди:",
|
||||
"settingsValue2": "Режим очереди: `{0}`\nМаксимальное количество песен: `{1}`\nРазрешить дублирование треков: `{2}`",
|
||||
"settingsValue2": "```Режим очереди: {0}\nМаксимальное количество песен: {1}\nРазрешить дублирование треков: {2}```",
|
||||
"settingsTitle3": "🎤 Информация о состоянии голоса:",
|
||||
"settingsPermTitle": "✨ Права:",
|
||||
"settingsPermValue": "{0} Администратор\n{1} Управление_Сервером\n{2} Управление_Каналом\n{3} Управление_Сообщениями",
|
||||
"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}**, моноуровень на **{1}**, фильтрующая полоса на **{2}** и ширина фильтра на **{3}**",
|
||||
"tremolo&vibrato": "Вы обновили значение эффекта tremolo на **{0}** и vibrato на **{1}**",
|
||||
"rotation": "Вы обновили значение эффекта rotation на **{0}**",
|
||||
"distortion": "Вы включили эффект rotation.",
|
||||
"lowpass": "Вы обновили значение эффекта lowpass на **{0}**",
|
||||
"channelmix": "Вы обновили ChannelMix. Левый-на-левый: **{0}**, Правый-на-правый: **{1}**, Левый-на-правый: **{2}**, Правый-на-левый: **{3}**",
|
||||
"nightcore": "Вы включили эффект Nightcore.",
|
||||
"8d": "Вы включили эффект 8D.",
|
||||
"vaporwave": "Вы включили эффект Vaporwave.",
|
||||
"cleareffect": "Звуковые эффекты были очищены!",
|
||||
"addEffect": "Примените эффект `{0}` фильтр.",
|
||||
"clearEffect": "Звуковые эффекты были очищены!",
|
||||
"FilterTagAlreadyInUse": "Этот звуковой эффект уже используется! Пожалуйста, используйте /cleareffect <Тег>, чтобы удалить его.",
|
||||
|
||||
"playlistViewTitle": "📜 Все плейлисты пользователя {0}",
|
||||
@@ -61,7 +54,7 @@
|
||||
"playlistBelongs": "Извините! Этот плейлист принадлежит <@{0}>.",
|
||||
"playlistShare": "Извините! Этим плейлистом уже поделились с {0}.",
|
||||
"playlistSent": "Извините! Вы уже отправили приглашение ранее.",
|
||||
"noPlaylistAcc": "{0} не создал учетную запись плейлиста.",
|
||||
"noPlaylistAcc": "Пользователь `{0}` не создал учетную запись плейлиста.",
|
||||
"overPlaylistCreation": "Вы не можете создавать больше `{0}` плейлистов!",
|
||||
"playlistExists": "Плейлист [`{0}`] уже существует.",
|
||||
"playlistNotInvaildUrl": "Пожалуйста, введите действительную ссылку или публичную плейлист-ссылку на Spotify или YouTube.",
|
||||
@@ -70,8 +63,8 @@
|
||||
"playlistLimitTrack": "Вы достигли лимита! Вы можете добавить только `{0}` треков в свой плейлист.",
|
||||
"playlistPlaylistLink": "Вам не разрешено использовать ссылку на плейлист.",
|
||||
"playlistStream": "Вам не разрешено добавлять потоковые видео в свой плейлист.",
|
||||
"playlistPositionNotFound": "Не удается найти позицию `{0}` в вашем плейлисте [`{1}`]!",
|
||||
"playlistRemoved": "👋 Удалено **{0}** из плейлиста {1} [`{2}`].",
|
||||
"playlistPositionNotFound": "Не получилось найти позицию `{0}` в вашем плейлисте [`{1}`]!",
|
||||
"playlistRemoved": "👋 Удален **{0}** из плейлиста {1} [`{2}`].",
|
||||
"playlistClear": "Вы успешно очистили свой плейлист [`{0}`].",
|
||||
"playlistView": "Просмотр плейлистов",
|
||||
"playlistViewDesc": "```Имя | ID: {0} | {1}\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```",
|
||||
@@ -83,9 +76,9 @@
|
||||
"inboxNoMsg": "В вашем почтовом ящике нет сообщений.",
|
||||
"invitationSent": "Приглашение отправлено {0}.",
|
||||
|
||||
"notInChannel": "{0}, для использования голосовых команд вы должны находиться в {1}. Пожалуйста, перезайдите, если вы уже в голосе!",
|
||||
"notInChannel": "{0}, для использования голосовых команд вы должны находиться в {1}. Пожалуйста, перезайдите, если вы уже находитесь в голосовом канале!",
|
||||
"noTrackPlaying": "Сейчас нет играющих треков.",
|
||||
"noTrackFound": "Треки с таким запросом не найдены! Пожалуйста, укажите действительную ссылку.",
|
||||
"noTrackFound": "Треки по такому запросу не найдены! Пожалуйста, укажите действительную ссылку.",
|
||||
"noLinkSupport": "Команда поиска не поддерживает ссылки!",
|
||||
"voted": "Вы проголосовали!",
|
||||
"missingPerms_pos": "Только DJ или администраторы могут изменять позицию.",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ Добавлен **{0}** в плейлист пользователя {1} [`{2}`]!",
|
||||
|
||||
"playerDropdown": "Выберите трек для перехода ...",
|
||||
"playerFilter": "Выберите фильтр для применения ...",
|
||||
|
||||
"buttonBack": "Назад",
|
||||
"buttonPause": "Пауза",
|
||||
@@ -120,7 +114,7 @@
|
||||
"buttonShuffle": "Перемешать",
|
||||
"buttonForward": "+10сек",
|
||||
"buttonRewind": "Назад",
|
||||
|
||||
|
||||
"nowplayingDesc": "**Сейчас играет:**\n```{0}```",
|
||||
"nowplayingField": "Следующее:",
|
||||
"nowplayingLink": "Слушать на {0}",
|
||||
@@ -129,7 +123,7 @@
|
||||
|
||||
"live": "В ЭФИРЕ",
|
||||
"playlistLoad": "🎶 Добавлен плейлист **{0}** с `{1}` треками в очередь.",
|
||||
"trackLoad": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) для начала проигрывания.\n",
|
||||
"trackLoad": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) в очередь.\n",
|
||||
"trackLoad_pos": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) в очередь на позицию **{4}**\n",
|
||||
"searchTitle": "Поиск: {0}",
|
||||
"searchDesc": "➥ Платформа: {0} **{1}**\n➥ Результаты: **{2}**\n\n{3}",
|
||||
@@ -144,13 +138,13 @@
|
||||
"viewFooter": "Страница: {0}/{1} | Общая продолжительность: {2}",
|
||||
|
||||
"pauseError": "Плеер уже на паузе.",
|
||||
"pauseVote": "Пользователь `{0}` проголосовал за паузу трека. [{1}/{2}]",
|
||||
"pauseVote": "Пользователь `{0}` проголосовал за паузу плеера. [{1}/{2}]",
|
||||
"paused": "Пользователь `{0}` поставил плеер на паузу.",
|
||||
"resumeError": "Плеер не на паузе.",
|
||||
"resumeVote": "Пользователь `{0}` проголосовал за продолжение трека. [{1}/{2}]",
|
||||
"resumed": "Пользователь `{0}` убрал плеер с паузы.",
|
||||
"shuffleError": "Недостаточно треков для перемешивания",
|
||||
"shuffleVote": "Пользователь `{0}` проголосовал за перемешивание очереди. [{1}/{2}]",
|
||||
"shuffleVote": "Пользователь `{0}` проголосовал за перемешивание треков. [{1}/{2}]",
|
||||
"shuffled": "Очередь перемешана.",
|
||||
"skipError": "Нет треков для пропуска.",
|
||||
"skipVote": "Пользователь `{0}` проголосовал за пропуск трека. [{1}/{2}]",
|
||||
@@ -160,7 +154,7 @@
|
||||
"backed": "Пользователь `{0}` вернулся к предыдущему треку.",
|
||||
|
||||
"leaveVote": "Пользователь `{0}` проголосовал за остановку плеера. [{1}/{2}]",
|
||||
"left": "`{0}` остановил плеер.",
|
||||
"left": "Пользователь `{0}` остановил плеер.",
|
||||
|
||||
"seek": "Установить плеер на **{0}**",
|
||||
"repeat": "Режим повтора `{0}`",
|
||||
@@ -186,5 +180,10 @@
|
||||
"voicelinkOutofList": "Пожалуйста, предоставьте действительный индекс трека!",
|
||||
"voicelinkDuplicateTrack": "Извините, этот трек уже есть в очереди.",
|
||||
|
||||
"deocdeError": "Что-то пошло не так при декодировании файла!"
|
||||
}
|
||||
"deocdeError": "Что-то пошло не так при декодировании файла!",
|
||||
"invalidStartTime": "Hевозможное время начала! Вход времени должен быть внутри `00:00` и `{0}`.",
|
||||
"invalidEndTime": "Невозможное время конца! Вход времени должен быть внутри `00:00` и `{0}`.",
|
||||
"invalidTimeOrder": "Время конца не может быть меньше или равно времени начала.",
|
||||
|
||||
"SetStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд."
|
||||
}
|
||||
@@ -23,25 +23,18 @@
|
||||
"toggleControllerMsg": "Тепер у вас `{0}` повідомлення від контролера музики.",
|
||||
"settingsMenu": "Налаштування сервера | {0}",
|
||||
"settingsTitle": "❤️ Основна інформація:",
|
||||
"settingsValue": "Префікс: `{0}`\nМова: `{1}`\nУвімкнути контролер музики: `{2}`\nDJ роль: {3}\nОбхід голосування: `{4}`\n24/7: `{5}`\nГромкість за замовчуванням: `{6}%`\nЧас програвання: `{7}`",
|
||||
"settingsValue": "```Префікс: {0}\nМова: {1}\nУвімкнути контролер музики: {2}\nDJ роль: {3}\nОбхід голосування: {4}\n24/7: {5}\nГромкість за замовчуванням: {6}%\nЧас програвання: {7}```",
|
||||
"settingsTitle2": "🔗 Інформація про чергу:",
|
||||
"settingsValue2": "Режим черги: `{0}`\nМаксимальна кількість пісень: `{1}`\nДозволити дублювання треків: `{2}`",
|
||||
"settingsValue2": "```Режим черги: {0}\nМаксимальна кількість пісень: {1}\nДозволити дублювання треків: {2}```",
|
||||
"settingsTitle3": "🎤 Інформація про стан голосу:",
|
||||
"settingsPermTitle": "✨ Права:",
|
||||
"settingsPermValue": "{0} Адміністратор\n{1} Керування_Сервером\n{2} Керування_Каналом\n{3} Керування_Сообщениями",
|
||||
"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}**, моно-рівень на **{1}**, фільтрувальну смугу на **{2}** і ширину фільтра на **{3}**",
|
||||
"tremolo&vibrato": "Ви оновили значення ефекту tremolo на **{0}** і vibrato на **{1}**",
|
||||
"rotation": "Ви оновили значення ефекту rotation на **{0}***",
|
||||
"distortion": "Ви ввімкнули ефект rotation.",
|
||||
"lowpass": "Ви оновили значення ефекту lowpass на **{0}**",
|
||||
"channelmix": "Ви оновили ChannelMix. Лівий-на-лівий: **{0}**, Правий-на-правий: **{1}**, Лівий-на-правий: **{2}**, Правий-на-лівий: **{3}**",
|
||||
"nightcore": "Ви ввімкнули ефект Nightcore.",
|
||||
"8d": "Ви ввімкнули ефект 8D.",
|
||||
"vaporwave": "Ви ввімкнули ефект Vaporwave.",
|
||||
"cleareffect": "Звукові ефекти були очищені!",
|
||||
"addEffect": "Застосуйте ефект `{0}` фільтр.",
|
||||
"clearEffect": "Звукові ефекти були очищені!",
|
||||
"FilterTagAlreadyInUse": "Цей звуковий ефект уже використовується! Будь ласка, використовуйте /cleareffect <Тег>, щоб видалити його.",
|
||||
|
||||
"playlistViewTitle": "📜 Усі плейлисти користувача {0}",
|
||||
@@ -105,6 +98,7 @@
|
||||
"playlistAdded": "❤️ Додано **{0}** до плейлиста користувача {1} [`{2}`]!",
|
||||
|
||||
"playerDropdown": "Виберіть пісню для переходу ...",
|
||||
"playerFilter": "Виберіть фільтр для застосування ...",
|
||||
|
||||
"buttonBack": "Назад",
|
||||
"buttonPause": "Призупинити",
|
||||
@@ -186,5 +180,10 @@
|
||||
"voicelinkOutofList": "Будь ласка, надайте дійсний індекс треку!",
|
||||
"voicelinkDuplicateTrack": "Вибачте, цей трек уже є в черзі.",
|
||||
|
||||
"deocdeError": "Щось пішло не так під час декодування файлу!"
|
||||
}
|
||||
"deocdeError": "Щось пішло не так під час декодування файлу!",
|
||||
"invalidStartTime": "Недійснений час початку! Час має бути в межах `00:00` та `{0}`.",
|
||||
"invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.",
|
||||
"invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.",
|
||||
|
||||
"SetStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд."
|
||||
}
|
||||
@@ -1,217 +1,214 @@
|
||||
{
|
||||
"language": "語言",
|
||||
"dj": "dj",
|
||||
"247": "247",
|
||||
"bypassvote": "繞過投票",
|
||||
"serversettings": "服務器設置",
|
||||
"volume": "音量",
|
||||
"togglecontroller": "切換控制器",
|
||||
"ping": "發信",
|
||||
"debug": "調試",
|
||||
"connect": "連接",
|
||||
"connect": "連線",
|
||||
"Connect to a voice channel.": "連線到語音頻道。",
|
||||
"channel": "頻道",
|
||||
"Provide a channel to 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.": "輸入查詢或可搜索鏈接。",
|
||||
"Loads your input and added it to the queue.": "載入您的輸入並將其加入排隊。",
|
||||
"query": "查詢",
|
||||
"Input a query or a searchable link.": "輸入查詢或可搜尋的連結。",
|
||||
"search": "搜尋",
|
||||
"Input the name of the song.": "輸入歌曲名稱。",
|
||||
"platform": "平台",
|
||||
"Select the platform you want to search.": "選擇您要搜索的平台。",
|
||||
"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)": "輸入歌曲名稱。(無網址)",
|
||||
"SoundCloud": "SoundCloud",
|
||||
"Apple Music": "Apple 音樂",
|
||||
"playtop": "將歌曲加入頂部",
|
||||
"Adds a song with the given url or query on the top of the queue.": "將指定的 URL 或查詢加入排隊的頂部。",
|
||||
"forceplay": "強制播放",
|
||||
"Enforce playback using the given URL or query.": "使用指定的 URL 或查詢強制播放。",
|
||||
"pause": "暫停",
|
||||
"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.": "改變玩家位置。",
|
||||
"resume": "繼續",
|
||||
"Resume the music.": "繼續音樂。",
|
||||
"skip": "跳過",
|
||||
"Skips to the next song or skips to the specified song.": "跳過到下一首歌曲或跳過到指定的歌曲。",
|
||||
"index": "索引",
|
||||
"Enter a index that you want to skip to.": "輸入您要跳過到的索引。",
|
||||
"back": "返回",
|
||||
"Skips back to the previous song or skips to the specified previous song.": "跳回上一首歌曲或跳回到指定的上一首歌曲。",
|
||||
"Enter a index that you want to skip back to.": "輸入您要跳回到的索引。",
|
||||
"seek": "搜尋",
|
||||
"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.": "更改循環模式。",
|
||||
"Input position. Exmaple: 1:20.": "輸入位置。範例:1:20。",
|
||||
"queue": "排隊",
|
||||
"Display the players queue songs in your queue.": "顯示您的排隊歌曲。",
|
||||
"export": "匯出",
|
||||
"Exports the entire queue to a text file": "將整個排隊匯出到文字檔。",
|
||||
"import": "匯入",
|
||||
"Imports the text file and adds the track to the current queue.": "匯入文字檔並將歌曲加入目前的排隊。",
|
||||
"attachment": "附件",
|
||||
"history": "歷史",
|
||||
"Display the players queue songs in your history queue.": "顯示您的歷史排隊歌曲。",
|
||||
"leave": "離開",
|
||||
"Disconnects the bot from your voice channel and chears the queue.": "從您的語音頻道斷開機器人連線並清除排隊。",
|
||||
"nowplaying": "正在播放",
|
||||
"Shows details of the current track.": "顯示目前歌曲的詳細資訊。",
|
||||
"loop": "迴圈",
|
||||
"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.": "選擇要清除的隊列。",
|
||||
"Choose a looping mode.": "選擇迴圈模式。",
|
||||
"Off": "關閉",
|
||||
"Track": "歌曲",
|
||||
"Queue": "排隊",
|
||||
"clear": "清除",
|
||||
"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.": "從隊列中輸入要刪除的位置。",
|
||||
"remove": "移除",
|
||||
"Removes specified track or a range of tracks from the queue.": "刪除指定的歌曲或從排隊中刪除一系列歌曲。",
|
||||
"position1": "位置1",
|
||||
"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 轉移到另一個。",
|
||||
"Set the range of the queue to be removed.": "設定要刪除的排隊範圍。",
|
||||
"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.": "切換自動播放模式,它會自動排隊播放最好的歌曲。",
|
||||
"Remove tracks requested by a specific member.": "刪除指定成員所要求的歌曲。",
|
||||
"forward": "前進",
|
||||
"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": "倒退",
|
||||
"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",
|
||||
"replay": "重新播放",
|
||||
"Reset the progress of the current song.": "重設目前歌曲的進度。",
|
||||
"shuffle": "隨機播放",
|
||||
"Randomizes the tracks in the queue.": "隨機排隊中的歌曲。",
|
||||
"swap": "交換",
|
||||
"Swaps the specified song to the specified song.": "交換指定的歌曲到另一個指定的歌曲。",
|
||||
"The track to swap. Example: 2": "要交換的歌曲。範例:2",
|
||||
"The track to swap with position1. Exmaple: 1": "與位置 1 交換的歌曲。範例:1",
|
||||
"move": "移動",
|
||||
"Moves the specified song to the specified position.": "將指定的歌曲移到指定的位置。",
|
||||
"target": "目標",
|
||||
"The track to move. Example: 2": "要移動的歌曲。範例:2",
|
||||
"to": "到",
|
||||
"The new position to move the track to. Exmaple: 1": "要移動到的新位置。範例:1",
|
||||
"lyrics": "歌詞",
|
||||
"Displays lyrics for the playing track.": "顯示播放中歌曲的歌詞。",
|
||||
"title": "標題",
|
||||
"Searches for your query and displays the reutned lyrics.": "搜尋您的查詢並顯示返回的歌詞。",
|
||||
"artist": "藝術家",
|
||||
"swapdj": "交換dj",
|
||||
"Transfer dj to another.": "將 DJ 轉移給另一個。",
|
||||
"Choose a member to transfer the dj role.": "選擇一個成員來轉移 DJ 角色。",
|
||||
"autoplay": "自動播放",
|
||||
"Toggles autoplay mode, it will automatically queue the best songs to play.": "切換自動播放模式,將自動將最好的歌曲加入排隊播放。",
|
||||
"help": "幫助",
|
||||
"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.": "清除所有音效。",
|
||||
"category": "分類",
|
||||
"Test if the bot is alive, and see the delay between your commands and my response.": "測試機器人是否活躍,並查看您的命令和我的回應之間的延遲。",
|
||||
"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.": "刪除您的自定義播放列表。",
|
||||
"Play all songs from your favorite playlist.": "播放您最愛的播放列表中的所有歌曲。",
|
||||
"name": "名稱",
|
||||
"Input the name of your custom playlist": "輸入您自定義播放列表的名稱",
|
||||
"value": "值",
|
||||
"Play the specific track from your custom playlist.": "播放您自定義播放列表中的指定歌曲。",
|
||||
"view": "查看",
|
||||
"List all your playlist and all songs in your favourite playlist.": "列出您所有的播放列表和您最愛的播放列表中的所有歌曲。",
|
||||
"create": "創建",
|
||||
"Create your custom playlist.": "創建您自定義的播放列表。",
|
||||
"Give a name to your playlist.": "給您的播放列表命名。",
|
||||
"link": "鏈接",
|
||||
"Provide a playlist link if you are creating link playlist.": "如果您正在創建鏈接播放列表,請提供播放列表鏈接。",
|
||||
"delete": "刪除",
|
||||
"Delete your custom playlist.": "刪除您自定義的播放列表。",
|
||||
"The name of the playlist.": "播放列表的名稱。",
|
||||
"Share your custom playlist with your friends.": "與您的朋友分享您的自定義播放列表。",
|
||||
"share": "分享",
|
||||
"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.": "更改為另一種隊列模式。",
|
||||
"The name of the playlist that you want to share.": "您想要分享的播放列表名稱。",
|
||||
"rename": "重新命名",
|
||||
"Rename your custom playlist.": "重新命名自定義的播放列表。",
|
||||
"The name of your playlist.": "您的播放列表名稱。",
|
||||
"newname": "新名稱",
|
||||
"The new name of your playlist.": "您的播放列表的新名稱。",
|
||||
"inbox": "收件箱",
|
||||
"Show your playlist invitation.": "顯示播放列表邀請。",
|
||||
"add": "添加",
|
||||
"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.": "刪除最愛的播放列表中的所有歌曲。",
|
||||
"Exports the entire playlist to a text file": "將整個播放列表匯出到文字檔。",
|
||||
"settings": "設定",
|
||||
"prefix": "前綴",
|
||||
"Change the default prefix for message commands.": "更改消息命令的預設前綴。",
|
||||
"language": "語言",
|
||||
"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 角色。",
|
||||
"role": "角色",
|
||||
"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": "設置"
|
||||
"Toggles 24/7 mode, which disables automatic inactivity-based disconnects.": "切換 24/7 模式,禁用自動休眠斷線。",
|
||||
"bypassvote": "繞過投票",
|
||||
"Toggles voting system.": "切換投票系統。",
|
||||
"Show all the bot settings in your server.": "顯示機器人所有設定在您的服務器中。",
|
||||
"volume": "音量",
|
||||
"Set the player's volume.": "設置播放器的音量。",
|
||||
"Input a integer.": "輸入一個整數。",
|
||||
"togglecontroller": "切換控制器",
|
||||
"Toggles the music controller.": "切換音樂控制器。",
|
||||
"duplicatetrack": "重複歌曲",
|
||||
"Toggle Vocard to prevent duplicate songs from queuing.": "切換 Vocard 以防止重複歌曲加入隊列。",
|
||||
"customcontroller": "自定義控制器",
|
||||
"Customizes music controller embeds.": "自定義音樂控制器嵌入。",
|
||||
"controllermsg": "控制器訊息",
|
||||
"Toggles to send a message when clicking the button in the music controller.": "切換發送訊息當點擊音樂控制器中的按鈕。",
|
||||
"debug": "除錯",
|
||||
"speed": "速度",
|
||||
"Sets the player's playback speed": "設置播放器的播放速度。",
|
||||
"The value to set the speed to. Default is `1.0`": "設置速度的值。預設為 `1.0`。",
|
||||
"karaoke": "卡拉ok",
|
||||
"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`",
|
||||
"tremolo": "顫音",
|
||||
"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`",
|
||||
"vibrato": "振動",
|
||||
"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`",
|
||||
"rotation": "旋轉",
|
||||
"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": "失真",
|
||||
"Distortion effect. It can generate some pretty unique audio effects.": "失真效果。可以生成一些非常獨特的音頻效果。",
|
||||
"lowpass": "低通",
|
||||
"Filter which supresses higher frequencies and allows lower frequencies to pass.": "濾波器,抑制高頻率,允許低頻率通過。",
|
||||
"smoothing": "平滑",
|
||||
"The level of the lowPass. Default is `20.0`": "低通的平滑級別。預設為 `20.0`",
|
||||
"channelmix": "頻道混合",
|
||||
"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`",
|
||||
"nightcore": "夜核",
|
||||
"Add nightcore filter into your player.": "將夜核濾波器添加到您的播放器中。",
|
||||
"Add 8D filter into your player.": "將 8D 濾波器添加到您的播放器中。",
|
||||
"vaporwave": "水蒸波",
|
||||
"Add vaporwave filter into your player.": "將水蒸波濾波器添加到您的播放器中。",
|
||||
"cleareffect": "清除效果",
|
||||
"Clear all or specific sound effects.": "清除所有或指定的音效。",
|
||||
"effect": "效果",
|
||||
"Remove a specific sound effects.": "刪除指定的音效。"
|
||||
}
|
||||
90
main.py
90
main.py
@@ -1,24 +1,24 @@
|
||||
import discord
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import aiohttp
|
||||
import update
|
||||
import logging
|
||||
import function as func
|
||||
|
||||
from discord.ext import commands
|
||||
from web import IPCServer
|
||||
from ipc import IPCClient
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from datetime import datetime
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from voicelink import VoicelinkException
|
||||
from addons import Settings
|
||||
|
||||
class Translator(discord.app_commands.Translator):
|
||||
async def load(self):
|
||||
print("Loaded Translator")
|
||||
func.logger.info("Loaded Translator")
|
||||
|
||||
async def unload(self):
|
||||
print("Unload Translator")
|
||||
func.logger.info("Unload Translator")
|
||||
|
||||
async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext):
|
||||
if str(locale) in func.LOCAL_LANGS:
|
||||
@@ -29,12 +29,7 @@ class Vocard(commands.Bot):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.ipc = IPCServer(
|
||||
self,
|
||||
host=func.settings.ipc_server["host"],
|
||||
port=func.settings.ipc_server["port"],
|
||||
secret_key=func.tokens.secret_key
|
||||
)
|
||||
self.ipc: IPCClient
|
||||
|
||||
async def on_message(self, message: discord.Message, /) -> None:
|
||||
if message.author.bot or not message.guild:
|
||||
@@ -49,16 +44,17 @@ class Vocard(commands.Bot):
|
||||
await self.process_commands(message)
|
||||
|
||||
async def connect_db(self) -> None:
|
||||
if not ((db_name := func.tokens.mongodb_name) and (db_url := func.tokens.mongodb_url)):
|
||||
if not ((db_name := func.settings.mongodb_name) and (db_url := func.settings.mongodb_url)):
|
||||
raise Exception("MONGODB_NAME and MONGODB_URL can't not be empty in settings.json")
|
||||
|
||||
try:
|
||||
func.MONGO_DB = AsyncIOMotorClient(host=db_url)
|
||||
await func.MONGO_DB.server_info()
|
||||
print("Successfully connected to MongoDB!")
|
||||
func.logger.info(f"Successfully connected to [{db_name}] MongoDB!")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception("Not able to connect MongoDB! Reason:", e)
|
||||
func.logger.error("Not able to connect MongoDB! Reason:", exc_info=e)
|
||||
exit()
|
||||
|
||||
func.SETTINGS_DB = func.MONGO_DB[db_name]["Settings"]
|
||||
func.USERS_DB = func.MONGO_DB[db_name]["Users"]
|
||||
@@ -74,12 +70,16 @@ class Vocard(commands.Bot):
|
||||
if module.endswith('.py'):
|
||||
try:
|
||||
await self.load_extension(f"cogs.{module[:-3]}")
|
||||
print(f"Loaded {module[:-3]}")
|
||||
func.logger.info(f"Loaded {module[:-3]}")
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
func.logger.error(f"Something went wrong while loading {module[:-3]} cog.", exc_info=e)
|
||||
|
||||
if func.settings.ipc_server.get("enable", False):
|
||||
await self.ipc.start()
|
||||
self.ipc = IPCClient(self, **func.settings.ipc_client)
|
||||
if func.settings.ipc_client.get("enable", False):
|
||||
try:
|
||||
await self.ipc.connect()
|
||||
except Exception as e:
|
||||
func.logger.error(f"Cannot connected to dashboard! - Reason: {e}")
|
||||
|
||||
if not func.settings.version or func.settings.version != update.__version__:
|
||||
func.update_json("settings.json", new_data={"version": update.__version__})
|
||||
@@ -88,15 +88,15 @@ class Vocard(commands.Bot):
|
||||
await self.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("------------------")
|
||||
func.logger.info("------------------")
|
||||
func.logger.info(f"Logging As {self.user}")
|
||||
func.logger.info(f"Bot ID: {self.user.id}")
|
||||
func.logger.info("------------------")
|
||||
func.logger.info(f"Discord Version: {discord.__version__}")
|
||||
func.logger.info(f"Python Version: {sys.version}")
|
||||
func.logger.info("------------------")
|
||||
|
||||
func.tokens.client_id = self.user.id
|
||||
func.settings.client_id = self.user.id
|
||||
func.LOCAL_LANGS.clear()
|
||||
|
||||
async def on_command_error(self, ctx: commands.Context, exception, /) -> None:
|
||||
@@ -124,10 +124,8 @@ class Vocard(commands.Bot):
|
||||
|
||||
elif not issubclass(error.__class__, VoicelinkException):
|
||||
error = await func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
|
||||
if (guildId := ctx.guild.id) not in func.ERROR_LOGS:
|
||||
func.ERROR_LOGS[guildId] = {}
|
||||
func.ERROR_LOGS[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
|
||||
|
||||
func.logger.error(f"An unexpected error occurred in the {ctx.command.name} command on the {ctx.guild.name}({ctx.guild.id}).", exc_info=exception)
|
||||
|
||||
try:
|
||||
return await ctx.reply(error, ephemeral=True)
|
||||
except:
|
||||
@@ -139,30 +137,42 @@ class CommandCheck(discord.app_commands.CommandTree):
|
||||
await interaction.response.send_message("This command can only be used in guilds!")
|
||||
return False
|
||||
|
||||
return await super().interaction_check(interaction)
|
||||
|
||||
return True
|
||||
|
||||
async def get_prefix(bot, message: discord.Message):
|
||||
settings = await func.get_settings(message.guild.id)
|
||||
return settings.get("prefix", func.settings.bot_prefix)
|
||||
|
||||
# Loading settings
|
||||
# Loading settings and logger
|
||||
func.settings = Settings(func.open_json("settings.json"))
|
||||
|
||||
LOG_SETTINGS = func.settings.logging
|
||||
if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True):
|
||||
log_path = os.path.abspath(LOG_FILE.get("path", "./logs"))
|
||||
if not os.path.exists(log_path):
|
||||
os.makedirs(log_path)
|
||||
|
||||
file_handler = TimedRotatingFileHandler(filename=f'{log_path}/vocard.log', encoding="utf-8", backupCount=LOG_SETTINGS.get("max-history", 30), when="d")
|
||||
file_handler.namer = lambda name: name.replace(".log", "") + ".log"
|
||||
file_handler.setFormatter(logging.Formatter('{asctime} [{levelname:<8}] {name}: {message}', '%Y-%m-%d %H:%M:%S', style='{'))
|
||||
|
||||
for log_name, log_level in LOG_SETTINGS.get("level", {}).items():
|
||||
_logger = logging.getLogger(log_name)
|
||||
_logger.setLevel(log_level)
|
||||
|
||||
logging.getLogger().addHandler(file_handler)
|
||||
|
||||
# Setup the bot object
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True if func.settings.bot_prefix else False
|
||||
intents.members = True
|
||||
member_cache = discord.MemberCacheFlags(
|
||||
voice=True,
|
||||
joined=False
|
||||
)
|
||||
intents.members = func.settings.ipc_client.get("enable", False)
|
||||
intents.voice_states = True
|
||||
|
||||
bot = Vocard(
|
||||
command_prefix=get_prefix,
|
||||
help_command=None,
|
||||
tree_cls=CommandCheck,
|
||||
chunk_guilds_at_startup=False,
|
||||
member_cache_flags=member_cache,
|
||||
activity=discord.Activity(type=discord.ActivityType.listening, name="Starting..."),
|
||||
case_insensitive=True,
|
||||
intents=intents
|
||||
@@ -170,4 +180,4 @@ bot = Vocard(
|
||||
|
||||
if __name__ == "__main__":
|
||||
update.check_version(with_msg=True)
|
||||
bot.run(func.tokens.token, log_handler=None)
|
||||
bot.run(func.settings.token, root_logger=True)
|
||||
@@ -1,13 +1,8 @@
|
||||
discord.py==2.3.2
|
||||
discord.py==2.4.0
|
||||
motor==3.3.2
|
||||
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
|
||||
websockets==10.4
|
||||
Flask==2.2.3
|
||||
Flask-SocketIO==5.3.2
|
||||
psutil==5.9.5
|
||||
werkzeug==2.2.2
|
||||
psutil==5.9.8
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"token": "YOUR_BOT_TOKEN",
|
||||
"client_id": "YOUR_BOT_CLIENT_ID",
|
||||
"spotify_client_id": "YOUR_SPOTIFY_CLIENT_ID",
|
||||
"spotify_client_secret": "YOUR_SPOTIFY_CLIENT_SECRET",
|
||||
"genius_token": "YOUR_GENIUS_TOKEN",
|
||||
"mongodb_url": "YOUR_MONGODB_URL",
|
||||
"mongodb_name": "YOUR_MONGODB_DB_NAME",
|
||||
"nodes": {
|
||||
"DEFAULT": {
|
||||
"host": "lavalink",
|
||||
@@ -9,16 +16,30 @@
|
||||
}
|
||||
},
|
||||
"prefix": "?",
|
||||
"activity":[
|
||||
{"listen": "/help"}
|
||||
"activity": [
|
||||
{"type": "listening", "name": "/help", "status": "online"}
|
||||
],
|
||||
"logging": {
|
||||
"file": {
|
||||
"path": "./logs",
|
||||
"enable": true
|
||||
},
|
||||
"level": {
|
||||
"discord": "INFO",
|
||||
"vocard": "INFO",
|
||||
"ipc_client": "INFO"
|
||||
},
|
||||
"max-history": 30
|
||||
},
|
||||
"bot_access_user": [],
|
||||
"embed_color":"0xb3b3b3",
|
||||
"default_max_queue": 1000,
|
||||
"lyrics_platform": "A_ZLyrics",
|
||||
"ipc_server": {
|
||||
"lyrics_platform": "lyrist",
|
||||
"ipc_client": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"password": "YOUR_PASSWORD",
|
||||
"secure": false,
|
||||
"enable": false
|
||||
},
|
||||
"sources_settings": {
|
||||
@@ -66,7 +87,7 @@
|
||||
"default_controller": {
|
||||
"embeds": {
|
||||
"active": {
|
||||
"description": "**Now Playing: ```[@@track_name@@]```\nLink: [Click Me](@@track_url@@) | Requester: @@requester@@ | DJ: @@dj@@**",
|
||||
"description": "**Now Playing: ```[@@track_name@@]```\nLink: [Click Me](@@track_url@@) | Requester: <#@@track_requester_id@@> | DJ: @@dj@@**",
|
||||
"footer": {
|
||||
"text": "Queue Length: @@queue_length@@ | Duration: @@track_duration@@ | Volume: @@volume@@% {{loop_mode != 'Off' ?? | Repeat: @@loop_mode@@}}"
|
||||
},
|
||||
@@ -92,6 +113,7 @@
|
||||
],
|
||||
"disableButtonText": false
|
||||
},
|
||||
"default_voice_status_template": "{{@@track_name@@ != 'None' ?? @@track_source_emoji@@ Now Playing: @@track_name@@ // Waiting for song requests}}",
|
||||
"cooldowns": {
|
||||
"connect": [2, 30],
|
||||
"playlist view": [1, 30]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
[program:main]
|
||||
command=python -u /app/main.py
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/main.err.log
|
||||
stdout_logfile=/var/log/main.out.log
|
||||
|
||||
[program:webapp]
|
||||
command=python -u /app/web/webapp.py
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/webapp.err.log
|
||||
stdout_logfile=/var/log/webapp.out.log
|
||||
@@ -2,11 +2,11 @@ import requests, zipfile, os, shutil, argparse
|
||||
from io import BytesIO
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
__version__ = "v2.6.8a"
|
||||
__version__ = "v2.6.9"
|
||||
|
||||
GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest"
|
||||
VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/"
|
||||
IGNORE_FILES = ["settings.json", ".env"]
|
||||
IGNORE_FILES = ["settings.json", "logs"]
|
||||
|
||||
class bcolors:
|
||||
WARNING = '\033[93m'
|
||||
|
||||
@@ -26,15 +26,10 @@ import voicelink
|
||||
import function as func
|
||||
|
||||
from discord.ext import commands
|
||||
from . import ButtonOnCooldown
|
||||
from function import (
|
||||
get_user,
|
||||
update_user,
|
||||
check_roles
|
||||
)
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from . import ButtonOnCooldown
|
||||
|
||||
def key(interaction: discord.Interaction):
|
||||
return interaction.user
|
||||
|
||||
@@ -48,7 +43,7 @@ class ControlButton(discord.ui.Button):
|
||||
self.player: voicelink.Player = player
|
||||
|
||||
self.disable_button_text: bool = func.settings.controller.get("disableButtonText", False)
|
||||
super().__init__(label=player.get_msg(label) if label and not self.disable_button_text else None, **kwargs)
|
||||
super().__init__(label=self.player.get_msg(label) if label and not self.disable_button_text else None, **kwargs)
|
||||
|
||||
async def send(self, interaction: discord.Interaction, key:str, *params, ephemeral: bool = False) -> None:
|
||||
stay = self.player.settings.get("controller_msg", True)
|
||||
@@ -114,7 +109,6 @@ class Resume(ControlButton):
|
||||
if len(votes) < (required := self.player.required()):
|
||||
return await self.send(interaction, f"{vote_type}Vote", interaction.user, len(votes), required)
|
||||
|
||||
votes.clear()
|
||||
self.emoji = emoji
|
||||
if not self.disable_button_text:
|
||||
self.label = await func.get_lang(interaction.guild.id, button)
|
||||
@@ -186,14 +180,14 @@ class Add(ControlButton):
|
||||
return await self.send(interaction, "noTrackPlaying")
|
||||
if track.is_stream:
|
||||
return await self.send(interaction, "playlistAddError")
|
||||
user = await get_user(interaction.user.id, 'playlist')
|
||||
rank, max_p, max_t = check_roles()
|
||||
user = await func.get_user(interaction.user.id, 'playlist')
|
||||
rank, max_p, max_t = func.check_roles()
|
||||
if len(user['200']['tracks']) >= max_t:
|
||||
return await self.send(interaction, "playlistlimited", max_t, ephemeral=True)
|
||||
|
||||
if track.track_id in user['200']['tracks']:
|
||||
return await self.send(interaction, "playlistrepeated", ephemeral=True)
|
||||
respond = await update_user(interaction.user.id, {"$push": {'playlist.200.tracks': track.track_id}})
|
||||
respond = await func.update_user(interaction.user.id, {"$push": {'playlist.200.tracks': track.track_id}})
|
||||
if respond:
|
||||
await self.send(interaction, "playlistAdded", track.title, interaction.user.mention, user['200']['name'], ephemeral=True)
|
||||
else:
|
||||
@@ -201,19 +195,35 @@ class Add(ControlButton):
|
||||
|
||||
class Loop(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
self.btn_emojis: dict[str, str] = {
|
||||
"off": "🔁",
|
||||
"track": "🔂",
|
||||
"queue": "🔁"
|
||||
}
|
||||
|
||||
super().__init__(
|
||||
emoji="🔁",
|
||||
emoji=self.get_next_loop_emoji(kwargs["player"]),
|
||||
label="buttonLoop",
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_next_loop_emoji(self, player) -> str:
|
||||
current_repeat_mode = player.queue.repeat.lower()
|
||||
if current_repeat_mode not in self.btn_emojis:
|
||||
raise ValueError(f"Invalid repeat mode: {current_repeat_mode}")
|
||||
|
||||
return self.btn_emojis[current_repeat_mode]
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, 'missingPerms_mode', ephemeral=True)
|
||||
|
||||
mode = await self.player.set_repeat()
|
||||
mode = await self.player.set_repeat(requester=interaction.user)
|
||||
self.emoji = self.get_next_loop_emoji(self.player)
|
||||
|
||||
await interaction.response.edit_message(view=self.view)
|
||||
await self.send(interaction, 'repeat', mode.capitalize())
|
||||
|
||||
|
||||
class VolumeUp(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
@@ -364,8 +374,7 @@ class Tracks(discord.ui.Select):
|
||||
options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatted_length), emoji=track.emoji))
|
||||
|
||||
super().__init__(
|
||||
placeholder=player.get_msg("playerDropdown"),
|
||||
min_values=1, max_values=1,
|
||||
placeholder=self.player.get_msg("playerDropdown"),
|
||||
options=options,
|
||||
row=row
|
||||
)
|
||||
@@ -380,7 +389,39 @@ class Tracks(discord.ui.Select):
|
||||
if self.player.settings.get("controller_msg", True):
|
||||
await func.send(interaction, "skipped", interaction.user)
|
||||
|
||||
btnType = {
|
||||
class Effects(discord.ui.Select):
|
||||
def __init__(self, player, style, row):
|
||||
|
||||
self.player: voicelink.Player = player
|
||||
|
||||
options = [discord.SelectOption(label="None", value="None")]
|
||||
for name in voicelink.Filters.get_available_filters():
|
||||
options.append(discord.SelectOption(label=name.capitalize(), value=name))
|
||||
|
||||
super().__init__(
|
||||
placeholder=self.player.get_msg("playerFilter"),
|
||||
options=options,
|
||||
row=row
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await func.send(interaction, "missingPerms_function", ephemeral=True)
|
||||
|
||||
avalibable_filters = voicelink.Filters.get_available_filters()
|
||||
if self.values[0] == "None":
|
||||
await self.player.reset_filter(requester=interaction.user)
|
||||
return await func.send(interaction, "clearEffect")
|
||||
|
||||
selected_filter = avalibable_filters.get(self.values[0].lower())()
|
||||
if self.player.filters.has_filter(filter_tag=selected_filter.tag):
|
||||
await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user)
|
||||
await func.send(interaction, "clearEffect")
|
||||
else:
|
||||
await self.player.add_filter(selected_filter, requester=interaction.user)
|
||||
await func.send(interaction, "addEffect", selected_filter.tag)
|
||||
|
||||
BUTTONTYPE: Dict[str, ControlButton] = {
|
||||
"back": Back,
|
||||
"resume": Resume,
|
||||
"skip": Skip,
|
||||
@@ -390,14 +431,15 @@ btnType = {
|
||||
"volumeup": VolumeUp,
|
||||
"volumedown": VolumeDown,
|
||||
"volumemute": VolumeMute,
|
||||
"tracks": Tracks,
|
||||
"autoplay": AutoPlay,
|
||||
"shuffle": Shuffle,
|
||||
"forward": Forward,
|
||||
"rewind": Rewind
|
||||
"rewind": Rewind,
|
||||
"tracks": Tracks,
|
||||
"effects": Effects
|
||||
}
|
||||
|
||||
btnColor = {
|
||||
BUTTONCOLOR: Dict[str, discord.ButtonStyle] = {
|
||||
"blue": discord.ButtonStyle.primary,
|
||||
"grey": discord.ButtonStyle.secondary,
|
||||
"red": discord.ButtonStyle.danger,
|
||||
@@ -415,8 +457,8 @@ class InteractiveController(discord.ui.View):
|
||||
if isinstance(btn, Dict):
|
||||
color = list(btn.values())[0]
|
||||
btn = list(btn.keys())[0]
|
||||
btnClass = btnType.get(btn.lower())
|
||||
style = btnColor.get(color.lower(), btnColor["grey"])
|
||||
btnClass = BUTTONTYPE.get(btn.lower())
|
||||
style = BUTTONCOLOR.get(color.lower(), BUTTONCOLOR["grey"])
|
||||
if not btnClass or (self.player.queue.is_empty and btn == "tracks"):
|
||||
continue
|
||||
self.add_item(btnClass(player=player, style=style, row=row))
|
||||
@@ -448,4 +490,4 @@ class InteractiveController(discord.ui.View):
|
||||
elif isinstance(error, Exception):
|
||||
await interaction.response.send_message(error)
|
||||
|
||||
return
|
||||
return
|
||||
218
views/debug.py
218
views/debug.py
@@ -22,12 +22,14 @@ SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import function
|
||||
import io
|
||||
import contextlib
|
||||
import textwrap
|
||||
import traceback
|
||||
import voicelink
|
||||
import function as func
|
||||
|
||||
from typing import Optional
|
||||
from discord.ext import commands
|
||||
|
||||
class ExceuteModal(discord.ui.Modal):
|
||||
@@ -49,13 +51,76 @@ class ExceuteModal(discord.ui.Modal):
|
||||
self.code = self.children[0].value
|
||||
self.stop()
|
||||
|
||||
class AddNodeModal(discord.ui.Modal):
|
||||
def __init__(self, view, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.view: NodesPanel = view
|
||||
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Host",
|
||||
placeholder="Enter the lavalink host e.g 0.0.0.0"
|
||||
)
|
||||
)
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Port",
|
||||
placeholder="Enter the lavalink port e.g 2333"
|
||||
)
|
||||
)
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Password",
|
||||
placeholder="Enter the lavalink password"
|
||||
)
|
||||
)
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Secure",
|
||||
placeholder="Specify if your Lavalink uses SSL. Enter 'true' or 'false'"
|
||||
)
|
||||
)
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Identifier",
|
||||
placeholder="Enter a name for your lavalink server"
|
||||
)
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
try:
|
||||
config = {
|
||||
"host": self.children[0].value,
|
||||
"port": int(self.children[1].value),
|
||||
"password": self.children[2].value,
|
||||
"secure": self.children[3].value.lower() == "true",
|
||||
"identifier": self.children[4].value
|
||||
}
|
||||
except Exception:
|
||||
return await interaction.response.send_message("Some of your input is invalid! Please try again.", ephemeral=True)
|
||||
|
||||
await interaction.response.defer()
|
||||
try:
|
||||
await voicelink.NodePool.create_node(
|
||||
bot=interaction.client,
|
||||
spotify_client_id=func.settings.spotify_client_id,
|
||||
spotify_client_secret=func.settings.spotify_client_secret,
|
||||
logger=func.logger,
|
||||
**config
|
||||
)
|
||||
await interaction.followup.send(f"Node {self.children[4].value} is connected!", ephemeral=True)
|
||||
await self.view.message.edit(embed=self.view.build_embed(), view=self.view)
|
||||
|
||||
except Exception as e:
|
||||
return await interaction.followup.send(e, ephemeral=True)
|
||||
|
||||
class CogsDropdown(discord.ui.Select):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot: commands.Bot = bot
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select a cog to reload...",
|
||||
min_values=1, max_values=1,
|
||||
options=[discord.SelectOption(label="All", description="All the cogs")] +
|
||||
[
|
||||
discord.SelectOption(label=name.capitalize(), description=cog.description[:50])
|
||||
@@ -76,6 +141,42 @@ class CogsDropdown(discord.ui.Select):
|
||||
|
||||
await interaction.response.send_message(f"Reloaded `{selected}` sucessfully!", ephemeral=True)
|
||||
|
||||
class NodesDropdown(discord.ui.Select):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot: commands.Bot = bot
|
||||
self.view: NodesPanel
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select a node to edit...",
|
||||
options=self.get_nodes()
|
||||
)
|
||||
|
||||
def get_nodes(self) -> list[discord.SelectOption]:
|
||||
nodes = [
|
||||
discord.SelectOption(
|
||||
label=name,
|
||||
description=("🟢 Connected" if node._available else "🔴 Disconnected") + f" - Players: {node.player_count} ({node.latency if node._available else 0:.2f}ms)")
|
||||
for name, node in voicelink.NodePool._nodes.items()
|
||||
]
|
||||
|
||||
if not nodes:
|
||||
nodes = [discord.SelectOption(label="The node could not be found!")]
|
||||
|
||||
return nodes
|
||||
|
||||
def update(self) -> None:
|
||||
self.options = self.get_nodes()
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
selected_node = self.values[0]
|
||||
node = voicelink.NodePool._nodes.get(selected_node, None)
|
||||
if not node:
|
||||
return await interaction.response.send_message("The node could not be found!", ephemeral=True)
|
||||
|
||||
self.view.selected_node = node
|
||||
await interaction.response.defer()
|
||||
await self.view.message.edit(embed=self.view.build_embed(), view=self.view)
|
||||
|
||||
class ExceutePanel(discord.ui.View):
|
||||
def __init__(self, bot, *, timeout = 180):
|
||||
self.bot: commands.Bot = bot
|
||||
@@ -160,12 +261,104 @@ class ExceutePanel(discord.ui.View):
|
||||
result = ''.join(traceback.format_exception(self._error, self._error, self._error.__traceback__))
|
||||
await self.message.edit(content=f"```py\n{result}```")
|
||||
|
||||
class NodesPanel(discord.ui.View):
|
||||
def __init__(self, bot, *, timeout: float | None = 180):
|
||||
super().__init__(timeout=timeout)
|
||||
self.message: Optional[discord.Message] = None
|
||||
self.selected_node: Optional[voicelink.Node] = None
|
||||
|
||||
self.add_item(NodesDropdown(bot))
|
||||
|
||||
def update_btn_status(self) -> None:
|
||||
for child in self._children:
|
||||
if isinstance(child, discord.ui.Button) and child.label != "Add":
|
||||
child.disabled = self.selected_node is None
|
||||
|
||||
if isinstance(child, discord.ui.Select):
|
||||
child.update()
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
self.update_btn_status()
|
||||
embed = discord.Embed(title="📡 Nodes Panel", color=func.settings.embed_color)
|
||||
|
||||
if not voicelink.NodePool._nodes:
|
||||
embed.description = "```There are no nodes are connected!```"
|
||||
|
||||
else:
|
||||
for name, node in voicelink.NodePool._nodes.items():
|
||||
if self.selected_node and self.selected_node._identifier != node._identifier:
|
||||
continue
|
||||
|
||||
if node._available:
|
||||
total_memory = node.stats.used + node.stats.free
|
||||
embed.add_field(
|
||||
name=f"{name} Node - 🟢 Connected",
|
||||
value=f"```• ADDRESS: {node._host}:{node._port}\n" \
|
||||
f"• PLAYERS: {len(node._players)}\n" \
|
||||
f"• CPU: {node.stats.cpu_process_load:.1f}%\n" \
|
||||
f"• RAM: {func.format_bytes(node.stats.free)}/{func.format_bytes(total_memory, True)} ({(node.stats.free/total_memory) * 100:.1f}%)\n"
|
||||
f"• LATENCY: {node.latency:.2f}ms\n" \
|
||||
f"• UPTIME: {func.time(node.stats.uptime)}```"
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=f"{name} Node - 🔴 Disconnected",
|
||||
value=f"```• ADDRESS: {node._host}:{node._port}\n" \
|
||||
f"• PLAYERS: {len(node._players)}\nNo extra data is available for display```",
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
async def on_error(self, interaction: discord.Interaction, error, item) -> None:
|
||||
return await interaction.followup.send(error, ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="Add", emoji="➕", style=discord.ButtonStyle.green)
|
||||
async def add(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
modal = AddNodeModal(self, title="Create Node")
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
@discord.ui.button(label="Remove", emoji="➖", style=discord.ButtonStyle.red, disabled=True)
|
||||
async def remove(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not self.selected_node:
|
||||
return await interaction.response.send_message("Please ensure that you have selected a node!", ephemeral=True)
|
||||
|
||||
identifier = self.selected_node._identifier
|
||||
await self.selected_node.disconnect(remove_from_pool=True)
|
||||
|
||||
self.selected_node = None
|
||||
|
||||
await self.message.edit(embed=self.build_embed(), view=self)
|
||||
await interaction.response.send_message(f"Removed {identifier} Node from the bot.", ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="Reconnect", disabled=True, row=1)
|
||||
async def reconnect(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.defer()
|
||||
if self.selected_node.is_connected:
|
||||
await self.selected_node.disconnect()
|
||||
await self.selected_node.connect()
|
||||
await self.message.edit(embed=self.build_embed(), view=self)
|
||||
|
||||
@discord.ui.button(label="Connect", disabled=True, row=1)
|
||||
async def connect(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.defer()
|
||||
if not self.selected_node.is_connected:
|
||||
await self.selected_node.connect()
|
||||
await self.message.edit(embed=self.build_embed(), view=self)
|
||||
|
||||
@discord.ui.button(label="Disconnect", style=discord.ButtonStyle.red, disabled=True, row=1)
|
||||
async def disconnect(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.defer()
|
||||
if self.selected_node.is_connected:
|
||||
await self.selected_node.disconnect()
|
||||
|
||||
await self.message.edit(embed=self.build_embed(), view=self)
|
||||
|
||||
class CogsView(discord.ui.View):
|
||||
def __init__(self, bot, *, timeout: float | None = 180):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.add_item(CogsDropdown(bot))
|
||||
|
||||
|
||||
class DebugView(discord.ui.View):
|
||||
def __init__(self, bot, *, timeout: float | None = 180):
|
||||
self.bot: commands.Bot = bot
|
||||
@@ -177,13 +370,18 @@ class DebugView(discord.ui.View):
|
||||
async def run_command(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.panel.execute(interaction)
|
||||
|
||||
@discord.ui.button(label='Cogs', emoji="🔃")
|
||||
@discord.ui.button(label='Cogs', emoji="⚙️")
|
||||
async def reload_cog(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
return await interaction.response.send_message("Reload Cogs", view=CogsView(self.bot), ephemeral=True)
|
||||
|
||||
@discord.ui.button(label='Send Logs', emoji="📥", style=discord.ButtonStyle.red)
|
||||
async def send_error_logs(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not function.ERROR_LOGS:
|
||||
return await interaction.response.send_message("Sorry there are not error logs!", ephemeral=True)
|
||||
|
||||
await interaction.response.send_message(file=function.gen_report(), ephemeral=True)
|
||||
@discord.ui.button(label="Re-Sync", emoji="🔄")
|
||||
async def sync(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message("🔄 Synchronizing all your commands and language settings!", ephemeral=True)
|
||||
await self.bot.tree.sync()
|
||||
await interaction.edit_original_response(content="✅ All commands and settings have been successfully synchronized!")
|
||||
|
||||
@discord.ui.button(label="Nodes", emoji="📡")
|
||||
async def nodes(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
view = NodesPanel(self.bot)
|
||||
await interaction.response.send_message(embed=view.build_embed(), view=view, ephemeral=True)
|
||||
view.message = await interaction.original_response()
|
||||
@@ -56,8 +56,8 @@ class HelpView(discord.ui.View):
|
||||
self.response: discord.Message = None
|
||||
self.categorys: list[str] = [ name.capitalize() for name, cog in bot.cogs.items() if len([c for c in cog.walk_commands()]) ]
|
||||
|
||||
self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.settings.invite_link))
|
||||
self.add_item(discord.ui.Button(label='Invite', emoji=':invite:915152589056790589', url=f'https://discord.com/oauth2/authorize?client_id={bot.user.id}&permissions=2184260928&scope=bot%20applications.commands'))
|
||||
self.add_item(discord.ui.Button(label='Website', emoji='🌎', url='https://vocard.xyz'))
|
||||
self.add_item(discord.ui.Button(label='Document', emoji=':support:915152950471581696', url='https://docs.vocard.xyz'))
|
||||
self.add_item(discord.ui.Button(label='Github', emoji=':github:1098265017268322406', 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))
|
||||
|
||||
@@ -21,7 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import discord, time
|
||||
import function as func
|
||||
|
||||
from typing import Any
|
||||
@@ -63,7 +63,7 @@ class InboxView(discord.ui.View):
|
||||
)
|
||||
|
||||
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')}```")
|
||||
embed.add_field(name="Message Info:", value=f"```{self.current['description']}\nSender ID: {self.current['sender']}\nPlaylist ID: {self.current['referId']}\nInvite Time: {time.strftime('%d-%m %H:%M:%S', time.gmtime(int(self.current['time'])))}```")
|
||||
return embed
|
||||
|
||||
async def button_change(self, interaction: discord.Interaction):
|
||||
|
||||
@@ -59,6 +59,17 @@ class SearchType(Enum):
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
class RequestMethod(Enum):
|
||||
"""The enum for the different request methods in Voicelink
|
||||
"""
|
||||
get = "get"
|
||||
patch = "patch"
|
||||
delete = "delete"
|
||||
post = "post"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
class NodeAlgorithm(Enum):
|
||||
"""The enum for the different node algorithms in Voicelink.
|
||||
|
||||
@@ -77,6 +88,7 @@ class NodeAlgorithm(Enum):
|
||||
# We don't have to define anything special for these, since these just serve as flags
|
||||
by_ping = auto()
|
||||
by_region = auto()
|
||||
by_players = auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -37,9 +37,19 @@ class Filter:
|
||||
these filters will not work.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.payload: Dict[str, List] = None
|
||||
self.payload: Dict[str, float] = None
|
||||
self.scope: Dict[str, List[int]] = None
|
||||
self.tag: str = None
|
||||
|
||||
def _init_with_scope(self, scope: Dict[str, List[int]], **kwargs):
|
||||
self.scope = scope
|
||||
for prop, (min_val, max_val) in scope.items():
|
||||
setattr(self, prop, kwargs.get(prop, scope[prop][0]))
|
||||
if not min_val <= getattr(self, prop) <= max_val:
|
||||
raise FilterInvalidArgument(f"{self.__class__.__name__} {prop} must be between {min_val} and {max_val}.")
|
||||
self.tag = kwargs.get("tag")
|
||||
self.payload = {self.__class__.__name__.lower(): {prop: getattr(self, prop) for prop in scope}}
|
||||
|
||||
class Filters:
|
||||
def __init__(self) -> None:
|
||||
self._filters: List[Filter] = []
|
||||
@@ -71,6 +81,20 @@ class Filters:
|
||||
|
||||
def get_filters(self) -> List[Filter]:
|
||||
return self._filters
|
||||
|
||||
@classmethod
|
||||
def get_available_filters(cls) -> Dict[str, Filter]:
|
||||
return {
|
||||
"karaoke": Karaoke,
|
||||
"tremolo": Tremolo,
|
||||
"vibrato": Vibrato,
|
||||
"rotation": Rotation,
|
||||
"distortion": Distortion,
|
||||
"lowpass": LowPass,
|
||||
"nightcore": Timescale.nightcore,
|
||||
"vaporwave": Timescale.vaporwave,
|
||||
"8d": Rotation.nightD,
|
||||
}
|
||||
|
||||
class Equalizer(Filter):
|
||||
"""
|
||||
@@ -80,7 +104,7 @@ class Equalizer(Filter):
|
||||
The format for the levels is: List[Tuple[int, float]]
|
||||
"""
|
||||
|
||||
def __init__(self, *, tag: str, levels: list):
|
||||
def __init__(self, *, tag: str = "equalizer", levels: list):
|
||||
super().__init__()
|
||||
|
||||
self.eq = self._factory(levels)
|
||||
@@ -156,7 +180,6 @@ class Equalizer(Filter):
|
||||
]
|
||||
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,
|
||||
@@ -167,32 +190,17 @@ class Timescale(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
tag: str = "timescale",
|
||||
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
|
||||
}
|
||||
}
|
||||
self._init_with_scope({
|
||||
"speed": [0, 5],
|
||||
"pitch": [0, 5],
|
||||
"rate": [0, 5]
|
||||
}, tag=tag, speed=speed, pitch=pitch, rate=rate)
|
||||
|
||||
@classmethod
|
||||
def vaporwave(cls):
|
||||
@@ -215,8 +223,7 @@ class Timescale(Filter):
|
||||
return cls(tag="nightcore", speed=1.25, pitch=1.3)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.TimescaleFilter tag={self.tag} speed={self.speed} pitch={self.pitch} rate={self.rate}>"
|
||||
|
||||
return f"<Voicelink.TimescaleFilter tag={self.tag} payload={self.payload}>"
|
||||
|
||||
class Karaoke(Filter):
|
||||
"""Filter which filters the vocal track from any song and leaves the instrumental.
|
||||
@@ -226,35 +233,22 @@ class Karaoke(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
tag: str = "karaoke",
|
||||
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
|
||||
}
|
||||
}
|
||||
self._init_with_scope({
|
||||
"level": [0, 5],
|
||||
"monoLevel": [0, 5],
|
||||
"filterBand": [0, 500],
|
||||
"filterWidth": [0, 300]
|
||||
}, tag=tag, level=level, mono_level=mono_level, filter_band=filter_band, filter_width=filter_width)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Voicelink.KaraokeFilter tag={self.tag} level={self.level} mono_level={self.mono_level} "
|
||||
f"filter_band={self.filter_band} filter_width={self.filter_width}>"
|
||||
)
|
||||
|
||||
return (f"<Voicelink.KaraokeFilter tag={self.tag} payload={self.payload}")
|
||||
|
||||
class Tremolo(Filter):
|
||||
"""Filter which produces a wavering tone in the music,
|
||||
@@ -264,31 +258,18 @@ class Tremolo(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
tag: str = "tremolo",
|
||||
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
|
||||
}
|
||||
}
|
||||
self._init_with_scope({
|
||||
"frequency": [0, 5],
|
||||
"depth": [0, 1]
|
||||
}, tag=tag, frequency=frequency, depth=depth)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.TremoloFilter tag={self.tag} frequency={self.frequency} depth={self.depth}>"
|
||||
|
||||
return f"<Voicelink.TremoloFilter tag={self.tag} payload={self.payload}"
|
||||
|
||||
class Vibrato(Filter):
|
||||
"""Filter which produces a wavering tone in the music, similar to the Tremolo filter,
|
||||
@@ -298,51 +279,36 @@ class Vibrato(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
tag: str = "vibrato",
|
||||
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
|
||||
}
|
||||
}
|
||||
self._init_with_scope({
|
||||
"frequency": [0, 14],
|
||||
"depth": [0, 1]
|
||||
}, tag=tag, frequenc=frequency, depth=depth)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Voicelink.VibratoFilter tag={self.tag} frequency={self.frequency} depth={self.depth}>"
|
||||
|
||||
return f"<Voicelink.VibratoFilter tag={self.tag} payload={self.payload}"
|
||||
|
||||
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):
|
||||
def __init__(self, *, tag: str = "rotation", rotation_hertz: float = 5):
|
||||
super().__init__()
|
||||
|
||||
self.rotation_hertz = rotation_hertz
|
||||
self.tag = tag
|
||||
self.payload = {"rotation": {"rotationHz": self.rotation_hertz}}
|
||||
self._init_with_scope({
|
||||
"rotationHz": [0, 10]
|
||||
}, tag=tag, rotationHz=rotation_hertz)
|
||||
|
||||
@classmethod
|
||||
def nightD(cls):
|
||||
return cls(tag="8d", rotation_hertz=0.2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.RotationFilter tag={self.tag} rotation_hertz={self.rotation_hertz}>"
|
||||
|
||||
return f"<Voicelink.RotationFilter tag={self.tag} payload={self.payload}"
|
||||
|
||||
class ChannelMix(Filter):
|
||||
"""Filter which manually adjusts the panning of the audio, which can make
|
||||
@@ -352,43 +318,22 @@ class ChannelMix(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
tag: str = "channelMix",
|
||||
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
|
||||
}
|
||||
}
|
||||
self._init_with_scope({
|
||||
"leftToLeft": [0, 1],
|
||||
"leftToRight": [0, 1],
|
||||
"rightToLeft": [0, 1],
|
||||
"rightToRight": [0, 1]
|
||||
}, tag=tag, leftToLeft=left_to_left, leftToRight=left_to_right, rightToLeft=right_to_left, rightToRight=right_to_right)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.ChannelMix tag={self.tag} left_to_left={self.left_to_left} left_to_right={self.left_to_right} "
|
||||
f"right_to_left={self.right_to_left} right_to_right={self.right_to_right}>"
|
||||
)
|
||||
return (f"<Voicelink.ChannelMix tag={self.tag} payload={self.payload}")
|
||||
|
||||
class Distortion(Filter):
|
||||
"""Filter which generates a distortion effect. Useful for certain filter implementations where
|
||||
@@ -398,7 +343,7 @@ class Distortion(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tag: str,
|
||||
tag: str = "distortion",
|
||||
sin_offset: float = 0,
|
||||
sin_scale: float = 1,
|
||||
cos_offset: float = 0,
|
||||
@@ -409,51 +354,39 @@ class Distortion(Filter):
|
||||
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
|
||||
}
|
||||
}
|
||||
self._init_with_scope({
|
||||
"sinOffset": [0, 1],
|
||||
"sinScale": [0, 1],
|
||||
"cosOffset": [0, 1],
|
||||
"cosScale": [0, 1],
|
||||
"tanOffset": [0, 1],
|
||||
"tanScale": [0, 1],
|
||||
"offset": [0, 1],
|
||||
"scale": [0, 1],
|
||||
},
|
||||
tag = tag,
|
||||
sinOffset = sin_offset,
|
||||
sinScale = sin_scale,
|
||||
cosOffset = cos_offset,
|
||||
cosScale = cos_scale,
|
||||
tanOffset = tan_offset,
|
||||
tanScale = tan_scale,
|
||||
offset = offset,
|
||||
scale = scale)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Voicelink.Distortion tag={self.tag} sin_offset={self.sin_offset} sin_scale={self.sin_scale}> "
|
||||
f"cos_offset={self.cos_offset} cos_scale={self.cos_scale} tan_offset={self.tan_offset} "
|
||||
f"tan_scale={self.tan_scale} offset={self.offset} scale={self.scale}"
|
||||
)
|
||||
|
||||
return (f"<Voicelink.Distortion tag={self.tag} payload={self.payload}")
|
||||
|
||||
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):
|
||||
def __init__(self, *, tag: str = "lowpass", smoothing: float = 20):
|
||||
super().__init__()
|
||||
|
||||
self.smoothing = smoothing
|
||||
self.tag = tag
|
||||
self.payload = {"lowPass": {"smoothing": self.smoothing}}
|
||||
self._init_with_scope({
|
||||
"smoothing": [0, 100]
|
||||
}, tag = tag, smoothing = smoothing)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.LowPass tag={self.tag} smoothing={self.smoothing}>"
|
||||
|
||||
|
||||
return f"<Voicelink.LowPass tag={self.tag} payload={self.payload}>"
|
||||
@@ -62,7 +62,8 @@ class Track:
|
||||
"requester",
|
||||
"is_stream",
|
||||
"is_seekable",
|
||||
"position"
|
||||
"position",
|
||||
"end_time"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -102,6 +103,8 @@ class Track:
|
||||
self.is_seekable: bool = info.get("isSeekable", True)
|
||||
self.position: int = info.get("position", 0)
|
||||
|
||||
self.end_time: Optional[int] = None
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Track):
|
||||
return False
|
||||
|
||||
@@ -32,9 +32,11 @@ class Placeholders:
|
||||
"track_duration": self.track_duration,
|
||||
"track_thumbnail": self.track_thumbnail,
|
||||
"track_color": self.track_color,
|
||||
"requester": self.requester,
|
||||
"requester_name": self.requester_name,
|
||||
"requester_avatar": self.requester_avatar,
|
||||
"track_requester_id": self.track_requester_id,
|
||||
"track_requester_name": self.track_requester_name,
|
||||
"track_requester_avatar": self.track_requester_avatar,
|
||||
"track_source_name": self.track_source_name,
|
||||
"track_source_emoji": self.track_source_emoji,
|
||||
"queue_length": self.queue_length,
|
||||
"volume": self.volume,
|
||||
"dj": self.dj,
|
||||
@@ -70,21 +72,29 @@ class Placeholders:
|
||||
return self.player.get_msg("live") if track.is_stream else func.time(track.length)
|
||||
|
||||
@ensure_track
|
||||
def requester(self, track: Track) -> str:
|
||||
return track.requester.mention if track.requester else self.bot.user.mention
|
||||
def track_requester_id(self, track: Track) -> str:
|
||||
return str(track.requester.id if track.requester else self.bot.user.id)
|
||||
|
||||
@ensure_track
|
||||
def requester_name(self, track: Track) -> str:
|
||||
def track_requester_name(self, track: Track) -> str:
|
||||
return track.requester.name if track.requester else self.bot.user.display_name
|
||||
|
||||
@ensure_track
|
||||
def requester_avatar(self, track: Track) -> str:
|
||||
def track_requester_avatar(self, track: Track) -> str:
|
||||
return track.requester.display_avatar.url if track.requester else self.bot.user.display_avatar.url
|
||||
|
||||
@ensure_track
|
||||
def track_color(self, track: Track) -> int:
|
||||
return int(func.get_source(track.source, "color"), 16)
|
||||
|
||||
@ensure_track
|
||||
def track_source_name(self, track: Track) -> str:
|
||||
return track.source
|
||||
|
||||
@ensure_track
|
||||
def track_source_emoji(self, track: Track) -> str:
|
||||
return track.emoji
|
||||
|
||||
def track_thumbnail(self) -> str:
|
||||
if not self.player or not self.player.current:
|
||||
return "https://i.imgur.com/dIFBwU7.png"
|
||||
|
||||
@@ -21,19 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
import time, logging
|
||||
import function as func
|
||||
|
||||
from math import ceil
|
||||
from asyncio import sleep
|
||||
from views import InteractiveController
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Optional,
|
||||
Union,
|
||||
List
|
||||
)
|
||||
from typing import Any, Dict, List, Optional, Union, Tuple
|
||||
|
||||
from discord import (
|
||||
Client,
|
||||
@@ -41,14 +35,14 @@ from discord import (
|
||||
VoiceChannel,
|
||||
VoiceProtocol,
|
||||
Member,
|
||||
ui,
|
||||
Message,
|
||||
Interaction
|
||||
Interaction,
|
||||
errors
|
||||
)
|
||||
|
||||
from discord.ext import commands
|
||||
from . import events
|
||||
from .enums import SearchType, LoopType
|
||||
from .enums import SearchType, LoopType, RequestMethod
|
||||
from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent
|
||||
from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, TrackLoadError, FilterTagAlreadyInUse, DuplicateTrack
|
||||
from .filters import Filter, Filters
|
||||
@@ -75,8 +69,9 @@ async def connect_channel(ctx: Union[commands.Context, Interaction], channel: Vo
|
||||
ctx.bot if isinstance(ctx, commands.Context) else ctx.client,
|
||||
channel, ctx, settings
|
||||
))
|
||||
|
||||
await player.send_ws({"op": "createPlayer", "members_id": [member.id for member in channel.members]})
|
||||
|
||||
if ctx.bot.ipc.is_connected:
|
||||
await player.send_ws({"op": "createPlayer", "member_ids": [str(member.id) for member in channel.members]})
|
||||
|
||||
return player
|
||||
|
||||
@@ -103,11 +98,13 @@ class Player(VoiceProtocol):
|
||||
):
|
||||
self.client: Client = client
|
||||
self._bot: Client = client
|
||||
self._ipc = self._bot.ipc
|
||||
self._ipc_connection = False
|
||||
|
||||
self.context = ctx
|
||||
self.dj: Member = ctx.user if isinstance(ctx, Interaction) else ctx.author
|
||||
self.channel: VoiceChannel = channel
|
||||
self._guild = channel.guild if channel else None
|
||||
self._ipc_connection: bool = False
|
||||
|
||||
self.settings: dict = settings
|
||||
self.joinTime: float = round(time.time())
|
||||
@@ -139,7 +136,8 @@ class Player(VoiceProtocol):
|
||||
self.shuffle_votes = set()
|
||||
self.stop_votes = set()
|
||||
|
||||
self.ph = Placeholders(client, self)
|
||||
self._ph = Placeholders(client, self)
|
||||
self._logger: Optional[logging.Logger] = self._node._logger
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
@@ -238,7 +236,7 @@ class Player(VoiceProtocol):
|
||||
|
||||
@property
|
||||
def is_ipc_connected(self) -> bool:
|
||||
return bool(self._ipc_connection and len(self.bot.ipc.connections))
|
||||
return self._ipc._is_connected and self._ipc_connection
|
||||
|
||||
def is_user_join(self, user: Member):
|
||||
if user not in self.channel.members:
|
||||
@@ -258,12 +256,18 @@ class Player(VoiceProtocol):
|
||||
return manage_perm or (self.settings['dj'] in [role.id for role in user.roles])
|
||||
return self.dj.id == user.id or manage_perm
|
||||
|
||||
async def send(self, method: RequestMethod, query: str = None, data: Union[Dict, str] = {}) -> Dict:
|
||||
uri: str = f"sessions/{self._node._session_id}/players/{self._guild.id}" + (f"?{query}" if query else "")
|
||||
return await self._node.send(method, query=uri, data=data)
|
||||
|
||||
async def _update_state(self, data: dict) -> None:
|
||||
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")
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) update state with data {data}")
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "playerUpdate",
|
||||
@@ -272,18 +276,21 @@ class Player(VoiceProtocol):
|
||||
"last_position": self._last_position
|
||||
})
|
||||
|
||||
async def _dispatch_voice_update(self, voice_data: Dict[str, Any]):
|
||||
async def _dispatch_voice_update(self, voice_data: Dict[str, Any] = None):
|
||||
if {"sessionId", "event"} != self._voice_state.keys():
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update failed {voice_data}")
|
||||
return
|
||||
|
||||
await self._node.send(
|
||||
method=0, guild_id=self._guild.id,
|
||||
data = {"voice": {
|
||||
"token": voice_data['event']['token'],
|
||||
"endpoint": voice_data['event']['endpoint'],
|
||||
"sessionId": voice_data['sessionId'],
|
||||
}}
|
||||
)
|
||||
state = voice_data or self._voice_state
|
||||
|
||||
data = {
|
||||
"token": state['event']['token'],
|
||||
"endpoint": state['event']['endpoint'],
|
||||
"sessionId": state['sessionId'],
|
||||
}
|
||||
|
||||
await self.send(method=RequestMethod.patch, data={"voice": data})
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update to {state['event']['endpoint']} with data {data}")
|
||||
|
||||
async def on_voice_server_update(self, data: dict):
|
||||
self._voice_state.update({"event": data})
|
||||
@@ -316,8 +323,10 @@ class Player(VoiceProtocol):
|
||||
if isinstance(event, TrackStartEvent):
|
||||
self._ending_track = self._current
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched event {event_type}.")
|
||||
|
||||
async def do_next(self):
|
||||
if self.is_playing or not self.channel:
|
||||
if self._current or self.is_playing or not self.channel:
|
||||
return
|
||||
|
||||
if self._paused:
|
||||
@@ -337,7 +346,7 @@ class Player(VoiceProtocol):
|
||||
self.shuffle_votes.clear()
|
||||
self.stop_votes.clear()
|
||||
|
||||
track: Track = self.queue.get()
|
||||
track = self.queue.get()
|
||||
|
||||
if not track:
|
||||
if self.settings.get("autoplay", False) and await self.get_recommendations():
|
||||
@@ -345,18 +354,21 @@ class Player(VoiceProtocol):
|
||||
else:
|
||||
try:
|
||||
await self.play(track, start=track.position)
|
||||
except:
|
||||
except Exception as e:
|
||||
self._logger.error(f"Something went wrong while playing music in {self.guild.name}({self.guild.id})", exc_info=e)
|
||||
await sleep(5)
|
||||
return await self.do_next()
|
||||
|
||||
if not track.requester.bot:
|
||||
await func.update_user(track.requester.id, {
|
||||
self._bot.loop.create_task(func.update_user(track.requester.id, {
|
||||
"$push": {"history": {"$each": [track.track_id], "$slice": -25}}
|
||||
})
|
||||
}))
|
||||
|
||||
if self.settings.get('controller', True):
|
||||
await self.invoke_controller()
|
||||
|
||||
await self.update_voice_status()
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "trackUpdate",
|
||||
@@ -371,28 +383,27 @@ class Player(VoiceProtocol):
|
||||
|
||||
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
|
||||
try:
|
||||
embed, view = await self.build_embed(), InteractiveController(self)
|
||||
if not self.controller:
|
||||
self.controller = await self.context.channel.send(embed=embed, view=view)
|
||||
|
||||
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:
|
||||
elif not await self.is_position_fresh():
|
||||
try:
|
||||
await self.controller.edit(embed=embed, view=InteractiveController(self))
|
||||
await self.controller.delete()
|
||||
except:
|
||||
pass
|
||||
self.controller = await self.context.channel.send(embed=embed, view=view)
|
||||
|
||||
else:
|
||||
await self.controller.edit(embed=embed, view=view)
|
||||
|
||||
except errors.Forbidden:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self._logger.error(f"Something went wrong while sending music controller to {self.guild.name}({self.guild.id})", exc_info=e)
|
||||
pass
|
||||
|
||||
self.updating = False
|
||||
|
||||
@@ -400,7 +411,7 @@ class Player(VoiceProtocol):
|
||||
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
|
||||
raw = controller.get("active" if self.current else "inactive", {})
|
||||
|
||||
return build_embed(raw, self.ph)
|
||||
return build_embed(raw, self._ph)
|
||||
|
||||
async def is_position_fresh(self):
|
||||
try:
|
||||
@@ -420,7 +431,7 @@ class Player(VoiceProtocol):
|
||||
"playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2)
|
||||
}}
|
||||
)
|
||||
|
||||
await self.update_voice_status(remove_status=True)
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "playerClose"})
|
||||
|
||||
@@ -457,10 +468,13 @@ class Player(VoiceProtocol):
|
||||
self._node._players[self.guild.id] = self
|
||||
self._is_connected = True
|
||||
|
||||
if self.channel:
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been connected to {self.channel.name}({self.channel.id}).")
|
||||
|
||||
async def stop(self):
|
||||
"""Stops the currently playing track."""
|
||||
self._current = None
|
||||
await self._node.send(method=0, guild_id=self._guild.id, data={'encodedTrack': None})
|
||||
await self.send(method=RequestMethod.patch, data={'encodedTrack': None})
|
||||
|
||||
async def disconnect(self, *, force: bool = False):
|
||||
"""Disconnects the player from voice."""
|
||||
@@ -470,6 +484,8 @@ class Player(VoiceProtocol):
|
||||
self.cleanup()
|
||||
self._is_connected = False
|
||||
self.channel = None
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been disconnected from a voice channel.")
|
||||
|
||||
async def destroy(self):
|
||||
"""Disconnects and destroys the player, and runs internal cleanup."""
|
||||
@@ -482,8 +498,8 @@ class Player(VoiceProtocol):
|
||||
assert self.channel is None and not self.is_connected
|
||||
|
||||
self._node._players.pop(self.guild.id)
|
||||
await self._node.send(method=1, guild_id=self._guild.id)
|
||||
|
||||
await self.send(method=RequestMethod.delete)
|
||||
|
||||
async def play(
|
||||
self,
|
||||
track: Track,
|
||||
@@ -498,88 +514,126 @@ class Player(VoiceProtocol):
|
||||
|
||||
if track.spotify:
|
||||
if not track.original:
|
||||
search: Track = (await self._node.get_tracks(
|
||||
f"ytsearch:{track.author} - {track.title}",
|
||||
requester=track.requester
|
||||
))
|
||||
|
||||
if not search:
|
||||
raise TrackLoadError("Can't not found a playable source!")
|
||||
search_results = await self._node.get_tracks(f"ytmsearch:{track.author} - {track.title}", requester=track.requester)
|
||||
if not search_results:
|
||||
raise TrackLoadError("Can't find a playable source!")
|
||||
track.original = search_results[0]
|
||||
|
||||
track.original = search[0]
|
||||
|
||||
data = {
|
||||
"encodedTrack": track.original.track_id if track.original else track.track_id,
|
||||
"position": str(start)
|
||||
"position": str(start if start else track.position)
|
||||
}
|
||||
|
||||
if end > 0:
|
||||
data["endTime"] = str(end)
|
||||
|
||||
await self._node.send(
|
||||
method=0, guild_id=self._guild.id,
|
||||
data=data,
|
||||
query=f"noReplace={ignore_if_playing}"
|
||||
)
|
||||
if end or track.end_time:
|
||||
data["endTime"] = str(end if end else track.end_time)
|
||||
|
||||
await self.send(method=RequestMethod.patch, query=f"noReplace={ignore_if_playing}", data=data)
|
||||
|
||||
self._current = track
|
||||
|
||||
if self.volume != 100:
|
||||
await self.set_volume(self.volume)
|
||||
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) playing {track.title} from uri {track.uri} with a length of {track.length}")
|
||||
return self._current
|
||||
|
||||
async def add_track(self, raw_tracks: Union[Track, List[Track]], *, at_font: bool = False, duplicate: bool = True) -> int:
|
||||
tracks = []
|
||||
def _validate_time(self, track: Track, start_time: int, end_time: int) -> None:
|
||||
if start_time or end_time:
|
||||
if not end_time:
|
||||
end_time = track.length
|
||||
|
||||
_duplicate_tracks = () if self.queue._allow_duplicate and duplicate else (track.uri for track in self.queue._queue)
|
||||
if start_time >= end_time:
|
||||
raise VoicelinkException(self.get_msg("invalidStartTime"))
|
||||
|
||||
track_length = track.length
|
||||
if not 0 <= start_time <= track_length:
|
||||
raise VoicelinkException(self.get_msg("invalidStartTime", func.time(track_length)))
|
||||
if not 0 <= end_time <= track_length:
|
||||
raise VoicelinkException(self.get_msg("invalidEndTime", func.time(track_length)))
|
||||
|
||||
track.position = start_time
|
||||
track.end_time = end_time
|
||||
|
||||
async def add_track(self, raw_tracks: Union[Track, List[Track]], *, start_time: int = 0, end_time: int = 0, at_front: bool = False, duplicate: bool = True) -> int:
|
||||
tracks: List[Track] = []
|
||||
_duplicate_tracks = [] if self.queue._allow_duplicate and duplicate else [track.uri for track in self.queue._queue]
|
||||
raw_tracks = raw_tracks[0] if isinstance(raw_tracks, List) and len(raw_tracks) == 1 else raw_tracks
|
||||
|
||||
try:
|
||||
if (isList := isinstance(raw_tracks, List)):
|
||||
if (is_list := isinstance(raw_tracks, List)):
|
||||
for track in raw_tracks:
|
||||
if track.uri in _duplicate_tracks:
|
||||
continue
|
||||
self.queue.put_at_front(track) if at_font else self.queue.put(track)
|
||||
|
||||
self._validate_time(track, start_time, end_time)
|
||||
self.queue.put_at_front(track) if at_front else self.queue.put(track)
|
||||
tracks.append(track)
|
||||
_duplicate_tracks.append(track.uri)
|
||||
else:
|
||||
if raw_tracks.uri in _duplicate_tracks:
|
||||
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
|
||||
|
||||
position = self.queue.put_at_front(raw_tracks) if at_font else self.queue.put(raw_tracks)
|
||||
self._validate_time(raw_tracks, start_time, end_time)
|
||||
position = self.queue.put_at_front(raw_tracks) if at_front else self.queue.put(raw_tracks)
|
||||
tracks.append(raw_tracks)
|
||||
|
||||
finally:
|
||||
if tracks:
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "addTrack", "tracks": [track.track_id for track in tracks]}, tracks[0].requester)
|
||||
return len(tracks) if isList else position
|
||||
|
||||
await self.send_ws({"op": "addTrack", "tracks": [track.track_id for track in tracks], "position": -1 if is_list else position}, tracks[0].requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been added {len(tracks)} tracks into the queue.")
|
||||
return len(tracks) if is_list else position
|
||||
|
||||
async def remove_track(self, index: int, index2: int = None, remove_target: Member = None, requester: Member = None) -> Dict[int, Track]:
|
||||
removed_tracks = self.queue.remove(index, index2, remove_target)
|
||||
if removed_tracks and self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "removeTrack",
|
||||
"indexes": list(removed_tracks.keys()),
|
||||
"first_track_id": list(removed_tracks.values())[0].track_id
|
||||
}, requester=requester)
|
||||
|
||||
return removed_tracks
|
||||
|
||||
async def seek(self, position: float, requester: Member = None) -> 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(method=0, guild_id=self._guild.id, data={"position": position})
|
||||
await self.send(method=RequestMethod.patch, data={"position": position})
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "updatePosition", "position": position}, requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been seeking to {position}.")
|
||||
return self._position
|
||||
|
||||
async def set_pause(self, pause: bool, requester: Member = None) -> bool:
|
||||
"""Sets the pause state of the currently playing track."""
|
||||
await self._node.send(method=0, guild_id=self._guild.id, data={"paused": pause})
|
||||
|
||||
self._paused = pause
|
||||
self.pause_votes.clear() if pause else self.resume_votes.clear()
|
||||
await self.send(method=RequestMethod.patch, data={"paused": pause})
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "updatePause", "pause": pause}, requester)
|
||||
self._paused = pause
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been {'paused' if pause else 'resumed'}.")
|
||||
return self._paused
|
||||
|
||||
async def set_volume(self, volume: int, requester: Member = None) -> int:
|
||||
"""Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500."""
|
||||
await self._node.send(method=0, guild_id=self._guild.id, data={"volume": volume})
|
||||
await self.send(method=RequestMethod.patch, data={"volume": volume})
|
||||
self._volume = volume
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "updateVolume", "volume": volume}, requester)
|
||||
self._volume = volume
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been update the volume to {volume}.")
|
||||
return self._volume
|
||||
|
||||
async def shuffle(self, queue_type: str, requester: Member = None) -> None:
|
||||
"""Shuffles the tracks in the specified queue or history."""
|
||||
replacement = self.queue.tracks() if queue_type == "queue" else self.queue.history()
|
||||
if len(replacement) < 3:
|
||||
raise VoicelinkException(self.get_msg('shuffleError'))
|
||||
@@ -590,14 +644,31 @@ class Player(VoiceProtocol):
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "shuffleTrack",
|
||||
"tracks": [track.track_id for track in self.queue._queue],
|
||||
"verified": {
|
||||
"index": self.queue._position if queue_type == "queue" else 0,
|
||||
"track_id": replacement[0].track_id,
|
||||
}
|
||||
"tracks": [{"track_id": track.track_id, "requester_id": str(track.requester.id)} for track in replacement],
|
||||
"queue_type": queue_type
|
||||
}, requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been shuffled the queue.")
|
||||
|
||||
async def set_repeat(self, mode: str = None) -> str:
|
||||
async def swap_track(self, index1: int, index2: int, requester: Member = None) -> Tuple[Track, Track]:
|
||||
track1, track2 = self.queue.swap(index1, index2)
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "swapTrack",
|
||||
"index1": {"index": index1, "trackId": track1.track_id},
|
||||
"index2": {"index": index2, "trackId": track2.track_id}
|
||||
}, requester)
|
||||
return track1, track2
|
||||
|
||||
async def move_track(self, index: int, new_index: int, requester: Member = None) -> Optional[Track]:
|
||||
moved_track = self.queue.move(index, new_index)
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "moveTrack", "movedTrack": {"index": index, "trackId": moved_track.track_id}, "newIndex": new_index}, requester)
|
||||
|
||||
return moved_track
|
||||
|
||||
async def set_repeat(self, mode: str = None, requester: Member = None) -> str:
|
||||
if not mode:
|
||||
mode = self.queue._repeat.next().name
|
||||
|
||||
@@ -612,37 +683,78 @@ class Player(VoiceProtocol):
|
||||
raise VoicelinkException("Invalid repeat mode.")
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "repeatTrack", "repeatMode": mode})
|
||||
await self.send_ws({"op": "repeatTrack", "repeatMode": mode}, requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been update the repeat mode to {mode}.")
|
||||
return mode
|
||||
|
||||
async def add_filter(self, filter: Filter, fast_apply=False) -> Filters:
|
||||
async def add_filter(self, filter: Filter, requester: Member = None, fast_apply: bool = 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(method=0, guild_id=self._guild.id, data={"filters": 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(method=0, guild_id=self._guild.id, data={"filters": payload})
|
||||
await self.send(method=RequestMethod.patch, data={"filters": payload})
|
||||
if fast_apply:
|
||||
await self.seek(self.position)
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "updateFilter",
|
||||
"filter": {"tag": filter.tag, "scope": filter.scope, "payload": filter.payload},
|
||||
"type": "add"
|
||||
}, requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been applied a {filter.tag} filter.")
|
||||
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(method=0, guild_id=self._guild.id, data={"filters": {}})
|
||||
|
||||
async def clear_queue(self, queue_type: str, requester: Member = None) -> None:
|
||||
queue_type = queue_type.lower()
|
||||
if queue_type == 'history':
|
||||
self.queue.history_clear(self.is_playing)
|
||||
elif queue_type == "queue":
|
||||
self.queue.clear()
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "clearQueue",
|
||||
"queue_type": queue_type
|
||||
}, requester)
|
||||
|
||||
async def remove_filter(self, filter_tag: str, requester: Member = None, fast_apply: bool = False) -> Filters:
|
||||
self._filters.remove_filter(filter_tag=filter_tag)
|
||||
payload = self._filters.get_all_payloads()
|
||||
await self.send(method=RequestMethod.patch, data={"filters": payload})
|
||||
if fast_apply:
|
||||
await self.seek(self.position)
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "updateFilter",
|
||||
"filter": {"tag": filter_tag},
|
||||
"type": "remove"
|
||||
}, requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been removed a {filter_tag} filter.")
|
||||
return self._filters
|
||||
|
||||
async def reset_filter(self, *, requester: Member = None, 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.send(method=RequestMethod.patch, data={"filters": {}})
|
||||
if fast_apply:
|
||||
await self.seek(self.position)
|
||||
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({
|
||||
"op": "updateFilter",
|
||||
"type": "reset"
|
||||
}, requester)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been removed all filters.")
|
||||
|
||||
async def change_node(self, identifier: str = None) -> None:
|
||||
"""Change node."""
|
||||
@@ -667,43 +779,43 @@ class Player(VoiceProtocol):
|
||||
if self.volume != 100:
|
||||
await self.set_volume(self.volume)
|
||||
|
||||
async def get_recommendations(self, *, track: Track = None) -> bool:
|
||||
async def get_recommendations(self, *, track: Optional[Track] = None) -> bool:
|
||||
"""Get recommendations from Youtube or Spotify."""
|
||||
if not track:
|
||||
track = choice(self.queue.history(incTrack=True)[-5:])
|
||||
|
||||
if track.spotify:
|
||||
spotify_tracks = await self._node._spotify_client.similar_track(seed_tracks=track.identifier)
|
||||
|
||||
tracks = [ Track(
|
||||
track_id=None,
|
||||
search_type=SearchType.ytsearch,
|
||||
spotify_track=track,
|
||||
info=track.to_dict(),
|
||||
requester=self.client.user
|
||||
)
|
||||
for track in spotify_tracks ]
|
||||
|
||||
else:
|
||||
if track.source != 'youtube':
|
||||
try:
|
||||
track = choice(self.queue.history(incTrack=True)[-5:])
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
tracks = await self.get_tracks(
|
||||
f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}",
|
||||
requester=self.client.user
|
||||
)
|
||||
|
||||
|
||||
tracks = await self._node.get_recommendations(track)
|
||||
if tracks:
|
||||
if isinstance(tracks, Playlist):
|
||||
await self.add_track(tracks.tracks, duplicate=False)
|
||||
else:
|
||||
await self.add_track(tracks, duplicate=False)
|
||||
await self.add_track(tracks, duplicate=False)
|
||||
|
||||
self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been requested recommendations.")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def update_voice_status(self, remove_status: bool = False) -> None:
|
||||
template = self.settings.get("stage_announce_template", func.settings.voice_status_template)
|
||||
if not template or not self.channel:
|
||||
return
|
||||
|
||||
try:
|
||||
rv = {key: func() if callable(func) else func for key, func in self._ph.variables.items()}
|
||||
status = None if remove_status else self._ph.replace(text=template, variables=rv)
|
||||
# if self.channel.status != status:
|
||||
await self.channel.edit(status=status)
|
||||
|
||||
except Exception as e:
|
||||
self._logger.error(
|
||||
f"Failed to update voice status in channel '{self.channel.name}' "
|
||||
f"({self.channel.id}) for guild '{self.channel.guild}' "
|
||||
f"({self.channel.guild.id})",
|
||||
exc_info=e
|
||||
)
|
||||
|
||||
async def send_ws(self, payload, requester: Member = None):
|
||||
payload['guild_id'] = self.guild.id
|
||||
payload['guild_id'] = str(self.guild.id)
|
||||
if requester:
|
||||
payload['requester_id'] = requester.id
|
||||
await self.bot.ipc.send(payload)
|
||||
payload['requester_id'] = str(requester.id)
|
||||
await self.bot.ipc.send(payload)
|
||||
@@ -27,6 +27,7 @@ import asyncio
|
||||
import os
|
||||
import re
|
||||
import aiohttp
|
||||
import logging
|
||||
|
||||
from discord import Client, Member
|
||||
from discord.ext.commands import Bot
|
||||
@@ -49,7 +50,8 @@ from .exceptions import (
|
||||
TrackLoadError
|
||||
)
|
||||
from .objects import Playlist, Track
|
||||
from .utils import ExponentialBackoff, NodeStats, Ping
|
||||
from .utils import ExponentialBackoff, NodeStats, NodeInfo, Ping
|
||||
from .enums import RequestMethod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .player import Player
|
||||
@@ -68,7 +70,6 @@ URL_REGEX = re.compile(
|
||||
)
|
||||
|
||||
NODE_VERSION = "v4"
|
||||
CALL_METHOD = ["PATCH", "DELETE"]
|
||||
|
||||
class Node:
|
||||
"""The base class for a node.
|
||||
@@ -90,8 +91,8 @@ class Node:
|
||||
session: Optional[aiohttp.ClientSession] = None,
|
||||
spotify_client_id: Optional[str] = None,
|
||||
spotify_client_secret: Optional[str] = None,
|
||||
resume_key: Optional[str] = None
|
||||
|
||||
resume_key: Optional[str] = None,
|
||||
logger: Optional[logging.Logger] = None
|
||||
):
|
||||
self._bot: Bot = bot
|
||||
self._host: str = host
|
||||
@@ -101,6 +102,7 @@ class Node:
|
||||
self._identifier: str = identifier
|
||||
self._heartbeat: int = heartbeat
|
||||
self._secure: bool = secure
|
||||
self._logger: Optional[logging.Logger] = logger
|
||||
|
||||
self._websocket_uri: str = f"{'wss' if self._secure else 'ws'}://{self._host}:{self._port}/" + NODE_VERSION + "/websocket"
|
||||
self._rest_uri: str = f"{'https' if self._secure else 'http'}://{self._host}:{self._port}"
|
||||
@@ -121,7 +123,8 @@ class Node:
|
||||
}
|
||||
|
||||
self._players: Dict[int, Player] = {}
|
||||
|
||||
self._info: Optional[NodeInfo] = None
|
||||
|
||||
self._spotify_client_id: Optional[str] = spotify_client_id
|
||||
self._spotify_client_secret: Optional[str] = spotify_client_secret
|
||||
self._spotify_client: Optional[spotify.Client] = None
|
||||
@@ -134,6 +137,10 @@ class Node:
|
||||
f"player_count={len(self._players)}>"
|
||||
)
|
||||
|
||||
def get_player(self, guild_id: int) -> Optional[Player]:
|
||||
"""Takes a guild ID as a parameter. Returns a voicelink Player object."""
|
||||
return self._players.get(guild_id, None)
|
||||
|
||||
@property
|
||||
def spotify_client(self) -> Optional[spotify.Client]:
|
||||
if not self._spotify_client:
|
||||
@@ -183,7 +190,7 @@ class Node:
|
||||
return Ping(self._host, port=self._port).get_ping()
|
||||
|
||||
async def _update_handler(self, data: dict) -> None:
|
||||
#await self._bot.wait_until_ready()
|
||||
await self._bot.wait_until_ready()
|
||||
|
||||
if not data:
|
||||
return
|
||||
@@ -219,7 +226,7 @@ class Node:
|
||||
self._available = False
|
||||
|
||||
retry = backoff.delay()
|
||||
print(f"Trying to reconnect {self._identifier} with {round(retry)}s")
|
||||
self._logger.info(f"Trying to reconnect node [{self._identifier}] with {round(retry)}s")
|
||||
await asyncio.sleep(retry)
|
||||
if not self.is_connected:
|
||||
try:
|
||||
@@ -250,22 +257,13 @@ class Node:
|
||||
elif op == "playerUpdate":
|
||||
await player._update_state(data)
|
||||
|
||||
async def send(
|
||||
self, method: int,
|
||||
guild_id: Union[str, int] = None,
|
||||
query: str = None,
|
||||
data: Union[dict, str] = {}
|
||||
) -> dict:
|
||||
async def send(self, method: RequestMethod, query: str, data: Union[dict, str] = {}) -> dict:
|
||||
if not self._available:
|
||||
raise NodeNotAvailable(f"The node '{self._identifier}' is unavailable.")
|
||||
|
||||
uri: str = f"{self._rest_uri}/{NODE_VERSION}" \
|
||||
f"/sessions/{self._session_id}/players" \
|
||||
f"/{guild_id}" if guild_id else "" \
|
||||
f"?{query}" if query else ""
|
||||
|
||||
uri: str = f"{self._rest_uri}/{NODE_VERSION}/{query}"
|
||||
async with self._session.request(
|
||||
method=CALL_METHOD[method],
|
||||
method=method.value,
|
||||
url=uri,
|
||||
headers={"Authorization": self._password},
|
||||
json=data
|
||||
@@ -273,15 +271,11 @@ class Node:
|
||||
if resp.status >= 300:
|
||||
raise NodeException(f"Getting errors from Lavalink REST api")
|
||||
|
||||
if method == CALL_METHOD[1]:
|
||||
if method == RequestMethod.delete:
|
||||
return await resp.json(content_type=None)
|
||||
|
||||
return await resp.json()
|
||||
|
||||
def get_player(self, guild_id: int) -> Optional[Player]:
|
||||
"""Takes a guild ID as a parameter. Returns a voicelink Player object."""
|
||||
return self._players.get(guild_id, None)
|
||||
|
||||
async def connect(self) -> Node:
|
||||
"""Initiates a connection with a Lavalink node and adds it to the node pool."""
|
||||
|
||||
@@ -292,8 +286,9 @@ class Node:
|
||||
|
||||
self._task = self._bot.loop.create_task(self._listen())
|
||||
self._available = True
|
||||
|
||||
print(f"{self._identifier} is connected!")
|
||||
self._info = NodeInfo(await self.send(RequestMethod.get, query="info"))
|
||||
|
||||
self._logger.info(f"Node [{self._identifier}] is connected!")
|
||||
|
||||
except aiohttp.ClientConnectorError:
|
||||
raise NodeConnectionFailure(
|
||||
@@ -313,20 +308,20 @@ class Node:
|
||||
|
||||
return self
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
async def disconnect(self, remove_from_pool: bool = False) -> None:
|
||||
"""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:
|
||||
await self.spotify_client.close()
|
||||
|
||||
await self._websocket.close()
|
||||
del self._pool._nodes[self._identifier]
|
||||
if remove_from_pool:
|
||||
del self._pool._nodes[self._identifier]
|
||||
self._available = False
|
||||
self._task.cancel()
|
||||
|
||||
self._logger.info(f"Node [{self._identifier}] is disconnected!")
|
||||
|
||||
async def reconnect(self) -> None:
|
||||
await asyncio.sleep(10)
|
||||
@@ -506,7 +501,7 @@ class Node:
|
||||
"please obtain Spotify API credentials here: https://developer.spotify.com/"
|
||||
)
|
||||
|
||||
tracks = await self._spotify_client.trackSearch(query=query)
|
||||
tracks = await self._spotify_client.track_search(query=query)
|
||||
except Exception as _:
|
||||
raise TrackLoadError("Not able to find the provided Spotify entity, is it private?")
|
||||
|
||||
@@ -520,6 +515,38 @@ class Node:
|
||||
)
|
||||
for track in tracks ]
|
||||
|
||||
async def get_recommendations(self, track: Track, limit: int = None) -> List[Optional[Track]]:
|
||||
if track.spotify:
|
||||
if not self.spotify_client:
|
||||
return []
|
||||
|
||||
spotify_tracks = await self.spotify_client.similar_track(seed_tracks=track.identifier, limit=limit)
|
||||
|
||||
tracks = [
|
||||
Track(
|
||||
track_id=None,
|
||||
search_type=SearchType.ytsearch,
|
||||
spotify_track=track,
|
||||
info=track.to_dict(),
|
||||
requester=self.bot.user
|
||||
)
|
||||
for track in spotify_tracks
|
||||
]
|
||||
|
||||
else:
|
||||
if track.source != 'youtube':
|
||||
return []
|
||||
|
||||
tracks = await self.get_tracks(
|
||||
f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}",
|
||||
requester=self.bot.user
|
||||
)
|
||||
|
||||
if isinstance(tracks, Playlist):
|
||||
tracks = tracks.tracks
|
||||
|
||||
return tracks[:limit] if limit else tracks
|
||||
|
||||
class NodePool:
|
||||
"""The base class for the node pool.
|
||||
This holds all the nodes that are to be used by the bot.
|
||||
@@ -538,7 +565,7 @@ class NodePool:
|
||||
@property
|
||||
def node_count(self) -> Optional[Node]:
|
||||
return len(self._nodes.values())
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_best_node(cls, *, algorithm: NodeAlgorithm) -> Node:
|
||||
"""Fetches the best node based on an NodeAlgorithm.
|
||||
@@ -601,18 +628,22 @@ class NodePool:
|
||||
spotify_client_secret: Optional[str] = None,
|
||||
session: Optional[aiohttp.ClientSession] = None,
|
||||
resume_key: Optional[str] = None,
|
||||
logger: Optional[logging.Logger] = 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.")
|
||||
|
||||
|
||||
if not logger:
|
||||
logger = logging.getLogger("voicelink")
|
||||
|
||||
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
|
||||
resume_key=resume_key, logger=logger
|
||||
)
|
||||
|
||||
await node.connect()
|
||||
|
||||
@@ -25,7 +25,7 @@ from .exceptions import QueueFull, OutofList
|
||||
from .objects import Track
|
||||
from .enums import LoopType
|
||||
|
||||
from typing import Optional, Tuple, List, Callable
|
||||
from typing import Optional, Tuple, List, Callable, Dict
|
||||
from itertools import cycle
|
||||
from discord import Member
|
||||
|
||||
@@ -124,11 +124,11 @@ class Queue:
|
||||
elif queue_type == "history":
|
||||
self._queue[:self._position] = replacement
|
||||
|
||||
def swap(self, num1: int, num2: int) -> Tuple[Track, Track]:
|
||||
def swap(self, track_index1: int, track_index2: int) -> Tuple[Track, Track]:
|
||||
try:
|
||||
pos = self._position - 1
|
||||
self._queue[pos + num1], self._queue[pos + num2] = self._queue[pos + num2], self._queue[pos + num1]
|
||||
return self._queue[pos + num1], self._queue[pos + num2]
|
||||
adjusted_position = self._position - 1
|
||||
self._queue[adjusted_position + track_index1], self._queue[adjusted_position + track_index2] = self._queue[adjusted_position + track_index2], self._queue[adjusted_position + track_index1]
|
||||
return self._queue[adjusted_position + track_index1], self._queue[adjusted_position + track_index2]
|
||||
except IndexError:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
@@ -137,14 +137,14 @@ class Queue:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
try:
|
||||
moveItem = self._queue[self._position + target - 1]
|
||||
self._queue.remove(moveItem)
|
||||
self.put_at_index(to, moveItem)
|
||||
return moveItem
|
||||
item = self._queue[self._position + target - 1]
|
||||
self._queue.remove(item)
|
||||
self.put_at_index(to, item)
|
||||
return item
|
||||
except:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
def remove(self, index: int, index2: int = None, member: Member = None) -> Optional[List[Track]]:
|
||||
def remove(self, index: int, index2: int = None, member: Member = None) -> Dict[int, Track]:
|
||||
pos = self._position - 1
|
||||
|
||||
if index2 is None:
|
||||
@@ -154,16 +154,15 @@ class Queue:
|
||||
index, index2 = index2, index
|
||||
|
||||
try:
|
||||
count = []
|
||||
removed_tracks: Dict[str, Track] = {}
|
||||
for i, track in enumerate(self._queue[pos + index: pos + index2 + 1]):
|
||||
if member:
|
||||
if track.requester != member:
|
||||
continue
|
||||
if member and track.requester != member:
|
||||
continue
|
||||
|
||||
self._queue.remove(track)
|
||||
count.append({"position": pos + index + i, "track": track})
|
||||
removed_tracks[pos + index + i] = track
|
||||
|
||||
return count
|
||||
return removed_tracks
|
||||
except:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
|
||||
@@ -26,14 +26,15 @@ import time
|
||||
import aiohttp
|
||||
|
||||
from base64 import b64encode
|
||||
from typing import List, Union
|
||||
from .objects import Track, Album, Artist, Playlist
|
||||
from typing import List, Union, Dict, Any
|
||||
from .objects import Track, Album, Artist, Playlist, Category
|
||||
from .exceptions import InvalidSpotifyURL, SpotifyRequestException
|
||||
|
||||
BASE_URL = "https://api.spotify.com/v1/"
|
||||
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}"
|
||||
REQUEST_URL = BASE_URL + "{type}s/{id}"
|
||||
SEARCH_URL = BASE_URL + "search?q={query}&type={type}&limit={limit}"
|
||||
SUGGESTION_URL = BASE_URL + "recommendations?limit={limit}&seed_tracks={seed_tracks}"
|
||||
SPOTIFY_URL_REGEX = re.compile(
|
||||
r"https?://open.spotify.com/(?P<type>album|playlist|track|artist)/(?P<id>[a-zA-Z0-9]+)"
|
||||
)
|
||||
@@ -45,16 +46,18 @@ class Client:
|
||||
"""
|
||||
|
||||
def __init__(self, client_id: str, client_secret: str) -> None:
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._client_id: str = client_id
|
||||
self._client_secret: str = client_secret
|
||||
|
||||
self.session = aiohttp.ClientSession()
|
||||
self.session: aiohttp.ClientSession = 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
|
||||
self._expiry: int = 0
|
||||
self._auth_token: str = b64encode(f"{self._client_id}:{self._client_secret}".encode())
|
||||
self._grant_headers: Dict[str, str] = {"Authorization": f"Basic {self._auth_token.decode()}"}
|
||||
self._bearer_headers: Dict[str, str] = None
|
||||
|
||||
self._categories: List[Category] = []
|
||||
|
||||
async def _fetch_bearer_token(self) -> None:
|
||||
_data = {"grant_type": "client_credentials"}
|
||||
@@ -65,48 +68,35 @@ class Client:
|
||||
f"Error fetching bearer token: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
data: dict = await resp.json()
|
||||
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[Track]:
|
||||
async def get_request(self, url: str) -> Dict:
|
||||
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:
|
||||
async with self.session.get(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 await resp.json()
|
||||
|
||||
async def track_search(self, query: str, track: str = "track", limit: int = 10) -> List[Track]:
|
||||
request_url = SEARCH_URL.format(query=query, type=track, limit=limit)
|
||||
data = await self.get_request(request_url)
|
||||
return [ Track(track) for track in data['tracks']['items'] ]
|
||||
|
||||
async def similar_track(self, seed_tracks: str, *, limit: int = 5) -> List[Track]:
|
||||
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'] ]
|
||||
data = await self.get_request(request_url)
|
||||
return [ Track(track) for track in data['tracks'] ]
|
||||
|
||||
async def search(self, *, query: str) -> Union[Track, Album, Playlist]:
|
||||
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")
|
||||
@@ -118,13 +108,7 @@ class Client:
|
||||
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()
|
||||
data = await self.get_request(request_url)
|
||||
|
||||
if spotify_type == "track":
|
||||
return Track(data)
|
||||
@@ -150,7 +134,7 @@ class Client:
|
||||
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||
)
|
||||
|
||||
next_data: dict = await resp.json()
|
||||
next_data: Dict = await resp.json()
|
||||
|
||||
tracks += [
|
||||
Track(track["track"])
|
||||
@@ -160,5 +144,13 @@ class Client:
|
||||
|
||||
return Playlist(data, tracks)
|
||||
|
||||
async def get_categories(self) -> List[Category]:
|
||||
if not self._categories:
|
||||
request_url = f"{BASE_URL}browse/categories"
|
||||
data = await self.get_request(request_url)
|
||||
self._categories = [Category(item) for item in data.get("items", [])]
|
||||
|
||||
return self._categories
|
||||
|
||||
async def close(self) -> None:
|
||||
await self.session.close()
|
||||
await self.session.close()
|
||||
@@ -13,8 +13,8 @@ class Track:
|
||||
|
||||
def __init__(self, data: dict, image = None) -> None:
|
||||
self.name: str = data.get('name', 'Unknown')
|
||||
self.artists: str = ", ".join(artist["name"] for artist in data.get('artists'))
|
||||
self.artist_id: list[str] = [artist['id'] for artist in data.get('artists')]
|
||||
self.artists: str = ", ".join(filter(None, (artist["name"] for artist in data.get('artists'))))
|
||||
self.artist_id: list[str] = [artist["id"] for artist in data.get('artists')]
|
||||
self.length: int = data.get('duration_ms')
|
||||
self.id: str = data.get('id')
|
||||
self.image: str = images[0]["url"] if (images := data.get("album", {}).get("images")) else image
|
||||
@@ -55,7 +55,7 @@ class Album:
|
||||
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.name: str = data.get('name', 'Unknown')
|
||||
self.artists: str = ", ".join(artist["name"] for artist in data.get('artists'))
|
||||
self.artists: str = ", ".join(filter(None, (artist["name"] for artist in data.get('artists'))))
|
||||
self.image: str = data["images"][0]["url"]
|
||||
self.tracks: list[Track] = [Track(track, image=self.image) for track in data["tracks"]["items"]]
|
||||
self.total_tracks: int = data["total_tracks"]
|
||||
@@ -122,4 +122,14 @@ class Playlist:
|
||||
return (
|
||||
f"<Voicelink.spotify.Playlist name={self.name} owner={self.owner} id={self.id} "
|
||||
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||
)
|
||||
)
|
||||
|
||||
class Category:
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.href: str = data.get("href")
|
||||
self.id: str = data.get("id")
|
||||
self.name: str = data.get("name")
|
||||
self.icon: str = data.get("icon", [])[0].get("url")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (f"<Voicelink.spotify.Category name={self.name} id={self.id}")
|
||||
@@ -27,9 +27,15 @@ import socket
|
||||
from timeit import default_timer as timer
|
||||
from itertools import zip_longest
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
__all__ = [
|
||||
"ExponentialBackoff",
|
||||
"NodeStats"
|
||||
"NodeStats",
|
||||
"NodeInfoVersion",
|
||||
"NodeInfo",
|
||||
"Plugin",
|
||||
"Ping"
|
||||
]
|
||||
|
||||
class ExponentialBackoff:
|
||||
@@ -85,26 +91,56 @@ class NodeStats:
|
||||
Gives critical information on the node, which is updated every minute.
|
||||
"""
|
||||
|
||||
def __init__(self, data: dict) -> None:
|
||||
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")
|
||||
memory: Dict = data.get("memory")
|
||||
self.used: int = memory.get("used")
|
||||
self.free: int = memory.get("free")
|
||||
self.reservable: int = memory.get("reservable")
|
||||
self.allocated: int = 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")
|
||||
cpu: Dict = data.get("cpu")
|
||||
self.cpu_cores: int = cpu.get("cores")
|
||||
self.cpu_system_load: float = cpu.get("systemLoad")
|
||||
self.cpu_process_load: float = cpu.get("lavalinkLoad")
|
||||
|
||||
self.players_active = data.get("playingPlayers")
|
||||
self.players_total = data.get("players")
|
||||
self.uptime = data.get("uptime")
|
||||
self.players_active: int = data.get("playingPlayers")
|
||||
self.players_total: int = data.get("players")
|
||||
self.uptime: int = data.get("uptime")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.NodeStats total_players={self.players_total!r} playing_active={self.players_active!r}>"
|
||||
|
||||
class NodeInfoVersion:
|
||||
"""The base class for the node info object.
|
||||
Gives version information on the node.
|
||||
"""
|
||||
def __init__(self, data: Dict) -> None:
|
||||
self.semver: str = data.get("semver")
|
||||
self.major: int = data.get("major")
|
||||
self.minor: int = data.get("minor")
|
||||
self.patch: int = data.get("patch")
|
||||
self.pre_release: Optional[str] = data.get("preRelease")
|
||||
self.build: Optional[str] = data.get("build")
|
||||
|
||||
class NodeInfo:
|
||||
"""The base class for the node info object.
|
||||
Gives basic information on the node.
|
||||
"""
|
||||
def __init__(self, data: Dict) -> None:
|
||||
self.version: NodeInfoVersion = NodeInfoVersion(data.get("version"))
|
||||
self.build_time: int = data.get("buildTime")
|
||||
self.jvm: str = data.get("jvm")
|
||||
self.lavaplayer: str = data.get("lavaplayer")
|
||||
self.plugins: Optional[Dict[str, Plugin]] = [Plugin(plugin_data) for plugin_data in data.get("plugins")]
|
||||
|
||||
class Plugin:
|
||||
"""The base class for the plugin object.
|
||||
Gives basic information on the plugin.
|
||||
"""
|
||||
def __init__(self, data: Dict) -> None:
|
||||
self.name: str = data.get("name")
|
||||
self.version: str = data.get("version")
|
||||
|
||||
class Ping:
|
||||
# Thanks to https://github.com/zhengxiaowai/tcping for the nice ping impl
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
## Dashboard Tutorial
|
||||
Click on the image below to watch the tutorial on Youtube.
|
||||
|
||||
[](https://youtu.be/5Y5GPO95uxE)
|
||||
|
||||
## Configuration
|
||||
1. **Fill in `CLIENT_SECRET_ID` and `SECRET_KEY` values in `.env`**
|
||||
|
||||
| Values | Description |
|
||||
| --- | --- |
|
||||
| CLIENT_SECRET_ID | Your Discord bot client secret id [(Discord Portal)](https://discord.com/developers/applications) ***(optional)*** |
|
||||
| SECRET_KEY | Secret key for dashboard ***(optional)*** |
|
||||
|
||||
2. **Add `ipc_server` configuration in `settings.json`**
|
||||
```json
|
||||
"ipc_server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"enable": false
|
||||
},
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
```sh
|
||||
cd web #Go to the web directory
|
||||
python webapp.py #Start the web server
|
||||
```
|
||||
@@ -1,2 +0,0 @@
|
||||
from .ipc import *
|
||||
from .objects import *
|
||||
@@ -1,2 +0,0 @@
|
||||
from .server import IPCServer
|
||||
from .client import IPCClient
|
||||
@@ -1,58 +0,0 @@
|
||||
import websockets, json, asyncio
|
||||
from uuid import uuid4
|
||||
|
||||
class IPCClient:
|
||||
def __init__(
|
||||
self,
|
||||
host = "127.0.0.1",
|
||||
port = 8000,
|
||||
secret_key = None,
|
||||
callback = None
|
||||
):
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.websocket = None
|
||||
self.secret_key = secret_key
|
||||
self.callback = callback
|
||||
self.id = uuid4()
|
||||
self.is_connected = False
|
||||
|
||||
async def connect_and_reconnect(self):
|
||||
while True:
|
||||
if not self.is_connected:
|
||||
await self.connect()
|
||||
print("Reconnect in next 10s!")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.websocket = await websockets.connect(f"ws://{self.host}:{self.port}", extra_headers={"Client-Id": str(self.id)})
|
||||
await self.start_receiver()
|
||||
except:
|
||||
self.is_connected = False
|
||||
pass
|
||||
|
||||
async def send(self, message, user):
|
||||
if not self.is_connected:
|
||||
await self.connect_and_reconnect()
|
||||
|
||||
data = json.loads(message)
|
||||
|
||||
payload = {
|
||||
"secret": self.secret_key,
|
||||
"data": data | {"user_id": user.id, "guild_id": user.guild_id}
|
||||
}
|
||||
|
||||
await self.websocket.send(json.dumps(payload))
|
||||
|
||||
async def receive(self):
|
||||
print("Connected ipc server!")
|
||||
self.is_connected = True
|
||||
async for message in self.websocket:
|
||||
if self.callback:
|
||||
data = json.loads(message)
|
||||
self.callback(data)
|
||||
|
||||
async def start_receiver(self):
|
||||
await self.receive()
|
||||
@@ -1,417 +0,0 @@
|
||||
import json, function as func
|
||||
|
||||
from discord import Member, VoiceChannel
|
||||
from discord.ext import commands
|
||||
from voicelink import Player, Track, Playlist, NodePool, connect_channel, decode, LoopType
|
||||
|
||||
class TempCtx():
|
||||
def __init__(self, author: Member, channel: VoiceChannel) -> None:
|
||||
self.author = author
|
||||
self.channel = channel
|
||||
self.guild = channel.guild
|
||||
|
||||
def missingPermission(user_id:int):
|
||||
payload = {"op": "errorMsg", "level": "info", "msg": "Only the DJ or admins may use this funciton!"}
|
||||
payload["user_id"] = user_id
|
||||
return payload
|
||||
|
||||
def error_msg(msg: str, *, user_id: int = None, guild_id: int = None, level: str = "info"):
|
||||
payload = {"op": "errorMsg", "level": level, "msg": msg}
|
||||
if user_id:
|
||||
payload["user_id"] = user_id
|
||||
if guild_id:
|
||||
payload["guild_id"] = guild_id
|
||||
|
||||
return payload
|
||||
|
||||
async def connect_channel(member: Member, bot: commands.Bot):
|
||||
if not member.voice:
|
||||
return
|
||||
|
||||
channel = member.voice.channel
|
||||
try:
|
||||
settings = await func.get_settings(channel.guild.id)
|
||||
player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings))
|
||||
await player.send_ws({"op": "createPlayer", "members_id": [member.id for member in channel.members]})
|
||||
return player
|
||||
except:
|
||||
return
|
||||
|
||||
async def initPlayer(player: Player, member: Member, data: dict):
|
||||
player._ipc_connection = True
|
||||
return {
|
||||
"op": "initPlayer",
|
||||
"guild_id": player.guild.id,
|
||||
"user_id": data.get("user_id"),
|
||||
"users": [{
|
||||
"user_id": member.id,
|
||||
"avatar_url": member.display_avatar.url,
|
||||
"name": member.name
|
||||
} for member in player.channel.members ],
|
||||
"tracks": [ track.track_id for track in player.queue._queue ],
|
||||
"repeat_mode": player.queue.repeat.lower(),
|
||||
"channel_name": player.channel.name,
|
||||
"current_queue_position": player.queue._position if player._current else player.queue._position + 1,
|
||||
"current_position": 0 or player.position if player.is_playing else 0,
|
||||
"is_playing": player.is_playing,
|
||||
"is_paused": player.is_paused,
|
||||
"is_dj": player.is_privileged(member, check_user_join=False),
|
||||
"autoplay": player.settings.get("autoplay", False)
|
||||
}
|
||||
|
||||
async def skipTo(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
if player.current and member == player.current.requester:
|
||||
pass
|
||||
|
||||
elif member in player.skip_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
else:
|
||||
player.skip_votes.add(member)
|
||||
if len(player.skip_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return error_msg(player.get_msg('skipVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
index = data.get("index", 1)
|
||||
if index > 1:
|
||||
player.queue.skipto(index)
|
||||
|
||||
if player.queue._repeat.mode == LoopType.track:
|
||||
await player.set_repeat(LoopType.off.name)
|
||||
await player.stop()
|
||||
|
||||
async def backTo(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
if player.current and member == player.current.requester:
|
||||
pass
|
||||
|
||||
elif member in player.skip_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
else:
|
||||
player.skip_votes.add(member)
|
||||
if len(player.skip_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return error_msg(player.get_msg('backVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
index = data.get("index", 1)
|
||||
if not player.is_playing:
|
||||
player.queue.backto(index)
|
||||
await player.do_next()
|
||||
else:
|
||||
player.queue.backto(index + 1)
|
||||
await player.stop()
|
||||
|
||||
async def moveTrack(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
return missingPermission(member.id)
|
||||
|
||||
c = player.queue._position - 1
|
||||
position = data.get("position")
|
||||
new_position = data.get("newPosition")
|
||||
|
||||
moveItem = player.queue._queue[position]
|
||||
player.queue._queue.remove(moveItem)
|
||||
player.queue._queue.insert(new_position, moveItem)
|
||||
|
||||
if position > c and new_position <= c:
|
||||
player.queue._position += 1
|
||||
|
||||
elif position < c and new_position >= c:
|
||||
player.queue._position -= 1
|
||||
|
||||
elif position == c:
|
||||
player.queue._position = new_position + 1
|
||||
|
||||
return {
|
||||
"op": "moveTrack",
|
||||
"position": {
|
||||
"index": position - c,
|
||||
"track_id": moveItem.track_id
|
||||
},
|
||||
"newPosition": {
|
||||
"index": new_position - c
|
||||
},
|
||||
"guild_id": player.guild.id,
|
||||
"requester_id": member.id,
|
||||
"skip_users": [member.id]
|
||||
}
|
||||
|
||||
async def addTracks(player: Player, member: Member, data: dict):
|
||||
raw_tracks = data.get("tracks", [])
|
||||
tracks = [Track(
|
||||
track_id=track_id,
|
||||
info=decode(track_id),
|
||||
requester=member
|
||||
) for track_id in raw_tracks]
|
||||
|
||||
await player.add_track(tracks)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
async def getTracks(player: Player, member: Member, data: dict):
|
||||
query = data.get("query", None)
|
||||
|
||||
if query:
|
||||
payload = {"op": "getTracks", "user_id": member.id}
|
||||
tracks = await player.get_tracks(query, requester=member)
|
||||
if not tracks:
|
||||
return payload
|
||||
|
||||
if isinstance(tracks, Playlist):
|
||||
tracks = [ track for track in tracks.tracks[:50] ]
|
||||
|
||||
payload["tracks"] = [ track.track_id for track in tracks ]
|
||||
return payload
|
||||
|
||||
async def shuffleTrack(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
|
||||
if member in player.shuffle_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
else:
|
||||
player.shuffle_votes.add(member)
|
||||
if len(player.shuffle_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return error_msg(player.get_msg('shuffleVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
await player.shuffle(data.get("type", "queue"), member)
|
||||
|
||||
async def repeatTrack(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
return missingPermission(member.id)
|
||||
|
||||
await player.set_repeat()
|
||||
|
||||
async def removeTrack(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
return missingPermission(member.id)
|
||||
|
||||
position = data.get("position")
|
||||
verify_id = data.get("track_id")
|
||||
|
||||
track = player.queue._queue[position]
|
||||
if track.track_id == verify_id:
|
||||
player.queue._queue.remove(track)
|
||||
|
||||
if position < player.queue._position:
|
||||
player.queue._position -= 1
|
||||
|
||||
return {
|
||||
"op": "removeTrack",
|
||||
"positions": [position],
|
||||
"track_ids": [track.track_id],
|
||||
"current_queue_position": player.queue._position,
|
||||
"requester_id": member.id,
|
||||
"guild_id": player.guild.id
|
||||
}
|
||||
|
||||
async def updatePause(player: Player, member: Member, data: dict):
|
||||
pause = data.get("pause", True)
|
||||
if not player.is_privileged(member):
|
||||
if pause:
|
||||
if member in player.pause_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
else:
|
||||
player.pause_votes.add(member)
|
||||
if len(player.pause_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return error_msg(player.get_msg('pauseVote').format(member, len(player.pause_votes), required), guild_id=player.guild.id)
|
||||
else:
|
||||
if member in player.resume_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
else:
|
||||
player.resume_votes.add(member)
|
||||
if len(player.resume_votes) >= (required := player.required()):
|
||||
pass
|
||||
else:
|
||||
return error_msg(player.get_msg('resumeVote').format(member, len(player.resume_votes), required), guild_id=player.guild.id)
|
||||
|
||||
if pause:
|
||||
player.pause_votes.clear()
|
||||
else:
|
||||
player.resume_votes.clear()
|
||||
|
||||
await player.set_pause(pause, member)
|
||||
|
||||
async def updatePosition(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
return missingPermission(member.id)
|
||||
|
||||
position = data.get("position");
|
||||
await player.seek(position, member);
|
||||
|
||||
async def toggleAutoplay(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
return error_msg(player.get_msg('missingPerms_autoplay'))
|
||||
|
||||
check = data.get("status", False)
|
||||
player.settings['autoplay'] = check
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
|
||||
return {
|
||||
"op": "toggleAutoplay",
|
||||
"status": check,
|
||||
"requester_id": member.id
|
||||
}
|
||||
|
||||
async def closeConnection(player: Player, member: Member, data: dict):
|
||||
player._ipc_connection = False
|
||||
|
||||
async def getPlaylists(member: Member, data: dict):
|
||||
playlists: dict = await func.get_user(member.id, "playlist")
|
||||
if not playlists:
|
||||
return
|
||||
|
||||
for pId, pList in playlists.copy().items():
|
||||
if "type" in pList:
|
||||
if pList["type"] == "link":
|
||||
tracks: Playlist = await NodePool.get_node().get_tracks(pList["uri"], requester=member)
|
||||
if tracks:
|
||||
playlists[pId]["tracks"] = [ track.track_id for track in tracks.tracks ]
|
||||
|
||||
elif pList["type"] == "share":
|
||||
playlist = await func.get_user(pList["user"], "playlist")
|
||||
playlist = playlist.get(pList["referId"])
|
||||
if playlist:
|
||||
if member.id not in playlist["perms"]["read"]:
|
||||
await func.update_user(member.id, {"$unset": {f"playlist.{pId}": 1}})
|
||||
del playlists[pId]
|
||||
continue
|
||||
|
||||
if playlist['type'] == 'link':
|
||||
tracks: Playlist = await NodePool.get_node().get_tracks(playlist["uri"], requester=member)
|
||||
playlists[pId]["tracks"] = [ track.track_id for track in tracks.tracks ]
|
||||
else:
|
||||
playlists[pId]["tracks"] = playlist["tracks"]
|
||||
|
||||
return {
|
||||
"op": "getPlaylists",
|
||||
"playlists": playlists,
|
||||
"user_id": member.id
|
||||
}
|
||||
|
||||
async def removePlaylist(member: Member, data:dict):
|
||||
pId = data.get("pId")
|
||||
isShare = data.get("isShare", False)
|
||||
|
||||
if pId == 200:
|
||||
return
|
||||
|
||||
if isShare:
|
||||
refer_user = data.get("refer_user")
|
||||
await func.update_user(refer_user, {"$pull": {f"playlist.{pId}.perms.read": member.id}})
|
||||
|
||||
await func.update_user(member.id, {"$unset": {f'playlist.{pId}': 1}})
|
||||
|
||||
async def addPlaylistTrack(member: Member, data: dict):
|
||||
track_id = data.get("track_id")
|
||||
pId = data.get("pId")
|
||||
if not track_id or not pId:
|
||||
return
|
||||
|
||||
playlist: dict = await func.get_user(member.id, 'playlist')
|
||||
playlist = playlist.get(pId)
|
||||
if not playlist:
|
||||
return
|
||||
|
||||
if playlist["type"] != "playlist":
|
||||
return error_msg(func.get_lang(member.guild.id, 'playlistNotAllow'), user_id=member.id)
|
||||
|
||||
rank, max_p, max_t = func.check_roles()
|
||||
if len(playlist["tracks"]) >= max_t:
|
||||
return error_msg(func.get_lang(member.guild.id, "playlistlimited").format(max_t), user_id=member.id)
|
||||
|
||||
if track_id in playlist['tracks']:
|
||||
return error_msg(func.get_lang(member.guild.id, "playlistrepeated"), user_id=member.id)
|
||||
|
||||
await func.update_user(member.id, {"$push": {f'playlist.{pId}.tracks': track_id}})
|
||||
|
||||
async def removePlaylistTrack(member: Member, data: dict):
|
||||
track_id = data.get("track_id")
|
||||
pId = data.get("pId")
|
||||
if not track_id or not pId:
|
||||
return
|
||||
|
||||
await func.update_user(member.id, {"$pull": {f'playlist.{pId}.tracks': track_id }})
|
||||
|
||||
methods = {
|
||||
"initPlayer": [initPlayer, False],
|
||||
"skipTo": [skipTo, False],
|
||||
"backTo": [backTo, False],
|
||||
"moveTrack": [moveTrack, False],
|
||||
"addTracks": [addTracks, True],
|
||||
"getTracks": [getTracks, True],
|
||||
"shuffleTrack": [shuffleTrack, False],
|
||||
"repeatTrack": [repeatTrack, False],
|
||||
"removeTrack": [removeTrack, False],
|
||||
"updatePause": [updatePause, False],
|
||||
"updatePosition": [updatePosition, False],
|
||||
"toggleAutoplay": [toggleAutoplay, False],
|
||||
"getPlaylists": [getPlaylists, False],
|
||||
"removePlaylist": [removePlaylist, False],
|
||||
"addPlaylistTrack": [addPlaylistTrack, False],
|
||||
"removePlaylistTrack": [removePlaylistTrack, False]
|
||||
}
|
||||
|
||||
async def process_methods(websocket, bot: commands.Bot, data: dict) -> None:
|
||||
method = methods.get(data.get("op", ""), None)
|
||||
if not method:
|
||||
return
|
||||
|
||||
guild, member = None, None
|
||||
guild_id = data.get("guild_id", None)
|
||||
user_id = data.get("user_id", None)
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
if guild_id is None:
|
||||
user = bot.get_user(user_id)
|
||||
if not user:
|
||||
return
|
||||
|
||||
for g in user.mutual_guilds:
|
||||
m = g.get_member(user.id)
|
||||
if m.voice and m.voice.channel:
|
||||
guild = g
|
||||
member = m
|
||||
break
|
||||
else:
|
||||
guild = bot.get_guild(guild_id)
|
||||
member = guild.get_member(user_id)
|
||||
|
||||
if not member:
|
||||
return
|
||||
|
||||
try:
|
||||
if 'player' in method[0].__code__.co_varnames:
|
||||
if not guild:
|
||||
return
|
||||
|
||||
player: Player = guild.voice_client
|
||||
if not player:
|
||||
if not method[1]:
|
||||
return
|
||||
player: Player = await connect_channel(member, bot)
|
||||
|
||||
resp: dict = await method[0](player, member, data)
|
||||
else:
|
||||
resp: dict = await method[0](member, data)
|
||||
|
||||
if resp:
|
||||
await websocket.send(json.dumps(resp))
|
||||
|
||||
except Exception as e:
|
||||
payload = {
|
||||
"op": "errorMsg",
|
||||
"level": "error",
|
||||
"msg": str(e),
|
||||
"user_id": member.id
|
||||
}
|
||||
await websocket.send(json.dumps(payload))
|
||||
@@ -1,67 +0,0 @@
|
||||
import json
|
||||
|
||||
from websockets.server import serve, WebSocketServerProtocol
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from discord.ext import commands
|
||||
|
||||
from typing import (
|
||||
Optional,
|
||||
Dict,
|
||||
Any
|
||||
)
|
||||
|
||||
class IPCServer:
|
||||
def __init__(
|
||||
self,
|
||||
bot: commands.Bot,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 8000,
|
||||
secret_key: Optional[str] = None
|
||||
):
|
||||
self.bot = bot
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.secret_key = secret_key
|
||||
|
||||
self.user = {}
|
||||
self.connections = set()
|
||||
|
||||
def is_secure(self, data: dict) -> bool:
|
||||
if (key := data.get("secret")):
|
||||
return str(key) == str(self.secret_key)
|
||||
return bool(self.secret_key is None)
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
print("Starting IPC")
|
||||
await serve(self.handle_ipc_connection, self.host, self.port)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
async def handle_ipc_connection(self, websocket: WebSocketServerProtocol):
|
||||
from .methods import process_methods
|
||||
|
||||
client_id = websocket.request_headers.get("Client-ID", None)
|
||||
if not client_id:
|
||||
return
|
||||
|
||||
self.connections.add(websocket)
|
||||
try:
|
||||
async for message in websocket:
|
||||
data: Dict[str, Any] = json.loads(message)
|
||||
if self.is_secure(data):
|
||||
action = data.get("data")
|
||||
await process_methods(websocket, self.bot, action)
|
||||
|
||||
except ConnectionClosed:
|
||||
pass
|
||||
|
||||
self.connections.remove(websocket)
|
||||
|
||||
async def send(self, payload):
|
||||
for conn in self.connections:
|
||||
try:
|
||||
await conn.send(json.dumps(payload))
|
||||
except:
|
||||
await conn.close()
|
||||
self.connections.remove(conn)
|
||||
@@ -1,25 +0,0 @@
|
||||
class Asset:
|
||||
def __init__(self, userId: str, key: str):
|
||||
self.key = key
|
||||
self.url = f"https://cdn.discordapp.com/avatars/{userId}/{key}.png"
|
||||
|
||||
class User:
|
||||
def __init__(self, data: dict):
|
||||
self.id = int(data.get("id"))
|
||||
self.username = data.get("username")
|
||||
self.display_name = data.get("display_name")
|
||||
self.avatar = Asset(self.id, data.get("avatar"))
|
||||
self.avatar_decoration = data.get("avatar_decoration")
|
||||
self.discriminator = data.get("discriminator")
|
||||
self.public_flag = data.get("public_flag")
|
||||
self.flags = data.get("flags")
|
||||
self.banner = data.get("banner")
|
||||
self.banner_color = data.get("banner_color")
|
||||
self.locale = data.get("locale")
|
||||
self.mfa_enabled = data.get("mfa_enabled")
|
||||
self.premium_type = data.get("premium_type")
|
||||
self.access_token = data.get("access_token")
|
||||
self.refresh_token = data.get("refresh_token")
|
||||
|
||||
self.sid = None
|
||||
self.guild_id = None
|
||||
@@ -1,920 +0,0 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
color: #ffffff;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #020202;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
max-height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--grey: rgba(142, 142, 142, 20%);
|
||||
--grey-hover: rgba(142, 142, 142, 30%);
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: absolute;
|
||||
opacity: 20%;
|
||||
border-radius: 40%;
|
||||
filter: blur(60px);
|
||||
transition: all 2s ease-in-out;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-container:hover {
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.dropdown-container:hover .dropdown-item {
|
||||
display: block;
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-content img {
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: .6rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dropdown-item a {
|
||||
display: block;
|
||||
padding: .5rem 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-item i {
|
||||
margin-right: .6rem;
|
||||
}
|
||||
|
||||
.dropdown-item a:hover {
|
||||
background-color: var(--grey-hover);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
.header.active {
|
||||
background-color: #020202;
|
||||
}
|
||||
|
||||
.header .left {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header .left p {
|
||||
font-size: 20px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
margin: 0rem 0.4rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header .left p:hover {
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.header .center .search-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header .center .search-container .search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding: .6rem 1rem;
|
||||
width: 500px;
|
||||
overflow: hidden;
|
||||
background-color: rgba(33, 33, 33, 255);
|
||||
}
|
||||
|
||||
.header .center .search-loader {
|
||||
display: none;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
border-top: 3px solid #c9cbcc;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header .center input {
|
||||
margin: 0 1rem;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.header .center .search-result-list {
|
||||
position: absolute;
|
||||
display: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border-radius: 8px;
|
||||
padding: 1rem .5rem;
|
||||
background-color: rgba(33, 33, 33, 255);
|
||||
}
|
||||
|
||||
.header .search-result {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding: .5rem 1rem;
|
||||
cursor: pointer;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.header .search-result:hover {
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.header .search-result-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
margin-right: .6rem;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.header .search-result-info p.info {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header .search-result-info p.desc {
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.header .search-result img {
|
||||
width: auto;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: 0rem 4rem;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.toastContrainer {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.toastContrainer .toast {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: .8rem;
|
||||
margin-top: 1rem;
|
||||
background-color: var(--grey-hover);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transform: translateX(-100%);
|
||||
animation: moveRight .5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes moveRight {
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.toastContrainer .toast::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: azure;
|
||||
animation: anim 6s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes anim {
|
||||
100% {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toastContrainer .toast img {
|
||||
border-radius: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: .7rem;
|
||||
}
|
||||
|
||||
.toastContrainer .toast .content p.username {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.toastContrainer .toast .content p.message {
|
||||
font-size: 10px;
|
||||
width: 300px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.main .thumbnail {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main .thumbnail img {
|
||||
padding: 1rem;
|
||||
width: 75%;
|
||||
height: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.main .thumbnail-background {
|
||||
position: absolute;
|
||||
padding: 1rem;
|
||||
filter: blur(50px);
|
||||
transform: translate3d(0px, 0px, 0) scale(1);
|
||||
opacity: var(0.6);
|
||||
border-radius: inherit;
|
||||
transition: all 1s ease-in-out;
|
||||
}
|
||||
|
||||
.player-controller {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.player-controller:hover .seek-bar {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.position-info {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar-container:hover .position-info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.seek-bar {
|
||||
position: absolute;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
background-color: rgb(73, 73, 73);
|
||||
}
|
||||
|
||||
.seek-bar::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgb(171, 168, 168);
|
||||
box-shadow: -1000px 0 0 1000px rgb(171, 168, 168);
|
||||
}
|
||||
|
||||
.control-container {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
align-items: center;
|
||||
background-color: #212121;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.player-controller .left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.player-controller .left .control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-controller .left .control i {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin-left: .8rem;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.player-controller .left .control #play-pause-btn {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.player-controller .left .position {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.player-controller .center {
|
||||
display: flex;
|
||||
padding: 0rem 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
flex: 2;
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.player-controller .center img {
|
||||
border-radius: .2rem;
|
||||
height: 40;
|
||||
}
|
||||
|
||||
.player-controller .center .data {
|
||||
overflow: hidden;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.player-controller .center .data p {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player-controller .center .desc {
|
||||
color: grey;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.player-controller .center i {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
font-size: 20px;
|
||||
color: gray;
|
||||
transition: color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.player-controller .center i:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.player-controller .right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.player-controller .right i {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin-left: .8rem;
|
||||
font-size: 20px;
|
||||
color: gray;
|
||||
transition: color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.player-controller .right i:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main .list {
|
||||
flex: 1;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main .auto-play {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.main .queue-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main .queue-list ul {
|
||||
list-style: none;
|
||||
width: 95%;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: .5rem;
|
||||
padding: 5px 15px;
|
||||
overflow: hidden;
|
||||
margin-bottom: .5rem;
|
||||
transition: background-color .2s, transform .2s;
|
||||
}
|
||||
|
||||
.track:hover {
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.track:hover .time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.track:hover i.action {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.track.active {
|
||||
background-color: var(--grey-hover);
|
||||
}
|
||||
|
||||
.track .left {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
margin-right: .5rem;
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track i {
|
||||
cursor: move;
|
||||
margin-right: 1rem;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.track i.action {
|
||||
display: none;
|
||||
padding: .5rem 1rem;
|
||||
border-radius: 50%;
|
||||
margin-right: 0;
|
||||
cursor: pointer;
|
||||
transition: background-color .2s;
|
||||
}
|
||||
|
||||
.track i.action:hover {
|
||||
background-color: rgba(157, 157, 157, 50%);
|
||||
}
|
||||
|
||||
.track i:active {
|
||||
transform: scale(0.85);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.track .left img {
|
||||
padding: 5px;
|
||||
height: 50;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.track .left .info {
|
||||
margin-left: .5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track .left .info p {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track .left .info p.desc {
|
||||
color: grey;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.users-bar {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 1.5rem 0;
|
||||
background-color: rgb(37, 37, 37);
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all .5s ease-in-out;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.users-bar.active {
|
||||
width: 350px;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.users-bar .info {
|
||||
padding: 0 .5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.users-bar .info h5 {
|
||||
color: rgba(142, 142, 142);
|
||||
}
|
||||
|
||||
.users-container {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.user {
|
||||
padding: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.user .left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user img {
|
||||
width: 35px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.context-menu ul {
|
||||
list-style: none;
|
||||
min-width: 150px;
|
||||
padding: .5rem 0;
|
||||
border-radius: .3rem;
|
||||
background-color: rgb(37, 37, 37);
|
||||
}
|
||||
|
||||
.context-menu li {
|
||||
padding: .5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color .2s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.context-menu li:hover {
|
||||
background-color: rgb(98, 98, 98);
|
||||
}
|
||||
|
||||
.context-menu i {
|
||||
margin-right: .8rem;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--grey-hover);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus+.slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.playlists {
|
||||
flex-grow: 1;
|
||||
padding: 0rem 4rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.playlists-grid {
|
||||
display: grid;
|
||||
grid-gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
grid-template-rows: repeat(auto-fit, 250px);
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 200px));
|
||||
grid-auto-rows: 250px;
|
||||
}
|
||||
|
||||
.images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
border-radius: .5rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 0;
|
||||
animation: created 1s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.images img {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.images i.action {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin: .5rem;
|
||||
padding: .5rem .9rem;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.images i.play {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 1rem;
|
||||
padding: .7rem .84rem;
|
||||
display: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.images::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: inset 0px 10px 40px rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.images:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.images:hover i {
|
||||
display: block;
|
||||
text-align: center;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
.images i.action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.images i.action:active {
|
||||
transform: scale(0.85);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.images i.play:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.images i.play:active {
|
||||
transform: scale(0.85);
|
||||
box-shadow: 0 0 10px rgba(230, 230, 230, 0.3);
|
||||
}
|
||||
|
||||
.playlist .info {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.playlist .info p {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playlist .info p.desc {
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
@keyframes created {
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 935px) {
|
||||
.main {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toastContrainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header .center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-controller .left .position {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-controller .center img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.playlists {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.playlists-grid {
|
||||
grid-gap: 1rem;
|
||||
padding: 1rem;
|
||||
grid-template-rows: repeat(auto-fit, 200px);
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 150px));
|
||||
grid-auto-rows: 200px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.images {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main .queue-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,370 +0,0 @@
|
||||
$(document).ready(function () {
|
||||
const player = new Player(userId);
|
||||
const largeImage = document.getElementById("largeImage");
|
||||
const img = document.getElementById("image");
|
||||
const canvas = document.createElement('canvas');
|
||||
const $positionInfo = $('.position-info');
|
||||
|
||||
var startPos = null;
|
||||
var selectedTrack = null;
|
||||
var selectedPlaylistId = null;
|
||||
var selectedPlaylistTrack = null;
|
||||
|
||||
var typingTimer;
|
||||
var doneTypingInterval = 2000;
|
||||
|
||||
largeImage.crossOrigin = "Anonymous";
|
||||
largeImage.onload = function () {
|
||||
try {
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
const width = canvas.width = largeImage.width;
|
||||
const height = canvas.height = Math.round(largeImage.height * 0.7);
|
||||
const startY = Math.round((largeImage.height - height) / 2);
|
||||
context.drawImage(largeImage, 0, startY, width, height, 0, 0, width, height);
|
||||
getMainColorsFromImage(4)
|
||||
.then(colors => {
|
||||
let bg = $(".thumbnail-background");
|
||||
bg.css({
|
||||
"background": `linear-gradient(-132deg, ${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[3]})`,
|
||||
"width": "70%",
|
||||
"padding-bottom": `${largeImage.naturalHeight / largeImage.naturalWidth * 70}%`,
|
||||
});
|
||||
bg.fadeIn(200);
|
||||
$("#largeImage").fadeIn(200);
|
||||
})
|
||||
.catch(error => { return });
|
||||
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
img.onload = function () {
|
||||
try {
|
||||
$("#image").fadeIn(200);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function getMainColorsFromImage(numColors) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const imageData = canvas.getContext('2d', { willReadFrequently: true }).getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const colorCount = {};
|
||||
const threshold = 20; // adjust this value to change the threshold for black/white similarity
|
||||
const colorGap = 30; // adjust this value to change the minimum color difference between each main color
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
const r = imageData[i];
|
||||
const g = imageData[i + 1];
|
||||
const b = imageData[i + 2];
|
||||
const color = `rgb(${r}, ${g}, ${b})`;
|
||||
if (!colorCount[color]) {
|
||||
// Check if the color is too close to black or white
|
||||
const isBlackOrWhite = r < threshold || r > 255 - threshold || g < threshold || g > 255 - threshold || b < threshold || b > 255 - threshold;
|
||||
if (isBlackOrWhite) continue;
|
||||
// Check if the color is too similar to previous colors
|
||||
const prevColors = Object.keys(colorCount);
|
||||
const isTooSimilar = prevColors.some(prevColor => {
|
||||
const [pr, pg, pb] = prevColor.match(/\d+/g).map(Number);
|
||||
const diff = Math.sqrt(Math.pow(r - pr, 2) + Math.pow(g - pg, 2) + Math.pow(b - pb, 2));
|
||||
return diff < colorGap;
|
||||
});
|
||||
if (isTooSimilar) continue;
|
||||
colorCount[color] = 0;
|
||||
}
|
||||
colorCount[color]++;
|
||||
}
|
||||
const colors = Object.keys(colorCount).sort((color1, color2) => colorCount[color2] - colorCount[color1]).slice(0, numColors);
|
||||
resolve(colors);
|
||||
});
|
||||
}
|
||||
|
||||
$("#seek-bar").on('mousemove', function (e) {
|
||||
let position = e.pageX - $(this).offset().left;
|
||||
let duration = $(this).width();
|
||||
let percentage = (position / duration) * 100;
|
||||
|
||||
let time = player.msToReadableTime(percentage * player.currentTrack?.length / 100);
|
||||
$positionInfo.text(time).css({
|
||||
left: e.pageX - $positionInfo.outerWidth() / 2,
|
||||
});
|
||||
});
|
||||
|
||||
$('body').click(function (event) {
|
||||
let $target = $(event.target);
|
||||
|
||||
if (!$target.closest(".search-container").length && !$target.is("#search-input") && $('#search-result-list').css("display") != 'none') {
|
||||
$("#search-result-list").fadeOut(200);
|
||||
}
|
||||
|
||||
else if (!$target.closest(".users-bar").is('.users-bar') &&
|
||||
!$target.closest("#users-btn").is('#users-btn') &&
|
||||
$(".users-bar").hasClass("active")) {
|
||||
$(".users-bar").removeClass("active");
|
||||
$("#users-btn").css({ "color": "" });
|
||||
|
||||
} else if ($target.closest(".action").is(".action") &&
|
||||
$target.closest(".playlists-tracks").is(".playlists-tracks")) {
|
||||
|
||||
selectedPlaylistTrack = $target.closest(".track").data("value");
|
||||
const $contextMenu = $("#context-menu");
|
||||
const menuHeight = $contextMenu.outerHeight();
|
||||
const windowHeight = $(window).height();
|
||||
const topPosition = event.pageY + 30;
|
||||
if (topPosition + menuHeight > windowHeight) {
|
||||
// If the menu would go out of the page, position it above the btn instead
|
||||
$contextMenu.css({ "left": `${event.pageX - 130}px`, "top": `${event.pageY - menuHeight - 30}px` }).fadeIn(200);
|
||||
} else {
|
||||
$contextMenu.css({ "left": `${event.pageX - 130}px`, "top": `${topPosition}px` }).fadeIn(200);
|
||||
}
|
||||
|
||||
} else if ($target.closest(".playlists-tracks").is(".playlists-tracks") &&
|
||||
$target.closest(".track").is(".track")) {
|
||||
|
||||
const track_id = $target.closest(".track").data("value");
|
||||
player.send({ "op": "addTracks", "tracks": [track_id] })
|
||||
|
||||
} else if ($target.closest(".playlist").is(".playlist")) {
|
||||
selectedPlaylistId = $target.closest(".playlist").data("value");
|
||||
|
||||
if ($target.closest(".action").is(".action")) {
|
||||
const $contextMenu = $("#playlist-context-menu")
|
||||
|
||||
const menuHeight = $contextMenu.outerHeight();
|
||||
const windowHeight = $(window).height();
|
||||
const topPosition = event.pageY + 30;
|
||||
if (topPosition + menuHeight > windowHeight) {
|
||||
// If the menu would go out of the page, position it above the btn instead
|
||||
$contextMenu.css({ "left": `${event.pageX - 130}px`, "top": `${event.pageY - menuHeight - 30}px` }).fadeIn(200);
|
||||
} else {
|
||||
$contextMenu.css({ "left": `${event.pageX - 130}px`, "top": `${topPosition}px` }).fadeIn(200);
|
||||
}
|
||||
} else if ($target.closest(".play").is(".play")) {
|
||||
if (selectedPlaylistId in player.playlists) {
|
||||
if ("tracks" in player.playlists[selectedPlaylistId]) {
|
||||
if (player.playlists[selectedPlaylistId]['tracks'].length != 0) {
|
||||
player.send({ "op": "addTracks", "tracks": player.playlists[selectedPlaylistId]["tracks"] });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const $pTracks = $("#playlists-tracks");
|
||||
if (selectedPlaylistId in player.playlists) {
|
||||
$pTracks.empty();
|
||||
for (const track_id of player.playlists[selectedPlaylistId]["tracks"]) {
|
||||
const track = decode(track_id);
|
||||
|
||||
let $trackDiv = $(`<div class="track" data-value=${track_id} ><div class="left">${(this.isDJ) ? '<i class="fa-solid fa-bars handle"></i>' : ''}<img src=${track.imageUrl} /><div class="info"><p>${track.title}</p><p class="desc">${track.author}</p></div></div><p class="time">${player.msToReadableTime(track.length)}</p><i class="fa-solid fa-ellipsis-vertical action"></i></div>`);
|
||||
$pTracks.append($trackDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$target.closest(".action").is(".action") &&
|
||||
!$target.closest("#context-menu li").is("#context-menu li") &&
|
||||
$("#context-menu").css("display") != "none") {
|
||||
$("#context-menu").fadeOut(200);
|
||||
}
|
||||
|
||||
if (!$target.closest(".action").is(".action") ||
|
||||
!$target.closest(".images").is(".images") &&
|
||||
$("#playlist-context-menu").css("display") != "none") {
|
||||
$("#playlist-context-menu").fadeOut(200);
|
||||
}
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$("#sortable").sortable({
|
||||
handle: ".handle",
|
||||
scroll: true,
|
||||
axis: "y",
|
||||
start: function (event, ui) {
|
||||
startPos = ui.item.index();
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
if (startPos != null) {
|
||||
let newPos = ui.item.index();
|
||||
if (startPos != newPos) {
|
||||
player.moveTrack(startPos, newPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#sortable').on('click', 'li', function (event) {
|
||||
let index = $(this).index();
|
||||
let position = player.current_queue_position;
|
||||
|
||||
if ($(event.target).hasClass('action')) {
|
||||
selectedTrack = { position: index, track: player.queue[index] };
|
||||
const menuHeight = $("#context-menu").outerHeight();
|
||||
const windowHeight = $(window).height();
|
||||
const topPosition = event.pageY + 30;
|
||||
if (topPosition + menuHeight > windowHeight) {
|
||||
// If the menu would go out of the page, position it above the btn instead
|
||||
$("#context-menu").css({ "left": `${event.pageX - 130}px`, "top": `${event.pageY - menuHeight - 30}px` }).fadeIn(200);
|
||||
} else {
|
||||
$("#context-menu").css({ "left": `${event.pageX - 130}px`, "top": `${topPosition}px` }).fadeIn(200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < position) {
|
||||
player.backTo(position - index);
|
||||
} else if (index > position) {
|
||||
player.skipTo(index - position)
|
||||
} else {
|
||||
player.togglePause();
|
||||
}
|
||||
})
|
||||
|
||||
$('#search-result-list').on('click', 'li', function () {
|
||||
let index = $(this).index();
|
||||
let track = player.searchList[index];
|
||||
if (track != undefined) {
|
||||
player.send({ "op": "addTracks", "tracks": [track] });
|
||||
}
|
||||
$("#search-result-list").fadeOut(200);
|
||||
})
|
||||
|
||||
$('#search-input').on('input', function () {
|
||||
clearTimeout(typingTimer);
|
||||
$("#search-loader").fadeIn(200);
|
||||
typingTimer = setTimeout(function () {
|
||||
let input = $('#search-input').val();
|
||||
if (input.replace(/\s+/g, '') != "") {
|
||||
player.send({ "op": "getTracks", "query": input })
|
||||
} else {
|
||||
$("#search-loader").fadeOut(200);
|
||||
$("#search-result-list").fadeOut(200);
|
||||
}
|
||||
}, doneTypingInterval);
|
||||
});
|
||||
|
||||
$('#search-input').focus(function () {
|
||||
if ($(this).val() != "") {
|
||||
$("#search-result-list").fadeIn(200);
|
||||
}
|
||||
})
|
||||
|
||||
$('#play-pause-btn').on('click', function () {
|
||||
player.togglePause();
|
||||
});
|
||||
|
||||
$('#skip-btn').on('click', function () {
|
||||
player.skipTo();
|
||||
});
|
||||
|
||||
$('#back-btn').on('click', function () {
|
||||
player.backTo();
|
||||
});
|
||||
|
||||
$('#seek-bar').change(function () {
|
||||
player.seekTo($(this).val());
|
||||
})
|
||||
|
||||
$("#repeat-btn").on('click', function () {
|
||||
player.repeatMode();
|
||||
})
|
||||
|
||||
$("#shuffle-btn").on('click', function () {
|
||||
player.shuffle();
|
||||
})
|
||||
|
||||
$("#users-btn").on('click', function () {
|
||||
const userBar = $(".users-bar")
|
||||
userBar.toggleClass("active");
|
||||
if (userBar.hasClass("active")) {
|
||||
$(this).css({ "color": "#fff" });
|
||||
} else {
|
||||
$(this).css({ "color": "" });
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
$("#auto-play").on("click", function () {
|
||||
let checkbox = $(this).is(':checked');
|
||||
player.send({ "op": "toggleAutoplay", "status": checkbox })
|
||||
player.autoplay = checkbox;
|
||||
});
|
||||
|
||||
$("#remove-track-btn").on('click', function () {
|
||||
if ($("#main").css("display") == "none") {
|
||||
if (selectedPlaylistId != null && selectedPlaylistId != null && player.playlists != null) {
|
||||
const playlist = player.playlists[selectedPlaylistId];
|
||||
if (playlist["type"] == "playlist") {
|
||||
player.send({ "op": "removePlaylistTrack", "pId": selectedPlaylistId, "track_id": selectedPlaylistTrack, "pType": playlist["type"] })
|
||||
let index = playlist["tracks"].indexOf(selectedPlaylistTrack);
|
||||
if (index !== -1) {
|
||||
playlist["tracks"].splice(index, 1);
|
||||
}
|
||||
$(`div [data-value=${selectedPlaylistTrack}]`).remove();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
player.removeTrack(selectedTrack?.position, selectedTrack?.track)
|
||||
}
|
||||
$("#context-menu").fadeOut(200);
|
||||
})
|
||||
|
||||
$("#copy-track-btn").on('click', function () {
|
||||
if ($("#main").css("display") == "none") {
|
||||
if (selectedPlaylistTrack != null) {
|
||||
const track = decode(selectedPlaylistTrack);
|
||||
navigator.clipboard.writeText(track.uri);
|
||||
}
|
||||
} else {
|
||||
navigator.clipboard.writeText(selectedTrack?.track.uri);
|
||||
}
|
||||
$("#context-menu").fadeOut(200);
|
||||
})
|
||||
|
||||
$("#remove-playlist-btn").on('click', function () {
|
||||
if (player.playlists != null && selectedPlaylistId in player.playlists) {
|
||||
if (selectedPlaylistId != 200) {
|
||||
playlist = player.playlists[selectedPlaylistId];
|
||||
console.log(player.playlists);
|
||||
let payload = { "op": "removePlaylist", "pId": selectedPlaylistId }
|
||||
if (playlist['type'] == "share") {
|
||||
payload["refer_user"] = playlist["user"];
|
||||
payload["isShare"] = true;
|
||||
}
|
||||
player.send(payload);
|
||||
delete player.playlists[selectedPlaylistId];
|
||||
player.updatePlaylists();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$("#homeBtn").on('click', function () {
|
||||
$("#playlists").fadeOut(200, function () {
|
||||
$("#main").fadeIn(200);
|
||||
$(".header").removeClass("active");
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
$("#playlistBtn").on('click', function () {
|
||||
$("#main").fadeOut(200, function () {
|
||||
if (player.playlists == null) {
|
||||
player.send({ "op": "getPlaylists" });
|
||||
}
|
||||
$("#playlists").fadeIn(200);
|
||||
$(".header").addClass("active");
|
||||
});
|
||||
})
|
||||
|
||||
$("#like-btn").on('click', function () {
|
||||
let currentTrack = player.currentTrack;
|
||||
if (currentTrack != undefined) {
|
||||
if (currentTrack.isStream) {
|
||||
return player.showToast("error", "You are not allowed to add streaming videos to your playlist!");
|
||||
}
|
||||
if ($(this).hasClass("fa-regular")) {
|
||||
$(this).removeClass("fa-regular").addClass("fa-solid");
|
||||
player.send({ "op": "addPlaylistTrack", "track_id": currentTrack.track_id, "pId": "200" })
|
||||
} else {
|
||||
$(this).removeClass("fa-solid").addClass("fa-regular");
|
||||
player.send({ "op": "removePlaylistTrack", "track_id": currentTrack.track_id, "pId": "200" })
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -1,673 +0,0 @@
|
||||
class Timer {
|
||||
constructor(callback, interval) {
|
||||
this.callback = callback;
|
||||
this.interval = interval;
|
||||
this.timerId = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.isRunning) {
|
||||
this.isRunning = true;
|
||||
this.timerId = setInterval(() => {
|
||||
this.callback();
|
||||
}, this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isRunning) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
getIsRunning() {
|
||||
return this.isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
class DataInput {
|
||||
#pos = 0;
|
||||
#buf = Uint8Array;
|
||||
#view = DataView;
|
||||
|
||||
constructor(bytes) {
|
||||
if (typeof bytes === "string") {
|
||||
const decodedString = atob(bytes);
|
||||
const byteArr = new Uint8Array(decodedString.length);
|
||||
for (let i = 0; i < decodedString.length; i++) {
|
||||
byteArr[i] = decodedString.charCodeAt(i);
|
||||
}
|
||||
bytes = byteArr;
|
||||
}
|
||||
this.#buf = bytes;
|
||||
this.#view = new DataView(bytes.buffer);
|
||||
}
|
||||
|
||||
#_advance(bytes) {
|
||||
if (this.#pos + bytes > this.#buf.length) {
|
||||
throw new Error("Buffer overflow: cannot advance further.");
|
||||
}
|
||||
const p = this.#pos;
|
||||
this.#pos += bytes;
|
||||
return p;
|
||||
}
|
||||
|
||||
readByte() {
|
||||
return this.#buf[this.#_advance(1)];
|
||||
}
|
||||
|
||||
readBoolean() {
|
||||
return this.readByte() !== 0;
|
||||
}
|
||||
|
||||
readUnsignedShort() {
|
||||
return this.#view.getUint16(this.#_advance(2), false);
|
||||
}
|
||||
|
||||
readInt() {
|
||||
return this.#view.getInt32(this.#_advance(4), false);
|
||||
}
|
||||
|
||||
readLong() {
|
||||
const msb = this.#view.getInt32(this.#_advance(4), false);
|
||||
const lsb = this.#view.getUint32(this.#_advance(4), false);
|
||||
return BigInt(msb) << 32n | BigInt(lsb);
|
||||
}
|
||||
|
||||
readUTF() {
|
||||
const len = this.readUnsignedShort();
|
||||
const start = this.#_advance(len);
|
||||
return new window.TextDecoder().decode(this.#buf.slice(start, start + len));
|
||||
}
|
||||
}
|
||||
|
||||
class Track {
|
||||
constructor(object) {
|
||||
this.title = object.title;
|
||||
this.author = object.author;
|
||||
this.source = object.source;
|
||||
this.identifier = object.identifier;
|
||||
if (object.thumbnail == undefined) {
|
||||
if (this.source == "youtube") {
|
||||
this.imageUrl = `https://img.youtube.com/vi/${this.identifier}/hqdefault.jpg`
|
||||
} else {
|
||||
this.imageUrl = "https://cdn.discordapp.com/attachments/674788144931012638/823086668445384704/eq-dribbble.gif";
|
||||
}
|
||||
} else {
|
||||
this.imageUrl = object.thumbnail;
|
||||
}
|
||||
this.isStream = object.isStream;
|
||||
this.length = Number(object.length);
|
||||
this.track_id = object.track_id;
|
||||
this.uri = object.uri;
|
||||
}
|
||||
}
|
||||
|
||||
const decoders = [
|
||||
(input, track_id) => {
|
||||
const title = input.readUTF();
|
||||
const author = input.readUTF();
|
||||
const length = input.readLong();
|
||||
const identifier = input.readUTF();
|
||||
const isStream = input.readBoolean();
|
||||
const uri = input.readBoolean() ? input.readUTF() : null;
|
||||
const thumbnail = input.readBoolean() ? input.readUTF() : null;
|
||||
const source = input.readUTF();
|
||||
|
||||
return { track_id, title, author, length, identifier, isStream, uri, thumbnail, source, position: 0n };
|
||||
},
|
||||
undefined,
|
||||
(input, track_id) => {
|
||||
const title = input.readUTF();
|
||||
const author = input.readUTF();
|
||||
const length = input.readLong();
|
||||
const identifier = input.readUTF();
|
||||
const isStream = input.readBoolean();
|
||||
const uri = input.readBoolean() ? input.readUTF() : null;
|
||||
const source = input.readUTF();
|
||||
|
||||
return { track_id, title, author, length, identifier, isStream, uri, thumbnail: null, source, position: 0n };
|
||||
},
|
||||
(input, track_id) => {
|
||||
const title = input.readUTF();
|
||||
const author = input.readUTF();
|
||||
const length = input.readLong();
|
||||
const identifier = input.readUTF();
|
||||
const isStream = input.readBoolean();
|
||||
const uri = input.readBoolean() ? input.readUTF() : null;
|
||||
const thumbnail = input.readBoolean() ? input.readUTF() : null;
|
||||
const isrc = input.readBoolean() ? input.readUTF() : null;
|
||||
const source = input.readUTF();
|
||||
|
||||
return { track_id, title, author, length, identifier, isStream, uri, thumbnail, source, position: 0n };
|
||||
}
|
||||
]
|
||||
function decode(track_id) {
|
||||
const input = new DataInput(track_id);
|
||||
const flags = input.readInt();
|
||||
const version = input.readByte();
|
||||
|
||||
const decoder = decoders[version];
|
||||
return new Track(decoder(input, track_id));
|
||||
}
|
||||
|
||||
const actions = {
|
||||
initPlayer: function (player, data) {
|
||||
player.init();
|
||||
player.isDJ = data['is_dj'];
|
||||
player.addTrack(data["tracks"]);
|
||||
player.updateCurrentQueuePos(data['current_queue_position']);
|
||||
player.is_paused = data['is_paused'];
|
||||
player.current_position = data['current_position'];
|
||||
player.repeat = data['repeat_mode'];
|
||||
player.channelName = data["channel_name"];
|
||||
player.autoplay = data["autoplay"];
|
||||
data["users"].forEach(user => {
|
||||
player.addUser(user);
|
||||
})
|
||||
|
||||
$("#sortable").sortable("option", "disabled", !player.isDJ);
|
||||
},
|
||||
|
||||
playerUpdate: function (player, data) {
|
||||
player.last_update = data["last_update"];
|
||||
player.is_connected = data["is_connected"];
|
||||
player.current_position = data["last_position"];
|
||||
},
|
||||
|
||||
trackUpdate: function (player, data) {
|
||||
let track = player.updateCurrentQueuePos(data['current_queue_position']);
|
||||
player.is_paused = data['is_paused'];
|
||||
if (track?.track_id != data["track_id"]) {
|
||||
player.send({ "op": "initPlayer" })
|
||||
}
|
||||
},
|
||||
|
||||
addTrack: function (player, data) {
|
||||
player.addTrack(data["tracks"]);
|
||||
var tracks = data["tracks"];
|
||||
if (tracks.length == 1) {
|
||||
var msg = `Added ${decode(tracks[0]).title} songs into the queue.`
|
||||
} else {
|
||||
var msg = `Added ${tracks.length} into the queue.`
|
||||
}
|
||||
player.showToast(data["requester_id"], msg)
|
||||
},
|
||||
|
||||
getTracks: function (player, data) {
|
||||
let tracks = data["tracks"];
|
||||
if (tracks != undefined) {
|
||||
const resultList = $("#search-result-list");
|
||||
resultList.empty();
|
||||
player.searchList = tracks;
|
||||
for (let i in tracks) {
|
||||
let track = decode(tracks[i])
|
||||
resultList.append(`<li class="search-result"><div class="search-result-left"><img src=${track.imageUrl} /><div class="search-result-info"><p class="info">${track.title}</p><p class="desc">${track.author}</p></div></div><p>${player.msToReadableTime(track.length)}</p></li>`)
|
||||
}
|
||||
}
|
||||
$("#search-result-list").fadeIn(200);
|
||||
$("#search-loader").fadeOut(200);
|
||||
},
|
||||
|
||||
playerClose: function (player, data) {
|
||||
player.init();
|
||||
},
|
||||
|
||||
updateGuild: function (player, data) {
|
||||
const user = data["user"];
|
||||
player.channelName = data["channel_name"];
|
||||
|
||||
if (user["user_id"] == player.userId) {
|
||||
if (data['is_joined']) {
|
||||
player.send({ "op": "initPlayer" });
|
||||
} else {
|
||||
player.init();
|
||||
}
|
||||
}
|
||||
if (data['is_joined']) {
|
||||
player.addUser(user);
|
||||
if (user["user_id"] != player.userId) {
|
||||
player.showToast(user["user_id"], "Joined your channel!");
|
||||
}
|
||||
} else {
|
||||
if (player.users.hasOwnProperty(user["user_id"])) {
|
||||
player.showToast(user["user_id"], "Left your channel!");
|
||||
delete player.users[user['user_id']];
|
||||
player.updateUser();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updatePause: function (player, data) {
|
||||
player.is_paused = data['pause'];
|
||||
if (data["pause"]) {
|
||||
var msg = "Paused the player."
|
||||
} else {
|
||||
var msg = "Resumed the player."
|
||||
}
|
||||
player.showToast(data["requester_id"], msg)
|
||||
},
|
||||
|
||||
updatePosition: function (player, data) {
|
||||
player.current_position = data["position"];
|
||||
},
|
||||
|
||||
swapTrack: function (player, data) {
|
||||
let index1 = player.current_queue_position + data['position2']["index"];
|
||||
let index2 = player.current_queue_position + data['position1']["index"];
|
||||
let track1 = player.queue[index1];
|
||||
let track2 = player.queue[index2];
|
||||
|
||||
if (track1?.track_id != data['position1']["track_id"] || track2?.track_id != data['position2']["track_id"]) {
|
||||
return player.send({ "op": "initPlayer" });
|
||||
}
|
||||
|
||||
player.queue[index1] = player.queue.splice(index2, 1, player.queue[index1])[0];
|
||||
player.initSortable();
|
||||
player.showToast(data["requester_id"], `${track1.title} and ${track2.title} are swapped`)
|
||||
},
|
||||
|
||||
moveTrack: function (player, data) {
|
||||
let position = player.current_queue_position + data["position"]["index"];
|
||||
let newPosition = player.current_queue_position + data["newPosition"]["index"];
|
||||
let element = player.queue.splice(position, 1)[0];
|
||||
if (element?.track_id != data["position"]["track_id"]) {
|
||||
return player.send({ "op": "initPlayer" });
|
||||
}
|
||||
player.queue.splice(newPosition, 0, element);
|
||||
|
||||
const $ul = $('#sortable');
|
||||
const $li = $ul.children().eq(position);
|
||||
$li.detach();
|
||||
$ul.children().eq(newPosition).before($li);
|
||||
player.showToast(data["requester_id"], `Moved ${element.title} to ${newPosition}`)
|
||||
},
|
||||
|
||||
shuffleTrack: function (player, data) {
|
||||
let tracks = data["tracks"];
|
||||
if (tracks != undefined) {
|
||||
player.queue = [];
|
||||
tracks.forEach(rawTrack => {
|
||||
let track = new Track(rawTrack);
|
||||
player.queue.push(track);
|
||||
});
|
||||
player.initSortable();
|
||||
player.showToast(data["requester_id"], "The queue is shuffled.")
|
||||
}
|
||||
},
|
||||
|
||||
repeatTrack: function (player, data) {
|
||||
player.repeat = data["repeatMode"];
|
||||
},
|
||||
|
||||
removeTrack: function (player, data) {
|
||||
const positions = data["positions"];
|
||||
const trackIds = data["track_ids"];
|
||||
var removeTracks = []
|
||||
var newQueue = [];
|
||||
|
||||
player.queue.forEach((track, index) => {
|
||||
if (positions.includes(index)) {
|
||||
if (trackIds.includes(track.track_id)) {
|
||||
if (index < player.current_queue_position) {
|
||||
player.current_queue_position -= 1;
|
||||
}
|
||||
removeTracks.push(track);
|
||||
} else {
|
||||
return player.send({ "op": "initPlayer" })
|
||||
}
|
||||
} else {
|
||||
newQueue.push(track);
|
||||
}
|
||||
})
|
||||
|
||||
player.queue = newQueue;
|
||||
player.initSortable();
|
||||
|
||||
if (removeTracks.length == 1) {
|
||||
var msg = `Removed ${removeTracks[0].title} from the queue.`;
|
||||
} else {
|
||||
var msg = `Removed ${removeTracks.length} tracks from the queue.`;
|
||||
}
|
||||
|
||||
player.showToast(data["requester_id"], msg);
|
||||
},
|
||||
|
||||
toggleAutoplay: function (player, data) {
|
||||
var status = data["status"];
|
||||
player.autoplay = status;
|
||||
},
|
||||
|
||||
errorMsg: function (player, data) {
|
||||
player.showToast(data["level"], data["msg"]);
|
||||
},
|
||||
|
||||
getPlaylists: function (player, data) {
|
||||
const playlists = data["playlists"]
|
||||
player.playlists = playlists;
|
||||
player.updatePlaylists();
|
||||
}
|
||||
}
|
||||
|
||||
class Player {
|
||||
constructor(userId) {
|
||||
this.socket = new Socket(`${window.location.protocol}//${window.location.hostname}:${window.location.port}`);
|
||||
this.socket.connect(this);
|
||||
this.socket.addMessageListener((msg) => this.handleMessage(msg));
|
||||
this.timer = new Timer(() => this.updateTime(), 1000);
|
||||
this.userId = parseInt(userId);
|
||||
this.isDJ = false;
|
||||
this.date = new Date();
|
||||
this.queue = [];
|
||||
this.users = {};
|
||||
this.searchList = [];
|
||||
this.repeat = "off";
|
||||
this.currentTrack = null;
|
||||
this.current_queue_position = 0;
|
||||
this.current_position = 0;
|
||||
this.is_paused = false;
|
||||
this.volume = 100;
|
||||
this.last_update = 0;
|
||||
this.is_connected = true;
|
||||
this.autoplay = false;
|
||||
|
||||
this.channelName = "";
|
||||
this.playlists = null;
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
const op = data["op"];
|
||||
const validActions = Object.keys(actions);
|
||||
|
||||
if (validActions.includes(op)) {
|
||||
actions[op](this, data);
|
||||
} else {
|
||||
console.log(`Invalid action: ${op}`)
|
||||
}
|
||||
|
||||
return this.updateInfo()
|
||||
}
|
||||
|
||||
init() {
|
||||
this.queue = []
|
||||
this.currentTrack = null;
|
||||
this.users = [];
|
||||
$('#sortable').empty();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
$('#sortable').empty();
|
||||
for (var i in this.queue) {
|
||||
var track = this.queue[i];
|
||||
$("#sortable").append(`<li><div class="track"><div class="left">${(this.isDJ) ? '<i class="fa-solid fa-bars handle"></i>' : ''}<img src=${track.imageUrl} /><div class="info"><p>${track.title}</p><p class="desc">${track.author}</p></div></div><p class="time">${this.msToReadableTime(track.length)}</p><i class="fa-solid fa-ellipsis-vertical action"></i></div></li>`)
|
||||
}
|
||||
this.updateCurrentQueuePos();
|
||||
}
|
||||
|
||||
updateCurrentQueuePos(pos) {
|
||||
if (pos != undefined) {
|
||||
this.current_queue_position = pos - 1;
|
||||
}
|
||||
this.currentTrack = this.queue[this.current_queue_position];
|
||||
|
||||
$('#sortable li div').removeClass('active');
|
||||
const li = $(`#sortable li:eq(${this.current_queue_position})`);
|
||||
li.find('div').addClass('active');
|
||||
|
||||
const queue = $('.queue-list')
|
||||
if (queue.prop('scrollHeight') > queue.prop('clientHeight')) {
|
||||
queue.animate({ scrollTop: li.position().top - queue.position().top }, 'slow');
|
||||
}
|
||||
return this.currentTrack;
|
||||
}
|
||||
|
||||
addUser(user) {
|
||||
this.users[user['user_id']] = { avatar_url: user["avatar_url"], name: user["name"] };
|
||||
this.updateUser();
|
||||
}
|
||||
|
||||
updateUser() {
|
||||
$("#users-container").empty();
|
||||
for (const [id, user] of Object.entries(this.users)) {
|
||||
$("#users-container").append(`<li class="user"><div class="left"><img src=${user.avatar_url} /><p>${user.name}</p></div><i class="fa-solid fa-microphone"></i></li>`)
|
||||
};
|
||||
}
|
||||
addTrack(tracks) {
|
||||
for (let i in tracks) {
|
||||
let track = decode(tracks[i])
|
||||
this.queue.push(track);
|
||||
$("#sortable").append(`<li><div class="track"><div class="left">${(this.isDJ) ? '<i class="fa-solid fa-bars handle"></i>' : ''}<img src=${track.imageUrl} /><div class="info"><p>${track.title}</p><p class="desc">${track.author}</p></div></div><p class="time">${this.msToReadableTime(track.length)}</p><i class="fa-solid fa-ellipsis-vertical action"></i></div></li>`)
|
||||
}
|
||||
}
|
||||
|
||||
moveTrack(target, to) {
|
||||
const c = this.current_queue_position;
|
||||
let element = this.queue.splice(target, 1)[0];
|
||||
this.queue.splice(to, 0, element);
|
||||
|
||||
const $ul = $('#sortable');
|
||||
const $li = $ul.children().eq(position);
|
||||
$li.detach();
|
||||
$ul.children().eq(to).before($li);
|
||||
|
||||
if (target > c && to <= c) {
|
||||
this.current_queue_position += 1;
|
||||
} else if (target < c && to >= c) {
|
||||
this.current_queue_position -= 1;
|
||||
} else if (target == c) {
|
||||
this.current_queue_position = to;
|
||||
}
|
||||
|
||||
this.send({ "op": "moveTrack", "position": target, "newPosition": to })
|
||||
}
|
||||
|
||||
removeTrack(position, track) {
|
||||
let rawTrack = this.queue[position];
|
||||
if (position == this.current_queue_position) {
|
||||
return this.showToast("error", "You are not allow to remove playing track!");
|
||||
}
|
||||
if (rawTrack.track_id == track.track_id) {
|
||||
this.send({ "op": "removeTrack", "position": position, "track_id": track.track_id });
|
||||
} else {
|
||||
this.showToast("error", "Track not found!");
|
||||
}
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
this.send({ "op": "updatePause", "pause": !this.is_paused });
|
||||
}
|
||||
|
||||
skipTo(index = 1) {
|
||||
this.send({ "op": "skipTo", "index": index });
|
||||
}
|
||||
|
||||
backTo(index = 1) {
|
||||
this.send({ "op": "backTo", "index": index });
|
||||
}
|
||||
|
||||
seekTo(tempPosition) {
|
||||
if (this.currentTrack == undefined) {
|
||||
return;
|
||||
}
|
||||
let position = tempPosition / 500 * this.currentTrack.length;
|
||||
this.send({ "op": "updatePosition", "position": Math.trunc(position) });
|
||||
}
|
||||
|
||||
shuffle() {
|
||||
if ((this.queue.length - this.current_queue_position) > 3) {
|
||||
this.send({ "op": "shuffleTrack" });
|
||||
} else {
|
||||
this.showToast("info", "Add more songs to the queue before shuffling.");
|
||||
}
|
||||
}
|
||||
|
||||
repeatMode() {
|
||||
this.send({ "op": "repeatTrack" })
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
let json = JSON.stringify(payload)
|
||||
this.socket.send(json);
|
||||
}
|
||||
|
||||
isPlaying() {
|
||||
return (this.currentTrack != undefined && this.is_connected);
|
||||
}
|
||||
|
||||
msToReadableTime(ms) {
|
||||
let totalSeconds = Math.floor(ms / 1000);
|
||||
|
||||
let hours = Math.floor(totalSeconds / 3600);
|
||||
let minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
let seconds = totalSeconds % 60;
|
||||
|
||||
minutes = (minutes < 10) ? "0" + minutes : minutes;
|
||||
seconds = (seconds < 10) ? "0" + seconds : seconds;
|
||||
|
||||
let timeString = "";
|
||||
if (hours > 0) {
|
||||
timeString += hours + ":" + minutes + ":" + seconds;
|
||||
} else {
|
||||
timeString += minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
showToast(userId, msg) {
|
||||
if (userId == "info") {
|
||||
var user = { avatar_url: "/static/img/info.png", name: "Info" }
|
||||
} else if (userId == "error") {
|
||||
var user = { avatar_url: "/static/img/error.png", name: "Error" }
|
||||
} else {
|
||||
var user = this.users[userId];
|
||||
}
|
||||
if (user != null) {
|
||||
let $element = $(`<div class="toast"><img src=${user['avatar_url']} alt="user-icon"/><div class="content"><p class="username">${user['name']}</p><p class="message">${msg}</p></div></div>`)
|
||||
$(".toastContrainer").append($element)
|
||||
|
||||
setTimeout(function () {
|
||||
$element.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
updateTime() {
|
||||
if (this.currentTrack == undefined) {
|
||||
return this.timer.stop();
|
||||
}
|
||||
|
||||
if (this.current_position >= this.currentTrack?.length) {
|
||||
return this.timer.stop();
|
||||
}
|
||||
this.current_position += 1000;
|
||||
$("#position").text(this.msToReadableTime(this.current_position));
|
||||
|
||||
let time = (this.current_position / this.currentTrack?.length) * 500;
|
||||
$("#seek-bar").val(time);
|
||||
}
|
||||
|
||||
updateInfo() {
|
||||
let currentTrack = this.currentTrack;
|
||||
if (currentTrack == undefined) {
|
||||
$("#title").text("");
|
||||
$("#author").text("");
|
||||
$("#position").text("00:00");
|
||||
$("#length").text("00:00");
|
||||
$("#image").fadeOut(100, function () { $(this).removeAttr("src"); });
|
||||
$("#largeImage").fadeOut(100, function () { $(this).removeAttr("src"); });
|
||||
$(".thumbnail-background").fadeOut(200);
|
||||
|
||||
} else {
|
||||
$("#title").text(currentTrack.title);
|
||||
$("#author").text(currentTrack.author);
|
||||
$("#length").text(this.msToReadableTime(currentTrack.length));
|
||||
$("#auto-play").prop('checked', this.autoplay);
|
||||
|
||||
let image = "";
|
||||
if (currentTrack.source == "youtube") {
|
||||
image += `${window.location.protocol}//${window.location.hostname}:${window.location.port}/`;
|
||||
}
|
||||
image += currentTrack.imageUrl;
|
||||
|
||||
if ($("#largeImage").attr("src") != image) {
|
||||
$(".thumbnail-background").fadeOut(100);
|
||||
$("#largeImage").fadeOut(function () {
|
||||
$(this).attr("src", image);
|
||||
|
||||
});
|
||||
$("#image").fadeOut(function () {
|
||||
$(this).attr("src", currentTrack.imageUrl);
|
||||
});
|
||||
|
||||
if (this.playlists != null) {
|
||||
if (this.playlists["200"]["tracks"].includes(currentTrack.track_id)) {
|
||||
$("#like-btn").removeClass("fa-regular").addClass("fa-solid");
|
||||
} else {
|
||||
$("#like-btn").removeClass("fa-solid").addClass("fa-regular");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#channel-name").text((this.channelName == "") ? "Not Found" : this.channelName);
|
||||
let play_pause_btn = $("#play-pause-btn");
|
||||
let repeat_btn = $("#repeat-btn");
|
||||
if (this.is_paused || currentTrack == undefined) {
|
||||
play_pause_btn.removeClass('fa-pause').addClass('fa-play');
|
||||
if (this.timer.getIsRunning()) {
|
||||
this.timer.stop();
|
||||
}
|
||||
} else {
|
||||
play_pause_btn.removeClass('fa-play').addClass('fa-pause');
|
||||
this.timer.start();
|
||||
}
|
||||
|
||||
repeat_btn.removeClass("fa-repeat-1").addClass("fa-repeat")
|
||||
if (this.repeat == "off") {
|
||||
repeat_btn.css('color', '');
|
||||
} else if (this.repeat == "track") {
|
||||
repeat_btn.css('color', '#fff');
|
||||
} else {
|
||||
repeat_btn.removeClass("fa-repeat").addClass("fa-repeat-1");
|
||||
}
|
||||
}
|
||||
|
||||
updatePlaylists() {
|
||||
$("#playlists-grid").empty();
|
||||
for (let key in this.playlists) {
|
||||
const pList = this.playlists[key];
|
||||
let pDiv = $(`<div class="playlist" data-value="${key}">`);
|
||||
let iDiv = $("<div class='images'>");
|
||||
let infoDiv = $("<div class='info'>");
|
||||
|
||||
if (pList["tracks"] === 0) {
|
||||
continue;
|
||||
} else {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (pList["tracks"][i] == undefined) {
|
||||
continue;
|
||||
}
|
||||
let track = decode(pList["tracks"][i])
|
||||
let img = $("<img>").attr("src", track.imageUrl);
|
||||
iDiv.append(img);
|
||||
|
||||
}
|
||||
}
|
||||
iDiv.append(`<i class="fa-solid fa-ellipsis-vertical action"></i>`);
|
||||
iDiv.append(`<i class="fa-solid fa-play play"></i>`);
|
||||
pDiv.append(iDiv);
|
||||
infoDiv.append(`<p>${pList["name"]}</p>`);
|
||||
infoDiv.append(`<p class="desc">${pList["type"]} • ${pList["tracks"].length} Tracks</p>`);
|
||||
pDiv.append(infoDiv);
|
||||
$("#playlists-grid").append(pDiv);
|
||||
$("#playlists-tracks").empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
class Socket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
connect(player) {
|
||||
this.socket = io.connect(this.url);
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log("Connected to server!");
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
player.init();
|
||||
console.log("Disconnected from server!");
|
||||
});
|
||||
|
||||
this.socket.on('error', (e) => {
|
||||
console.log(e);
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
if (this.socket) {
|
||||
this.socket.emit('message', msg);
|
||||
}
|
||||
}
|
||||
|
||||
addMessageListener(callback) {
|
||||
if (this.socket) {
|
||||
this.socket.on('message', callback);
|
||||
}
|
||||
}
|
||||
|
||||
removeMessageListener(callback) {
|
||||
if (this.socket) {
|
||||
this.socket.off('message', callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Vocard Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
|
||||
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ+jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"
|
||||
integrity="sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/action.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/objects.js') }}"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
||||
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script> const userId = "{{ user.id }}"; </script>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<p id="homeBtn">Home</p>
|
||||
<p id="playlistBtn">Playlist</p>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="search-container">
|
||||
<div class="search-bar">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<input id="search-input" class="search-input"
|
||||
placeholder="Search your input and add it to the queue." />
|
||||
<div id="search-loader" class="search-loader"></div>
|
||||
</div>
|
||||
<ul id="search-result-list" class="search-result-list">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="dropdown-container">
|
||||
<div class="dropdown-content">
|
||||
<img src="{{ user.avatar.url }}" />
|
||||
<p>{{ user.username }}</p>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="logout"><i class="fa-solid fa-right-from-bracket"></i>Log Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main" id="main">
|
||||
<div class="toastContrainer"></div>
|
||||
<div class="thumbnail">
|
||||
<img id="largeImage" alt="" />
|
||||
<div class="thumbnail-background"></div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="auto-play">
|
||||
<div>
|
||||
<p>Autoplay</p>
|
||||
<p>Similar track will be to the queue</p>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="auto-play">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="queue-list">
|
||||
<ul id="sortable">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlists" id="playlists" style="display: None">
|
||||
<div class="playlists-grid" id="playlists-grid">
|
||||
</div>
|
||||
<div class="playlists-tracks" id="playlists-tracks">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="users-bar">
|
||||
<div class="info">
|
||||
<h3 id="channel-name"> </h3>
|
||||
<h5>You are listen in</h5>
|
||||
</div>
|
||||
<ul id="users-container" class="users-container">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="player-controller">
|
||||
<div class="progress-bar-container">
|
||||
<input type="range" value="0" min="0" max="500" class="seek-bar" id="seek-bar">
|
||||
<div class="position-info"></div>
|
||||
</div>
|
||||
<div class="control-container">
|
||||
<div class="left">
|
||||
<div class="control">
|
||||
<i id="back-btn" class="fa-solid fa-backward-step"></i>
|
||||
<i id="play-pause-btn" class="fa-solid fa-play"></i>
|
||||
<i id="skip-btn" class="fa-solid fa-forward-step"></i>
|
||||
</div>
|
||||
|
||||
<div class="position">
|
||||
<p id="position">00:00 </p>/ <p id="length"> 00:00</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<img class="image" id="image">
|
||||
<div class="data">
|
||||
<p id="title"></p>
|
||||
<p class="desc" id="author"></p>
|
||||
</div>
|
||||
<i class="fa-regular fa-thumbs-up" id="like-btn"></i>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<i id="repeat-btn" class="fa-solid fa-repeat"></i>
|
||||
<i id="shuffle-btn" class="fa-solid fa-shuffle"></i>
|
||||
<i id="users-btn" class="fa-solid fa-user-group"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="context-menu" class="context-menu">
|
||||
<ul>
|
||||
<li id="remove-track-btn"><i class="fa-solid fa-trash"></i>Remove</li>
|
||||
<li id="copy-track-btn"><i class="fa-solid fa-share"></i>Copy Song Link</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="playlist-context-menu" class="context-menu">
|
||||
<ul>
|
||||
<li id="remove-playlist-btn"><i class="fa-solid fa-trash"></i>Remove</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
215
web/webapp.py
215
web/webapp.py
@@ -1,215 +0,0 @@
|
||||
from flask import Flask, redirect, url_for, session, request, render_template, Response, stream_with_context
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room, rooms, disconnect
|
||||
from ipc import IPCClient
|
||||
from objects import User
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.getenv("DASHBOARD_SECRET_KEY")
|
||||
socketio = SocketIO(app)
|
||||
|
||||
# Discord OAuth2 credentials
|
||||
CLIENT_ID = os.getenv("CLIENT_ID")
|
||||
CLIENT_SECRET = os.getenv("CLIENT_SECRET_ID")
|
||||
HOST = os.getenv("DASHBOARD_HOST")
|
||||
PORT = os.getenv("DASHBOARD_PORT")
|
||||
IS_SECURE = os.getenv("DASHBOARD_SECURE", "False").lower() == "true"
|
||||
|
||||
REDIRECT_URI = f'http{'s' if IS_SECURE else ''}://{HOST}:{PORT}/callback'
|
||||
DISCORD_API_BASE_URL = 'https://discord.com/api'
|
||||
|
||||
USERS = {}
|
||||
|
||||
def get_user(user_id: int):
|
||||
for user in USERS.values():
|
||||
if user.id == user_id:
|
||||
return user
|
||||
return None
|
||||
|
||||
def user_join_room(guild_id: int, user: User) -> None:
|
||||
if not user.sid:
|
||||
return
|
||||
join_room(guild_id, sid=user.sid)
|
||||
user.guild_id = guild_id
|
||||
|
||||
def user_leave_room(guild_id: int, user: User) -> None:
|
||||
if not user.sid:
|
||||
return
|
||||
leave_room(guild_id, sid=user.sid)
|
||||
user.guild_id = None
|
||||
|
||||
def message_handler(data: dict):
|
||||
op = data.get("op")
|
||||
|
||||
user_id = data.get("user_id", None)
|
||||
if user_id:
|
||||
user: User = get_user(user_id)
|
||||
else:
|
||||
user = None
|
||||
|
||||
guild_id = data.get("guild_id", None)
|
||||
|
||||
if op == "updateGuild":
|
||||
user_id = data.get("user", {}).get("user_id", None)
|
||||
is_joined = data.get("is_joined")
|
||||
|
||||
user: User = get_user(user_id)
|
||||
if user and guild_id:
|
||||
user_join_room(guild_id, user) if is_joined else user_leave_room(guild_id, user)
|
||||
|
||||
elif op == "createPlayer":
|
||||
members_id = data.get("members_id", [])
|
||||
for member_id in members_id:
|
||||
user: User = get_user(member_id)
|
||||
if user:
|
||||
user_join_room(guild_id, user)
|
||||
emit('message', {
|
||||
"op": "updateGuild",
|
||||
"user": {
|
||||
"user_id": user.id,
|
||||
"avatar_url": user.avatar.url,
|
||||
"name": user.username
|
||||
},
|
||||
"is_joined": True
|
||||
}, to=user.sid)
|
||||
return
|
||||
|
||||
if user:
|
||||
if guild_id and user.sid:
|
||||
join_room(guild_id, sid=user.sid)
|
||||
elif guild_id in rooms(user.sid):
|
||||
pass
|
||||
else:
|
||||
emit('message', data, to=user.sid)
|
||||
|
||||
if op == "initPlayer":
|
||||
return emit('message', data, to=user.sid)
|
||||
|
||||
if guild_id:
|
||||
skip_sids = []
|
||||
if skip_users := data.get("skip_users"):
|
||||
for user_id in skip_users:
|
||||
if user := get_user(user_id):
|
||||
skip_sids.append(user.sid)
|
||||
|
||||
emit('message', data, room=guild_id, skip_sid=skip_sids)
|
||||
|
||||
ipc_client = IPCClient(secret_key=app.secret_key, callback=message_handler)
|
||||
|
||||
def login_required(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
token = session.get("discord_token", None)
|
||||
if not token:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if token not in USERS:
|
||||
resp = requests_api(f'{DISCORD_API_BASE_URL}/users/@me', headers={'Authorization': f'Bearer {token}'})
|
||||
if resp:
|
||||
user = USERS[token] = User(resp)
|
||||
else:
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
user = USERS[token]
|
||||
|
||||
return func(user, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def requests_api(url: str, headers=None):
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
if not resp:
|
||||
return False
|
||||
|
||||
return resp.json()
|
||||
|
||||
@app.route('/<path:url>', methods=["GET"])
|
||||
@login_required
|
||||
def proxy(user: User, url):
|
||||
request = requests.get(url, stream=True)
|
||||
response = Response(
|
||||
stream_with_context(request.iter_content()),
|
||||
content_type=request.headers['content-type'],
|
||||
status=request.status_code
|
||||
)
|
||||
response.headers['Access-Control-Allow-Origin'] = "*"
|
||||
return response
|
||||
|
||||
# home page
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def home(user: User):
|
||||
return render_template("index.html", user=user)
|
||||
|
||||
# login page
|
||||
@app.route('/login')
|
||||
def login():
|
||||
# redirect to Discord OAuth2 login page
|
||||
params = {
|
||||
'client_id': CLIENT_ID,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'scope': 'identify'
|
||||
}
|
||||
return redirect(f'{DISCORD_API_BASE_URL}/oauth2/authorize?{"&".join([f"{k}={v}" for k, v in params.items()])}')
|
||||
|
||||
# Logout page
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout(user: User):
|
||||
session.pop("discord_token", None)
|
||||
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# callback page
|
||||
@app.route('/callback')
|
||||
def callback():
|
||||
# fetch user token from Discord OAuth2
|
||||
code = request.args.get('code')
|
||||
data = {
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'scope': 'identify'
|
||||
}
|
||||
response = requests.post(f'{DISCORD_API_BASE_URL}/oauth2/token', data=data)
|
||||
token_data = json.loads(response.content.decode('utf-8'))
|
||||
session.permanent = True
|
||||
app.permanent_session_lifetime = timedelta(days=30)
|
||||
session['discord_token'] = token_data.get("access_token")
|
||||
|
||||
return redirect(url_for("home"))
|
||||
|
||||
|
||||
@socketio.on("connect")
|
||||
@login_required
|
||||
def handle_connect(user: User):
|
||||
if user.sid:
|
||||
disconnect(sid=user.sid)
|
||||
user.sid = request.sid
|
||||
asyncio.run(ipc_client.send('{"op": "initPlayer"}', user))
|
||||
|
||||
@socketio.on("disconnect")
|
||||
@login_required
|
||||
def handle_disconnect(user: User):
|
||||
user.sid = None
|
||||
|
||||
@socketio.on("message")
|
||||
@login_required
|
||||
def handle_message(user: User, msg):
|
||||
asyncio.run(ipc_client.send(msg, user))
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, host="0.0.0.0", port=PORT, allow_unsafe_werkzeug=True)
|
||||
Reference in New Issue
Block a user