Merge pull request #122 from ChocoMeow/beta
Vocard v2.7.3 Update: A lot new features, bug fixes, and code clean-up
This commit is contained in:
@@ -11,7 +11,7 @@ Vocard is a highly customizable Discord music bot, designed to deliver a user-fr
|
||||
* Lightweight design
|
||||
* Smooth playback
|
||||
* Clean and nice interface
|
||||
* Supports many music platforms (YouTube, SoundCloud, etc.)
|
||||
* Supports many music platforms (YouTube, SoundCloud, Spotify, Apple Music etc.)
|
||||
* Built-in playlist support
|
||||
* Fully customizable settings
|
||||
* Lyrics support
|
||||
@@ -19,6 +19,7 @@ Vocard is a highly customizable Discord music bot, designed to deliver a user-fr
|
||||
* Multiple languages available
|
||||
* Easy to update
|
||||
* Supports docker
|
||||
* [One Click Installer](https://github.com/ChocoMeow/Vocard-Installer)
|
||||
* [Premium dashboard](https://github.com/ChocoMeow/Vocard-Dashboard)
|
||||
|
||||
## Screenshot
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .lyrics import LYRICS_PLATFORMS
|
||||
from .placeholders import Placeholders
|
||||
from .settings import Settings
|
||||
@@ -1,63 +0,0 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from discord.ext import commands
|
||||
from re import findall
|
||||
from importlib import import_module
|
||||
|
||||
class Placeholders:
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
self.voicelink = import_module("voicelink")
|
||||
self.variables = {
|
||||
"guilds": self.guilds_count,
|
||||
"users": self.users_count,
|
||||
"players": self.players_count,
|
||||
"nodes": self.nodes_count
|
||||
}
|
||||
|
||||
def guilds_count(self) -> int:
|
||||
return len(self.bot.guilds)
|
||||
|
||||
def users_count(self) -> int:
|
||||
return len(self.bot.users)
|
||||
|
||||
def players_count(self) -> int:
|
||||
count = 0
|
||||
for node in self.voicelink.NodePool._nodes.values():
|
||||
count += len(node._players)
|
||||
|
||||
return count
|
||||
|
||||
def nodes_count(self):
|
||||
return len(self.voicelink.NodePool._nodes)
|
||||
|
||||
def replace(self, msg: str) -> str:
|
||||
keys = findall(r'@@(.*?)@@', msg)
|
||||
|
||||
for key in keys:
|
||||
value = self.variables.get(key.lower(), None)
|
||||
if value:
|
||||
msg = msg.replace(f"@@{key}@@", str(value()))
|
||||
|
||||
return msg
|
||||
@@ -1,59 +0,0 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from typing import (
|
||||
Dict,
|
||||
List,
|
||||
Any,
|
||||
Union
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class Settings:
|
||||
def __init__(self, settings: Dict) -> None:
|
||||
self.token: str = settings.get("token") or os.getenv("TOKEN")
|
||||
self.client_id: int = int(settings.get("client_id", 0)) or int(os.getenv("CLIENT_ID"))
|
||||
self.genius_token: str = settings.get("genius_token") or os.getenv("GENIUS_TOKEN")
|
||||
self.mongodb_url: str = settings.get("mongodb_url") or os.getenv("MONGODB_URL")
|
||||
self.mongodb_name: str = settings.get("mongodb_name") or os.getenv("MONGODB_NAME")
|
||||
|
||||
self.invite_link: str = "https://discord.gg/wRCgB7vBQv"
|
||||
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: 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[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_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {})
|
||||
self.version: str = settings.get("version", "")
|
||||
338
cogs/basic.py
338
cogs/basic.py
@@ -21,39 +21,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord, voicelink, re
|
||||
import re
|
||||
import discord
|
||||
import voicelink
|
||||
|
||||
from io import StringIO
|
||||
from validators import url
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from function import (
|
||||
settings,
|
||||
send,
|
||||
time as ctime,
|
||||
format_time,
|
||||
get_source,
|
||||
get_user,
|
||||
get_lang,
|
||||
truncate_string,
|
||||
cooldown_check,
|
||||
get_aliases,
|
||||
logger
|
||||
)
|
||||
|
||||
from voicelink import SearchType, LoopType
|
||||
from addons import LYRICS_PLATFORMS
|
||||
from views import SearchView, ListView, LinkView, LyricsView, HelpView
|
||||
from validators import url
|
||||
from voicelink import MongoDBHandler, LangHandler, Config
|
||||
from voicelink.views import SearchView, QueueView, LinkView, LyricsView, HelpView
|
||||
from voicelink.utils import format_ms, format_to_ms, truncate_string, dispatch_message, send_localized_message
|
||||
|
||||
async def nowplay(ctx: commands.Context, player: voicelink.Player):
|
||||
track = player.current
|
||||
if not track:
|
||||
return await send(ctx, 'noTrackPlaying', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'player.errors.noTrackPlaying', ephemeral=True)
|
||||
|
||||
texts = await get_lang(ctx.guild.id, "nowplayingDesc", "nowplayingField", "nowplayingLink")
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "player.playback.nowplayingDesc", "player.playback.nowplayingField", "player.playback.nowplayingLink")
|
||||
upnext = "\n".join(f"`{index}.` `[{track.formatted_length}]` [{truncate_string(track.title)}]({track.uri})" for index, track in enumerate(player.queue.tracks()[:2], start=2))
|
||||
|
||||
embed = discord.Embed(description=texts[0].format(track.title), color=settings.embed_color)
|
||||
embed = discord.Embed(description=texts[0].format(track.title), color=Config().embed_color)
|
||||
embed.set_author(
|
||||
name=track.requester.display_name,
|
||||
icon_url=track.requester.display_avatar.url
|
||||
@@ -65,9 +59,9 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player):
|
||||
|
||||
pbar = "".join(":radio_button:" if i == round(player.position // round(track.length // 15)) else "▬" for i in range(15))
|
||||
icon = ":red_circle:" if track.is_stream else (":pause_button:" if player.is_paused else ":arrow_forward:")
|
||||
embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatted_length}]**", inline=False)
|
||||
embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{format_ms(player.position)}/{track.formatted_length}]**", inline=False)
|
||||
|
||||
return await send(ctx, embed, view=LinkView(texts[2].format(track.source.title()), track.emoji, track.uri))
|
||||
return await dispatch_message(ctx, embed, view=LinkView(texts[2].format(track.source.title()), track.emoji, track.uri))
|
||||
|
||||
class Basic(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
@@ -101,10 +95,10 @@ class Basic(commands.Cog):
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
tracks = tracks.tracks
|
||||
|
||||
return [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]
|
||||
return [app_commands.Choice(name=truncate_string(f"🎵 [{track.formatted_length}] {track.author} - {track.title}", 100), value=track.uri) for track in tracks]
|
||||
|
||||
history = {track["identifier"]: track for track_id in reversed(await get_user(interaction.user.id, "history")) if (track := voicelink.decode(track_id))["uri"]}
|
||||
return [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]
|
||||
history = {track["identifier"]: track for track_id in reversed(await MongoDBHandler.get_user(interaction.user.id, d_type="history")) if (track := voicelink.Track.decode(track_id))["uri"]}
|
||||
return [app_commands.Choice(name=truncate_string(f"🕒 [{format_ms(track['length'])}] {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25]
|
||||
|
||||
@commands.hybrid_command(name="connect", aliases=get_aliases("connect"))
|
||||
@app_commands.describe(channel="Provide a channel to connect.")
|
||||
@@ -114,9 +108,9 @@ class Basic(commands.Cog):
|
||||
try:
|
||||
player = await voicelink.connect_channel(ctx, channel)
|
||||
except discord.errors.ClientException:
|
||||
return await send(ctx, "alreadyConnected")
|
||||
return await send_localized_message(ctx, "voice.connection.alreadyConnected")
|
||||
|
||||
await send(ctx, 'connect', player.channel)
|
||||
await send_localized_message(ctx, "voice.connection.connect", player.channel)
|
||||
|
||||
@commands.hybrid_command(name="play", aliases=get_aliases("play"))
|
||||
@app_commands.describe(
|
||||
@@ -127,33 +121,35 @@ class Basic(commands.Cog):
|
||||
@app_commands.autocomplete(query=play_autocomplete)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
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."
|
||||
"Loads your input into the queue."
|
||||
if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done():
|
||||
await ctx.defer()
|
||||
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
player = await voicelink.connect_channel(ctx)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.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")
|
||||
return await send_localized_message(ctx, "player.errors.noTrackFound")
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end))
|
||||
await send(ctx, "playlistLoad", tracks.name, index)
|
||||
index = await player.add_track(tracks.tracks, start_time=format_to_ms(start), end_time=format_to_ms(end))
|
||||
await send_localized_message(ctx, "player.playback.playlistLoad", tracks.name, index)
|
||||
else:
|
||||
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")
|
||||
|
||||
position = await player.add_track(tracks[0], start_time=format_to_ms(start), end_time=format_to_ms(end))
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "common.status.live", "player.playback.trackLoadPos", "player.playback.trackLoad")
|
||||
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
|
||||
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
|
||||
|
||||
await send(
|
||||
await dispatch_message(
|
||||
ctx,
|
||||
stream_content + additional_content,
|
||||
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
|
||||
@@ -176,32 +172,32 @@ class Basic(commands.Cog):
|
||||
query = message.attachments[0].url
|
||||
|
||||
if not query:
|
||||
return await send(interaction, "noPlaySource", ephemeral=True)
|
||||
return await send_localized_message(interaction, "player.errors.noPlaySource", ephemeral=True)
|
||||
|
||||
player: voicelink.Player = interaction.guild.voice_client
|
||||
if not player:
|
||||
player = await voicelink.connect_channel(interaction)
|
||||
|
||||
if not player.is_user_join(interaction.user):
|
||||
return await send(interaction, "notInChannel", interaction.user.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(interaction, "voice.connection.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")
|
||||
return await send_localized_message(interaction, "player.errors.noTrackFound")
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
index = await player.add_track(tracks.tracks)
|
||||
await send(interaction, "playlistLoad", tracks.name, index)
|
||||
await send_localized_message(interaction, "player.playback.playlistLoad", tracks.name, index)
|
||||
else:
|
||||
position = await player.add_track(tracks[0])
|
||||
texts = await get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad")
|
||||
texts = await LangHandler.get_lang(interaction.guild.id, "common.status.live", "player.playback.trackLoadPos", "player.playback.trackLoad")
|
||||
|
||||
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
|
||||
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
|
||||
|
||||
await send(
|
||||
await dispatch_message(
|
||||
interaction,
|
||||
stream_content + additional_content,
|
||||
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
|
||||
@@ -218,31 +214,34 @@ class Basic(commands.Cog):
|
||||
)
|
||||
@app_commands.choices(platform=[
|
||||
app_commands.Choice(name=search_type.display_name, value=search_type.name)
|
||||
for search_type in SearchType
|
||||
for search_type in voicelink.SearchType
|
||||
])
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def search(self, ctx: commands.Context, *, query: str, platform: str = SearchType.YOUTUBE.name):
|
||||
"Loads your input and added it to the queue."
|
||||
async def search(self, ctx: commands.Context, *, query: str, platform: str = Config().search_platform.name):
|
||||
"Searches your query and displays the results."
|
||||
if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done():
|
||||
await ctx.defer()
|
||||
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
player = await voicelink.connect_channel(ctx)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
if url(query):
|
||||
return await send(ctx, "noLinkSupport", ephemeral=True)
|
||||
return await send_localized_message(ctx, "search.noLinkSupport", ephemeral=True)
|
||||
|
||||
search_type: SearchType = SearchType.match(platform) or SearchType.YOUTUBE
|
||||
search_type: voicelink.SearchType = voicelink.SearchType.from_platform(platform) or Config().search_platform
|
||||
tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=search_type)
|
||||
if not tracks:
|
||||
return await send(ctx, "noTrackFound")
|
||||
return await send_localized_message(ctx, "player.errors.noTrackFound")
|
||||
|
||||
texts = await get_lang(ctx.guild.id, "searchTitle", "searchDesc", "live", "trackLoad_pos", "trackLoad", "searchWait", "searchSuccess")
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "search.title", "search.desc", "common.status.live", "player.playback.trackLoadPos", "player.playback.trackLoad", "search.wait", "search.success")
|
||||
query_track = "\n".join(f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1))
|
||||
embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(get_source(search_type.display_name, "emoji"), search_type.display_name, len(tracks[0:10]), query_track), color=settings.embed_color)
|
||||
embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(Config().get_source_config(search_type.display_name, "emoji"), search_type.display_name, len(tracks[0:10]), query_track), color=Config().embed_color)
|
||||
view = SearchView(tracks=tracks[0:10], texts=[texts[5], texts[6]])
|
||||
view.response = await send(ctx, embed, view=view, ephemeral=True)
|
||||
view.response = await dispatch_message(ctx, embed, view=view, ephemeral=True)
|
||||
|
||||
await view.wait()
|
||||
if view.values is not None:
|
||||
@@ -251,7 +250,7 @@ class Basic(commands.Cog):
|
||||
track = tracks[int(value.split(". ")[0]) - 1]
|
||||
position = await player.add_track(track)
|
||||
msg += (f"`{texts[2]}`" if track.is_stream else "") + (texts[3].format(track.title, track.uri, track.author, track.formatted_length, position) if position >= 1 else texts[4].format(track.title, track.uri, track.author, track.formatted_length))
|
||||
await send(ctx, msg)
|
||||
await dispatch_message(ctx, msg)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
@@ -266,32 +265,35 @@ class Basic(commands.Cog):
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
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."
|
||||
if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done():
|
||||
await ctx.defer()
|
||||
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
player = await voicelink.connect_channel(ctx)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.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")
|
||||
return await send_localized_message(ctx, "player.errors.noTrackFound")
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
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)
|
||||
index = await player.add_track(tracks.tracks, start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
|
||||
await send_localized_message(ctx, "player.playback.playlistLoad", tracks.name, index)
|
||||
else:
|
||||
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")
|
||||
position = await player.add_track(tracks[0], start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "common.status.live", "player.playback.trackLoadPos", "player.playback.trackLoad")
|
||||
|
||||
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
|
||||
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
|
||||
|
||||
await send(
|
||||
await dispatch_message(
|
||||
ctx,
|
||||
stream_content + additional_content,
|
||||
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
|
||||
@@ -315,26 +317,26 @@ class Basic(commands.Cog):
|
||||
player = await voicelink.connect_channel(ctx)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingFunctionPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingFunction", 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")
|
||||
return await send_localized_message(ctx, "player.errors.noTrackFound")
|
||||
|
||||
try:
|
||||
if isinstance(tracks, voicelink.Playlist):
|
||||
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)
|
||||
index = await player.add_track(tracks.tracks, start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
|
||||
await send_localized_message(ctx, "player.playback.playlistLoad", tracks.name, index)
|
||||
else:
|
||||
texts = await get_lang(ctx.guild.id, "live", "trackLoad")
|
||||
await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "common.status.live", "player.playback.trackLoad")
|
||||
await player.add_track(tracks[0], start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
|
||||
|
||||
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
|
||||
|
||||
await send(
|
||||
await dispatch_message(
|
||||
ctx,
|
||||
stream_content + texts[1],
|
||||
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
|
||||
@@ -351,21 +353,21 @@ class Basic(commands.Cog):
|
||||
"Pause the music."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if player.is_paused:
|
||||
return await send(ctx, "pauseError", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.controls.pause.error", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.pause_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
return await send_localized_message(ctx, "voting.voted", ephemeral=True)
|
||||
|
||||
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)
|
||||
return await send_localized_message(ctx, "player.controls.pause.vote", ctx.author, len(player.pause_votes), required)
|
||||
|
||||
await player.set_pause(True, ctx.author)
|
||||
await send(ctx, "paused", ctx.author)
|
||||
await send_localized_message(ctx, player.controls.pause.success, ctx.author)
|
||||
|
||||
@commands.hybrid_command(name="resume", aliases=get_aliases("resume"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -373,21 +375,21 @@ class Basic(commands.Cog):
|
||||
"Resume the music."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_paused:
|
||||
return await send(ctx, "resumeError")
|
||||
return await send_localized_message(ctx, "player.controls.resume.error")
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.resume_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
return await send_localized_message(ctx, "voting.voted", ephemeral=True)
|
||||
|
||||
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)
|
||||
return await send_localized_message(ctx, "player.controls.resume.vote", ctx.author, len(player.resume_votes), required)
|
||||
|
||||
await player.set_pause(False, ctx.author)
|
||||
await send(ctx, "resumed", ctx.author)
|
||||
await send_localized_message(ctx, "player.controls.resume.success", ctx.author)
|
||||
|
||||
@commands.hybrid_command(name="skip", aliases=get_aliases("skip"))
|
||||
@app_commands.describe(index="Enter a index that you want to skip to.")
|
||||
@@ -396,28 +398,28 @@ class Basic(commands.Cog):
|
||||
"Skips to the next song or skips to the specified song."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.node._available:
|
||||
return await send(ctx, "nodeReconnect")
|
||||
return await send_localized_message(ctx, "player.errors.nodeReconnect")
|
||||
|
||||
if not player.is_playing:
|
||||
return await send(ctx, "skipError", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.controls.skip.error", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author == player.current.requester:
|
||||
pass
|
||||
elif ctx.author in player.skip_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
return await send_localized_message(ctx, "voting.voted", ephemeral=True)
|
||||
else:
|
||||
player.skip_votes.add(ctx.author)
|
||||
if len(player.skip_votes) < (required := player.required()):
|
||||
return await send(ctx, "skipVote", ctx.author, len(player.skip_votes), required)
|
||||
return await send_localized_message(ctx, "player.controls.skip.vote", ctx.author, len(player.skip_votes), required)
|
||||
|
||||
if index:
|
||||
player.queue.skipto(index)
|
||||
|
||||
await send(ctx, "skipped", ctx.author)
|
||||
await send_localized_message(ctx, "player.controls.skip.success", ctx.author)
|
||||
if player.queue._repeat.mode == voicelink.LoopType.TRACK:
|
||||
await player.set_repeat(voicelink.LoopType.OFF)
|
||||
|
||||
@@ -430,18 +432,18 @@ class Basic(commands.Cog):
|
||||
"Skips back to the previous song or skips to the specified previous song."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.node._available:
|
||||
return await send(ctx, "nodeReconnectode")
|
||||
return await send_localized_message(ctx, "player.errors.nodeReconnectode")
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.previous_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
return await send_localized_message(ctx, "voting.voted", ephemeral=True)
|
||||
|
||||
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)
|
||||
return await send_localized_message(ctx, "player.controls.back.vote", ctx.author, len(player.previous_votes), required)
|
||||
|
||||
if not player.is_playing:
|
||||
player.queue.backto(index)
|
||||
@@ -450,7 +452,7 @@ class Basic(commands.Cog):
|
||||
player.queue.backto(index + 1)
|
||||
await player.stop()
|
||||
|
||||
await send(ctx, "backed", ctx.author)
|
||||
await send_localized_message(ctx, "player.controls.back.success", ctx.author)
|
||||
if player.queue._repeat.mode == voicelink.LoopType.TRACK:
|
||||
await player.set_repeat(voicelink.LoopType.OFF)
|
||||
|
||||
@@ -461,19 +463,19 @@ class Basic(commands.Cog):
|
||||
"Change the player position."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPosPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True)
|
||||
|
||||
if not player.current or player.position == 0:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
if not (num := format_time(position)):
|
||||
return await send(ctx, "timeFormatError", ephemeral=True)
|
||||
if not (num := format_to_ms(position)):
|
||||
return await send_localized_message(ctx, "time.formatError", ephemeral=True)
|
||||
|
||||
await player.seek(num, ctx.author)
|
||||
await send(ctx, "seek", position)
|
||||
await send_localized_message(ctx, "player.controls.seek", position)
|
||||
|
||||
@commands.hybrid_group(
|
||||
name="queue",
|
||||
@@ -486,15 +488,15 @@ class Basic(commands.Cog):
|
||||
"Display the players queue songs in your queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
if player.queue.is_empty:
|
||||
return await nowplay(ctx, player)
|
||||
view = ListView(player=player, author=ctx.author)
|
||||
view.response = await send(ctx, await view.build_embed(), view=view)
|
||||
view = QueueView(player=player, author=ctx.author)
|
||||
view.response = await dispatch_message(ctx, await view.build_embed(), view=view)
|
||||
|
||||
@queue.command(name="export", aliases=get_aliases("export"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -502,13 +504,13 @@ class Basic(commands.Cog):
|
||||
"Exports the entire queue to a text file"
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
if player.queue.is_empty and not player.current:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
await ctx.defer()
|
||||
|
||||
@@ -518,7 +520,7 @@ class Basic(commands.Cog):
|
||||
|
||||
total_length = 0
|
||||
for index, track in enumerate(tracks, start=1):
|
||||
temp += f"{index}. {track.title} [{ctime(track.length)}]\n"
|
||||
temp += f"{index}. {track.title} [{format_ms(track.length)}]\n"
|
||||
raw += track.track_id
|
||||
if index != len(tracks):
|
||||
raw += ","
|
||||
@@ -527,7 +529,7 @@ class Basic(commands.Cog):
|
||||
temp = "!Remember do not change this file!\n------------->Info<-------------\nGuild: {} ({})\nRequester: {} ({})\nTracks: {} - {}\n------------>Tracks<------------\n".format(
|
||||
ctx.guild.name, ctx.guild.id,
|
||||
ctx.author.display_name, ctx.author.id,
|
||||
len(tracks), ctime(total_length)
|
||||
len(tracks), format_ms(total_length)
|
||||
) + temp
|
||||
temp += raw
|
||||
|
||||
@@ -542,19 +544,19 @@ class Basic(commands.Cog):
|
||||
player = await voicelink.connect_channel(ctx)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
try:
|
||||
bytes = await attachment.read()
|
||||
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.Track.decode(track_id), requester=ctx.author) for track_id in track_ids]
|
||||
if not tracks:
|
||||
return await send(ctx, "noTrackFound")
|
||||
return await send_localized_message(ctx, "player.errors.noTrackFound")
|
||||
|
||||
index = await player.add_track(tracks)
|
||||
await send(ctx, "playlistLoad", attachment.filename, index)
|
||||
await send_localized_message(ctx, "player.playback.playlistLoad", attachment.filename, index)
|
||||
except Exception as e:
|
||||
logger.error("error", exc_info=e)
|
||||
raise e
|
||||
@@ -569,16 +571,16 @@ class Basic(commands.Cog):
|
||||
"Display the players queue songs in your history queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
if not player.queue.history():
|
||||
return await nowplay(ctx, player)
|
||||
|
||||
view = ListView(player=player, author=ctx.author, is_queue=False)
|
||||
view.response = await send(ctx, await view.build_embed(), view=view)
|
||||
view = QueueView(player=player, author=ctx.author, is_queue=False)
|
||||
view.response = await dispatch_message(ctx, await view.build_embed(), view=view)
|
||||
|
||||
@commands.hybrid_command(name="leave", aliases=get_aliases("leave"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -586,19 +588,19 @@ class Basic(commands.Cog):
|
||||
"Disconnects the bot from your voice channel and chears the queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.stop_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
return await send_localized_message(ctx, "voting.voted", ephemeral=True)
|
||||
else:
|
||||
player.stop_votes.add(ctx.author)
|
||||
if len(player.stop_votes) >= (required := player.required(leave=True)):
|
||||
pass
|
||||
else:
|
||||
return await send(ctx, "leaveVote", ctx.author, len(player.stop_votes), required)
|
||||
return await send_localized_message(ctx, "player.controls.leave.vote", ctx.author, len(player.stop_votes), required)
|
||||
|
||||
await send(ctx, "left", ctx.author)
|
||||
await send_localized_message(ctx, "player.controls.leave.success", ctx.author)
|
||||
await player.teardown()
|
||||
|
||||
@commands.hybrid_command(name="nowplaying", aliases=get_aliases("nowplaying"))
|
||||
@@ -607,10 +609,10 @@ class Basic(commands.Cog):
|
||||
"Shows details of the current track."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
await nowplay(ctx, player)
|
||||
|
||||
@@ -618,20 +620,20 @@ class Basic(commands.Cog):
|
||||
@app_commands.describe(mode="Choose a looping mode.")
|
||||
@app_commands.choices(mode=[
|
||||
app_commands.Choice(name=loop_type.name.title(), value=loop_type.name)
|
||||
for loop_type in LoopType
|
||||
for loop_type in voicelink.LoopType
|
||||
])
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def loop(self, ctx: commands.Context, mode: str):
|
||||
"Changes Loop mode."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingModePerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingMode", ephemeral=True)
|
||||
|
||||
await player.set_repeat(LoopType[mode] if mode in LoopType.__members__ else LoopType.OFF, ctx.author)
|
||||
await send(ctx, "repeat", mode.capitalize())
|
||||
await player.set_repeat(voicelink.LoopType[mode] if mode in voicelink.LoopType.__members__ else voicelink.LoopType.OFF, ctx.author)
|
||||
await send_localized_message(ctx, "player.controls.repeat", mode.capitalize())
|
||||
|
||||
@commands.hybrid_command(name="clear", aliases=get_aliases("clear"))
|
||||
@app_commands.describe(queue="Choose a queue that you want to clear.")
|
||||
@@ -644,13 +646,13 @@ class Basic(commands.Cog):
|
||||
"Remove all the tracks in your queue or history queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingQueuePerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingQueue", ephemeral=True)
|
||||
|
||||
await player.clear_queue(queue, ctx.author)
|
||||
await send(ctx, "cleared", queue.capitalize())
|
||||
await send_localized_message(ctx, "queue.management.cleared", queue.capitalize())
|
||||
|
||||
@commands.hybrid_command(name="remove", aliases=get_aliases("remove"))
|
||||
@app_commands.describe(
|
||||
@@ -663,13 +665,13 @@ class Basic(commands.Cog):
|
||||
"Removes specified track or a range of tracks from the queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingQueuePerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingQueue", ephemeral=True)
|
||||
|
||||
removed_tracks = await player.remove_track(position1, position2, remove_target=member, requester=ctx.author)
|
||||
await send(ctx, "removed", len(removed_tracks.keys()))
|
||||
await send_localized_message(ctx, "queue.management.removed", len(removed_tracks.keys()))
|
||||
|
||||
@commands.hybrid_command(name="forward", aliases=get_aliases("forward"))
|
||||
@app_commands.describe(position="Input an amount that you to forward to. Exmaple: 1:20")
|
||||
@@ -678,19 +680,19 @@ class Basic(commands.Cog):
|
||||
"Forwards by a certain amount of time in the current track. The default is 10 seconds."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPosPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
if not (num := format_time(position)):
|
||||
return await send(ctx, "timeFormatError", ephemeral=True)
|
||||
if not (num := format_to_ms(position)):
|
||||
return await send_localized_message(ctx, "time.formatError", ephemeral=True)
|
||||
|
||||
await player.seek(int(player.position + num))
|
||||
await send(ctx, "forward", ctime(player.position + num))
|
||||
await send_localized_message(ctx, "player.controls.forward", format_ms(player.position + num))
|
||||
|
||||
@commands.hybrid_command(name="rewind", aliases=get_aliases("rewind"))
|
||||
@app_commands.describe(position="Input an amount that you to rewind to. Exmaple: 1:20")
|
||||
@@ -699,19 +701,19 @@ class Basic(commands.Cog):
|
||||
"Rewind by a certain amount of time in the current track. The default is 10 seconds."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPosPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
if not (num := format_time(position)):
|
||||
return await send(ctx, "timeFormatError", ephemeral=True)
|
||||
if not (num := format_to_ms(position)):
|
||||
return await send_localized_message(ctx, "time.formatError", ephemeral=True)
|
||||
|
||||
await player.seek(int(player.position - num))
|
||||
await send(ctx, "rewind", ctime(player.position - num))
|
||||
await send_localized_message(ctx, "player.controls.rewind", format_ms(player.position - num))
|
||||
|
||||
@commands.hybrid_command(name="replay", aliases=get_aliases("replay"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -719,16 +721,16 @@ class Basic(commands.Cog):
|
||||
"Reset the progress of the current song."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPosPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True)
|
||||
|
||||
if not player.current:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
await player.seek(0)
|
||||
await send(ctx, "replay")
|
||||
await send_localized_message(ctx, "player.controls.replay")
|
||||
|
||||
@commands.hybrid_command(name="shuffle", aliases=get_aliases("shuffle"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -736,18 +738,18 @@ class Basic(commands.Cog):
|
||||
"Randomizes the tracks in the queue."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
if ctx.author in player.shuffle_votes:
|
||||
return await send(ctx, "voted", ephemeral=True)
|
||||
return await send_localized_message(ctx, "voting.voted", ephemeral=True)
|
||||
|
||||
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)
|
||||
return await send_localized_message(ctx, "player.controls.shuffle.vote", ctx.author, len(player.shuffle_votes), required)
|
||||
|
||||
await player.shuffle("queue", ctx.author)
|
||||
await send(ctx, "shuffled")
|
||||
await send_localized_message(ctx, "player.controls.shuffle.success")
|
||||
|
||||
@commands.hybrid_command(name="swap", aliases=get_aliases("swap"))
|
||||
@app_commands.describe(
|
||||
@@ -759,13 +761,13 @@ class Basic(commands.Cog):
|
||||
"Swaps the specified song to the specified song."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPosPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True)
|
||||
|
||||
track1, track2 = await player.swap_track(position1, position2, ctx.author)
|
||||
await send(ctx, "swapped", track1.title, track2.title)
|
||||
await send_localized_message(ctx, "queue.management.swapped", track1.title, track2.title)
|
||||
|
||||
@commands.hybrid_command(name="move", aliases=get_aliases("move"))
|
||||
@app_commands.describe(
|
||||
@@ -777,36 +779,36 @@ class Basic(commands.Cog):
|
||||
"Moves the specified song to the specified position."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingPosPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True)
|
||||
|
||||
moved_track = await player.move_track(target, to, ctx.author)
|
||||
await send(ctx, "moved", moved_track, to)
|
||||
await send_localized_message(ctx, "queue.management.moved", moved_track, to)
|
||||
|
||||
@commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics"))
|
||||
@app_commands.describe(title="Searches for your query and displays the reutned lyrics.")
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def lyrics(self, ctx: commands.Context, title: str = "", artist: str = ""):
|
||||
async def lyrics(self, ctx: commands.Context, *, title: str = "", artist: str = ""):
|
||||
"Displays lyrics for the playing track."
|
||||
if not title:
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player or not player.is_playing:
|
||||
return await send(ctx, "noTrackPlaying", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
title = player.current.title
|
||||
artist = player.current.author
|
||||
|
||||
await ctx.defer()
|
||||
lyrics_platform = LYRICS_PLATFORMS.get(settings.lyrics_platform)
|
||||
lyrics_platform = voicelink.LYRICS_PLATFORMS.get(Config().lyrics_platform)
|
||||
if lyrics_platform:
|
||||
lyrics = await lyrics_platform().get_lyrics(title, artist)
|
||||
if not lyrics:
|
||||
return await send(ctx, "lyricsNotFound", ephemeral=True)
|
||||
return await send_localized_message(ctx, "lyrics.notFound", ephemeral=True)
|
||||
|
||||
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=ctx.author)
|
||||
view.response = await send(ctx, view.build_embed(), view=view)
|
||||
view.response = await dispatch_message(ctx, await view.build_embed(), view=view)
|
||||
|
||||
@commands.hybrid_command(name="swapdj", aliases=get_aliases("swapdj"))
|
||||
@app_commands.describe(member="Choose a member to transfer the dj role.")
|
||||
@@ -815,22 +817,22 @@ class Basic(commands.Cog):
|
||||
"Transfer dj to another."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_user_join(ctx.author):
|
||||
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "voice.connection.notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
|
||||
|
||||
if player.dj.id != ctx.author.id or player.settings.get('dj', False):
|
||||
return await send(ctx, "notdj", f"<@&{player.settings['dj']}>" if player.settings.get('dj') else player.dj.mention, ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.notDj", f"<@&{player.settings['dj']}>" if player.settings.get('dj') else player.dj.mention, ephemeral=True)
|
||||
|
||||
if player.dj.id == member.id or member.bot:
|
||||
return await send(ctx, "djToMe", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.djToSelf", ephemeral=True)
|
||||
|
||||
if member not in player.channel.members:
|
||||
return await send(ctx, "djNotInChannel", member, ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.djNotInChannel", member, ephemeral=True)
|
||||
|
||||
player.dj = member
|
||||
await send(ctx, "djswap", member)
|
||||
await send_localized_message(ctx, "permissions.djSwapped", member)
|
||||
|
||||
@commands.hybrid_command(name="autoplay", aliases=get_aliases("autoplay"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -838,14 +840,14 @@ class Basic(commands.Cog):
|
||||
"Toggles autoplay mode, it will automatically queue the best songs to play."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
return await send(ctx, "noPlayer", ephemeral=True)
|
||||
return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True)
|
||||
|
||||
if not player.is_privileged(ctx.author):
|
||||
return await send(ctx, "missingAutoPlayPerm", ephemeral=True)
|
||||
return await send_localized_message(ctx, "permissions.missingAutoPlay", ephemeral=True)
|
||||
|
||||
check = not player.settings.get("autoplay", False)
|
||||
player.settings['autoplay'] = check
|
||||
await send(ctx, "autoplay", await get_lang(ctx.guild.id, "enabled" if check else "disabled"))
|
||||
await send_localized_message(ctx, "player.controls.autoplay", await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if check else "common.status.disabled"))
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
@@ -862,7 +864,7 @@ class Basic(commands.Cog):
|
||||
category = "News"
|
||||
view = HelpView(self.bot, ctx.author)
|
||||
embed = view.build_embed(category)
|
||||
view.response = await send(ctx, embed, view=view)
|
||||
view.response = await dispatch_message(ctx, embed, view=view)
|
||||
|
||||
@commands.hybrid_command(name="ping", aliases=get_aliases("ping"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -870,9 +872,9 @@ class Basic(commands.Cog):
|
||||
"Test if the bot is alive, and see the delay between your commands and my response."
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
|
||||
value = await get_lang(ctx.guild.id, "pingTitle1", "pingField1", "pingTitle2", "pingField2")
|
||||
value = await LangHandler.get_lang(ctx.guild.id, "ping.title1", "ping.field1", "ping.title2", "ping.field2")
|
||||
|
||||
embed = discord.Embed(color=settings.embed_color)
|
||||
embed = discord.Embed(color=Config().embed_color)
|
||||
embed.add_field(
|
||||
name=value[0],
|
||||
value=value[1].format(
|
||||
@@ -887,7 +889,7 @@ class Basic(commands.Cog):
|
||||
inline=False
|
||||
)
|
||||
|
||||
await send(ctx, embed)
|
||||
await dispatch_message(ctx, embed)
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Basic(bot))
|
||||
@@ -25,24 +25,24 @@ import discord
|
||||
import voicelink
|
||||
|
||||
from function import (
|
||||
send,
|
||||
get_lang,
|
||||
get_aliases,
|
||||
cooldown_check
|
||||
)
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from voicelink import LangHandler
|
||||
from voicelink.utils import send_localized_message
|
||||
|
||||
|
||||
async def check_access(ctx: commands.Context):
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
text = await get_lang(ctx.guild.id, "noPlayer")
|
||||
text = await LangHandler.get_lang(ctx.guild.id, "player.errors.noPlayer")
|
||||
raise voicelink.exceptions.VoicelinkException(text)
|
||||
|
||||
if ctx.author not in player.channel.members:
|
||||
if not ctx.author.guild_permissions.manage_guild:
|
||||
text = await get_lang(ctx.guild.id, "notInChannel")
|
||||
text = await LangHandler.get_lang(ctx.guild.id, "voice.connection.notInChannel")
|
||||
raise voicelink.exceptions.VoicelinkException(text.format(ctx.author.mention, player.channel.mention))
|
||||
|
||||
return player
|
||||
@@ -72,7 +72,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Timescale(tag="speed", speed=value)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="karaoke", aliases=get_aliases("karaoke"))
|
||||
@app_commands.describe(
|
||||
@@ -91,7 +91,7 @@ class Effect(commands.Cog):
|
||||
|
||||
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)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="tremolo", aliases=get_aliases("tremolo"))
|
||||
@app_commands.describe(
|
||||
@@ -108,7 +108,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Tremolo(tag="tremolo", frequency=frequency, depth=depth)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="vibrato", aliases=get_aliases("vibrato"))
|
||||
@app_commands.describe(
|
||||
@@ -125,7 +125,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Vibrato(tag="vibrato", frequency=frequency, depth=depth)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="rotation", aliases=get_aliases("rotation"))
|
||||
@app_commands.describe(hertz="The hertz of the rotation. Default is `0.2`")
|
||||
@@ -139,7 +139,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Rotation(tag="rotation", rotation_hertz=hertz)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="distortion", aliases=get_aliases("distortion"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -152,7 +152,7 @@ class Effect(commands.Cog):
|
||||
|
||||
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)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="lowpass", aliases=get_aliases("lowpass"))
|
||||
@app_commands.describe(smoothing="The level of the lowPass. Default is `20.0`")
|
||||
@@ -166,7 +166,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.LowPass(tag="lowpass", smoothing=smoothing)
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="channelmix", aliases=get_aliases("channelmix"))
|
||||
@app_commands.describe(
|
||||
@@ -185,7 +185,7 @@ class Effect(commands.Cog):
|
||||
|
||||
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)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="nightcore", aliases=get_aliases("nightcore"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -195,7 +195,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Timescale.nightcore()
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="8d", aliases=get_aliases("8d"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -205,7 +205,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Rotation.nightD()
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="vaporwave", aliases=get_aliases("vaporwave"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -215,7 +215,7 @@ class Effect(commands.Cog):
|
||||
|
||||
effect = voicelink.Timescale.vaporwave()
|
||||
await player.add_filter(effect, ctx.author)
|
||||
await send(ctx, "addEffect", effect.tag)
|
||||
await send_localized_message(ctx, "effects.added", effect.tag)
|
||||
|
||||
@commands.hybrid_command(name="cleareffect", aliases=get_aliases("cleareffect"))
|
||||
@app_commands.describe(effect="Remove a specific sound effects.")
|
||||
@@ -230,7 +230,7 @@ class Effect(commands.Cog):
|
||||
else:
|
||||
await player.reset_filter()
|
||||
|
||||
await send(ctx, "clearEffect")
|
||||
await send_localized_message(ctx, "effects.cleared")
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Effect(bot))
|
||||
|
||||
@@ -29,6 +29,9 @@ import function as func
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from voicelink import MongoDBHandler, Config
|
||||
from voicelink.utils import TempCtx
|
||||
|
||||
class Listeners(commands.Cog):
|
||||
"""Music Cog."""
|
||||
|
||||
@@ -41,20 +44,16 @@ class Listeners(commands.Cog):
|
||||
|
||||
async def start_nodes(self) -> None:
|
||||
"""Connect and intiate nodes."""
|
||||
for n in func.settings.nodes.values():
|
||||
for n in Config().nodes.values():
|
||||
try:
|
||||
await self.voicelink.create_node(
|
||||
bot=self.bot,
|
||||
logger=func.logger,
|
||||
**n
|
||||
)
|
||||
await self.voicelink.create_node(bot=self.bot, **n)
|
||||
except Exception as e:
|
||||
func.logger.error(f'Node {n["identifier"]} is not able to connect! - Reason: {e}')
|
||||
|
||||
async def restore_last_session_players(self) -> None:
|
||||
"""Re-establish connections for players from the last session."""
|
||||
await self.bot.wait_until_ready()
|
||||
players = func.open_json(func.LAST_SESSION_FILE_NAME)
|
||||
players = func.open_json(Config.LAST_SESSION_FILE_DIR)
|
||||
if not players:
|
||||
return
|
||||
|
||||
@@ -75,11 +74,11 @@ class Listeners(commands.Cog):
|
||||
continue
|
||||
|
||||
# Get the guild settings
|
||||
settings = await func.get_settings(channel.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(channel.guild.id)
|
||||
|
||||
# Connect to the channel and initialize the player.
|
||||
player: voicelink.Player = await channel.connect(
|
||||
cls=voicelink.Player(self.bot, channel, func.TempCtx(dj_member, channel), settings)
|
||||
cls=voicelink.Player(self.bot, channel, TempCtx(dj_member, channel), settings)
|
||||
)
|
||||
|
||||
# Restore the queue.
|
||||
@@ -89,7 +88,7 @@ class Listeners(commands.Cog):
|
||||
if not track_id:
|
||||
continue
|
||||
|
||||
decoded_track = voicelink.decode(track_id)
|
||||
decoded_track = voicelink.Track.decode(track_id)
|
||||
requester = channel.guild.get_member(track_data.get("requester_id"))
|
||||
track = voicelink.Track(track_id=track_id, info=decoded_track, requester=requester)
|
||||
player.queue._queue.append(track)
|
||||
@@ -125,12 +124,11 @@ class Listeners(commands.Cog):
|
||||
|
||||
# Delete the last session file if it exists.
|
||||
try:
|
||||
file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if os.path.exists(Config.LAST_SESSION_FILE_DIR):
|
||||
os.remove(Config.LAST_SESSION_FILE_DIR)
|
||||
|
||||
except Exception as del_error:
|
||||
func.logger.error("Failed to remove session file: %s", file_path, exc_info=del_error)
|
||||
func.logger.error("Failed to remove session file: %s", Config.LAST_SESSION_FILE_DIR, exc_info=del_error)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voicelink_track_end(self, player: voicelink.Player, track, _):
|
||||
@@ -175,11 +173,22 @@ class Listeners(commands.Cog):
|
||||
is_joined = False
|
||||
|
||||
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:
|
||||
if player.is_paused and len([m for m in player.channel.members if not m.bot or not m.voice.self_deaf]) == 1:
|
||||
await player.set_pause(False, member)
|
||||
|
||||
if self.bot.ipc._is_connected:
|
||||
await self.bot.ipc.send({
|
||||
|
||||
if not is_joined:
|
||||
if not player.is_paused and len([m for m in player.channel.members if not m.bot or not m.voice.self_deaf]) == 0:
|
||||
player._schedule_inactive_cleanup_timer()
|
||||
|
||||
# if dj is not in the channel, find a new DJ
|
||||
if player.dj not in player.channel.members:
|
||||
for m in player.channel.members:
|
||||
if not m.bot or not m.voice.self_deaf:
|
||||
player.dj = m
|
||||
break
|
||||
|
||||
if player.is_ipc_connected:
|
||||
await player._ipc_client.send({
|
||||
"op": "updateGuild",
|
||||
"user": {
|
||||
"userId": str(member.id),
|
||||
|
||||
570
cogs/playlist.py
570
cogs/playlist.py
@@ -21,71 +21,252 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord, voicelink, time
|
||||
import time
|
||||
import discord
|
||||
import voicelink
|
||||
|
||||
from io import StringIO
|
||||
from typing import Optional, Tuple
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from function import (
|
||||
send,
|
||||
time as ctime,
|
||||
get_user,
|
||||
update_user,
|
||||
check_roles,
|
||||
get_lang,
|
||||
settings,
|
||||
get_aliases,
|
||||
cooldown_check,
|
||||
logger
|
||||
)
|
||||
|
||||
from views import PlaylistView, InboxView, HelpView
|
||||
from voicelink import MongoDBHandler, Config
|
||||
from voicelink.views import PlaylistViewManager, InboxView, HelpView
|
||||
from voicelink.utils import format_ms, dispatch_message, send_localized_message
|
||||
|
||||
def assign_playlist_id(existed: list) -> str:
|
||||
for i in range(200, 210):
|
||||
if str(i) not in existed:
|
||||
return str(i)
|
||||
|
||||
async def check_playlist_perms(user_id: int, author_id: int, d_id: str) -> dict:
|
||||
playlist = await get_user(author_id, 'playlist')
|
||||
playlist = playlist.get(d_id)
|
||||
if not playlist or user_id not in playlist['perms']['read']:
|
||||
return {}
|
||||
return playlist
|
||||
def resolve_owner_display(ctx: commands.Context[commands.Bot], owner_id: int):
|
||||
if owner_id == ctx.author.id:
|
||||
return ctx.author
|
||||
if ctx.guild:
|
||||
member = ctx.guild.get_member(owner_id)
|
||||
if member:
|
||||
return member
|
||||
user = ctx.bot.get_user(owner_id)
|
||||
return user if user else f"<@{owner_id}>"
|
||||
|
||||
async def check_playlist(ctx: commands.Context, name: str = None, full: bool = False, share: bool = True) -> dict:
|
||||
user = await get_user(ctx.author.id, 'playlist')
|
||||
async def check_playlist_perms(
|
||||
user_id: int,
|
||||
author_id: int,
|
||||
playlist_id: str,
|
||||
*,
|
||||
required_perm: Optional[str] = "read"
|
||||
) -> Tuple[Optional[dict], Optional[str]]:
|
||||
"""Check if user has the requested permissions for a specific playlist."""
|
||||
user_data = await MongoDBHandler.get_user(author_id, d_type='playlist')
|
||||
playlist = user_data.get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
return None, "not_found"
|
||||
|
||||
perms = playlist.get('perms', {})
|
||||
if user_id not in perms.get('read', []):
|
||||
return None, "no_read"
|
||||
|
||||
if required_perm and required_perm != "read":
|
||||
if user_id not in perms.get(required_perm, []):
|
||||
return None, "no_permission"
|
||||
|
||||
return playlist, None
|
||||
|
||||
await ctx.defer()
|
||||
async def check_playlist(
|
||||
ctx: commands.Context,
|
||||
name: str = None,
|
||||
full: bool = False,
|
||||
share: bool = True,
|
||||
share_perm: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Get user's playlist data with various filtering options."""
|
||||
if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done():
|
||||
await ctx.defer()
|
||||
|
||||
user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist')
|
||||
|
||||
if full:
|
||||
return user
|
||||
return user_playlists
|
||||
|
||||
if not name:
|
||||
return {'playlist': user['200'], 'position': 1, 'id': "200"}
|
||||
|
||||
for index, data in enumerate(user, start=1):
|
||||
playlist = user[data]
|
||||
if playlist['name'].lower() == name:
|
||||
return {
|
||||
'playlist': user_playlists['200'],
|
||||
'position': 1,
|
||||
'id': "200",
|
||||
'is_shared': False,
|
||||
'owner_id': ctx.author.id,
|
||||
'owner_playlist_id': "200",
|
||||
'error': None
|
||||
}
|
||||
|
||||
for index, playlist_id in enumerate(user_playlists, start=1):
|
||||
playlist = user_playlists[playlist_id]
|
||||
|
||||
if playlist['name'].lower() == name.lower():
|
||||
if playlist['type'] == 'share' and share:
|
||||
playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId'])
|
||||
if not playlist or ctx.author.id not in playlist['perms']['read']:
|
||||
return {'playlist': None, 'position': index, 'id': data}
|
||||
return {'playlist': playlist, 'position': index, 'id': data}
|
||||
return {'playlist': None, 'position': None, 'id': None}
|
||||
shared_playlist, error = await check_playlist_perms(
|
||||
ctx.author.id,
|
||||
playlist['user'],
|
||||
playlist['referId'],
|
||||
required_perm=share_perm or "read"
|
||||
)
|
||||
|
||||
if not shared_playlist:
|
||||
if error == "not_found":
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}})
|
||||
return {
|
||||
'playlist': None,
|
||||
'position': index,
|
||||
'id': playlist_id,
|
||||
'is_shared': True,
|
||||
'owner_id': playlist['user'],
|
||||
'owner_playlist_id': playlist['referId'],
|
||||
'error': 'permission' if error in {'no_read', 'no_permission'} else None
|
||||
}
|
||||
|
||||
return {
|
||||
'playlist': shared_playlist,
|
||||
'position': index,
|
||||
'id': playlist_id,
|
||||
'is_shared': True,
|
||||
'owner_id': playlist['user'],
|
||||
'owner_playlist_id': playlist['referId'],
|
||||
'error': None
|
||||
}
|
||||
|
||||
return {
|
||||
'playlist': playlist,
|
||||
'position': index,
|
||||
'id': playlist_id,
|
||||
'is_shared': False,
|
||||
'owner_id': ctx.author.id,
|
||||
'owner_playlist_id': playlist_id,
|
||||
'error': None
|
||||
}
|
||||
|
||||
return {
|
||||
'playlist': None,
|
||||
'position': None,
|
||||
'id': None,
|
||||
'is_shared': False,
|
||||
'owner_id': ctx.author.id,
|
||||
'owner_playlist_id': None,
|
||||
'error': None
|
||||
}
|
||||
|
||||
async def search_playlist(url: str, requester: discord.Member, time_needed: bool = True) -> dict:
|
||||
"""Search for playlist tracks from a URL."""
|
||||
try:
|
||||
tracks = await voicelink.NodePool.get_node().get_tracks(url, requester=requester)
|
||||
tracks = {"name": tracks.name, "tracks": tracks.tracks}
|
||||
result = {"name": tracks.name, "tracks": tracks.tracks}
|
||||
|
||||
if time_needed:
|
||||
time = sum([track.length for track in tracks["tracks"]])
|
||||
except:
|
||||
result["time"] = format_ms(sum(track.length for track in tracks.tracks))
|
||||
|
||||
return result
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
if time_needed:
|
||||
tracks["time"] = ctime(time)
|
||||
|
||||
return tracks
|
||||
async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist_id: str, is_locked: bool):
|
||||
"""Process a single playlist and return its formatted data."""
|
||||
playlist_type = playlist_data['type']
|
||||
|
||||
# Get appropriate emoji
|
||||
if is_locked:
|
||||
emoji = '🔒'
|
||||
elif playlist_type == 'link':
|
||||
emoji = '🌐'
|
||||
elif playlist_type == 'share':
|
||||
emoji = '🤝'
|
||||
else:
|
||||
emoji = '❤️'
|
||||
|
||||
# Handle link playlist
|
||||
if playlist_type == 'link':
|
||||
tracks = await search_playlist(playlist_data['uri'], requester=ctx.author)
|
||||
if not tracks:
|
||||
return None
|
||||
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'id': playlist_id,
|
||||
'time': tracks['time'],
|
||||
'name': playlist_data['name'],
|
||||
'tracks': tracks['tracks'],
|
||||
'perms': playlist_data['perms'],
|
||||
'type': playlist_data['type']
|
||||
}
|
||||
|
||||
# Handle shared playlist
|
||||
if playlist_type == 'share':
|
||||
shared_playlist, error = await check_playlist_perms(
|
||||
ctx.author.id,
|
||||
playlist_data['user'],
|
||||
playlist_data['referId']
|
||||
)
|
||||
|
||||
if not shared_playlist:
|
||||
if error == "not_found":
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}})
|
||||
return None
|
||||
|
||||
if shared_playlist['type'] == 'link':
|
||||
tracks = await search_playlist(shared_playlist['uri'], requester=ctx.author)
|
||||
if not tracks:
|
||||
return None
|
||||
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'id': playlist_id,
|
||||
'time': tracks['time'],
|
||||
'name': playlist_data['name'],
|
||||
'tracks': tracks['tracks'],
|
||||
'perms': shared_playlist['perms'],
|
||||
'owner': playlist_data['user'],
|
||||
'type': 'share'
|
||||
}
|
||||
|
||||
decoded_tracks = []
|
||||
total_time = 0
|
||||
for track in shared_playlist['tracks']:
|
||||
decoded_track = voicelink.Track.decode(track)
|
||||
total_time += decoded_track.get("length", 0)
|
||||
decoded_tracks.append(decoded_track)
|
||||
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'id': playlist_id,
|
||||
'time': format_ms(total_time),
|
||||
'name': playlist_data['name'],
|
||||
'tracks': decoded_tracks,
|
||||
'perms': shared_playlist['perms'],
|
||||
'owner': playlist_data['user'],
|
||||
'type': 'share'
|
||||
}
|
||||
|
||||
decoded_tracks = []
|
||||
total_time = 0
|
||||
for track in playlist_data['tracks']:
|
||||
decoded_track = voicelink.Track.decode(track)
|
||||
total_time += decoded_track.get("length", 0)
|
||||
decoded_tracks.append(decoded_track)
|
||||
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'id': playlist_id,
|
||||
'time': format_ms(total_time),
|
||||
'name': playlist_data['name'],
|
||||
'tracks': decoded_tracks,
|
||||
'perms': playlist_data['perms'],
|
||||
'owner': playlist_data.get('owner', ctx.author.id),
|
||||
'type': playlist_data['type']
|
||||
}
|
||||
|
||||
class Playlists(commands.Cog, name="playlist"):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
@@ -93,7 +274,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
self.description = "This is the Vocard playlist system. You can save your favorites and use Vocard to play on any server."
|
||||
|
||||
async def playlist_autocomplete(self, interaction: discord.Interaction, current: str) -> list:
|
||||
playlists_raw: dict[str, dict] = await get_user(interaction.user.id, 'playlist')
|
||||
playlists_raw: dict[str, dict] = await MongoDBHandler.get_user(interaction.user.id, d_type='playlist')
|
||||
playlists = [value['name'] for value in playlists_raw.values()] if playlists_raw else []
|
||||
if current:
|
||||
return [app_commands.Choice(name=p, value=p) for p in playlists if current in p]
|
||||
@@ -107,7 +288,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
async def playlist(self, ctx: commands.Context):
|
||||
view = HelpView(self.bot, ctx.author)
|
||||
embed = view.build_embed(self.qualified_name)
|
||||
view.response = send(ctx, embed, view=view)
|
||||
view.response = dispatch_message(ctx, embed, view=view)
|
||||
|
||||
@playlist.command(name="play", aliases=get_aliases("play"))
|
||||
@app_commands.describe(
|
||||
@@ -121,10 +302,10 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
result = await check_playlist(ctx, name.lower() if name else None)
|
||||
|
||||
if not result['playlist']:
|
||||
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
|
||||
rank, max_p, max_t = check_roles()
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
max_p, max_t, _ = Config().get_playlist_config()
|
||||
if result['position'] > max_p:
|
||||
return await send(ctx, 'playlistNotAccess', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True)
|
||||
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if not player:
|
||||
@@ -134,21 +315,21 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
tracks = await search_playlist(result['playlist']['uri'], ctx.author, time_needed=False)
|
||||
else:
|
||||
if not result['playlist']['tracks']:
|
||||
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.noTrack', result['playlist']['name'], ephemeral=True)
|
||||
|
||||
_tracks = []
|
||||
for track in result['playlist']['tracks'][:max_t]:
|
||||
_tracks.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author))
|
||||
_tracks.append(voicelink.Track(track_id=track, info=voicelink.Track.decode(track), requester=ctx.author))
|
||||
|
||||
tracks = {"name": result['playlist']['name'], "tracks": _tracks}
|
||||
|
||||
if not tracks:
|
||||
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.noTrack', result['playlist']['name'], ephemeral=True)
|
||||
|
||||
if value and 0 < value <= (len(tracks['tracks'])):
|
||||
tracks['tracks'] = [tracks['tracks'][value - 1]]
|
||||
await player.add_track(tracks['tracks'])
|
||||
await send(ctx, 'playlistPlay', result['playlist']['name'], len(tracks['tracks'][:max_t]))
|
||||
await send_localized_message(ctx, 'playlist.actions.play', result['playlist']['name'], len(tracks['tracks'][:max_t]))
|
||||
|
||||
if not player.is_playing:
|
||||
await player.do_next()
|
||||
@@ -156,63 +337,32 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
@playlist.command(name="view", aliases=get_aliases("view"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def view(self, ctx: commands.Context) -> None:
|
||||
"List all your playlist and all songs in your favourite playlist."
|
||||
user = await check_playlist(ctx, full=True)
|
||||
rank, max_p, max_t = check_roles()
|
||||
|
||||
results = []
|
||||
for index, data in enumerate(user, start=1):
|
||||
playlist = user[data]
|
||||
time = 0
|
||||
try:
|
||||
if playlist['type'] == 'link':
|
||||
tracks = await search_playlist(playlist['uri'], requester=ctx.author)
|
||||
results.append({'emoji': ('🔒' if max_p < index else '🔗'), 'id': data, 'time': tracks['time'], 'name': playlist['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'type': playlist['type']})
|
||||
|
||||
else:
|
||||
if share := playlist['type'] == 'share':
|
||||
playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId'])
|
||||
if not playlist:
|
||||
await update_user(ctx.author.id, {"$unset": {f"playlist.{data}": 1}})
|
||||
continue
|
||||
|
||||
if playlist['type'] == 'link':
|
||||
tracks = await search_playlist(playlist['uri'], requester=ctx.author)
|
||||
results.append({'emoji': ('🔒' if max_p < index else '🤝'), 'id': data, 'time': tracks['time'], 'name': user[data]['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'owner': user[data]['user'], 'type': 'share'})
|
||||
continue
|
||||
|
||||
init = []
|
||||
for track in playlist['tracks']:
|
||||
dt = voicelink.decode(track)
|
||||
time += dt.get("length", 0)
|
||||
init.append(dt)
|
||||
playlist['tracks'] = init
|
||||
results.append({'emoji': ('🔒' if max_p < index else ('🤝' if share else '❤️')), 'id': data, 'time': ctime(time), 'name': user[data]['name'], 'tracks': playlist['tracks'], 'perms': playlist['perms'], 'owner': user[data].get('user', None), 'type': user[data]['type']})
|
||||
"""List all your playlists and all songs in your favourite playlist."""
|
||||
user_playlists = await check_playlist(ctx, full=True)
|
||||
max_p, _, _ = Config().get_playlist_config()
|
||||
|
||||
except:
|
||||
results.append({'emoji': '⛔', 'id': data, 'time': '--:--', 'name': 'Error', 'tracks': [], 'type': 'error'})
|
||||
|
||||
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 %12s %10s\n' % tuple(text[1].split(",")),
|
||||
color=settings.embed_color
|
||||
)
|
||||
playlist_results = []
|
||||
|
||||
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'])}")
|
||||
except IndexError:
|
||||
track_info = ("🎵", "-"*3, "[--:--]", "-"*6, f"-")
|
||||
|
||||
embed.description += '%0s %3s. %10s %12s %10s\n' % track_info
|
||||
for index, playlist_id in enumerate(user_playlists, start=1):
|
||||
playlist_data = user_playlists[playlist_id]
|
||||
is_locked = max_p < index
|
||||
|
||||
embed.description += "```"
|
||||
embed.set_footer(text=text[2])
|
||||
|
||||
view = PlaylistView(embed, results, ctx.author)
|
||||
view.response = await send(ctx, embed, view=view, ephemeral=True)
|
||||
try:
|
||||
result = await _process_playlist(ctx, playlist_data, playlist_id, is_locked)
|
||||
if result:
|
||||
playlist_results.append(result)
|
||||
except Exception:
|
||||
playlist_results.append({
|
||||
'emoji': '⛔',
|
||||
'id': playlist_id,
|
||||
'time': '--:--',
|
||||
'name': 'Error',
|
||||
'tracks': [],
|
||||
'type': 'error'
|
||||
})
|
||||
|
||||
view = PlaylistViewManager(ctx, playlist_results)
|
||||
view.response = await dispatch_message(ctx, content=view.build_embed(), view=view, ephemeral=True)
|
||||
|
||||
@playlist.command(name="create", aliases=get_aliases("create"))
|
||||
@app_commands.describe(
|
||||
@@ -223,25 +373,25 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
async def create(self, ctx: commands.Context, name: str, link: str = None):
|
||||
"Create your custom playlist."
|
||||
if len(name) > 10:
|
||||
return await send(ctx, 'playlistOverText', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.nameOverLimit', ephemeral=True)
|
||||
|
||||
rank, max_p, max_t = check_roles()
|
||||
max_p, _, _ = Config().get_playlist_config()
|
||||
user = await check_playlist(ctx, full=True)
|
||||
|
||||
if len(user) >= max_p:
|
||||
return await send(ctx, 'overPlaylistCreation', max_p, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.limitReached', max_p, ephemeral=True)
|
||||
|
||||
for data in user:
|
||||
if user[data]['name'].lower() == name.lower():
|
||||
return await send(ctx, 'playlistExists', name, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.exists', name, ephemeral=True)
|
||||
if link:
|
||||
tracks = await voicelink.NodePool.get_node().get_tracks(link, requester=ctx.author)
|
||||
if not isinstance(tracks, voicelink.Playlist):
|
||||
return await send(ctx, "playlistNotInvalidUrl", ephemeral=True)
|
||||
return await send_localized_message(ctx, "playlist.errors.invalidUrl", 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_playlist_id([data for data in user])}": data}})
|
||||
await send(ctx, "playlistCreated", name)
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
|
||||
await send_localized_message(ctx, "playlist.actions.created", name)
|
||||
|
||||
@playlist.command(name="delete", aliases=get_aliases("delete"))
|
||||
@app_commands.describe(name="The name of the playlist.")
|
||||
@@ -251,15 +401,15 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
"Delete your custom playlist."
|
||||
result = await check_playlist(ctx, name.lower(), share=False)
|
||||
if not result['playlist']:
|
||||
return await ctx(ctx, "playlistNotFound", name, ephemeral=True)
|
||||
return await send_localized_message(ctx, "playlist.errors.notFound", name, ephemeral=True)
|
||||
if result['id'] == "200":
|
||||
return await send(ctx, "playlistDeleteError", ephemeral=True)
|
||||
return await send_localized_message(ctx, "playlist.errors.deleteDefault", ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] == 'share':
|
||||
await update_user(result['playlist']['user'], {"$pull": {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}})
|
||||
await MongoDBHandler.update_user(result['playlist']['user'], {"$pull": {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}})
|
||||
|
||||
await update_user(ctx.author.id, {"$unset": {f"playlist.{result['id']}": 1}})
|
||||
return await send(ctx, "playlistRemove", result["playlist"]["name"])
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{result['id']}": 1}})
|
||||
return await send_localized_message(ctx, "playlist.actions.removed", result["playlist"]["name"])
|
||||
|
||||
@playlist.command(name="share", aliases=get_aliases("share"))
|
||||
@app_commands.describe(
|
||||
@@ -271,28 +421,28 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
async def share(self, ctx: commands.Context, member: discord.Member, name: str):
|
||||
"Share your custom playlist with your friends."
|
||||
if member.id == ctx.author.id:
|
||||
return await send(ctx, 'playlistSendErrorPlayer', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.sharing.sendErrorPlayer', ephemeral=True)
|
||||
if member.bot:
|
||||
return await send(ctx, 'playlistSendErrorBot', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.sharing.sendErrorBot', ephemeral=True)
|
||||
result = await check_playlist(ctx, name.lower(), share=False)
|
||||
if not result['playlist']:
|
||||
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] == 'share':
|
||||
return await send(ctx, 'playlistBelongs', result['playlist']['user'], ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.sharing.belongs', result['playlist']['user'], ephemeral=True)
|
||||
if member.id in result['playlist']['perms']['read']:
|
||||
return await send(ctx, 'playlistShare', member, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.sharing.alreadyShared', member, ephemeral=True)
|
||||
|
||||
receiver = await get_user(member.id)
|
||||
receiver = await MongoDBHandler.get_user(member.id)
|
||||
if not receiver:
|
||||
return await send(ctx, 'noPlaylistAcc', member)
|
||||
return await send_localized_message(ctx, 'playlist.sharing.noAccount', member)
|
||||
for mail in receiver['inbox']:
|
||||
if mail['sender'] == ctx.author.id and mail['referId'] == result['id']:
|
||||
return await send(ctx, 'playlistSent', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.sharing.alreadySent', ephemeral=True)
|
||||
if len(receiver['inbox']) >= 10:
|
||||
return await send(ctx.guild.id, 'inboxFull', member, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.inbox.full', member, ephemeral=True)
|
||||
|
||||
await update_user(
|
||||
await MongoDBHandler.update_user(
|
||||
member.id,
|
||||
{"$push": {"inbox": {
|
||||
'sender': ctx.author.id,
|
||||
@@ -303,7 +453,71 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
'type': 'invite'
|
||||
}}}
|
||||
)
|
||||
return await send(ctx, "invitationSent", member)
|
||||
return await send_localized_message(ctx, "playlist.sharing.invitationSent", member)
|
||||
|
||||
@playlist.command(name="permission", aliases=get_aliases("permission"))
|
||||
@app_commands.describe(
|
||||
name="The name of the playlist.",
|
||||
member="The user to grant or revoke permissions for.",
|
||||
permission="The permission type: read, write, or remove.",
|
||||
action="Whether to grant or revoke the permission."
|
||||
)
|
||||
@app_commands.choices(permission=[
|
||||
app_commands.Choice(name="read", value="read"),
|
||||
app_commands.Choice(name="write", value="write"),
|
||||
app_commands.Choice(name="remove", value="remove")
|
||||
])
|
||||
@app_commands.choices(action=[
|
||||
app_commands.Choice(name="grant", value="grant"),
|
||||
app_commands.Choice(name="revoke", value="revoke")
|
||||
])
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def permission(self, ctx: commands.Context, member: discord.Member, name: str, permission: str, action: str):
|
||||
"Grant or revoke permissions for a playlist."
|
||||
if member.id == ctx.author.id:
|
||||
return await send_localized_message(ctx, 'playlist.permissions.cannotModifySelf', ephemeral=True)
|
||||
if member.bot:
|
||||
return await send_localized_message(ctx, 'playlist.sharing.sendErrorBot', ephemeral=True)
|
||||
|
||||
result = await check_playlist(ctx, name.lower(), share=False)
|
||||
if not result['playlist']:
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] in ['share', 'link']:
|
||||
return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True)
|
||||
|
||||
perm_type = result['playlist'].get('perms', {})
|
||||
if permission not in perm_type:
|
||||
return await send_localized_message(ctx, 'playlist.permissions.invalidPermission', ephemeral=True)
|
||||
|
||||
perm_list = perm_type.get(permission, [])
|
||||
if action == "grant":
|
||||
if member.id in perm_list:
|
||||
return await send_localized_message(ctx, 'playlist.permissions.alreadyGranted', member, permission, ephemeral=True)
|
||||
|
||||
# Ensure user has read access first
|
||||
if permission != 'read' and member.id not in perm_type.get('read', []):
|
||||
return await send_localized_message(ctx, f'You haven\'t shared your playlist to {member.mention}', member, ephemeral=True)
|
||||
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$push": {f"playlist.{result['id']}.perms.{permission}": member.id}})
|
||||
return await send_localized_message(ctx, 'playlist.permissions.granted', member, permission, result['playlist']['name'])
|
||||
|
||||
elif action == "revoke":
|
||||
if member.id not in perm_list:
|
||||
return await send_localized_message(ctx, 'playlist.permissions.notGranted', member, permission, ephemeral=True)
|
||||
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f"playlist.{result['id']}.perms.{permission}": member.id}})
|
||||
|
||||
# If revoking read, also revoke write and remove
|
||||
if permission == 'read':
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f"playlist.{result['id']}.perms.write": member.id}})
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f"playlist.{result['id']}.perms.remove": member.id}})
|
||||
|
||||
return await send_localized_message(ctx, 'playlist.permissions.revoked', member, permission, result['playlist']['name'])
|
||||
|
||||
else:
|
||||
return await send_localized_message(ctx, 'playlist.permissions.invalidAction', ephemeral=True)
|
||||
|
||||
@playlist.command(name="rename", aliases=get_aliases("rename"))
|
||||
@app_commands.describe(
|
||||
@@ -315,36 +529,36 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
async def rename(self, ctx: commands.Context, name: str, newname: str) -> None:
|
||||
"Rename your custom playlist."
|
||||
if len(newname) > 10:
|
||||
return await send(ctx, 'playlistOverText', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.nameOverLimit', ephemeral=True)
|
||||
if name.lower() == newname.lower():
|
||||
return await send(ctx, 'playlistSameName', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.sameName', ephemeral=True)
|
||||
user = await check_playlist(ctx, full=True)
|
||||
found, id = False, 0
|
||||
for data in user:
|
||||
if user[data]['name'].lower() == name.lower():
|
||||
found, id = True, data
|
||||
if user[data]['name'].lower() == newname.lower():
|
||||
return await send(ctx, 'playlistExists', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.exists', ephemeral=True)
|
||||
|
||||
if not found:
|
||||
return await send(ctx.guild.id, 'playlistNotFound', name, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
|
||||
await update_user(ctx.author.id, {"$set": {f'playlist.{id}.name': newname}})
|
||||
await send(ctx, 'playlistRenamed', name, newname)
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{id}.name': newname}})
|
||||
await send_localized_message(ctx, 'playlist.actions.renamed', name, newname)
|
||||
|
||||
@playlist.command(name="inbox", aliases=get_aliases("inbox"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def inbox(self, ctx: commands.Context) -> None:
|
||||
"Show your playlist invitation."
|
||||
user = await get_user(ctx.author.id)
|
||||
rank, max_p, max_t = check_roles()
|
||||
user = await MongoDBHandler.get_user(ctx.author.id)
|
||||
max_p, _, _ = Config().get_playlist_config()
|
||||
|
||||
if not user['inbox']:
|
||||
return await send(ctx, 'inboxNoMsg', ephemeral=True)
|
||||
return await send_localized_message(ctx, "playlist.inbox.noMessages", ephemeral=True)
|
||||
|
||||
inbox = user['inbox'].copy()
|
||||
view = InboxView(ctx.author, user['inbox'])
|
||||
view.response = await send(ctx, view.build_embed(), view=view, ephemeral=True)
|
||||
view.response = await dispatch_message(ctx, view.build_embed(), view=view, ephemeral=True)
|
||||
await view.wait()
|
||||
|
||||
if inbox == user['inbox']:
|
||||
@@ -353,7 +567,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
update_data, dId = {}, {dId for dId in user["playlist"]}
|
||||
for data in view.new_playlist[:(max_p - len(user['playlist']))]:
|
||||
addId = assign_playlist_id(dId)
|
||||
await update_user(data['sender'], {"$push": {f"playlist.{data['referId']}.perms.read": ctx.author.id}})
|
||||
await MongoDBHandler.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{time.strftime('%M%S', time.gmtime(int(data['time'])))}",
|
||||
@@ -363,7 +577,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
dId.add(addId)
|
||||
|
||||
if update_data:
|
||||
await update_user(ctx.author.id, {"$set": update_data})
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$set": update_data})
|
||||
|
||||
@playlist.command(name="add", aliases=get_aliases("add"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -374,28 +588,33 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def add(self, ctx: commands.Context, name: str, query: str) -> None:
|
||||
"Add tracks in to your custom playlist."
|
||||
result = await check_playlist(ctx, name.lower(), share=False)
|
||||
result = await check_playlist(ctx, name.lower(), share=True, share_perm='write')
|
||||
if not result['playlist']:
|
||||
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
|
||||
if result['playlist']['type'] in ['share', 'link']:
|
||||
return await send(ctx, 'playlistNotAllow', ephemeral=True)
|
||||
if result.get('error') == 'permission':
|
||||
return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
if result['playlist']['type'] == 'link':
|
||||
return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True)
|
||||
|
||||
rank, max_p, max_t = check_roles()
|
||||
_, max_t, _ = Config().get_playlist_config()
|
||||
if len(result['playlist']['tracks']) >= max_t:
|
||||
return await send(ctx, 'playlistLimitTrack', max_t, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.trackLimitReached', max_t, ephemeral=True)
|
||||
|
||||
results = await voicelink.NodePool.get_node().get_tracks(query, requester=ctx.author)
|
||||
if not results:
|
||||
return await send(ctx, 'noTrackFound')
|
||||
return await send_localized_message(ctx, 'player.errors.noTrackFound')
|
||||
|
||||
if isinstance(results, voicelink.Playlist):
|
||||
return await send(ctx, 'playlistPlaylistLink', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.playlistLinkNotAllowed', ephemeral=True)
|
||||
|
||||
if results[0].is_stream:
|
||||
return await send(ctx, 'playlistStream', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.streamNotAllowed', ephemeral=True)
|
||||
|
||||
await update_user(ctx.author.id, {"$push": {f'playlist.{result["id"]}.tracks': results[0].track_id}})
|
||||
await send(ctx, 'playlistAdded', results[0].title, ctx.author, result['playlist']['name'])
|
||||
owner_id = result.get('owner_id', ctx.author.id)
|
||||
owner_playlist_id = result.get('owner_playlist_id', result['id'])
|
||||
await MongoDBHandler.update_user(owner_id, {"$push": {f'playlist.{owner_playlist_id}.tracks': results[0].track_id}})
|
||||
owner_display = resolve_owner_display(ctx, owner_id)
|
||||
await send_localized_message(ctx, 'playlist.actions.trackAdded', results[0].title, owner_display, result['playlist']['name'])
|
||||
|
||||
@playlist.command(name="remove", aliases=get_aliases("remove"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -406,33 +625,42 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def remove(self, ctx: commands.Context, name: str, position: int):
|
||||
"Remove song from your favorite playlist."
|
||||
result = await check_playlist(ctx, name.lower(), share=False)
|
||||
result = await check_playlist(ctx, name.lower(), share=True, share_perm='remove')
|
||||
if not result['playlist']:
|
||||
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
|
||||
if result['playlist']['type'] in ['link', 'share']:
|
||||
return await send(ctx, 'playlistNotAllow', ephemeral=True)
|
||||
if result.get('error') == 'permission':
|
||||
return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
if result['playlist']['type'] == 'link':
|
||||
return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True)
|
||||
if not 0 < position <= len(result['playlist']['tracks']):
|
||||
return await send(ctx, 'playlistPositionNotFound', position, name)
|
||||
return await send_localized_message(ctx, 'playlist.errors.positionNotFound', position, name)
|
||||
|
||||
await update_user(ctx.author.id, {"$pull": {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}})
|
||||
owner_id = result.get('owner_id', ctx.author.id)
|
||||
owner_playlist_id = result.get('owner_playlist_id', result['id'])
|
||||
await MongoDBHandler.update_user(owner_id, {"$pull": {f'playlist.{owner_playlist_id}.tracks': result['playlist']['tracks'][position - 1]}})
|
||||
|
||||
track = voicelink.decode(result['playlist']['tracks'][position - 1])
|
||||
await send(ctx, 'playlistRemoved', track.get("title"), ctx.author, name)
|
||||
track = voicelink.Track.decode(result['playlist']['tracks'][position - 1])
|
||||
owner_display = resolve_owner_display(ctx, owner_id)
|
||||
await send_localized_message(ctx, 'playlist.actions.trackRemoved', track.get("title"), owner_display, name)
|
||||
|
||||
@playlist.command(name="clear", aliases=get_aliases("clear"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@app_commands.autocomplete(name=playlist_autocomplete)
|
||||
async def clear(self, ctx: commands.Context, name: str) -> None:
|
||||
"Remove all songs from your favorite playlist."
|
||||
result = await check_playlist(ctx, name.lower(), share=False)
|
||||
result = await check_playlist(ctx, name.lower(), share=True, share_perm='remove')
|
||||
if not result['playlist']:
|
||||
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
|
||||
if result.get('error') == 'permission':
|
||||
return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] in ['link', 'share']:
|
||||
return await send(ctx, 'playlistNotAllow', ephemeral=True)
|
||||
if result['playlist']['type'] == 'link':
|
||||
return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True)
|
||||
|
||||
await update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}})
|
||||
await send(ctx, 'playlistClear', name)
|
||||
owner_id = result.get('owner_id', ctx.author.id)
|
||||
owner_playlist_id = result.get('owner_playlist_id', result['id'])
|
||||
await MongoDBHandler.update_user(owner_id, {"$set": {f'playlist.{owner_playlist_id}.tracks': []}})
|
||||
await send_localized_message(ctx, 'playlist.actions.cleared', name)
|
||||
|
||||
@playlist.command(name="export", aliases=get_aliases("export"))
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
@@ -441,29 +669,29 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
"Exports the entire playlist to a text file"
|
||||
result = await check_playlist(ctx, name.lower())
|
||||
if not result['playlist']:
|
||||
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True)
|
||||
|
||||
if result['playlist']['type'] == 'link':
|
||||
tracks = await search_playlist(result['playlist']['uri'], ctx.author, time_needed=False)
|
||||
else:
|
||||
if not result['playlist']['tracks']:
|
||||
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.noTrack', result['playlist']['name'], ephemeral=True)
|
||||
|
||||
_tracks = []
|
||||
for track in result['playlist']['tracks']:
|
||||
_tracks.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author))
|
||||
_tracks.append(voicelink.Track(track_id=track, info=voicelink.Track.decode(track), requester=ctx.author))
|
||||
|
||||
tracks = {"name": result['playlist']['name'], "tracks": _tracks}
|
||||
|
||||
if not tracks:
|
||||
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.noTrack', result['playlist']['name'], ephemeral=True)
|
||||
|
||||
temp = ""
|
||||
raw = "----------->Raw Info<-----------\n"
|
||||
|
||||
total_length = 0
|
||||
for index, track in enumerate(tracks['tracks'], start=1):
|
||||
temp += f"{index}. {track.title} [{ctime(track.length)}]\n"
|
||||
temp += f"{index}. {track.title} [{format_ms(track.length)}]\n"
|
||||
raw += track.track_id
|
||||
if index != len(tracks['tracks']):
|
||||
raw += ","
|
||||
@@ -472,7 +700,7 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
temp = "!Remember do not change this file!\n------------->Info<-------------\nPlaylist: {} ({})\nRequester: {} ({})\nTracks: {} - {}\n------------>Tracks<------------\n".format(
|
||||
tracks['name'], result['playlist']['type'],
|
||||
ctx.author.display_name, ctx.author.id,
|
||||
len(tracks['tracks']), ctime(total_length)
|
||||
len(tracks['tracks']), format_ms(total_length)
|
||||
) + temp
|
||||
temp += raw
|
||||
|
||||
@@ -484,17 +712,17 @@ class Playlists(commands.Cog, name="playlist"):
|
||||
async def _import(self, ctx: commands.Context, name: str, attachment: discord.Attachment):
|
||||
"Create your custom playlist."
|
||||
if len(name) > 10:
|
||||
return await send(ctx, 'playlistOverText', ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.nameOverLimit', ephemeral=True)
|
||||
|
||||
rank, max_p, max_t = check_roles()
|
||||
max_p, _, _ = Config().get_playlist_config()
|
||||
user = await check_playlist(ctx, full=True)
|
||||
|
||||
if len(user) >= max_p:
|
||||
return await send(ctx, 'overPlaylistCreation', max_p, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.limitReached', max_p, ephemeral=True)
|
||||
|
||||
for data in user:
|
||||
if user[data]['name'].lower() == name.lower():
|
||||
return await send(ctx, 'playlistExists', name, ephemeral=True)
|
||||
return await send_localized_message(ctx, 'playlist.errors.exists', name, ephemeral=True)
|
||||
|
||||
try:
|
||||
bytes = await attachment.read()
|
||||
@@ -502,8 +730,8 @@ 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_playlist_id([data for data in user])}": data}})
|
||||
await send(ctx, 'playlistCreated', name)
|
||||
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
|
||||
await send_localized_message(ctx, 'playlist.actions.create', name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Decode Error", exc_info=e)
|
||||
|
||||
176
cogs/settings.py
176
cogs/settings.py
@@ -29,19 +29,32 @@ import function as func
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from function import (
|
||||
LANGS,
|
||||
send,
|
||||
update_settings,
|
||||
get_settings,
|
||||
get_lang,
|
||||
time as ctime,
|
||||
get_aliases,
|
||||
cooldown_check,
|
||||
format_bytes
|
||||
cooldown_check
|
||||
)
|
||||
|
||||
from views import DebugView, HelpView, EmbedBuilderView
|
||||
from voicelink import MongoDBHandler, LangHandler
|
||||
from voicelink.views import DebugView, HelpView, EmbedBuilderView
|
||||
from voicelink.placeholders import PlayerPlaceholder
|
||||
from voicelink.utils import format_ms, format_bytes, dispatch_message, send_localized_message
|
||||
|
||||
_RESET_LABELS: dict[str, str] = {
|
||||
"prefix": "prefix",
|
||||
"language": "lang",
|
||||
"dj role": "dj",
|
||||
"queue mode": "queue_type",
|
||||
"24/7 mode": "24/7",
|
||||
"vote bypass": "disabled_vote",
|
||||
"default volume": "volume",
|
||||
"music controller": "controller",
|
||||
"duplicate tracks": "duplicate_track",
|
||||
"controller messages": "controller_msg",
|
||||
"silent messages": "silent_msg",
|
||||
"stage announce template": "stage_announce_template",
|
||||
"song request channel": "music_request_channel",
|
||||
"custom controller embeds": "default_controller"
|
||||
}
|
||||
|
||||
def status_icon(status: bool) -> str:
|
||||
return "✅" if status else "❌"
|
||||
|
||||
@@ -58,7 +71,7 @@ class Settings(commands.Cog, name="settings"):
|
||||
async def settings(self, ctx: commands.Context):
|
||||
view = HelpView(self.bot, ctx.author)
|
||||
embed = view.build_embed(self.qualified_name)
|
||||
view.response = await send(ctx, embed, view=view)
|
||||
view.response = await dispatch_message(ctx, embed, view=view)
|
||||
|
||||
@settings.command(name="prefix", aliases=get_aliases("prefix"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@@ -66,10 +79,10 @@ class Settings(commands.Cog, name="settings"):
|
||||
async def prefix(self, ctx: commands.Context, prefix: str):
|
||||
"Change the default prefix for message commands."
|
||||
if not self.bot.intents.message_content:
|
||||
return await send(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
|
||||
return await send_localized_message(ctx, "common.error.missingIntents", "MESSAGE_CONTENT", ephemeral=True)
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {"prefix": prefix}})
|
||||
await send(ctx, "setPrefix", prefix, prefix)
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {"prefix": prefix}})
|
||||
await send_localized_message(ctx, "settings.actions.prefixSet", prefix, prefix)
|
||||
|
||||
@settings.command(name="language", aliases=get_aliases("language"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@@ -77,25 +90,25 @@ class Settings(commands.Cog, name="settings"):
|
||||
async def language(self, ctx: commands.Context, language: str):
|
||||
"You can choose your preferred language, the bot message will change to the language you set."
|
||||
language = language.upper()
|
||||
if language not in LANGS:
|
||||
return await send(ctx, "languageNotFound")
|
||||
if language not in voicelink.LangHandler.get_all_languages():
|
||||
return await send_localized_message(ctx, "settings.actions.languageNotFound")
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {'lang': language}})
|
||||
await send(ctx, 'changedLanguage', language)
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'lang': language}})
|
||||
await send_localized_message(ctx, 'settings.actions.languageChanged', language)
|
||||
|
||||
@language.autocomplete('language')
|
||||
async def autocomplete_callback(self, interaction: discord.Interaction, current: str) -> list:
|
||||
if current:
|
||||
return [app_commands.Choice(name=lang, value=lang) for lang in LANGS.keys() if current.upper() in lang]
|
||||
return [app_commands.Choice(name=lang, value=lang) for lang in LANGS.keys()]
|
||||
return [app_commands.Choice(name=lang, value=lang) for lang in voicelink.LangHandler.get_all_languages() if current.upper() in lang]
|
||||
return [app_commands.Choice(name=lang, value=lang) for lang in voicelink.LangHandler.get_all_languages()]
|
||||
|
||||
@settings.command(name="dj", aliases=get_aliases("dj"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def dj(self, ctx: commands.Context, role: discord.Role = None):
|
||||
"Set a DJ role or remove DJ role."
|
||||
await update_settings(ctx.guild.id, {"$set": {'dj': role.id}} if role else {"$unset": {'dj': None}})
|
||||
await send(ctx, 'setDJ', f"<@&{role.id}>" if role else "None")
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'dj': role.id}} if role else {"$unset": {'dj': None}})
|
||||
await send_localized_message(ctx, 'settings.actions.djSet', f"<@&{role.id}>" if role else "None")
|
||||
|
||||
@settings.command(name="queue", aliases=get_aliases("queue"))
|
||||
@app_commands.choices(mode=[
|
||||
@@ -107,57 +120,57 @@ class Settings(commands.Cog, name="settings"):
|
||||
async def queue(self, ctx: commands.Context, mode: str):
|
||||
"Change to another type of queue mode."
|
||||
mode = mode if mode.lower() in voicelink.queue.QUEUE_TYPES else next(iter(voicelink.queue.QUEUE_TYPES))
|
||||
await update_settings(ctx.guild.id, {"$set": {"queue_type": mode}})
|
||||
await send(ctx, "setQueue", mode.capitalize())
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {"queue_type": mode}})
|
||||
await send_localized_message(ctx, "settings.actions.queueModeSet", mode.capitalize())
|
||||
|
||||
@settings.command(name="247", aliases=get_aliases("247"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def playforever(self, ctx: commands.Context):
|
||||
"Toggles 24/7 mode, which disables automatic inactivity-based disconnects."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
toggle = settings.get('24/7', False)
|
||||
await update_settings(ctx.guild.id, {"$set": {'24/7': not toggle}})
|
||||
await send(ctx, '247', await get_lang(ctx.guild.id, "enabled" if not toggle else "disabled"))
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'24/7': not toggle}})
|
||||
await send_localized_message(ctx, 'settings.actions.mode247', await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if not toggle else "common.status.disabled"))
|
||||
|
||||
@settings.command(name="bypassvote", aliases=get_aliases("bypassvote"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def bypassvote(self, ctx: commands.Context):
|
||||
"Toggles voting system."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
toggle = settings.get('disabled_vote', True)
|
||||
await update_settings(ctx.guild.id, {"$set": {'disabled_vote': not toggle}})
|
||||
await send(ctx, 'bypassVote', await get_lang(ctx.guild.id, "enabled" if not toggle else "disabled"))
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'disabled_vote': not toggle}})
|
||||
await send_localized_message(ctx, 'settings.actions.bypassVote', await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if not toggle else "common.status.disabled"))
|
||||
|
||||
@settings.command(name="view", aliases=get_aliases("view"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def view(self, ctx: commands.Context):
|
||||
"Show all the bot settings in your server."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
|
||||
texts = await get_lang(ctx.guild.id, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsTitle3", "settingsPermTitle", "settingsPermValue")
|
||||
embed = discord.Embed(color=func.settings.embed_color)
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "settings.menu", "settings.basic.title", "settings.basic.value", "settings.queue.title", "settings.queue.value", "settings.voice.title", "settings.permissions.title", "settings.permissions.value")
|
||||
embed = discord.Embed(color=voicelink.Config().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('lang', 'EN'),
|
||||
settings.get('prefix', voicelink.Config().bot_prefix) or 'None',
|
||||
settings.get('lang', LangHandler._default_lang),
|
||||
settings.get('controller', True),
|
||||
dj_role.name if dj_role else 'None',
|
||||
settings.get('disabled_vote', False),
|
||||
settings.get('24/7', False),
|
||||
settings.get('volume', 100),
|
||||
ctime(settings.get('played_time', 0) * 60 * 1000),
|
||||
format_ms(settings.get('played_time', 0) * 60 * 1000),
|
||||
inline=True)
|
||||
)
|
||||
embed.add_field(name=texts[3], value=texts[4].format(
|
||||
settings.get("queue_type", "Queue"),
|
||||
func.settings.max_queue,
|
||||
voicelink.Config().max_queue,
|
||||
settings.get("duplicate_track", True)
|
||||
))
|
||||
|
||||
@@ -173,7 +186,7 @@ class Settings(commands.Cog, name="settings"):
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
await send(ctx, embed)
|
||||
await dispatch_message(ctx, embed)
|
||||
|
||||
@settings.command(name="volume", aliases=get_aliases("volume"))
|
||||
@app_commands.describe(value="Input a integer.")
|
||||
@@ -185,15 +198,15 @@ class Settings(commands.Cog, name="settings"):
|
||||
if player:
|
||||
await player.set_volume(value, ctx.author)
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {'volume': value}})
|
||||
await send(ctx, 'setVolume', value)
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'volume': value}})
|
||||
await send_localized_message(ctx, 'settings.actions.volumeSet', value)
|
||||
|
||||
@settings.command(name="togglecontroller", aliases=get_aliases("togglecontroller"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def togglecontroller(self, ctx: commands.Context):
|
||||
"Toggles the music controller."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
toggle = not settings.get('controller', True)
|
||||
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
@@ -203,63 +216,64 @@ class Settings(commands.Cog, name="settings"):
|
||||
except:
|
||||
discord.ui.View.from_message(player.controller).stop()
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {'controller': toggle}})
|
||||
await send(ctx, 'toggleController', await get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'controller': toggle}})
|
||||
await send_localized_message(ctx, 'settings.actions.controllerToggled', await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if toggle else "common.status.disabled"))
|
||||
|
||||
@settings.command(name="duplicatetrack", aliases=get_aliases("duplicatetrack"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def duplicatetrack(self, ctx: commands.Context):
|
||||
"Toggle Vocard to prevent duplicate songs from queuing."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
toggle = not settings.get('duplicate_track', False)
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if player:
|
||||
player.queue._allow_duplicate = toggle
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {'duplicate_track': toggle}})
|
||||
return await send(ctx, "toggleDuplicateTrack", await get_lang(ctx.guild.id, "disabled" if toggle else "enabled"))
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'duplicate_track': toggle}})
|
||||
return await send_localized_message(ctx, "settings.actions.duplicateTrackToggled", await LangHandler.get_lang(ctx.guild.id, "common.status.disabled" if toggle else "common.status.enabled"))
|
||||
|
||||
@settings.command(name="customcontroller", aliases=get_aliases("customcontroller"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def customcontroller(self, ctx: commands.Context):
|
||||
"Customizes music controller embeds."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
controller_settings = settings.get("default_controller", func.settings.controller)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
controller_settings = settings.get("default_controller", voicelink.Config().controller)
|
||||
|
||||
view = EmbedBuilderView(ctx, controller_settings.get("embeds").copy())
|
||||
view.response = await send(ctx, view.build_embed(), view=view)
|
||||
placeholder = PlayerPlaceholder(ctx.bot)
|
||||
view = EmbedBuilderView(ctx, placeholder, controller_settings.get("embeds").copy())
|
||||
view.response = await dispatch_message(ctx, view.build_embed(), view=view)
|
||||
|
||||
@settings.command(name="controllermsg", aliases=get_aliases("controllermsg"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def controllermsg(self, ctx: commands.Context):
|
||||
"Toggles to send a message when clicking the button in the music controller."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
toggle = not settings.get('controller_msg', True)
|
||||
|
||||
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"))
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'controller_msg': toggle}})
|
||||
await send_localized_message(ctx, 'settings.actions.controllerMsgToggled', await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if toggle else "common.status.disabled"))
|
||||
|
||||
@settings.command(name="silentmsg", aliases=get_aliases("silentmsg"))
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def silentmsg(self, ctx: commands.Context):
|
||||
"Toggle silent messaging to send discreet messages without alerting recipients."
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
toggle = not settings.get('silent_msg', False)
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {'silent_msg': toggle}})
|
||||
await send(ctx, 'toggleSilentMsg', await get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'silent_msg': toggle}})
|
||||
await send_localized_message(ctx, 'settings.actions.silentMsgToggled', await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if toggle else "common.status.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")
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}})
|
||||
await send_localized_message(ctx, "voice.stageChannel.setAnnounceTemplate")
|
||||
|
||||
@settings.command(name="setupchannel", aliases=get_aliases("setupchannel"))
|
||||
@app_commands.describe(
|
||||
@@ -270,7 +284,7 @@ class Settings(commands.Cog, name="settings"):
|
||||
async def setupchannel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None:
|
||||
"Sets up a dedicated channel for song requests in your server."
|
||||
if not self.bot.intents.message_content:
|
||||
return await send(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
|
||||
return await send_localized_message(ctx, "common.error.missingIntents", "MESSAGE_CONTENT", ephemeral=True)
|
||||
|
||||
if not channel:
|
||||
try:
|
||||
@@ -282,33 +296,55 @@ class Settings(commands.Cog, name="settings"):
|
||||
}
|
||||
channel = await ctx.guild.create_text_channel("vocard-song-requests", overwrites=overwrites)
|
||||
except:
|
||||
return await send(ctx, "noCreatePermission")
|
||||
return await send_localized_message(ctx, "permissions.noCreatePermission")
|
||||
|
||||
channel_perms = channel.permissions_for(ctx.me)
|
||||
if not channel_perms.text() and not channel_perms.manage_messages:
|
||||
return await send(ctx, "noCreatePermission")
|
||||
return await send_localized_message(ctx, "permissions.noCreatePermission")
|
||||
|
||||
settings = await func.get_settings(ctx.guild.id)
|
||||
controller = settings.get("default_controller", func.settings.controller).get("embeds", {}).get("inactive", {})
|
||||
message = await channel.send(embed=voicelink.build_embed(controller, voicelink.Placeholders(self.bot)))
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
controller = settings.get("default_controller", voicelink.Config().controller).get("embeds", {}).get("inactive", {})
|
||||
message = await channel.send(embed=PlayerPlaceholder.build_embed(controller, PlayerPlaceholder(self.bot)))
|
||||
|
||||
await update_settings(ctx.guild.id, {"$set": {'music_request_channel': {
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'music_request_channel': {
|
||||
"text_channel_id": channel.id,
|
||||
"controller_msg_id": message.id,
|
||||
}}})
|
||||
await send(ctx, "createSongRequestChannel", channel.mention)
|
||||
await send_localized_message(ctx, "songRequest.channelCreated", channel.mention)
|
||||
|
||||
@settings.command(name="reset", aliases=get_aliases("reset"))
|
||||
@app_commands.describe(setting="Which setting to restore to defaults.")
|
||||
@app_commands.choices(setting=[
|
||||
app_commands.Choice(name=label.capitalize(), value=label)
|
||||
for label in _RESET_LABELS.keys()
|
||||
])
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||
async def reset_setting(self, ctx: commands.Context, *, setting: str):
|
||||
"Remove one guild setting override so the bot uses its default again."
|
||||
setting_key = _RESET_LABELS.get(setting.lower())
|
||||
if not setting_key:
|
||||
return await send_localized_message(ctx, "settings.actions.resetNotFound", ", ".join(_RESET_LABELS.keys()))
|
||||
|
||||
await MongoDBHandler.update_settings(ctx.guild.id, {"$unset": {setting_key: ""}})
|
||||
player: voicelink.Player = ctx.guild.voice_client
|
||||
if setting_key == "volume" and player:
|
||||
await player.set_volume(100, ctx.author)
|
||||
elif setting_key == "duplicate_track" and player:
|
||||
player.queue._allow_duplicate = True
|
||||
await send_localized_message(ctx, "settings.actions.resetDone", setting.capitalize())
|
||||
|
||||
@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!")
|
||||
if interaction.user.id not in voicelink.Config().bot_access_user:
|
||||
return await interaction.response.send_message("You are not able to use this command!", ephemeral=True)
|
||||
|
||||
memory = psutil.virtual_memory()
|
||||
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 = discord.Embed(title="📄 Debug Panel", color=voicelink.Config().embed_color)
|
||||
embed.description = "```== System Info ==\n" \
|
||||
f"• CPU: {psutil.cpu_freq().current}Mhz ({psutil.cpu_percent()}%)\n" \
|
||||
f"• RAM: {format_bytes(total_memory - available_memory)}/{format_bytes(total_memory, True)} ({memory.percent}%)\n" \
|
||||
@@ -316,7 +352,7 @@ class Settings(commands.Cog, name="settings"):
|
||||
|
||||
embed.add_field(
|
||||
name="🤖 Bot Information",
|
||||
value=f"```• VERSION: {func.settings.version}\n" \
|
||||
value=f"```• VERSION: {voicelink.Config().version}\n" \
|
||||
f"• LATENCY: {self.bot.latency:.2f}ms\n" \
|
||||
f"• GUILDS: {len(self.bot.guilds)}\n" \
|
||||
f"• USERS: {sum([guild.member_count or 0 for guild in self.bot.guilds])}\n" \
|
||||
@@ -335,7 +371,7 @@ class Settings(commands.Cog, name="settings"):
|
||||
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)}```"
|
||||
f"• UPTIME: {format_ms(node.stats.uptime)}```"
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
|
||||
54
cogs/task.py
54
cogs/task.py
@@ -25,30 +25,28 @@ import voicelink
|
||||
import discord
|
||||
import function as func
|
||||
|
||||
from voicelink.config import Config
|
||||
from discord.ext import commands, tasks
|
||||
from addons import Placeholders
|
||||
|
||||
class Task(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.activity_update.start()
|
||||
self.player_check.start()
|
||||
self.cache_cleaner.start()
|
||||
|
||||
self.current_act = 0
|
||||
self.placeholder = Placeholders(bot)
|
||||
self.placeholder = voicelink.BotPlaceholder(bot)
|
||||
|
||||
def cog_unload(self):
|
||||
self.activity_update.cancel()
|
||||
self.player_check.cancel()
|
||||
self.cache_cleaner.cancel()
|
||||
|
||||
@tasks.loop(minutes=10.0)
|
||||
@tasks.loop(seconds=Config().timer_settings.get("bot_activity_update", 600))
|
||||
async def activity_update(self):
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
try:
|
||||
act_data = func.settings.activity[(self.current_act + 1) % len(func.settings.activity) - 1]
|
||||
act_data = Config().activity[(self.current_act + 1) % len(Config().activity) - 1]
|
||||
act_original = self.bot.activity
|
||||
act_type = getattr(discord.ActivityType, act_data.get("type", "").lower(), discord.ActivityType.playing)
|
||||
act_name = self.placeholder.replace(act_data.get("name", ""))
|
||||
@@ -58,54 +56,16 @@ class Task(commands.Cog):
|
||||
if act_original.type != act_type or act_original.name != act_name:
|
||||
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)
|
||||
self.current_act = (self.current_act + 1) % len(Config().activity)
|
||||
|
||||
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):
|
||||
for identifier, node in voicelink.NodePool._nodes.items():
|
||||
for guild_id, player in node._players.copy().items():
|
||||
try:
|
||||
if not player.channel or not player.context or not player.guild:
|
||||
await player.teardown()
|
||||
continue
|
||||
except:
|
||||
await player.teardown()
|
||||
continue
|
||||
|
||||
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 Exception as e:
|
||||
func.logger.error("Error occurred while checking the player!", exc_info=e)
|
||||
|
||||
@tasks.loop(hours=12.0)
|
||||
@tasks.loop(seconds=Config().timer_settings.get("cache_cleanup", 43200))
|
||||
async def cache_cleaner(self):
|
||||
func.SETTINGS_BUFFER.clear()
|
||||
func.USERS_BUFFER.clear()
|
||||
await voicelink.MongoDBHandler.cleanup_cache()
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(Task(bot))
|
||||
@@ -1,106 +0,0 @@
|
||||
# ------------------------------------------------------------------------------------------------------------ #
|
||||
|
||||
# For installation instructions please visit - https://docs.vocard.xyz/latest/bot/setup/docker-linux/
|
||||
# or for Windows Docker - https://docs.vocard.xyz/latest/bot/setup/docker-windows/
|
||||
|
||||
# ------------------------------------------ THANK YOU FOR READING! ------------------------------------------ #
|
||||
name: vocard
|
||||
services:
|
||||
lavalink:
|
||||
container_name: lavalink
|
||||
# image: ghcr.io/lavalink-devs/lavalink:latest
|
||||
# Use build to run Lavalink on the latest JRE 23 version (for better performance)
|
||||
build:
|
||||
context: ./lavalink
|
||||
dockerfile: ./Dockerfile-lavalink
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- _JAVA_OPTIONS=-Xmx1G
|
||||
- SERVER_PORT=2333
|
||||
# there is no point in changing the password here, since the container is available only in docker network
|
||||
- LAVALINK_SERVER_PASSWORD=youshallnotpass # Change password if needed (don't forget to change it in healthcheck below and settings.json)
|
||||
volumes:
|
||||
- ./lavalink/application.yml:/opt/Lavalink/application.yml
|
||||
networks:
|
||||
- vocard
|
||||
expose:
|
||||
- "2333"
|
||||
healthcheck:
|
||||
test: nc -z -v localhost 2333
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
spotify-tokener:
|
||||
image: ghcr.io/topi314/spotify-tokener:master
|
||||
container_name: spotify-tokener
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SPOTIFY_TOKENER_ADDR=0.0.0.0:49152
|
||||
networks:
|
||||
- vocard
|
||||
ports:
|
||||
- 49152:49152
|
||||
healthcheck:
|
||||
test: nc -z -v localhost 49152
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
vocard-db:
|
||||
container_name: vocard-db
|
||||
image: mongo:8
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/mongo/db:/data/db
|
||||
- ./data/mongo/conf:/data/configdb
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=admin # For your MongoDB URL use "mongodb://admin:admin@vocard-db:27017"
|
||||
- MONGO_INITDB_ROOT_PASSWORD=admin
|
||||
expose:
|
||||
- "27017"
|
||||
networks:
|
||||
- vocard
|
||||
command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"]
|
||||
healthcheck:
|
||||
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
# vocard-dashboard:
|
||||
# container_name: vocard-dashboard
|
||||
# image: ghcr.io/chocomeow/vocard-dashboard:latest
|
||||
# restart: unless-stopped
|
||||
# # If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line.
|
||||
# # build:
|
||||
# # context: ./dashboard
|
||||
# # dockerfile: ./Dockerfile
|
||||
# volumes:
|
||||
# - ./dashboard/settings.json:/app/settings.json
|
||||
# ports:
|
||||
# - 8000:8000
|
||||
# networks:
|
||||
# - vocard
|
||||
|
||||
vocard:
|
||||
container_name: vocard
|
||||
restart: unless-stopped
|
||||
# If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line.
|
||||
image: ghcr.io/chocomeow/vocard:latest
|
||||
# build:
|
||||
# dockerfile: ./Dockerfile
|
||||
volumes:
|
||||
- ./settings.json:/app/settings.json
|
||||
networks:
|
||||
- vocard
|
||||
depends_on:
|
||||
lavalink:
|
||||
condition: service_healthy
|
||||
vocard-db:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
vocard:
|
||||
name: vocard
|
||||
283
function.py
283
function.py
@@ -21,68 +21,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord, json, os, copy, logging
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import voicelink
|
||||
|
||||
from discord.ext import commands
|
||||
from time import strptime
|
||||
from addons import Settings
|
||||
|
||||
from typing import (
|
||||
Optional,
|
||||
Union,
|
||||
Dict,
|
||||
Any
|
||||
)
|
||||
|
||||
from motor.motor_asyncio import (
|
||||
AsyncIOMotorClient,
|
||||
AsyncIOMotorCollection,
|
||||
)
|
||||
from typing import Optional
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
logger: logging.Logger = logging.getLogger("vocard")
|
||||
|
||||
if not os.path.exists(os.path.join(ROOT_DIR, "settings.json")):
|
||||
raise Exception("Settings file not set!")
|
||||
|
||||
#--------------- Cache Var ---------------
|
||||
settings: Settings
|
||||
logger: logging.Logger = logging.getLogger("vocard")
|
||||
|
||||
MONGO_DB: AsyncIOMotorClient
|
||||
SETTINGS_DB: AsyncIOMotorCollection
|
||||
USERS_DB: AsyncIOMotorCollection
|
||||
|
||||
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] = {}
|
||||
|
||||
MISSING_TRANSLATOR: dict[str, list[str]] = {}
|
||||
|
||||
USER_BASE: dict[str, Any] = {
|
||||
'playlist': {
|
||||
'200': {
|
||||
'tracks':[],
|
||||
'perms': {'read': [], 'write':[], 'remove': []},
|
||||
'name':'Favourite',
|
||||
'type':'playlist'
|
||||
}
|
||||
},
|
||||
'history': [],
|
||||
'inbox':[]
|
||||
}
|
||||
|
||||
ALLOWED_MENTIONS = discord.AllowedMentions().none()
|
||||
LAST_SESSION_FILE_NAME = "last-session.json"
|
||||
|
||||
#-------------- Vocard Classes --------------
|
||||
class TempCtx():
|
||||
def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None:
|
||||
self.author: discord.Member = author
|
||||
self.channel: discord.VoiceChannel = channel
|
||||
self.guild: discord.Guild = channel.guild
|
||||
|
||||
#-------------- Vocard Functions --------------
|
||||
def open_json(path: str) -> dict:
|
||||
try:
|
||||
with open(os.path.join(ROOT_DIR, path), encoding="utf8") as json_file:
|
||||
@@ -100,229 +53,13 @@ def update_json(path: str, new_data: dict) -> None:
|
||||
with open(os.path.join(ROOT_DIR, path), "w") as json_file:
|
||||
json.dump(data, json_file, indent=4)
|
||||
|
||||
def langs_setup() -> None:
|
||||
for language in os.listdir(os.path.join(ROOT_DIR, "langs")):
|
||||
if language.endswith('.json'):
|
||||
LANGS[language[:-5]] = {}
|
||||
|
||||
for language in os.listdir(os.path.join(ROOT_DIR, "local_langs")):
|
||||
if language.endswith('.json'):
|
||||
LOCAL_LANGS[language[:-5]] = open_json(os.path.join("local_langs", language))
|
||||
|
||||
return
|
||||
|
||||
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 format_time(number:str) -> int:
|
||||
try:
|
||||
try:
|
||||
num = strptime(number, '%M:%S')
|
||||
except ValueError:
|
||||
try:
|
||||
num = strptime(number, '%S')
|
||||
except ValueError:
|
||||
num = strptime(number, '%H:%M:%S')
|
||||
except:
|
||||
return 0
|
||||
|
||||
return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000
|
||||
|
||||
def get_source(source: str, type: str) -> str:
|
||||
source_settings: dict[str, str] = settings.sources_settings.get(source.lower().replace(" ", ""), settings.sources_settings.get("others"))
|
||||
return source_settings.get(type)
|
||||
|
||||
def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]:
|
||||
if ctx.author.id in settings.bot_access_user:
|
||||
if ctx.author.id in voicelink.Config().bot_access_user:
|
||||
return None
|
||||
cooldown = settings.cooldowns_settings.get(f"{ctx.command.parent.qualified_name} {ctx.command.name}" if ctx.command.parent else ctx.command.name)
|
||||
cooldown = voicelink.Config().cooldowns_settings.get(f"{ctx.command.parent.qualified_name} {ctx.command.name}" if ctx.command.parent else ctx.command.name)
|
||||
if not cooldown:
|
||||
return None
|
||||
return commands.Cooldown(cooldown[0], cooldown[1])
|
||||
|
||||
def get_aliases(name: str) -> list:
|
||||
return settings.aliases_settings.get(name, [])
|
||||
|
||||
def check_roles() -> tuple[str, int, int]:
|
||||
return 'Normal', 5, 500
|
||||
|
||||
def truncate_string(text: str, length: int = 40) -> str:
|
||||
return text[:length - 3] + "..." if len(text) > length else text
|
||||
|
||||
def get_lang_non_async(guild_id: int, *keys) -> Union[list[str], str]:
|
||||
settings = SETTINGS_BUFFER.get(guild_id, {})
|
||||
lang = settings.get("lang", "EN")
|
||||
if lang in LANGS and not LANGS[lang]:
|
||||
LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json"))
|
||||
|
||||
if len(keys) == 1:
|
||||
return LANGS.get(lang, {}).get(keys[0], "Not found!")
|
||||
return [LANGS.get(lang, {}).get(key, "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) -> Optional[Union[list[str], str]]:
|
||||
settings = await get_settings(guild_id)
|
||||
lang = settings.get("lang", "EN")
|
||||
if lang in LANGS and not LANGS[lang]:
|
||||
LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json"))
|
||||
|
||||
if len(keys) == 1:
|
||||
return LANGS.get(lang, {}).get(keys[0])
|
||||
return [LANGS.get(lang, {}).get(key) for key in keys]
|
||||
|
||||
async def send(
|
||||
ctx: Union[commands.Context, discord.Interaction],
|
||||
content: Union[str, discord.Embed] = None,
|
||||
*params,
|
||||
view: discord.ui.View = None,
|
||||
delete_after: float = None,
|
||||
ephemeral: bool = False,
|
||||
requires_fetch: bool = False
|
||||
) -> Optional[discord.Message]:
|
||||
if content is None:
|
||||
content = "No content provided."
|
||||
|
||||
# Determine the text to send
|
||||
if isinstance(content, discord.Embed):
|
||||
embed = content
|
||||
text = None
|
||||
else:
|
||||
text = await get_lang(ctx.guild.id, content)
|
||||
if text:
|
||||
text = text.format(*params)
|
||||
else:
|
||||
text = content.format(*params)
|
||||
embed = None
|
||||
|
||||
# Determine the sending function
|
||||
send_func = (
|
||||
ctx.send if isinstance(ctx, commands.Context) else
|
||||
ctx.channel.send if isinstance(ctx, TempCtx) else
|
||||
ctx.followup.send if ctx.response.is_done() else
|
||||
ctx.response.send_message
|
||||
)
|
||||
|
||||
# Check settings for delete_after duration
|
||||
settings = await get_settings(ctx.guild.id)
|
||||
send_kwargs = {
|
||||
"content": text,
|
||||
"embed": embed,
|
||||
"allowed_mentions": ALLOWED_MENTIONS,
|
||||
"silent": settings.get("silent_msg", False),
|
||||
}
|
||||
|
||||
if "delete_after" in send_func.__code__.co_varnames:
|
||||
if settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"):
|
||||
delete_after = 10
|
||||
send_kwargs["delete_after"] = delete_after
|
||||
|
||||
if "ephemeral" in send_func.__code__.co_varnames:
|
||||
send_kwargs["ephemeral"] = ephemeral
|
||||
|
||||
if view:
|
||||
send_kwargs["view"] = view
|
||||
|
||||
# Send the message or embed
|
||||
message = await send_func(**send_kwargs)
|
||||
|
||||
if isinstance(message, discord.InteractionCallbackResponse):
|
||||
message = message.resource
|
||||
|
||||
if requires_fetch and isinstance(message, (discord.WebhookMessage, discord.InteractionMessage)):
|
||||
message = await message.fetch()
|
||||
|
||||
return message
|
||||
|
||||
async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool:
|
||||
for mode, action in data.items():
|
||||
for key, value in action.items():
|
||||
cursors = key.split(".")
|
||||
|
||||
nested_data = tempStore
|
||||
for c in cursors[:-1]:
|
||||
nested_data = nested_data.setdefault(c, {})
|
||||
|
||||
if mode == "$set":
|
||||
try:
|
||||
nested_data[cursors[-1]] = value
|
||||
except TypeError:
|
||||
nested_data[int(cursors[-1])] = value
|
||||
|
||||
elif mode == "$unset":
|
||||
nested_data.pop(cursors[-1], None)
|
||||
|
||||
elif mode == "$inc":
|
||||
nested_data[cursors[-1]] = nested_data.get(cursors[-1], 0) + value
|
||||
|
||||
elif mode == "$push":
|
||||
if isinstance(value, dict) and "$each" in value:
|
||||
nested_data.setdefault(cursors[-1], []).extend(value["$each"][value.get("$slice", len(value["$each"])):])
|
||||
else:
|
||||
nested_data.setdefault(cursors[-1], []).extend([value])
|
||||
|
||||
elif mode == "$push":
|
||||
if isinstance(value, dict) and "$each" in value:
|
||||
nested_data.setdefault(cursors[-1], []).extend(value["$each"])
|
||||
nested_data[cursors[-1]] = nested_data[cursors[-1]][value.get("$slice", len(value["$each"])):]
|
||||
else:
|
||||
nested_data.setdefault(cursors[-1], []).extend([value])
|
||||
|
||||
elif mode == "$pull":
|
||||
if cursors[-1] in nested_data:
|
||||
value = value.get("$in", []) if isinstance(value, dict) else [value]
|
||||
nested_data[cursors[-1]] = [item for item in nested_data[cursors[-1]] if item not in value]
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
result = await db.update_one(filter, data)
|
||||
return result.modified_count > 0
|
||||
|
||||
async def get_settings(guild_id:int) -> dict[str, Any]:
|
||||
settings = SETTINGS_BUFFER.get(guild_id, None)
|
||||
if not settings:
|
||||
settings = await SETTINGS_DB.find_one({"_id": guild_id})
|
||||
if not settings:
|
||||
await SETTINGS_DB.insert_one({"_id": guild_id})
|
||||
|
||||
settings = SETTINGS_BUFFER[guild_id] = settings or {}
|
||||
return settings
|
||||
|
||||
async def update_settings(guild_id: int, data: dict[str, dict[str, Any]]) -> bool:
|
||||
settings = await get_settings(guild_id)
|
||||
return await update_db(SETTINGS_DB, settings, {"_id": guild_id}, data)
|
||||
|
||||
async def get_user(user_id: int, d_type: Optional[str] = None, need_copy: bool = True) -> Dict[str, Any]:
|
||||
user = USERS_BUFFER.get(user_id)
|
||||
if not user:
|
||||
user = await USERS_DB.find_one({"_id": user_id})
|
||||
if not user:
|
||||
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(USER_BASE.get(d_type)))
|
||||
|
||||
return copy.deepcopy(user) if need_copy else user
|
||||
|
||||
async def update_user(user_id:int, data:dict) -> bool:
|
||||
playlist = await get_user(user_id, need_copy=False)
|
||||
return await update_db(USERS_DB, playlist, {"_id": user_id}, data)
|
||||
return voicelink.Config().aliases_settings.get(name, [])
|
||||
172
langs/CH.json
172
langs/CH.json
@@ -1,172 +0,0 @@
|
||||
{
|
||||
"unknownException": "⚠️ 執行命令時出現問題!請稍後再試,或加入我們的 Discord 伺服器獲得進一步支援。",
|
||||
"enabled": "已啟用",
|
||||
"disabled": "已停用",
|
||||
"nodeReconnect": "請稍後再試!在節點重新連接後再嘗試。",
|
||||
"noChannel": "沒有語音頻道可供連接。請提供一個語音頻道或加入一個語音頻道。",
|
||||
"alreadyConnected": "已經連接到語音頻道。",
|
||||
"noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。",
|
||||
"noCreatePermission": "抱歉!我沒有權限建立歌曲請求頻道。",
|
||||
"noPlaySource": "找不到任何可播放的來源!",
|
||||
"noPlayer": "在此伺服器上找不到播放器。",
|
||||
"notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。",
|
||||
"missingIntents": "抱歉,此命令無法執行,因為機器人缺少所需的請求意圖:`({0})`.",
|
||||
"languageNotFound": "找不到語言包!請選擇一個現有的語言包。",
|
||||
"changedLanguage": "已成功切換到 `{0}` 語言包。",
|
||||
"setPrefix": "完成!我的前綴在您的伺服器中現在是 `{0}`。嘗試運行 `{1}ping` 來測試它。",
|
||||
"setDJ": "已將 DJ 設置為 {0}。",
|
||||
"setQueue": "已將隊列模式設置為 `{0}`。",
|
||||
"247": "現在您有 `{0}` 24/7 模式。",
|
||||
"bypassVote": "現在您有 `{0}` 投票系統。",
|
||||
"setVolume": "已將音量設置為 `{0}`%",
|
||||
"toggleController": "現在您已 `{0}` 音樂控制器。",
|
||||
"toggleDuplicateTrack": "現在您已 `{0}` 防止隊列中存在重複曲目。",
|
||||
"toggleControllerMsg": "現在您已從音樂控制器 `{0}` 消息。",
|
||||
"toggleSilentMsg": "你{0}靜默消息。",
|
||||
"settingsMenu": "伺服器設置 | {0}",
|
||||
"settingsTitle": "❤️ 基本資訊:",
|
||||
"settingsValue": "```前綴:{0}\n語言:{1}\n音樂控制器:{2}\nDJ 角色:@{3}\n投票繞過:{4}\n24/7:{5}\n默認音量:{6}%\n播放時間:{7}```",
|
||||
"settingsTitle2": "🔗 隊列資訊:",
|
||||
"settingsValue2": "```隊列模式:{0}\n最大歌曲數:{1}\n允許重複曲目:{2}```",
|
||||
"settingsTitle3": "🎤 語音狀態資訊:",
|
||||
"settingsPermTitle": "✨ 權限:",
|
||||
"settingsPermValue": "```{0} 管理員\n{1} 管理伺服器\n{2} 管理頻道\n{3} 管理訊息```",
|
||||
"pingTitle1": "機器人資訊:",
|
||||
"pingTitle2": "播放器信息:",
|
||||
"pingField1": "```分片 ID: {0}/{1}\n分片延遲: {2:.3f}s {3}\n區域: {4}```",
|
||||
"pingField2": "```節點: {0} - {1:.3f}s\n播放器數量: {2}\n語音區域: {3}```",
|
||||
"addEffect": "套用音效`{0}`濾鏡。",
|
||||
"clearEffect": "聲音效果已清除!",
|
||||
"filterTagAlreadyInUse": "此聲音效果已在使用中!請使用 /cleareffect <Tag> 移除它。",
|
||||
"playlistViewTitle": "📜 所有 {0} 的播放清單",
|
||||
"playlistViewHeaders": "ID:,時間:,名稱:,曲目數:",
|
||||
"playlistFooter": "輸入 /playlist play [播放清單] 加入此播放清單至隊列中。",
|
||||
"playlistNotFound": "找不到播放清單 [`{0}`]。輸入 /playlist view 查看所有播放清單。",
|
||||
"playlistNotAccess": "抱歉!你無權訪問此播放清單!",
|
||||
"playlistNoTrack": "抱歉!播放清單 [`{0}`] 中沒有曲目。",
|
||||
"playlistNotAllow": "此命令不允許使用於已連結或共享的播放清單。",
|
||||
"playlistPlay": "已加入播放清單 [`{0}`] 的 `{1}` 首歌曲至隊列中。",
|
||||
"playlistOverText": "抱歉!播放清單名稱不能超過 10 個字符。",
|
||||
"playlistSameName": "抱歉!此名稱不能與你的新名稱相同。",
|
||||
"playlistDeleteError": "你無權刪除預設播放清單。",
|
||||
"playlistRemove": "你已移除播放清單 [`{0}`]。",
|
||||
"playlistSendErrorPlayer": "抱歉!你無法向自己發送邀請。",
|
||||
"playlistSendErrorBot": "抱歉!你無法向機器人發送邀請。",
|
||||
"playlistBelongs": "抱歉!此播放清單屬於 <@{0}>。",
|
||||
"playlistShare": "抱歉!此播放清單已與 {0} 共享。",
|
||||
"playlistSent": "抱歉!你已發送過邀請。",
|
||||
"noPlaylistAcc": "{0} 沒有建立播放清單帳戶。",
|
||||
"overPlaylistCreation": "你不能建立超過 `{0}` 個播放清單!",
|
||||
"playlistExists": "播放清單 [`{0}`] 已存在。",
|
||||
"playlistNotInvalidUrl": "請輸入有效的連結或公開的 Spotify 或 YouTube 播放清單連結。",
|
||||
"playlistCreated": "你已建立 `{0}` 播放清單。輸入 /playlist view 檢視更多資訊。",
|
||||
"playlistRenamed": "你已將 `{0}` 更名為 `{1}`。",
|
||||
"playlistLimitTrack": "你已達到限制!你只能將 `{0}` 首歌曲添加至你的播放清單中。",
|
||||
"playlistPlaylistLink": "你無法使用播放清單連結。",
|
||||
"playlistStream": "你無法將串流影片添加至你的播放清單中。",
|
||||
"playlistPositionNotFound": "找不到播放清單 [`{1}`] 中位置為 `{0}` 的曲目!",
|
||||
"playlistRemoved": "👋 已從 {1} 的播放清單 [`{2}`] 中刪除 **{0}**。",
|
||||
"playlistClear": "你已成功清除播放清單 [`{0}`]。",
|
||||
"playlistView": "播放清單檢視器",
|
||||
"playlistViewDesc": "```名稱 | ID: {0} | {1}\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}",
|
||||
"playlistViewPermsValue2": "📖 讀取: {0}",
|
||||
"playlistViewTrack": "音軌",
|
||||
"playlistViewPage": "頁面: {0}/{1} | 總長度: {2}",
|
||||
"inboxFull": "抱歉!{0} 的收件匣已滿。",
|
||||
"inboxNoMsg": "您的收件匣中沒有任何訊息。",
|
||||
"invitationSent": "已發送邀請給 {0}。",
|
||||
"notInChannel": "{0},您必須在 {1} 中使用語音指令。如果您已在語音頻道中,請重新加入!",
|
||||
"noTrackPlaying": "現在沒有歌曲正在播放",
|
||||
"noTrackFound": "找不到符合該查詢的歌曲!請提供有效的網址。",
|
||||
"noLinkSupport": "搜索命令不支援網址!",
|
||||
"voted": "您已投票!",
|
||||
"missingPosPerm": "只有 DJ 或管理員才能更改位置。",
|
||||
"missingModePerm": "只有 DJ 或管理員才能切換循環模式。",
|
||||
"missingQueuePerm": "只有 DJ 或管理員才能從隊列中移除音軌。",
|
||||
"missingAutoPlayPerm": "只有 DJ 或管理員才能啟用或停用自動播放模式!",
|
||||
"missingFunctionPerm": "只有 DJ 或管理員才能使用此功能。",
|
||||
"timeFormatError": "時間格式不正確。例如:2:42 或 12:39:31",
|
||||
"lyricsNotFound": "找不到歌詞。輸入 /lyrics <歌曲名稱> <作者> 查找歌詞。",
|
||||
"missingTrackInfo": "有些音軌資訊缺失。",
|
||||
"noVoiceChannel": "找不到語音頻道!",
|
||||
"playlistAddError": "您無權將串流視訊添加到播放清單中!",
|
||||
"playlistAddError2": "添加音軌到播放清單時發生問題!",
|
||||
"playlistLimited": "您已達到上限!您只能將 {0} 首歌曲添加到播放清單中。",
|
||||
"playlistRepeated": "您的播放清單中已經存在相同的音軌!",
|
||||
"playlistAdded": "❤️ 已將 **{0}** 添加到 {1} 的播放清單中 [`{2}`]!",
|
||||
"playerDropdown": "選擇要跳轉到的音軌...",
|
||||
"playerFilter": "選擇要套用的篩選器...",
|
||||
"buttonBack": "返回",
|
||||
"buttonPause": "暫停",
|
||||
"buttonResume": "繼續",
|
||||
"buttonSkip": "跳過",
|
||||
"buttonLeave": "離開",
|
||||
"buttonLoop": "循環",
|
||||
"buttonVolumeUp": "增加音量",
|
||||
"buttonVolumeDown": "降低音量",
|
||||
"buttonVolumeMute": "靜音",
|
||||
"buttonVolumeUnmute": "取消靜音",
|
||||
"buttonAutoPlay": "自動播放",
|
||||
"buttonShuffle": "隨機播放",
|
||||
"buttonForward": "前進",
|
||||
"buttonRewind": "後退",
|
||||
"buttonLyrics": "歌詞",
|
||||
"nowplayingDesc": "**現在播放:**\n```{0}```",
|
||||
"nowplayingField": "接下來播放:",
|
||||
"nowplayingLink": "在 {0} 上收聽",
|
||||
"connect": "已連接至 {0}",
|
||||
"live": "直播",
|
||||
"playlistLoad": " 🎶 已添加播放清單 **{0}**,共 `{1}` 首歌曲至隊列。",
|
||||
"trackLoad": "已添加 **[{0}](<{1}>)**,由 **{2}** (`{3}`) 開始播放。\n",
|
||||
"trackLoad_pos": "已將 **[{0}](<{1}>)**,由 **{2}** (`{3}`) 添加到隊列中位置 **{4}**\n",
|
||||
"searchTitle": "搜索查詢: {0}",
|
||||
"searchDesc": "➥ 平台: {0} **{1}**\n➥ 結果: **{2}**\n\n{3}",
|
||||
"searchWait": "選擇您想要添加到隊列中的歌曲。",
|
||||
"searchTimeout": "搜索超時。請稍後再試。",
|
||||
"searchSuccess": "已將歌曲添加到隊列中。",
|
||||
"queueTitle": "即將播放的隊列:",
|
||||
"historyTitle": "歷史隊列:",
|
||||
"viewTitle": "音樂隊列",
|
||||
"viewDesc": "**現正播放:[點擊我]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "頁數:{0}/{1} | 總長度:{2}",
|
||||
"pauseError": "播放器已經暫停。",
|
||||
"pauseVote": "{0} 已投票暫停歌曲。[{1}/{2}]",
|
||||
"paused": "播放器已被 `{0}` 暫停。",
|
||||
"resumeError": "播放器未暫停。",
|
||||
"resumeVote": "{0} 已投票恢復歌曲。[{1}/{2}]",
|
||||
"resumed": "播放器已被 `{0}` 恢復。",
|
||||
"shuffleError": "在洗牌之前必須添加更多歌曲到隊列中。",
|
||||
"shuffleVote": "{0} 已投票洗牌隊列。[{1}/{2}]",
|
||||
"shuffled": "隊列已被洗牌。",
|
||||
"skipError": "沒有歌曲可以跳過。",
|
||||
"skipVote": "{0} 已投票跳過歌曲。[{1}/{2}]",
|
||||
"skipped": "播放器已被 `{0}` 跳過歌曲。",
|
||||
"backVote": "{0} 已投票跳到上一首歌曲。[{1}/{2}]",
|
||||
"backed": "播放器已被 `{0}` 跳到上一首歌曲。",
|
||||
"leaveVote": "{0} 已投票停止播放器。[{1}/{2}]",
|
||||
"left": "播放器已被 `{0}` 停止。",
|
||||
"seek": "將播放器設置到 **{0}**。",
|
||||
"repeat": "重複模式已設置為 `{0}`。",
|
||||
"cleared": "清除了 `{0}` 中的所有歌曲。",
|
||||
"removed": "已從隊列中刪除 `{0}` 首歌曲。",
|
||||
"forward": "將播放器快進到 **{0}**。",
|
||||
"rewind": "將播放器倒回到 **{0}**。",
|
||||
"replay": "重新播放當前歌曲。",
|
||||
"swapped": "已交換 `{0}` 和 `{1}`。",
|
||||
"moved": "已將 `{0}` 移動到 `{1}`。",
|
||||
"autoplay": "自動播放模式現在為 **{0}**。",
|
||||
"notdj": "您不是DJ,當前DJ為 {0}。",
|
||||
"djToMe": "您無法將DJ權限轉移給自己或機器人。",
|
||||
"djNotInChannel": "`{0}` 不在語音頻道中。",
|
||||
"djswap": "您已將DJ權限轉移給 `{0}`。",
|
||||
"voicelinkQueueFull": "抱歉,您已達到隊列中 `{0}` 首歌曲的最大數量!",
|
||||
"voicelinkOutofList": "請提供有效的歌曲索引!",
|
||||
"voicelinkDuplicateTrack": "抱歉,此歌曲已在隊列中。",
|
||||
"decodeError": "解碼文件時出現問題!",
|
||||
"invalidStartTime": "無效的開始時間! 時間必須在 `00:00` 和 `{0}` 之間。",
|
||||
"invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。",
|
||||
"invalidTimeOrder": "結束時間不能小於或等於開始時間。",
|
||||
"setStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。",
|
||||
"createSongRequestChannel": "一個歌曲請求頻道 ({0}) 已建立!您可以在該頻道中透過歌曲名稱或 URL 開始要求任何歌曲,而無需使用機器人前綴。"
|
||||
}
|
||||
435
langs/DE.json
435
langs/DE.json
@@ -1,172 +1,265 @@
|
||||
{
|
||||
"unknownException": "⚠️ Beim Ausführen des Befehls ist etwas schiefgelaufen! Bitte versuche es später erneut oder tritt unserem Discord-Server bei, um weiteren Support zu erhalten.",
|
||||
"enabled": "aktiviert",
|
||||
"disabled": "deaktiviert",
|
||||
"nodeReconnect": "Bitte versuche es erneut, nachdem sich die Node wieder verbunden hat.",
|
||||
"noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stelle entweder einen zur Verfügung oder trete einem bei.",
|
||||
"alreadyConnected": "Bereits mit einem Sprachkanal verbunden.",
|
||||
"noPermission": "Es tut mir leid, ich bin nicht berechtigt, dem Sprachkanal beizutreten oder darin zu sprechen.",
|
||||
"noCreatePermission": "Es tut mir leid, ich habe keine Berechtigung, einen Song Request Channel zu erstellen.",
|
||||
"noPlaySource": "Ich kann keine abspielbaren Quellen finden!",
|
||||
"noPlayer": "Auf diesem Server wurden kein Player gefunden.",
|
||||
"notVote": "Dieser Befehl erfordert Deine Stimme! Gebe `/vote` ein, um weitere Informationen zu erhalten.",
|
||||
"missingIntents": "Es tut mir leid, dieser Befehl kann nicht ausgeführt werden, da mir der `({0})` Intent fehlt.",
|
||||
"languageNotFound": "Kein Sprachpaket gefunden. Bitte wähle ein vorhandenes Sprachpaket aus.",
|
||||
"changedLanguage": "Erfolgreich auf das Sprachpaket `{0}` geändert.",
|
||||
"setPrefix": "Erledigt! Mein Präfix ist jetzt `{0}` auf deinem Server. Versuche, `{1}ping` auszuführen, um es zu testen.",
|
||||
"setDJ": "Stelle den DJ auf {0}.",
|
||||
"setQueue": "Stelle den Warteschlangenmodus auf `{0}` ein.",
|
||||
"247": "Der 24/7-Modus wurde erfolgreich `{0}`.",
|
||||
"bypassVote": "Das Abstimmungssystem wurde `{0}`",
|
||||
"setVolume": "Stelle die Lautstärke auf `{0}`%.",
|
||||
"toggleController": "Der Musikcontroller wurde erfolgreich `{0}`",
|
||||
"toggleDuplicateTrack": "Du hast das hinzufügen von doppelten Tracks in der Warteschlange `{0}`",
|
||||
"toggleControllerMsg": "Nachrichten vom Musik-Controller wurden erfolgreich `{0}`",
|
||||
"toggleSilentMsg": "Sie haben {0} stille Nachrichten.",
|
||||
"settingsMenu": "Servereinstellungen | {0}",
|
||||
"settingsTitle": "❤️ Grundlegende Informationen:",
|
||||
"settingsValue": "```Präfix: {0}`\nSprache: {1}\nMusik-Controller: {2}\nDJ-Rolle: @{3}\nAbstimmungsumgehung: {4}\n24/7 Play: {5}\nStandard Lautstärke: {6}%\nSpielzeit: {7}```",
|
||||
"settingsTitle2": "🔗 Warteschlangeninformationen:",
|
||||
"settingsValue2": "```Warteschlangenmodus: {0}\nMaximale Songs: {1}\nDoppelte Tracks zulassen: {2}```",
|
||||
"settingsTitle3": "🎤 Sprachstatusinfo:",
|
||||
"settingsPermTitle": "✨ Berechtigungen:",
|
||||
"settingsPermValue": "```{0} Administrator\n{1} Guild verwalten\n{2} Kanal verwalten\n{3} Manage_Messages```",
|
||||
"pingTitle1": "Bot-Info:",
|
||||
"pingTitle2": "Player Info:",
|
||||
"pingField1": "```Shard-ID: {0}/{1}\nShard-Latenz: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"pingField2": "```Node: {0} - {1:.3f}s\nPlayer: {2}\nSprachregion: {3}```",
|
||||
"addEffect": "Wende den Effekt `{0}` Filter an.",
|
||||
"clearEffect": "Die Soundeffekte wurden gelöscht!",
|
||||
"filterTagAlreadyInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwende /cleareffect <Tag>, um sie zu entfernen.",
|
||||
"playlistViewTitle": "📜 Alle Playlists von {0}",
|
||||
"playlistViewHeaders": "ID:,Zeit:,Name:,Tracks:",
|
||||
"playlistFooter": "Gebe /playlist play [playlist] ein, um die Playlist in die Warteschlange einzufügen.",
|
||||
"playlistNotFound": "Die Wiedergabeliste [`{0}`] wurde nicht gefunden. Gebe /playlist view ein, um Deine gesamte Wiedergabeliste anzuzeigen.",
|
||||
"playlistNotAccess": "Es tut mir leid! Du bist nicht berechtigt, auf diese Playlist zuzugreifen!",
|
||||
"playlistNoTrack": "Es tut uns leid! Es gibt keine Titel in der Wiedergabeliste [`{0}`].",
|
||||
"playlistNotAllow": "Dieser Befehl ist für verknüpfte und freigegebene Wiedergabelisten nicht zulässig.",
|
||||
"playlistPlay": "Die Playlist [`{0}`] mit `{1}` Songs wurde zur Warteschlange hinzugefügt.",
|
||||
"playlistOverText": "Verzeihung, der Name der Playlist darf nicht mehr als 10 Zeichen enthalten.",
|
||||
"playlistSameName": "Verzeihung, dieser Name darf nicht mit Deinem neuen Namen identisch sein.",
|
||||
"playlistDeleteError": "Du kannst die Standard-Wiedergabeliste nicht löschen.",
|
||||
"playlistRemove": "Du hast die Wiedergabeliste [`{0}`] entfernt.",
|
||||
"playlistSendErrorPlayer": "Entschuldigung, du kannst keine Einladung an dich selber senden.",
|
||||
"playlistSendErrorBot": "Entschuldigung, Du kannst keine Einladung an eine App senden.",
|
||||
"playlistBelongs": "Verzeihung, diese Playlist gehört <@{0}>.",
|
||||
"playlistShare": "Verzeigung, Diese Playlist wurde bereits mit {0} geteilt.",
|
||||
"playlistSent": "Verzeihung, Du hast bereits eine Einladung gesendet.",
|
||||
"noPlaylistAcc": "{0} hat noch kein Playlist-Konto erstellt.",
|
||||
"overPlaylistCreation": "Du kannst nicht mehr als `{0}` Wiedergabelisten erstellen!",
|
||||
"playlistExists": "Die Playlist [`{0}`] existiert bereits.",
|
||||
"playlistNotInvalidUrl": "Bitte gebe einen gültigen Link oder öffentlichen Spotify/YouTube-Playlist-Link ein.",
|
||||
"playlistCreated": "Du hast die Wiedergabeliste `{0}` erstellt. Gebe /playlist view ein, um weitere Informationen zu erhalten.",
|
||||
"playlistRenamed": "Du hast `{0}` zu `{1}` umbenannt.",
|
||||
"playlistLimitTrack": "Du hast die Grenze erreicht! Du kannst deiner Playlist nur noch `{0}` Songs hinzufügen.",
|
||||
"playlistPlaylistLink": "Du kannst keine Playlist-Links verwenden.",
|
||||
"playlistStream": "Du darfst deiner Playlist keine aktiven Streams hinzufügen.",
|
||||
"playlistPositionNotFound": "Die Position `{0}` kann in Deiner Playlist [`{1}`] nicht gefunden werden.",
|
||||
"playlistRemoved": "👋 {0} aus {1}s Playlist [{2}] entfernt.",
|
||||
"playlistClear": "Du hast Deine Wiedergabenliste [`{0}`] erfolgreich gelöscht.",
|
||||
"playlistView": "Playlist-Viewer",
|
||||
"playlistViewDesc": "```Name | ID: {0} | {1}\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Lesen: ✓ ✍🏽 Schreiben: {0} 🗑️ Entfernen: {1}",
|
||||
"playlistViewPermsValue2": "📖 Lesen: {0}",
|
||||
"playlistViewTrack": "Tracks",
|
||||
"playlistViewPage": "Seite: {0}/{1} | Gesamtdauer: {2}",
|
||||
"inboxFull": "Es tut mir leid! Der Posteingang von {0} ist voll.",
|
||||
"inboxNoMsg": "Es sind keine Nachrichten in Deinem Posteingang.",
|
||||
"invitationSent": "Einladung an {0} gesendet.",
|
||||
"notInChannel": "{0}, Du musst in {1} sein, um Sprachbefehle nutzen zu können. Bitte betrete den Sprachkanal erneut bei, wenn Du dich im Voice Channel befindest!",
|
||||
"noTrackPlaying": "Es werden derzeit keine Songs abgespielt",
|
||||
"noTrackFound": "Es wurden keine Songs mit dieser Abfrage gefunden! Bitte gib eine gültige URL an.",
|
||||
"noLinkSupport": "Der Suchbefehl unterstützt keine Links!",
|
||||
"voted": "Du hast abgestimmt!",
|
||||
"missingPosPerm": "Nur der DJ oder Admins können die Position ändern.",
|
||||
"missingModePerm": "Nur der DJ oder Admins können den Wiederholungsmodus wechseln.",
|
||||
"missingQueuePerm": "Nur der DJ oder Admins können Tracks aus der Warteschlange entfernen.",
|
||||
"missingAutoPlayPerm": "Nur der DJ oder Admins können den Autoplay-Modus aktivieren oder deaktivieren!",
|
||||
"missingFunctionPerm": "Nur DJ oder Admins können diese Funktion verwenden.",
|
||||
"timeFormatError": "Falsches Zeitformat. Beispiel: 2:42 oder 12:39:31",
|
||||
"lyricsNotFound": "Es wurden keine Songtexte gefunden. Gebe /lyrics <Song Name> <Autor> ein, um die Songtexte zu finden.",
|
||||
"missingTrackInfo": "Einige Track-Informationen fehlen.",
|
||||
"noVoiceChannel": "Dieser Sprachkanal wurde nicht gefunden!",
|
||||
"playlistAddError": "Du darfst Deiner Wiedergabenliste keine aktiven Streams hinzufügen!",
|
||||
"playlistAddError2": "Es gab ein Problem beim Hinzufügen von Tracks zur Wiedergabenliste!",
|
||||
"playlistLimited": "Du hast das Limit erreicht! Du kannst nur noch {0} Songs zu Deiner Wiedergabenliste hinzufügen.",
|
||||
"playlistRepeated": "In Deiner Wiedergabenliste gibt es bereits den gleichen Track!",
|
||||
"playlistAdded": "❤️ **{0}** wurde in der Wiedergabenliste [`{2}`] von {1} hinzugefügt.",
|
||||
"playerDropdown": "Wähle einen Song aus, um zu überspringen ...",
|
||||
"playerFilter": "Wähle einen Filter aus, um ihn anzuwenden ...",
|
||||
"buttonBack": "Zurück",
|
||||
"buttonPause": "Pause",
|
||||
"buttonResume": "Weiter",
|
||||
"buttonSkip": "Überspringen",
|
||||
"buttonLeave": "Verlassen",
|
||||
"buttonLoop": "Endlosschleife",
|
||||
"buttonVolumeUp": "Lauter",
|
||||
"buttonVolumeDown": "Leiser",
|
||||
"buttonVolumeMute": "Stummschalten",
|
||||
"buttonVolumeUnmute": "Stummschaltung aufheben",
|
||||
"buttonAutoPlay": "Autoplay",
|
||||
"buttonShuffle": "Zufall",
|
||||
"buttonForward": "Vorspulen",
|
||||
"buttonRewind": "Zurückspulen",
|
||||
"buttonLyrics": "Liedtexte",
|
||||
"nowplayingDesc": "**Jetzt wird abgespielt:**\n```{0}```",
|
||||
"nowplayingField": "Als nächstes:",
|
||||
"nowplayingLink": "Auf {0} anhören",
|
||||
"connect": "Verbunden mit {0}",
|
||||
"live": "LIVE",
|
||||
"playlistLoad": " 🎶 Die Wiedergabeliste **{0}** mit `{1}` Songs wurde zur Warteschlange hinzugefügt.",
|
||||
"trackLoad": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde zum Abspielen hinzugefügt.\n",
|
||||
"trackLoad_pos": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde in der Warteschlange zu Position **{4}** hinzugefügt.\n",
|
||||
"searchTitle": "Suchabfrage: {0}",
|
||||
"searchDesc": "➥ Plattform: {0} **{1}**\n➥ Ergebnisse: **{2}**\n\n{3}",
|
||||
"searchWait": "Wähle den Song aus, den Du zur Warteschlange hinzufügen möchtest.",
|
||||
"searchTimeout": "Die Suche wurde abgebrochen. Bitte versuche es später erneut.",
|
||||
"searchSuccess": "Der Song wurde zur Warteschlange hinzugefügt.",
|
||||
"queueTitle": "Kommende Warteschlange:",
|
||||
"historyTitle": "Vorherige Warteschlange:",
|
||||
"viewTitle": "Musik-Warteschlange",
|
||||
"viewDesc": "**Jetzt wird abgespielt: [Hier klicken]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Seite: {0}/{1} | Gesamtdauer: {2}",
|
||||
"pauseError": "Der Player ist bereits pausiert.",
|
||||
"pauseVote": "{0} hat für eine Pause des Songs abgestimmt. [{1}/{2}]",
|
||||
"paused": "`{0}` hat den Player pausiert.",
|
||||
"resumeError": "Der Player ist nicht pausiert.",
|
||||
"resumeVote": "{0} hat für das Fortsetzen des Songs abgestimmt. [{1}/{2}]",
|
||||
"resumed": "`{0}` hat den Player fortgesetzt.",
|
||||
"shuffleError": "Füge mehr Songs zur Warteschlange hinzu, bevor Du die Songs mischst.",
|
||||
"shuffleVote": "{0} hat für das Mischen der Warteschlange gestimmt. [{1}/{2}]",
|
||||
"shuffled": "Die Warteschlange wurde gemischt.",
|
||||
"skipError": "Es gibt keine Songs, die übersprungen werden können.",
|
||||
"skipVote": "{0} hat für das Überspringen des Songs abgestimmt. [{1}/{2}]",
|
||||
"skipped": "`{0}` hat den Song übersprungen.",
|
||||
"backVote": "{0} hat für das Überspringen zum vorherigen Song gestimmt. [{1}/{2}]",
|
||||
"backed": "{0} hat zum vorherigen Song übersprungen.",
|
||||
"leaveVote": "{0} hat für das Anhalten des Players gestimmt. [{1}/{2}]",
|
||||
"left": "`{0}` hat den Player angehalten.",
|
||||
"seek": "Der Player wurde auf **{0}** gesetzt.",
|
||||
"repeat": "Der Wiederholungsmodus wurde auf `{0}` gesetzt",
|
||||
"cleared": "Alle Tracks in `{0}` wurden gelöscht",
|
||||
"removed": "`{0}` Tracks wurden aus der Warteschlange entfernt.",
|
||||
"forward": "Der Player wurde auf **{0}** vorgespult.",
|
||||
"rewind": "Der Player wurde auf **{0}** zurückgespult",
|
||||
"replay": "Der aktuelle Song wird wiederholt.",
|
||||
"swapped": "`{0}` und `{1}` wurden ausgetauscht.",
|
||||
"moved": "`{0}` zu `{1}` verschoben",
|
||||
"autoplay": "Der Autoplay-Modus ist jetzt **{0}**",
|
||||
"notdj": "Du bist kein DJ, der aktuelle DJ ist {0}.",
|
||||
"djToMe": "Du kannst den DJ nicht an dich selbst oder einer App übertragen.",
|
||||
"djNotInChannel": "`{0}` ist nicht im Sprachkanal.",
|
||||
"djswap": "Du hast die DJ-Rolle auf `{0}` übertragen.",
|
||||
"voicelinkQueueFull": "Entschuldigung, Du hast das Maximum von `{0}` Tracks in der Warteschlange erreicht.",
|
||||
"voicelinkOutofList": "Bitte gib einen gültigen Track-Index an!",
|
||||
"voicelinkDuplicateTrack": "Entschuldigung, dieser Track ist bereits in der Warteschlange.",
|
||||
"decodeError": "Beim Dekodieren der Datei ist etwas schief gelaufen!",
|
||||
"invalidStartTime": "Ungültige Startzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.",
|
||||
"invalidEndTime": "Ungültige Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.",
|
||||
"invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein.",
|
||||
"setStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen.",
|
||||
"createSongRequestChannel": "Der Song Request Channel ({0}) wurde erstellt! Du kannst jeden Song nach Namen oder URL in diesem Kanal anfordern, ohne den Bot-Präfix verwenden zu müssen."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Beim Ausführen des Befehls ist etwas schiefgelaufen! Bitte versuche es später erneut oder tritt unserem Discord-Server bei, um weiteren Support zu erhalten.",
|
||||
"missingIntents": "Es tut mir leid, dieser Befehl kann nicht ausgeführt werden, da mir der `({0})` Intent fehlt."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "aktiviert",
|
||||
"disabled": "deaktiviert",
|
||||
"live": "LIVE"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Bitte versuche es erneut, nachdem sich die Node wieder verbunden hat.",
|
||||
"noPlaySource": "Ich kann keine abspielbaren Quellen finden!",
|
||||
"noPlayer": "Auf diesem Server wurden kein Player gefunden.",
|
||||
"noTrackPlaying": "Es werden derzeit keine Songs abgespielt",
|
||||
"noTrackFound": "Es wurden keine Songs mit dieser Abfrage gefunden! Bitte gib eine gültige URL an."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Wähle einen Song aus, um zu überspringen ...",
|
||||
"filterSelect": "Wähle einen Filter aus, um ihn anzuwenden ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Zurück",
|
||||
"pause": "Pause",
|
||||
"resume": "Weiter",
|
||||
"skip": "Überspringen",
|
||||
"leave": "Verlassen",
|
||||
"loopOff": "Aus",
|
||||
"loopTrack": "Track",
|
||||
"loopQueue": "Warteschlange",
|
||||
"volumeDown": "Leiser",
|
||||
"volumeMute": "Stummschalten",
|
||||
"volumeUnmute": "Stummschaltung aufheben",
|
||||
"autoPlay": "Autoplay",
|
||||
"shuffle": "Zufall",
|
||||
"forward": "Vorspulen",
|
||||
"rewind": "Zurückspulen",
|
||||
"lyrics": "Liedtexte"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Jetzt wird abgespielt:**\n```{0}```",
|
||||
"nowplayingField": "Als nächstes:",
|
||||
"nowplayingLink": "Auf {0} anhören",
|
||||
"playlistLoad": " 🎶 Die Wiedergabeliste **{0}** mit `{1}` Songs wurde zur Warteschlange hinzugefügt.",
|
||||
"trackLoad": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde zum Abspielen hinzugefügt.\n",
|
||||
"trackLoadPos": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde in der Warteschlange zu Position **{4}** hinzugefügt.\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "Der Player ist bereits pausiert.",
|
||||
"vote": "{0} hat für eine Pause des Songs abgestimmt. [{1}/{2}]",
|
||||
"success": "`{0}` hat den Player pausiert."
|
||||
},
|
||||
"resume": {
|
||||
"error": "Der Player ist nicht pausiert.",
|
||||
"vote": "{0} hat für das Fortsetzen des Songs abgestimmt. [{1}/{2}]",
|
||||
"success": "`{0}` hat den Player fortgesetzt."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Füge mehr Songs zur Warteschlange hinzu, bevor Du die Songs mischst.",
|
||||
"vote": "{0} hat für das Mischen der Warteschlange gestimmt. [{1}/{2}]",
|
||||
"success": "Die Warteschlange wurde gemischt."
|
||||
},
|
||||
"skip": {
|
||||
"error": "Es gibt keine Songs, die übersprungen werden können.",
|
||||
"vote": "{0} hat für das Überspringen des Songs abgestimmt. [{1}/{2}]",
|
||||
"success": "`{0}` hat den Song übersprungen."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} hat für das Überspringen zum vorherigen Song gestimmt. [{1}/{2}]",
|
||||
"success": "{0} hat zum vorherigen Song übersprungen."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} hat für das Anhalten des Players gestimmt. [{1}/{2}]",
|
||||
"success": "`{0}` hat den Player angehalten."
|
||||
},
|
||||
"seek": "Der Player wurde auf **{0}** gesetzt.",
|
||||
"repeat": "Der Wiederholungsmodus wurde auf `{0}` gesetzt",
|
||||
"forward": "Der Player wurde auf **{0}** vorgespult.",
|
||||
"rewind": "Der Player wurde auf **{0}** zurückgespult",
|
||||
"replay": "Der aktuelle Song wird wiederholt.",
|
||||
"autoplay": "Der Autoplay-Modus ist jetzt **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stelle entweder einen zur Verfügung oder trete einem bei.",
|
||||
"alreadyConnected": "Bereits mit einem Sprachkanal verbunden.",
|
||||
"noPermission": "Es tut mir leid, ich bin nicht berechtigt, dem Sprachkanal beizutreten oder darin zu sprechen.",
|
||||
"notInChannel": "{0}, Du musst in {1} sein, um Sprachbefehle nutzen zu können. Bitte betrete den Sprachkanal erneut bei, wenn Du dich im Voice Channel befindest!",
|
||||
"connect": "Verbunden mit {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Es tut mir leid, ich habe keine Berechtigung, einen Song Request Channel zu erstellen.",
|
||||
"missingPosition": "Nur der DJ oder Admins können die Position ändern.",
|
||||
"missingMode": "Nur der DJ oder Admins können den Wiederholungsmodus wechseln.",
|
||||
"missingQueue": "Nur der DJ oder Admins können Tracks aus der Warteschlange entfernen.",
|
||||
"missingAutoPlay": "Nur der DJ oder Admins können den Autoplay-Modus aktivieren oder deaktivieren!",
|
||||
"missingFunction": "Nur DJ oder Admins können diese Funktion verwenden.",
|
||||
"notDj": "Du bist kein DJ, der aktuelle DJ ist {0}.",
|
||||
"djToSelf": "Du kannst den DJ nicht an dich selbst oder einer App übertragen.",
|
||||
"djNotInChannel": "`{0}` ist nicht im Sprachkanal.",
|
||||
"djSwapped": "Du hast die DJ-Rolle auf `{0}` übertragen."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "Kein Sprachpaket gefunden. Bitte wähle ein vorhandenes Sprachpaket aus.",
|
||||
"languageChanged": "Erfolgreich auf das Sprachpaket `{0}` geändert.",
|
||||
"prefixSet": "Erledigt! Mein Präfix ist jetzt `{0}` auf deinem Server. Versuche, `{1}ping` auszuführen, um es zu testen.",
|
||||
"djSet": "Stelle den DJ auf {0}.",
|
||||
"queueModeSet": "Stelle den Warteschlangenmodus auf `{0}` ein.",
|
||||
"mode247": "Der 24/7-Modus wurde erfolgreich `{0}`.",
|
||||
"bypassVote": "Das Abstimmungssystem wurde `{0}`",
|
||||
"volumeSet": "Stelle die Lautstärke auf `{0}`%.",
|
||||
"controllerToggled": "Der Musikcontroller wurde erfolgreich `{0}`",
|
||||
"duplicateTrackToggled": "Du hast das hinzufügen von doppelten Tracks in der Warteschlange `{0}`",
|
||||
"controllerMsgToggled": "Nachrichten vom Musik-Controller wurden erfolgreich `{0}`",
|
||||
"silentMsgToggled": "Sie haben {0} stille Nachrichten.",
|
||||
"resetDone": "`{0}` wurde auf die Standardwerte des Servers zurückgesetzt.",
|
||||
"resetNotFound": "Ungültiger Einstellungsname. Bitte verwende eine der folgenden Einstellungen: {0}"
|
||||
},
|
||||
"menu": "Servereinstellungen | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Grundlegende Informationen:",
|
||||
"value": "```Präfix: {0}`\nSprache: {1}\nMusik-Controller: {2}\nDJ-Rolle: @{3}\nAbstimmungsumgehung: {4}\n24/7 Play: {5}\nStandard Lautstärke: {6}%\nSpielzeit: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Warteschlangeninformationen:",
|
||||
"value": "```Warteschlangenmodus: {0}\nMaximale Songs: {1}\nDoppelte Tracks zulassen: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Sprachstatusinfo:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Berechtigungen:",
|
||||
"value": "```{0} Administrator\n{1} Guild verwalten\n{2} Kanal verwalten\n{3} Manage_Messages```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Bot-Info:",
|
||||
"title2": "Player Info:",
|
||||
"field1": "```Shard-ID: {0}/{1}\nShard-Latenz: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"field2": "```Node: {0} - {1:.3f}s\nPlayer: {2}\nSprachregion: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Wende den Effekt `{0}` Filter an.",
|
||||
"cleared": "Die Soundeffekte wurden gelöscht!",
|
||||
"tagInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwende /cleareffect <Tag>, um sie zu entfernen."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Alle Playlists von {0}",
|
||||
"headers": "ID:,Zeit:,Name:,Tracks:",
|
||||
"footer": "Gebe /playlist play [playlist] ein, um die Playlist in die Warteschlange einzufügen.",
|
||||
"detailTitle": "Playlist-Viewer",
|
||||
"detailDesc": "```Name: {0} [{1}]\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```",
|
||||
"permsValue": "📖 Lesen: ✓ ✍🏽 Schreiben: {0} 🗑️ Entfernen: {1}",
|
||||
"permsValue2": "📖 Lesen: {0}",
|
||||
"trackList": "Tracks",
|
||||
"footer2": "Gesamtdauer: {0}",
|
||||
"playlistSelect": "Wählen Sie eine Wiedergabeliste zur Ansicht ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Die Wiedergabeliste [`{0}`] wurde nicht gefunden. Gebe /playlist view ein, um Deine gesamte Wiedergabeliste anzuzeigen.",
|
||||
"noAccess": "Es tut mir leid! Du bist nicht berechtigt, auf diese Playlist zuzugreifen!",
|
||||
"noTrack": "Es tut uns leid! Es gibt keine Titel in der Wiedergabeliste [`{0}`].",
|
||||
"notAllowed": "Dieser Befehl ist für verknüpfte und freigegebene Wiedergabelisten nicht zulässig.",
|
||||
"nameOverLimit": "Verzeihung, der Name der Playlist darf nicht mehr als 10 Zeichen enthalten.",
|
||||
"sameName": "Verzeihung, dieser Name darf nicht mit Deinem neuen Namen identisch sein.",
|
||||
"deleteDefault": "Du kannst die Standard-Wiedergabeliste nicht löschen.",
|
||||
"limitReached": "Du kannst nicht mehr als `{0}` Wiedergabelisten erstellen!",
|
||||
"exists": "Die Playlist [`{0}`] existiert bereits.",
|
||||
"invalidUrl": "Bitte geben Sie einen gültigen öffentlichen Playlist-Link ein.",
|
||||
"trackLimitReached": "Du hast die Grenze erreicht! Du kannst deiner Playlist nur noch `{0}` Songs hinzufügen.",
|
||||
"playlistLinkNotAllowed": "Du kannst keine Playlist-Links verwenden.",
|
||||
"streamNotAllowed": "Du darfst deiner Playlist keine aktiven Streams hinzufügen.",
|
||||
"positionNotFound": "Die Position `{0}` kann in Deiner Playlist [`{1}`] nicht gefunden werden.",
|
||||
"streaming": "Du darfst Deiner Wiedergabenliste keine aktiven Streams hinzufügen!",
|
||||
"track": "Es gab ein Problem beim Hinzufügen von Tracks zur Wiedergabenliste!",
|
||||
"trackLimited": "Du hast das Limit erreicht! Du kannst nur noch {0} Songs zu Deiner Wiedergabenliste hinzufügen.",
|
||||
"trackRepeated": "In Deiner Wiedergabenliste gibt es bereits den gleichen Track!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Die Playlist [`{0}`] mit `{1}` Songs wurde zur Warteschlange hinzugefügt.",
|
||||
"removed": "Du hast die Wiedergabeliste [`{0}`] entfernt.",
|
||||
"created": "Du hast die Wiedergabeliste `{0}` erstellt. Gebe /playlist view ein, um weitere Informationen zu erhalten.",
|
||||
"renamed": "Du hast `{0}` zu `{1}` umbenannt.",
|
||||
"trackRemoved": "👋 {0} aus {1}s Playlist [{2}] entfernt.",
|
||||
"cleared": "Du hast Deine Wiedergabenliste [`{0}`] erfolgreich gelöscht.",
|
||||
"trackAdded": "❤️ **{0}** wurde in der Wiedergabenliste [`{2}`] von {1} hinzugefügt."
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Entschuldigung, du kannst keine Einladung an dich selber senden.",
|
||||
"sendErrorBot": "Entschuldigung, Du kannst keine Einladung an eine App senden.",
|
||||
"belongs": "Verzeihung, diese Playlist gehört <@{0}>.",
|
||||
"alreadyShared": "Verzeigung, Diese Playlist wurde bereits mit {0} geteilt.",
|
||||
"alreadySent": "Verzeihung, Du hast bereits eine Einladung gesendet.",
|
||||
"noAccount": "{0} hat noch kein Playlist-Konto erstellt.",
|
||||
"invitationSent": "Einladung an {0} gesendet."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Es tut mir leid! Der Posteingang von {0} ist voll.",
|
||||
"noMessages": "Es sind keine Nachrichten in Deinem Posteingang."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "Der Suchbefehl unterstützt keine Links!",
|
||||
"title": "Suchabfrage: {0}",
|
||||
"desc": "➥ Plattform: {0} **{1}**\n➥ Ergebnisse: **{2}**\n\n{3}",
|
||||
"wait": "Wähle den Song aus, den Du zur Warteschlange hinzufügen möchtest.",
|
||||
"success": "Der Song wurde zur Warteschlange hinzugefügt."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "Du hast abgestimmt!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Falsches Zeitformat. Beispiel: 2:42 oder 12:39:31",
|
||||
"invalidStartTime": "Ungültige Startzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.",
|
||||
"invalidEndTime": "Ungültige Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen."
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Es wurden keine Songtexte gefunden. Gebe /lyrics <Song Name> <Autor> ein, um die Songtexte zu finden."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Kommende Warteschlange:",
|
||||
"cleared": "Alle Tracks in `{0}` wurden gelöscht",
|
||||
"removed": "`{0}` Tracks wurden aus der Warteschlange entfernt.",
|
||||
"swapped": "`{0}` und `{1}` wurden ausgetauscht.",
|
||||
"moved": "`{0}` zu `{1}` verschoben"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "Vorherige Warteschlange:",
|
||||
"title": "Musik-Warteschlange",
|
||||
"desc": "**Jetzt wird abgespielt: [Hier klicken]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Entschuldigung, Du hast das Maximum von `{0}` Tracks in der Warteschlange erreicht.",
|
||||
"outOfList": "Bitte gib einen gültigen Track-Index an!",
|
||||
"duplicateTrack": "Entschuldigung, dieser Track ist bereits in der Warteschlange."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "Der Song Request Channel ({0}) wurde erstellt! Du kannst jeden Song nach Namen oder URL in diesem Kanal anfordern, ohne den Bot-Präfix verwenden zu müssen."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Zurück",
|
||||
"next": "Weiter",
|
||||
"play": "Spielen",
|
||||
"share": "Teilen",
|
||||
"export": "Exportieren",
|
||||
"delete": "Löschen",
|
||||
"page": {
|
||||
"title": "Seitenzahl eingeben",
|
||||
"field": "Seitenzahl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
445
langs/EN.json
445
langs/EN.json
@@ -1,172 +1,275 @@
|
||||
{
|
||||
"unknownException": "⚠️ Something wrong while running the command! Please try again later or join our discord server for further support.",
|
||||
"enabled": "enabled",
|
||||
"disabled": "disabled",
|
||||
"nodeReconnect": "Please try again! After the node reconnects.",
|
||||
"noChannel": "No voice channel to connect. Please either provide one or join one.",
|
||||
"alreadyConnected": "Already connected to a voice channel.",
|
||||
"noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.",
|
||||
"noCreatePermission": "Sorry! i don't have permissions to create a song requesting channel.",
|
||||
"noPlaySource": "Can't find any playable sources!",
|
||||
"noPlayer": "No player has been found on this server.",
|
||||
"notVote": "This command requires your vote! Type `/vote` for more info.",
|
||||
"missingIntents": "Sorry, this command cannot be executed because the bot is missing the required request intent: `({0})`.",
|
||||
"languageNotFound": "No language pack found! please select an existing language pack.",
|
||||
"changedLanguage": "Successfully changed to `{0}` language pack.",
|
||||
"setPrefix": "Done! My prefix in your server is now `{0}`. Try running `{1}ping` to test it out.",
|
||||
"setDJ": "Set the dj to {0}.",
|
||||
"setQueue": "Set queue mode to `{0}`.",
|
||||
"247": "24/7 mode is now `{0}`.",
|
||||
"bypassVote": "You have `{0}` voting system.",
|
||||
"setVolume": "Set the volume to `{0}`%",
|
||||
"toggleController": "You now have `{0}` the music controller.",
|
||||
"toggleDuplicateTrack": "You have now `{0}` duplicate track prevention.",
|
||||
"toggleControllerMsg": "You now have `{0}` messages from the music controller.",
|
||||
"toggleSilentMsg": "You have {0} silent messaging.",
|
||||
"settingsMenu": "Server Settings | {0}",
|
||||
"settingsTitle": "❤️ Basic Info:",
|
||||
"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}\nDuplicate Track: {2}```",
|
||||
"settingsTitle3": "🎤 Voice Status Info:",
|
||||
"settingsPermTitle": "✨ Permissions:",
|
||||
"settingsPermValue": "```{0} Administrator\n{1} Manage_Guild\n{2} Manage_Channel\n{3} Manage_Messages```",
|
||||
"pingTitle1": "Bot Info:",
|
||||
"pingTitle2": "Player Info:",
|
||||
"pingField1": "```Shard ID: {0}/{1}\nShard Latency: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"pingField2": "```Node: {0} - {1:.3f}s\nPlayers: {2}\nVoice Region: {3}```",
|
||||
"addEffect": "Applied the `{0}` effect.",
|
||||
"clearEffect": "The sound effects have been cleared!",
|
||||
"filterTagAlreadyInUse": "This sound effect is already in use! Please use /cleareffect <Tag> to remove it.",
|
||||
"playlistViewTitle": "📜 All of {0}'s Playlists",
|
||||
"playlistViewHeaders": "ID:,Time:,Name:,Tracks:",
|
||||
"playlistFooter": "Type /playlist play [playlist] to add a playlist the into the queue.",
|
||||
"playlistNotFound": "Playlist [`{0}`] not found. Type /playlist view to view all your playlist.",
|
||||
"playlistNotAccess": "Sorry! You are not allowed to access this playlist!",
|
||||
"playlistNoTrack": "Sorry! There are no tracks in the playlist [`{0}`].",
|
||||
"playlistNotAllow": "This command is not allowed on linked and shared playlists.",
|
||||
"playlistPlay": "Added the playlist [`{0}`] with `{1}` songs to the queue.",
|
||||
"playlistOverText": "Sorry! The name of the playlist cannot exceed 10 characters.",
|
||||
"playlistSameName": "Sorry! Please select a new name for the playlist.",
|
||||
"playlistDeleteError": "You are not allowed to delete the default playlist.",
|
||||
"playlistRemove": "You have removed playlist [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Sorry! You are not able to send an invitation to yourself.",
|
||||
"playlistSendErrorBot": "Sorry! You are not able to send an invitation to a bot.",
|
||||
"playlistBelongs": "Sorry! This playlist belongs to <@{0}>.",
|
||||
"playlistShare": "Sorry! This playlist has been shared with {0}.",
|
||||
"playlistSent": "Sorry! You have already sent an invitation to that user before.",
|
||||
"noPlaylistAcc": "{0} didn't create a playlist account.",
|
||||
"overPlaylistCreation": "You cannot create more than `{0}` playlists!",
|
||||
"playlistExists": "Playlist [`{0}`] already exists.",
|
||||
"playlistNotInvalidUrl": "Please enter a valid public spotify or youtube playlist link.",
|
||||
"playlistCreated": "You have created `{0}` playlist. Type /playlist view for more info.",
|
||||
"playlistRenamed": "You have renamed `{0}` to `{1}`.",
|
||||
"playlistLimitTrack": "You have reached the limit! You can only add `{0}` songs to your playlist.",
|
||||
"playlistPlaylistLink": "You are not allowed to use playlist links.",
|
||||
"playlistStream": "You are not allowed to add streaming videos to your playlist.",
|
||||
"playlistPositionNotFound": "Can't find position `{0}` from your playlist [`{1}`]!",
|
||||
"playlistRemoved": "👋 Removed **{0}** from {1}'s playlist [`{2}`].",
|
||||
"playlistClear": "You have successfully clear your playlist [`{0}`].",
|
||||
"playlistView": "Playlist Viewer",
|
||||
"playlistViewDesc": "```Name | ID: {0} | {1}\nTotal Tracks: {2}\nOwner: {3}\nType: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Read: ✓ ✍🏽 Write: {0} 🗑️ Remove: {1}",
|
||||
"playlistViewPermsValue2": "📖 Read: {0}",
|
||||
"playlistViewTrack": "Tracks",
|
||||
"playlistViewPage": "Page: {0}/{1} | Total Duration: {2}",
|
||||
"inboxFull": "Sorry! {0}'s inbox is full.",
|
||||
"inboxNoMsg": "There are no messages in your inbox.",
|
||||
"invitationSent": "Invitation sent to {0}.",
|
||||
"notInChannel": "{0}, you must be in {1} to use voice commands. Please rejoin if you are in voice!",
|
||||
"noTrackPlaying": "There are no songs playing right now",
|
||||
"noTrackFound": "No songs were found with that query! Please provide a valid url.",
|
||||
"noLinkSupport": "Search command does not support links!",
|
||||
"voted": "You have voted!",
|
||||
"missingPosPerm": "Only the DJ or admins may change the position.",
|
||||
"missingModePerm": "Only the DJ or admins may switch loop mode.",
|
||||
"missingQueuePerm": "Only the DJ or admins may remove track from the queue.",
|
||||
"missingAutoPlayPerm": "Only the DJ or admins can enable or disable autoplay mode!",
|
||||
"missingFunctionPerm": "Only DJ or Admin can use this function.",
|
||||
"timeFormatError": "Incorrect time format. Example: 2:42 or 12:39:31",
|
||||
"lyricsNotFound": "Lyrics not found. Type /lyrics <Song Name> <Author> to find the lyrics.",
|
||||
"missingTrackInfo": "Some track details are missing.",
|
||||
"noVoiceChannel": "Voice Channel Not Found!",
|
||||
"playlistAddError": "You are not allowed to add streaming videos to your playlist!",
|
||||
"playlistAddError2": "There was a problem adding tracks to the playlist!",
|
||||
"playlistLimited": "You have reached the limit! You can only add {0} songs to your playlist.",
|
||||
"playlistRepeated": "This track is already in your playlist!",
|
||||
"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",
|
||||
"buttonResume": "Resume",
|
||||
"buttonSkip": "Skip",
|
||||
"buttonLeave": "Leave",
|
||||
"buttonLoop": "Loop",
|
||||
"buttonVolumeUp": "Volume Up",
|
||||
"buttonVolumeDown": "Volume Down",
|
||||
"buttonVolumeMute": "Mute",
|
||||
"buttonVolumeUnmute": "Unmute",
|
||||
"buttonAutoPlay": "Autoplay",
|
||||
"buttonShuffle": "Shuffle",
|
||||
"buttonForward": "Forward",
|
||||
"buttonRewind": "Rewind",
|
||||
"buttonLyrics": "Lyrics",
|
||||
"nowplayingDesc": "**Now Playing:**\n```{0}```",
|
||||
"nowplayingField": "Up Next:",
|
||||
"nowplayingLink": "Listen on {0}",
|
||||
"connect": "Connected to {0}",
|
||||
"live": "LIVE",
|
||||
"playlistLoad": " 🎶 Added the playlist **{0}** with `{1}` songs to the queue.",
|
||||
"trackLoad": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to begin playing.\n",
|
||||
"trackLoad_pos": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to the queue at position **{4}**\n",
|
||||
"searchTitle": "Search Query: {0}",
|
||||
"searchDesc": "➥ Platform: {0} **{1}**\n➥ Results: **{2}**\n\n{3}",
|
||||
"searchWait": "Select the song you want to add to the queue.",
|
||||
"searchTimeout": "Search timed out. Please try again later.",
|
||||
"searchSuccess": "Song added to the queue.",
|
||||
"queueTitle": "Upcoming Queue:",
|
||||
"historyTitle": "History Queue:",
|
||||
"viewTitle": "Music Queue",
|
||||
"viewDesc": "**Now Playing: [Click Me]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Page: {0}/{1} | Total Duration: {2}",
|
||||
"pauseError": "The player is already paused.",
|
||||
"pauseVote": "{0} has voted to pause the song. [{1}/{2}]",
|
||||
"paused": "`{0}` has paused the player.",
|
||||
"resumeError": "The player is not paused.",
|
||||
"resumeVote": "{0} has voted to resume the song. [{1}/{2}]",
|
||||
"resumed": "`{0}` has resumed the player.",
|
||||
"shuffleError": "Add more songs to the queue before shuffling.",
|
||||
"shuffleVote": "{0} has voted to shuffle the queue. [{1}/{2}]",
|
||||
"shuffled": "The queue is shuffled.",
|
||||
"skipError": "There are no songs too skip to.",
|
||||
"skipVote": "{0} has voted to skip the song. [{1}/{2}]",
|
||||
"skipped": "`{0}` has skipped the song.",
|
||||
"backVote": "{0} has voted to skip to the previous song. [{1}/{2}]",
|
||||
"backed": "`{0}` has skipping to previous song.",
|
||||
"leaveVote": "{0} has voted to stop the player. [{1}/{2}]",
|
||||
"left": "`{0}` has stopped the player.",
|
||||
"seek": "Set the player to **{0}**",
|
||||
"repeat": "The repeat mode has been set to `{0}`",
|
||||
"cleared": "Cleared all tracks in the `{0}`",
|
||||
"removed": "`{0}` tracks were removed from the queue.",
|
||||
"forward": "Forward the player to **{0}**",
|
||||
"rewind": "Rewound the player to **{0}**",
|
||||
"replay": "Replaying the current song.",
|
||||
"swapped": "`{0}` and `{1}` have been swapped",
|
||||
"moved": "Moved `{0}` to `{1}`",
|
||||
"autoplay": "Autoplay mode is now **{0}**",
|
||||
"notdj": "You are not DJ. The current DJ is {0}.",
|
||||
"djToMe": "You cannot transfer role of DJ to yourself or a bot.",
|
||||
"djNotInChannel": "`{0}` is not in the voice channel.",
|
||||
"djswap": "You have transferred the role of dj to `{0}`.",
|
||||
"voicelinkQueueFull": "Sorry, you have reached the maximum of `{0}` tracks in the queue!",
|
||||
"voicelinkOutofList": "Please provide a valid track index!",
|
||||
"voicelinkDuplicateTrack": "Sorry, this track is already in the queue.",
|
||||
"decodeError": "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.",
|
||||
"createSongRequestChannel": "A song request channel ({0}) has been created! You can start requesting any song by name or URL in that channel, without needing to use the bot prefix."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Something wrong while running the command! Please try again later or join our discord server for further support.",
|
||||
"missingIntents": "Sorry, this command cannot be executed because the bot is missing the required request intent: `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "enabled",
|
||||
"disabled": "disabled",
|
||||
"live": "LIVE"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Please try again! After the node reconnects.",
|
||||
"noPlaySource": "Can't find any playable sources!",
|
||||
"noPlayer": "No player has been found on this server.",
|
||||
"noTrackPlaying": "There are no songs playing right now",
|
||||
"noTrackFound": "No songs were found with that query! Please provide a valid url."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Select a song to skip to ...",
|
||||
"filterSelect": "Select a filter to apply ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Back",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
"skip": "Skip",
|
||||
"leave": "Leave",
|
||||
"loopOff": "Off",
|
||||
"loopTrack": "Track",
|
||||
"loopQueue": "Queue",
|
||||
"volumeUp": "Volume Up",
|
||||
"volumeDown": "Volume Down",
|
||||
"volumeMute": "Mute",
|
||||
"volumeUnmute": "Unmute",
|
||||
"autoPlay": "Autoplay",
|
||||
"shuffle": "Shuffle",
|
||||
"forward": "Forward",
|
||||
"rewind": "Rewind",
|
||||
"lyrics": "Lyrics"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Now Playing:**\n```{0}```",
|
||||
"nowplayingField": "Up Next:",
|
||||
"nowplayingLink": "Listen on {0}",
|
||||
"playlistLoad": " 🎶 Added the playlist **{0}** with `{1}` songs to the queue.",
|
||||
"trackLoad": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to begin playing.\n",
|
||||
"trackLoadPos": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to the queue at position **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "The player is already paused.",
|
||||
"vote": "{0} has voted to pause the song. [{1}/{2}]",
|
||||
"success": "`{0}` has paused the player."
|
||||
},
|
||||
"resume": {
|
||||
"error": "The player is not paused.",
|
||||
"vote": "{0} has voted to resume the song. [{1}/{2}]",
|
||||
"success": "`{0}` has resumed the player."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Add more songs to the queue before shuffling.",
|
||||
"vote": "{0} has voted to shuffle the queue. [{1}/{2}]",
|
||||
"success": "The queue is shuffled."
|
||||
},
|
||||
"skip": {
|
||||
"error": "There are no songs too skip to.",
|
||||
"vote": "{0} has voted to skip the song. [{1}/{2}]",
|
||||
"success": "`{0}` has skipped the song."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} has voted to skip to the previous song. [{1}/{2}]",
|
||||
"success": "`{0}` has skipping to previous song."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} has voted to stop the player. [{1}/{2}]",
|
||||
"success": "`{0}` has stopped the player."
|
||||
},
|
||||
"seek": "Set the player to **{0}**",
|
||||
"repeat": "The repeat mode has been set to `{0}`",
|
||||
"forward": "Forward the player to **{0}**",
|
||||
"rewind": "Rewound the player to **{0}**",
|
||||
"replay": "Replaying the current song.",
|
||||
"autoplay": "Autoplay mode is now **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "No voice channel to connect. Please either provide one or join one.",
|
||||
"alreadyConnected": "Already connected to a voice channel.",
|
||||
"noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.",
|
||||
"notInChannel": "{0}, you must be in {1} to use voice commands. Please rejoin if you are in voice!",
|
||||
"connect": "Connected to {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "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."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Sorry! i don't have permissions to create a song requesting channel.",
|
||||
"missingPosition": "Only the DJ or admins may change the position.",
|
||||
"missingMode": "Only the DJ or admins may switch loop mode.",
|
||||
"missingQueue": "Only the DJ or admins may remove track from the queue.",
|
||||
"missingAutoPlay": "Only the DJ or admins can enable or disable autoplay mode!",
|
||||
"missingFunction": "Only DJ or Admin can use this function.",
|
||||
"notDj": "You are not DJ. The current DJ is {0}.",
|
||||
"djToSelf": "You cannot transfer role of DJ to yourself or a bot.",
|
||||
"djNotInChannel": "`{0}` is not in the voice channel.",
|
||||
"djSwapped": "You have transferred the role of dj to `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "No language pack found! please select an existing language pack.",
|
||||
"languageChanged": "Successfully changed to `{0}` language pack.",
|
||||
"prefixSet": "Done! My prefix in your server is now `{0}`. Try running `{1}ping` to test it out.",
|
||||
"djSet": "Set the dj to {0}.",
|
||||
"queueModeSet": "Set queue mode to `{0}`.",
|
||||
"mode247": "24/7 mode is now `{0}`.",
|
||||
"bypassVote": "You have `{0}` voting system.",
|
||||
"volumeSet": "Set the volume to `{0}`%",
|
||||
"controllerToggled": "You now have `{0}` the music controller.",
|
||||
"duplicateTrackToggled": "You have now `{0}` duplicate track prevention.",
|
||||
"controllerMsgToggled": "You now have `{0}` messages from the music controller.",
|
||||
"silentMsgToggled": "You have {0} silent messaging.",
|
||||
"resetDone": "Reset `{0}` to server defaults.",
|
||||
"resetNotFound": "Invalid setting name. Please use one of the following: {0}"
|
||||
},
|
||||
"menu": "Server Settings | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Basic Info:",
|
||||
"value": "```Prefix: {0}\nLanguage: {1}\nMusic Controller: {2}\nDJ Role: @{3}\nVote Bypass: {4}\n24/7: {5}\nDefault Volume: {6}%\nPlay Time: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Queue info:",
|
||||
"value": "```Queue Mode: {0}\nMax Song: {1}\nDuplicate Track: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Voice Status Info:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Permissions:",
|
||||
"value": "```{0} Administrator\n{1} Manage_Guild\n{2} Manage_Channel\n{3} Manage_Messages```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Bot Info:",
|
||||
"title2": "Player Info:",
|
||||
"field1": "```Shard ID: {0}/{1}\nShard Latency: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"field2": "```Node: {0} - {1:.3f}s\nPlayers: {2}\nVoice Region: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Applied the `{0}` effect.",
|
||||
"cleared": "The sound effects have been cleared!",
|
||||
"tagInUse": "This sound effect is already in use! Please use /cleareffect <Tag> to remove it."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "All of {0}'s Playlists",
|
||||
"headers": "ID:,Time:,Name:,Tracks:",
|
||||
"footer": "Type /playlist play [playlist] to add a playlist the into the queue.",
|
||||
"detailTitle": "Playlist Viewer",
|
||||
"detailDesc": "```Name: {0} [{1}]\nTotal Tracks: {2}\nOwner: {3}\nType: {4}\n```",
|
||||
"permsValue": "📖 Read: ✓ ✍🏽 Write: {0} 🗑️ Remove: {1}",
|
||||
"permsValue2": "📖 Read: {0}",
|
||||
"trackList": "Tracks",
|
||||
"footer2": "Total Duration: {0}",
|
||||
"playlistSelect": "Select a playlist to view ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Playlist [`{0}`] not found. Type /playlist view to view all your playlist.",
|
||||
"noAccess": "Sorry! You are not allowed to access this playlist!",
|
||||
"noTrack": "Sorry! There are no tracks in the playlist [`{0}`].",
|
||||
"notAllowed": "This command is not allowed on linked and shared playlists.",
|
||||
"nameOverLimit": "Sorry! The name of the playlist cannot exceed 10 characters.",
|
||||
"sameName": "Sorry! Please select a new name for the playlist.",
|
||||
"deleteDefault": "You are not allowed to delete the default playlist.",
|
||||
"limitReached": "You cannot create more than `{0}` playlists!",
|
||||
"exists": "Playlist [`{0}`] already exists.",
|
||||
"invalidUrl": "Please enter a valid public playlist link.",
|
||||
"trackLimitReached": "You have reached the limit! You can only add `{0}` songs to your playlist.",
|
||||
"playlistLinkNotAllowed": "You are not allowed to use playlist links.",
|
||||
"streamNotAllowed": "You are not allowed to add streaming videos to your playlist.",
|
||||
"positionNotFound": "Can't find position `{0}` from your playlist [`{1}`]!",
|
||||
"streaming": "You are not allowed to add streaming videos to your playlist!",
|
||||
"track": "There was a problem adding tracks to the playlist!",
|
||||
"trackLimited": "You have reached the limit! You can only add {0} songs to your playlist.",
|
||||
"trackRepeated": "This track is already in your playlist!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Added the playlist [`{0}`] with `{1}` songs to the queue.",
|
||||
"removed": "You have removed playlist [`{0}`].",
|
||||
"created": "You have created `{0}` playlist. Type /playlist view for more info.",
|
||||
"renamed": "You have renamed `{0}` to `{1}`.",
|
||||
"trackRemoved": "👋 Removed **{0}** from {1}'s playlist [`{2}`].",
|
||||
"cleared": "You have successfully clear your playlist [`{0}`].",
|
||||
"trackAdded": "❤️ Added **{0}** into {1}'s playlist [`{2}`]!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Sorry! You are not able to send an invitation to yourself.",
|
||||
"sendErrorBot": "Sorry! You are not able to send an invitation to a bot.",
|
||||
"belongs": "Sorry! This playlist belongs to <@{0}>.",
|
||||
"alreadyShared": "Sorry! This playlist has been shared with {0}.",
|
||||
"alreadySent": "Sorry! You have already sent an invitation to that user before.",
|
||||
"noAccount": "{0} didn't create a playlist account.",
|
||||
"invitationSent": "Invitation sent to {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Sorry! {0}'s inbox is full.",
|
||||
"noMessages": "There are no messages in your inbox."
|
||||
},
|
||||
"permissions": {
|
||||
"granted": "Granted **{1}** permission to {0} for playlist [`{2}`].",
|
||||
"revoked": "Revoked **{1}** permission from {0} for playlist [`{2}`].",
|
||||
"alreadyGranted": "{0} already has **{1}** permission for this playlist.",
|
||||
"notGranted": "{0} does not have **{1}** permission for this playlist.",
|
||||
"cannotModifySelf": "You cannot modify your own permissions.",
|
||||
"invalidPermission": "Invalid permission type. Must be: read, write, or remove.",
|
||||
"invalidAction": "Invalid action. Must be: grant or revoke."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "Search command does not support links!",
|
||||
"title": "Search Query: {0}",
|
||||
"desc": "➥ Platform: {0} **{1}**\n➥ Results: **{2}**\n\n{3}",
|
||||
"wait": "Select the song you want to add to the queue.",
|
||||
"success": "Song added to the queue."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "You have voted!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Incorrect time format. Example: 2:42 or 12:39:31",
|
||||
"invalidStartTime": "Invalid start time, it must be between `00:00` and `{0}`",
|
||||
"invalidEndTime": "Invalid end time, it must be between `00:00` and `{0}`"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Lyrics not found. Type /lyrics <Song Name> <Author> to find the lyrics."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Upcoming Queue:",
|
||||
"cleared": "Cleared all tracks in the `{0}`",
|
||||
"removed": "`{0}` tracks were removed from the queue.",
|
||||
"swapped": "`{0}` and `{1}` have been swapped",
|
||||
"moved": "Moved `{0}` to `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "History Queue:",
|
||||
"title": "Music Queue",
|
||||
"desc": "**Now Playing: [Click Me]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Sorry, you have reached the maximum of `{0}` tracks in the queue!",
|
||||
"outOfList": "Please provide a valid track index!",
|
||||
"duplicateTrack": "Sorry, this track is already in the queue."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "A song request channel ({0}) has been created! You can start requesting any song by name or URL in that channel, without needing to use the bot prefix."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Prev",
|
||||
"next": "Next",
|
||||
"play": "Play",
|
||||
"share": "Share",
|
||||
"export": "Export",
|
||||
"delete": "Delete",
|
||||
"page": {
|
||||
"title": "Enter Page Number",
|
||||
"field": "Page Number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/ES.json
436
langs/ES.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ ¡Algo salió mal al ejecutar el comando! Por favor, intente de nuevo más tarde o únase a nuestro servidor de Discord para obtener más soporte.",
|
||||
"enabled": "habilitado",
|
||||
"disabled": "deshabilitado",
|
||||
"nodeReconnect": "¡Por favor, inténtelo de nuevo! Después de que el nodo se reconecte.",
|
||||
"noChannel": "No hay canal de voz al que conectarse. Por favor, proporcione uno o únase a uno.",
|
||||
"alreadyConnected": "Ya conectado a un canal de voz.",
|
||||
"noPermission": "¡Lo siento! No tengo permisos para unirme o hablar en su canal de voz.",
|
||||
"noCreatePermission": "¡Lo siento! No tengo permisos para crear un canal de solicitud de canciones.",
|
||||
"noPlaySource": "¡No se puede encontrar ninguna fuente reproducible!",
|
||||
"noPlayer": "No se ha encontrado ningún reproductor en este servidor.",
|
||||
"notVote": "¡Este comando requiere su voto! Escriba `/vote` para obtener más información.",
|
||||
"missingIntents": "Lo siento, este comando no se puede ejecutar porque el bot carece de la intención de solicitud requerida: `({0})`.",
|
||||
"languageNotFound": "¡No se encontró paquete de idioma! por favor seleccione un paquete de idioma existente.",
|
||||
"changedLanguage": "Cambiado con éxito al paquete de idioma `{0}`.",
|
||||
"setPrefix": "¡Listo! Mi prefijo en tu servidor ahora es `{0}`. Intenta ejecutar `{1}ping` para probarlo.",
|
||||
"setDJ": "Establecer el DJ a {0}.",
|
||||
"setQueue": "Establecer el modo de cola en `{0}`.",
|
||||
"247": "Ahora tienes `{0}` modo 24/7.",
|
||||
"bypassVote": "Ahora tienes `{0}` sistema de votación.",
|
||||
"setVolume": "Ajustar el volumen a `{0}`%",
|
||||
"toggleController": "Ahora tienes `{0}` el controlador de música.",
|
||||
"toggleDuplicateTrack": "Ahora tienes `{0}` para evitar que se agreguen pistas duplicadas a la cola.",
|
||||
"toggleControllerMsg": "Ahora tiene `{0}` mensajes del controlador de música.",
|
||||
"toggleSilentMsg": "Tienes {0} mensajes silenciosos.",
|
||||
"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}```",
|
||||
"settingsTitle2": "🔗 Información de la cola:",
|
||||
"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```",
|
||||
"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}```",
|
||||
"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}",
|
||||
"playlistViewHeaders": "ID:,Tiempo:,Nombre:,Pistas:",
|
||||
"playlistFooter": "Escriba /playlist play [playlist] para agregar la lista de reproducción a la cola.",
|
||||
"playlistNotFound": "Lista de reproducción [`{0}`] no encontrada. Escriba /playlist view para ver todas sus listas de reproducción.",
|
||||
"playlistNotAccess": "¡Lo siento! No está autorizado para acceder a esta lista de reproducción.",
|
||||
"playlistNoTrack": "¡Lo siento! No hay pistas en la lista de reproducción [`{0}`].",
|
||||
"playlistNotAllow": "Este comando no está permitido en listas de reproducción vinculadas y compartidas.",
|
||||
"playlistPlay": "Se agregó la lista de reproducción [`{0}`] con `{1}` canciones a la cola.",
|
||||
"playlistOverText": "¡Lo siento! El nombre de la lista de reproducción no puede superar los 10 caracteres.",
|
||||
"playlistSameName": "¡Lo siento! Este nombre no puede ser igual que su nuevo nombre.",
|
||||
"playlistDeleteError": "No está autorizado para eliminar la lista de reproducción predeterminada.",
|
||||
"playlistRemove": "Ha eliminado la lista de reproducción [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Lo siento! No puede enviar una invitación a sí mismo.",
|
||||
"playlistSendErrorBot": "Lo siento! No puede enviar una invitación a un bot.",
|
||||
"playlistBelongs": "Lo siento! Esta lista de reproducción pertenece a <@{0}>.",
|
||||
"playlistShare": "Lo siento! Esta lista de reproducción se ha compartido con `{0}`.",
|
||||
"playlistSent": "Lo siento! Ya ha enviado una invitación antes.",
|
||||
"noPlaylistAcc": "`{0}` no ha creado una cuenta de lista de reproducción.",
|
||||
"overPlaylistCreation": "¡No puede crear más de `{0}` listas de reproducción!",
|
||||
"playlistExists": "La lista de reproducción [`{0}`] ya existe.",
|
||||
"playlistNotInvalidUrl": "Ingrese un enlace válido o un enlace público de lista de reproducción de Spotify o YouTube.",
|
||||
"playlistCreated": "Ha creado una lista de reproducción llamada `{0}`. Escriba /playlist view para obtener más información.",
|
||||
"playlistRenamed": "Ha cambiado el nombre de `{0}` a `{1}`.",
|
||||
"playlistLimitTrack": "¡Ha alcanzado el límite! Solo puede agregar `{0}` canciones a su lista de reproducción.",
|
||||
"playlistPlaylistLink": "No está autorizado para usar enlaces de lista de reproducción.",
|
||||
"playlistStream": "No está autorizado para agregar videos de transmisión a su lista de reproducción.",
|
||||
"playlistPositionNotFound": "No se puede encontrar la posición {0} en su lista de reproducción [`{1}`]!",
|
||||
"playlistRemoved": "👋 Se quitó {0} de la lista de reproducción de {1} [`{2}`].",
|
||||
"playlistClear": "Ha borrado con éxito su lista de reproducción [`{0}`].",
|
||||
"playlistView": "Visor de lista de reproducción",
|
||||
"playlistViewDesc": "```Nombre | ID: {0} | {1}\nTotal de pistas: {2}\nPropietario: {3}\nTipo: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Leer: ✓ ✍🏽 Escribir: {0} 🗑️ Eliminar: {1}",
|
||||
"playlistViewPermsValue2": "📖 Leer: {0}",
|
||||
"playlistViewTrack": "Pistas",
|
||||
"playlistViewPage": "Página: {0}/{1} | Duración total: {2}",
|
||||
"inboxFull": "¡Lo siento! La bandeja de entrada de {0} está llena.",
|
||||
"inboxNoMsg": "No hay mensajes en su bandeja de entrada.",
|
||||
"invitationSent": "Se ha enviado una invitación a {0}.",
|
||||
"notInChannel": "{0}, debes estar en {1} para usar comandos de voz. ¡Vuelve a unirte si estás en voz!",
|
||||
"noTrackPlaying": "Actualmente no hay canciones reproduciéndose.",
|
||||
"noTrackFound": "No se encontraron canciones con esa consulta. Proporcione una URL válida.",
|
||||
"noLinkSupport": "¡El comando de búsqueda no admite enlaces!",
|
||||
"voted": "¡Has votado!",
|
||||
"missingPosPerm": "Solo el DJ o los administradores pueden cambiar la posición.",
|
||||
"missingModePerm": "Solo el DJ o los administradores pueden cambiar el modo de bucle.",
|
||||
"missingQueuePerm": "Solo el DJ o los administradores pueden eliminar una canción de la cola.",
|
||||
"missingAutoPlayPerm": "¡Solo el DJ o los administradores pueden habilitar o deshabilitar el modo de reproducción automática!",
|
||||
"missingFunctionPerm": "Solo el DJ o los administradores pueden usar esta función.",
|
||||
"timeFormatError": "Formato de tiempo incorrecto. Ejemplo: 2:42 o 12:39:31",
|
||||
"lyricsNotFound": "No se encontraron letras. Escriba /lyrics <Nombre de la canción> <Autor> para buscar las letras.",
|
||||
"missingTrackInfo": "Falta información de la canción.",
|
||||
"noVoiceChannel": "¡Canal de voz no encontrado!",
|
||||
"playlistAddError": "No está autorizado para agregar videos de transmisión a su lista de reproducción.",
|
||||
"playlistAddError2": "Hubo un problema al agregar canciones a la lista de reproducción.",
|
||||
"playlistLimited": "¡Ha alcanzado el límite! Solo puede agregar {0} canciones a su lista de reproducción.",
|
||||
"playlistRepeated": "¡Ya hay una canción igual en su lista de reproducción!",
|
||||
"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",
|
||||
"buttonResume": "Reanudar",
|
||||
"buttonSkip": "Saltar",
|
||||
"buttonLeave": "Salir",
|
||||
"buttonLoop": "Bucle",
|
||||
"buttonVolumeUp": "Subir Volumen",
|
||||
"buttonVolumeDown": "Bajar Volumen",
|
||||
"buttonVolumeMute": "Silenciar",
|
||||
"buttonVolumeUnmute": "Activar Sonido",
|
||||
"buttonAutoPlay": "Reproducción Automática",
|
||||
"buttonShuffle": "Aleatorio",
|
||||
"buttonForward": "Adelante",
|
||||
"buttonRewind": "Atrás",
|
||||
"buttonLyrics": "Letras",
|
||||
"nowplayingDesc": "**Reproduciendo ahora:**\n```{0}```",
|
||||
"nowplayingField": "A continuación:",
|
||||
"nowplayingLink": "Escucha en {0}",
|
||||
"connect": "Conectado a {0}",
|
||||
"live": "EN VIVO",
|
||||
"playlistLoad": " 🎶 Se agregó la lista de reproducción {0} con {1} canciones a la cola.",
|
||||
"trackLoad": "Se agregó **[{0}](<{1}>)** de {2} ({3}) para comenzar a reproducir.\n",
|
||||
"trackLoad_pos": "Se agregó **[{0}](<{1}>)** de {2} ({3}) a la cola en la posición {4}\n",
|
||||
"searchTitle": "Consulta de búsqueda: {0}",
|
||||
"searchDesc": "➥ Plataforma: {0} **{1}**\n➥ Resultados: **{2}**\n\n{3}",
|
||||
"searchWait": "Seleccione la canción que desea agregar a la cola.",
|
||||
"searchTimeout": "La búsqueda ha expirado. Por favor, inténtelo de nuevo más tarde.",
|
||||
"searchSuccess": "Canción agregada a la cola.",
|
||||
"queueTitle": "Cola de próximas canciones:",
|
||||
"historyTitle": "Cola de historial:",
|
||||
"viewTitle": "Cola de música",
|
||||
"viewDesc": "**Reproduciendo ahora: [Haga clic aquí]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Página: {0}/{1} | Duración total: {2}",
|
||||
"pauseError": "El reproductor ya está en pausa.",
|
||||
"pauseVote": "{0} ha votado para pausar la canción. [{1}/{2}]",
|
||||
"paused": "`{0}` ha pausado el reproductor.",
|
||||
"resumeError": "El reproductor no está en pausa.",
|
||||
"resumeVote": "{0} ha votado para reanudar la canción. [{1}/{2}]",
|
||||
"resumed": "`{0}` ha reanudado el reproductor.",
|
||||
"shuffleError": "Agregue más canciones a la cola antes de barajar.",
|
||||
"shuffleVote": "{0} ha votado para barajar la cola. [{1}/{2}]",
|
||||
"shuffled": "La cola ha sido barajada.",
|
||||
"skipError": "No hay canciones para saltar.",
|
||||
"skipVote": "{0} ha votado para saltar la canción. [{1}/{2}]",
|
||||
"skipped": "`{0}` ha saltado la canción.",
|
||||
"backVote": "{0} ha votado para volver a la canción anterior. [{1}/{2}]",
|
||||
"backed": "`{0}` ha vuelto a la canción anterior.",
|
||||
"leaveVote": "{0} ha votado para detener el reproductor. [{1}/{2}]",
|
||||
"left": "`{0}` ha detenido el reproductor.",
|
||||
"seek": "El reproductor se estableció en **{0}**",
|
||||
"repeat": "El modo de repetición se ha establecido en `{0}`",
|
||||
"cleared": "Se borraron todas las canciones de `{0}`",
|
||||
"removed": "Se eliminaron `{0}` canciones de la cola.",
|
||||
"forward": "Avanzar el reproductor a **{0}**",
|
||||
"rewind": "Rebobinar el reproductor a **{0}**",
|
||||
"replay": "Reproduciendo de nuevo la canción actual.",
|
||||
"swapped": "Se intercambiaron `{0}` y `{1}`",
|
||||
"moved": "Se movió `{0}` a `{1}`",
|
||||
"autoplay": "El modo de reproducción automática es ahora **{0}**",
|
||||
"notdj": "No eres el DJ. El DJ actual es {0}.",
|
||||
"djToMe": "No puedes transferir el rol de DJ a ti mismo o a un bot.",
|
||||
"djNotInChannel": "`{0}` no está en el canal de voz.",
|
||||
"djswap": "Has transferido el rol de DJ a `{0}`.",
|
||||
"voicelinkQueueFull": "Lo siento, ¡ha alcanzado el máximo de `{0}` canciones en la cola!",
|
||||
"voicelinkOutofList": "¡Proporcione un índice de pista válido!",
|
||||
"voicelinkDuplicateTrack": "Lo siento, esta canción ya está en la cola.",
|
||||
"decodeError": "¡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.",
|
||||
"createSongRequestChannel": "¡Se ha creado un canal de solicitudes de canciones ({0})! Puedes empezar a solicitar cualquier canción por nombre o URL en ese canal, sin necesidad de usar el prefijo del bot."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ ¡Algo salió mal al ejecutar el comando! Por favor, intente de nuevo más tarde o únase a nuestro servidor de Discord para obtener más soporte.",
|
||||
"missingIntents": "Lo siento, este comando no se puede ejecutar porque el bot carece de la intención de solicitud requerida: `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "habilitado",
|
||||
"disabled": "deshabilitado",
|
||||
"live": "EN VIVO"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "¡Por favor, inténtelo de nuevo! Después de que el nodo se reconecte.",
|
||||
"noPlaySource": "¡No se puede encontrar ninguna fuente reproducible!",
|
||||
"noPlayer": "No se ha encontrado ningún reproductor en este servidor.",
|
||||
"noTrackPlaying": "Actualmente no hay canciones reproduciéndose.",
|
||||
"noTrackFound": "No se encontraron canciones con esa consulta. Proporcione una URL válida."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Seleccione una canción para saltar a ...",
|
||||
"filterSelect": "Seleccione un filtro para aplicar ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Atrás",
|
||||
"pause": "Pausa",
|
||||
"resume": "Reanudar",
|
||||
"skip": "Saltar",
|
||||
"leave": "Salir",
|
||||
"loopOff": "Apagar",
|
||||
"loopTrack": "Pista",
|
||||
"loopQueue": "Cola",
|
||||
"volumeUp": "Subir Volumen",
|
||||
"volumeDown": "Bajar Volumen",
|
||||
"volumeMute": "Silenciar",
|
||||
"volumeUnmute": "Activar Sonido",
|
||||
"autoPlay": "Reproducción Automática",
|
||||
"shuffle": "Aleatorio",
|
||||
"forward": "Adelante",
|
||||
"rewind": "Atrás",
|
||||
"lyrics": "Letras"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Reproduciendo ahora:**\n```{0}```",
|
||||
"nowplayingField": "A continuación:",
|
||||
"nowplayingLink": "Escucha en {0}",
|
||||
"playlistLoad": " 🎶 Se agregó la lista de reproducción {0} con {1} canciones a la cola.",
|
||||
"trackLoad": "Se agregó **[{0}](<{1}>)** de {2} ({3}) para comenzar a reproducir.\n",
|
||||
"trackLoadPos": "Se agregó **[{0}](<{1}>)** de {2} ({3}) a la cola en la posición {4}\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "El reproductor ya está en pausa.",
|
||||
"vote": "{0} ha votado para pausar la canción. [{1}/{2}]",
|
||||
"success": "`{0}` ha pausado el reproductor."
|
||||
},
|
||||
"resume": {
|
||||
"error": "El reproductor no está en pausa.",
|
||||
"vote": "{0} ha votado para reanudar la canción. [{1}/{2}]",
|
||||
"success": "`{0}` ha reanudado el reproductor."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Agregue más canciones a la cola antes de barajar.",
|
||||
"vote": "{0} ha votado para barajar la cola. [{1}/{2}]",
|
||||
"success": "La cola ha sido barajada."
|
||||
},
|
||||
"skip": {
|
||||
"error": "No hay canciones para saltar.",
|
||||
"vote": "{0} ha votado para saltar la canción. [{1}/{2}]",
|
||||
"success": "`{0}` ha saltado la canción."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} ha votado para volver a la canción anterior. [{1}/{2}]",
|
||||
"success": "`{0}` ha vuelto a la canción anterior."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} ha votado para detener el reproductor. [{1}/{2}]",
|
||||
"success": "`{0}` ha detenido el reproductor."
|
||||
},
|
||||
"seek": "El reproductor se estableció en **{0}**",
|
||||
"repeat": "El modo de repetición se ha establecido en `{0}`",
|
||||
"forward": "Avanzar el reproductor a **{0}**",
|
||||
"rewind": "Rebobinar el reproductor a **{0}**",
|
||||
"replay": "Reproduciendo de nuevo la canción actual.",
|
||||
"autoplay": "El modo de reproducción automática es ahora **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "No hay canal de voz al que conectarse. Por favor, proporcione uno o únase a uno.",
|
||||
"alreadyConnected": "Ya conectado a un canal de voz.",
|
||||
"noPermission": "¡Lo siento! No tengo permisos para unirme o hablar en su canal de voz.",
|
||||
"notInChannel": "{0}, debes estar en {1} para usar comandos de voz. ¡Vuelve a unirte si estás en voz!",
|
||||
"connect": "Conectado a {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "¡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."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "¡Lo siento! No tengo permisos para crear un canal de solicitud de canciones.",
|
||||
"missingPosition": "Solo el DJ o los administradores pueden cambiar la posición.",
|
||||
"missingMode": "Solo el DJ o los administradores pueden cambiar el modo de bucle.",
|
||||
"missingQueue": "Solo el DJ o los administradores pueden eliminar una canción de la cola.",
|
||||
"missingAutoPlay": "¡Solo el DJ o los administradores pueden habilitar o deshabilitar el modo de reproducción automática!",
|
||||
"missingFunction": "Solo el DJ o los administradores pueden usar esta función.",
|
||||
"notDj": "No eres el DJ. El DJ actual es {0}.",
|
||||
"djToSelf": "No puedes transferir el rol de DJ a ti mismo o a un bot.",
|
||||
"djNotInChannel": "`{0}` no está en el canal de voz.",
|
||||
"djSwapped": "Has transferido el rol de DJ a `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "¡No se encontró paquete de idioma! por favor seleccione un paquete de idioma existente.",
|
||||
"languageChanged": "Cambiado con éxito al paquete de idioma `{0}`.",
|
||||
"prefixSet": "¡Listo! Mi prefijo en tu servidor ahora es `{0}`. Intenta ejecutar `{1}ping` para probarlo.",
|
||||
"djSet": "Establecer el DJ a {0}.",
|
||||
"queueModeSet": "Establecer el modo de cola en `{0}`.",
|
||||
"mode247": "Ahora tienes `{0}` modo 24/7.",
|
||||
"bypassVote": "Ahora tienes `{0}` sistema de votación.",
|
||||
"volumeSet": "Ajustar el volumen a `{0}`%",
|
||||
"controllerToggled": "Ahora tienes `{0}` el controlador de música.",
|
||||
"duplicateTrackToggled": "Ahora tienes `{0}` para evitar que se agreguen pistas duplicadas a la cola.",
|
||||
"controllerMsgToggled": "Ahora tiene `{0}` mensajes del controlador de música.",
|
||||
"silentMsgToggled": "Tienes {0} mensajes silenciosos.",
|
||||
"resetDone": "Reset `{0}` to server defaults.",
|
||||
"resetNotFound": "Nombre de opción inválido. Por favor, use una de las siguientes opciones: {0}"
|
||||
},
|
||||
"menu": "Configuración del servidor | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Información básica:",
|
||||
"value": "```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}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Información de la cola:",
|
||||
"value": "```Modo de cola: {0}\nMáximo de canciones: {1}\nPermitir pistas duplicadas: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Información del estado de voz:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Permisos:",
|
||||
"value": "```{0} Administrador\n{1} Administrar servidor\n{2} Administrar canal\n{3} Administrar mensajes```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Información del bot:",
|
||||
"title2": "Información del reproductor:",
|
||||
"field1": "```ID de fragmento: {0}/{1}\nLatencia de fragmento: {2:.3f}s {3}\nRegión: {4}```",
|
||||
"field2": "```Nodo: {0} - {1:.3f}s\nReproductores: {2}\nRegión de voz: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "plica el efecto `{0}` filtro.",
|
||||
"cleared": "¡Los efectos de sonido se han borrado!",
|
||||
"tagInUse": "¡Este efecto de sonido ya está en uso! Utilice /cleareffect <Tag> para eliminarlo."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Todas las listas de reproducción de {0}",
|
||||
"headers": "ID:,Tiempo:,Nombre:,Pistas:",
|
||||
"footer": "Escriba /playlist play [playlist] para agregar la lista de reproducción a la cola.",
|
||||
"detailTitle": "Visor de lista de reproducción",
|
||||
"detailDesc": "```Nombre: {0} [{1}]\nTotal de pistas: {2}\nPropietario: {3}\nTipo: {4}\n```",
|
||||
"permsValue": "📖 Leer: ✓ ✍🏽 Escribir: {0} 🗑️ Eliminar: {1}",
|
||||
"permsValue2": "📖 Leer: {0}",
|
||||
"trackList": "Pistas",
|
||||
"footer2": "Duración total: {0}",
|
||||
"playlistSelect": "Selecciona una lista de reproducción para ver ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Lista de reproducción [`{0}`] no encontrada. Escriba /playlist view para ver todas sus listas de reproducción.",
|
||||
"noAccess": "¡Lo siento! No está autorizado para acceder a esta lista de reproducción.",
|
||||
"noTrack": "¡Lo siento! No hay pistas en la lista de reproducción [`{0}`].",
|
||||
"notAllowed": "Este comando no está permitido en listas de reproducción vinculadas y compartidas.",
|
||||
"nameOverLimit": "¡Lo siento! El nombre de la lista de reproducción no puede superar los 10 caracteres.",
|
||||
"sameName": "¡Lo siento! Este nombre no puede ser igual que su nuevo nombre.",
|
||||
"deleteDefault": "No está autorizado para eliminar la lista de reproducción predeterminada.",
|
||||
"limitReached": "¡No puede crear más de `{0}` listas de reproducción!",
|
||||
"exists": "La lista de reproducción [`{0}`] ya existe.",
|
||||
"invalidUrl": "Por favor, introduzca un enlace válido de lista de reproducción pública.",
|
||||
"trackLimitReached": "¡Ha alcanzado el límite! Solo puede agregar `{0}` canciones a su lista de reproducción.",
|
||||
"playlistLinkNotAllowed": "No está autorizado para usar enlaces de lista de reproducción.",
|
||||
"streamNotAllowed": "No está autorizado para agregar videos de transmisión a su lista de reproducción.",
|
||||
"positionNotFound": "No se puede encontrar la posición {0} en su lista de reproducción [`{1}`]!",
|
||||
"streaming": "No está autorizado para agregar videos de transmisión a su lista de reproducción.",
|
||||
"track": "Hubo un problema al agregar canciones a la lista de reproducción.",
|
||||
"trackLimited": "¡Ha alcanzado el límite! Solo puede agregar {0} canciones a su lista de reproducción.",
|
||||
"trackRepeated": "¡Ya hay una canción igual en su lista de reproducción!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Se agregó la lista de reproducción [`{0}`] con `{1}` canciones a la cola.",
|
||||
"removed": "Ha eliminado la lista de reproducción [`{0}`].",
|
||||
"created": "Ha creado una lista de reproducción llamada `{0}`. Escriba /playlist view para obtener más información.",
|
||||
"renamed": "Ha cambiado el nombre de `{0}` a `{1}`.",
|
||||
"trackRemoved": "👋 Se quitó {0} de la lista de reproducción de {1} [`{2}`].",
|
||||
"cleared": "Ha borrado con éxito su lista de reproducción [`{0}`].",
|
||||
"trackAdded": "❤️ Agregado **{0}** a la lista de reproducción de {1} [`{2}`]!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Lo siento! No puede enviar una invitación a sí mismo.",
|
||||
"sendErrorBot": "Lo siento! No puede enviar una invitación a un bot.",
|
||||
"belongs": "Lo siento! Esta lista de reproducción pertenece a <@{0}>.",
|
||||
"alreadyShared": "Lo siento! Esta lista de reproducción se ha compartido con `{0}`.",
|
||||
"alreadySent": "Lo siento! Ya ha enviado una invitación antes.",
|
||||
"noAccount": "`{0}` no ha creado una cuenta de lista de reproducción.",
|
||||
"invitationSent": "Se ha enviado una invitación a {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "¡Lo siento! La bandeja de entrada de {0} está llena.",
|
||||
"noMessages": "No hay mensajes en su bandeja de entrada."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "¡El comando de búsqueda no admite enlaces!",
|
||||
"title": "Consulta de búsqueda: {0}",
|
||||
"desc": "➥ Plataforma: {0} **{1}**\n➥ Resultados: **{2}**\n\n{3}",
|
||||
"wait": "Seleccione la canción que desea agregar a la cola.",
|
||||
"success": "Canción agregada a la cola."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "¡Has votado!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Formato de tiempo incorrecto. Ejemplo: 2:42 o 12:39:31",
|
||||
"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}`."
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "No se encontraron letras. Escriba /lyrics <Nombre de la canción> <Autor> para buscar las letras."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Cola de próximas canciones:",
|
||||
"cleared": "Se borraron todas las canciones de `{0}`",
|
||||
"removed": "Se eliminaron `{0}` canciones de la cola.",
|
||||
"swapped": "Se intercambiaron `{0}` y `{1}`",
|
||||
"moved": "Se movió `{0}` a `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "Cola de historial:",
|
||||
"title": "Cola de música",
|
||||
"desc": "**Reproduciendo ahora: [Haga clic aquí]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Lo siento, ¡ha alcanzado el máximo de `{0}` canciones en la cola!",
|
||||
"outOfList": "¡Proporcione un índice de pista válido!",
|
||||
"duplicateTrack": "Lo siento, esta canción ya está en la cola."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "¡Se ha creado un canal de solicitudes de canciones ({0})! Puedes empezar a solicitar cualquier canción por nombre o URL en ese canal, sin necesidad de usar el prefijo del bot."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Anterior",
|
||||
"next": "Siguiente",
|
||||
"play": "Reproducir",
|
||||
"share": "Compartir",
|
||||
"export": "Exportar",
|
||||
"delete": "Eliminar",
|
||||
"page": {
|
||||
"title": "Ingrese el número de página",
|
||||
"field": "Número de página"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/FR.json
436
langs/FR.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ Une erreur est survenue lors de l'exécution de la commande ! Veuillez réessayer plus tard ou rejoignez notre serveur Discord pour plus d'assistance.",
|
||||
"enabled": "activé",
|
||||
"disabled": "désactivé",
|
||||
"nodeReconnect": "Veuillez réessayer après la reconnexion du node.",
|
||||
"noChannel": "Aucun salon vocal pour se connecter. Veuillez en fournir un ou rejoindre un salon vocal.",
|
||||
"alreadyConnected": "Déjà connecté à un salon vocal.",
|
||||
"noPermission": "Désolé ! Je n'ai pas les permissions pour rejoindre ou parler dans votre salon vocal.",
|
||||
"noCreatePermission": "Désolé ! Je n'ai pas les permissions pour créer un salon de demande de chansons.",
|
||||
"noPlaySource": "Impossible de trouver une source à lire !",
|
||||
"noPlayer": "Aucun lecteur n'a été trouvé sur ce serveur.",
|
||||
"notVote": "Cette commande nécessite votre vote ! Tapez `/vote` pour en savoir plus.",
|
||||
"missingIntents": "Désolé, cette commande ne peut pas être exécutée car le bot n'a pas l'intent requis : `({0})`.",
|
||||
"languageNotFound": "Aucun pack de langue trouvé ! Veuillez sélectionner un pack de langue existant.",
|
||||
"changedLanguage": "Changement réussi vers le pack de langue `{0}`.",
|
||||
"setPrefix": "C'est fait ! Mon préfixe dans votre serveur est désormais `{0}`. Essayez de lancer `{1}ping` pour tester.",
|
||||
"setDJ": "Le DJ a été défini sur {0}.",
|
||||
"setQueue": "Le mode file d'attente est défini sur `{0}`.",
|
||||
"247": "Le mode 24/7 est maintenant `{0}`.",
|
||||
"bypassVote": "Vous avez le système de vote `{0}`.",
|
||||
"setVolume": "Le volume est réglé à `{0}`%",
|
||||
"toggleController": "Vous avez maintenant `{0}` le contrôleur de musique.",
|
||||
"toggleDuplicateTrack": "Vous avez maintenant `{0}` la prévention des morceaux en double.",
|
||||
"toggleControllerMsg": "Vous avez maintenant `{0}` les messages du contrôleur musical.",
|
||||
"toggleSilentMsg": "Vous avez {0} messages silencieux.",
|
||||
"settingsMenu": "Paramètres du serveur | {0}",
|
||||
"settingsTitle": "❤️ Infos de base :",
|
||||
"settingsValue": "```Préfixe : {0}\nLangue : {1}\nContrôleur musique : {2}\nRôle DJ : @{3}\nVote Bypass : {4}\n24/7 : {5}\nVolume par défaut : {6}%\nTemps de lecture : {7}```",
|
||||
"settingsTitle2": "🔗 Infos sur la file :",
|
||||
"settingsValue2": "```Mode de file : {0}\nMaximum de chansons : {1}\nPiste dupliquée : {2}```",
|
||||
"settingsTitle3": "🎤 Infos sur l'état vocal :",
|
||||
"settingsPermTitle": "✨ Permissions :",
|
||||
"settingsPermValue": "```{0} Administrateur\n{1} Gérer_serveur\n{2} Gérer_salons\n{3} Gérer_messages```",
|
||||
"pingTitle1": "Infos Bot :",
|
||||
"pingTitle2": "Infos Lecteur :",
|
||||
"pingField1": "```Shard ID : {0}/{1}\nLatence Shard : {2:.3f}s {3}\nRégion : {4}```",
|
||||
"pingField2": "```Node : {0} - {1:.3f}s\nLecteurs : {2}\nRégion vocale : {3}```",
|
||||
"addEffect": "Effet `{0}` appliqué.",
|
||||
"clearEffect": "Les effets sonores ont été supprimés !",
|
||||
"filterTagAlreadyInUse": "Cet effet sonore est déjà utilisé ! Veuillez utiliser /cleareffect <Tag> pour le retirer.",
|
||||
"playlistViewTitle": "📜 Toutes les playlists de {0}",
|
||||
"playlistViewHeaders": "ID:,Durée:,Nom:,Morceaux:",
|
||||
"playlistFooter": "Tapez /playlist play [playlist] pour ajouter une playlist à la file.",
|
||||
"playlistNotFound": "Playlist [`{0}`] introuvable. Tapez /playlist view pour voir toutes vos playlists.",
|
||||
"playlistNotAccess": "Désolé ! Vous n'êtes pas autorisé à accéder à cette playlist !",
|
||||
"playlistNoTrack": "Désolé ! Il n'y a aucun morceau dans la playlist [`{0}`].",
|
||||
"playlistNotAllow": "Cette commande n'est pas autorisée sur les playlists liées ou partagées.",
|
||||
"playlistPlay": "La playlist [`{0}`] avec `{1}` chansons a été ajoutée à la file.",
|
||||
"playlistOverText": "Désolé ! Le nom de la playlist ne peut pas dépasser 10 caractères.",
|
||||
"playlistSameName": "Désolé ! Veuillez choisir un nouveau nom pour la playlist.",
|
||||
"playlistDeleteError": "Vous n'êtes pas autorisé à supprimer la playlist par défaut.",
|
||||
"playlistRemove": "Vous avez supprimé la playlist [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Désolé ! Vous ne pouvez pas vous envoyer une invitation à vous-même.",
|
||||
"playlistSendErrorBot": "Désolé ! Vous ne pouvez pas envoyer une invitation à un bot.",
|
||||
"playlistBelongs": "Désolé ! Cette playlist appartient à <@{0}>.",
|
||||
"playlistShare": "Désolé ! Cette playlist a été partagée avec {0}.",
|
||||
"playlistSent": "Désolé ! Vous avez déjà envoyé une invitation à cet utilisateur auparavant.",
|
||||
"noPlaylistAcc": "{0} n'a pas créé de compte playlist.",
|
||||
"overPlaylistCreation": "Vous ne pouvez pas créer plus de `{0}` playlists !",
|
||||
"playlistExists": "La playlist [`{0}`] existe déjà.",
|
||||
"playlistNotInvalidUrl": "Veuillez entrer un lien valide de playlist publique Spotify ou YouTube.",
|
||||
"playlistCreated": "Vous avez créé `{0}` playlists. Tapez /playlist view pour plus d'infos.",
|
||||
"playlistRenamed": "Vous avez renommé `{0}` en `{1}`.",
|
||||
"playlistLimitTrack": "Vous avez atteint la limite ! Vous ne pouvez ajouter que `{0}` chansons à votre playlist.",
|
||||
"playlistPlaylistLink": "Vous n'êtes pas autorisé à utiliser des liens de playlist.",
|
||||
"playlistStream": "Vous n'êtes pas autorisé à ajouter des vidéos en streaming à votre playlist.",
|
||||
"playlistPositionNotFound": "Impossible de trouver la position `{0}` dans votre playlist [`{1}`] !",
|
||||
"playlistRemoved": "👋 **{0}** a été retiré de la playlist [`{2}`] de {1}.",
|
||||
"playlistClear": "Vous avez vidé avec succès votre playlist [`{0}`].",
|
||||
"playlistView": "Visualiseur de playlist",
|
||||
"playlistViewDesc": "```Nom | ID : {0} | {1}\nTotal Morceaux : {2}\nPropriétaire : {3}\nType : {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Lecture : ✓ ✍🏽 Écriture : {0} 🗑️ Suppression : {1}",
|
||||
"playlistViewPermsValue2": "📖 Lecture : {0}",
|
||||
"playlistViewTrack": "Morceaux",
|
||||
"playlistViewPage": "Page : {0}/{1} | Durée totale : {2}",
|
||||
"inboxFull": "Désolé ! La boîte de réception de {0} est pleine.",
|
||||
"inboxNoMsg": "Aucun message dans votre boîte de réception.",
|
||||
"invitationSent": "Invitation envoyée à {0}.",
|
||||
"notInChannel": "{0}, vous devez être dans {1} pour utiliser les commandes vocales. Veuillez rejoindre à nouveau si vous êtes déjà en vocal !",
|
||||
"noTrackPlaying": "Aucune chanson n'est en cours de lecture pour le moment",
|
||||
"noTrackFound": "Aucune chanson trouvée pour cette requête ! Veuillez fournir une URL valide.",
|
||||
"noLinkSupport": "La commande de recherche ne prend pas en charge les liens !",
|
||||
"voted": "Vous avez voté !",
|
||||
"missingPosPerm": "Seul le DJ ou les admins peuvent changer la position.",
|
||||
"missingModePerm": "Seul le DJ ou les admins peuvent changer le mode de boucle.",
|
||||
"missingQueuePerm": "Seul le DJ ou les admins peuvent retirer un morceau de la file.",
|
||||
"missingAutoPlayPerm": "Seul le DJ ou les admins peuvent activer/désactiver l'autoplay !",
|
||||
"missingFunctionPerm": "Seul le DJ ou un admin peut utiliser cette fonction.",
|
||||
"timeFormatError": "Format de temps incorrect. Exemple : 2:42 ou 12:39:31",
|
||||
"lyricsNotFound": "Paroles non trouvées. Tapez /lyrics <Nom de la Chanson> <Auteur> pour chercher les paroles.",
|
||||
"missingTrackInfo": "Certaines informations sur le morceau sont manquantes.",
|
||||
"noVoiceChannel": "Aucun salon vocal trouvé !",
|
||||
"playlistAddError": "Vous n'êtes pas autorisé à ajouter des vidéos en streaming à votre playlist !",
|
||||
"playlistAddError2": "Problème lors de l'ajout de morceaux à la playlist !",
|
||||
"playlistLimited": "Vous avez atteint la limite ! Vous ne pouvez ajouter que {0} chansons à votre playlist.",
|
||||
"playlistRepeated": "Ce morceau est déjà dans votre playlist !",
|
||||
"playlistAdded": "❤️ **{0}** ajouté à la playlist [`{2}`] de {1} !",
|
||||
"playerDropdown": "Sélectionnez une chanson vers laquelle passer...",
|
||||
"playerFilter": "Sélectionnez un filtre à appliquer...",
|
||||
"buttonBack": "Précédent",
|
||||
"buttonPause": "Pause",
|
||||
"buttonResume": "Reprendre",
|
||||
"buttonSkip": "Passer",
|
||||
"buttonLeave": "Quitter",
|
||||
"buttonLoop": "Boucle",
|
||||
"buttonVolumeUp": "Volume +",
|
||||
"buttonVolumeDown": "Volume -",
|
||||
"buttonVolumeMute": "Muet",
|
||||
"buttonVolumeUnmute": "Son activé",
|
||||
"buttonAutoPlay": "Autoplay",
|
||||
"buttonShuffle": "Mélanger",
|
||||
"buttonForward": "Avancer",
|
||||
"buttonRewind": "Rembobiner",
|
||||
"buttonLyrics": "Paroles",
|
||||
"nowplayingDesc": "**Lecture en cours :**\n```{0}```",
|
||||
"nowplayingField": "À suivre :",
|
||||
"nowplayingLink": "Écouter sur {0}",
|
||||
"connect": "Connecté à {0}",
|
||||
"live": "EN DIRECT",
|
||||
"playlistLoad": " 🎶 La playlist **{0}** avec `{1}` chansons a été ajoutée à la file.",
|
||||
"trackLoad": "**[{0}](<{1}>)** de **{2}** (`{3}`) a été ajoutée et commencera à jouer.\n",
|
||||
"trackLoad_pos": "**[{0}](<{1}>)** de **{2}** (`{3}`) a été ajoutée à la file à la position **{4}**\n",
|
||||
"searchTitle": "Recherche : {0}",
|
||||
"searchDesc": "➥ Plateforme : {0} **{1}**\n➥ Résultats : **{2}**\n\n{3}",
|
||||
"searchWait": "Choisissez la chanson à ajouter à la file.",
|
||||
"searchTimeout": "Recherche expirée. Veuillez réessayer plus tard.",
|
||||
"searchSuccess": "Chanson ajoutée à la file.",
|
||||
"queueTitle": "File à venir :",
|
||||
"historyTitle": "Historique :",
|
||||
"viewTitle": "File de musiques",
|
||||
"viewDesc": "**Lecture en cours : [Cliquez ici]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Page : {0}/{1} | Durée totale : {2}",
|
||||
"pauseError": "Le lecteur est déjà en pause.",
|
||||
"pauseVote": "{0} a voté pour mettre la chanson en pause. [{1}/{2}]",
|
||||
"paused": "`{0}` a mis le lecteur en pause.",
|
||||
"resumeError": "Le lecteur n'est pas en pause.",
|
||||
"resumeVote": "{0} a voté pour reprendre la chanson. [{1}/{2}]",
|
||||
"resumed": "`{0}` a repris le lecteur.",
|
||||
"shuffleError": "Ajoutez plus de chansons à la file avant de mélanger.",
|
||||
"shuffleVote": "{0} a voté pour mélanger la file. [{1}/{2}]",
|
||||
"shuffled": "La file a été mélangée.",
|
||||
"skipError": "Aucune chanson vers laquelle passer.",
|
||||
"skipVote": "{0} a voté pour passer la chanson. [{1}/{2}]",
|
||||
"skipped": "`{0}` a passé la chanson.",
|
||||
"backVote": "{0} a voté pour passer à la chanson précédente. [{1}/{2}]",
|
||||
"backed": "`{0}` est en train de passer à la chanson précédente.",
|
||||
"leaveVote": "{0} a voté pour arrêter le lecteur. [{1}/{2}]",
|
||||
"left": "`{0}` a arrêté le lecteur.",
|
||||
"seek": "Le lecteur a été placé à **{0}**",
|
||||
"repeat": "Le mode de répétition a été réglé sur `{0}`",
|
||||
"cleared": "Tous les morceaux dans `{0}` ont été supprimés",
|
||||
"removed": "`{0}` morceaux ont été retirés de la file.",
|
||||
"forward": "Le lecteur a avancé à **{0}**",
|
||||
"rewind": "Le lecteur a été rembobiné à **{0}**",
|
||||
"replay": "Relecture de la chanson actuelle.",
|
||||
"swapped": "`{0}` et `{1}` ont été échangés",
|
||||
"moved": "`{0}` a été déplacé vers `{1}`",
|
||||
"autoplay": "Le mode autoplay est maintenant **{0}**",
|
||||
"notdj": "Vous n'êtes pas DJ. Le DJ actuel est {0}.",
|
||||
"djToMe": "Impossible de transférer le rôle de DJ à vous-même ou à un bot.",
|
||||
"djNotInChannel": "`{0}` n'est pas dans le salon vocal.",
|
||||
"djswap": "Vous avez transféré le rôle de DJ à `{0}`.",
|
||||
"voicelinkQueueFull": "Désolé, vous avez atteint le maximum de `{0}` morceaux dans la file !",
|
||||
"voicelinkOutofList": "Veuillez fournir un index de morceau valide !",
|
||||
"voicelinkDuplicateTrack": "Désolé, ce morceau est déjà dans la file.",
|
||||
"decodeError": "Une erreur s'est produite lors du décodage du fichier !",
|
||||
"invalidStartTime": "Heure de début invalide, doit être entre `00:00` et `{0}`",
|
||||
"invalidEndTime": "Heure de fin invalide, doit être entre `00:00` et `{0}`",
|
||||
"invalidTimeOrder": "L'heure de fin ne peut pas être inférieure ou égale à celle de début",
|
||||
"setStageAnnounceTemplate": "C'est fait ! Désormais, le statut vocal tel que celui dans lequel vous êtes sera nommé selon votre modèle. Vous devriez le voir se mettre à jour dans quelques secondes.",
|
||||
"createSongRequestChannel": "Un salon de demande de chansons ({0}) a été créé ! Vous pouvez commencer à demander n'importe quelle chanson par nom ou URL dans ce salon, sans besoin d'utiliser le préfixe du bot."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Une erreur est survenue lors de l'exécution de la commande ! Veuillez réessayer plus tard ou rejoignez notre serveur Discord pour plus d'assistance.",
|
||||
"missingIntents": "Désolé, cette commande ne peut pas être exécutée car le bot n'a pas l'intent requis : `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "activé",
|
||||
"disabled": "désactivé",
|
||||
"live": "EN DIRECT"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Veuillez réessayer après la reconnexion du node.",
|
||||
"noPlaySource": "Impossible de trouver une source à lire !",
|
||||
"noPlayer": "Aucun lecteur n'a été trouvé sur ce serveur.",
|
||||
"noTrackPlaying": "Aucune chanson n'est en cours de lecture pour le moment",
|
||||
"noTrackFound": "Aucune chanson trouvée pour cette requête ! Veuillez fournir une URL valide."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Sélectionnez une chanson vers laquelle passer...",
|
||||
"filterSelect": "Sélectionnez un filtre à appliquer..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Précédent",
|
||||
"pause": "Pause",
|
||||
"resume": "Reprendre",
|
||||
"skip": "Passer",
|
||||
"leave": "Quitter",
|
||||
"loopOff": "Désactivé",
|
||||
"loopTrack": "Piste",
|
||||
"loopQueue": "File d'attente",
|
||||
"volumeUp": "Volume +",
|
||||
"volumeDown": "Volume -",
|
||||
"volumeMute": "Muet",
|
||||
"volumeUnmute": "Son activé",
|
||||
"autoPlay": "Autoplay",
|
||||
"shuffle": "Mélanger",
|
||||
"forward": "Avancer",
|
||||
"rewind": "Rembobiner",
|
||||
"lyrics": "Paroles"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Lecture en cours :**\n```{0}```",
|
||||
"nowplayingField": "À suivre :",
|
||||
"nowplayingLink": "Écouter sur {0}",
|
||||
"playlistLoad": " 🎶 La playlist **{0}** avec `{1}` chansons a été ajoutée à la file.",
|
||||
"trackLoad": "**[{0}](<{1}>)** de **{2}** (`{3}`) a été ajoutée et commencera à jouer.\n",
|
||||
"trackLoadPos": "**[{0}](<{1}>)** de **{2}** (`{3}`) a été ajoutée à la file à la position **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "Le lecteur est déjà en pause.",
|
||||
"vote": "{0} a voté pour mettre la chanson en pause. [{1}/{2}]",
|
||||
"success": "`{0}` a mis le lecteur en pause."
|
||||
},
|
||||
"resume": {
|
||||
"error": "Le lecteur n'est pas en pause.",
|
||||
"vote": "{0} a voté pour reprendre la chanson. [{1}/{2}]",
|
||||
"success": "`{0}` a repris le lecteur."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Ajoutez plus de chansons à la file avant de mélanger.",
|
||||
"vote": "{0} a voté pour mélanger la file. [{1}/{2}]",
|
||||
"success": "La file a été mélangée."
|
||||
},
|
||||
"skip": {
|
||||
"error": "Aucune chanson vers laquelle passer.",
|
||||
"vote": "{0} a voté pour passer la chanson. [{1}/{2}]",
|
||||
"success": "`{0}` a passé la chanson."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} a voté pour passer à la chanson précédente. [{1}/{2}]",
|
||||
"success": "`{0}` est en train de passer à la chanson précédente."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} a voté pour arrêter le lecteur. [{1}/{2}]",
|
||||
"success": "`{0}` a arrêté le lecteur."
|
||||
},
|
||||
"seek": "Le lecteur a été placé à **{0}**",
|
||||
"repeat": "Le mode de répétition a été réglé sur `{0}`",
|
||||
"forward": "Le lecteur a avancé à **{0}**",
|
||||
"rewind": "Le lecteur a été rembobiné à **{0}**",
|
||||
"replay": "Relecture de la chanson actuelle.",
|
||||
"autoplay": "Le mode autoplay est maintenant **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "Aucun salon vocal pour se connecter. Veuillez en fournir un ou rejoindre un salon vocal.",
|
||||
"alreadyConnected": "Déjà connecté à un salon vocal.",
|
||||
"noPermission": "Désolé ! Je n'ai pas les permissions pour rejoindre ou parler dans votre salon vocal.",
|
||||
"notInChannel": "{0}, vous devez être dans {1} pour utiliser les commandes vocales. Veuillez rejoindre à nouveau si vous êtes déjà en vocal !",
|
||||
"connect": "Connecté à {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "C'est fait ! Désormais, le statut vocal tel que celui dans lequel vous êtes sera nommé selon votre modèle. Vous devriez le voir se mettre à jour dans quelques secondes."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Désolé ! Je n'ai pas les permissions pour créer un salon de demande de chansons.",
|
||||
"missingPosition": "Seul le DJ ou les admins peuvent changer la position.",
|
||||
"missingMode": "Seul le DJ ou les admins peuvent changer le mode de boucle.",
|
||||
"missingQueue": "Seul le DJ ou les admins peuvent retirer un morceau de la file.",
|
||||
"missingAutoPlay": "Seul le DJ ou les admins peuvent activer/désactiver l'autoplay !",
|
||||
"missingFunction": "Seul le DJ ou un admin peut utiliser cette fonction.",
|
||||
"notDj": "Vous n'êtes pas DJ. Le DJ actuel est {0}.",
|
||||
"djToSelf": "Impossible de transférer le rôle de DJ à vous-même ou à un bot.",
|
||||
"djNotInChannel": "`{0}` n'est pas dans le salon vocal.",
|
||||
"djSwapped": "Vous avez transféré le rôle de DJ à `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "Aucun pack de langue trouvé ! Veuillez sélectionner un pack de langue existant.",
|
||||
"languageChanged": "Changement réussi vers le pack de langue `{0}`.",
|
||||
"prefixSet": "C'est fait ! Mon préfixe dans votre serveur est désormais `{0}`. Essayez de lancer `{1}ping` pour tester.",
|
||||
"djSet": "Le DJ a été défini sur {0}.",
|
||||
"queueModeSet": "Le mode file d'attente est défini sur `{0}`.",
|
||||
"mode247": "Le mode 24/7 est maintenant `{0}`.",
|
||||
"bypassVote": "Vous avez le système de vote `{0}`.",
|
||||
"volumeSet": "Le volume est réglé à `{0}`%",
|
||||
"controllerToggled": "Vous avez maintenant `{0}` le contrôleur de musique.",
|
||||
"duplicateTrackToggled": "Vous avez maintenant `{0}` la prévention des morceaux en double.",
|
||||
"controllerMsgToggled": "Vous avez maintenant `{0}` les messages du contrôleur musical.",
|
||||
"silentMsgToggled": "Vous avez {0} messages silencieux.",
|
||||
"resetDone": "`{0}` a été réinitialisé aux paramètres par défaut du serveur.",
|
||||
"resetNotFound": "Nom d'option invalide. Veuillez utiliser une des options suivantes: {0}"
|
||||
},
|
||||
"menu": "Paramètres du serveur | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Infos de base :",
|
||||
"value": "```Préfixe : {0}\nLangue : {1}\nContrôleur musique : {2}\nRôle DJ : @{3}\nVote Bypass : {4}\n24/7 : {5}\nVolume par défaut : {6}%\nTemps de lecture : {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Infos sur la file :",
|
||||
"value": "```Mode de file : {0}\nMaximum de chansons : {1}\nPiste dupliquée : {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Infos sur l'état vocal :"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Permissions :",
|
||||
"value": "```{0} Administrateur\n{1} Gérer_serveur\n{2} Gérer_salons\n{3} Gérer_messages```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Infos Bot :",
|
||||
"title2": "Infos Lecteur :",
|
||||
"field1": "```Shard ID : {0}/{1}\nLatence Shard : {2:.3f}s {3}\nRégion : {4}```",
|
||||
"field2": "```Node : {0} - {1:.3f}s\nLecteurs : {2}\nRégion vocale : {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Effet `{0}` appliqué.",
|
||||
"cleared": "Les effets sonores ont été supprimés !",
|
||||
"tagInUse": "Cet effet sonore est déjà utilisé ! Veuillez utiliser /cleareffect <Tag> pour le retirer."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Toutes les playlists de {0}",
|
||||
"headers": "ID:,Durée:,Nom:,Morceaux:",
|
||||
"footer": "Tapez /playlist play [playlist] pour ajouter une playlist à la file.",
|
||||
"detailTitle": "Visualiseur de playlist",
|
||||
"detailDesc": "```Nom: {0} [{1}]\nTotal Morceaux : {2}\nPropriétaire : {3}\nType : {4}\n```",
|
||||
"permsValue": "📖 Lecture : ✓ ✍🏽 Écriture : {0} 🗑️ Suppression : {1}",
|
||||
"permsValue2": "📖 Lecture : {0}",
|
||||
"trackList": "Morceaux",
|
||||
"footer2": "Durée totale : {0}",
|
||||
"playlistSelect": "Sélectionnez une playlist à afficher..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Playlist [`{0}`] introuvable. Tapez /playlist view pour voir toutes vos playlists.",
|
||||
"noAccess": "Désolé ! Vous n'êtes pas autorisé à accéder à cette playlist !",
|
||||
"noTrack": "Désolé ! Il n'y a aucun morceau dans la playlist [`{0}`].",
|
||||
"notAllowed": "Cette commande n'est pas autorisée sur les playlists liées ou partagées.",
|
||||
"nameOverLimit": "Désolé ! Le nom de la playlist ne peut pas dépasser 10 caractères.",
|
||||
"sameName": "Désolé ! Veuillez choisir un nouveau nom pour la playlist.",
|
||||
"deleteDefault": "Vous n'êtes pas autorisé à supprimer la playlist par défaut.",
|
||||
"limitReached": "Vous ne pouvez pas créer plus de `{0}` playlists !",
|
||||
"exists": "La playlist [`{0}`] existe déjà.",
|
||||
"invalidUrl": "Veuillez entrer un lien valide de playlist publique.",
|
||||
"trackLimitReached": "Vous avez atteint la limite ! Vous ne pouvez ajouter que `{0}` chansons à votre playlist.",
|
||||
"playlistLinkNotAllowed": "Vous n'êtes pas autorisé à utiliser des liens de playlist.",
|
||||
"streamNotAllowed": "Vous n'êtes pas autorisé à ajouter des vidéos en streaming à votre playlist.",
|
||||
"positionNotFound": "Impossible de trouver la position `{0}` dans votre playlist [`{1}`] !",
|
||||
"streaming": "Vous n'êtes pas autorisé à ajouter des vidéos en streaming à votre playlist !",
|
||||
"track": "Problème lors de l'ajout de morceaux à la playlist !",
|
||||
"trackLimited": "Vous avez atteint la limite ! Vous ne pouvez ajouter que {0} chansons à votre playlist.",
|
||||
"trackRepeated": "Ce morceau est déjà dans votre playlist !"
|
||||
},
|
||||
"actions": {
|
||||
"play": "La playlist [`{0}`] avec `{1}` chansons a été ajoutée à la file.",
|
||||
"removed": "Vous avez supprimé la playlist [`{0}`].",
|
||||
"created": "Vous avez créé `{0}` playlists. Tapez /playlist view pour plus d'infos.",
|
||||
"renamed": "Vous avez renommé `{0}` en `{1}`.",
|
||||
"trackRemoved": "👋 **{0}** a été retiré de la playlist [`{2}`] de {1}.",
|
||||
"cleared": "Vous avez vidé avec succès votre playlist [`{0}`].",
|
||||
"trackAdded": "❤️ **{0}** ajouté à la playlist [`{2}`] de {1} !"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Désolé ! Vous ne pouvez pas vous envoyer une invitation à vous-même.",
|
||||
"sendErrorBot": "Désolé ! Vous ne pouvez pas envoyer une invitation à un bot.",
|
||||
"belongs": "Désolé ! Cette playlist appartient à <@{0}>.",
|
||||
"alreadyShared": "Désolé ! Cette playlist a été partagée avec {0}.",
|
||||
"alreadySent": "Désolé ! Vous avez déjà envoyé une invitation à cet utilisateur auparavant.",
|
||||
"noAccount": "{0} n'a pas créé de compte playlist.",
|
||||
"invitationSent": "Invitation envoyée à {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Désolé ! La boîte de réception de {0} est pleine.",
|
||||
"noMessages": "Aucun message dans votre boîte de réception."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "La commande de recherche ne prend pas en charge les liens !",
|
||||
"title": "Recherche : {0}",
|
||||
"desc": "➥ Plateforme : {0} **{1}**\n➥ Résultats : **{2}**\n\n{3}",
|
||||
"wait": "Choisissez la chanson à ajouter à la file.",
|
||||
"success": "Chanson ajoutée à la file."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "Vous avez voté !"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Format de temps incorrect. Exemple : 2:42 ou 12:39:31",
|
||||
"invalidStartTime": "Heure de début invalide, doit être entre `00:00` et `{0}`",
|
||||
"invalidEndTime": "Heure de fin invalide, doit être entre `00:00` et `{0}`"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Paroles non trouvées. Tapez /lyrics <Nom de la Chanson> <Auteur> pour chercher les paroles."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "File à venir :",
|
||||
"cleared": "Tous les morceaux dans `{0}` ont été supprimés",
|
||||
"removed": "`{0}` morceaux ont été retirés de la file.",
|
||||
"swapped": "`{0}` et `{1}` ont été échangés",
|
||||
"moved": "`{0}` a été déplacé vers `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "Historique :",
|
||||
"title": "File de musiques",
|
||||
"desc": "**Lecture en cours : [Cliquez ici]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Désolé, vous avez atteint le maximum de `{0}` morceaux dans la file !",
|
||||
"outOfList": "Veuillez fournir un index de morceau valide !",
|
||||
"duplicateTrack": "Désolé, ce morceau est déjà dans la file."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "Un salon de demande de chansons ({0}) a été créé ! Vous pouvez commencer à demander n'importe quelle chanson par nom ou URL dans ce salon, sans besoin d'utiliser le préfixe du bot."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Précédent",
|
||||
"next": "Suivant",
|
||||
"play": "Jouer",
|
||||
"share": "Partager",
|
||||
"export": "Exporter",
|
||||
"delete": "Supprimer",
|
||||
"page": {
|
||||
"title": "Entrez le numéro de page",
|
||||
"field": "Numéro de page"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/JA.json
436
langs/JA.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ コマンドの実行中に何かが間違っています!後でもう一度試してください。または、より詳しいサポートを求めるために当社のDiscordサーバーに参加してください。",
|
||||
"enabled": "有効化",
|
||||
"disabled": "無効化",
|
||||
"nodeReconnect": "再試行してください!ノードが再接続した後。",
|
||||
"noChannel": "接続する音声チャンネルがありません。提供するか、参加してください。",
|
||||
"alreadyConnected": "すでに音声チャンネルに接続しています。",
|
||||
"noPermission": "申し訳ありません!私はあなたの音声チャンネルに参加または話すための許可がありません。",
|
||||
"noCreatePermission": "ごめんなさい!曲リクエストチャンネルを作成する権限がありません。",
|
||||
"noPlaySource": "再生可能なソースが見つかりません!",
|
||||
"noPlayer": "このサーバーにプレイヤーが見つかりません。",
|
||||
"notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。",
|
||||
"missingIntents": "申し訳ありませんが、このコマンドは実行できません。ボットに必要なリクエストインテントが不足しています:`({0})`.",
|
||||
"languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。",
|
||||
"changedLanguage": "「{0}」言語パックに正常に変更しました。",
|
||||
"setPrefix": "完了!あなたのサーバーのプレフィックスは今や「{0}」です。 `{1}ping`を実行してテストしてみてください。",
|
||||
"setDJ": "{0}にDJを設定しました。",
|
||||
"setQueue": "キューモードを「{0}」に設定しました。",
|
||||
"247": "今、24/7モードは「{0}」です。",
|
||||
"bypassVote": "今、投票システムは「{0}」になりました。",
|
||||
"setVolume": "音量を「{0}%」に設定しました。",
|
||||
"toggleController": "今、音楽コントローラーは「{0}」です。",
|
||||
"toggleDuplicateTrack": "今、キュー内の重複トラックを防止するための設定は「{0}」です。",
|
||||
"toggleControllerMsg": "現在、音楽コントローラーから `{0}` 件のメッセージがあります。",
|
||||
"toggleSilentMsg": "あなたは{0}サイレントメッセージを持っています。",
|
||||
"settingsMenu": "サーバー設定| {0}",
|
||||
"settingsTitle": "❤️ 基本情報:",
|
||||
"settingsValue": "```プレフィックス:{0}\n言語:{1}\n音楽コントローラーを有効にする:{2}\nDJロール:@{3}\n投票バイパス:{4}\n24/7:{5}\nデフォルトボリューム:{6}%\n再生時間:{7}```",
|
||||
"settingsTitle2": "🔗 キュー情報:",
|
||||
"settingsValue2": "```キューモード:{0}\n最大曲数:{1}\n重複トラックを許可する:{2}```",
|
||||
"settingsTitle3": "🎤 音声ステータス情報:",
|
||||
"settingsPermTitle": "✨ 権限:",
|
||||
"settingsPermValue": "```{0} 管理者\n{1} ギルド管理\n{2} チャンネル管理\n{3} メッセージ管理```",
|
||||
"pingTitle1": "ボット情報:",
|
||||
"pingTitle2": "プレーヤー情報:",
|
||||
"pingField1": "```シャードID:{0}/{1}\nシャードレイテンシ:{2:.3f}s {3}\nリージョン:{4}```",
|
||||
"pingField2": "```ノード:{0} - {1:.3f}s\nプレイヤー数:{2}\n音声リージョン:{3}```",
|
||||
"addEffect": "`{0}` フィルターを適用します。",
|
||||
"clearEffect": "効果音がクリアされました!",
|
||||
"filterTagAlreadyInUse": "このサウンドエフェクトはすでに使用されています!削除するには/cleareffect <Tag>を使用してください。",
|
||||
"playlistViewTitle": "📜 {0}のすべてのプレイリスト",
|
||||
"playlistViewHeaders": "ID:,時間:,名前:,トラック:",
|
||||
"playlistFooter": "プレイリストをキューに追加するには、/playlist play [playlist]を入力してください。",
|
||||
"playlistNotFound": "プレイリスト[{0}]が見つかりません。すべてのプレイリストを表示するには、/playlist viewを入力してください。",
|
||||
"playlistNotAccess": "申し訳ありません!このプレイリストにアクセスすることはできません!",
|
||||
"playlistNoTrack": "申し訳ありません!プレイリスト[{0}]にはトラックがありません。",
|
||||
"playlistNotAllow": "リンクまたは共有プレイリストでは、このコマンドは許可されていません。",
|
||||
"playlistPlay": "プレイリスト[{0}]をキューに{1}曲追加しました。",
|
||||
"playlistOverText": "申し訳ありません!プレイリストの名前は10文字を超えることはできません。",
|
||||
"playlistSameName": "申し訳ありません!この名前は新しい名前と同じであってはなりません。",
|
||||
"playlistDeleteError": "デフォルトのプレイリストを削除することはできません。",
|
||||
"playlistRemove": "プレイリスト[{0}]を削除しました。",
|
||||
"playlistSendErrorPlayer": "申し訳ありません!自分に招待状を送信することはできません。",
|
||||
"playlistSendErrorBot": "申し訳ありません!ボットに招待状を送信することはできません。",
|
||||
"playlistBelongs": "申し訳ありません!このプレイリストは<@{0}>さんのものです。",
|
||||
"playlistShare": "申し訳ありません!このプレイリストは{0}さんと共有されています。",
|
||||
"playlistSent": "申し訳ありません!以前に招待状を送信しました。",
|
||||
"noPlaylistAcc": "{0}さんはプレイリストアカウントを作成していません。",
|
||||
"overPlaylistCreation": " {0}個以上のプレイリストを作成することはできません!",
|
||||
"playlistExists": "プレイリスト[{0}]はすでに存在します。",
|
||||
"playlistNotInvalidUrl": "有効なリンクまたは公開SpotifyまたはYouTubeプレイリストリンクを入力してください。",
|
||||
"playlistCreated": "{0}プレイリストを作成しました。詳細については、/playlist viewを入力してください。",
|
||||
"playlistRenamed": "{0}を{1}に名前を変更しました。",
|
||||
"playlistLimitTrack": "この制限に達しました!プレイリストには{0}曲しか追加できません。",
|
||||
"playlistPlaylistLink": "プレイリストリンクを使用することはできません。",
|
||||
"playlistStream": "ストリーミングビデオをプレイリストに追加することはできません。",
|
||||
"playlistPositionNotFound": "プレイリスト[{1}]から位置{0}を見つけることができませんでした!",
|
||||
"playlistRemoved": "👋 {1}さんのプレイリスト[{2}]から**{0}**を削除しました。",
|
||||
"playlistClear": "プレイリスト[{0}]を正常にクリアしました。",
|
||||
"playlistView": "プレイリストビューアー",
|
||||
"playlistViewDesc": "```名前 | ID: {0} | {1}\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 読み込み: ✓ ✍🏽 書き込み: {0} 🗑️ 削除: {1}",
|
||||
"playlistViewPermsValue2": "📖 読み込み: {0}",
|
||||
"playlistViewTrack": "トラック",
|
||||
"playlistViewPage": "ページ: {0}/{1} | トータルダレーション: {2}",
|
||||
"inboxFull": "申し訳ありません!{0}さんの受信トレイはいっぱいです。",
|
||||
"inboxNoMsg": "受信トレイにメッセージはありません。",
|
||||
"invitationSent": "{0}さんに招待状を送信しました。",
|
||||
"notInChannel": "{0}さん、音声コマンドを使用するには{1}にいる必要があります。音声に接続している場合は再接続してください!",
|
||||
"noTrackPlaying": "現在再生中の曲はありません",
|
||||
"noTrackFound": "そのクエリで曲が見つかりませんでした!有効なURLを入力してください。",
|
||||
"noLinkSupport": "検索コマンドはリンクをサポートしていません!",
|
||||
"voted": "投票しました!",
|
||||
"missingPosPerm": "DJまたは管理者のみが位置を変更できます。",
|
||||
"missingModePerm": "DJまたは管理者のみがループモードを切り替えることができます。",
|
||||
"missingQueuePerm": "DJまたは管理者のみがキューからトラックを削除できます。",
|
||||
"missingAutoPlayPerm": "DJまたは管理者のみがオートプレイモードを有効化または無効化できます!",
|
||||
"missingFunctionPerm": "DJまたは管理者のみがこの機能を使用できます。",
|
||||
"timeFormatError": "時間の形式が間違っています。例:2:42または12:39:31",
|
||||
"lyricsNotFound": "歌詞が見つかりませんでした。/lyrics <曲名> <アーティスト>と入力して歌詞を検索してください。",
|
||||
"missingTrackInfo": "一部のトラック情報が欠落しています。",
|
||||
"noVoiceChannel": "音声チャンネルが見つかりません!",
|
||||
"playlistAddError": "ストリーミングビデオをプレイリストに追加することはできません!",
|
||||
"playlistAddError2": "トラックをプレイリストに追加する際に問題が発生しました!",
|
||||
"playlistLimited": "制限に達しました!プレイリストには{0}曲しか追加できません。",
|
||||
"playlistRepeated": "すでにプレイリストに同じトラックがあります!",
|
||||
"playlistAdded": "❤️ **{0}**を{1}のプレイリスト[`{2}`]に追加しました!",
|
||||
"playerDropdown": "スキップする曲を選択してください...",
|
||||
"playerFilter": "適用するフィルターを選択してください...",
|
||||
"buttonBack": "戻る",
|
||||
"buttonPause": "一時停止",
|
||||
"buttonResume": "再開",
|
||||
"buttonSkip": "スキップ",
|
||||
"buttonLeave": "退出",
|
||||
"buttonLoop": "ループ",
|
||||
"buttonVolumeUp": "音量を上げる",
|
||||
"buttonVolumeDown": "音量を下げる",
|
||||
"buttonVolumeMute": "消音",
|
||||
"buttonVolumeUnmute": "消音を解除",
|
||||
"buttonAutoPlay": "自動再生",
|
||||
"buttonShuffle": "シャッフル",
|
||||
"buttonForward": "進む",
|
||||
"buttonRewind": "戻る",
|
||||
"buttonLyrics": "歌詞",
|
||||
"nowplayingDesc": "**現在再生中:**\n```{0}```",
|
||||
"nowplayingField": "次に再生する曲:",
|
||||
"nowplayingLink": "{0}で聴く",
|
||||
"connect": "{0}に接続しました",
|
||||
"live": "ライブ",
|
||||
"playlistLoad": " 🎶 プレイリスト**{0}**をキューに`{1}`曲追加しました。",
|
||||
"trackLoad": "**{2}**の**[{0}](<{1}>)** (`{3}`)を再生を開始するために追加しました。\n",
|
||||
"trackLoad_pos": "**{2}**の**[{0}](<{1}>)** (`{3}`)をキューの位置**{4}**に追加しました。\n",
|
||||
"searchTitle": "検索クエリ:{0}",
|
||||
"searchDesc": "➥ プラットフォーム:{0} **{1}**\n➥ 結果:**{2}**\n\n{3}",
|
||||
"searchWait": "キューに追加したい曲を選択してください。",
|
||||
"searchTimeout": "検索がタイムアウトしました。後でもう一度お試しください。",
|
||||
"searchSuccess": "曲がキューに追加されました。",
|
||||
"queueTitle": "キューに追加された曲:",
|
||||
"historyTitle": "再生履歴:",
|
||||
"viewTitle": "音楽キュー",
|
||||
"viewDesc": "**現在再生中:[ここをクリックして聴く]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "ページ:{0}/{1} | 合計再生時間:{2}",
|
||||
"pauseError": "プレイヤーはすでに一時停止しています。",
|
||||
"pauseVote": "{0}が曲を一時停止することに賛成しました。[{1}/{2}]",
|
||||
"paused": "{0}がプレイヤーを一時停止しました。",
|
||||
"resumeError": "プレイヤーは一時停止していません。",
|
||||
"resumeVote": "{0}が曲を再開することに賛成しました。[{1}/{2}]",
|
||||
"resumed": "{0}がプレイヤーを再開しました。",
|
||||
"shuffleError": "シャッフルする前にキューに曲を追加してください。",
|
||||
"shuffleVote": "{0}がキューをシャッフルすることに賛成しました。[{1}/{2}]",
|
||||
"shuffled": "キューがシャッフルされました。",
|
||||
"skipError": "スキップする曲はありません。",
|
||||
"skipVote": "{0}が曲をスキップすることに賛成しました。[{1}/{2}]",
|
||||
"skipped": "{0}が曲をスキップしました。",
|
||||
"backVote": "{0}が前の曲にスキップすることに賛成しました。[{1}/{2}]",
|
||||
"backed": "`{0}`が前の曲にスキップしました。",
|
||||
"leaveVote": "{0}がプレイヤーを停止することに賛成しました。[{1}/{2}]",
|
||||
"left": "`{0}`がプレイヤーを停止しました。",
|
||||
"seek": "プレイヤーを **{0}** に設定しました。",
|
||||
"repeat": "リピートモードが `{0}` に設定されました。",
|
||||
"cleared": "`{0}`のすべてのトラックをクリアしました。",
|
||||
"removed": "`{0}`のトラックがキューから削除されました。",
|
||||
"forward": "プレイヤーを **{0}** に進めました。",
|
||||
"rewind": "プレイヤーを **{0}** に戻しました。",
|
||||
"replay": "現在の曲をリプレイします。",
|
||||
"swapped": "`{0}`と`{1}`が交換されました。",
|
||||
"moved": "`{0}`を`{1}`に移動しました。",
|
||||
"autoplay": "オートプレイモードが **{0}** に設定されました。",
|
||||
"notdj": "DJではありません。現在のDJは{0}です。",
|
||||
"djToMe": "自分自身またはボットにDJ権限を移行することはできません。",
|
||||
"djNotInChannel": "`{0}`はボイスチャンネルにいません。",
|
||||
"djswap": "DJの役割を`{0}`に移行しました。",
|
||||
"voicelinkQueueFull": "申し訳ありませんが、キュー内の曲数が最大値の`{0}`に達しました!",
|
||||
"voicelinkOutofList": "有効なトラックインデックスを指定してください!",
|
||||
"voicelinkDuplicateTrack": "申し訳ありませんが、このトラックは既にキューに存在します。",
|
||||
"decodeError": "ファイルのデコード中に問題が発生しました!",
|
||||
"invalidStartTime": "無効な開始時間!時間は `00:00` と `{0}` の間に設定する必要があります。",
|
||||
"invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。",
|
||||
"invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。",
|
||||
"setStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。",
|
||||
"createSongRequestChannel": "曲のリクエストチャンネル ({0}) が作成されました!そのチャンネルで、曲名または URL を使用して任意の曲をリクエストできます。ボットのプレフィックスは必要ありません。"
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ コマンドの実行中に何かが間違っています!後でもう一度試してください。または、より詳しいサポートを求めるために当社のDiscordサーバーに参加してください。",
|
||||
"missingIntents": "申し訳ありませんが、このコマンドは実行できません。ボットに必要なリクエストインテントが不足しています:`({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "有効化",
|
||||
"disabled": "無効化",
|
||||
"live": "ライブ"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "再試行してください!ノードが再接続した後。",
|
||||
"noPlaySource": "再生可能なソースが見つかりません!",
|
||||
"noPlayer": "このサーバーにプレイヤーが見つかりません。",
|
||||
"noTrackPlaying": "現在再生中の曲はありません",
|
||||
"noTrackFound": "そのクエリで曲が見つかりませんでした!有効なURLを入力してください。"
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "スキップする曲を選択してください...",
|
||||
"filterSelect": "適用するフィルターを選択してください..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "戻る",
|
||||
"pause": "一時停止",
|
||||
"resume": "再開",
|
||||
"skip": "スキップ",
|
||||
"leave": "退出",
|
||||
"loopOff": "オフ",
|
||||
"loopTrack": "トラック",
|
||||
"loopQueue": "キュー",
|
||||
"volumeUp": "音量を上げる",
|
||||
"volumeDown": "音量を下げる",
|
||||
"volumeMute": "消音",
|
||||
"volumeUnmute": "消音を解除",
|
||||
"autoPlay": "自動再生",
|
||||
"shuffle": "シャッフル",
|
||||
"forward": "進む",
|
||||
"rewind": "戻る",
|
||||
"lyrics": "歌詞"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**現在再生中:**\n```{0}```",
|
||||
"nowplayingField": "次に再生する曲:",
|
||||
"nowplayingLink": "{0}で聴く",
|
||||
"playlistLoad": " 🎶 プレイリスト**{0}**をキューに`{1}`曲追加しました。",
|
||||
"trackLoad": "**{2}**の**[{0}](<{1}>)** (`{3}`)を再生を開始するために追加しました。\n",
|
||||
"trackLoadPos": "**{2}**の**[{0}](<{1}>)** (`{3}`)をキューの位置**{4}**に追加しました。\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "プレイヤーはすでに一時停止しています。",
|
||||
"vote": "{0}が曲を一時停止することに賛成しました。[{1}/{2}]",
|
||||
"success": "{0}がプレイヤーを一時停止しました。"
|
||||
},
|
||||
"resume": {
|
||||
"error": "プレイヤーは一時停止していません。",
|
||||
"vote": "{0}が曲を再開することに賛成しました。[{1}/{2}]",
|
||||
"success": "{0}がプレイヤーを再開しました。"
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "シャッフルする前にキューに曲を追加してください。",
|
||||
"vote": "{0}がキューをシャッフルすることに賛成しました。[{1}/{2}]",
|
||||
"success": "キューがシャッフルされました。"
|
||||
},
|
||||
"skip": {
|
||||
"error": "スキップする曲はありません。",
|
||||
"vote": "{0}が曲をスキップすることに賛成しました。[{1}/{2}]",
|
||||
"success": "{0}が曲をスキップしました。"
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0}が前の曲にスキップすることに賛成しました。[{1}/{2}]",
|
||||
"success": "`{0}`が前の曲にスキップしました。"
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0}がプレイヤーを停止することに賛成しました。[{1}/{2}]",
|
||||
"success": "`{0}`がプレイヤーを停止しました。"
|
||||
},
|
||||
"seek": "プレイヤーを **{0}** に設定しました。",
|
||||
"repeat": "リピートモードが `{0}` に設定されました。",
|
||||
"forward": "プレイヤーを **{0}** に進めました。",
|
||||
"rewind": "プレイヤーを **{0}** に戻しました。",
|
||||
"replay": "現在の曲をリプレイします。",
|
||||
"autoplay": "オートプレイモードが **{0}** に設定されました。"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "接続する音声チャンネルがありません。提供するか、参加してください。",
|
||||
"alreadyConnected": "すでに音声チャンネルに接続しています。",
|
||||
"noPermission": "申し訳ありません!私はあなたの音声チャンネルに参加または話すための許可がありません。",
|
||||
"notInChannel": "{0}さん、音声コマンドを使用するには{1}にいる必要があります。音声に接続している場合は再接続してください!",
|
||||
"connect": "{0}に接続しました"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "ごめんなさい!曲リクエストチャンネルを作成する権限がありません。",
|
||||
"missingPosition": "DJまたは管理者のみが位置を変更できます。",
|
||||
"missingMode": "DJまたは管理者のみがループモードを切り替えることができます。",
|
||||
"missingQueue": "DJまたは管理者のみがキューからトラックを削除できます。",
|
||||
"missingAutoPlay": "DJまたは管理者のみがオートプレイモードを有効化または無効化できます!",
|
||||
"missingFunction": "DJまたは管理者のみがこの機能を使用できます。",
|
||||
"notDj": "DJではありません。現在のDJは{0}です。",
|
||||
"djToSelf": "自分自身またはボットにDJ権限を移行することはできません。",
|
||||
"djNotInChannel": "`{0}`はボイスチャンネルにいません。",
|
||||
"djSwapped": "DJの役割を`{0}`に移行しました。"
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。",
|
||||
"languageChanged": "「{0}」言語パックに正常に変更しました。",
|
||||
"prefixSet": "完了!あなたのサーバーのプレフィックスは今や「{0}」です。 `{1}ping`を実行してテストしてみてください。",
|
||||
"djSet": "{0}にDJを設定しました。",
|
||||
"queueModeSet": "キューモードを「{0}」に設定しました。",
|
||||
"mode247": "今、24/7モードは「{0}」です。",
|
||||
"bypassVote": "今、投票システムは「{0}」になりました。",
|
||||
"volumeSet": "音量を「{0}%」に設定しました。",
|
||||
"controllerToggled": "今、音楽コントローラーは「{0}」です。",
|
||||
"duplicateTrackToggled": "今、キュー内の重複トラックを防止するための設定は「{0}」です。",
|
||||
"controllerMsgToggled": "現在、音楽コントローラーから `{0}` 件のメッセージがあります。",
|
||||
"silentMsgToggled": "あなたは{0}サイレントメッセージを持っています。",
|
||||
"resetDone": "`{0}`をサーバーのデフォルトにリセットしました。",
|
||||
"resetNotFound": "無効な設定名です。次のいずれかを使用してください: {0}"
|
||||
},
|
||||
"menu": "サーバー設定| {0}",
|
||||
"basic": {
|
||||
"title": "❤️ 基本情報:",
|
||||
"value": "```プレフィックス:{0}\n言語:{1}\n音楽コントローラーを有効にする:{2}\nDJロール:@{3}\n投票バイパス:{4}\n24/7:{5}\nデフォルトボリューム:{6}%\n再生時間:{7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 キュー情報:",
|
||||
"value": "```キューモード:{0}\n最大曲数:{1}\n重複トラックを許可する:{2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 音声ステータス情報:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ 権限:",
|
||||
"value": "```{0} 管理者\n{1} ギルド管理\n{2} チャンネル管理\n{3} メッセージ管理```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "ボット情報:",
|
||||
"title2": "プレーヤー情報:",
|
||||
"field1": "```シャードID:{0}/{1}\nシャードレイテンシ:{2:.3f}s {3}\nリージョン:{4}```",
|
||||
"field2": "```ノード:{0} - {1:.3f}s\nプレイヤー数:{2}\n音声リージョン:{3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "`{0}` フィルターを適用します。",
|
||||
"cleared": "効果音がクリアされました!",
|
||||
"tagInUse": "このサウンドエフェクトはすでに使用されています!削除するには/cleareffect <Tag>を使用してください。"
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "{0}のすべてのプレイリスト",
|
||||
"headers": "ID:,時間:,名前:,トラック:",
|
||||
"footer": "プレイリストをキューに追加するには、/playlist play [playlist]を入力してください。",
|
||||
"detailTitle": "プレイリストビューアー",
|
||||
"detailDesc": "```名前: {0} [{1}]\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```",
|
||||
"permsValue": "📖 読み込み: ✓ ✍🏽 書き込み: {0} 🗑️ 削除: {1}",
|
||||
"permsValue2": "📖 読み込み: {0}",
|
||||
"trackList": "トラック",
|
||||
"footer2": "トータルダレーション: {0}",
|
||||
"playlistSelect": "表示するプレイリストを選択してください..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "プレイリスト[{0}]が見つかりません。すべてのプレイリストを表示するには、/playlist viewを入力してください。",
|
||||
"noAccess": "申し訳ありません!このプレイリストにアクセスすることはできません!",
|
||||
"noTrack": "申し訳ありません!プレイリスト[{0}]にはトラックがありません。",
|
||||
"notAllowed": "リンクまたは共有プレイリストでは、このコマンドは許可されていません。",
|
||||
"nameOverLimit": "申し訳ありません!プレイリストの名前は10文字を超えることはできません。",
|
||||
"sameName": "申し訳ありません!この名前は新しい名前と同じであってはなりません。",
|
||||
"deleteDefault": "デフォルトのプレイリストを削除することはできません。",
|
||||
"limitReached": " {0}個以上のプレイリストを作成することはできません!",
|
||||
"exists": "プレイリスト[{0}]はすでに存在します。",
|
||||
"invalidUrl": "有効な公開プレイリストのリンクを入力してください。",
|
||||
"trackLimitReached": "この制限に達しました!プレイリストには{0}曲しか追加できません。",
|
||||
"playlistLinkNotAllowed": "プレイリストリンクを使用することはできません。",
|
||||
"streamNotAllowed": "ストリーミングビデオをプレイリストに追加することはできません。",
|
||||
"positionNotFound": "プレイリスト[{1}]から位置{0}を見つけることができませんでした!",
|
||||
"streaming": "ストリーミングビデオをプレイリストに追加することはできません!",
|
||||
"track": "トラックをプレイリストに追加する際に問題が発生しました!",
|
||||
"trackLimited": "制限に達しました!プレイリストには{0}曲しか追加できません。",
|
||||
"trackRepeated": "すでにプレイリストに同じトラックがあります!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "プレイリスト[{0}]をキューに{1}曲追加しました。",
|
||||
"removed": "プレイリスト[{0}]を削除しました。",
|
||||
"created": "{0}プレイリストを作成しました。詳細については、/playlist viewを入力してください。",
|
||||
"renamed": "{0}を{1}に名前を変更しました。",
|
||||
"trackRemoved": "👋 {1}さんのプレイリスト[{2}]から**{0}**を削除しました。",
|
||||
"cleared": "プレイリスト[{0}]を正常にクリアしました。",
|
||||
"trackAdded": "❤️ **{0}**を{1}のプレイリスト[`{2}`]に追加しました!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "申し訳ありません!自分に招待状を送信することはできません。",
|
||||
"sendErrorBot": "申し訳ありません!ボットに招待状を送信することはできません。",
|
||||
"belongs": "申し訳ありません!このプレイリストは<@{0}>さんのものです。",
|
||||
"alreadyShared": "申し訳ありません!このプレイリストは{0}さんと共有されています。",
|
||||
"alreadySent": "申し訳ありません!以前に招待状を送信しました。",
|
||||
"noAccount": "{0}さんはプレイリストアカウントを作成していません。",
|
||||
"invitationSent": "{0}さんに招待状を送信しました。"
|
||||
},
|
||||
"inbox": {
|
||||
"full": "申し訳ありません!{0}さんの受信トレイはいっぱいです。",
|
||||
"noMessages": "受信トレイにメッセージはありません。"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "検索コマンドはリンクをサポートしていません!",
|
||||
"title": "検索クエリ:{0}",
|
||||
"desc": "➥ プラットフォーム:{0} **{1}**\n➥ 結果:**{2}**\n\n{3}",
|
||||
"wait": "キューに追加したい曲を選択してください。",
|
||||
"success": "曲がキューに追加されました。"
|
||||
},
|
||||
"voting": {
|
||||
"voted": "投票しました!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "時間の形式が間違っています。例:2:42または12:39:31",
|
||||
"invalidStartTime": "無効な開始時間!時間は `00:00` と `{0}` の間に設定する必要があります。",
|
||||
"invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "歌詞が見つかりませんでした。/lyrics <曲名> <アーティスト>と入力して歌詞を検索してください。"
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "キューに追加された曲:",
|
||||
"cleared": "`{0}`のすべてのトラックをクリアしました。",
|
||||
"removed": "`{0}`のトラックがキューから削除されました。",
|
||||
"swapped": "`{0}`と`{1}`が交換されました。",
|
||||
"moved": "`{0}`を`{1}`に移動しました。"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "再生履歴:",
|
||||
"title": "音楽キュー",
|
||||
"desc": "**現在再生中:[ここをクリックして聴く]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "申し訳ありませんが、キュー内の曲数が最大値の`{0}`に達しました!",
|
||||
"outOfList": "有効なトラックインデックスを指定してください!",
|
||||
"duplicateTrack": "申し訳ありませんが、このトラックは既にキューに存在します。"
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "曲のリクエストチャンネル ({0}) が作成されました!そのチャンネルで、曲名または URL を使用して任意の曲をリクエストできます。ボットのプレフィックスは必要ありません。"
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "前へ",
|
||||
"next": "次へ",
|
||||
"play": "再生",
|
||||
"share": "共有",
|
||||
"export": "エクスポート",
|
||||
"delete": "削除",
|
||||
"page": {
|
||||
"title": "ページ番号を入力",
|
||||
"field": "ページ番号"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/KO.json
436
langs/KO.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ 명령어 실행 중 오류가 발생했습니다! 나중에 다시 시도하거나 추가 지원을 위해 디스코드 서버에 참여하십시오.",
|
||||
"enabled": "활성화됨",
|
||||
"disabled": "비활성화됨",
|
||||
"nodeReconnect": "다시 시도하세요! 노드가 다시 연결될 때까지 기다려주세요.",
|
||||
"noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 참여하십시오.",
|
||||
"alreadyConnected": "이미 음성 채널에 연결되어 있습니다.",
|
||||
"noPermission": "죄송합니다! 음성 채널에 참여하거나 말할 권한이 없습니다.",
|
||||
"noCreatePermission": "죄송합니다! 노래 요청 채널을 생성할 권한이 없습니다.",
|
||||
"noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!",
|
||||
"noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.",
|
||||
"notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.",
|
||||
"missingIntents": "죄송하지만 이 명령을 실행할 수 없습니다. 봇에 필요한 요청 의도가 없습니다:`({0})`.",
|
||||
"languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.",
|
||||
"changedLanguage": "성공적으로 `{0}` 언어 팩으로 변경되었습니다.",
|
||||
"setPrefix": "완료되었습니다! 이 서버에서 내 접두사는 이제 `{0}`입니다. `{1}ping`을 실행하여 테스트해보세요.",
|
||||
"setDJ": "DJ를 {0}(으)로 설정했습니다.",
|
||||
"setQueue": "큐 모드를 `{0}`(으)로 설정했습니다.",
|
||||
"247": "이제 24/7 모드에서 `{0}`으로 변경되었습니다.",
|
||||
"bypassVote": "이제 `{0}` 투표 시스템을 사용할 수 있습니다.",
|
||||
"setVolume": "볼륨을 `{0}`%로 설정했습니다.",
|
||||
"toggleController": "이제 음악 컨트롤러가 `{0}`(으)로 설정되었습니다.",
|
||||
"toggleDuplicateTrack": "이제 대기열에서 중복된 트랙을 방지하기 위해 `{0}`(으)로 설정되었습니다.",
|
||||
"toggleControllerMsg": "이제 음악 컨트롤러로부터 `{0}` 메시지가 있습니다.",
|
||||
"toggleSilentMsg": "당신은 {0} 침묵 메시지를 사용하고 있습니다.",
|
||||
"settingsMenu": "서버 설정 | {0}",
|
||||
"settingsTitle": "❤️ 기본 정보:",
|
||||
"settingsValue": "```접두사: {0}\n언어: {1}\n음악 컨트롤러 사용: {2}\nDJ 역할: @{3}\n투표 우회: {4}\n24/7 모드: {5}\n기본 볼륨: {6}%\n재생 시간: {7}```",
|
||||
"settingsTitle2": "🔗 대기열 정보:",
|
||||
"settingsValue2": "```큐 모드: {0}\n최대 노래: {1}\n중복된 트랙 허용: {2}```",
|
||||
"settingsTitle3": "🎤 음성 상태 정보:",
|
||||
"settingsPermTitle": "✨ 권한:",
|
||||
"settingsPermValue": "```{0} 관리자\n{1} 서버 관리\n{2} 채널 관리\n{3} 메시지 관리```",
|
||||
"pingTitle1": "봇 정보:",
|
||||
"pingTitle2": "플레이어 정보:",
|
||||
"pingField1": "```쉬드 ID: {0}/{1}\n쉬드 대기 시간: {2:.3f}s {3}\n지역: {4}```",
|
||||
"pingField2": "```노드: {0} - {1:.3f}s\n플레이어: {2}\n음성 지역: {3}```",
|
||||
"addEffect": "`{0}` 필터를 적용하세요.",
|
||||
"clearEffect": "효과가 삭제되었습니다!",
|
||||
"filterTagAlreadyInUse": "이 필터는 이미 사용 중입니다! 삭제하려면 /cleareffect <Tag>를 사용하십시오.",
|
||||
"playlistViewTitle": "📜 {0}의 모든 재생 목록",
|
||||
"playlistViewHeaders": "ID:,시간:,이름:,트랙:",
|
||||
"playlistFooter": "/playlist play [재생 목록]을 입력하여 재생 목록을 대기열에 추가하세요.",
|
||||
"playlistNotFound": "재생 목록 [{0}]을(를) 찾을 수 없습니다. 모든 재생 목록을 보려면 /playlist view를 입력하세요.",
|
||||
"playlistNotAccess": "죄송합니다! 이 재생 목록에 액세스할 수 없습니다!",
|
||||
"playlistNoTrack": "죄송합니다! 재생 목록 [{0}]에 트랙이 없습니다.",
|
||||
"playlistNotAllow": "이 명령어는 연결된 및 공유된 재생 목록에서 허용되지 않습니다.",
|
||||
"playlistPlay": "재생 목록 [{0}]을(를) 대기열에 {1}곡 추가했습니다.",
|
||||
"playlistOverText": "죄송합니다! 재생 목록의 이름은 10자를 초과할 수 없습니다.",
|
||||
"playlistSameName": "죄송합니다! 이 이름은 새 이름과 동일할 수 없습니다.",
|
||||
"playlistDeleteError": "기본 재생 목록은 삭제할 수 없습니다.",
|
||||
"playlistRemove": "재생 목록 [{0}]을(를) 삭제했습니다.",
|
||||
"playlistSendErrorPlayer": "죄송합니다! 자신에게 초대장을 보낼 수 없습니다.",
|
||||
"playlistSendErrorBot": "죄송합니다! 봇에게 초대장을 보낼 수 없습니다.",
|
||||
"playlistBelongs": "죄송합니다! 이 재생 목록은 <@{0}>님의 것입니다.",
|
||||
"playlistShare": "죄송합니다! 이 재생 목록은 {0}과(와) 공유되었습니다.",
|
||||
"playlistSent": "죄송합니다! 이미 초대장을 보냈습니다.",
|
||||
"noPlaylistAcc": "{0}님은 재생 목록 계정을 만들지 않았습니다.",
|
||||
"overPlaylistCreation": "더 이상 {0}개 이상의 재생 목록을 만들 수 없습니다!",
|
||||
"playlistExists": "재생 목록 [{0}]이(가) 이미 있습니다.",
|
||||
"playlistNotInvalidUrl": "유효한 링크 또는 공개 Spotify 또는 YouTube 재생 목록 링크를 입력하세요.",
|
||||
"playlistCreated": "{0} 재생 목록을 만들었습니다. 자세한 내용은 /playlist view를 입력하세요.",
|
||||
"playlistRenamed": "{0}을(를) {1}(으)로 이름을 바꿨습니다.",
|
||||
"playlistLimitTrack": "죄송합니다! 재생 목록에 추가할 수 있는 노래는 {0}곡까지입니다.",
|
||||
"playlistPlaylistLink": "재생 목록 링크를 사용할 수 없습니다.",
|
||||
"playlistStream": "스트리밍 비디오를 재생 목록에 추가할 수 없습니다.",
|
||||
"playlistPositionNotFound": "재생 목록 [{1}]에서 위치 {0}을(를) 찾을 수 없습니다!",
|
||||
"playlistRemoved": "👋 {1}님의 재생 목록 [{2}]에서 **{0}**을(를) 제거했습니다.",
|
||||
"playlistClear": "재생 목록 [{0}]을(를) 성공적으로 지웠습니다.",
|
||||
"playlistView": "재생 목록 뷰어",
|
||||
"playlistViewDesc": "```이름 | ID: {0} | {1}\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 읽기: ✓ ✍🏽 쓰기: {0} 🗑️ 삭제: {1}",
|
||||
"playlistViewPermsValue2": "📖 읽기: {0}",
|
||||
"playlistViewTrack": "트랙",
|
||||
"playlistViewPage": "페이지: {0}/{1} | 총 길이: {2}",
|
||||
"inboxFull": "죄송합니다! {0}님의 받은 편지함이 가득 찼습니다.",
|
||||
"inboxNoMsg": "받은 편지함에 메시지가 없습니다.",
|
||||
"invitationSent": "{0}님에게 초대장을 보냈습니다.",
|
||||
"notInChannel": "{0}, 음성 명령을 사용하려면 {1}에 있어야합니다. 음성 채널에 있지 않으면 다시 참여하세요!",
|
||||
"noTrackPlaying": "현재 재생중인 곡이 없습니다.",
|
||||
"noTrackFound": "해당 쿼리로 곡을 찾을 수 없습니다. 유효한 URL을 제공해주세요.",
|
||||
"noLinkSupport": "검색 명령은 링크를 지원하지 않습니다!",
|
||||
"voted": "투표하셨습니다!",
|
||||
"missingPosPerm": "DJ 또는 관리자만 위치를 변경할 수 있습니다.",
|
||||
"missingModePerm": "DJ 또는 관리자만 루프 모드를 전환할 수 있습니다.",
|
||||
"missingQueuePerm": "DJ 또는 관리자만 대기열에서 곡을 제거할 수 있습니다.",
|
||||
"missingAutoPlayPerm": "DJ 또는 관리자만 자동재생 모드를 활성화하거나 비활성화할 수 있습니다!",
|
||||
"missingFunctionPerm": "DJ 또는 관리자만이 이 기능을 사용할 수 있습니다.",
|
||||
"timeFormatError": "잘못된 시간 형식입니다. 예: 2:42 또는 12:39:31",
|
||||
"lyricsNotFound": "가사를 찾을 수 없습니다. 가사를 찾으려면 /lyrics <노래 제목> <작곡가>를 입력하세요.",
|
||||
"missingTrackInfo": "일부 트랙 정보가 누락되었습니다.",
|
||||
"noVoiceChannel": "음성 채널을 찾을 수 없습니다!",
|
||||
"playlistAddError": "스트리밍 비디오를 재생목록에 추가할 수 없습니다!",
|
||||
"playlistAddError2": "재생목록에 곡을 추가하는 중 문제가 발생했습니다!",
|
||||
"playlistLimited": "한 재생목록에 최대 {0}곡까지 추가 가능합니다. 이제 한도에 도달했습니다!",
|
||||
"playlistRepeated": "재생목록에 이미 같은 곡이 있습니다!",
|
||||
"playlistAdded": "❤️ **{0}**을(를) {1}의 재생목록 [`{2}`] 에 추가했습니다!",
|
||||
"playerDropdown": "건너뛰기 할 노래를 선택하세요...",
|
||||
"playerFilter": "적용할 필터를 선택하세요...",
|
||||
"buttonBack": "이전",
|
||||
"buttonPause": "일시정지",
|
||||
"buttonResume": "재개",
|
||||
"buttonSkip": "스킵",
|
||||
"buttonLeave": "나가기",
|
||||
"buttonLoop": "반복",
|
||||
"buttonVolumeUp": "볼륨 높이기",
|
||||
"buttonVolumeDown": "볼륨 낮추기",
|
||||
"buttonVolumeMute": "음소거",
|
||||
"buttonVolumeUnmute": "음소거 해제",
|
||||
"buttonAutoPlay": "자동재생",
|
||||
"buttonShuffle": "셔플",
|
||||
"buttonForward": "앞으로",
|
||||
"buttonRewind": "뒤로",
|
||||
"buttonLyrics": "가사",
|
||||
"nowplayingDesc": "**현재 재생중인 곡:**\n```{0}```",
|
||||
"nowplayingField": "다음 곡:",
|
||||
"nowplayingLink": "{0}에서 듣기",
|
||||
"connect": "{0}에 연결되었습니다.",
|
||||
"live": "라이브",
|
||||
"playlistLoad": "재생목록 **{0}**을(를) 대기열에 `{1}`개의 곡과 함께 추가했습니다.",
|
||||
"trackLoad": "**{2}**의 **[{0}](<{1}>)** (`{3}`)를 재생목록에 추가하고 재생을 시작합니다.\n",
|
||||
"trackLoad_pos": "**{2}**의 **[{0}](<{1}>)** (`{3}`)를 대기열의 **{4}**번째로 추가합니다.\n",
|
||||
"searchTitle": "검색 쿼리: {0}",
|
||||
"searchDesc": "➥ 플랫폼: {0} **{1}**\n➥ 결과: **{2}**\n\n{3}",
|
||||
"searchWait": "대기열에 추가할 곡을 선택하세요.",
|
||||
"searchTimeout": "검색이 시간 초과되었습니다. 나중에 다시 시도해주세요.",
|
||||
"searchSuccess": "곡이 대기열에 추가되었습니다.",
|
||||
"queueTitle": "다음 대기열:",
|
||||
"historyTitle": "이전 곡 대기열:",
|
||||
"viewTitle": "음악 대기열",
|
||||
"viewDesc": "**현재 재생중: [여기를 클릭하여 듣기]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "페이지: {0}/{1} | 총 재생 시간: {2}",
|
||||
"pauseError": "플레이어가 이미 일시정지되었습니다.",
|
||||
"pauseVote": "{0}님이 노래 일시정지 투표를 했습니다. [{1}/{2}]",
|
||||
"paused": "{0}님이 플레이어를 일시정지했습니다.",
|
||||
"resumeError": "플레이어가 일시정지되지 않았습니다.",
|
||||
"resumeVote": "{0}님이 노래 재생 투표를 했습니다. [{1}/{2}]",
|
||||
"resumed": "{0}님이 플레이어를 재생했습니다.",
|
||||
"shuffleError": "셔플하기 전에 더 많은 노래를 추가해주세요.",
|
||||
"shuffleVote": "{0}님이 플레이어 셔플 투표를 했습니다. [{1}/{2}]",
|
||||
"shuffled": "플레이어 큐가 셔플되었습니다.",
|
||||
"skipError": "건너뛸 노래가 없습니다.",
|
||||
"skipVote": "{0}님이 노래 건너뛰기 투표를 했습니다. [{1}/{2}]",
|
||||
"skipped": "{0}님이 노래를 건너뛰었습니다.",
|
||||
"backVote": "{0}님이 이전 노래로 건너뛰기 투표를 했습니다. [{1}/{2}]",
|
||||
"backed": "`{0}`님이 이전 노래로 건너뛰었습니다.",
|
||||
"leaveVote": "{0}님이 플레이어를 중지하기 투표를 했습니다. [{1}/{2}]",
|
||||
"left": "`{0}`님이 플레이어를 중지했습니다.",
|
||||
"seek": "플레이어가 **{0}**로 설정되었습니다.",
|
||||
"repeat": "반복 모드가 `{0}`(으)로 설정되었습니다.",
|
||||
"cleared": " `{0}`의 모든 트랙이 삭제되었습니다.",
|
||||
"removed": " `{0}`개의 트랙이 큐에서 제거되었습니다.",
|
||||
"forward": "플레이어가 **{0}**로 전진되었습니다.",
|
||||
"rewind": "플레이어가 **{0}**로 되감기되었습니다.",
|
||||
"replay": "현재 노래를 다시 재생합니다.",
|
||||
"swapped": "`{0}`와 `{1}`이(가) 스왑되었습니다.",
|
||||
"moved": "`{0}`을(를) `{1}`로 이동했습니다.",
|
||||
"autoplay": "자동 재생 모드가 **{0}**(으)로 변경되었습니다.",
|
||||
"notdj": "당신은 DJ가 아닙니다. 현재 DJ는 {0}입니다.",
|
||||
"djToMe": "자신이나 봇에게 DJ를 이전할 수 없습니다.",
|
||||
"djNotInChannel": "`{0}`님이 음성 채널에 없습니다.",
|
||||
"djswap": "당신은 DJ 권한을 `{0}`님에게 이전했습니다.",
|
||||
"voicelinkQueueFull": "죄송합니다. 큐에 `{0}`개의 트랙을 모두 추가하셨습니다!",
|
||||
"voicelinkOutofList": "유효한 트랙 인덱스를 제공해주세요!",
|
||||
"voicelinkDuplicateTrack": "죄송합니다. 이 트랙은 이미 큐에 있습니다.",
|
||||
"decodeError": "파일 디코딩 중 문제가 발생했습니다!",
|
||||
"invalidStartTime": "효력 없는 시작 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.",
|
||||
"invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.",
|
||||
"invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다.",
|
||||
"setStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다.",
|
||||
"createSongRequestChannel": "노래 요청 채널 ({0})이 생성되었습니다! 해당 채널에서 노래 제목이나 URL로 원하는 노래를 요청할 수 있으며, 봇 접두사를 사용할 필요가 없습니다."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ 명령어 실행 중 오류가 발생했습니다! 나중에 다시 시도하거나 추가 지원을 위해 디스코드 서버에 참여하십시오.",
|
||||
"missingIntents": "죄송하지만 이 명령을 실행할 수 없습니다. 봇에 필요한 요청 의도가 없습니다:`({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "활성화됨",
|
||||
"disabled": "비활성화됨",
|
||||
"live": "라이브"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "다시 시도하세요! 노드가 다시 연결될 때까지 기다려주세요.",
|
||||
"noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!",
|
||||
"noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.",
|
||||
"noTrackPlaying": "현재 재생중인 곡이 없습니다.",
|
||||
"noTrackFound": "해당 쿼리로 곡을 찾을 수 없습니다. 유효한 URL을 제공해주세요."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "건너뛰기 할 노래를 선택하세요...",
|
||||
"filterSelect": "적용할 필터를 선택하세요..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "이전",
|
||||
"pause": "일시정지",
|
||||
"resume": "재개",
|
||||
"skip": "스킵",
|
||||
"leave": "나가기",
|
||||
"loopOff": "끄기",
|
||||
"loopTrack": "트랙",
|
||||
"loopQueue": "대기열",
|
||||
"volumeUp": "볼륨 높이기",
|
||||
"volumeDown": "볼륨 낮추기",
|
||||
"volumeMute": "음소거",
|
||||
"volumeUnmute": "음소거 해제",
|
||||
"autoPlay": "자동재생",
|
||||
"shuffle": "셔플",
|
||||
"forward": "앞으로",
|
||||
"rewind": "뒤로",
|
||||
"lyrics": "가사"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**현재 재생중인 곡:**\n```{0}```",
|
||||
"nowplayingField": "다음 곡:",
|
||||
"nowplayingLink": "{0}에서 듣기",
|
||||
"playlistLoad": "재생목록 **{0}**을(를) 대기열에 `{1}`개의 곡과 함께 추가했습니다.",
|
||||
"trackLoad": "**{2}**의 **[{0}](<{1}>)** (`{3}`)를 재생목록에 추가하고 재생을 시작합니다.\n",
|
||||
"trackLoadPos": "**{2}**의 **[{0}](<{1}>)** (`{3}`)를 대기열의 **{4}**번째로 추가합니다.\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "플레이어가 이미 일시정지되었습니다.",
|
||||
"vote": "{0}님이 노래 일시정지 투표를 했습니다. [{1}/{2}]",
|
||||
"success": "{0}님이 플레이어를 일시정지했습니다."
|
||||
},
|
||||
"resume": {
|
||||
"error": "플레이어가 일시정지되지 않았습니다.",
|
||||
"vote": "{0}님이 노래 재생 투표를 했습니다. [{1}/{2}]",
|
||||
"success": "{0}님이 플레이어를 재생했습니다."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "셔플하기 전에 더 많은 노래를 추가해주세요.",
|
||||
"vote": "{0}님이 플레이어 셔플 투표를 했습니다. [{1}/{2}]",
|
||||
"success": "플레이어 큐가 셔플되었습니다."
|
||||
},
|
||||
"skip": {
|
||||
"error": "건너뛸 노래가 없습니다.",
|
||||
"vote": "{0}님이 노래 건너뛰기 투표를 했습니다. [{1}/{2}]",
|
||||
"success": "{0}님이 노래를 건너뛰었습니다."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0}님이 이전 노래로 건너뛰기 투표를 했습니다. [{1}/{2}]",
|
||||
"success": "`{0}`님이 이전 노래로 건너뛰었습니다."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0}님이 플레이어를 중지하기 투표를 했습니다. [{1}/{2}]",
|
||||
"success": "`{0}`님이 플레이어를 중지했습니다."
|
||||
},
|
||||
"seek": "플레이어가 **{0}**로 설정되었습니다.",
|
||||
"repeat": "반복 모드가 `{0}`(으)로 설정되었습니다.",
|
||||
"forward": "플레이어가 **{0}**로 전진되었습니다.",
|
||||
"rewind": "플레이어가 **{0}**로 되감기되었습니다.",
|
||||
"replay": "현재 노래를 다시 재생합니다.",
|
||||
"autoplay": "자동 재생 모드가 **{0}**(으)로 변경되었습니다."
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 참여하십시오.",
|
||||
"alreadyConnected": "이미 음성 채널에 연결되어 있습니다.",
|
||||
"noPermission": "죄송합니다! 음성 채널에 참여하거나 말할 권한이 없습니다.",
|
||||
"notInChannel": "{0}, 음성 명령을 사용하려면 {1}에 있어야합니다. 음성 채널에 있지 않으면 다시 참여하세요!",
|
||||
"connect": "{0}에 연결되었습니다."
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "죄송합니다! 노래 요청 채널을 생성할 권한이 없습니다.",
|
||||
"missingPosition": "DJ 또는 관리자만 위치를 변경할 수 있습니다.",
|
||||
"missingMode": "DJ 또는 관리자만 루프 모드를 전환할 수 있습니다.",
|
||||
"missingQueue": "DJ 또는 관리자만 대기열에서 곡을 제거할 수 있습니다.",
|
||||
"missingAutoPlay": "DJ 또는 관리자만 자동재생 모드를 활성화하거나 비활성화할 수 있습니다!",
|
||||
"missingFunction": "DJ 또는 관리자만이 이 기능을 사용할 수 있습니다.",
|
||||
"notDj": "당신은 DJ가 아닙니다. 현재 DJ는 {0}입니다.",
|
||||
"djToSelf": "자신이나 봇에게 DJ를 이전할 수 없습니다.",
|
||||
"djNotInChannel": "`{0}`님이 음성 채널에 없습니다.",
|
||||
"djSwapped": "당신은 DJ 권한을 `{0}`님에게 이전했습니다."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.",
|
||||
"languageChanged": "성공적으로 `{0}` 언어 팩으로 변경되었습니다.",
|
||||
"prefixSet": "완료되었습니다! 이 서버에서 내 접두사는 이제 `{0}`입니다. `{1}ping`을 실행하여 테스트해보세요.",
|
||||
"djSet": "DJ를 {0}(으)로 설정했습니다.",
|
||||
"queueModeSet": "큐 모드를 `{0}`(으)로 설정했습니다.",
|
||||
"mode247": "이제 24/7 모드에서 `{0}`으로 변경되었습니다.",
|
||||
"bypassVote": "이제 `{0}` 투표 시스템을 사용할 수 있습니다.",
|
||||
"volumeSet": "볼륨을 `{0}`%로 설정했습니다.",
|
||||
"controllerToggled": "이제 음악 컨트롤러가 `{0}`(으)로 설정되었습니다.",
|
||||
"duplicateTrackToggled": "이제 대기열에서 중복된 트랙을 방지하기 위해 `{0}`(으)로 설정되었습니다.",
|
||||
"controllerMsgToggled": "이제 음악 컨트롤러로부터 `{0}` 메시지가 있습니다.",
|
||||
"silentMsgToggled": "당신은 {0} 침묵 메시지를 사용하고 있습니다.",
|
||||
"resetDone": "`{0}`을(를) 서버 기본값으로 초기화했습니다.",
|
||||
"resetNotFound": "잘못된 설정 이름입니다. 다음 중 하나를 사용하세요: {0}"
|
||||
},
|
||||
"menu": "서버 설정 | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ 기본 정보:",
|
||||
"value": "```접두사: {0}\n언어: {1}\n음악 컨트롤러 사용: {2}\nDJ 역할: @{3}\n투표 우회: {4}\n24/7 모드: {5}\n기본 볼륨: {6}%\n재생 시간: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 대기열 정보:",
|
||||
"value": "```큐 모드: {0}\n최대 노래: {1}\n중복된 트랙 허용: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 음성 상태 정보:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ 권한:",
|
||||
"value": "```{0} 관리자\n{1} 서버 관리\n{2} 채널 관리\n{3} 메시지 관리```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "봇 정보:",
|
||||
"title2": "플레이어 정보:",
|
||||
"field1": "```쉬드 ID: {0}/{1}\n쉬드 대기 시간: {2:.3f}s {3}\n지역: {4}```",
|
||||
"field2": "```노드: {0} - {1:.3f}s\n플레이어: {2}\n음성 지역: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "`{0}` 필터를 적용하세요.",
|
||||
"cleared": "효과가 삭제되었습니다!",
|
||||
"tagInUse": "이 필터는 이미 사용 중입니다! 삭제하려면 /cleareffect <Tag>를 사용하십시오."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "{0}의 모든 재생 목록",
|
||||
"headers": "ID:,시간:,이름:,트랙:",
|
||||
"footer": "/playlist play [재생 목록]을 입력하여 재생 목록을 대기열에 추가하세요.",
|
||||
"detailTitle": "재생 목록 뷰어",
|
||||
"detailDesc": "```이름: {0} [{1}]\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```",
|
||||
"permsValue": "📖 읽기: ✓ ✍🏽 쓰기: {0} 🗑️ 삭제: {1}",
|
||||
"permsValue2": "📖 읽기: {0}",
|
||||
"trackList": "트랙",
|
||||
"footer2": "총 길이: {0}",
|
||||
"playlistSelect": "보기 위해 재생 목록을 선택하세요..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "재생 목록 [{0}]을(를) 찾을 수 없습니다. 모든 재생 목록을 보려면 /playlist view를 입력하세요.",
|
||||
"noAccess": "죄송합니다! 이 재생 목록에 액세스할 수 없습니다!",
|
||||
"noTrack": "죄송합니다! 재생 목록 [{0}]에 트랙이 없습니다.",
|
||||
"notAllowed": "이 명령어는 연결된 및 공유된 재생 목록에서 허용되지 않습니다.",
|
||||
"nameOverLimit": "죄송합니다! 재생 목록의 이름은 10자를 초과할 수 없습니다.",
|
||||
"sameName": "죄송합니다! 이 이름은 새 이름과 동일할 수 없습니다.",
|
||||
"deleteDefault": "기본 재생 목록은 삭제할 수 없습니다.",
|
||||
"limitReached": "더 이상 {0}개 이상의 재생 목록을 만들 수 없습니다!",
|
||||
"exists": "재생 목록 [{0}]이(가) 이미 있습니다.",
|
||||
"invalidUrl": "유효한 공개 재생목록 링크를 입력하세요.",
|
||||
"trackLimitReached": "죄송합니다! 재생 목록에 추가할 수 있는 노래는 {0}곡까지입니다.",
|
||||
"playlistLinkNotAllowed": "재생 목록 링크를 사용할 수 없습니다.",
|
||||
"streamNotAllowed": "스트리밍 비디오를 재생 목록에 추가할 수 없습니다.",
|
||||
"positionNotFound": "재생 목록 [{1}]에서 위치 {0}을(를) 찾을 수 없습니다!",
|
||||
"streaming": "스트리밍 비디오를 재생목록에 추가할 수 없습니다!",
|
||||
"track": "재생목록에 곡을 추가하는 중 문제가 발생했습니다!",
|
||||
"trackLimited": "한 재생목록에 최대 {0}곡까지 추가 가능합니다. 이제 한도에 도달했습니다!",
|
||||
"trackRepeated": "재생목록에 이미 같은 곡이 있습니다!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "재생 목록 [{0}]을(를) 대기열에 {1}곡 추가했습니다.",
|
||||
"removed": "재생 목록 [{0}]을(를) 삭제했습니다.",
|
||||
"created": "{0} 재생 목록을 만들었습니다. 자세한 내용은 /playlist view를 입력하세요.",
|
||||
"renamed": "{0}을(를) {1}(으)로 이름을 바꿨습니다.",
|
||||
"trackRemoved": "👋 {1}님의 재생 목록 [{2}]에서 **{0}**을(를) 제거했습니다.",
|
||||
"cleared": "재생 목록 [{0}]을(를) 성공적으로 지웠습니다.",
|
||||
"trackAdded": "❤️ **{0}**을(를) {1}의 재생목록 [`{2}`] 에 추가했습니다!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "죄송합니다! 자신에게 초대장을 보낼 수 없습니다.",
|
||||
"sendErrorBot": "죄송합니다! 봇에게 초대장을 보낼 수 없습니다.",
|
||||
"belongs": "죄송합니다! 이 재생 목록은 <@{0}>님의 것입니다.",
|
||||
"alreadyShared": "죄송합니다! 이 재생 목록은 {0}과(와) 공유되었습니다.",
|
||||
"alreadySent": "죄송합니다! 이미 초대장을 보냈습니다.",
|
||||
"noAccount": "{0}님은 재생 목록 계정을 만들지 않았습니다.",
|
||||
"invitationSent": "{0}님에게 초대장을 보냈습니다."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "죄송합니다! {0}님의 받은 편지함이 가득 찼습니다.",
|
||||
"noMessages": "받은 편지함에 메시지가 없습니다."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "검색 명령은 링크를 지원하지 않습니다!",
|
||||
"title": "검색 쿼리: {0}",
|
||||
"desc": "➥ 플랫폼: {0} **{1}**\n➥ 결과: **{2}**\n\n{3}",
|
||||
"wait": "대기열에 추가할 곡을 선택하세요.",
|
||||
"success": "곡이 대기열에 추가되었습니다."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "투표하셨습니다!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "잘못된 시간 형식입니다. 예: 2:42 또는 12:39:31",
|
||||
"invalidStartTime": "효력 없는 시작 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.",
|
||||
"invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다."
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "가사를 찾을 수 없습니다. 가사를 찾으려면 /lyrics <노래 제목> <작곡가>를 입력하세요."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "다음 대기열:",
|
||||
"cleared": " `{0}`의 모든 트랙이 삭제되었습니다.",
|
||||
"removed": " `{0}`개의 트랙이 큐에서 제거되었습니다.",
|
||||
"swapped": "`{0}`와 `{1}`이(가) 스왑되었습니다.",
|
||||
"moved": "`{0}`을(를) `{1}`로 이동했습니다."
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "이전 곡 대기열:",
|
||||
"title": "음악 대기열",
|
||||
"desc": "**현재 재생중: [여기를 클릭하여 듣기]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "죄송합니다. 큐에 `{0}`개의 트랙을 모두 추가하셨습니다!",
|
||||
"outOfList": "유효한 트랙 인덱스를 제공해주세요!",
|
||||
"duplicateTrack": "죄송합니다. 이 트랙은 이미 큐에 있습니다."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "노래 요청 채널 ({0})이 생성되었습니다! 해당 채널에서 노래 제목이나 URL로 원하는 노래를 요청할 수 있으며, 봇 접두사를 사용할 필요가 없습니다."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "이전",
|
||||
"next": "다음",
|
||||
"play": "재생",
|
||||
"share": "공유",
|
||||
"export": "내보내기",
|
||||
"delete": "삭제",
|
||||
"page": {
|
||||
"title": "페이지 번호 입력",
|
||||
"field": "페이지 번호"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/PL.json
436
langs/PL.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ Coś poszło nie tak podczas wykonywania polecenia. Spróbuj ponownie później lub dołąćz do serwera discord w celu uzyskania pomocy.",
|
||||
"enabled": "włączony",
|
||||
"disabled": "wyłączony",
|
||||
"nodeReconnect": "Spróbuj ponownie! Trwa ponowne łącznie z node'm.",
|
||||
"noChannel": "Brak kanału głosowego do dołącznia. Dołącz do jakiegoś, lub wskaż kanał komendą /connect",
|
||||
"alreadyConnected": "Już połączono z kanałem głosowym.",
|
||||
"noPermission": "Niestety nie mam uprawnień, aby dołączyć do Twojego kanału głosowego lub na nim mówić.",
|
||||
"noCreatePermission": "Niestety, nie mam uprawnień, aby stworzyć kanał do żądania utworów.",
|
||||
"noPlaySource": "Nie można znaleźć żadnych odtwarzalnych źródeł!",
|
||||
"noPlayer": "Nie znaleziono żadnego odtwarzacza na tym serwerze.",
|
||||
"notVote": "To polecenie wymaga głosowania! Wpisz `/vote`, aby uzyskać więcej informacji.",
|
||||
"missingIntents": "Niestety to polecenie nie może zostać wykonane, ponieważ bot nie posiada wymaganej intencji żądania: `({0})`.",
|
||||
"languageNotFound": "Nie znaleziono pakietu językowego! wybierz istniejący pakiet językowy.",
|
||||
"changedLanguage": "Pomyślnie zmieniono pakiet językowy na: `{0}` .",
|
||||
"setPrefix": "Gotowe! Mój prefix na tym serwerze to teraz `{0}`. Spróbuj uruchomić polecenie `{1}ping`, aby go przetestować.",
|
||||
"setDJ": "Ustawiono DJ jako {0}.",
|
||||
"setQueue": "Zmienionio tryb kolejki na `{0}`.",
|
||||
"247": "Tryb 24/7: `{0}`",
|
||||
"bypassVote": "System głosowania: `{0}`",
|
||||
"setVolume": "Ustawiono głośność na `{0}`%",
|
||||
"toggleController": "Kontroler muzyki: `{0}`",
|
||||
"toggleDuplicateTrack": "Unikanie duplikowania pozycji w kolejce: `{0}`",
|
||||
"toggleControllerMsg": "Wiadomości od kontrolera muzyki: `{0}`",
|
||||
"toggleSilentMsg": "Masz {0} ciche wiadomości.",
|
||||
"settingsMenu": "Ustawnienia serwera | {0}",
|
||||
"settingsTitle": "❤️ Podstawowe informacje:",
|
||||
"settingsValue": "```Prefix: {0}\nJęzyk: {1}\nKontroler muzyki: {2}\nRola DJ: @{3}\nOminięcie głosowania: {4}\n24/7: {5}\nDomyślna głośność: {6}%\nCzas odtwarzania: {7}```",
|
||||
"settingsTitle2": "🔗 Informacje o kolejce:",
|
||||
"settingsValue2": "```Tryb kolejki: {0}\nMaksymlna liczba utworów: {1}\nZduplikowane pozycje: {2}```",
|
||||
"settingsTitle3": "🎤 Informacie w statusie kanału głosowego:",
|
||||
"settingsPermTitle": "✨ Uprawnienia:",
|
||||
"settingsPermValue": "```{0} Administrator\n{1} Zarządzanie_serwerem\n{2} Zarządzanie_kanałem\n{3} Zarządzanie_wiadomościami```",
|
||||
"pingTitle1": "Informacje o bocie:",
|
||||
"pingTitle2": "Infomracje o odtwarzaczu:",
|
||||
"pingField1": "```ID Shard'a: {0}/{1}\nOpóźnienie shard'a: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"pingField2": "```Węzeł: {0} - {1:.3f}s\nOdtwarzacze: {2}\nRegion: {3}```",
|
||||
"addEffect": "Nałożono efekt: `{0}`",
|
||||
"clearEffect": "Efekty dźwiękowe zostały wyczyszczone.",
|
||||
"filterTagAlreadyInUse": "Ten efekt dźwiękowy jest już w używany! Użyj /cleareffect <Efekt> aby go wyłączyć.",
|
||||
"playlistViewTitle": "📜 Playlisty użytkownika {0}:",
|
||||
"playlistViewHeaders": "ID:,Czas:,Nazwa:,Ilość pozycji:",
|
||||
"playlistFooter": "Użyj /playlist play [nazwa_playlisty] by dodać playlistę do kolejki.",
|
||||
"playlistNotFound": "Playlista [`{0}`] nie została znaleziona. Użyj /playlist view by zobaczyć wszystkie swoje playlisty.",
|
||||
"playlistNotAccess": "Przepraszamy! Nie masz uprawnień do tej playlisty!",
|
||||
"playlistNoTrack": "Bład! W playliście [`{0}`] nie ma utworów.",
|
||||
"playlistNotAllow": "Ta komenda jest niedozwolona w odniesienu do udostępnionych playlist.",
|
||||
"playlistPlay": "Dodano playlistę [`{0}`] z `{1}` pozycjami do kolejki.",
|
||||
"playlistOverText": "Bład! Nazwa playlisty nie może być dłuższa niż 10 znaków.",
|
||||
"playlistSameName": "Bład! Nowa nazwa musi się różnić od starej.",
|
||||
"playlistDeleteError": "Nie możesz usuwać domyślnej playlisty",
|
||||
"playlistRemove": "Usunąłeś playlistę [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Błąd! Nie możesz wysłać zaproszenia do samego siebie.",
|
||||
"playlistSendErrorBot": "Błąd! Nie możesz wysyłać zaproszeń do bota.",
|
||||
"playlistBelongs": "Błąd! Ta playlista należy do <@{0}>.",
|
||||
"playlistShare": "Błąd! Ta playlista została udostępniona {0}.",
|
||||
"playlistSent": "Błąd! Zaproszenie zostało już wcześniej wysłane.",
|
||||
"noPlaylistAcc": "{0} nie stworzył konta do przechowywania playlist.",
|
||||
"overPlaylistCreation": "Nie możesz stworzyć więcej niż `{0}` playlist!",
|
||||
"playlistExists": "Playlista [`{0}`] już istnieje.",
|
||||
"playlistNotInvalidUrl": "Podaj poprawny link do playlisty z Spotify lub Youtube.",
|
||||
"playlistCreated": "Stworzyłeś playlistę: `{0}`. Użyj /playlist view by uzyskać więcej informacji.",
|
||||
"playlistRenamed": "Zmieniłeś nazwę `{0}` na `{1}`.",
|
||||
"playlistLimitTrack": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.",
|
||||
"playlistPlaylistLink": "Linki do playlist są nie obsługiwane.",
|
||||
"playlistStream": "Nie możesz dodać transmisji na żywo do playlisty.",
|
||||
"playlistPositionNotFound": "Pozycja `{0}` z playlisty [`{1}`] nie została znaleziona!",
|
||||
"playlistRemoved": "👋 Usunięto **{0}** z playlisty [`{2}`] użytkownika {1}.",
|
||||
"playlistClear": "Pomyślnie wyczyściłeś swoja playlistę: [`{0}`].",
|
||||
"playlistView": "Szczegóły playlisty",
|
||||
"playlistViewDesc": "```Nazwa | ID: {0} | {1}\nLiczba utworów: {2}\nWłaściciel: {3}\nTyp: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Odczyt: ✓ ✍🏽 Zapis: {0} 🗑️ Usuwanie: {1}",
|
||||
"playlistViewPermsValue2": "📖 Odczyt: {0}",
|
||||
"playlistViewTrack": "Utwory",
|
||||
"playlistViewPage": "Strona: {0}/{1} | Całkowity czas trwania: {2}",
|
||||
"inboxFull": "Błąd! Skrzynka odbiorcza {0} jest pełna.",
|
||||
"inboxNoMsg": "W twojej skrzynce odbiorczej nie ma wiadomości.",
|
||||
"invitationSent": "Zaproszenie wysłane do {0}.",
|
||||
"notInChannel": "{0}, musisz być na kanale {1} by używać komend głosowych. Jeżeli jesteś na kanale połącz się ponownie!",
|
||||
"noTrackPlaying": "Aktualnie nic nie jest odtwarzane",
|
||||
"noTrackFound": "Nie znaleziono utworu! Podaj poprawny adres URL.",
|
||||
"noLinkSupport": "Komenda /search nie obsługuje linków!",
|
||||
"voted": "Zagłosowałeś!",
|
||||
"missingPosPerm": "Tylko DJ lub administracja może zmieniać pozycje.",
|
||||
"missingModePerm": "Tylko DJ lub administracja może zmieniać tryb pętli.",
|
||||
"missingQueuePerm": "Tylko DJ lub administracja może usuwać pozycje z kolejki.",
|
||||
"missingAutoPlayPerm": "Tylko DJ lub administracja może włączać lub wyłączać tryb autoplay!",
|
||||
"missingFunctionPerm": "Tylko DJ lub administracja może używać tej funkcji.",
|
||||
"timeFormatError": "Niepoprawny format czasu. Przykład: 2:42 lub 12:39:31",
|
||||
"lyricsNotFound": "Tekst nie został znaleziony. Użyj /lyrics <nazwa utworu> <autor> by znaleźć tekst piosenki.",
|
||||
"missingTrackInfo": "Brakuje niektórych informacji o utworze.",
|
||||
"noVoiceChannel": "Kanał nie został znaleziony",
|
||||
"playlistAddError": "Niemożesz dodawać transmisji na żywo do playlisty!",
|
||||
"playlistAddError2": "Podczas dodawania utworu do playlisty wystąpił błąd!",
|
||||
"playlistLimited": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.",
|
||||
"playlistRepeated": "Ten utwór znajduje sie już na playliście!",
|
||||
"playlistAdded": "❤️ Dodano **{0}** do playlisty [`{2}`] użytkownika {1}!",
|
||||
"playerDropdown": "Wybierz pozycję do której pominąć ...",
|
||||
"playerFilter": "Wybierz efekt który chesz nałożyć ...",
|
||||
"buttonBack": "Cofnij",
|
||||
"buttonPause": "Pauza",
|
||||
"buttonResume": "Wznów",
|
||||
"buttonSkip": "Pomiń",
|
||||
"buttonLeave": "Opuść",
|
||||
"buttonLoop": "Pętla",
|
||||
"buttonVolumeUp": "Głośność +",
|
||||
"buttonVolumeDown": "Głośność -",
|
||||
"buttonVolumeMute": "Wycisz",
|
||||
"buttonVolumeUnmute": "Odcisz",
|
||||
"buttonAutoPlay": "Autoodtwarzanie",
|
||||
"buttonShuffle": "Losowe odtwarzanie",
|
||||
"buttonForward": "Przewiń do przodu",
|
||||
"buttonRewind": "Przewiń do tyłu",
|
||||
"buttonLyrics": "Tekst utworu",
|
||||
"nowplayingDesc": "**Aktualnie odtwarzane:**\n```{0}```",
|
||||
"nowplayingField": "Następne:",
|
||||
"nowplayingLink": "Słuchaj na: {0}",
|
||||
"connect": "Połączono do {0}",
|
||||
"live": "LIVE",
|
||||
"playlistLoad": " 🎶 Dodano playlistę **{0}** z `{1}` pozycjami do kolejki.",
|
||||
"trackLoad": "Rozpoczęto odtwarzanie **[{0}](<{1}>)** autorstwa **{2}** (`{3}`).\n",
|
||||
"trackLoad_pos": "Dodano **[{0}](<{1}>)** autorstwa **{2}** (`{3}`) do kolejki na pozycji **{4}**\n",
|
||||
"searchTitle": "Wyszukiwana fraza: {0}",
|
||||
"searchDesc": "➥ Platforma: {0} **{1}**\n➥ Wyniki: **{2}**\n\n{3}",
|
||||
"searchWait": "Wybierz utwór, który chcesz dodać do playlisty.",
|
||||
"searchTimeout": "Upłynął czas wyszukiwania. Spróbuj ponownie później.",
|
||||
"searchSuccess": "Dodano utwór do kolejki.",
|
||||
"queueTitle": "Nadchodząca kolejka:",
|
||||
"historyTitle": "Historia kolejki:",
|
||||
"viewTitle": "Kolejka utworów",
|
||||
"viewDesc": "**Aktualnie odtwarzane: [Kliknij mnie]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Strona: {0}/{1} | Całkowity czas trwania: {2}",
|
||||
"pauseError": "Odtwarzacz jest już zapauzowany.",
|
||||
"pauseVote": "{0} zagłosował nad zapauzowaniem odtwarzania. [{1}/{2}]",
|
||||
"paused": "`{0}` zapauzował odtwarznaie.",
|
||||
"resumeError": "Odtwarzacz nie jest zapauzowany.",
|
||||
"resumeVote": "{0} zagłosował nad wznowieniem odtwarzania. [{1}/{2}]",
|
||||
"resumed": "`{0}` wznowił odtwarzanie.",
|
||||
"shuffleError": "Dodaj więcej utworów przed przemieszaniem kolejki.",
|
||||
"shuffleVote": "{0} zagłosował nad przemieszaniem kolejki. [{1}/{2}]",
|
||||
"shuffled": "Kolejka została przemieszana.",
|
||||
"skipError": "Nie ma utworu, do którego można by pominąć.",
|
||||
"skipVote": "{0} zagłosował za pominięciem tego utworu. [{1}/{2}]",
|
||||
"skipped": "`{0}` pominął utwór.",
|
||||
"backVote": "{0} zagłosował za przejściem do poprzedniego utworu. [{1}/{2}]",
|
||||
"backed": "`{0}` cofnął do poprzedniego utworu.",
|
||||
"leaveVote": "{0} zagłosował nad zatrzyamniem odtwarzaza. [{1}/{2}]",
|
||||
"left": "`{0}` zatrzymał odtwarzacz.",
|
||||
"seek": "Ustawiono odtwarzacz na **{0}**",
|
||||
"repeat": "Tryb pętli został ustawiony na: `{0}`",
|
||||
"cleared": "Wyczyszczono wszystkie utwory w: `{0}`",
|
||||
"removed": "`{0}` utworów zostało usunięte z kolejki.",
|
||||
"forward": "Przewinięto odtwarzacz do **{0}**",
|
||||
"rewind": "Cofnięto odtwarzacz do **{0}**",
|
||||
"replay": "Ponowne odtwarzanie bieżącego utworu.",
|
||||
"swapped": "`{0}` oraz `{1}` są zamienione",
|
||||
"moved": "Przeniesiono `{0}` do `{1}`",
|
||||
"autoplay": "Tryb autoodwarzania jest teraz **{0}**",
|
||||
"notdj": "Nie jesteś DJ'em. Obecnym DJ'em jest {0}.",
|
||||
"djToMe": "Nie możesz przekazać roli DJ'a sobie lub botu.",
|
||||
"djNotInChannel": "`{0}` nie jest na kanale głosowym.",
|
||||
"djswap": "Przekazałeś rolę DJ'a użytkownikowi `{0}`.",
|
||||
"voicelinkQueueFull": "Przepraszamy osiągnięto limit `{0}` pozycji w kolejce!",
|
||||
"voicelinkOutofList": "Podaj właściwy indeks utworu!",
|
||||
"voicelinkDuplicateTrack": "Błąd! Ten utwór znajduje się już w kolejce.",
|
||||
"decodeError": "Coś poszło nie tak podczas dekodowania pliku!",
|
||||
"invalidStartTime": "Niepoprawny czas startu, musi on mieścić się pomiędzy `00:00` a `{0}`",
|
||||
"invalidEndTime": "Niepoprawny czas końca, musi on mieścić się pomiędzy `00:00` a `{0}`",
|
||||
"invalidTimeOrder": "Czas zakończenia odtwarzania nie może być mniejszy lub równy czasowi startu",
|
||||
"setStageAnnounceTemplate": "Gotowe! Od tego momentu status kanału w którym znajduje się bot będzie wyglądał tak jak podany szablon. Aktualizacja powinna nastąpić za kilka sekund.",
|
||||
"createSongRequestChannel": "Utworzono kanał do obsługi bota - ({0})! Możesz w nim dodawać utwory po nazwie lub adresie URL bez dodawnia prefixu"
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Coś poszło nie tak podczas wykonywania polecenia. Spróbuj ponownie później lub dołąćz do serwera discord w celu uzyskania pomocy.",
|
||||
"missingIntents": "Niestety to polecenie nie może zostać wykonane, ponieważ bot nie posiada wymaganej intencji żądania: `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "włączony",
|
||||
"disabled": "wyłączony",
|
||||
"live": "LIVE"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Spróbuj ponownie! Trwa ponowne łącznie z node'm.",
|
||||
"noPlaySource": "Nie można znaleźć żadnych odtwarzalnych źródeł!",
|
||||
"noPlayer": "Nie znaleziono żadnego odtwarzacza na tym serwerze.",
|
||||
"noTrackPlaying": "Aktualnie nic nie jest odtwarzane",
|
||||
"noTrackFound": "Nie znaleziono utworu! Podaj poprawny adres URL."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Wybierz pozycję do której pominąć ...",
|
||||
"filterSelect": "Wybierz efekt który chesz nałożyć ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Cofnij",
|
||||
"pause": "Pauza",
|
||||
"resume": "Wznów",
|
||||
"skip": "Pomiń",
|
||||
"leave": "Opuść",
|
||||
"loopOff": "Wyłącz",
|
||||
"loopTrack": "Ścieżka",
|
||||
"loopQueue": "Kolejka",
|
||||
"volumeUp": "Głośność +",
|
||||
"volumeDown": "Głośność -",
|
||||
"volumeMute": "Wycisz",
|
||||
"volumeUnmute": "Odcisz",
|
||||
"autoPlay": "Autoodtwarzanie",
|
||||
"shuffle": "Losowe odtwarzanie",
|
||||
"forward": "Przewiń do przodu",
|
||||
"rewind": "Przewiń do tyłu",
|
||||
"lyrics": "Tekst utworu"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Aktualnie odtwarzane:**\n```{0}```",
|
||||
"nowplayingField": "Następne:",
|
||||
"nowplayingLink": "Słuchaj na: {0}",
|
||||
"playlistLoad": " 🎶 Dodano playlistę **{0}** z `{1}` pozycjami do kolejki.",
|
||||
"trackLoad": "Rozpoczęto odtwarzanie **[{0}](<{1}>)** autorstwa **{2}** (`{3}`).\n",
|
||||
"trackLoadPos": "Dodano **[{0}](<{1}>)** autorstwa **{2}** (`{3}`) do kolejki na pozycji **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "Odtwarzacz jest już zapauzowany.",
|
||||
"vote": "{0} zagłosował nad zapauzowaniem odtwarzania. [{1}/{2}]",
|
||||
"success": "`{0}` zapauzował odtwarznaie."
|
||||
},
|
||||
"resume": {
|
||||
"error": "Odtwarzacz nie jest zapauzowany.",
|
||||
"vote": "{0} zagłosował nad wznowieniem odtwarzania. [{1}/{2}]",
|
||||
"success": "`{0}` wznowił odtwarzanie."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Dodaj więcej utworów przed przemieszaniem kolejki.",
|
||||
"vote": "{0} zagłosował nad przemieszaniem kolejki. [{1}/{2}]",
|
||||
"success": "Kolejka została przemieszana."
|
||||
},
|
||||
"skip": {
|
||||
"error": "Nie ma utworu, do którego można by pominąć.",
|
||||
"vote": "{0} zagłosował za pominięciem tego utworu. [{1}/{2}]",
|
||||
"success": "`{0}` pominął utwór."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} zagłosował za przejściem do poprzedniego utworu. [{1}/{2}]",
|
||||
"success": "`{0}` cofnął do poprzedniego utworu."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} zagłosował nad zatrzyamniem odtwarzaza. [{1}/{2}]",
|
||||
"success": "`{0}` zatrzymał odtwarzacz."
|
||||
},
|
||||
"seek": "Ustawiono odtwarzacz na **{0}**",
|
||||
"repeat": "Tryb pętli został ustawiony na: `{0}`",
|
||||
"forward": "Przewinięto odtwarzacz do **{0}**",
|
||||
"rewind": "Cofnięto odtwarzacz do **{0}**",
|
||||
"replay": "Ponowne odtwarzanie bieżącego utworu.",
|
||||
"autoplay": "Tryb autoodwarzania jest teraz **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "Brak kanału głosowego do dołącznia. Dołącz do jakiegoś, lub wskaż kanał komendą /connect",
|
||||
"alreadyConnected": "Już połączono z kanałem głosowym.",
|
||||
"noPermission": "Niestety nie mam uprawnień, aby dołączyć do Twojego kanału głosowego lub na nim mówić.",
|
||||
"notInChannel": "{0}, musisz być na kanale {1} by używać komend głosowych. Jeżeli jesteś na kanale połącz się ponownie!",
|
||||
"connect": "Połączono do {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "Gotowe! Od tego momentu status kanału w którym znajduje się bot będzie wyglądał tak jak podany szablon. Aktualizacja powinna nastąpić za kilka sekund."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Niestety, nie mam uprawnień, aby stworzyć kanał do żądania utworów.",
|
||||
"missingPosition": "Tylko DJ lub administracja może zmieniać pozycje.",
|
||||
"missingMode": "Tylko DJ lub administracja może zmieniać tryb pętli.",
|
||||
"missingQueue": "Tylko DJ lub administracja może usuwać pozycje z kolejki.",
|
||||
"missingAutoPlay": "Tylko DJ lub administracja może włączać lub wyłączać tryb autoplay!",
|
||||
"missingFunction": "Tylko DJ lub administracja może używać tej funkcji.",
|
||||
"notDj": "Nie jesteś DJ'em. Obecnym DJ'em jest {0}.",
|
||||
"djToSelf": "Nie możesz przekazać roli DJ'a sobie lub botu.",
|
||||
"djNotInChannel": "`{0}` nie jest na kanale głosowym.",
|
||||
"djSwapped": "Przekazałeś rolę DJ'a użytkownikowi `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "Nie znaleziono pakietu językowego! wybierz istniejący pakiet językowy.",
|
||||
"languageChanged": "Pomyślnie zmieniono pakiet językowy na: `{0}` .",
|
||||
"prefixSet": "Gotowe! Mój prefix na tym serwerze to teraz `{0}`. Spróbuj uruchomić polecenie `{1}ping`, aby go przetestować.",
|
||||
"djSet": "Ustawiono DJ jako {0}.",
|
||||
"queueModeSet": "Zmienionio tryb kolejki na `{0}`.",
|
||||
"mode247": "Tryb 24/7: `{0}`",
|
||||
"bypassVote": "System głosowania: `{0}`",
|
||||
"volumeSet": "Ustawiono głośność na `{0}`%",
|
||||
"controllerToggled": "Kontroler muzyki: `{0}`",
|
||||
"duplicateTrackToggled": "Unikanie duplikowania pozycji w kolejce: `{0}`",
|
||||
"controllerMsgToggled": "Wiadomości od kontrolera muzyki: `{0}`",
|
||||
"silentMsgToggled": "Masz {0} ciche wiadomości.",
|
||||
"resetDone": "`{0}` zostało zresetowane na domyślne ustawienia serwera.",
|
||||
"resetNotFound": "Nieprawidłowa nazwa ustawienia. Proszę użyć jednej z następujących opcji: {0}"
|
||||
},
|
||||
"menu": "Ustawnienia serwera | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Podstawowe informacje:",
|
||||
"value": "```Prefix: {0}\nJęzyk: {1}\nKontroler muzyki: {2}\nRola DJ: @{3}\nOminięcie głosowania: {4}\n24/7: {5}\nDomyślna głośność: {6}%\nCzas odtwarzania: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Informacje o kolejce:",
|
||||
"value": "```Tryb kolejki: {0}\nMaksymlna liczba utworów: {1}\nZduplikowane pozycje: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Informacie w statusie kanału głosowego:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Uprawnienia:",
|
||||
"value": "```{0} Administrator\n{1} Zarządzanie_serwerem\n{2} Zarządzanie_kanałem\n{3} Zarządzanie_wiadomościami```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Informacje o bocie:",
|
||||
"title2": "Infomracje o odtwarzaczu:",
|
||||
"field1": "```ID Shard'a: {0}/{1}\nOpóźnienie shard'a: {2:.3f}s {3}\nRegion: {4}```",
|
||||
"field2": "```Węzeł: {0} - {1:.3f}s\nOdtwarzacze: {2}\nRegion: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Nałożono efekt: `{0}`",
|
||||
"cleared": "Efekty dźwiękowe zostały wyczyszczone.",
|
||||
"tagInUse": "Ten efekt dźwiękowy jest już w używany! Użyj /cleareffect <Efekt> aby go wyłączyć."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Playlisty użytkownika {0}:",
|
||||
"headers": "ID:,Czas:,Nazwa:,Ilość pozycji:",
|
||||
"footer": "Użyj /playlist play [nazwa_playlisty] by dodać playlistę do kolejki.",
|
||||
"detailTitle": "Szczegóły playlisty",
|
||||
"detailDesc": "```Nazwa: {0} [{1}]\nLiczba utworów: {2}\nWłaściciel: {3}\nTyp: {4}\n```",
|
||||
"permsValue": "📖 Odczyt: ✓ ✍🏽 Zapis: {0} 🗑️ Usuwanie: {1}",
|
||||
"permsValue2": "📖 Odczyt: {0}",
|
||||
"trackList": "Utwory",
|
||||
"footer2": "Całkowity czas trwania: {0}",
|
||||
"playlistSelect": "Wybierz listę odtwarzania do wyświetlenia ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Playlista [`{0}`] nie została znaleziona. Użyj /playlist view by zobaczyć wszystkie swoje playlisty.",
|
||||
"noAccess": "Przepraszamy! Nie masz uprawnień do tej playlisty!",
|
||||
"noTrack": "Bład! W playliście [`{0}`] nie ma utworów.",
|
||||
"notAllowed": "Ta komenda jest niedozwolona w odniesienu do udostępnionych playlist.",
|
||||
"nameOverLimit": "Bład! Nazwa playlisty nie może być dłuższa niż 10 znaków.",
|
||||
"sameName": "Bład! Nowa nazwa musi się różnić od starej.",
|
||||
"deleteDefault": "Nie możesz usuwać domyślnej playlisty",
|
||||
"limitReached": "Nie możesz stworzyć więcej niż `{0}` playlist!",
|
||||
"exists": "Playlista [`{0}`] już istnieje.",
|
||||
"invalidUrl": "Proszę podać prawidłowy link do publicznej playlisty.",
|
||||
"trackLimitReached": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.",
|
||||
"playlistLinkNotAllowed": "Linki do playlist są nie obsługiwane.",
|
||||
"streamNotAllowed": "Nie możesz dodać transmisji na żywo do playlisty.",
|
||||
"positionNotFound": "Pozycja `{0}` z playlisty [`{1}`] nie została znaleziona!",
|
||||
"streaming": "Niemożesz dodawać transmisji na żywo do playlisty!",
|
||||
"track": "Podczas dodawania utworu do playlisty wystąpił błąd!",
|
||||
"trackLimited": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.",
|
||||
"trackRepeated": "Ten utwór znajduje sie już na playliście!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Dodano playlistę [`{0}`] z `{1}` pozycjami do kolejki.",
|
||||
"removed": "Usunąłeś playlistę [`{0}`].",
|
||||
"created": "Stworzyłeś playlistę: `{0}`. Użyj /playlist view by uzyskać więcej informacji.",
|
||||
"renamed": "Zmieniłeś nazwę `{0}` na `{1}`.",
|
||||
"trackRemoved": "👋 Usunięto **{0}** z playlisty [`{2}`] użytkownika {1}.",
|
||||
"cleared": "Pomyślnie wyczyściłeś swoja playlistę: [`{0}`].",
|
||||
"trackAdded": "❤️ Dodano **{0}** do playlisty [`{2}`] użytkownika {1}!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Błąd! Nie możesz wysłać zaproszenia do samego siebie.",
|
||||
"sendErrorBot": "Błąd! Nie możesz wysyłać zaproszeń do bota.",
|
||||
"belongs": "Błąd! Ta playlista należy do <@{0}>.",
|
||||
"alreadyShared": "Błąd! Ta playlista została udostępniona {0}.",
|
||||
"alreadySent": "Błąd! Zaproszenie zostało już wcześniej wysłane.",
|
||||
"noAccount": "{0} nie stworzył konta do przechowywania playlist.",
|
||||
"invitationSent": "Zaproszenie wysłane do {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Błąd! Skrzynka odbiorcza {0} jest pełna.",
|
||||
"noMessages": "W twojej skrzynce odbiorczej nie ma wiadomości."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "Komenda /search nie obsługuje linków!",
|
||||
"title": "Wyszukiwana fraza: {0}",
|
||||
"desc": "➥ Platforma: {0} **{1}**\n➥ Wyniki: **{2}**\n\n{3}",
|
||||
"wait": "Wybierz utwór, który chcesz dodać do playlisty.",
|
||||
"success": "Dodano utwór do kolejki."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "Zagłosowałeś!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Niepoprawny format czasu. Przykład: 2:42 lub 12:39:31",
|
||||
"invalidStartTime": "Niepoprawny czas startu, musi on mieścić się pomiędzy `00:00` a `{0}`",
|
||||
"invalidEndTime": "Niepoprawny czas końca, musi on mieścić się pomiędzy `00:00` a `{0}`"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Tekst nie został znaleziony. Użyj /lyrics <nazwa utworu> <autor> by znaleźć tekst piosenki."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Nadchodząca kolejka:",
|
||||
"cleared": "Wyczyszczono wszystkie utwory w: `{0}`",
|
||||
"removed": "`{0}` utworów zostało usunięte z kolejki.",
|
||||
"swapped": "`{0}` oraz `{1}` są zamienione",
|
||||
"moved": "Przeniesiono `{0}` do `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "Historia kolejki:",
|
||||
"title": "Kolejka utworów",
|
||||
"desc": "**Aktualnie odtwarzane: [Kliknij mnie]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Przepraszamy osiągnięto limit `{0}` pozycji w kolejce!",
|
||||
"outOfList": "Podaj właściwy indeks utworu!",
|
||||
"duplicateTrack": "Błąd! Ten utwór znajduje się już w kolejce."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "Utworzono kanał do obsługi bota - ({0})! Możesz w nim dodawać utwory po nazwie lub adresie URL bez dodawnia prefixu"
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Poprzedni",
|
||||
"next": "Następny",
|
||||
"play": "Odtwórz",
|
||||
"share": "Udostępnij",
|
||||
"export": "Eksportuj",
|
||||
"delete": "Usuń",
|
||||
"page": {
|
||||
"title": "Wprowadź numer strony",
|
||||
"field": "Numer strony"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/RU.json
436
langs/RU.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ Что-то пошло не так при выполнении команды! Пожалуйста, попробуйте позже или присоединитесь к нашему серверу Discord для получения дополнительной поддержки.",
|
||||
"enabled": "вкл",
|
||||
"disabled": "выкл",
|
||||
"nodeReconnect": "Пожалуйста, попробуйте чуть позже!",
|
||||
"noChannel": "Нет голосового канала для подключения. Пожалуйста, укажите или присоединитесь к одному.",
|
||||
"alreadyConnected": "Уже подключен к голосовому каналу.",
|
||||
"noPermission": "Извините! У меня нет разрешения на подключение или разговор в вашем голосовом канале.",
|
||||
"noCreatePermission": "Извините! У меня нет прав на создание канала для запроса песен.",
|
||||
"noPlaySource": "Не получилось найти рабочие источники!",
|
||||
"noPlayer": "На этом сервере не найдено ни одного активного плеера.",
|
||||
"notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.",
|
||||
"missingIntents": "Извините, но эту команду нельзя выполнить, так как у бота отсутствует необходимый запрос намерения: `({0})`.",
|
||||
"languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.",
|
||||
"changedLanguage": "Язык успешно изменен на `{0}`.",
|
||||
"setPrefix": "Готово! Мой префикс на вашем сервере теперь `{0}`. Попробуйте запустить `{1}ping`, чтобы проверить его.",
|
||||
"setDJ": "Установить роль DJ {0}.",
|
||||
"setQueue": "Установить режим очереди на `{0}`.",
|
||||
"247": "Теперь у вас `{0}` режим 24/7.",
|
||||
"bypassVote": "Теперь у вас `{0}` система голосования.",
|
||||
"setVolume": "Установить громкость на `{0}`%",
|
||||
"toggleController": "Теперь у вас `{0}` контроллер музыки.",
|
||||
"toggleDuplicateTrack": "Теперь у вас `{0}` фильтр дубликатов в очереди.",
|
||||
"toggleControllerMsg": "Теперь у вас `{0}` сообщения от плеера.",
|
||||
"toggleSilentMsg": "У вас {0} тихих сообщений.",
|
||||
"settingsMenu": "Настройки сервера | {0}",
|
||||
"settingsTitle": "❤️ Основная информация:",
|
||||
"settingsValue": "```Префикс: {0}\nЯзык: {1}\nВключить контроллер музыки: {2}\nDJ роль: @{3}\nОбход голосования: {4}\n24/7: {5}\nГромкость по умолчанию: {6}%\nВремя проигрывания: {7}```",
|
||||
"settingsTitle2": "🔗 Информация об очереди:",
|
||||
"settingsValue2": "```Режим очереди: {0}\nМаксимальное количество песен: {1}\nРазрешить дублирование треков: {2}```",
|
||||
"settingsTitle3": "🎤 Информация о состоянии голоса:",
|
||||
"settingsPermTitle": "✨ Права:",
|
||||
"settingsPermValue": "```{0} Администратор\n{1} Управление_Сервером\n{2} Управление_Каналом\n{3} Управление_Сообщениями```",
|
||||
"pingTitle1": "Информация о боте:",
|
||||
"pingTitle2": "Информация о плеере:",
|
||||
"pingField1": "```ID Шарда: {0}/{1}\nЗадержка Шарда: {2:.3f}s {3}\nРегион: {4}```",
|
||||
"pingField2": "```Сервис: {0} - {1:.3f}s\nИгроки: {2}\nРегион Голоса: {3}```",
|
||||
"addEffect": "Применен `{0}` эффект.",
|
||||
"clearEffect": "Звуковые эффекты были очищены!",
|
||||
"filterTagAlreadyInUse": "Этот звуковой эффект уже используется! Пожалуйста, используйте /cleareffect <Тег>, чтобы удалить его.",
|
||||
"playlistViewTitle": "📜 Все плейлисты пользователя {0}",
|
||||
"playlistViewHeaders": "ID:,Время:,Название:,Треки:",
|
||||
"playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.",
|
||||
"playlistNotFound": "Плейлист [`{0}`] не найден. Введите /playlist view, чтобы посмотреть все ваши плейлисты.",
|
||||
"playlistNotAccess": "Извините! У вас нет доступа к этому плейлисту!",
|
||||
"playlistNoTrack": "Извините! В плейлисте [`{0}`] нет треков.",
|
||||
"playlistNotAllow": "Эта команда не разрешена для общих плейлистов.",
|
||||
"playlistPlay": "Добавлен плейлист [`{0}`] с `{1}` треками в очередь.",
|
||||
"playlistOverText": "Извините! Имя плейлиста не может превышать 10 символов.",
|
||||
"playlistSameName": "Извините! Это имя не может совпадать с вашим новым именем.",
|
||||
"playlistDeleteError": "Вы не можете удалить плейлист по умолчанию.",
|
||||
"playlistRemove": "Вы удалили плейлист [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Извините! Вы не можете отправить приглашение самому себе.",
|
||||
"playlistSendErrorBot": "Извините! Вы не можете отправить приглашение боту.",
|
||||
"playlistBelongs": "Извините! Этот плейлист принадлежит <@{0}>.",
|
||||
"playlistShare": "Извините! Этим плейлистом уже поделились с {0}.",
|
||||
"playlistSent": "Извините! Вы уже отправили приглашение ранее.",
|
||||
"noPlaylistAcc": "Пользователь `{0}` не создал учетную запись плейлиста.",
|
||||
"overPlaylistCreation": "Вы не можете создавать больше `{0}` плейлистов!",
|
||||
"playlistExists": "Плейлист [`{0}`] уже существует.",
|
||||
"playlistNotInvalidUrl": "Пожалуйста, введите действительную ссылку или публичную плейлист-ссылку на Spotify или YouTube.",
|
||||
"playlistCreated": "Вы создали плейлист `{0}`. Введите /playlist view для получения дополнительной информации.",
|
||||
"playlistRenamed": "Вы переименовали `{0}` в `{1}`.",
|
||||
"playlistLimitTrack": "Вы достигли лимита! Вы можете добавить только `{0}` треков в свой плейлист.",
|
||||
"playlistPlaylistLink": "Вам не разрешено использовать ссылку на плейлист.",
|
||||
"playlistStream": "Вам не разрешено добавлять потоковые видео в свой плейлист.",
|
||||
"playlistPositionNotFound": "Не получилось найти позицию `{0}` в вашем плейлисте [`{1}`]!",
|
||||
"playlistRemoved": "👋 Удален **{0}** из плейлиста {1} [`{2}`].",
|
||||
"playlistClear": "Вы успешно очистили свой плейлист [`{0}`].",
|
||||
"playlistView": "Просмотр плейлистов",
|
||||
"playlistViewDesc": "```Имя | ID: {0} | {1}\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Чтение: ✓ ✍🏽 Запись: {0} 🗑️ Удаление: {1}",
|
||||
"playlistViewPermsValue2": "📖 Чтение: {0}",
|
||||
"playlistViewTrack": "Треки",
|
||||
"playlistViewPage": "Страница: {0}/{1} | Общая продолжительность: {2}",
|
||||
"inboxFull": "Извините! Почтовый ящик {0} переполнен.",
|
||||
"inboxNoMsg": "В вашем почтовом ящике нет сообщений.",
|
||||
"invitationSent": "Приглашение отправлено {0}.",
|
||||
"notInChannel": "{0}, для использования голосовых команд вы должны находиться в {1}. Пожалуйста, перезайдите, если вы уже находитесь в голосовом канале!",
|
||||
"noTrackPlaying": "Сейчас нет играющих треков.",
|
||||
"noTrackFound": "Треки по такому запросу не найдены! Пожалуйста, укажите действительную ссылку.",
|
||||
"noLinkSupport": "Команда поиска не поддерживает ссылки!",
|
||||
"voted": "Вы проголосовали!",
|
||||
"missingPosPerm": "Только DJ или администраторы могут изменять позицию.",
|
||||
"missingModePerm": "Только DJ или администраторы могут переключить режим цикла.",
|
||||
"missingQueuePerm": "Только DJ или администраторы могут удалять треки из очереди.",
|
||||
"missingAutoPlayPerm": "Только DJ или администраторы могут включать или выключать режим autoplay!",
|
||||
"missingFunctionPerm": "Только DJ или администраторы могут использовать эту функцию.",
|
||||
"timeFormatError": "Неверный формат времени. Пример: 2:42PM или 12:39:31",
|
||||
"lyricsNotFound": "Текст песни не найден. Введите /lyrics <Название песни> <Автор> для поиска текста.",
|
||||
"missingTrackInfo": "Некоторая информация о треке отсутствует.",
|
||||
"noVoiceChannel": "Голосовой канал не найден!",
|
||||
"playlistAddError": "Вам не разрешено добавлять потоковые видео в плейлист!",
|
||||
"playlistAddError2": "Произошла ошибка при добавлении треков в плейлист!",
|
||||
"playlistLimited": "Вы достигли лимита! Вы можете добавить только {0} треков в свой плейлист.",
|
||||
"playlistRepeated": "Такой трек уже есть в плейлисте!",
|
||||
"playlistAdded": "❤️ Добавлен **{0}** в плейлист пользователя {1} [`{2}`]!",
|
||||
"playerDropdown": "Выберите трек для перехода ...",
|
||||
"playerFilter": "Выберите фильтр для применения ...",
|
||||
"buttonBack": "Назад",
|
||||
"buttonPause": "Пауза",
|
||||
"buttonResume": "Пуск",
|
||||
"buttonSkip": "Вперед",
|
||||
"buttonLeave": "СТОП",
|
||||
"buttonLoop": "Зациклить",
|
||||
"buttonVolumeUp": "Громкость +",
|
||||
"buttonVolumeDown": "Громкость -",
|
||||
"buttonVolumeMute": "Выключить звук",
|
||||
"buttonVolumeUnmute": "Включить звук",
|
||||
"buttonAutoPlay": "Autoplay",
|
||||
"buttonShuffle": "Перемешать",
|
||||
"buttonForward": "+10сек",
|
||||
"buttonRewind": "Назад",
|
||||
"buttonLyrics": "Тексты песен",
|
||||
"nowplayingDesc": "**Сейчас играет:**\n```{0}```",
|
||||
"nowplayingField": "Следующее:",
|
||||
"nowplayingLink": "Слушать на {0}",
|
||||
"connect": "Подключено к {0}",
|
||||
"live": "В ЭФИРЕ",
|
||||
"playlistLoad": "🎶 Добавлен плейлист **{0}** с `{1}` треками в очередь.",
|
||||
"trackLoad": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) в очередь.\n",
|
||||
"trackLoad_pos": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) в очередь на позицию **{4}**\n",
|
||||
"searchTitle": "Поиск: {0}",
|
||||
"searchDesc": "➥ Платформа: {0} **{1}**\n➥ Результаты: **{2}**\n\n{3}",
|
||||
"searchWait": "Выберите трек, который хотите добавить в очередь.",
|
||||
"searchTimeout": "Поиск прерван: превышено время ожидания. Пожалуйста, попробуйте позже.",
|
||||
"searchSuccess": "Трек добавлен в очередь.",
|
||||
"queueTitle": "Следующий в очереди:",
|
||||
"historyTitle": "История очереди:",
|
||||
"viewTitle": "Текущая очередь",
|
||||
"viewDesc": "**Сейчас играет: [*ссылка*]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Страница: {0}/{1} | Общая продолжительность: {2}",
|
||||
"pauseError": "Плеер уже на паузе.",
|
||||
"pauseVote": "Пользователь `{0}` проголосовал за паузу плеера. [{1}/{2}]",
|
||||
"paused": "Пользователь `{0}` поставил плеер на паузу.",
|
||||
"resumeError": "Плеер не на паузе.",
|
||||
"resumeVote": "Пользователь `{0}` проголосовал за продолжение трека. [{1}/{2}]",
|
||||
"resumed": "Пользователь `{0}` убрал плеер с паузы.",
|
||||
"shuffleError": "Недостаточно треков для перемешивания",
|
||||
"shuffleVote": "Пользователь `{0}` проголосовал за перемешивание треков. [{1}/{2}]",
|
||||
"shuffled": "Очередь перемешана.",
|
||||
"skipError": "Нет треков для пропуска.",
|
||||
"skipVote": "Пользователь `{0}` проголосовал за пропуск трека. [{1}/{2}]",
|
||||
"skipped": "Пользователь `{0}` пропустил трек.",
|
||||
"backVote": "Пользователь `{0}` проголосовал за возврат к предыдущему треку. [{1}/{2}]",
|
||||
"backed": "Пользователь `{0}` вернулся к предыдущему треку.",
|
||||
"leaveVote": "Пользователь `{0}` проголосовал за остановку плеера. [{1}/{2}]",
|
||||
"left": "Пользователь `{0}` остановил плеер.",
|
||||
"seek": "Перемотать на **{0}**",
|
||||
"repeat": "Режим повтора `{0}`",
|
||||
"cleared": "Очищены все треки в `{0}`",
|
||||
"removed": "Удалено `{0}` треков из очереди.",
|
||||
"forward": "Перемотать плеер вперед на **{0}**",
|
||||
"rewind": "Отмотать плеер назад на **{0}**",
|
||||
"replay": "Повтор текущего трека.",
|
||||
"swapped": "Треки `{0}` и `{1}` поменяны местами",
|
||||
"moved": "Трек `{0}` смещён на `{1}`",
|
||||
"autoplay": "Режим autoplay **{0}**",
|
||||
"notdj": "Вы не DJ. Текущий DJ: {0}.",
|
||||
"djToMe": "Вы не можете передать роль DJ себе или боту.",
|
||||
"djNotInChannel": "Пользователь `{0}` не находится в голосовом канале.",
|
||||
"djswap": "Вы передали роль DJ пользователю `{0}`.",
|
||||
"voicelinkQueueFull": "Извините, вы достигли максимального количества `{0}` треков в очереди!",
|
||||
"voicelinkOutofList": "Пожалуйста, предоставьте действительный индекс трека!",
|
||||
"voicelinkDuplicateTrack": "Извините, этот трек уже есть в очереди.",
|
||||
"decodeError": "Что-то пошло не так при декодировании файла!",
|
||||
"invalidStartTime": "Недействительное значение! Диапозон времени должен быть между `00:00` и `{0}`.",
|
||||
"invalidEndTime": "Недействительное значение! Диапозон времени должен быть между `00:00` и `{0}`.",
|
||||
"invalidTimeOrder": "Время конца трека не может быть меньше или равно времени начала.",
|
||||
"setStageAnnounceTemplate": "Готово! Новые настройки статуса канала скоро применятся в соответсвии с вашим шаблоном.",
|
||||
"createSongRequestChannel": "Канал для запроса песен ({0}) создан! Вы можете запросить любую песню по названию или URL в этом канале без использования префикса бота."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Что-то пошло не так при выполнении команды! Пожалуйста, попробуйте позже или присоединитесь к нашему серверу Discord для получения дополнительной поддержки.",
|
||||
"missingIntents": "Извините, но эту команду нельзя выполнить, так как у бота отсутствует необходимый запрос намерения: `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "вкл",
|
||||
"disabled": "выкл",
|
||||
"live": "В ЭФИРЕ"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Пожалуйста, попробуйте чуть позже!",
|
||||
"noPlaySource": "Не получилось найти рабочие источники!",
|
||||
"noPlayer": "На этом сервере не найдено ни одного активного плеера.",
|
||||
"noTrackPlaying": "Сейчас нет играющих треков.",
|
||||
"noTrackFound": "Треки по такому запросу не найдены! Пожалуйста, укажите действительную ссылку."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Выберите трек для перехода ...",
|
||||
"filterSelect": "Выберите фильтр для применения ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Назад",
|
||||
"pause": "Пауза",
|
||||
"resume": "Пуск",
|
||||
"skip": "Вперед",
|
||||
"leave": "СТОП",
|
||||
"loopOff": "Выключить",
|
||||
"loopTrack": "Трек",
|
||||
"loopQueue": "Очередь",
|
||||
"volumeUp": "Громкость +",
|
||||
"volumeDown": "Громкость -",
|
||||
"volumeMute": "Выключить звук",
|
||||
"volumeUnmute": "Включить звук",
|
||||
"autoPlay": "Autoplay",
|
||||
"shuffle": "Перемешать",
|
||||
"forward": "+10сек",
|
||||
"rewind": "Назад",
|
||||
"lyrics": "Тексты песен"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Сейчас играет:**\n```{0}```",
|
||||
"nowplayingField": "Следующее:",
|
||||
"nowplayingLink": "Слушать на {0}",
|
||||
"playlistLoad": "🎶 Добавлен плейлист **{0}** с `{1}` треками в очередь.",
|
||||
"trackLoad": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) в очередь.\n",
|
||||
"trackLoadPos": "Добавлен **[{0}](<{1}>)** от **{2}** (`{3}`) в очередь на позицию **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "Плеер уже на паузе.",
|
||||
"vote": "Пользователь `{0}` проголосовал за паузу плеера. [{1}/{2}]",
|
||||
"success": "Пользователь `{0}` поставил плеер на паузу."
|
||||
},
|
||||
"resume": {
|
||||
"error": "Плеер не на паузе.",
|
||||
"vote": "Пользователь `{0}` проголосовал за продолжение трека. [{1}/{2}]",
|
||||
"success": "Пользователь `{0}` убрал плеер с паузы."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Недостаточно треков для перемешивания",
|
||||
"vote": "Пользователь `{0}` проголосовал за перемешивание треков. [{1}/{2}]",
|
||||
"success": "Очередь перемешана."
|
||||
},
|
||||
"skip": {
|
||||
"error": "Нет треков для пропуска.",
|
||||
"vote": "Пользователь `{0}` проголосовал за пропуск трека. [{1}/{2}]",
|
||||
"success": "Пользователь `{0}` пропустил трек."
|
||||
},
|
||||
"back": {
|
||||
"vote": "Пользователь `{0}` проголосовал за возврат к предыдущему треку. [{1}/{2}]",
|
||||
"success": "Пользователь `{0}` вернулся к предыдущему треку."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "Пользователь `{0}` проголосовал за остановку плеера. [{1}/{2}]",
|
||||
"success": "Пользователь `{0}` остановил плеер."
|
||||
},
|
||||
"seek": "Перемотать на **{0}**",
|
||||
"repeat": "Режим повтора `{0}`",
|
||||
"forward": "Перемотать плеер вперед на **{0}**",
|
||||
"rewind": "Отмотать плеер назад на **{0}**",
|
||||
"replay": "Повтор текущего трека.",
|
||||
"autoplay": "Режим autoplay **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "Нет голосового канала для подключения. Пожалуйста, укажите или присоединитесь к одному.",
|
||||
"alreadyConnected": "Уже подключен к голосовому каналу.",
|
||||
"noPermission": "Извините! У меня нет разрешения на подключение или разговор в вашем голосовом канале.",
|
||||
"notInChannel": "{0}, для использования голосовых команд вы должны находиться в {1}. Пожалуйста, перезайдите, если вы уже находитесь в голосовом канале!",
|
||||
"connect": "Подключено к {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "Готово! Новые настройки статуса канала скоро применятся в соответсвии с вашим шаблоном."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Извините! У меня нет прав на создание канала для запроса песен.",
|
||||
"missingPosition": "Только DJ или администраторы могут изменять позицию.",
|
||||
"missingMode": "Только DJ или администраторы могут переключить режим цикла.",
|
||||
"missingQueue": "Только DJ или администраторы могут удалять треки из очереди.",
|
||||
"missingAutoPlay": "Только DJ или администраторы могут включать или выключать режим autoplay!",
|
||||
"missingFunction": "Только DJ или администраторы могут использовать эту функцию.",
|
||||
"notDj": "Вы не DJ. Текущий DJ: {0}.",
|
||||
"djToSelf": "Вы не можете передать роль DJ себе или боту.",
|
||||
"djNotInChannel": "Пользователь `{0}` не находится в голосовом канале.",
|
||||
"djSwapped": "Вы передали роль DJ пользователю `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.",
|
||||
"languageChanged": "Язык успешно изменен на `{0}`.",
|
||||
"prefixSet": "Готово! Мой префикс на вашем сервере теперь `{0}`. Попробуйте запустить `{1}ping`, чтобы проверить его.",
|
||||
"djSet": "Установить роль DJ {0}.",
|
||||
"queueModeSet": "Установить режим очереди на `{0}`.",
|
||||
"mode247": "Теперь у вас `{0}` режим 24/7.",
|
||||
"bypassVote": "Теперь у вас `{0}` система голосования.",
|
||||
"volumeSet": "Установить громкость на `{0}`%",
|
||||
"controllerToggled": "Теперь у вас `{0}` контроллер музыки.",
|
||||
"duplicateTrackToggled": "Теперь у вас `{0}` фильтр дубликатов в очереди.",
|
||||
"controllerMsgToggled": "Теперь у вас `{0}` сообщения от плеера.",
|
||||
"silentMsgToggled": "У вас {0} тихих сообщений.",
|
||||
"resetDone": "`{0}` установлено на значения по умолчанию сервера.",
|
||||
"resetNotFound": "Неверное имя настройки. Пожалуйста, используйте одно из следующих значений: {0}"
|
||||
},
|
||||
"menu": "Настройки сервера | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Основная информация:",
|
||||
"value": "```Префикс: {0}\nЯзык: {1}\nВключить контроллер музыки: {2}\nDJ роль: @{3}\nОбход голосования: {4}\n24/7: {5}\nГромкость по умолчанию: {6}%\nВремя проигрывания: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Информация об очереди:",
|
||||
"value": "```Режим очереди: {0}\nМаксимальное количество песен: {1}\nРазрешить дублирование треков: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Информация о состоянии голоса:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Права:",
|
||||
"value": "```{0} Администратор\n{1} Управление_Сервером\n{2} Управление_Каналом\n{3} Управление_Сообщениями```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Информация о боте:",
|
||||
"title2": "Информация о плеере:",
|
||||
"field1": "```ID Шарда: {0}/{1}\nЗадержка Шарда: {2:.3f}s {3}\nРегион: {4}```",
|
||||
"field2": "```Сервис: {0} - {1:.3f}s\nИгроки: {2}\nРегион Голоса: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Применен `{0}` эффект.",
|
||||
"cleared": "Звуковые эффекты были очищены!",
|
||||
"tagInUse": "Этот звуковой эффект уже используется! Пожалуйста, используйте /cleareffect <Тег>, чтобы удалить его."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Все плейлисты пользователя {0}",
|
||||
"headers": "ID:,Время:,Название:,Треки:",
|
||||
"footer": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.",
|
||||
"detailTitle": "Просмотр плейлистов",
|
||||
"detailDesc": "```Имя: {0} [{1}]\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```",
|
||||
"permsValue": "📖 Чтение: ✓ ✍🏽 Запись: {0} 🗑️ Удаление: {1}",
|
||||
"permsValue2": "📖 Чтение: {0}",
|
||||
"trackList": "Треки",
|
||||
"footer2": "Общая продолжительность: {0}",
|
||||
"playlistSelect": "Выберите плейлист для просмотра ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Плейлист [`{0}`] не найден. Введите /playlist view, чтобы посмотреть все ваши плейлисты.",
|
||||
"noAccess": "Извините! У вас нет доступа к этому плейлисту!",
|
||||
"noTrack": "Извините! В плейлисте [`{0}`] нет треков.",
|
||||
"notAllowed": "Эта команда не разрешена для общих плейлистов.",
|
||||
"nameOverLimit": "Извините! Имя плейлиста не может превышать 10 символов.",
|
||||
"sameName": "Извините! Это имя не может совпадать с вашим новым именем.",
|
||||
"deleteDefault": "Вы не можете удалить плейлист по умолчанию.",
|
||||
"limitReached": "Вы не можете создавать больше `{0}` плейлистов!",
|
||||
"exists": "Плейлист [`{0}`] уже существует.",
|
||||
"invalidUrl": "Пожалуйста, введите действительную ссылку на публичный плейлист.",
|
||||
"trackLimitReached": "Вы достигли лимита! Вы можете добавить только `{0}` треков в свой плейлист.",
|
||||
"playlistLinkNotAllowed": "Вам не разрешено использовать ссылку на плейлист.",
|
||||
"streamNotAllowed": "Вам не разрешено добавлять потоковые видео в свой плейлист.",
|
||||
"positionNotFound": "Не получилось найти позицию `{0}` в вашем плейлисте [`{1}`]!",
|
||||
"streaming": "Вам не разрешено добавлять потоковые видео в плейлист!",
|
||||
"track": "Произошла ошибка при добавлении треков в плейлист!",
|
||||
"trackLimited": "Вы достигли лимита! Вы можете добавить только {0} треков в свой плейлист.",
|
||||
"trackRepeated": "Такой трек уже есть в плейлисте!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Добавлен плейлист [`{0}`] с `{1}` треками в очередь.",
|
||||
"removed": "Вы удалили плейлист [`{0}`].",
|
||||
"created": "Вы создали плейлист `{0}`. Введите /playlist view для получения дополнительной информации.",
|
||||
"renamed": "Вы переименовали `{0}` в `{1}`.",
|
||||
"trackRemoved": "👋 Удален **{0}** из плейлиста {1} [`{2}`].",
|
||||
"cleared": "Вы успешно очистили свой плейлист [`{0}`].",
|
||||
"trackAdded": "❤️ Добавлен **{0}** в плейлист пользователя {1} [`{2}`]!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Извините! Вы не можете отправить приглашение самому себе.",
|
||||
"sendErrorBot": "Извините! Вы не можете отправить приглашение боту.",
|
||||
"belongs": "Извините! Этот плейлист принадлежит <@{0}>.",
|
||||
"alreadyShared": "Извините! Этим плейлистом уже поделились с {0}.",
|
||||
"alreadySent": "Извините! Вы уже отправили приглашение ранее.",
|
||||
"noAccount": "Пользователь `{0}` не создал учетную запись плейлиста.",
|
||||
"invitationSent": "Приглашение отправлено {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Извините! Почтовый ящик {0} переполнен.",
|
||||
"noMessages": "В вашем почтовом ящике нет сообщений."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "Команда поиска не поддерживает ссылки!",
|
||||
"title": "Поиск: {0}",
|
||||
"desc": "➥ Платформа: {0} **{1}**\n➥ Результаты: **{2}**\n\n{3}",
|
||||
"wait": "Выберите трек, который хотите добавить в очередь.",
|
||||
"success": "Трек добавлен в очередь."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "Вы проголосовали!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Неверный формат времени. Пример: 2:42PM или 12:39:31",
|
||||
"invalidStartTime": "Недействительное значение! Диапозон времени должен быть между `00:00` и `{0}`.",
|
||||
"invalidEndTime": "Недействительное значение! Диапозон времени должен быть между `00:00` и `{0}`."
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Текст песни не найден. Введите /lyrics <Название песни> <Автор> для поиска текста."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Следующий в очереди:",
|
||||
"cleared": "Очищены все треки в `{0}`",
|
||||
"removed": "Удалено `{0}` треков из очереди.",
|
||||
"swapped": "Треки `{0}` и `{1}` поменяны местами",
|
||||
"moved": "Трек `{0}` смещён на `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "История очереди:",
|
||||
"title": "Текущая очередь",
|
||||
"desc": "**Сейчас играет: [*ссылка*]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Извините, вы достигли максимального количества `{0}` треков в очереди!",
|
||||
"outOfList": "Пожалуйста, предоставьте действительный индекс трека!",
|
||||
"duplicateTrack": "Извините, этот трек уже есть в очереди."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "Канал для запроса песен ({0}) создан! Вы можете запросить любую песню по названию или URL в этом канале без использования префикса бота."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Назад",
|
||||
"next": "Вперед",
|
||||
"play": "Играть",
|
||||
"share": "Поделиться",
|
||||
"export": "Экспортировать",
|
||||
"delete": "Удалить",
|
||||
"page": {
|
||||
"title": "Введите номер страницы",
|
||||
"field": "Номер страницы"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/UA.json
436
langs/UA.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ Щось пішло не так під час виконання команди! Будь ласка, спробуйте пізніше або приєднайтеся до нашого сервера Discord для отримання додаткової підтримки.",
|
||||
"enabled": "вкл",
|
||||
"disabled": "викл",
|
||||
"nodeReconnect": "Будь ласка, спробуйте знову! Після перепідключення вузла.",
|
||||
"noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.",
|
||||
"alreadyConnected": "Уже підключений до голосового каналу.",
|
||||
"noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.",
|
||||
"noCreatePermission": "Вибачте! У мене немає прав для створення каналу запитів на пісні.",
|
||||
"noPlaySource": "Неможливо знайти робочі джерела!",
|
||||
"noPlayer": "На цьому сервері не знайдено жодного активного плеєра.",
|
||||
"notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.",
|
||||
"missingIntents": "Вибачте, але цю команду не можна виконати, оскільки у бота відсутній необхідний запит на інтенцію: `({0})`.",
|
||||
"languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.",
|
||||
"changedLanguage": "Успішно змінено на мовний пакет `{0}`.",
|
||||
"setPrefix": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.",
|
||||
"setDJ": "Встановити роль DJ {0}.",
|
||||
"setQueue": "Встановити режим черги на `{0}`.",
|
||||
"247": "Тепер у вас `{0}` режим 24/7.",
|
||||
"bypassVote": "Тепер у вас `{0}` система голосування.",
|
||||
"setVolume": "Встановити гучність на `{0}`%",
|
||||
"toggleController": "Тепер у вас `{0}` контролер музики.",
|
||||
"toggleDuplicateTrack": "Тепер у вас `{0}` запобігання дублюванню треку в черзі.",
|
||||
"toggleControllerMsg": "Тепер у вас `{0}` повідомлення від контролера музики.",
|
||||
"toggleSilentMsg": "У вас {0} тихих повідомлень.",
|
||||
"settingsMenu": "Налаштування сервера | {0}",
|
||||
"settingsTitle": "❤️ Основна інформація:",
|
||||
"settingsValue": "```Префікс: {0}\nМова: {1}\nУвімкнути контролер музики: {2}\nDJ роль: {3}\nОбхід голосування: {4}\n24/7: {5}\nГромкість за замовчуванням: {6}%\nЧас програвання: {7}```",
|
||||
"settingsTitle2": "🔗 Інформація про чергу:",
|
||||
"settingsValue2": "```Режим черги: {0}\nМаксимальна кількість пісень: {1}\nДозволити дублювання треків: {2}```",
|
||||
"settingsTitle3": "🎤 Інформація про стан голосу:",
|
||||
"settingsPermTitle": "✨ Права:",
|
||||
"settingsPermValue": "```{0} Адміністратор\n{1} Керування_Сервером\n{2} Керування_Каналом\n{3} Керування_Сообщениями```",
|
||||
"pingTitle1": "Інформація про бота:",
|
||||
"pingTitle2": "Інформація про плеєр:",
|
||||
"pingField1": "````ID Шарда: {0}/{1}\nЗатримка Шарда: {2:.3f}s {3}\nРегіон: {4}````",
|
||||
"pingField2": "```Вузол: {0} - {1:.3f}s\nГравці: {2}\nРегіон Голосу: {3}```",
|
||||
"addEffect": "Застосуйте ефект `{0}` фільтр.",
|
||||
"clearEffect": "Звукові ефекти були очищені!",
|
||||
"filterTagAlreadyInUse": "Цей звуковий ефект уже використовується! Будь ласка, використовуйте /cleareffect <Тег>, щоб видалити його.",
|
||||
"playlistViewTitle": "📜 Усі плейлисти користувача {0}",
|
||||
"playlistViewHeaders": "ID:,Час:,Назва:,Треки:",
|
||||
"playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.",
|
||||
"playlistNotFound": "Плейлист [`{0}`] не знайдено. Введіть /playlist view, щоб подивитися всі ваші плейлисти.",
|
||||
"playlistNotAccess": "Вибачте! У вас немає доступу до цього плейлиста!",
|
||||
"playlistNoTrack": "Вибачте! У плейлисті [`{0}`] немає треків.",
|
||||
"playlistNotAllow": "Ця команда не дозволена для пов'язаних і загальних плейлистів.",
|
||||
"playlistPlay": "Додано плейлист [`{0}`] з `{1}` піснями в чергу.",
|
||||
"playlistOverText": "Вибачте! Ім'я плейлиста не може перевищувати 10 символів.",
|
||||
"playlistSameName": "Вибачте! Це ім'я не може збігатися з вашим новим ім'ям.",
|
||||
"playlistDeleteError": "Ви не можете видалити плейлист за замовчуванням.",
|
||||
"playlistRemove": "Ви видалили плейлист [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Вибачте! Ви не можете надіслати запрошення самому собі.",
|
||||
"playlistSendErrorBot": "Вибачте! Ви не можете надіслати запрошення боту.",
|
||||
"playlistBelongs": "Вибачте! Цей плейлист належить <@{0}>.",
|
||||
"playlistShare": "Вибачте! Цим плейлистом уже поділилися з {0}.",
|
||||
"playlistSent": "Вибачте! Ви вже надіслали запрошення раніше.",
|
||||
"noPlaylistAcc": "{0} не створив обліковий запис плейлиста.",
|
||||
"overPlaylistCreation": "Ви не можете створювати більше `{0}` плейлистів!",
|
||||
"playlistExists": "Плейлист [`{0}`] вже існує.",
|
||||
"playlistNotInvalidUrl": "Будь ласка, введіть дійсне посилання або публічне плейлист-посилання на Spotify або YouTube.",
|
||||
"playlistCreated": "Ви створили плейлист `{0}`. Введіть /playlist view для отримання додаткової інформації.",
|
||||
"playlistRenamed": "Ви перейменували `{0}` на `{1}`.",
|
||||
"playlistLimitTrack": "Ви досягли ліміту! Ви можете додати тільки `{0}` пісень до свого плейлиста.",
|
||||
"playlistPlaylistLink": "Вам не дозволено використовувати посилання на плейлист.",
|
||||
"playlistStream": "Вам не дозволено додавати потокові відео у свій плейлист.",
|
||||
"playlistPositionNotFound": "Не вдається знайти позицію `{0}` у вашому плейлисті [`{1}`]!",
|
||||
"playlistRemoved": "👋 Видалено **{0}** з плейлиста {1} [`{2}`].",
|
||||
"playlistClear": "Ви успішно очистили свій плейлист [`{0}`].",
|
||||
"playlistView": "Перегляд плейлистів",
|
||||
"playlistViewDesc": "```Ім'я | ID: {0} | {1}\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Читання: ✓ ✍🏽 Запис: {0} 🗑️ Видалення: {1}",
|
||||
"playlistViewPermsValue2": "📖 Читання: {0}",
|
||||
"playlistViewTrack": "Треки",
|
||||
"playlistViewPage": "Сторінка: {0}/{1} | Загальна тривалість: {2}",
|
||||
"inboxFull": "Вибачте! Поштова скринька {0} переповнена.",
|
||||
"inboxNoMsg": "У вашій поштовій скриньці немає повідомлень.",
|
||||
"invitationSent": "Запрошення надіслано {0}.",
|
||||
"notInChannel": "{0}, для використання голосових команд ви маєте перебувати в {1}. Будь ласка, перезайдіть, якщо ви вже в голосі!",
|
||||
"noTrackPlaying": "Зараз немає пісень, які грають.",
|
||||
"noTrackFound": "Пісні з таким запитом не знайдено! Будь ласка, вкажіть дійсне посилання.",
|
||||
"noLinkSupport": "Команда пошуку не підтримує посилання!",
|
||||
"voted": "Ви проголосували!",
|
||||
"missingPosPerm": "Тільки DJ або адміністратори можуть змінювати позицію.",
|
||||
"missingModePerm": "Тільки DJ або адміністратори можуть перемкнути режим циклу.",
|
||||
"missingQueuePerm": "Тільки DJ або адміністратори можуть видаляти треки з черги.",
|
||||
"missingAutoPlayPerm": "Тільки DJ або адміністратори можуть вмикати або вимикати режим autoplay!",
|
||||
"missingFunctionPerm": "Тільки DJ або адміністратори можуть використовувати цю функцію.",
|
||||
"timeFormatError": "Неправильний формат часу. Приклад: 2:42",
|
||||
"lyricsNotFound": "Текст пісні не знайдено. Введіть /lyrics <Назва пісні> <Автор> для пошуку тексту.",
|
||||
"missingTrackInfo": "Деяка інформація про трек відсутня.",
|
||||
"noVoiceChannel": "Голосовий канал не знайдено!",
|
||||
"playlistAddError": "Вам не дозволено додавати потокові відео в плейлист!",
|
||||
"playlistAddError2": "Сталася помилка під час додавання треків у плейлист!",
|
||||
"playlistLimited": "Ви досягли ліміту! Ви можете додати тільки {0} пісні до свого плейлиста.",
|
||||
"playlistRepeated": "Такий самий трек уже є у вашому плейлисті!",
|
||||
"playlistAdded": "❤️ Додано **{0}** до плейлиста користувача {1} [`{2}`]!",
|
||||
"playerDropdown": "Виберіть пісню для переходу ...",
|
||||
"playerFilter": "Виберіть фільтр для застосування ...",
|
||||
"buttonBack": "Назад",
|
||||
"buttonPause": "Призупинити",
|
||||
"buttonResume": "Продовжити",
|
||||
"buttonSkip": "Скіпнути трек",
|
||||
"buttonLeave": "СТОП",
|
||||
"buttonLoop": "Зациклити",
|
||||
"buttonVolumeUp": "Гучність +",
|
||||
"buttonVolumeDown": "Гучність -",
|
||||
"buttonVolumeMute": "Вимкнути звук",
|
||||
"buttonVolumeUnmute": "Увімкнути звук",
|
||||
"buttonAutoPlay": "Autoplay",
|
||||
"buttonShuffle": "Перемішати",
|
||||
"buttonForward": "Вперед",
|
||||
"buttonRewind": "Назад",
|
||||
"buttonLyrics": "Тексти пісень",
|
||||
"nowplayingDesc": "**Зараз грає:**\n```{0}```",
|
||||
"nowplayingField": "Наступне:",
|
||||
"nowplayingLink": "Слухати на {0}",
|
||||
"connect": "Підключено до {0}",
|
||||
"live": "Прямий ефір",
|
||||
"playlistLoad": "🎶 Додано плейлист **{0}** з `{1}` піснями в чергу.",
|
||||
"trackLoad": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) для початку програвання.\n",
|
||||
"trackLoad_pos": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) у чергу на позицію **{4}**\n",
|
||||
"searchTitle": "Пошук: {0}",
|
||||
"searchDesc": "➥ Платформа: {0} **{1}**\n➥ Результати: **{2}**\n\n{3}",
|
||||
"searchWait": "Виберіть пісню, яку хочете додати в чергу.",
|
||||
"searchTimeout": "Пошук перервано: перевищено час очікування. Будь ласка, спробуйте пізніше.",
|
||||
"searchSuccess": "Пісню додано в чергу.",
|
||||
"queueTitle": "Наступний у черзі:",
|
||||
"historyTitle": "Історія черги:",
|
||||
"viewTitle": "Поточна черга",
|
||||
"viewDesc": "**Заразом грає: [*посилання*]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Сторінка: {0}/{1} | Загальна тривалість: {2}",
|
||||
"pauseError": "Плеєр уже на паузі.",
|
||||
"pauseVote": "{0} проголосував за паузу пісні. [{1}/{2}]",
|
||||
"paused": "`{0}` Поставив плеєр на паузу.",
|
||||
"resumeError": "Плеєр не на паузі.",
|
||||
"resumeVote": "{0} проголосував за продовження пісні. [{1}/{2}]",
|
||||
"resumed": "`{0}` Прибрав плеєр із паузи.",
|
||||
"shuffleError": "Додати більше пісень у чергу перед перемішуванням.",
|
||||
"shuffleVote": "{0} проголосував за перемішування черги. [{1}/{2}]",
|
||||
"shuffled": "Черга перемішана.",
|
||||
"skipError": "Немає пісень для пропуску.",
|
||||
"skipVote": "{0} проголосував за пропуск пісні. [{1}/{2}]",
|
||||
"skipped": "`{0}` пропустив пісню.",
|
||||
"backVote": "{0} проголосував за повернення до попередньої пісні. [{1}/{2}]",
|
||||
"backed": "`{0}` повернувся до попередньої пісні.",
|
||||
"leaveVote": "{0} проголосував за зупинку плеєра. [{1}/{2}]",
|
||||
"left": "`{0}` зупинив плеєр.",
|
||||
"seek": "Встановити плеєр на **{0}**",
|
||||
"repeat": "Режим циклу встановлено на `{0}`",
|
||||
"cleared": "Очищено всі треки в `{0}`",
|
||||
"removed": "Видалено `{0}` треків із черги.",
|
||||
"forward": "Перемотати плеєр вперед на **{0}**",
|
||||
"rewind": "Перемотати плеєр назад на **{0}**",
|
||||
"replay": "Повторення поточної пісні.",
|
||||
"swapped": "Треки `{0}` і `{1}` обміняні місцями",
|
||||
"moved": "Трек `{0}` зміщений на `{1}`",
|
||||
"autoplay": "Режим autoplay встановлено на **{0}**",
|
||||
"notdj": "Ви не DJ. Поточний DJ: {0}.",
|
||||
"djToMe": "Ви не можете передати роль DJ собі або боту.",
|
||||
"djNotInChannel": "`{0}` не знаходиться в голосовому каналі.",
|
||||
"djswap": "Ви передали роль DJ `{0}`.",
|
||||
"voicelinkQueueFull": "Вибачте, ви досягли максимальної кількості `{0}` треків у черзі!",
|
||||
"voicelinkOutofList": "Будь ласка, надайте дійсний індекс треку!",
|
||||
"voicelinkDuplicateTrack": "Вибачте, цей трек уже є в черзі.",
|
||||
"decodeError": "Щось пішло не так під час декодування файлу!",
|
||||
"invalidStartTime": "Недійснений час початку! Час має бути в межах `00:00` та `{0}`.",
|
||||
"invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.",
|
||||
"invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.",
|
||||
"setStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд.",
|
||||
"createSongRequestChannel": "Канал для запитів пісень ({0}) створено! Ви можете почати запитувати будь-яку пісню за назвою або URL у цьому каналі без необхідності використовувати префікс бота."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Щось пішло не так під час виконання команди! Будь ласка, спробуйте пізніше або приєднайтеся до нашого сервера Discord для отримання додаткової підтримки.",
|
||||
"missingIntents": "Вибачте, але цю команду не можна виконати, оскільки у бота відсутній необхідний запит на інтенцію: `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "вкл",
|
||||
"disabled": "викл",
|
||||
"live": "Прямий ефір"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Будь ласка, спробуйте знову! Після перепідключення вузла.",
|
||||
"noPlaySource": "Неможливо знайти робочі джерела!",
|
||||
"noPlayer": "На цьому сервері не знайдено жодного активного плеєра.",
|
||||
"noTrackPlaying": "Зараз немає пісень, які грають.",
|
||||
"noTrackFound": "Пісні з таким запитом не знайдено! Будь ласка, вкажіть дійсне посилання."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Виберіть пісню для переходу ...",
|
||||
"filterSelect": "Виберіть фільтр для застосування ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Назад",
|
||||
"pause": "Призупинити",
|
||||
"resume": "Продовжити",
|
||||
"skip": "Скіпнути трек",
|
||||
"leave": "СТОП",
|
||||
"loopOff": "Вимкнути",
|
||||
"loopTrack": "Трек",
|
||||
"loopQueue": "Черга",
|
||||
"volumeUp": "Гучність +",
|
||||
"volumeDown": "Гучність -",
|
||||
"volumeMute": "Вимкнути звук",
|
||||
"volumeUnmute": "Увімкнути звук",
|
||||
"autoPlay": "Autoplay",
|
||||
"shuffle": "Перемішати",
|
||||
"forward": "Вперед",
|
||||
"rewind": "Назад",
|
||||
"lyrics": "Тексти пісень"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Зараз грає:**\n```{0}```",
|
||||
"nowplayingField": "Наступне:",
|
||||
"nowplayingLink": "Слухати на {0}",
|
||||
"playlistLoad": "🎶 Додано плейлист **{0}** з `{1}` піснями в чергу.",
|
||||
"trackLoad": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) для початку програвання.\n",
|
||||
"trackLoadPos": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) у чергу на позицію **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "Плеєр уже на паузі.",
|
||||
"vote": "{0} проголосував за паузу пісні. [{1}/{2}]",
|
||||
"success": "`{0}` Поставив плеєр на паузу."
|
||||
},
|
||||
"resume": {
|
||||
"error": "Плеєр не на паузі.",
|
||||
"vote": "{0} проголосував за продовження пісні. [{1}/{2}]",
|
||||
"success": "`{0}` Прибрав плеєр із паузи."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Додати більше пісень у чергу перед перемішуванням.",
|
||||
"vote": "{0} проголосував за перемішування черги. [{1}/{2}]",
|
||||
"success": "Черга перемішана."
|
||||
},
|
||||
"skip": {
|
||||
"error": "Немає пісень для пропуску.",
|
||||
"vote": "{0} проголосував за пропуск пісні. [{1}/{2}]",
|
||||
"success": "`{0}` пропустив пісню."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} проголосував за повернення до попередньої пісні. [{1}/{2}]",
|
||||
"success": "`{0}` повернувся до попередньої пісні."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} проголосував за зупинку плеєра. [{1}/{2}]",
|
||||
"success": "`{0}` зупинив плеєр."
|
||||
},
|
||||
"seek": "Встановити плеєр на **{0}**",
|
||||
"repeat": "Режим циклу встановлено на `{0}`",
|
||||
"forward": "Перемотати плеєр вперед на **{0}**",
|
||||
"rewind": "Перемотати плеєр назад на **{0}**",
|
||||
"replay": "Повторення поточної пісні.",
|
||||
"autoplay": "Режим autoplay встановлено на **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.",
|
||||
"alreadyConnected": "Уже підключений до голосового каналу.",
|
||||
"noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.",
|
||||
"notInChannel": "{0}, для використання голосових команд ви маєте перебувати в {1}. Будь ласка, перезайдіть, якщо ви вже в голосі!",
|
||||
"connect": "Підключено до {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Вибачте! У мене немає прав для створення каналу запитів на пісні.",
|
||||
"missingPosition": "Тільки DJ або адміністратори можуть змінювати позицію.",
|
||||
"missingMode": "Тільки DJ або адміністратори можуть перемкнути режим циклу.",
|
||||
"missingQueue": "Тільки DJ або адміністратори можуть видаляти треки з черги.",
|
||||
"missingAutoPlay": "Тільки DJ або адміністратори можуть вмикати або вимикати режим autoplay!",
|
||||
"missingFunction": "Тільки DJ або адміністратори можуть використовувати цю функцію.",
|
||||
"notDj": "Ви не DJ. Поточний DJ: {0}.",
|
||||
"djToSelf": "Ви не можете передати роль DJ собі або боту.",
|
||||
"djNotInChannel": "`{0}` не знаходиться в голосовому каналі.",
|
||||
"djSwapped": "Ви передали роль DJ `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.",
|
||||
"languageChanged": "Успішно змінено на мовний пакет `{0}`.",
|
||||
"prefixSet": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.",
|
||||
"djSet": "Встановити роль DJ {0}.",
|
||||
"queueModeSet": "Встановити режим черги на `{0}`.",
|
||||
"mode247": "Тепер у вас `{0}` режим 24/7.",
|
||||
"bypassVote": "Тепер у вас `{0}` система голосування.",
|
||||
"volumeSet": "Встановити гучність на `{0}`%",
|
||||
"controllerToggled": "Тепер у вас `{0}` контролер музики.",
|
||||
"duplicateTrackToggled": "Тепер у вас `{0}` запобігання дублюванню треку в черзі.",
|
||||
"controllerMsgToggled": "Тепер у вас `{0}` повідомлення від контролера музики.",
|
||||
"silentMsgToggled": "У вас {0} тихих повідомлень.",
|
||||
"resetDone": "`{0}` встановлено на значення за замовчуванням сервера.",
|
||||
"resetNotFound": "Неправильна назва налаштування. Будь ласка, використовуйте одне з наведених нижче значень: {0}"
|
||||
},
|
||||
"menu": "Налаштування сервера | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Основна інформація:",
|
||||
"value": "```Префікс: {0}\nМова: {1}\nУвімкнути контролер музики: {2}\nDJ роль: {3}\nОбхід голосування: {4}\n24/7: {5}\nГромкість за замовчуванням: {6}%\nЧас програвання: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Інформація про чергу:",
|
||||
"value": "```Режим черги: {0}\nМаксимальна кількість пісень: {1}\nДозволити дублювання треків: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Інформація про стан голосу:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Права:",
|
||||
"value": "```{0} Адміністратор\n{1} Керування_Сервером\n{2} Керування_Каналом\n{3} Керування_Сообщениями```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Інформація про бота:",
|
||||
"title2": "Інформація про плеєр:",
|
||||
"field1": "````ID Шарда: {0}/{1}\nЗатримка Шарда: {2:.3f}s {3}\nРегіон: {4}````",
|
||||
"field2": "```Вузол: {0} - {1:.3f}s\nГравці: {2}\nРегіон Голосу: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Застосуйте ефект `{0}` фільтр.",
|
||||
"cleared": "Звукові ефекти були очищені!",
|
||||
"tagInUse": "Цей звуковий ефект уже використовується! Будь ласка, використовуйте /cleareffect <Тег>, щоб видалити його."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Усі плейлисти користувача {0}",
|
||||
"headers": "ID:,Час:,Назва:,Треки:",
|
||||
"footer": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.",
|
||||
"detailTitle": "Перегляд плейлистів",
|
||||
"detailDesc": "```Ім'я: {0} [{1}]\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```",
|
||||
"permsValue": "📖 Читання: ✓ ✍🏽 Запис: {0} 🗑️ Видалення: {1}",
|
||||
"permsValue2": "📖 Читання: {0}",
|
||||
"trackList": "Треки",
|
||||
"footer2": "Загальна тривалість: {0}",
|
||||
"playlistSelect": "Виберіть плейлист для перегляду ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Плейлист [`{0}`] не знайдено. Введіть /playlist view, щоб подивитися всі ваші плейлисти.",
|
||||
"noAccess": "Вибачте! У вас немає доступу до цього плейлиста!",
|
||||
"noTrack": "Вибачте! У плейлисті [`{0}`] немає треків.",
|
||||
"notAllowed": "Ця команда не дозволена для пов'язаних і загальних плейлистів.",
|
||||
"nameOverLimit": "Вибачте! Ім'я плейлиста не може перевищувати 10 символів.",
|
||||
"sameName": "Вибачте! Це ім'я не може збігатися з вашим новим ім'ям.",
|
||||
"deleteDefault": "Ви не можете видалити плейлист за замовчуванням.",
|
||||
"limitReached": "Ви не можете створювати більше `{0}` плейлистів!",
|
||||
"exists": "Плейлист [`{0}`] вже існує.",
|
||||
"invalidUrl": "Будь ласка, введіть дійсне посилання на публічний плейлист.",
|
||||
"trackLimitReached": "Ви досягли ліміту! Ви можете додати тільки `{0}` пісень до свого плейлиста.",
|
||||
"playlistLinkNotAllowed": "Вам не дозволено використовувати посилання на плейлист.",
|
||||
"streamNotAllowed": "Вам не дозволено додавати потокові відео у свій плейлист.",
|
||||
"positionNotFound": "Не вдається знайти позицію `{0}` у вашому плейлисті [`{1}`]!",
|
||||
"streaming": "Вам не дозволено додавати потокові відео в плейлист!",
|
||||
"track": "Сталася помилка під час додавання треків у плейлист!",
|
||||
"trackLimited": "Ви досягли ліміту! Ви можете додати тільки {0} пісні до свого плейлиста.",
|
||||
"trackRepeated": "Такий самий трек уже є у вашому плейлисті!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Додано плейлист [`{0}`] з `{1}` піснями в чергу.",
|
||||
"removed": "Ви видалили плейлист [`{0}`].",
|
||||
"created": "Ви створили плейлист `{0}`. Введіть /playlist view для отримання додаткової інформації.",
|
||||
"renamed": "Ви перейменували `{0}` на `{1}`.",
|
||||
"trackRemoved": "👋 Видалено **{0}** з плейлиста {1} [`{2}`].",
|
||||
"cleared": "Ви успішно очистили свій плейлист [`{0}`].",
|
||||
"trackAdded": "❤️ Додано **{0}** до плейлиста користувача {1} [`{2}`]!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Вибачте! Ви не можете надіслати запрошення самому собі.",
|
||||
"sendErrorBot": "Вибачте! Ви не можете надіслати запрошення боту.",
|
||||
"belongs": "Вибачте! Цей плейлист належить <@{0}>.",
|
||||
"alreadyShared": "Вибачте! Цим плейлистом уже поділилися з {0}.",
|
||||
"alreadySent": "Вибачте! Ви вже надіслали запрошення раніше.",
|
||||
"noAccount": "{0} не створив обліковий запис плейлиста.",
|
||||
"invitationSent": "Запрошення надіслано {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Вибачте! Поштова скринька {0} переповнена.",
|
||||
"noMessages": "У вашій поштовій скриньці немає повідомлень."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "Команда пошуку не підтримує посилання!",
|
||||
"title": "Пошук: {0}",
|
||||
"desc": "➥ Платформа: {0} **{1}**\n➥ Результати: **{2}**\n\n{3}",
|
||||
"wait": "Виберіть пісню, яку хочете додати в чергу.",
|
||||
"success": "Пісню додано в чергу."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "Ви проголосували!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Неправильний формат часу. Приклад: 2:42",
|
||||
"invalidStartTime": "Недійснений час початку! Час має бути в межах `00:00` та `{0}`.",
|
||||
"invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`."
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Текст пісні не знайдено. Введіть /lyrics <Назва пісні> <Автор> для пошуку тексту."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Наступний у черзі:",
|
||||
"cleared": "Очищено всі треки в `{0}`",
|
||||
"removed": "Видалено `{0}` треків із черги.",
|
||||
"swapped": "Треки `{0}` і `{1}` обміняні місцями",
|
||||
"moved": "Трек `{0}` зміщений на `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "Історія черги:",
|
||||
"title": "Поточна черга",
|
||||
"desc": "**Заразом грає: [*посилання*]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Вибачте, ви досягли максимальної кількості `{0}` треків у черзі!",
|
||||
"outOfList": "Будь ласка, надайте дійсний індекс треку!",
|
||||
"duplicateTrack": "Вибачте, цей трек уже є в черзі."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "Канал для запитів пісень ({0}) створено! Ви можете почати запитувати будь-яку пісню за назвою або URL у цьому каналі без необхідності використовувати префікс бота."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Назад",
|
||||
"next": "Далі",
|
||||
"play": "Грати",
|
||||
"share": "Поділитися",
|
||||
"export": "Експортувати",
|
||||
"delete": "Видалити",
|
||||
"page": {
|
||||
"title": "Введіть номер сторінки",
|
||||
"field": "Номер сторінки"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
langs/VN.json
436
langs/VN.json
@@ -1,172 +1,266 @@
|
||||
{
|
||||
"unknownException": "⚠️ Đã xảy ra lỗi khi chạy lệnh! Vui lòng thử lại sau hoặc vào server Discord của chúng tôi để được hỗ trợ.",
|
||||
"enabled": "đã bật",
|
||||
"disabled": "đã tắt",
|
||||
"nodeReconnect": "Vui lòng thử lại! Sau khi node kết nối lại.",
|
||||
"noChannel": "Không có kênh voice để kết nối. Vui lòng cung cấp một kênh hoặc tham gia một kênh.",
|
||||
"alreadyConnected": "Đã kết nối với kênh voice.",
|
||||
"noPermission": "Xin lỗi! Tôi không có quyền tham gia hoặc nói trong kênh voice của bạn.",
|
||||
"noCreatePermission": "Xin lỗi! Tôi không có quyền tạo kênh yêu cầu bài hát.",
|
||||
"noPlaySource": "Không thể tìm thấy nguồn phát nào!",
|
||||
"noPlayer": "Không tìm thấy trình phát nào trên máy chủ này.",
|
||||
"notVote": "Lệnh này yêu cầu bạn bình chọn! Gõ `/vote` để biết thêm thông tin.",
|
||||
"missingIntents": "Xin lỗi, lệnh này không thể thực hiện vì bot thiếu quyền yêu cầu cần thiết: `({0})`.",
|
||||
"languageNotFound": "Không tìm thấy gói ngôn ngữ! Vui lòng chọn một gói ngôn ngữ hiện có.",
|
||||
"changedLanguage": "Đã thay đổi thành công sang gói ngôn ngữ `{0}`.",
|
||||
"setPrefix": "Hoàn thành! Tiền tố của tôi trong máy chủ của bạn bây giờ là `{0}`. Thử chạy `{1}ping` để kiểm tra.",
|
||||
"setDJ": "Đặt DJ thành {0}.",
|
||||
"setQueue": "Đặt chế độ hàng đợi thành `{0}`.",
|
||||
"247": "Chế độ 24/7 bây giờ là `{0}`.",
|
||||
"bypassVote": "Bạn có hệ thống bình chọn `{0}`.",
|
||||
"setVolume": "Đặt âm lượng thành `{0}`%",
|
||||
"toggleController": "Bây giờ bạn đã `{0}` bộ điều khiển nhạc.",
|
||||
"toggleDuplicateTrack": "Bây giờ bạn đã `{0}` tính năng ngăn chặn bài hát trùng lặp.",
|
||||
"toggleControllerMsg": "Bây giờ bạn đã `{0}` tin nhắn từ bộ điều khiển nhạc.",
|
||||
"toggleSilentMsg": "Bạn có {0} tin nhắn im lặng.",
|
||||
"settingsMenu": "Cài Đặt Máy Chủ | {0}",
|
||||
"settingsTitle": "❤️ Thông Tin Cơ Bản:",
|
||||
"settingsValue": "```Tiền tố: {0}\nNgôn ngữ: {1}\nBộ Điều Khiển Nhạc: {2}\nVai Trò DJ: @{3}\nBỏ Qua Bình Chọn: {4}\n24/7: {5}\nÂm Lượng Mặc Định: {6}%\nThời Gian Phát: {7}```",
|
||||
"settingsTitle2": "🔗 Thông tin hàng đợi:",
|
||||
"settingsValue2": "```Chế Độ Hàng Đợi: {0}\nSố Bài Tối Đa: {1}\nBài Hát Trùng Lặp: {2}```",
|
||||
"settingsTitle3": "🎤 Thông Tin Trạng Thái Voice:",
|
||||
"settingsPermTitle": "✨ Quyền hạn:",
|
||||
"settingsPermValue": "```{0} Quản Trị Viên\n{1} Quản_Lý_Guild\n{2} Quản_Lý_Kênh\n{3} Quản_Lý_Tin_Nhắn```",
|
||||
"pingTitle1": "Thông Tin Bot:",
|
||||
"pingTitle2": "Thông Tin Trình Phát:",
|
||||
"pingField1": "```ID Shard: {0}/{1}\nĐộ Trễ Shard: {2:.3f}s {3}\nKhu Vực: {4}```",
|
||||
"pingField2": "```Node: {0} - {1:.3f}s\nTrình Phát: {2}\nKhu Vực Voice: {3}```",
|
||||
"addEffect": "Đã áp dụng hiệu ứng `{0}`.",
|
||||
"clearEffect": "Các hiệu ứng âm thanh đã được xóa!",
|
||||
"filterTagAlreadyInUse": "Hiệu ứng âm thanh này đã được sử dụng! Vui lòng sử dụng /cleareffect <Tag> để xóa nó.",
|
||||
"playlistViewTitle": "📜 Tất Cả Playlist Của {0}",
|
||||
"playlistViewHeaders": "ID:,Thời Gian:,Tên:,Bài Hát:",
|
||||
"playlistFooter": "Gõ /playlist play [playlist] để thêm playlist vào hàng đợi.",
|
||||
"playlistNotFound": "Không tìm thấy playlist [`{0}`]. Gõ /playlist view để xem tất cả playlist của bạn.",
|
||||
"playlistNotAccess": "Xin lỗi! Bạn không được phép truy cập playlist này!",
|
||||
"playlistNoTrack": "Xin lỗi! Không có bài hát nào trong playlist [`{0}`].",
|
||||
"playlistNotAllow": "Lệnh này không được phép trên playlist được liên kết và chia sẻ.",
|
||||
"playlistPlay": "Đã thêm playlist [`{0}`] với `{1}` bài hát vào hàng đợi.",
|
||||
"playlistOverText": "Xin lỗi! Tên playlist không thể vượt quá 10 ký tự.",
|
||||
"playlistSameName": "Xin lỗi! Vui lòng chọn tên mới cho playlist.",
|
||||
"playlistDeleteError": "Bạn không được phép xóa playlist mặc định.",
|
||||
"playlistRemove": "Bạn đã xóa playlist [`{0}`].",
|
||||
"playlistSendErrorPlayer": "Xin lỗi! Bạn không thể gửi lời mời cho chính mình.",
|
||||
"playlistSendErrorBot": "Xin lỗi! Bạn không thể gửi lời mời cho một bot.",
|
||||
"playlistBelongs": "Xin lỗi! Playlist này thuộc về <@{0}>.",
|
||||
"playlistShare": "Xin lỗi! Playlist này đã được chia sẻ với {0}.",
|
||||
"playlistSent": "Xin lỗi! Bạn đã gửi lời mời cho người dùng đó trước đó rồi.",
|
||||
"noPlaylistAcc": "{0} không tạo tài khoản playlist.",
|
||||
"overPlaylistCreation": "Bạn không thể tạo nhiều hơn `{0}` playlist!",
|
||||
"playlistExists": "Playlist [`{0}`] đã tồn tại.",
|
||||
"playlistNotInvalidUrl": "Vui lòng nhập liên kết playlist Spotify hoặc Youtube công khai hợp lệ.",
|
||||
"playlistCreated": "Bạn đã tạo playlist `{0}`. Gõ /playlist view để biết thêm thông tin.",
|
||||
"playlistRenamed": "Bạn đã đổi tên `{0}` thành `{1}`.",
|
||||
"playlistLimitTrack": "Bạn đã đạt giới hạn! Bạn chỉ có thể thêm `{0}` bài hát vào playlist của mình.",
|
||||
"playlistPlaylistLink": "Bạn không được phép sử dụng liên kết playlist.",
|
||||
"playlistStream": "Bạn không được phép thêm video trực tiếp vào playlist của mình.",
|
||||
"playlistPositionNotFound": "Không thể tìm thấy vị trí `{0}` từ playlist [`{1}`] của bạn!",
|
||||
"playlistRemoved": "👋 Đã xóa **{0}** khỏi playlist [`{2}`] của {1}.",
|
||||
"playlistClear": "Bạn đã xóa thành công playlist [`{0}`] của mình.",
|
||||
"playlistView": "Trình Xem Playlist",
|
||||
"playlistViewDesc": "```Tên | ID: {0} | {1}\nTổng Số Bài Hát: {2}\nChủ Sở Hữu: {3}\nLoại: {4}\n```",
|
||||
"playlistViewPermsValue": "📖 Đọc: ✓ ✍🏽 Viết: {0} 🗑️ Xóa: {1}",
|
||||
"playlistViewPermsValue2": "📖 Đọc: {0}",
|
||||
"playlistViewTrack": "Bài Hát",
|
||||
"playlistViewPage": "Trang: {0}/{1} | Tổng Thời Lượng: {2}",
|
||||
"inboxFull": "Xin lỗi! Hộp thư của {0} đã đầy.",
|
||||
"inboxNoMsg": "Không có tin nhắn nào trong hộp thư của bạn.",
|
||||
"invitationSent": "Đã gửi lời mời đến {0}.",
|
||||
"notInChannel": "{0}, bạn phải ở trong {1} để sử dụng lệnh voice. Vui lòng tham gia lại nếu bạn đang ở trong voice!",
|
||||
"noTrackPlaying": "Hiện tại không có bài hát nào đang phát",
|
||||
"noTrackFound": "Không tìm thấy bài hát nào với truy vấn đó! Vui lòng cung cấp url hợp lệ.",
|
||||
"noLinkSupport": "Lệnh tìm kiếm không hỗ trợ liên kết!",
|
||||
"voted": "Bạn đã bình chọn!",
|
||||
"missingPosPerm": "Chỉ DJ hoặc quản trị viên mới có thể thay đổi vị trí.",
|
||||
"missingModePerm": "Chỉ DJ hoặc quản trị viên mới có thể chuyển chế độ lặp.",
|
||||
"missingQueuePerm": "Chỉ DJ hoặc quản trị viên mới có thể xóa bài hát khỏi hàng đợi.",
|
||||
"missingAutoPlayPerm": "Chỉ DJ hoặc quản trị viên mới có thể bật hoặc tắt chế độ tự động phát!",
|
||||
"missingFunctionPerm": "Chỉ DJ hoặc Admin mới có thể sử dụng chức năng này.",
|
||||
"timeFormatError": "Định dạng thời gian không chính xác. Ví dụ: 2:42 hoặc 12:39:31",
|
||||
"lyricsNotFound": "Không tìm thấy lời bài hát. Gõ /lyrics <Tên Bài Hát> <Tác Giả> để tìm lời bài hát.",
|
||||
"missingTrackInfo": "Một số chi tiết bài hát bị thiếu.",
|
||||
"noVoiceChannel": "Không Tìm Thấy Kênh Voice!",
|
||||
"playlistAddError": "Bạn không được phép thêm video trực tiếp vào playlist của mình!",
|
||||
"playlistAddError2": "Đã xảy ra sự cố khi thêm bài hát vào playlist!",
|
||||
"playlistLimited": "Bạn đã đạt giới hạn! Bạn chỉ có thể thêm {0} bài hát vào playlist của mình.",
|
||||
"playlistRepeated": "Bài hát này đã có trong playlist của bạn!",
|
||||
"playlistAdded": "❤️ Đã thêm **{0}** vào playlist [`{2}`] của {1}!",
|
||||
"playerDropdown": "Chọn một bài hát để bỏ qua đến ...",
|
||||
"playerFilter": "Chọn một bộ lọc để áp dụng ...",
|
||||
"buttonBack": "Quay Lại",
|
||||
"buttonPause": "Tạm Dừng",
|
||||
"buttonResume": "Tiếp Tục",
|
||||
"buttonSkip": "Bỏ Qua",
|
||||
"buttonLeave": "Rời Khỏi",
|
||||
"buttonLoop": "Lặp Lại",
|
||||
"buttonVolumeUp": "Tăng Âm Lượng",
|
||||
"buttonVolumeDown": "Giảm Âm Lượng",
|
||||
"buttonVolumeMute": "Tắt Tiếng",
|
||||
"buttonVolumeUnmute": "Bật Tiếng",
|
||||
"buttonAutoPlay": "Tự Động Phát",
|
||||
"buttonShuffle": "Trộn Bài",
|
||||
"buttonForward": "Tua Tới",
|
||||
"buttonRewind": "Tua Lùi",
|
||||
"buttonLyrics": "Lời Bài Hát",
|
||||
"nowplayingDesc": "**Đang Phát:**\n```{0}```",
|
||||
"nowplayingField": "Tiếp Theo:",
|
||||
"nowplayingLink": "Nghe trên {0}",
|
||||
"connect": "Đã kết nối với {0}",
|
||||
"live": "TRỰC TIẾP",
|
||||
"playlistLoad": " 🎶 Đã thêm playlist **{0}** với `{1}` bài hát vào hàng đợi.",
|
||||
"trackLoad": "Đã thêm **[{0}](<{1}>)** bởi **{2}** (`{3}`) để bắt đầu phát.\n",
|
||||
"trackLoad_pos": "Đã thêm **[{0}](<{1}>)** bởi **{2}** (`{3}`) vào hàng đợi ở vị trí **{4}**\n",
|
||||
"searchTitle": "Truy Vấn Tìm Kiếm: {0}",
|
||||
"searchDesc": "➥ Nền tảng: {0} **{1}**\n➥ Kết quả: **{2}**\n\n{3}",
|
||||
"searchWait": "Chọn bài hát bạn muốn thêm vào hàng đợi.",
|
||||
"searchTimeout": "Tìm kiếm hết thời gian. Vui lòng thử lại sau.",
|
||||
"searchSuccess": "Đã thêm bài hát vào hàng đợi.",
|
||||
"queueTitle": "Hàng Đợi Sắp Tới:",
|
||||
"historyTitle": "Lịch Sử Hàng Đợi:",
|
||||
"viewTitle": "Hàng Đợi Nhạc",
|
||||
"viewDesc": "**Đang Phát: [Nhấp Vào Đây]({0}) ⮯**\n{1}",
|
||||
"viewFooter": "Trang: {0}/{1} | Tổng Thời Lượng: {2}",
|
||||
"pauseError": "Trình phát đã được tạm dừng.",
|
||||
"pauseVote": "{0} đã bình chọn tạm dừng bài hát. [{1}/{2}]",
|
||||
"paused": "`{0}` đã tạm dừng trình phát.",
|
||||
"resumeError": "Trình phát không bị tạm dừng.",
|
||||
"resumeVote": "{0} đã bình chọn tiếp tục bài hát. [{1}/{2}]",
|
||||
"resumed": "`{0}` đã tiếp tục trình phát.",
|
||||
"shuffleError": "Thêm nhiều bài hát vào hàng đợi trước khi trộn bài.",
|
||||
"shuffleVote": "{0} đã bình chọn trộn hàng đợi. [{1}/{2}]",
|
||||
"shuffled": "Hàng đợi đã được trộn.",
|
||||
"skipError": "Không có bài hát nào để bỏ qua.",
|
||||
"skipVote": "{0} đã bình chọn bỏ qua bài hát. [{1}/{2}]",
|
||||
"skipped": "`{0}` đã bỏ qua bài hát.",
|
||||
"backVote": "{0} đã bình chọn quay lại bài hát trước. [{1}/{2}]",
|
||||
"backed": "`{0}` đang quay lại bài hát trước.",
|
||||
"leaveVote": "{0} đã bình chọn dừng trình phát. [{1}/{2}]",
|
||||
"left": "`{0}` đã dừng trình phát.",
|
||||
"seek": "Đặt trình phát thành **{0}**",
|
||||
"repeat": "Chế độ lặp lại đã được đặt thành `{0}`",
|
||||
"cleared": "Đã xóa tất cả bài hát trong `{0}`",
|
||||
"removed": "`{0}` bài hát đã được xóa khỏi hàng đợi.",
|
||||
"forward": "Tua tới trình phát đến **{0}**",
|
||||
"rewind": "Tua lùi trình phát đến **{0}**",
|
||||
"replay": "Phát lại bài hát hiện tại.",
|
||||
"swapped": "`{0}` và `{1}` đã được hoán đổi",
|
||||
"moved": "Đã di chuyển `{0}` đến `{1}`",
|
||||
"autoplay": "Chế độ tự động phát bây giờ là **{0}**",
|
||||
"notdj": "Bạn không phải là DJ. DJ hiện tại là {0}.",
|
||||
"djToMe": "Bạn không thể chuyển vai trò DJ cho chính mình hoặc một bot.",
|
||||
"djNotInChannel": "`{0}` không ở trong kênh voice.",
|
||||
"djswap": "Bạn đã chuyển vai trò DJ cho `{0}`.",
|
||||
"voicelinkQueueFull": "Xin lỗi, bạn đã đạt tối đa `{0}` bài hát trong hàng đợi!",
|
||||
"voicelinkOutofList": "Vui lòng cung cấp chỉ số bài hát hợp lệ!",
|
||||
"voicelinkDuplicateTrack": "Xin lỗi, bài hát này đã có trong hàng đợi.",
|
||||
"decodeError": "Đã xảy ra lỗi khi giải mã tệp!",
|
||||
"invalidStartTime": "Thời gian bắt đầu không hợp lệ, phải nằm giữa `00:00` và `{0}`",
|
||||
"invalidEndTime": "Thời gian kết thúc không hợp lệ, phải nằm giữa `00:00` và `{0}`",
|
||||
"invalidTimeOrder": "Thời gian kết thúc không thể nhỏ hơn hoặc bằng thời gian bắt đầu",
|
||||
"setStageAnnounceTemplate": "Hoàn thành! Từ bây giờ, trạng thái voice như cái bạn đang ở sẽ được đặt tên theo mẫu của bạn. Bạn sẽ thấy nó cập nhật trong vài giây.",
|
||||
"createSongRequestChannel": "Một kênh yêu cầu bài hát ({0}) đã được tạo! Bạn có thể bắt đầu yêu cầu bất kỳ bài hát nào bằng tên hoặc URL trong kênh đó, mà không cần sử dụng tiền tố bot."
|
||||
}
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ Đã xảy ra lỗi khi chạy lệnh! Vui lòng thử lại sau hoặc vào server Discord của chúng tôi để được hỗ trợ.",
|
||||
"missingIntents": "Xin lỗi, lệnh này không thể thực hiện vì bot thiếu quyền yêu cầu cần thiết: `({0})`."
|
||||
},
|
||||
"status": {
|
||||
"enabled": "đã bật",
|
||||
"disabled": "đã tắt",
|
||||
"live": "TRỰC TIẾP"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "Vui lòng thử lại! Sau khi node kết nối lại.",
|
||||
"noPlaySource": "Không thể tìm thấy nguồn phát nào!",
|
||||
"noPlayer": "Không tìm thấy trình phát nào trên máy chủ này.",
|
||||
"noTrackPlaying": "Hiện tại không có bài hát nào đang phát",
|
||||
"noTrackFound": "Không tìm thấy bài hát nào với truy vấn đó! Vui lòng cung cấp url hợp lệ."
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "Chọn một bài hát để bỏ qua đến ...",
|
||||
"filterSelect": "Chọn một bộ lọc để áp dụng ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Quay Lại",
|
||||
"pause": "Tạm Dừng",
|
||||
"resume": "Tiếp Tục",
|
||||
"skip": "Bỏ Qua",
|
||||
"leave": "Rời Khỏi",
|
||||
"loopOff": "Tắt",
|
||||
"loopTrack": "Bài hát",
|
||||
"loopQueue": "Hàng đợi",
|
||||
"volumeUp": "Tăng Âm Lượng",
|
||||
"volumeDown": "Giảm Âm Lượng",
|
||||
"volumeMute": "Tắt Tiếng",
|
||||
"volumeUnmute": "Bật Tiếng",
|
||||
"autoPlay": "Tự Động Phát",
|
||||
"shuffle": "Trộn Bài",
|
||||
"forward": "Tua Tới",
|
||||
"rewind": "Tua Lùi",
|
||||
"lyrics": "Lời Bài Hát"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**Đang Phát:**\n```{0}```",
|
||||
"nowplayingField": "Tiếp Theo:",
|
||||
"nowplayingLink": "Nghe trên {0}",
|
||||
"playlistLoad": " 🎶 Đã thêm playlist **{0}** với `{1}` bài hát vào hàng đợi.",
|
||||
"trackLoad": "Đã thêm **[{0}](<{1}>)** bởi **{2}** (`{3}`) để bắt đầu phát.\n",
|
||||
"trackLoadPos": "Đã thêm **[{0}](<{1}>)** bởi **{2}** (`{3}`) vào hàng đợi ở vị trí **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "Trình phát đã được tạm dừng.",
|
||||
"vote": "{0} đã bình chọn tạm dừng bài hát. [{1}/{2}]",
|
||||
"success": "`{0}` đã tạm dừng trình phát."
|
||||
},
|
||||
"resume": {
|
||||
"error": "Trình phát không bị tạm dừng.",
|
||||
"vote": "{0} đã bình chọn tiếp tục bài hát. [{1}/{2}]",
|
||||
"success": "`{0}` đã tiếp tục trình phát."
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "Thêm nhiều bài hát vào hàng đợi trước khi trộn bài.",
|
||||
"vote": "{0} đã bình chọn trộn hàng đợi. [{1}/{2}]",
|
||||
"success": "Hàng đợi đã được trộn."
|
||||
},
|
||||
"skip": {
|
||||
"error": "Không có bài hát nào để bỏ qua.",
|
||||
"vote": "{0} đã bình chọn bỏ qua bài hát. [{1}/{2}]",
|
||||
"success": "`{0}` đã bỏ qua bài hát."
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} đã bình chọn quay lại bài hát trước. [{1}/{2}]",
|
||||
"success": "`{0}` đang quay lại bài hát trước."
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} đã bình chọn dừng trình phát. [{1}/{2}]",
|
||||
"success": "`{0}` đã dừng trình phát."
|
||||
},
|
||||
"seek": "Đặt trình phát thành **{0}**",
|
||||
"repeat": "Chế độ lặp lại đã được đặt thành `{0}`",
|
||||
"forward": "Tua tới trình phát đến **{0}**",
|
||||
"rewind": "Tua lùi trình phát đến **{0}**",
|
||||
"replay": "Phát lại bài hát hiện tại.",
|
||||
"autoplay": "Chế độ tự động phát bây giờ là **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "Không có kênh voice để kết nối. Vui lòng cung cấp một kênh hoặc tham gia một kênh.",
|
||||
"alreadyConnected": "Đã kết nối với kênh voice.",
|
||||
"noPermission": "Xin lỗi! Tôi không có quyền tham gia hoặc nói trong kênh voice của bạn.",
|
||||
"notInChannel": "{0}, bạn phải ở trong {1} để sử dụng lệnh voice. Vui lòng tham gia lại nếu bạn đang ở trong voice!",
|
||||
"connect": "Đã kết nối với {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "Hoàn thành! Từ bây giờ, trạng thái voice như cái bạn đang ở sẽ được đặt tên theo mẫu của bạn. Bạn sẽ thấy nó cập nhật trong vài giây."
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "Xin lỗi! Tôi không có quyền tạo kênh yêu cầu bài hát.",
|
||||
"missingPosition": "Chỉ DJ hoặc quản trị viên mới có thể thay đổi vị trí.",
|
||||
"missingMode": "Chỉ DJ hoặc quản trị viên mới có thể chuyển chế độ lặp.",
|
||||
"missingQueue": "Chỉ DJ hoặc quản trị viên mới có thể xóa bài hát khỏi hàng đợi.",
|
||||
"missingAutoPlay": "Chỉ DJ hoặc quản trị viên mới có thể bật hoặc tắt chế độ tự động phát!",
|
||||
"missingFunction": "Chỉ DJ hoặc Admin mới có thể sử dụng chức năng này.",
|
||||
"notDj": "Bạn không phải là DJ. DJ hiện tại là {0}.",
|
||||
"djToSelf": "Bạn không thể chuyển vai trò DJ cho chính mình hoặc một bot.",
|
||||
"djNotInChannel": "`{0}` không ở trong kênh voice.",
|
||||
"djSwapped": "Bạn đã chuyển vai trò DJ cho `{0}`."
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "Không tìm thấy gói ngôn ngữ! Vui lòng chọn một gói ngôn ngữ hiện có.",
|
||||
"languageChanged": "Đã thay đổi thành công sang gói ngôn ngữ `{0}`.",
|
||||
"prefixSet": "Hoàn thành! Tiền tố của tôi trong máy chủ của bạn bây giờ là `{0}`. Thử chạy `{1}ping` để kiểm tra.",
|
||||
"djSet": "Đặt DJ thành {0}.",
|
||||
"queueModeSet": "Đặt chế độ hàng đợi thành `{0}`.",
|
||||
"mode247": "Chế độ 24/7 bây giờ là `{0}`.",
|
||||
"bypassVote": "Bạn có hệ thống bình chọn `{0}`.",
|
||||
"volumeSet": "Đặt âm lượng thành `{0}`%",
|
||||
"controllerToggled": "Bây giờ bạn đã `{0}` bộ điều khiển nhạc.",
|
||||
"duplicateTrackToggled": "Bây giờ bạn đã `{0}` tính năng ngăn chặn bài hát trùng lặp.",
|
||||
"controllerMsgToggled": "Bây giờ bạn đã `{0}` tin nhắn từ bộ điều khiển nhạc.",
|
||||
"silentMsgToggled": "Bạn có {0} tin nhắn im lặng.",
|
||||
"resetDone": "Reset `{0}` to server defaults.",
|
||||
"resetNotFound": "Tên cài đặt không hợp lệ. Vui lòng sử dụng một trong các tùy chọn sau: {0}"
|
||||
},
|
||||
"menu": "Cài Đặt Máy Chủ | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ Thông Tin Cơ Bản:",
|
||||
"value": "```Tiền tố: {0}\nNgôn ngữ: {1}\nBộ Điều Khiển Nhạc: {2}\nVai Trò DJ: @{3}\nBỏ Qua Bình Chọn: {4}\n24/7: {5}\nÂm Lượng Mặc Định: {6}%\nThời Gian Phát: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 Thông tin hàng đợi:",
|
||||
"value": "```Chế Độ Hàng Đợi: {0}\nSố Bài Tối Đa: {1}\nBài Hát Trùng Lặp: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 Thông Tin Trạng Thái Voice:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ Quyền hạn:",
|
||||
"value": "```{0} Quản Trị Viên\n{1} Quản_Lý_Guild\n{2} Quản_Lý_Kênh\n{3} Quản_Lý_Tin_Nhắn```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "Thông Tin Bot:",
|
||||
"title2": "Thông Tin Trình Phát:",
|
||||
"field1": "```ID Shard: {0}/{1}\nĐộ Trễ Shard: {2:.3f}s {3}\nKhu Vực: {4}```",
|
||||
"field2": "```Node: {0} - {1:.3f}s\nTrình Phát: {2}\nKhu Vực Voice: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "Đã áp dụng hiệu ứng `{0}`.",
|
||||
"cleared": "Các hiệu ứng âm thanh đã được xóa!",
|
||||
"tagInUse": "Hiệu ứng âm thanh này đã được sử dụng! Vui lòng sử dụng /cleareffect <Tag> để xóa nó."
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "Tất Cả Playlist Của {0}",
|
||||
"headers": "ID:,Thời Gian:,Tên:,Bài Hát:",
|
||||
"footer": "Gõ /playlist play [playlist] để thêm playlist vào hàng đợi.",
|
||||
"detailTitle": "Trình Xem Playlist",
|
||||
"detailDesc": "```Tên: {0} [{1}]\nTổng Số Bài Hát: {2}\nChủ Sở Hữu: {3}\nLoại: {4}\n```",
|
||||
"permsValue": "📖 Đọc: ✓ ✍🏽 Viết: {0} 🗑️ Xóa: {1}",
|
||||
"permsValue2": "📖 Đọc: {0}",
|
||||
"trackList": "Bài Hát",
|
||||
"footer2": "Tổng thời gian: {0}",
|
||||
"playlistSelect": "Chọn một danh sách phát để xem ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Không tìm thấy playlist [`{0}`]. Gõ /playlist view để xem tất cả playlist của bạn.",
|
||||
"noAccess": "Xin lỗi! Bạn không được phép truy cập playlist này!",
|
||||
"noTrack": "Xin lỗi! Không có bài hát nào trong playlist [`{0}`].",
|
||||
"notAllowed": "Lệnh này không được phép trên playlist được liên kết và chia sẻ.",
|
||||
"nameOverLimit": "Xin lỗi! Tên playlist không thể vượt quá 10 ký tự.",
|
||||
"sameName": "Xin lỗi! Vui lòng chọn tên mới cho playlist.",
|
||||
"deleteDefault": "Bạn không được phép xóa playlist mặc định.",
|
||||
"limitReached": "Bạn không thể tạo nhiều hơn `{0}` playlist!",
|
||||
"exists": "Playlist [`{0}`] đã tồn tại.",
|
||||
"invalidUrl": "Vui lòng nhập liên kết hợp lệ đến danh sách phát công khai.",
|
||||
"trackLimitReached": "Bạn đã đạt giới hạn! Bạn chỉ có thể thêm `{0}` bài hát vào playlist của mình.",
|
||||
"playlistLinkNotAllowed": "Bạn không được phép sử dụng liên kết playlist.",
|
||||
"streamNotAllowed": "Bạn không được phép thêm video trực tiếp vào playlist của mình.",
|
||||
"positionNotFound": "Không thể tìm thấy vị trí `{0}` từ playlist [`{1}`] của bạn!",
|
||||
"streaming": "Bạn không được phép thêm video trực tiếp vào playlist của mình!",
|
||||
"track": "Đã xảy ra sự cố khi thêm bài hát vào playlist!",
|
||||
"trackLimited": "Bạn đã đạt giới hạn! Bạn chỉ có thể thêm {0} bài hát vào playlist của mình.",
|
||||
"trackRepeated": "Bài hát này đã có trong playlist của bạn!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "Đã thêm playlist [`{0}`] với `{1}` bài hát vào hàng đợi.",
|
||||
"removed": "Bạn đã xóa playlist [`{0}`].",
|
||||
"created": "Bạn đã tạo playlist `{0}`. Gõ /playlist view để biết thêm thông tin.",
|
||||
"renamed": "Bạn đã đổi tên `{0}` thành `{1}`.",
|
||||
"trackRemoved": "👋 Đã xóa **{0}** khỏi playlist [`{2}`] của {1}.",
|
||||
"cleared": "Bạn đã xóa thành công playlist [`{0}`] của mình.",
|
||||
"trackAdded": "❤️ Đã thêm **{0}** vào playlist [`{2}`] của {1}!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "Xin lỗi! Bạn không thể gửi lời mời cho chính mình.",
|
||||
"sendErrorBot": "Xin lỗi! Bạn không thể gửi lời mời cho một bot.",
|
||||
"belongs": "Xin lỗi! Playlist này thuộc về <@{0}>.",
|
||||
"alreadyShared": "Xin lỗi! Playlist này đã được chia sẻ với {0}.",
|
||||
"alreadySent": "Xin lỗi! Bạn đã gửi lời mời cho người dùng đó trước đó rồi.",
|
||||
"noAccount": "{0} không tạo tài khoản playlist.",
|
||||
"invitationSent": "Đã gửi lời mời đến {0}."
|
||||
},
|
||||
"inbox": {
|
||||
"full": "Xin lỗi! Hộp thư của {0} đã đầy.",
|
||||
"noMessages": "Không có tin nhắn nào trong hộp thư của bạn."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "Lệnh tìm kiếm không hỗ trợ liên kết!",
|
||||
"title": "Truy Vấn Tìm Kiếm: {0}",
|
||||
"desc": "➥ Nền tảng: {0} **{1}**\n➥ Kết quả: **{2}**\n\n{3}",
|
||||
"wait": "Chọn bài hát bạn muốn thêm vào hàng đợi.",
|
||||
"success": "Đã thêm bài hát vào hàng đợi."
|
||||
},
|
||||
"voting": {
|
||||
"voted": "Bạn đã bình chọn!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "Định dạng thời gian không chính xác. Ví dụ: 2:42 hoặc 12:39:31",
|
||||
"invalidStartTime": "Thời gian bắt đầu không hợp lệ, phải nằm giữa `00:00` và `{0}`",
|
||||
"invalidEndTime": "Thời gian kết thúc không hợp lệ, phải nằm giữa `00:00` và `{0}`"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "Không tìm thấy lời bài hát. Gõ /lyrics <Tên Bài Hát> <Tác Giả> để tìm lời bài hát."
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "Hàng Đợi Sắp Tới:",
|
||||
"cleared": "Đã xóa tất cả bài hát trong `{0}`",
|
||||
"removed": "`{0}` bài hát đã được xóa khỏi hàng đợi.",
|
||||
"swapped": "`{0}` và `{1}` đã được hoán đổi",
|
||||
"moved": "Đã di chuyển `{0}` đến `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "Lịch Sử Hàng Đợi:",
|
||||
"title": "Hàng Đợi Nhạc",
|
||||
"desc": "**Đang Phát: [Nhấp Vào Đây]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "Xin lỗi, bạn đã đạt tối đa `{0}` bài hát trong hàng đợi!",
|
||||
"outOfList": "Vui lòng cung cấp chỉ số bài hát hợp lệ!",
|
||||
"duplicateTrack": "Xin lỗi, bài hát này đã có trong hàng đợi."
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "Một kênh yêu cầu bài hát ({0}) đã được tạo! Bạn có thể bắt đầu yêu cầu bất kỳ bài hát nào bằng tên hoặc URL trong kênh đó, mà không cần sử dụng tiền tố bot."
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Trước",
|
||||
"next": "Tiếp",
|
||||
"play": "Phát",
|
||||
"share": "Chia sẻ",
|
||||
"export": "Xuất",
|
||||
"delete": "Xóa",
|
||||
"page": {
|
||||
"title": "Nhập số trang",
|
||||
"field": "Số trang"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
266
langs/ZHCN.json
Normal file
266
langs/ZHCN.json
Normal file
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ 执行命令时出错!请稍后再试,或加入我们的 Discord 服务器获取支持。",
|
||||
"missingIntents": "抱歉,无法执行此命令,因为缺少所需的请求 intent:`({0})`。"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"live": "直播"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "请在节点重连后再试!",
|
||||
"noPlaySource": "找不到可播放的资源!",
|
||||
"noPlayer": "该服务器上没有找到播放器。",
|
||||
"noTrackPlaying": "当前没有正在播放的歌曲",
|
||||
"noTrackFound": "未找到对应的歌曲,请提供有效链接。"
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "选择要跳转的歌曲 ...",
|
||||
"filterSelect": "选择要应用的音效 ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "上一首",
|
||||
"pause": "暂停",
|
||||
"resume": "继续",
|
||||
"skip": "下一首",
|
||||
"leave": "退出",
|
||||
"loopOff": "关闭",
|
||||
"loopTrack": "音轨",
|
||||
"loopQueue": "队列",
|
||||
"volumeUp": "音量+",
|
||||
"volumeDown": "音量-",
|
||||
"volumeMute": "静音",
|
||||
"volumeUnmute": "取消静音",
|
||||
"autoPlay": "自动播放",
|
||||
"shuffle": "随机播放",
|
||||
"forward": "快进",
|
||||
"rewind": "快退",
|
||||
"lyrics": "歌词"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**正在播放:**\n```{0}```",
|
||||
"nowplayingField": "即将播放:",
|
||||
"nowplayingLink": "在 {0} 收听",
|
||||
"playlistLoad": " 🎶 已将播放列表 **{0}** 的 `{1}` 首歌添加到队列中。",
|
||||
"trackLoad": "已添加 **[{0}](<{1}>)** - **{2}** (`{3}`) 并开始播放。\n",
|
||||
"trackLoadPos": "已添加 **[{0}](<{1}>)** - **{2}** (`{3}`) 到队列位置 **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "播放器已暂停。",
|
||||
"vote": "{0} 投票暂停了歌曲。[{1}/{2}]",
|
||||
"success": "`{0}` 已暂停播放器。"
|
||||
},
|
||||
"resume": {
|
||||
"error": "播放器未暂停。",
|
||||
"vote": "{0} 投票继续播放。[{1}/{2}]",
|
||||
"success": "`{0}` 已继续播放。"
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "请在队列中添加更多歌曲后再随机播放。",
|
||||
"vote": "{0} 投票随机播放队列。[{1}/{2}]",
|
||||
"success": "队列已随机排序。"
|
||||
},
|
||||
"skip": {
|
||||
"error": "没有歌曲可跳过。",
|
||||
"vote": "{0} 投票跳过歌曲。[{1}/{2}]",
|
||||
"success": "`{0}` 已跳过歌曲。"
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} 投票播放上一首歌。[{1}/{2}]",
|
||||
"success": "`{0}` 已切换到上一首歌曲。"
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} 投票停止播放。[{1}/{2}]",
|
||||
"success": "`{0}` 已停止播放器。"
|
||||
},
|
||||
"seek": "已将播放器定位到 **{0}**",
|
||||
"repeat": "循环模式已设置为 `{0}`",
|
||||
"forward": "已快进到 **{0}**",
|
||||
"rewind": "已快退到 **{0}**",
|
||||
"replay": "正在重播当前歌曲。",
|
||||
"autoplay": "自动播放模式现已 **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "未检测到语音频道,请提供一个或加入语音频道。",
|
||||
"alreadyConnected": "已连接到语音频道。",
|
||||
"noPermission": "抱歉!我没有权限加入或在您的语音频道中发言。",
|
||||
"notInChannel": "{0},您必须在 {1} 才能使用语音命令。如果您已在频道,请重新加入!",
|
||||
"connect": "已连接到 {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "完成!从现在起,您所在的语音状态将按照模板命名。几秒后即可看到更新。"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "抱歉!我没有权限创建点歌频道。",
|
||||
"missingPosition": "只有 DJ 或管理员才能更改位置。",
|
||||
"missingMode": "只有 DJ 或管理员才能切换循环模式。",
|
||||
"missingQueue": "只有 DJ 或管理员才能从队列中移除曲目。",
|
||||
"missingAutoPlay": "只有 DJ 或管理员才能启用或禁用自动播放!",
|
||||
"missingFunction": "只有 DJ 或管理员才能使用此功能。",
|
||||
"notDj": "您不是 DJ。当前 DJ 是 {0}。",
|
||||
"djToSelf": "您不能将 DJ 身份转移给自己或机器人。",
|
||||
"djNotInChannel": "`{0}` 不在语音频道中。",
|
||||
"djSwapped": "您已将 DJ 身份转移给 `{0}`。"
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "未找到语言包!请选择一个已有的语言包。",
|
||||
"languageChanged": "已成功切换为 `{0}` 语言包。",
|
||||
"prefixSet": "完成!我在您服务器的前缀现在是 `{0}`。尝试运行 `{1}ping` 测试一下吧。",
|
||||
"djSet": "已将 DJ 设置为 {0}。",
|
||||
"queueModeSet": "已将队列模式设置为 `{0}`。",
|
||||
"mode247": "24/7 模式现已 `{0}`。",
|
||||
"bypassVote": "您拥有 `{0}` 投票系统。",
|
||||
"volumeSet": "已将音量设置为 `{0}`%",
|
||||
"controllerToggled": "您现在已 `{0}` 音乐控制器。",
|
||||
"duplicateTrackToggled": "您已 `{0}` 重复曲目预防功能。",
|
||||
"controllerMsgToggled": "您现在已 `{0}` 来自音乐控制器的消息。",
|
||||
"silentMsgToggled": "您已 {0} 静默消息。",
|
||||
"resetDone": "`{0}` 已重置为服务器默认值。",
|
||||
"resetNotFound": "无效的设置名称。请使用以下之一: {0}"
|
||||
},
|
||||
"menu": "服务器设置 | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ 基本信息:",
|
||||
"value": "```前缀: {0}\n语言: {1}\n音乐控制器: {2}\nDJ 角色: @{3}\n投票跳过: {4}\n24/7: {5}\n默认音量: {6}%\n播放时长: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 队列信息:",
|
||||
"value": "```队列模式: {0}\n最大歌曲数: {1}\n重复曲目: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 语音状态信息:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ 权限:",
|
||||
"value": "```{0} 管理员\n{1} 管理服务器\n{2} 管理频道\n{3} 管理消息```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "机器人信息:",
|
||||
"title2": "播放器信息:",
|
||||
"field1": "```分片 ID: {0}/{1}\n分片延迟: {2:.3f}s {3}\n区域: {4}```",
|
||||
"field2": "```节点: {0} - {1:.3f}s\n播放器数: {2}\n语音区域: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "已应用 `{0}` 音效。",
|
||||
"cleared": "音效已清除!",
|
||||
"tagInUse": "该音效已在使用中!请使用 /cleareffect <标签> 移除。"
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "{0} 的所有播放列表",
|
||||
"headers": "ID:,时长:,名称:,曲目数:",
|
||||
"footer": "输入 /playlist play [播放列表] 将其添加到队列中。",
|
||||
"detailTitle": "播放列表查看器",
|
||||
"detailDesc": "```名称 | ID: {0} | {1}\n总曲目: {2}\n拥有者: {3}\n类型: {4}\n```",
|
||||
"permsValue": "📖 读取: ✓ ✍🏽 写入: {0} 🗑️ 删除: {1}",
|
||||
"permsValue2": "📖 读取: {0}",
|
||||
"trackList": "曲目列表",
|
||||
"footer2": "总持续时间:{0}",
|
||||
"playlistSelect": "选择要查看的播放列表 ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "未找到播放列表 [`{0}`]。输入 /playlist view 查看所有播放列表。",
|
||||
"noAccess": "抱歉!您无权访问此播放列表!",
|
||||
"noTrack": "抱歉!播放列表 [`{0}`] 中没有曲目。",
|
||||
"notAllowed": "该命令不支持已关联或共享的播放列表。",
|
||||
"nameOverLimit": "抱歉!播放列表名称不能超过 10 个字符。",
|
||||
"sameName": "抱歉!请为播放列表选择一个新名字。",
|
||||
"deleteDefault": "不允许删除默认播放列表。",
|
||||
"limitReached": "您最多只能创建 `{0}` 个播放列表!",
|
||||
"exists": "播放列表 [`{0}`] 已存在。",
|
||||
"invalidUrl": "请输入有效的公共播放列表链接。",
|
||||
"trackLimitReached": "已达到上限!每个播放列表最多只能添加 `{0}` 首歌。",
|
||||
"playlistLinkNotAllowed": "不允许使用播放列表链接。",
|
||||
"streamNotAllowed": "不允许将流媒体视频添加到播放列表。",
|
||||
"positionNotFound": "在播放列表 [`{1}`] 中找不到位置 `{0}`!",
|
||||
"streaming": "不允许将流媒体视频添加到播放列表!",
|
||||
"track": "添加曲目到播放列表时出现问题!",
|
||||
"trackLimited": "已达到上限!您最多只能在播放列表中添加 {0} 首歌。",
|
||||
"trackRepeated": "该曲目已在播放列表中!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "已将播放列表 [`{0}`] 的 `{1}` 首歌添加到队列中。",
|
||||
"removed": "您已删除播放列表 [`{0}`]。",
|
||||
"created": "您已创建 `{0}` 播放列表。输入 /playlist view 查看详情。",
|
||||
"renamed": "您已将 `{0}` 重命名为 `{1}`。",
|
||||
"trackRemoved": "👋 已从 {1} 的播放列表 [`{2}`] 中移除 **{0}**。",
|
||||
"cleared": "您已成功清空播放列表 [`{0}`]。",
|
||||
"trackAdded": "❤️ 已将 **{0}** 添加到 {1} 的播放列表 [`{2}`]!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "抱歉!您不能给自己发送邀请。",
|
||||
"sendErrorBot": "抱歉!您不能给机器人发送邀请。",
|
||||
"belongs": "抱歉!此播放列表属于 <@{0}>。",
|
||||
"alreadyShared": "抱歉!此播放列表已与 {0} 共享。",
|
||||
"alreadySent": "抱歉!您之前已经给该用户发送过邀请。",
|
||||
"noAccount": "{0} 还没有创建播放列表账号。",
|
||||
"invitationSent": "邀请已发送给 {0}。"
|
||||
},
|
||||
"inbox": {
|
||||
"full": "抱歉!{0} 的收件箱已满。",
|
||||
"noMessages": "您的收件箱中没有消息。"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "搜索命令不支持链接!",
|
||||
"title": "搜索关键词: {0}",
|
||||
"desc": "➥ 平台: {0} **{1}**\n➥ 结果数: **{2}**\n\n{3}",
|
||||
"wait": "请选择要添加到队列的歌曲。",
|
||||
"success": "歌曲已添加到队列。"
|
||||
},
|
||||
"voting": {
|
||||
"voted": "您已投票!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "时间格式错误。示例: 2:42 或 12:39:31",
|
||||
"invalidStartTime": "无效的开始时间,必须在 `00:00` 和 `{0}` 之间",
|
||||
"invalidEndTime": "无效的结束时间,必须在 `00:00` 和 `{0}` 之间"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "未找到歌词。输入 /lyrics <歌曲名> <作者> 查找歌词。"
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "即将播放队列:",
|
||||
"cleared": "已清空 `{0}` 中的所有曲目",
|
||||
"removed": "已从队列中移除 `{0}` 首歌。",
|
||||
"swapped": "`{0}` 和 `{1}` 已交换位置",
|
||||
"moved": "已将 `{0}` 移动到 `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "播放历史:",
|
||||
"title": "音乐队列",
|
||||
"desc": "**正在播放: [点我]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "抱歉,队列已达到最大 `{0}` 首歌!",
|
||||
"outOfList": "请输入有效的曲目序号!",
|
||||
"duplicateTrack": "抱歉,该曲目已在队列中。"
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "已创建点歌频道 ({0})!您可以在该频道输入歌曲名称或链接来点歌,无需使用机器人前缀。"
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "上一页",
|
||||
"next": "下一页",
|
||||
"play": "播放",
|
||||
"share": "分享",
|
||||
"export": "导出",
|
||||
"delete": "删除",
|
||||
"page": {
|
||||
"title": "输入页码",
|
||||
"field": "页码"
|
||||
}
|
||||
}
|
||||
}
|
||||
266
langs/ZHTW.json
Normal file
266
langs/ZHTW.json
Normal file
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"common": {
|
||||
"errors": {
|
||||
"unknown": "⚠️ 執行指令時發生錯誤!請稍後再試,或加入我們的 Discord 伺服器取得協助。",
|
||||
"missingIntents": "抱歉,無法執行此指令,因為缺少必要的請求 intent:`({0})`。"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已啟用",
|
||||
"disabled": "已停用",
|
||||
"live": "直播"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"errors": {
|
||||
"nodeReconnect": "請在節點重新連線後再試!",
|
||||
"noPlaySource": "找不到可播放的來源!",
|
||||
"noPlayer": "此伺服器上沒有找到播放器。",
|
||||
"noTrackPlaying": "目前沒有正在播放的歌曲",
|
||||
"noTrackFound": "未找到對應的歌曲,請提供有效連結。"
|
||||
},
|
||||
"dropdown": {
|
||||
"trackSelect": "選擇要跳轉的歌曲 ...",
|
||||
"filterSelect": "選擇要套用的音效 ..."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "上一首",
|
||||
"pause": "暫停",
|
||||
"resume": "繼續",
|
||||
"skip": "下一首",
|
||||
"leave": "離開",
|
||||
"loopOff": "關閉",
|
||||
"loopTrack": "音軌",
|
||||
"loopQueue": "隊列",
|
||||
"volumeUp": "音量+",
|
||||
"volumeDown": "音量-",
|
||||
"volumeMute": "靜音",
|
||||
"volumeUnmute": "取消靜音",
|
||||
"autoPlay": "自動播放",
|
||||
"shuffle": "隨機播放",
|
||||
"forward": "快轉",
|
||||
"rewind": "倒轉",
|
||||
"lyrics": "歌詞"
|
||||
},
|
||||
"playback": {
|
||||
"nowplayingDesc": "**正在播放:**\n```{0}```",
|
||||
"nowplayingField": "接下來:",
|
||||
"nowplayingLink": "在 {0} 收聽",
|
||||
"playlistLoad": " 🎶 已將播放清單 **{0}** 的 `{1}` 首歌曲加入隊列。",
|
||||
"trackLoad": "已新增 **[{0}](<{1}>)** - **{2}** (`{3}`) 並開始播放。\n",
|
||||
"trackLoadPos": "已新增 **[{0}](<{1}>)** - **{2}** (`{3}`) 到隊列位置 **{4}**\n"
|
||||
},
|
||||
"controls": {
|
||||
"pause": {
|
||||
"error": "播放器已暫停。",
|
||||
"vote": "{0} 投票暫停了歌曲。[{1}/{2}]",
|
||||
"success": "`{0}` 已暫停播放器。"
|
||||
},
|
||||
"resume": {
|
||||
"error": "播放器未暫停。",
|
||||
"vote": "{0} 投票繼續播放。[{1}/{2}]",
|
||||
"success": "`{0}` 已繼續播放。"
|
||||
},
|
||||
"shuffle": {
|
||||
"error": "請在隊列中新增更多歌曲後再隨機播放。",
|
||||
"vote": "{0} 投票隨機播放隊列。[{1}/{2}]",
|
||||
"success": "隊列已隨機排序。"
|
||||
},
|
||||
"skip": {
|
||||
"error": "沒有歌曲可跳過。",
|
||||
"vote": "{0} 投票跳過歌曲。[{1}/{2}]",
|
||||
"success": "`{0}` 已跳過歌曲。"
|
||||
},
|
||||
"back": {
|
||||
"vote": "{0} 投票播放上一首歌。[{1}/{2}]",
|
||||
"success": "`{0}` 已切換到上一首歌曲。"
|
||||
},
|
||||
"leave": {
|
||||
"vote": "{0} 投票停止播放。[{1}/{2}]",
|
||||
"success": "`{0}` 已停止播放器。"
|
||||
},
|
||||
"seek": "已將播放器定位至 **{0}**",
|
||||
"repeat": "循環模式已設定為 `{0}`",
|
||||
"forward": "已快轉至 **{0}**",
|
||||
"rewind": "已倒轉至 **{0}**",
|
||||
"replay": "正在重播目前的歌曲。",
|
||||
"autoplay": "自動播放模式現在是 **{0}**"
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"connection": {
|
||||
"noChannel": "未偵測到語音頻道,請提供一個或先加入語音頻道。",
|
||||
"alreadyConnected": "已連線至語音頻道。",
|
||||
"noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。",
|
||||
"notInChannel": "{0},您必須在 {1} 才能使用語音指令。如果您已在頻道,請重新加入!",
|
||||
"connect": "已連線至 {0}"
|
||||
},
|
||||
"stageChannel": {
|
||||
"setAnnounceTemplate": "完成!從現在起,您所在的語音狀態將依照模板命名。幾秒後即可看到更新。"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"noCreatePermission": "抱歉!我沒有權限建立點歌頻道。",
|
||||
"missingPosition": "只有 DJ 或管理員能更改位置。",
|
||||
"missingMode": "只有 DJ 或管理員能切換循環模式。",
|
||||
"missingQueue": "只有 DJ 或管理員能從隊列中移除曲目。",
|
||||
"missingAutoPlay": "只有 DJ 或管理員能啟用或停用自動播放!",
|
||||
"missingFunction": "只有 DJ 或管理員能使用此功能。",
|
||||
"notDj": "您不是 DJ。目前 DJ 是 {0}。",
|
||||
"djToSelf": "您不能將 DJ 身分轉移給自己或機器人。",
|
||||
"djNotInChannel": "`{0}` 不在語音頻道中。",
|
||||
"djSwapped": "您已將 DJ 身分轉移給 `{0}`。"
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"languageNotFound": "未找到語言包!請選擇一個現有的語言包。",
|
||||
"languageChanged": "已成功切換為 `{0}` 語言包。",
|
||||
"prefixSet": "完成!我在伺服器的前綴現在是 `{0}`。試著輸入 `{1}ping` 測試一下吧。",
|
||||
"djSet": "已將 DJ 設定為 {0}。",
|
||||
"queueModeSet": "已將隊列模式設定為 `{0}`。",
|
||||
"mode247": "24/7 模式現在是 `{0}`。",
|
||||
"bypassVote": "您擁有 `{0}` 投票系統。",
|
||||
"volumeSet": "已將音量設定為 `{0}`%",
|
||||
"controllerToggled": "您現在已 `{0}` 音樂控制器。",
|
||||
"duplicateTrackToggled": "您已 `{0}` 重複曲目防止功能。",
|
||||
"controllerMsgToggled": "您現在已 `{0}` 來自音樂控制器的訊息。",
|
||||
"silentMsgToggled": "您已 {0} 靜音訊息。",
|
||||
"resetDone": "`{0}` 已重置為伺服器預設值。",
|
||||
"resetNotFound": "無效的設定名稱。請使用以下之一: {0}"
|
||||
},
|
||||
"menu": "伺服器設定 | {0}",
|
||||
"basic": {
|
||||
"title": "❤️ 基本資訊:",
|
||||
"value": "```前綴: {0}\n語言: {1}\n音樂控制器: {2}\nDJ 身分組: @{3}\n投票略過: {4}\n24/7: {5}\n預設音量: {6}%\n播放時長: {7}```"
|
||||
},
|
||||
"queue": {
|
||||
"title": "🔗 隊列資訊:",
|
||||
"value": "```隊列模式: {0}\n最大歌曲數: {1}\n重複曲目: {2}```"
|
||||
},
|
||||
"voice": {
|
||||
"title": "🎤 語音狀態資訊:"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "✨ 權限:",
|
||||
"value": "```{0} 管理員\n{1} 管理伺服器\n{2} 管理頻道\n{3} 管理訊息```"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"title1": "機器人資訊:",
|
||||
"title2": "播放器資訊:",
|
||||
"field1": "```分片 ID: {0}/{1}\n分片延遲: {2:.3f}s {3}\n區域: {4}```",
|
||||
"field2": "```節點: {0} - {1:.3f}s\n播放器數: {2}\n語音區域: {3}```"
|
||||
},
|
||||
"effects": {
|
||||
"added": "已套用 `{0}` 效果。",
|
||||
"cleared": "音效已清除!",
|
||||
"tagInUse": "該音效已在使用中!請使用 /cleareffect <標籤> 移除。"
|
||||
},
|
||||
"playlist": {
|
||||
"view": {
|
||||
"title": "{0} 的所有播放清單",
|
||||
"headers": "ID:,時長:,名稱:,曲目數:",
|
||||
"footer": "輸入 /playlist play [播放清單] 將其加入隊列。",
|
||||
"detailTitle": "播放清單檢視器",
|
||||
"detailDesc": "```名稱 | ID: {0} | {1}\n總曲目: {2}\n擁有者: {3}\n類型: {4}\n```",
|
||||
"permsValue": "📖 讀取: ✓ ✍🏽 寫入: {0} 🗑️ 移除: {1}",
|
||||
"permsValue2": "📖 讀取: {0}",
|
||||
"trackList": "曲目列表",
|
||||
"footer2": "總持續時間:{0}",
|
||||
"playlistSelect": "選擇要查看的播放清單 ..."
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "未找到播放清單 [`{0}`]。輸入 /playlist view 查看所有播放清單。",
|
||||
"noAccess": "抱歉!您無法存取此播放清單!",
|
||||
"noTrack": "抱歉!播放清單 [`{0}`] 中沒有曲目。",
|
||||
"notAllowed": "此指令不支援已連結或共享的播放清單。",
|
||||
"nameOverLimit": "抱歉!播放清單名稱不能超過 10 個字元。",
|
||||
"sameName": "抱歉!請為播放清單選擇新名稱。",
|
||||
"deleteDefault": "不允許刪除預設播放清單。",
|
||||
"limitReached": "您最多只能建立 `{0}` 個播放清單!",
|
||||
"exists": "播放清單 [`{0}`] 已存在。",
|
||||
"invalidUrl": "請輸入有效的公開播放清單連結。",
|
||||
"trackLimitReached": "已達到上限!每個播放清單最多只能新增 `{0}` 首歌曲。",
|
||||
"playlistLinkNotAllowed": "不允許使用播放清單連結。",
|
||||
"streamNotAllowed": "不允許將串流影片加入播放清單。",
|
||||
"positionNotFound": "在播放清單 [`{1}`] 中找不到位置 `{0}`!",
|
||||
"streaming": "不允許將串流影片加入播放清單!",
|
||||
"track": "新增曲目到播放清單時發生問題!",
|
||||
"trackLimited": "已達到上限!播放清單最多只能新增 {0} 首歌曲。",
|
||||
"trackRepeated": "此曲目已在播放清單中!"
|
||||
},
|
||||
"actions": {
|
||||
"play": "已將播放清單 [`{0}`] 的 `{1}` 首歌曲加入隊列。",
|
||||
"removed": "您已刪除播放清單 [`{0}`]。",
|
||||
"created": "您已建立 `{0}` 播放清單。輸入 /playlist view 查看詳情。",
|
||||
"renamed": "您已將 `{0}` 重新命名為 `{1}`。",
|
||||
"trackRemoved": "👋 已從 {1} 的播放清單 [`{2}`] 中移除 **{0}**。",
|
||||
"cleared": "您已成功清空播放清單 [`{0}`]。",
|
||||
"trackAdded": "❤️ 已將 **{0}** 加入 {1} 的播放清單 [`{2}`]!"
|
||||
},
|
||||
"sharing": {
|
||||
"sendErrorPlayer": "抱歉!您不能邀請自己。",
|
||||
"sendErrorBot": "抱歉!您不能邀請機器人。",
|
||||
"belongs": "抱歉!此播放清單屬於 <@{0}>。",
|
||||
"alreadyShared": "抱歉!此播放清單已與 {0} 分享。",
|
||||
"alreadySent": "抱歉!您之前已經邀請過該使用者。",
|
||||
"noAccount": "{0} 尚未建立播放清單帳號。",
|
||||
"invitationSent": "邀請已發送給 {0}。"
|
||||
},
|
||||
"inbox": {
|
||||
"full": "抱歉!{0} 的收件匣已滿。",
|
||||
"noMessages": "您的收件匣中沒有訊息。"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"noLinkSupport": "搜尋指令不支援連結!",
|
||||
"title": "搜尋關鍵字: {0}",
|
||||
"desc": "➥ 平台: {0} **{1}**\n➥ 結果數: **{2}**\n\n{3}",
|
||||
"wait": "請選擇要加入隊列的歌曲。",
|
||||
"success": "歌曲已加入隊列。"
|
||||
},
|
||||
"voting": {
|
||||
"voted": "您已投票!"
|
||||
},
|
||||
"time": {
|
||||
"formatError": "時間格式錯誤。範例: 2:42 或 12:39:31",
|
||||
"invalidStartTime": "無效的開始時間,必須在 `00:00` 和 `{0}` 之間",
|
||||
"invalidEndTime": "無效的結束時間,必須在 `00:00` 和 `{0}` 之間"
|
||||
},
|
||||
"lyrics": {
|
||||
"notFound": "未找到歌詞。輸入 /lyrics <歌曲名稱> <作者> 查找歌詞。"
|
||||
},
|
||||
"queue": {
|
||||
"management": {
|
||||
"title": "即將播放隊列:",
|
||||
"cleared": "已清空 `{0}` 中的所有曲目",
|
||||
"removed": "已從隊列中移除 `{0}` 首歌曲。",
|
||||
"swapped": "`{0}` 與 `{1}` 已交換位置",
|
||||
"moved": "已將 `{0}` 移動到 `{1}`"
|
||||
},
|
||||
"view": {
|
||||
"historyTitle": "播放歷史:",
|
||||
"title": "音樂隊列",
|
||||
"desc": "**正在播放: [點我]({0}) ⮯**\n{1}"
|
||||
},
|
||||
"errors": {
|
||||
"queueFull": "抱歉,隊列已達最大 `{0}` 首歌曲!",
|
||||
"outOfList": "請輸入有效的曲目序號!",
|
||||
"duplicateTrack": "抱歉,此曲目已在隊列中。"
|
||||
}
|
||||
},
|
||||
"songRequest": {
|
||||
"channelCreated": "已建立點歌頻道 ({0})!您可以在該頻道輸入歌曲名稱或連結來點歌,無需使用機器人前綴。"
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "上一頁",
|
||||
"next": "下一頁",
|
||||
"play": "播放",
|
||||
"share": "分享",
|
||||
"export": "匯出",
|
||||
"delete": "刪除",
|
||||
"page": {
|
||||
"title": "輸入頁碼",
|
||||
"field": "頁碼"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM eclipse-temurin:24-jre-noble
|
||||
|
||||
RUN groupadd -g 322 lavalink && \
|
||||
useradd -u 322 -g lavalink -m -d /opt/Lavalink lavalink
|
||||
|
||||
WORKDIR /opt/Lavalink
|
||||
|
||||
ADD --chown=lavalink:lavalink https://github.com/lavalink-devs/Lavalink/releases/latest/download/Lavalink.jar .
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends netcat-openbsd && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
USER lavalink
|
||||
|
||||
ENTRYPOINT ["java", "-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2", "-jar", "Lavalink.jar"]
|
||||
@@ -1,197 +0,0 @@
|
||||
server: # REST and WS server
|
||||
port: 2333
|
||||
address: 0.0.0.0
|
||||
http2:
|
||||
enabled: true # Whether to enable HTTP/2 support
|
||||
plugins:
|
||||
youtube:
|
||||
enabled: true # Whether this source can be used.
|
||||
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used.
|
||||
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
|
||||
allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded.
|
||||
# The clients to use for track loading. See below for a list of valid clients.
|
||||
# Clients are queried in the order they are given (so the first client is queried first and so on...)
|
||||
clients:
|
||||
- MUSIC
|
||||
- ANDROID_VR
|
||||
- ANDROID_MUSIC
|
||||
- WEB
|
||||
- WEBEMBEDDED
|
||||
- TVHTML5EMBEDDED
|
||||
# The below section of the config allows setting specific options for each client, such as the requests they will handle.
|
||||
# If an option, or client, is unspecified, then the default option value/client values will be used instead.
|
||||
# If a client is configured, but is not registered above, the options for that client will be ignored.
|
||||
# WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT
|
||||
# WARNING!: UNDERSTANDING WHAT IT DOES. MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY.
|
||||
|
||||
# Write the names of clients as they are specified under the heading "Available Clients".
|
||||
clientOptions:
|
||||
WEB:
|
||||
# Example: Disabling a client's playback capabilities.
|
||||
playback: false
|
||||
videoLoading: false # Disables loading of videos for this client. A client may still be used for playback even if this is set to 'false'.
|
||||
WEBEMBEDDED:
|
||||
# Example: Configuring a client to exclusively be used for video loading and playback.
|
||||
playlistLoading: false # Disables loading of playlists and mixes.
|
||||
searching: false # Disables the ability to search for videos.
|
||||
lavasrc:
|
||||
providers: # Custom providers for track loading. This is the default
|
||||
# - "dzisrc:%ISRC%" # Deezer ISRC provider
|
||||
# - "dzsearch:%QUERY%" # Deezer search provider
|
||||
- "ytsearch:\"%ISRC%\"" # Will be ignored if track does not have an ISRC. See https://en.wikipedia.org/wiki/International_Standard_Recording_Code
|
||||
- "ytsearch:%QUERY%" # Will be used if track has no ISRC or no track could be found for the ISRC
|
||||
# you can add multiple other fallback sources here
|
||||
sources:
|
||||
spotify: true # Enable Spotify source
|
||||
applemusic: false # Enable Apple Music source
|
||||
deezer: false # Enable Deezer source
|
||||
yandexmusic: false # Enable Yandex Music source
|
||||
flowerytts: false # Enable Flowery TTS source
|
||||
youtube: false # Enable YouTube search source (https://github.com/topi314/LavaSearch)
|
||||
vkmusic: false # Enable Vk Music source
|
||||
lyrics-sources:
|
||||
spotify: false # Enable Spotify lyrics source
|
||||
deezer: false # Enable Deezer lyrics source
|
||||
youtube: false # Enable YouTube lyrics source
|
||||
yandexmusic: false # Enable Yandex Music lyrics source
|
||||
vkmusic: false # Enable Vk Music lyrics source
|
||||
spotify:
|
||||
clientId: ""
|
||||
clientSecret: ""
|
||||
# spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
|
||||
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
playlistLoadLimit: 6 # The number of pages at 100 tracks each
|
||||
albumLoadLimit: 6 # The number of pages at 50 tracks each
|
||||
resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow)
|
||||
localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"`
|
||||
preferAnonymousToken: true # Whether to use the anonymous token for resolving tracks, artists and albums. Spotify generated playlists are always resolved with the anonymous tokens since they do not work otherwise. This requires the customTokenEndpoint to be set.
|
||||
customTokenEndpoint: "http://spotify-tokener:49152/api/token" # Optional custom endpoint for getting the anonymous token. If not set, spotify's default endpoint will be used which might not work. The response must match spotify's anonymous token response format.
|
||||
applemusic:
|
||||
countryCode: "US" # the country code you want to use for filtering the artists top tracks and language. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
mediaAPIToken: "your apple music api token" # apple music api token
|
||||
# or specify an apple music key
|
||||
keyID: "your key id"
|
||||
teamID: "your team id"
|
||||
musicKitKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
your key
|
||||
-----END PRIVATE KEY-----
|
||||
playlistLoadLimit: 6 # The number of pages at 300 tracks each
|
||||
albumLoadLimit: 6 # The number of pages at 300 tracks each
|
||||
deezer:
|
||||
masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else)
|
||||
# arl: "your deezer arl" # the arl cookie used for accessing the deezer api this is optional but required for formats above MP3_128
|
||||
formats: [ "FLAC", "MP3_320", "MP3_256", "MP3_128", "MP3_64", "AAC_64" ] # the formats you want to use for the deezer tracks. "FLAC", "MP3_320", "MP3_256" & "AAC_64" are only available for premium users and require a valid arl
|
||||
yandexmusic:
|
||||
accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music
|
||||
playlistLoadLimit: 1 # The number of pages at 100 tracks each
|
||||
albumLoadLimit: 1 # The number of pages at 50 tracks each
|
||||
artistLoadLimit: 1 # The number of pages at 10 tracks each
|
||||
flowerytts:
|
||||
voice: "default voice" # (case-sensitive) get default voice from here https://api.flowery.pw/v1/tts/voices
|
||||
translate: false # whether to translate the text to the native language of voice
|
||||
silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0.
|
||||
speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.)
|
||||
audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3
|
||||
youtube:
|
||||
countryCode: "US" # the country code you want to use for searching lyrics via ISRC. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
vkmusic:
|
||||
userToken: "your user token" # This token is needed for authorization in the api. Guide: https://github.com/topi314/LavaSrc#vk-music
|
||||
playlistLoadLimit: 1 # The number of pages at 50 tracks each
|
||||
artistLoadLimit: 1 # The number of pages at 10 tracks each
|
||||
recommendationsLoadLimit: 10 # Number of tracks
|
||||
lavalink:
|
||||
plugins:
|
||||
- dependency: "dev.lavalink.youtube:youtube-plugin:1.13.5" # Please check the latest version at https://github.com/lavalink-devs/youtube-source/releases
|
||||
snapshot: false
|
||||
- dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.7.3" # Please check the latest version at https://github.com/topi314/LavaSrc/releases
|
||||
snapshot: false
|
||||
# - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin
|
||||
# repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default
|
||||
# snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository
|
||||
# pluginsDir: "./plugins" # optional, defaults to "./plugins"
|
||||
# defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository
|
||||
# defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository
|
||||
server:
|
||||
password: "youshallnotpass"
|
||||
sources:
|
||||
# The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead.
|
||||
youtube: false
|
||||
bandcamp: true
|
||||
soundcloud: true
|
||||
twitch: true
|
||||
vimeo: true
|
||||
nico: true
|
||||
http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address.
|
||||
local: false
|
||||
filters: # All filters are enabled by default
|
||||
volume: true
|
||||
equalizer: true
|
||||
karaoke: true
|
||||
timescale: true
|
||||
tremolo: true
|
||||
vibrato: true
|
||||
distortion: true
|
||||
rotation: true
|
||||
channelMix: true
|
||||
lowPass: true
|
||||
nonAllocatingFrameBuffer: false # Setting to true reduces the number of allocations made by each player at the expense of frame rebuilding (e.g. non-instantaneous volume changes)
|
||||
bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses.
|
||||
frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered
|
||||
opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
|
||||
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
|
||||
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
|
||||
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
|
||||
youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
|
||||
playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds
|
||||
youtubeSearchEnabled: true
|
||||
soundcloudSearchEnabled: true
|
||||
gc-warnings: true
|
||||
#ratelimit:
|
||||
#ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
|
||||
#excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
|
||||
#strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
|
||||
#searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
|
||||
#retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
|
||||
#youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without.
|
||||
#email: "" # Email of Google account
|
||||
#password: "" # Password of Google account
|
||||
#httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
|
||||
#proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
|
||||
#proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
|
||||
#proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
|
||||
#proxyPassword: "" # Password for basic authentication
|
||||
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
endpoint: /metrics
|
||||
|
||||
sentry:
|
||||
dsn: ""
|
||||
environment: ""
|
||||
# tags:
|
||||
# some_key: some_value
|
||||
# another_key: another_value
|
||||
|
||||
logging:
|
||||
file:
|
||||
path: ./logs/
|
||||
|
||||
level:
|
||||
root: INFO
|
||||
lavalink: INFO
|
||||
|
||||
request:
|
||||
enabled: true
|
||||
includeClientInfo: true
|
||||
includeHeaders: false
|
||||
includeQueryString: true
|
||||
includePayload: true
|
||||
maxPayloadLength: 10000
|
||||
|
||||
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 1GB
|
||||
max-history: 30
|
||||
@@ -4,7 +4,8 @@
|
||||
"channel": "canal",
|
||||
"Provide a channel to connect.": "Proporciona un canal para conectar.",
|
||||
"play": "reproducir",
|
||||
"Loads your input and added it to the queue.": "Carga tu entrada y la agrega a la cola.",
|
||||
"Loads your input into the queue.": "Carga tu entrada en la cola.",
|
||||
"Searches your query and displays the results.": "Busca tu consulta y muestra los resultados.",
|
||||
"query": "consulta",
|
||||
"Input a query or a searchable link.": "Ingresa una consulta o un enlace de búsqueda.",
|
||||
"search": "buscar",
|
||||
@@ -92,7 +93,7 @@
|
||||
"title": "titulo",
|
||||
"Searches for your query and displays the reutned lyrics.": "Busca tu consulta y muestra la letra encontrada.",
|
||||
"artist": "artista",
|
||||
"swapdj": "cambiar_dj",
|
||||
"swapdj": "cambiardj",
|
||||
"Transfer dj to another.": "Transfiere el DJ a otro.",
|
||||
"Choose a member to transfer the dj role.": "Elige un miembro para transferir el rol de DJ.",
|
||||
"autoplay": "reproduccion_auto",
|
||||
@@ -108,7 +109,7 @@
|
||||
"value": "valor",
|
||||
"Play the specific track from your custom playlist.": "Reproduce la pista específica de tu lista de reproducción personalizada.",
|
||||
"view": "ver",
|
||||
"List all your playlist and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
|
||||
"List all your playlists and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
|
||||
"create": "crear",
|
||||
"Create your custom playlist.": "Crea tu lista de reproducción personalizada.",
|
||||
"Give a name to your playlist.": "Dale un nombre a tu lista de reproducción.",
|
||||
@@ -231,5 +232,8 @@
|
||||
"stageannounce": "anuncio_escenario",
|
||||
"Soundcloud": "Soundcloud",
|
||||
"Fairqueue":"ColaJusta",
|
||||
"togglecontroller":"cambiarcontrolador"
|
||||
"togglecontroller":"cambiarcontrolador",
|
||||
"reset": "reiniciar",
|
||||
"Which setting to restore to defaults": "Qué configuración restaurar a los valores por defecto.",
|
||||
"Remove one guild setting override so the bot uses its default again.": "Restablece una opción del servidor a su valor predeterminado."
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"channel": "canal",
|
||||
"Provide a channel to connect.": "Proporciona un canal para conectar.",
|
||||
"play": "reproducir",
|
||||
"Loads your input and added it to the queue.": "Carga tu entrada y la añade a la cola.",
|
||||
"Loads your input into the queue.": "Carga tu entrada en la cola.",
|
||||
"Searches your query and displays the results.": "Busca tu consulta y muestra los resultados.",
|
||||
"query": "consulta",
|
||||
"Input a query or a searchable link.": "Introduce una consulta o un enlace de búsqueda.",
|
||||
"search": "buscar",
|
||||
@@ -92,7 +93,7 @@
|
||||
"title": "titulo",
|
||||
"Searches for your query and displays the reutned lyrics.": "Busca tu consulta y muestra la letra encontrada.",
|
||||
"artist": "artista",
|
||||
"swapdj": "cambiar_dj",
|
||||
"swapdj": "cambiardj",
|
||||
"Transfer dj to another.": "Transfiere el DJ a otro.",
|
||||
"Choose a member to transfer the dj role.": "Elige un miembro para transferir el rol de DJ.",
|
||||
"autoplay": "reproduccion_auto",
|
||||
@@ -108,7 +109,7 @@
|
||||
"value": "valor",
|
||||
"Play the specific track from your custom playlist.": "Reproduce la pista específica de tu lista de reproducción personalizada.",
|
||||
"view": "ver",
|
||||
"List all your playlist and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
|
||||
"List all your playlists and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
|
||||
"create": "crear",
|
||||
"Create your custom playlist.": "Crea tu lista de reproducción personalizada.",
|
||||
"Give a name to your playlist.": "Dale un nombre a tu lista de reproducción.",
|
||||
@@ -231,5 +232,8 @@
|
||||
"stageannounce": "anuncio_escenario",
|
||||
"Soundcloud": "Soundcloud",
|
||||
"Fairqueue":"ColaJusta",
|
||||
"togglecontroller":"cambiarcontrolador"
|
||||
"togglecontroller":"cambiarcontrolador",
|
||||
"reset": "reiniciar",
|
||||
"Which setting to restore to defaults": "Qué configuración restaurar a los valores por defecto.",
|
||||
"Remove one guild setting override so the bot uses its default again.": "Restablece una opción del servidor a su valor predeterminado."
|
||||
}
|
||||
|
||||
239
local_langs/zh-CN.json
Normal file
239
local_langs/zh-CN.json
Normal file
@@ -0,0 +1,239 @@
|
||||
{
|
||||
"connect": "连接",
|
||||
"Connect to a voice channel.": "连接到语音频道。",
|
||||
"channel": "频道",
|
||||
"Provide a channel to connect.": "提供要连接的频道。",
|
||||
"play": "播放",
|
||||
"Loads your input into the queue.": "将您的输入加载到队列中。",
|
||||
"Searches your query and displays the results.": "搜索您的查询并显示结果。",
|
||||
"query": "查询",
|
||||
"Input a query or a searchable link.": "输入查询或可搜索的链接。",
|
||||
"search": "搜索",
|
||||
"Input the name of the song.": "输入歌曲名称。",
|
||||
"platform": "平台",
|
||||
"Select the platform you want to search.": "选择您要搜索的平台。",
|
||||
"Youtube": "YouTube",
|
||||
"Youtube Music": "YouTube 音乐",
|
||||
"Spotify": "Spotify",
|
||||
"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": "继续",
|
||||
"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。",
|
||||
"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": "队列",
|
||||
"clear": "清空",
|
||||
"Remove all the tracks in your queue or history queue.": "清空队列或历史队列中的所有歌曲。",
|
||||
"Choose a queue that you want to clear.": "选择要清空的队列。",
|
||||
"History": "历史",
|
||||
"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.": "设置要删除的队列范围。",
|
||||
"member": "成员",
|
||||
"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 an 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 an 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": "分类",
|
||||
"Test if the bot is alive, and see the delay between your commands and my response.": "测试机器人是否运行,并查看您的命令与我的响应之间的延迟。",
|
||||
"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 playlists 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": "分享",
|
||||
"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": "重命名",
|
||||
"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": "公平队列",
|
||||
"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.": "切换以防止重复歌曲加入队列。",
|
||||
"customcontroller": "自定义控制器",
|
||||
"Customizes music controller embeds.": "自定义音乐控制器嵌入。",
|
||||
"controllermsg": "控制器消息",
|
||||
"silentmsg": "静默消息",
|
||||
"Toggles to send a message when clicking the button in the music controller.": "切换在点击音乐控制器按钮时是否发送消息。",
|
||||
"Toggle silent messaging to send discreet messages without alerting recipients.": "切换静默消息以私密发送,不提醒接收者。",
|
||||
"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.": "删除指定音效。",
|
||||
"start": "开始",
|
||||
"end": "结束",
|
||||
"Specify a time you would like to start, e.g. 1:00": "指定开始时间,例如:1:00。",
|
||||
"Specify a time you would like to end, e.g. 4:00": "指定结束时间,例如:4:00。",
|
||||
"list": "列表",
|
||||
"Customize the channel topic template": "自定义频道主题模板",
|
||||
"template": "模板",
|
||||
"setupchannel": "设置频道",
|
||||
"Sets up a dedicated channel for song requests in your server.": "在服务器中设置专用点歌频道。",
|
||||
"Provide a request channel. If not, a text channel will be generated.": "提供点歌频道。如果没有,将生成文本频道。",
|
||||
"ping": "ping",
|
||||
"…": "...",
|
||||
"8d": "8d",
|
||||
"dj": "dj",
|
||||
"247": "247",
|
||||
"stageannounce": "舞台公告",
|
||||
"Soundcloud": "SoundCloud",
|
||||
"Fairqueue": "公平队列",
|
||||
"togglecontroller": "切换控制器",
|
||||
"reset": "重置",
|
||||
"Which setting to restore to defaults": "要重置为默认值的设置。",
|
||||
"Remove one guild setting override so the bot uses its default again.": "移除一个服务器设置覆盖,使机器人使用其默认值。"
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"channel": "頻道",
|
||||
"Provide a channel to connect.": "提供要連線的頻道。",
|
||||
"play": "播放",
|
||||
"Loads your input and added it to the queue.": "載入您的輸入並將其加入排隊。",
|
||||
"Loads your input into the queue.": "將您的輸入加載到隊列中。",
|
||||
"Searches your query and displays the results.": "搜尋您的查詢並顯示結果。",
|
||||
"query": "查詢",
|
||||
"Input a query or a searchable link.": "輸入查詢或可搜尋的連結。",
|
||||
"search": "搜尋",
|
||||
@@ -108,7 +109,7 @@
|
||||
"value": "值",
|
||||
"Play the specific track from your custom playlist.": "播放您自定義播放列表中的指定歌曲。",
|
||||
"view": "查看",
|
||||
"List all your playlist and all songs in your favourite playlist.": "列出您所有的播放列表和您最愛的播放列表中的所有歌曲。",
|
||||
"List all your playlists and all songs in your favourite playlist.": "列出您所有的播放列表和您最愛的播放列表中的所有歌曲。",
|
||||
"create": "創建",
|
||||
"Create your custom playlist.": "創建您自定義的播放列表。",
|
||||
"Give a name to your playlist.": "給您的播放列表命名。",
|
||||
@@ -231,5 +232,8 @@
|
||||
"stageannounce": "舞台公告",
|
||||
"Soundcloud": "Soundcloud",
|
||||
"Fairqueue": "公平隊列",
|
||||
"togglecontroller": "切換控制器"
|
||||
"togglecontroller": "切換控制器",
|
||||
"reset": "重置",
|
||||
"Which setting to restore to defaults": "要重置為預設值的設定。",
|
||||
"Remove one guild setting override so the bot uses its default again.": "移除一個伺服器設定覆蓋,使機器人使用其預設值。"
|
||||
}
|
||||
134
main.py
134
main.py
@@ -27,42 +27,48 @@ import os
|
||||
import aiohttp
|
||||
import update
|
||||
import logging
|
||||
import voicelink
|
||||
import function as func
|
||||
|
||||
from discord.ext import commands
|
||||
from ipc import IPCClient
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from addons import Settings
|
||||
from voicelink import Config, LangHandler, MongoDBHandler, IPCClient, VoicelinkException
|
||||
from voicelink.utils import dispatch_message
|
||||
|
||||
class Translator(discord.app_commands.Translator):
|
||||
MISSING_TRANSLATOR: dict[str, list[str]] = {}
|
||||
|
||||
async def load(self):
|
||||
func.logger.info("Loaded Translator")
|
||||
|
||||
|
||||
async def unload(self):
|
||||
func.logger.info("Unload Translator")
|
||||
|
||||
async def translate(
|
||||
self,
|
||||
string: discord.app_commands.locale_str,
|
||||
locale: discord.Locale,
|
||||
context: discord.app_commands.TranslationContext
|
||||
) -> str | None:
|
||||
locale_key = str(locale).upper()
|
||||
local_translations = LangHandler._local_langs.get(locale_key)
|
||||
if not local_translations:
|
||||
return None
|
||||
|
||||
async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext):
|
||||
locale_key = str(locale)
|
||||
|
||||
if locale_key in func.LOCAL_LANGS:
|
||||
translated_text = func.LOCAL_LANGS[locale_key].get(string.message)
|
||||
|
||||
if translated_text is None:
|
||||
missing_translations = func.MISSING_TRANSLATOR.setdefault(locale_key, [])
|
||||
if string.message not in missing_translations:
|
||||
missing_translations.append(string.message)
|
||||
|
||||
translated_text = local_translations.get(string.message)
|
||||
if translated_text is not None:
|
||||
return translated_text
|
||||
|
||||
missing = self.MISSING_TRANSLATOR.setdefault(locale_key, [])
|
||||
if string.message not in missing:
|
||||
missing.append(string.message)
|
||||
|
||||
return None
|
||||
|
||||
class Vocard(commands.Bot):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.ipc: IPCClient
|
||||
self.ipc_client: IPCClient
|
||||
|
||||
async def on_message(self, message: discord.Message, /) -> None:
|
||||
# Ignore messages from bots or DMs
|
||||
@@ -77,48 +83,31 @@ class Vocard(commands.Bot):
|
||||
return await message.channel.send(f"My prefix is `{prefix}`")
|
||||
|
||||
# Fetch guild settings and check if the mesage is in the music request channel
|
||||
settings = await func.get_settings(message.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(message.guild.id)
|
||||
if settings and (request_channel := settings.get("music_request_channel")):
|
||||
if message.channel.id == request_channel.get("text_channel_id"):
|
||||
ctx = await self.get_context(message)
|
||||
ctx = await self.get_context(message)
|
||||
try:
|
||||
cmd = self.get_command("play")
|
||||
if message.content:
|
||||
await cmd(ctx, query=message.content)
|
||||
if not ctx.prefix:
|
||||
cmd = self.get_command("play")
|
||||
if message.content:
|
||||
await cmd(ctx, query=message.content)
|
||||
|
||||
elif message.attachments:
|
||||
for attachment in message.attachments:
|
||||
await cmd(ctx, query=attachment.url)
|
||||
elif message.attachments:
|
||||
for attachment in message.attachments:
|
||||
await cmd(ctx, query=attachment.url)
|
||||
|
||||
except Exception as e:
|
||||
await func.send(ctx, str(e), ephemeral=True)
|
||||
await dispatch_message(ctx, str(e), ephemeral=True)
|
||||
|
||||
finally:
|
||||
return await message.delete()
|
||||
await message.delete()
|
||||
|
||||
await self.process_commands(message)
|
||||
|
||||
async def connect_db(self) -> None:
|
||||
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()
|
||||
func.logger.info(f"Successfully connected to [{db_name}] MongoDB!")
|
||||
|
||||
except Exception as 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"]
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
func.langs_setup()
|
||||
|
||||
# Connecting to MongoDB
|
||||
await self.connect_db()
|
||||
await MongoDBHandler.init(bot_config.mongodb_url, bot_config.mongodb_name)
|
||||
|
||||
# Set translator
|
||||
await self.tree.set_translator(Translator())
|
||||
@@ -132,32 +121,34 @@ class Vocard(commands.Bot):
|
||||
except Exception as e:
|
||||
func.logger.error(f"Something went wrong while loading {module[:-3]} cog.", exc_info=e)
|
||||
|
||||
self.ipc = IPCClient(self, **func.settings.ipc_client)
|
||||
if func.settings.ipc_client.get("enable", False):
|
||||
self.ipc_client: IPCClient = IPCClient(self, **bot_config.ipc_client)
|
||||
if bot_config.ipc_client.get("enable", False):
|
||||
try:
|
||||
await self.ipc.connect()
|
||||
await self.ipc_client.connect()
|
||||
except Exception as e:
|
||||
func.logger.error(f"Cannot connected to dashboard! - Reason: {e}")
|
||||
|
||||
# Update version tracking
|
||||
if not func.settings.version or func.settings.version != update.__version__:
|
||||
if not bot_config.version or bot_config.version != update.__version__:
|
||||
await self.tree.sync()
|
||||
func.update_json("settings.json", new_data={"version": update.__version__})
|
||||
for locale_key, values in func.MISSING_TRANSLATOR.items():
|
||||
|
||||
for locale_key, values in self.tree.translator.MISSING_TRANSLATOR.items():
|
||||
func.logger.warning(f'Missing translation for "{", ".join(values)}" in "{locale_key}"')
|
||||
self.tree.translator.MISSING_TRANSLATOR.clear()
|
||||
|
||||
async def on_ready(self):
|
||||
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"Vocard Version: {update.__version__}")
|
||||
func.logger.info(f"Discord Version: {discord.__version__}")
|
||||
func.logger.info(f"Python Version: {sys.version}")
|
||||
func.logger.info("------------------")
|
||||
|
||||
func.settings.client_id = self.user.id
|
||||
func.LOCAL_LANGS.clear()
|
||||
func.MISSING_TRANSLATOR.clear()
|
||||
bot_config.client_id = self.user.id
|
||||
LangHandler._local_langs.clear()
|
||||
|
||||
async def on_command_error(self, ctx: commands.Context, exception, /) -> None:
|
||||
error = getattr(exception, 'original', exception)
|
||||
@@ -178,12 +169,12 @@ class Vocard(commands.Bot):
|
||||
description += f"**Aliases:**\n`{', '.join([f'{ctx.prefix}{alias}' for alias in ctx.command.aliases])}`\n\n"
|
||||
description += f"**Description:**\n{ctx.command.help}\n\u200b"
|
||||
|
||||
embed = discord.Embed(description=description, color=func.settings.embed_color)
|
||||
embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {func.settings.invite_link}")
|
||||
embed = discord.Embed(description=description, color=bot_config.embed_color)
|
||||
embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {bot_config.invite_link}")
|
||||
return await ctx.reply(embed=embed)
|
||||
|
||||
elif not issubclass(error.__class__, voicelink.VoicelinkException):
|
||||
error = await func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
|
||||
elif not issubclass(error.__class__, VoicelinkException):
|
||||
error = await Lang_handler.get_lang(ctx.guild.id, "common.errors.unknown") + bot_config.invite_link
|
||||
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:
|
||||
@@ -200,31 +191,27 @@ class CommandCheck(discord.app_commands.CommandTree):
|
||||
|
||||
channel_perm = interaction.channel.permissions_for(interaction.guild.me)
|
||||
if not channel_perm.read_messages or not channel_perm.send_messages:
|
||||
await interaction.response.send_message("I don't have permission to read or send messages in this channel.")
|
||||
await interaction.response.send_message("I don't have permission to read or send messages in this channel.", ephemeral=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def get_prefix(bot: commands.Bot, message: discord.Message) -> str:
|
||||
settings = await func.get_settings(message.guild.id)
|
||||
prefix = settings.get("prefix", func.settings.bot_prefix)
|
||||
|
||||
# Allow owner to use the bot without a prefix
|
||||
if prefix and not message.content.startswith(prefix) and (await bot.is_owner(message.author) or message.author.id in func.settings.bot_access_user):
|
||||
return ""
|
||||
|
||||
return prefix
|
||||
settings = await MongoDBHandler.get_settings(message.guild.id)
|
||||
prefix = settings.get("prefix", bot_config.bot_prefix)
|
||||
return prefix if prefix is not None else ""
|
||||
|
||||
# Loading settings and logger
|
||||
func.settings = Settings(func.open_json("settings.json"))
|
||||
bot_config = Config(func.open_json("settings.json"))
|
||||
Lang_handler = LangHandler.init()
|
||||
|
||||
LOG_SETTINGS = func.settings.logging
|
||||
LOG_SETTINGS = bot_config.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 = 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='{'))
|
||||
logging.getLogger().addHandler(file_handler)
|
||||
@@ -235,9 +222,10 @@ for log_name, log_level in LOG_SETTINGS.get("level", {}).items():
|
||||
|
||||
# Setup the bot object
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = False if func.settings.bot_prefix is None else True
|
||||
intents.members = func.settings.ipc_client.get("enable", False)
|
||||
intents.message_content = False if bot_config.bot_prefix is None else True
|
||||
intents.members = bot_config.ipc_client.get("enable", False)
|
||||
intents.voice_states = True
|
||||
intents.presences = False
|
||||
|
||||
bot = Vocard(
|
||||
command_prefix=get_prefix,
|
||||
@@ -251,4 +239,4 @@ bot = Vocard(
|
||||
|
||||
if __name__ == "__main__":
|
||||
update.check_version(with_msg=True)
|
||||
bot.run(func.settings.token, root_logger=True)
|
||||
bot.run(bot_config.token, root_logger=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
discord.py==2.5.2
|
||||
discord.py==2.7.1
|
||||
motor==3.6.0
|
||||
dnspython==2.2.1
|
||||
tldextract==3.2.1
|
||||
|
||||
@@ -35,11 +35,12 @@
|
||||
"vocard": "INFO",
|
||||
"ipc_client": "INFO"
|
||||
},
|
||||
"max-history": 30
|
||||
"max_history": 30
|
||||
},
|
||||
"bot_access_user": [],
|
||||
"embed_color":"0xb3b3b3",
|
||||
"default_max_queue": 1000,
|
||||
"default_search_platform": "youtube",
|
||||
"lyrics_platform": "lrclib",
|
||||
"ipc_client": {
|
||||
"host": "127.0.0.1",
|
||||
@@ -48,6 +49,16 @@
|
||||
"secure": false,
|
||||
"enable": false
|
||||
},
|
||||
"timer_settings": {
|
||||
"bot_activity_update": 600,
|
||||
"inactive_player_cleanup": 600,
|
||||
"cache_cleanup": 43200
|
||||
},
|
||||
"playlist_settings": {
|
||||
"max_playlists": 5,
|
||||
"max_tracks_per_playlist": 500,
|
||||
"default_playlist_name": "Favourite"
|
||||
},
|
||||
"sources_settings": {
|
||||
"youtube": {
|
||||
"emoji": "<:youtube:826661982760992778>",
|
||||
@@ -121,27 +132,27 @@
|
||||
{
|
||||
"back": {
|
||||
"emoji": "⏮️",
|
||||
"label": "@@t_buttonBack@@"
|
||||
"label": "@@t_player.buttons.back@@"
|
||||
},
|
||||
"play-pause": {
|
||||
"states": {
|
||||
"pause": {
|
||||
"emoji": "⏸️",
|
||||
"label": "@@t_buttonPause@@"
|
||||
"label": "@@t_player.buttons.pause@@"
|
||||
},
|
||||
"resume": {
|
||||
"emoji": "▶️",
|
||||
"label": "@@t_buttonResume@@"
|
||||
"label": "@@t_player.buttons.resume@@"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skip": {
|
||||
"emoji": "⏭️",
|
||||
"label": "@@t_buttonSkip@@"
|
||||
"label": "@@t_player.buttons.skip@@"
|
||||
},
|
||||
"stop": {
|
||||
"emoji": "⏹️",
|
||||
"label": "@@t_buttonLeave@@",
|
||||
"label": "@@t_player.buttons.leave@@",
|
||||
"style": "red"
|
||||
},
|
||||
"add-fav": {
|
||||
@@ -151,7 +162,7 @@
|
||||
},
|
||||
{
|
||||
"tracks": {
|
||||
"label": "@@t_playerDropdown@@",
|
||||
"label": "@@t_player.dropdown.trackSelect@@",
|
||||
"max_options": 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import subprocess
|
||||
from io import BytesIO
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
__version__ = "v2.7.2"
|
||||
__version__ = "v2.7.3"
|
||||
|
||||
# URLs for update and migration
|
||||
PYTHON_CMD_NAME = os.path.basename(sys.executable)
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord, copy
|
||||
import function as func
|
||||
|
||||
from typing import List
|
||||
from discord.ext import commands
|
||||
|
||||
class Modal(discord.ui.Modal):
|
||||
def __init__(self, items: List[discord.ui.Item], *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
for item in items:
|
||||
self.add_item(item)
|
||||
|
||||
self.values: dict = {}
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
for children in self.children:
|
||||
self.values[children.label.lower()] = children.value
|
||||
|
||||
self.stop()
|
||||
|
||||
class Dropdown(discord.ui.Select):
|
||||
def __init__(self):
|
||||
options = [
|
||||
discord.SelectOption(label='Active', description='The controller embed when music is playing', emoji='🟩'),
|
||||
discord.SelectOption(label='Inactive', description='The controller embed when music is not playing', emoji='🟥'),
|
||||
]
|
||||
super().__init__(placeholder='Select a embed to edit...', min_values=1, max_values=1, options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
self.view.embedType = self.values[0].lower()
|
||||
if self.view.embedType not in self.view.data:
|
||||
self.view.data[self.view.embedType] = {}
|
||||
|
||||
await interaction.response.edit_message(embed=self.view.build_embed())
|
||||
|
||||
class EmbedBuilderView(discord.ui.View):
|
||||
def __init__(self, context: commands.Context, data: dict) -> None:
|
||||
from voicelink import Placeholders, build_embed
|
||||
|
||||
super().__init__(timeout=300)
|
||||
self.add_item(Dropdown())
|
||||
|
||||
self.author: discord.Member = context.author
|
||||
self.response: discord.Message = None
|
||||
|
||||
self.original_data: dict = copy.deepcopy(data)
|
||||
self.data: dict = copy.deepcopy(data)
|
||||
self.embedType: str = "active"
|
||||
|
||||
self.ph: Placeholders = Placeholders(context.bot)
|
||||
self.build_embed = lambda: build_embed(self.data.get(self.embedType, {}), self.ph)
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction):
|
||||
return interaction.user == self.author
|
||||
|
||||
@discord.ui.button(label="Edit Content", style=discord.ButtonStyle.blurple)
|
||||
async def edit_content(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embedType, {})
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Title",
|
||||
placeholder="The title of the embed",
|
||||
style=discord.TextStyle.paragraph,
|
||||
max_length=1000,
|
||||
default=data.get("title", {}).get("name"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Url",
|
||||
placeholder="The url of the title",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=100,
|
||||
default=data.get("title", {}).get("url"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Color",
|
||||
placeholder="The color of the embed",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=100,
|
||||
default=data.get("color"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Description",
|
||||
placeholder="The description of the title",
|
||||
style=discord.TextStyle.paragraph,
|
||||
max_length=200,
|
||||
default=data.get("description"),
|
||||
required=False
|
||||
)
|
||||
]
|
||||
|
||||
modal = Modal(items, title="Edit Content")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
try:
|
||||
data["description"] = v["description"]
|
||||
data["color"] = int(v["color"], 16)
|
||||
|
||||
if "title" not in data:
|
||||
data["title"] = {}
|
||||
|
||||
data["title"]["name"] = v['title']
|
||||
data["title"]["url"] = v['url']
|
||||
except:
|
||||
pass
|
||||
|
||||
return await interaction.edit_original_response(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Edit Author",)
|
||||
async def edit_author(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embedType, {})
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Name",
|
||||
placeholder="The name of the author",
|
||||
style=discord.TextStyle.paragraph,
|
||||
max_length=200,
|
||||
default=data.get("author", {}).get("name"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Url",
|
||||
placeholder="The url of the author",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=100,
|
||||
default=data.get("author", {}).get("url"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Icon Url",
|
||||
placeholder="The icon url of the author",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=100,
|
||||
default=data.get("author", {}).get("icon_url"),
|
||||
required=False
|
||||
),
|
||||
]
|
||||
|
||||
modal = Modal(items, title="Edit Author")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
|
||||
if v['name'] != "":
|
||||
if "author" not in data:
|
||||
data["author"] = {}
|
||||
|
||||
data["author"]["name"] = v['name']
|
||||
data["author"]["url"] = v['url']
|
||||
data["author"]["icon_url"] = v['icon url']
|
||||
else:
|
||||
del data["author"]
|
||||
|
||||
return await interaction.edit_original_response(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Edit Image")
|
||||
async def edit_image(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embedType, {})
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Thumbnail",
|
||||
placeholder="The url of the thumbnail",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=200,
|
||||
default=data.get("thumbnail"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Image",
|
||||
placeholder="The url of the image",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=100,
|
||||
default=data.get("image"),
|
||||
required=False
|
||||
)
|
||||
]
|
||||
|
||||
modal = Modal(items, title="Edit Image")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
|
||||
data["thumbnail"] = v['thumbnail']
|
||||
data["image"] = v['image']
|
||||
|
||||
return await interaction.edit_original_response(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Edit Footer")
|
||||
async def edit_footer(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embedType, {})
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Text",
|
||||
placeholder="The text of the footer",
|
||||
style=discord.TextStyle.paragraph,
|
||||
max_length=200,
|
||||
default=data.get("footer", {}).get("text"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Icon Url",
|
||||
placeholder="The url of the icon",
|
||||
style=discord.TextStyle.short,
|
||||
max_length=100,
|
||||
default=data.get("footer", {}).get("icon_url"),
|
||||
required=False
|
||||
)
|
||||
]
|
||||
|
||||
modal = Modal(items, title="Edit Footer")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
if "footer" not in data:
|
||||
data["footer"] = {}
|
||||
|
||||
data["footer"]["text"] = v['text']
|
||||
data["footer"]["icon_url"] = v['icon url']
|
||||
|
||||
return await interaction.edit_original_response(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Add Field", style=discord.ButtonStyle.green, row=1)
|
||||
async def add_field(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embedType)
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Name",
|
||||
placeholder="The name of the field",
|
||||
style=discord.TextStyle.paragraph,
|
||||
max_length=256
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Value",
|
||||
placeholder="The value of the field",
|
||||
style=discord.TextStyle.long,
|
||||
max_length=1024
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Inline",
|
||||
placeholder="The inline of the field, e.g. True or False",
|
||||
style=discord.TextStyle.short
|
||||
)
|
||||
]
|
||||
|
||||
if "fields" not in data:
|
||||
data["fields"] = []
|
||||
|
||||
if len(data["fields"]) >= 25:
|
||||
return await interaction.response.send_message("You have already reached the maximum of fields!", ephemeral=True)
|
||||
|
||||
modal = Modal(items, title="Add Field")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
data["fields"].append({
|
||||
"name": v["name"],
|
||||
"value": v["value"],
|
||||
"inline": True if v["inline"].lower() == "true" else False
|
||||
})
|
||||
|
||||
return await interaction.edit_original_response(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Remove Field", style=discord.ButtonStyle.red, row=1)
|
||||
async def remove_field(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Index",
|
||||
placeholder="The number of fields to remove, e.g. 1",
|
||||
style=discord.TextStyle.short
|
||||
)
|
||||
]
|
||||
|
||||
data = self.data.get(self.embedType)
|
||||
if "fields" not in data:
|
||||
data["fields"] = []
|
||||
|
||||
if len(data["fields"]) == 0:
|
||||
return await interaction.response.send_message("There are no fields to remove!", ephemeral=True)
|
||||
|
||||
modal = Modal(items, title="Remove Field")
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
try:
|
||||
del data["fields"][int(modal.values["index"])]
|
||||
except:
|
||||
return await interaction.followup.send("Can't found the field", ephemeral=True)
|
||||
|
||||
return await interaction.edit_original_response(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Apply", style=discord.ButtonStyle.green, row=1)
|
||||
async def apply(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await func.update_settings(
|
||||
interaction.guild_id,
|
||||
{"$set": {"default_controller.embeds": self.data}},
|
||||
)
|
||||
|
||||
await self.on_timeout()
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Reset", style=discord.ButtonStyle.red, row=1)
|
||||
async def reset(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
self.data.update(copy.deepcopy(self.original_data))
|
||||
return await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(emoji='🗑️', row=1)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
|
||||
|
||||
120
views/list.py
120
views/list.py
@@ -1,120 +0,0 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import function as func
|
||||
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from voicelink import Player, Track
|
||||
|
||||
class ListView(discord.ui.View):
|
||||
def __init__(
|
||||
self,
|
||||
player: "Player",
|
||||
author: discord.Member,
|
||||
is_queue: bool = True
|
||||
) -> None:
|
||||
super().__init__(timeout=60)
|
||||
self.player: Player = player
|
||||
|
||||
self.is_queue: bool = is_queue
|
||||
self.tracks: list[Track] = player.queue.tracks() if is_queue else player.queue.history()
|
||||
self.response: discord.Message = None
|
||||
|
||||
if not is_queue:
|
||||
self.tracks.reverse()
|
||||
self.author: discord.Member = author
|
||||
|
||||
self.page: int = ceil(len(self.tracks) / 7)
|
||||
self.current_page: int = 1
|
||||
|
||||
try:
|
||||
self.time: str = func.time(sum([track.length for track in self.tracks]))
|
||||
except Exception as _:
|
||||
self.time = "∞"
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_error(self, error, item, interaction) -> None:
|
||||
return
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
return interaction.user == self.author
|
||||
|
||||
async def build_embed(self) -> discord.Embed:
|
||||
offset: int = self.current_page * 7
|
||||
tracks: list[Track] = self.tracks[(offset-7):offset]
|
||||
texts = await func.get_lang(self.author.guild.id, "viewTitle", "viewDesc", "nowplayingDesc", "live", "queueTitle", "historyTitle", "viewFooter")
|
||||
|
||||
embed = discord.Embed(title=texts[0], color=func.settings.embed_color)
|
||||
embed.description=texts[1].format(self.player.current.uri, f"```{self.player.current.title}```") if self.player.current else texts[2].format("None")
|
||||
|
||||
embed.description += "\n**" + (texts[4] if self.is_queue else texts[5]) + "**\n" + "\n".join([
|
||||
f"{track.emoji} `{i:>2}.` `[" + (texts[3] if track.is_stream else func.time(track.length)) + f']` [{func.truncate_string(track.title)}]({track.uri})' + (track.requester.mention)
|
||||
for i, track in enumerate(tracks, start=offset-6)
|
||||
])
|
||||
embed.set_footer(text=texts[6].format(self.current_page, self.page, self.time))
|
||||
|
||||
return embed
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page < self.page:
|
||||
self.current_page += 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page != self.page:
|
||||
self.current_page = self.page
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
110
views/lyrics.py
110
views/lyrics.py
@@ -1,110 +0,0 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import function as func
|
||||
|
||||
class LyricsDropdown(discord.ui.Select):
|
||||
def __init__(self, langs: list[str]) -> None:
|
||||
self.view: LyricsView
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select A Lyrics Translation",
|
||||
min_values=1, max_values=1,
|
||||
options=[discord.SelectOption(label=lang) for lang in langs],
|
||||
custom_id="selectLyricsLangs"
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
self.view.lang = self.values[0]
|
||||
self.view.current_page = 1
|
||||
self.view.pages = len(self.view.source.get(self.values[0]))
|
||||
await interaction.response.edit_message(embed=self.view.build_embed())
|
||||
|
||||
class LyricsView(discord.ui.View):
|
||||
def __init__(self, name: str, source: dict, author: discord.Member) -> None:
|
||||
super().__init__(timeout=60)
|
||||
|
||||
self.name: str = name
|
||||
self.source: dict[str, list[str]] = source
|
||||
self.lang: list[str] = list(source.keys())[0]
|
||||
self.author: discord.Member = author
|
||||
|
||||
self.response: discord.Message = None
|
||||
self.pages: int = len(self.source.get(self.lang))
|
||||
self.current_page: int = 1
|
||||
self.add_item(LyricsDropdown(list(source.keys())))
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
return interaction.user == self.author
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_error(self, error, item, interaction) -> None:
|
||||
return
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
chunk = self.source.get(self.lang)[self.current_page - 1]
|
||||
embed=discord.Embed(description=chunk, color=func.settings.embed_color)
|
||||
embed.set_author(name=f"Searching Query: {self.name}", icon_url=self.author.display_avatar.url)
|
||||
embed.set_footer(text=f"Page: {self.current_page}/{self.pages}")
|
||||
return embed
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
return await interaction.response.edit_message(embed=self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
return await interaction.response.edit_message(embed=self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page < self.pages:
|
||||
self.current_page += 1
|
||||
return await interaction.response.edit_message(embed=self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page != self.pages:
|
||||
self.current_page = self.pages
|
||||
return await interaction.response.edit_message(embed=self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
@@ -1,164 +0,0 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import function as func
|
||||
|
||||
from math import ceil
|
||||
from tldextract import extract
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from voicelink import Track
|
||||
|
||||
class Select_playlist(discord.ui.Select):
|
||||
def __init__(self, results):
|
||||
self.view: PlaylistView
|
||||
|
||||
super().__init__(
|
||||
placeholder="Select a playlist to view ..",
|
||||
custom_id="selector",
|
||||
options=[discord.SelectOption(emoji='🌎', label='All Playlist')] +
|
||||
[
|
||||
discord.SelectOption(emoji=playlist['emoji'], label=f'{index}. {playlist["name"]}', description=f"{playlist['time']} · {playlist['type']}")
|
||||
for index, playlist in enumerate(results, start=1) if playlist['type'] != 'error'
|
||||
]
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
if self.values[0] == 'All Playlist':
|
||||
self.view.current = None
|
||||
self.view.toggle_btn(True)
|
||||
return await interaction.response.edit_message(embed=self.view.viewEmbed, view=self.view)
|
||||
|
||||
self.view.current = self.view.results[int(self.values[0].split(". ")[0]) - 1]
|
||||
self.view.page = ceil(len(self.view.current['tracks']) / 7)
|
||||
self.view.current_page = 1
|
||||
self.view.toggle_btn(False)
|
||||
await interaction.response.edit_message(embed=await self.view.build_embed(), view=self.view)
|
||||
|
||||
class PlaylistView(discord.ui.View):
|
||||
def __init__(
|
||||
self,
|
||||
viewEmbed: discord.Embed,
|
||||
results: list[dict[str, Any]],
|
||||
author: discord.Message
|
||||
) -> None:
|
||||
super().__init__(timeout=60)
|
||||
|
||||
self.viewEmbed: discord.Embed = viewEmbed
|
||||
self.results: list[dict[str, Any]] = results
|
||||
self.author: discord.Member = author
|
||||
self.response: discord.Message = None
|
||||
|
||||
self.current: dict[str, Any] = None
|
||||
self.page: int = 0
|
||||
self.current_page: int = 1
|
||||
|
||||
self.add_item(Select_playlist(results))
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
return interaction.user == self.author
|
||||
|
||||
async def on_error(self, error, item, interaction) -> None:
|
||||
return
|
||||
|
||||
def toggle_btn(self, action: bool) -> None:
|
||||
for child in self.children:
|
||||
if child.custom_id not in ("delete", "selector"):
|
||||
child.disabled = action
|
||||
|
||||
async def build_embed(self) -> discord.Embed:
|
||||
offset: int = self.current_page * 7
|
||||
tracks: list[Track] = self.current['tracks'][(offset-7):offset]
|
||||
texts = await func.get_lang(self.author.guild.id, "playlistView", "playlistViewDesc", "settingsPermTitle", "playlistViewPermsValue", "playlistViewPermsValue2", "playlistViewTrack", "playlistNoTrack", "playlistViewPage")
|
||||
|
||||
embed = discord.Embed(title=texts[0], color=func.settings.embed_color)
|
||||
embed.description = texts[1].format(self.current['name'], self.current['id'], len(self.current['tracks']), owner if (owner := self.current.get('owner')) else f"{self.author.id} (You)", self.current['type']) + "\n"
|
||||
|
||||
perms = self.current['perms']
|
||||
if self.current['type'] == 'share':
|
||||
embed.description += texts[2] + "\n" + texts[3].format('✓' if 'write' in perms and self.author.id in perms['write'] else '✘', '✓' if 'remove' in perms and self.author.id in perms['remove'] else '✘')
|
||||
else:
|
||||
embed.description += texts[2] + "\n" + texts[4].format(', '.join(f'<@{user}>' for user in perms['read']))
|
||||
|
||||
embed.description += f"\n\n**{texts[5]}:**\n"
|
||||
if tracks:
|
||||
if self.current.get("type") == "playlist":
|
||||
embed.description += "\n".join(f"{func.get_source(track['sourceName'], 'emoji')} `{index:>2}.` `[{func.time(track['length'])}]` [{func.truncate_string(track['title'])}]({track['uri']})" for index, track in enumerate(tracks, start=offset - 6))
|
||||
else:
|
||||
embed.description += '\n'.join(f"{func.get_source(extract(track.info['uri']).domain, 'emoji')} `{index:>2}.` `[{func.time(track.length)}]` [{func.truncate_string(track.title)}]({track.uri})" for index, track in enumerate(tracks, start=offset - 6))
|
||||
else:
|
||||
embed.description += texts[6].format(self.current['name'])
|
||||
|
||||
embed.set_footer(text=texts[7].format(self.current_page, self.page, self.current['time']))
|
||||
return embed
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey, disabled=True)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple, disabled=True)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple, disabled=True)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page < self.page:
|
||||
self.current_page += 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey, disabled=True)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if not self.current:
|
||||
return
|
||||
if self.current_page != self.page:
|
||||
self.current_page = self.page
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(emoji='🗑️', custom_id="delete", style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
@@ -21,18 +21,22 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
__version__ = "1.4"
|
||||
__version__ = "1.5"
|
||||
__author__ = 'Vocard Development, ChocoMeow'
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2023 - present (c) Vocard Development, ChocoMeow"
|
||||
|
||||
from .enums import SearchType, LoopType
|
||||
from .config import Config
|
||||
from .enums import SearchType, LoopType, TrackRecType
|
||||
from .events import *
|
||||
from .exceptions import *
|
||||
from .filters import *
|
||||
from .objects import *
|
||||
from .player import Player, connect_channel
|
||||
from .pool import *
|
||||
from .queue import *
|
||||
from .placeholders import Placeholders, build_embed
|
||||
from .transformer import encode, decode
|
||||
from .player import Player, connect_channel
|
||||
from .placeholders import PlayerPlaceholder, BotPlaceholder
|
||||
from .mongodb import MongoDBHandler
|
||||
from .language import LangHandler
|
||||
from .lyrics import LYRICS_PLATFORMS
|
||||
from .ipc import IPCClient
|
||||
|
||||
138
voicelink/config.py
Normal file
138
voicelink/config.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from typing import (
|
||||
Dict,
|
||||
List,
|
||||
Any,
|
||||
Union,
|
||||
Optional
|
||||
)
|
||||
|
||||
from .enums import SearchType
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class Config:
|
||||
_instance: Optional['Config'] = None
|
||||
WORKING_DIR: Path = Path(__file__).resolve().parent.parent
|
||||
LAST_SESSION_FILE_DIR: str = WORKING_DIR / "last-session.json"
|
||||
|
||||
def __new__(cls, settings: Dict[str, Any] = None) -> 'Config':
|
||||
"""
|
||||
Singleton pattern to ensure only one instance of Config exists.
|
||||
If settings are provided, creates a new instance that replaces the old one.
|
||||
|
||||
Args:
|
||||
settings (Dict[str, Any], optional): A dictionary containing configuration settings. Defaults to None.
|
||||
If provided, creates a new instance that replaces the old one.
|
||||
"""
|
||||
if settings is not None:
|
||||
instance = super(Config, cls).__new__(cls)
|
||||
instance.__init__(settings)
|
||||
cls._instance = instance
|
||||
return instance
|
||||
|
||||
if cls._instance is None:
|
||||
cls._instance = super(Config, cls).__new__(cls)
|
||||
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, settings: Dict[str, Any] = None) -> None:
|
||||
"""
|
||||
Initialize configuration settings.
|
||||
|
||||
Args:
|
||||
settings (Dict[str, Any], optional): A dictionary containing configuration settings.
|
||||
If None, uses empty dict with default values.
|
||||
"""
|
||||
if hasattr(self, 'initialized'):
|
||||
return
|
||||
|
||||
settings = settings or {}
|
||||
|
||||
self.token: str = settings.get("token") or os.getenv("TOKEN")
|
||||
self.client_id: int = int(settings.get("client_id", 0)) or int(os.getenv("CLIENT_ID"))
|
||||
self.genius_token: str = settings.get("genius_token") or os.getenv("GENIUS_TOKEN")
|
||||
self.mongodb_url: str = settings.get("mongodb_url") or os.getenv("MONGODB_URL")
|
||||
self.mongodb_name: str = settings.get("mongodb_name") or os.getenv("MONGODB_NAME")
|
||||
|
||||
self.invite_link: str = "https://discord.gg/wRCgB7vBQv"
|
||||
self.nodes: Dict[str, Dict[str, Union[str, int, bool]]] = settings.get("nodes", {})
|
||||
self.max_queue: int = settings.get("default_max_queue", 1000)
|
||||
self.search_platform: SearchType = SearchType.from_platform(settings.get("default_search_platform", "youtube")) or SearchType.YOUTUBE
|
||||
self.bot_prefix: str = settings.get("prefix", "")
|
||||
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[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_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {})
|
||||
self.playlist_settings: Dict[str, Union[str, int]] = settings.get("playlist_settings", {})
|
||||
self.timer_settings: Dict[str, int] = settings.get("timer_settings", {})
|
||||
self.version: str = settings.get("version", "")
|
||||
|
||||
self.initialized = True
|
||||
|
||||
@classmethod
|
||||
def get_source_config(cls, source: str, type: str) -> Union[str, None]:
|
||||
"""
|
||||
Get source configuration for a specific source and type.
|
||||
|
||||
Args:
|
||||
source (str): The source identifier (e.g., 'youtube', 'spotify').
|
||||
Case-insensitive and spaces are removed.
|
||||
type (str): The type of configuration to retrieve (e.g., 'emoji', 'color').
|
||||
|
||||
Returns:
|
||||
Union[str, None]: The configuration value for the specified source and type.
|
||||
Returns None if either the source or type doesn't exist.
|
||||
|
||||
Example:
|
||||
>>> Config().get_source("youtube", "emoji")
|
||||
"🎵"
|
||||
"""
|
||||
if not isinstance(source, str) or not isinstance(type, str):
|
||||
return None
|
||||
|
||||
normalized_source: str = source.lower().strip().replace(" ", "")
|
||||
source_settings: dict[str, str] = cls._instance.sources_settings.get(
|
||||
normalized_source,
|
||||
cls._instance.sources_settings.get("others", {})
|
||||
)
|
||||
|
||||
return source_settings.get(type)
|
||||
|
||||
@classmethod
|
||||
def get_playlist_config(cls) -> tuple[int, int, str]:
|
||||
config = cls._instance.playlist_settings
|
||||
return config.get("max_playlists", 5), config.get("max_tracks_per_playlist", 500), config.get("default_playlist_name", "Favourite")
|
||||
@@ -37,22 +37,43 @@ class LoopType(Enum):
|
||||
QUEUE = auto()
|
||||
|
||||
class SearchType(Enum):
|
||||
"""The enum for the different search types for Voicelink.
|
||||
"""Enum representing different search types for Voicelink.
|
||||
|
||||
SearchType.YOUTUBE searches using regular Youtube,
|
||||
which is best for all scenarios.
|
||||
Each search type corresponds to a specific platform that can be used
|
||||
for retrieving audio or video content. The options available are:
|
||||
|
||||
SearchType.YOUTUBE_MUSIC searches using YouTube Music,
|
||||
which is best for getting audio-only results.
|
||||
|
||||
SearchType.SPOTIFY searches using Spotify,
|
||||
which is an alternative to YouTube or YouTube Music.
|
||||
|
||||
SearchType.SOUNDCLOUD searches using SoundCloud,
|
||||
which is an alternative to YouTube or YouTube Music.
|
||||
|
||||
SearchType.APPLE_MUSIC searches using Apple Music,
|
||||
which is an alternative to YouTube or YouTube Music.
|
||||
- SearchType.YOUTUBE:
|
||||
Searches using regular YouTube, ideal for all scenarios.
|
||||
|
||||
- SearchType.YOUTUBE_MUSIC:
|
||||
Searches using YouTube Music, best for obtaining audio-only results.
|
||||
|
||||
- SearchType.SPOTIFY:
|
||||
Searches using Spotify, a viable alternative to YouTube and YouTube Music.
|
||||
|
||||
- SearchType.SOUNDCLOUD:
|
||||
Searches using SoundCloud, another alternative to YouTube and YouTube Music.
|
||||
|
||||
- SearchType.APPLE_MUSIC:
|
||||
Searches using Apple Music, offering a different option for audio content.
|
||||
|
||||
- SearchType.DEEZER:
|
||||
Searches using Deezer, providing another music streaming alternative.
|
||||
|
||||
- SearchType.YANDEX_MUSIC:
|
||||
Searches using Yandex Music, catering to users in the Yandex ecosystem.
|
||||
|
||||
- SearchType.VK_MUSIC:
|
||||
Searches using VK Music, popular in certain regions for music streaming.
|
||||
|
||||
- SearchType.TIDAL:
|
||||
Searches using Tidal, known for high-fidelity music streaming.
|
||||
|
||||
- SearchType.QOBUZ:
|
||||
Searches using Qobuz, recognized for high-resolution audio.
|
||||
|
||||
- SearchType.JIOSAAVN:
|
||||
Searches using JioSaavn, a popular platform in India for music streaming.
|
||||
"""
|
||||
|
||||
YOUTUBE = "ytsearch"
|
||||
@@ -60,12 +81,18 @@ class SearchType(Enum):
|
||||
SPOTIFY = "spsearch"
|
||||
SOUNDCLOUD = "scsearch"
|
||||
APPLE_MUSIC = "amsearch"
|
||||
DEEZER = "dzsearch"
|
||||
YANDEX_MUSIC = "ymsearch"
|
||||
VK_MUSIC = "vksearch"
|
||||
TIDAL = "tdsearch"
|
||||
QOBUZ = "qbsearch"
|
||||
JIOSAAVN = "jssearch"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def match(cls, value: str):
|
||||
def from_platform(cls, value: str):
|
||||
"""find an enum based on a search string."""
|
||||
normalized_value = value.lower().replace("_", "").replace(" ", "")
|
||||
|
||||
@@ -79,6 +106,81 @@ class SearchType(Enum):
|
||||
def display_name(self) -> str:
|
||||
return self.name.replace("_", " ").title()
|
||||
|
||||
class TrackRecType(Enum):
|
||||
"""Enum representing track recommendation key formats for various platforms.
|
||||
|
||||
Each key format is used to generate a recommendation link or identifier
|
||||
for a given track ID on the respective platform.
|
||||
|
||||
- RecommendationType.SPOTIFY:
|
||||
Generates a Spotify recommendation key in the format 'sprec:mix:track:{track_id}'.
|
||||
|
||||
- RecommendationType.YOUTUBE:
|
||||
Generates a YouTube recommendation URL with a playlist context.
|
||||
|
||||
- RecommendationType.YOUTUBE_MUSIC:
|
||||
Same as YouTube, generates a YouTube Music recommendation URL.
|
||||
|
||||
- RecommendationType.DEEZER:
|
||||
Generates a Deezer recommendation key in the format 'dzrec:{track_id}'.
|
||||
|
||||
- RecommendationType.YANDEX_MUSIC:
|
||||
Generates a Yandex Music recommendation key in the format 'ymrec:{track_id}'.
|
||||
|
||||
- RecommendationType.VK_MUSIC:
|
||||
Generates a VK Music recommendation key in the format 'vkrec:{track_id}'.
|
||||
|
||||
- RecommendationType.TIDAL:
|
||||
Generates a Tidal recommendation key in the format 'tdrec:{track_id}'.
|
||||
|
||||
- RecommendationType.QOBUZ:
|
||||
Generates a Qobuz recommendation key in the format 'qbrec:{track_id}'.
|
||||
|
||||
- RecommendationType.JIOSAAVN:
|
||||
Generates a JioSaavn recommendation key in the format 'jsrec:{track_id}'
|
||||
"""
|
||||
|
||||
YOUTUBE = "https://www.youtube.com/watch?v={track_id}&list=RD{track_id}"
|
||||
YOUTUBE_MUSIC = YOUTUBE
|
||||
SPOTIFY = "sprec:mix:track:{track_id}"
|
||||
DEEZER = "dzrec:{track_id}"
|
||||
YANDEX_MUSIC = "ymrec:{track_id}"
|
||||
VK_MUSIC = "vkrec:{track_id}"
|
||||
TIDAL = "tdrec:{track_id}"
|
||||
QOBUZ = "qbrec:{track_id}"
|
||||
JIOSAAVN = "jsrec:{track_id}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def format(self, track_id: str) -> str:
|
||||
"""Format the recommendation key using the provided track ID.
|
||||
|
||||
Args:
|
||||
track_id (str): The ID of the track to format.
|
||||
|
||||
Returns:
|
||||
str: The formatted recommendation link.
|
||||
"""
|
||||
return self.value.format(track_id=track_id)
|
||||
|
||||
@classmethod
|
||||
def from_platform(cls, platform: str) -> 'TrackRecType':
|
||||
"""Find the enum member based on a platform name.
|
||||
|
||||
Args:
|
||||
platform (str): The name of the platform.
|
||||
|
||||
Returns:
|
||||
TrackRecType: The corresponding enum member, or None if not found.
|
||||
"""
|
||||
normalized = platform.lower().replace("_", "").replace(" ", "")
|
||||
for member in cls:
|
||||
if member.name.lower().replace("_", "") == normalized:
|
||||
return member
|
||||
|
||||
return None
|
||||
|
||||
class RequestMethod(Enum):
|
||||
"""The enum for the different request methods in Voicelink
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import function as func
|
||||
import voicelink
|
||||
|
||||
from discord.ext import commands
|
||||
from typing import Optional
|
||||
@@ -29,7 +29,7 @@ class IPCClient:
|
||||
self._is_secure: bool = secure
|
||||
self._is_connected: bool = False
|
||||
self._is_connecting: bool = False
|
||||
self._logger: logging.Logger = logging.getLogger("ipc_client")
|
||||
self._logger: logging.Logger = logging.getLogger("vocard.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
|
||||
@@ -39,7 +39,7 @@ class IPCClient:
|
||||
self._heanders = {
|
||||
"Authorization": self._password,
|
||||
"User-Id": str(bot.user.id),
|
||||
"Client-Version": func.settings.version
|
||||
"Client-Version": voicelink.Config().version
|
||||
}
|
||||
|
||||
async def _listen(self) -> None:
|
||||
@@ -1,13 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time, re
|
||||
import function as func
|
||||
|
||||
from typing import List, Dict, Union, Optional
|
||||
from typing import List, Dict, Union, Optional, TYPE_CHECKING
|
||||
|
||||
from discord import User, Member, VoiceChannel
|
||||
from discord import User, Member
|
||||
from discord.ext import commands
|
||||
from voicelink import Player, Track, Playlist, NodePool, decode, LoopType, Filters
|
||||
from addons import LYRICS_PLATFORMS
|
||||
from voicelink import Player, Track, Playlist, NodePool, LoopType, Filters, Config, MongoDBHandler, LangHandler, LYRICS_PLATFORMS
|
||||
from voicelink.utils import TempCtx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import IPCClient
|
||||
|
||||
RATELIMIT_COUNTER: Dict[int, Dict[str, float]] = {}
|
||||
SCOPES = {
|
||||
"prefix": str,
|
||||
@@ -62,8 +66,8 @@ async def connect_channel(member: Member, bot: commands.Bot) -> Player:
|
||||
|
||||
channel = member.voice.channel
|
||||
try:
|
||||
settings = await func.get_settings(channel.guild.id)
|
||||
player: Player = await channel.connect(cls=Player(bot, channel, func.TempCtx(member, channel), settings))
|
||||
settings = await MongoDBHandler.get_settings(channel.guild.id)
|
||||
player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings))
|
||||
await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]})
|
||||
return player
|
||||
except:
|
||||
@@ -86,7 +90,7 @@ async def initBot(bot: commands.Bot, data: Dict) -> Dict:
|
||||
|
||||
async def initUser(bot: commands.Bot, data: Dict) -> Dict:
|
||||
user_id = int(data.get("userId"))
|
||||
data = await func.get_user(user_id)
|
||||
data = await MongoDBHandler.get_user(user_id)
|
||||
|
||||
for mail in data.get("inbox"):
|
||||
sender = bot.get_user(mail.get("sender"))
|
||||
@@ -146,9 +150,9 @@ async def getRecommendation(bot: commands.Bot, data: Dict) -> None:
|
||||
if not node:
|
||||
return
|
||||
|
||||
track_data = decode(track_id := data.get("trackId"))
|
||||
track_data = Track.decode(track_id := data.get("trackId"))
|
||||
track = Track(track_id=track_id, info=track_data, requester=bot.user)
|
||||
tracks: List[Track] = await node.get_recommendations(track, limit=60)
|
||||
tracks: List[Track] = await track.get_recommendations(node)
|
||||
|
||||
return {
|
||||
"op": "getRecommendation",
|
||||
@@ -163,12 +167,12 @@ async def skipTo(player: Player, member: Member, data: Dict) -> None:
|
||||
pass
|
||||
|
||||
elif member in player.skip_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
return error_msg(player.get_msg('voting.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)
|
||||
return error_msg(player.get_msg('player.controls.skip.vote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
index = data.get("index", 1)
|
||||
if index > 1:
|
||||
@@ -184,12 +188,12 @@ async def backTo(player: Player, member: Member, data: Dict) -> None:
|
||||
pass
|
||||
|
||||
elif member in player.skip_votes:
|
||||
return error_msg(player.get_msg('voted'), user_id=member.id)
|
||||
return error_msg(player.get_msg('voting.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)
|
||||
return error_msg(player.get_msg('player.controls.back.vote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
index = data.get("index", 1)
|
||||
if not player.is_playing:
|
||||
@@ -212,7 +216,7 @@ 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),
|
||||
info=Track.decode(track_id),
|
||||
requester=member
|
||||
) for track_id in data.get("tracks", [])]
|
||||
|
||||
@@ -249,11 +253,11 @@ async def searchAndPlay(player: Player, member: Member, data: Dict) -> None:
|
||||
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)
|
||||
return error_msg(player.get_msg('voting.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)
|
||||
return error_msg(player.get_msg('player.controls.shuffle.vote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
|
||||
|
||||
await player.shuffle(data.get("type", "queue"), member)
|
||||
|
||||
@@ -274,7 +278,7 @@ async def clearQueue(player: Player, member: Member, data: Dict) -> None:
|
||||
@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 MongoDBHandler.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:
|
||||
@@ -282,19 +286,19 @@ async def updatePause(player: Player, member: Member, data: Dict) -> None:
|
||||
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)
|
||||
return error_msg(player.get_msg('voting.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)
|
||||
return error_msg(player.get_msg('player.controls.pause.vote').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)
|
||||
return error_msg(player.get_msg('voting.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)
|
||||
return error_msg(player.get_msg('player.controls.resume.vote').format(member, len(player.resume_votes), required), guild_id=player.guild.id)
|
||||
|
||||
await player.set_pause(pause, member)
|
||||
|
||||
@@ -305,7 +309,7 @@ async def updatePosition(player: Player, member: Member, data: Dict) -> None:
|
||||
|
||||
async def toggleAutoplay(player: Player, member: Member, data: Dict) -> Dict:
|
||||
if not player.is_privileged(member):
|
||||
return error_msg(player.get_msg('missingAutoPlayPerm'))
|
||||
return error_msg(player.get_msg('permissions.missingAutoPlay'))
|
||||
|
||||
check = data.get("status", False)
|
||||
player.settings['autoplay'] = check
|
||||
@@ -358,13 +362,13 @@ def _assign_playlist_id(existed: list) -> str:
|
||||
return str(i)
|
||||
|
||||
async def _getPlaylist(user_id: int, playlist_id: str) -> Dict:
|
||||
playlists = await func.get_user(user_id, "playlist")
|
||||
playlists = await MongoDBHandler.get_user(user_id, d_type="playlist")
|
||||
playlist = playlists.get(playlist_id)
|
||||
if not playlist:
|
||||
return
|
||||
|
||||
if playlist["type"] == "share":
|
||||
target_user = await func.get_user(playlist["user"], "playlist")
|
||||
target_user = await MongoDBHandler.get_user(playlist["user"], d_type="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)
|
||||
@@ -391,7 +395,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
if not playlist_id and not _type == "createPlaylist":
|
||||
return error_msg("Unable to process this request without a playlist ID.", user_id=user_id, level="error")
|
||||
|
||||
rank, max_p, max_t = func.check_roles()
|
||||
max_p, max_t, _ = Config().get_playlist_config()
|
||||
if _type == "createPlaylist":
|
||||
name, playlist_url = data.get("playlistName"), data.get("playlistUrl")
|
||||
if not name:
|
||||
@@ -403,7 +407,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
"userId": str(user_id)
|
||||
}
|
||||
|
||||
playlist = await func.get_user(user_id, "playlist")
|
||||
playlist = await MongoDBHandler.get_user(user_id, d_type="playlist")
|
||||
if len(list(playlist.keys())) >= max_p:
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
@@ -429,14 +433,14 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "error",
|
||||
"msg": f"Please enter a valid link or public spotify or youtube playlist link.",
|
||||
"msg": f"Please enter a valid link or public playlist link.",
|
||||
"field": "playlistUrl",
|
||||
"userId": str(user_id)
|
||||
}
|
||||
|
||||
assigned_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.{assigned_playlist_id}": data}})
|
||||
await MongoDBHandler.update_user(user_id, {"$set": {f"playlist.{assigned_playlist_id}": data}})
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "created",
|
||||
@@ -450,9 +454,9 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
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 MongoDBHandler.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}})
|
||||
await MongoDBHandler.update_user(user_id, {"$unset": {f"playlist.{playlist_id}": 1}})
|
||||
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
@@ -473,7 +477,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
"userId": str(user_id)
|
||||
}
|
||||
|
||||
playlist = await func.get_user(user_id, "playlist")
|
||||
playlist = await MongoDBHandler.get_user(user_id, d_type="playlist")
|
||||
for data in playlist.values():
|
||||
if data['name'].lower() == name.lower():
|
||||
return {
|
||||
@@ -484,7 +488,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
"userId": str(user_id)
|
||||
}
|
||||
|
||||
await func.update_user(user_id, {"$set": {f'playlist.{playlist_id}.name': name}})
|
||||
await MongoDBHandler.update_user(user_id, {"$set": {f'playlist.{playlist_id}.name': name}})
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "renamed",
|
||||
@@ -504,15 +508,14 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
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)
|
||||
decoded_track = Track(track_id=track_id, info=Track.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}})
|
||||
await MongoDBHandler.update_user(user_id, {"$push": {f'playlist.{playlist_id}.tracks': track_id}})
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "addTrack",
|
||||
@@ -540,9 +543,9 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
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]}})
|
||||
await MongoDBHandler.update_user(user_id, {"$pull": {f'playlist.{playlist_id}.tracks': playlist['tracks'][track_position]}})
|
||||
|
||||
decoded_track = decode(playlist['tracks'][track_position])
|
||||
decoded_track = Track.decode(playlist['tracks'][track_position])
|
||||
return {
|
||||
"op": "updatePlaylist",
|
||||
"status": "removeTrack",
|
||||
@@ -554,7 +557,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
}
|
||||
|
||||
elif _type == "updateInbox":
|
||||
user = await func.get_user(user_id)
|
||||
user = await MongoDBHandler.get_user(user_id)
|
||||
is_accept = data.get("accept", False)
|
||||
|
||||
if is_accept and len(list(user.get("playlist").keys())) >= max_p:
|
||||
@@ -571,7 +574,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
|
||||
del inbox[index]
|
||||
if is_accept:
|
||||
share_playlists = await func.get_user(mail["sender"], "playlist")
|
||||
share_playlists = await MongoDBHandler.get_user(mail["sender"], d_type="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)
|
||||
|
||||
@@ -582,8 +585,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
"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": {
|
||||
await MongoDBHandler.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}})
|
||||
await MongoDBHandler.update_user(user_id, {"$set": {
|
||||
f'playlist.{assigned_playlist_id}': {
|
||||
'user': mail['sender'], 'referId': mail['referId'],
|
||||
'name': playlist_name,
|
||||
@@ -598,7 +601,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
|
||||
"data": share_playlist,
|
||||
})
|
||||
|
||||
await func.update_user(user_id, {"$set": {"inbox": inbox}})
|
||||
await MongoDBHandler.update_user(user_id, {"$set": {"inbox": inbox}})
|
||||
return payload
|
||||
|
||||
async def getMutualGuilds(bot: commands.Bot, data: Dict) -> Dict:
|
||||
@@ -631,7 +634,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
|
||||
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)
|
||||
settings = await MongoDBHandler.get_settings(guild_id)
|
||||
if "dj" in settings:
|
||||
role = guild.get_role(settings["dj"])
|
||||
if role:
|
||||
@@ -641,7 +644,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
|
||||
"op": "getSettings",
|
||||
"settings": settings,
|
||||
"options": {
|
||||
"languages": list(func.LANGS.keys()),
|
||||
"languages": list(LangHandler.get_all_languages()),
|
||||
"queueModes": ["Queue", "FairQueue"],
|
||||
"roles": [role.name for role in guild.roles]
|
||||
},
|
||||
@@ -656,7 +659,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
|
||||
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 LYRICS_PLATFORMS:
|
||||
platform = func.settings.lyrics_platform
|
||||
platform = Config().lyrics_platform
|
||||
|
||||
lyrics_platform = LYRICS_PLATFORMS.get(platform)
|
||||
if lyrics_platform:
|
||||
@@ -697,7 +700,7 @@ async def updateSettings(bot: commands.Bot, data: Dict) -> None:
|
||||
if key not in SCOPES or not isinstance(value, SCOPES[key]):
|
||||
del data[key]
|
||||
|
||||
await func.update_settings(guild.id, {"$set": data})
|
||||
await MongoDBHandler.update_settings(guild.id, {"$set": data})
|
||||
|
||||
METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = {
|
||||
"initBot": SystemMethod(initBot, credit=0),
|
||||
@@ -728,7 +731,7 @@ METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = {
|
||||
"searchAndPlay": PlayerMethod(searchAndPlay, credit=5, auto_connect=True)
|
||||
}
|
||||
|
||||
async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None:
|
||||
async def process_methods(ipc_client: IPCClient, bot: commands.Bot, data: Dict) -> None:
|
||||
op: str = data.get("op", "")
|
||||
method = METHODS.get(op)
|
||||
if not method or not (user_id := data.get("userId")):
|
||||
@@ -788,8 +791,7 @@ async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None:
|
||||
await ipc_client.send(resp)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
ipc_client._logger.error(f"Failed to process method '{op}' by user {user_id}", exc_info=e)
|
||||
payload = {
|
||||
"op": "errorMsg",
|
||||
"level": "error",
|
||||
162
voicelink/language.py
Normal file
162
voicelink/language.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from typing import Optional, Union, List, Dict, Any
|
||||
|
||||
from .config import Config
|
||||
from .mongodb import MongoDBHandler
|
||||
|
||||
logger = logging.getLogger("vocard.language")
|
||||
|
||||
class LangHandler:
|
||||
"""
|
||||
Static class for handling language and localization strings.
|
||||
Preloads all language files at initialization for fast access.
|
||||
"""
|
||||
|
||||
_langs: dict[str, dict[str, str]] = {}
|
||||
_local_langs: dict[str, dict[str, str]] = {}
|
||||
_default_lang: str = "EN"
|
||||
|
||||
@classmethod
|
||||
def init(
|
||||
cls,
|
||||
langs_dir: str = Config.WORKING_DIR / "langs",
|
||||
local_langs_dir: str = Config.WORKING_DIR / "local_langs"
|
||||
) -> "LangHandler":
|
||||
"""
|
||||
Initialize the language handler by preloading all language files
|
||||
from the given directories into memory.
|
||||
|
||||
Args:
|
||||
langs_dir (str): Directory containing main language JSON files.
|
||||
local_langs_dir (str): Directory containing localization JSON files.
|
||||
|
||||
Returns:
|
||||
LangHandler: The class itself with loaded languages.
|
||||
"""
|
||||
targets = [
|
||||
(langs_dir, cls._langs, "language"),
|
||||
(local_langs_dir, cls._local_langs, "local language"),
|
||||
]
|
||||
|
||||
for directory, storage, label in targets:
|
||||
if not os.path.exists(directory):
|
||||
logger.warning(f"{label.capitalize()} directory '{directory}' does not exist.")
|
||||
continue
|
||||
|
||||
for file in os.listdir(directory):
|
||||
if file.endswith(".json"):
|
||||
lang_code = file.split(".")[0].upper()
|
||||
filepath = os.path.join(directory, file)
|
||||
try:
|
||||
with open(filepath, encoding="utf8") as f:
|
||||
storage[lang_code] = cls._flatten_json(json.load(f))
|
||||
logger.info(f"Loaded {label}: {lang_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load {label} file '{filepath}': {e}")
|
||||
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
def _flatten_json(cls, nested_json: Dict[str, Any], parent_key: str = '', separator: str = '.') -> Dict[str, str]:
|
||||
"""
|
||||
Flatten a nested JSON object into a single-level dictionary with dot-separated keys.
|
||||
|
||||
Args:
|
||||
nested_json (dict): The nested JSON object to flatten
|
||||
parent_key (str): The base key for recursion (used internally)
|
||||
separator (str): The separator to use between keys (default: '.')
|
||||
|
||||
Returns:
|
||||
dict: Flattened dictionary with dot-separated keys
|
||||
"""
|
||||
flattened = {}
|
||||
|
||||
for key, value in nested_json.items():
|
||||
new_key = f"{parent_key}{separator}{key}" if parent_key else key
|
||||
|
||||
if isinstance(value, dict):
|
||||
flattened.update(cls._flatten_json(value, new_key, separator))
|
||||
else:
|
||||
flattened[new_key] = value
|
||||
|
||||
return flattened
|
||||
|
||||
@classmethod
|
||||
def _get_lang(cls, lang: str, *keys) -> Optional[Union[list[str], str]]:
|
||||
"""
|
||||
Internal helper to retrieve strings from the preloaded cls._lang cache.
|
||||
|
||||
Args:
|
||||
lang (str): Language code (e.g., "EN", "FR").
|
||||
*keys: One or more keys to fetch from the language dictionary.
|
||||
|
||||
Returns:
|
||||
str | list[str] | None: The requested string(s), or "Not found!" if missing.
|
||||
"""
|
||||
if not lang:
|
||||
lang = cls._default_lang
|
||||
|
||||
lang = lang.upper()
|
||||
if lang not in cls._langs:
|
||||
lang = cls._default_lang
|
||||
|
||||
lang_dict = cls._langs.get(lang, {})
|
||||
if len(keys) == 1:
|
||||
value = lang_dict.get(keys[0], "Not found!")
|
||||
return value
|
||||
|
||||
values = [lang_dict.get(key, "Not found!") for key in keys]
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
async def get_lang(cls, guild_id: int, *keys) -> Optional[Union[list[str], str]]:
|
||||
"""
|
||||
Fetch the language setting for a guild from the database and return
|
||||
the requested string(s).
|
||||
|
||||
Args:
|
||||
guild_id (int): Guild ID to fetch settings for.
|
||||
*keys: One or more keys to fetch from the language dictionary.
|
||||
|
||||
Returns:
|
||||
str | list[str] | None: The requested string(s).
|
||||
"""
|
||||
settings = await MongoDBHandler.get_settings(guild_id)
|
||||
lang = settings.get("lang", LangHandler._default_lang)
|
||||
return cls._get_lang(lang, *keys)
|
||||
|
||||
@classmethod
|
||||
def get_all_languages(cls) -> List[str]:
|
||||
"""
|
||||
Get a combined list of all loaded language codes (main + local).
|
||||
|
||||
Returns:
|
||||
list[str]: List of all available language codes.
|
||||
"""
|
||||
return cls._langs.keys()
|
||||
@@ -29,15 +29,16 @@ import hashlib
|
||||
import base64
|
||||
import json
|
||||
import urllib.parse
|
||||
import function as func
|
||||
|
||||
from datetime import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from urllib.parse import quote
|
||||
from math import floor
|
||||
from urllib.parse import quote
|
||||
from abc import ABC, abstractmethod
|
||||
from importlib import import_module
|
||||
from datetime import datetime
|
||||
from typing import Optional, Type
|
||||
|
||||
from .config import Config
|
||||
|
||||
userAgents = '''Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.11 Safari/535.19
|
||||
@@ -202,7 +203,7 @@ class A_ZLyrics(LyricsPlatform):
|
||||
class Genius(LyricsPlatform):
|
||||
def __init__(self) -> None:
|
||||
self.module = import_module("lyricsgenius")
|
||||
self.genius = self.module.Genius(func.settings.genius_token)
|
||||
self.genius = self.module.Genius(Config().genius_token)
|
||||
|
||||
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
|
||||
song = self.genius.search_song(title=title, artist=artist)
|
||||
542
voicelink/mongodb.py
Normal file
542
voicelink/mongodb.py
Normal file
@@ -0,0 +1,542 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from typing import Any, Dict, Optional, Literal, TypedDict, List
|
||||
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
|
||||
|
||||
from .config import Config
|
||||
|
||||
logger: logging.Logger = logging.getLogger("vocard.db")
|
||||
|
||||
# Type definitions for better code clarity
|
||||
class PlaylistPerms(TypedDict):
|
||||
read: List[int]
|
||||
write: List[int]
|
||||
remove: List[int]
|
||||
|
||||
class Playlist(TypedDict):
|
||||
tracks: List[Dict[str, Any]]
|
||||
perms: PlaylistPerms
|
||||
name: str
|
||||
type: Literal["playlist"]
|
||||
|
||||
class UserData(TypedDict):
|
||||
_id: int
|
||||
playlist: Dict[str, Playlist]
|
||||
history: List[Dict[str, Any]]
|
||||
inbox: List[Dict[str, Any]]
|
||||
|
||||
UpdateOperationType = Literal["$set", "$unset", "$inc", "$push", "$pull"]
|
||||
|
||||
|
||||
class MongoDBHandler:
|
||||
"""
|
||||
Handles MongoDB operations with connection pooling and caching.
|
||||
Implements a thread-safe singleton pattern for database connections.
|
||||
"""
|
||||
|
||||
# Static instance variables
|
||||
_client: Optional[AsyncIOMotorClient] = None
|
||||
_db: Optional[Any] = None
|
||||
_settings_db: Optional[AsyncIOMotorCollection] = None
|
||||
_users_db: Optional[AsyncIOMotorCollection] = None
|
||||
_lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
# Cache with TTL (Time To Live in seconds)
|
||||
_CACHE_TTL: int = 300 # 5 minutes
|
||||
_settings_buffer: Dict[int, Dict[str, Any]] = {}
|
||||
_users_buffer: Dict[int, Dict[str, Any]] = {}
|
||||
_last_access: Dict[int, float] = {} # Tracks last access time for cache entries
|
||||
|
||||
# Maximum cache size to prevent memory issues
|
||||
_MAX_CACHE_SIZE: int = 10000
|
||||
|
||||
# Default user template
|
||||
_user_base: UserData = {
|
||||
"_id": 0, # Will be replaced with actual user ID
|
||||
"playlist": {
|
||||
"200": {
|
||||
"tracks": [],
|
||||
"perms": {"read": [], "write": [], "remove": []},
|
||||
"name": "Favourite",
|
||||
"type": "playlist",
|
||||
}
|
||||
},
|
||||
"history": [],
|
||||
"inbox": [],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def init(cls, uri: str, db_name: str) -> None:
|
||||
"""
|
||||
Initialize the MongoDB connection with connection pooling and error handling.
|
||||
|
||||
Args:
|
||||
uri (str): MongoDB connection URI
|
||||
db_name (str): Name of the database to use
|
||||
|
||||
Raises:
|
||||
ConnectionError: If unable to connect to MongoDB
|
||||
Exception: For other initialization errors
|
||||
"""
|
||||
if not uri or not db_name:
|
||||
logger.error("MongoDB initialization failed: URI or database name is missing.")
|
||||
raise ValueError("Both URI and database name must be provided.")
|
||||
|
||||
async with cls._lock:
|
||||
if cls._client is not None:
|
||||
logger.warning("MongoDB client is already initialized. Skipping reinitialization.")
|
||||
return
|
||||
|
||||
logger.debug("Initializing MongoDB client with URI: %s and DB name: %s", uri, db_name)
|
||||
|
||||
try:
|
||||
cls._client = AsyncIOMotorClient(
|
||||
uri,
|
||||
maxPoolSize=50,
|
||||
minPoolSize=5,
|
||||
maxIdleTimeMS=60000,
|
||||
retryWrites=True
|
||||
)
|
||||
logger.debug("MongoDB client created successfully. Testing connection...")
|
||||
|
||||
await cls._client.server_info()
|
||||
logger.debug("MongoDB connection test passed.")
|
||||
|
||||
cls._db = cls._client[db_name]
|
||||
cls._settings_db = cls._db["Settings"]
|
||||
cls._users_db = cls._db["Users"]
|
||||
|
||||
logger.info("MongoDB databases initialized: %s", db_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("MongoDB initialization failed: %s", str(e), exc_info=True)
|
||||
|
||||
cls._client = None
|
||||
cls._db = None
|
||||
cls._settings_db = None
|
||||
cls._users_db = None
|
||||
|
||||
raise ConnectionError(f"Failed to initialize MongoDB: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def cleanup_cache(cls) -> None:
|
||||
"""
|
||||
Cleanup expired cache entries to prevent memory leaks.
|
||||
Should be called periodically or when cache size exceeds _MAX_CACHE_SIZE.
|
||||
"""
|
||||
current_time = time.time()
|
||||
logger.info("Starting cache cleanup at timestamp: %.2f", current_time)
|
||||
|
||||
async with cls._lock:
|
||||
try:
|
||||
# Remove expired entries from settings cache
|
||||
expired_settings = [
|
||||
guild_id for guild_id, last_access in cls._last_access.items()
|
||||
if current_time - last_access > cls._CACHE_TTL and guild_id in cls._settings_buffer
|
||||
]
|
||||
logger.debug("Found %d expired cache entries.", len(expired_settings))
|
||||
|
||||
for guild_id in expired_settings:
|
||||
del cls._settings_buffer[guild_id]
|
||||
del cls._last_access[guild_id]
|
||||
logger.debug("Removed expired cache for guild_id: %s", guild_id)
|
||||
|
||||
# If still too large, remove oldest entries
|
||||
while len(cls._settings_buffer) > cls._MAX_CACHE_SIZE:
|
||||
oldest_id = min(cls._last_access.items(), key=lambda x: x[1])[0]
|
||||
del cls._settings_buffer[oldest_id]
|
||||
del cls._last_access[oldest_id]
|
||||
logger.warning("Cache size exceeded. Removed oldest entry: %s", oldest_id)
|
||||
|
||||
logger.info("Cache cleanup completed. Current cache size: %d", len(cls._settings_buffer))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Cache cleanup failed: %s", str(e), exc_info=True)
|
||||
|
||||
@classmethod
|
||||
async def _update_db(
|
||||
cls,
|
||||
db: AsyncIOMotorCollection,
|
||||
cache: Dict[str, Any],
|
||||
filter_: Dict[str, Any],
|
||||
data: Dict[UpdateOperationType, Dict[str, Any]],
|
||||
) -> bool:
|
||||
"""
|
||||
Update database and cache atomically with error handling and validation.
|
||||
|
||||
Args:
|
||||
db: MongoDB collection to update
|
||||
cache: Cache dictionary to update
|
||||
filter_: MongoDB filter for the update
|
||||
data: Update operations to perform
|
||||
|
||||
Returns:
|
||||
bool: True if update was successful, False otherwise
|
||||
|
||||
Raises:
|
||||
ValueError: If invalid update operation is provided
|
||||
"""
|
||||
async with cls._lock:
|
||||
try:
|
||||
# Validate update operations
|
||||
valid_operations = {"$set", "$unset", "$inc", "$push", "$pull"}
|
||||
if not all(op in valid_operations for op in data.keys()):
|
||||
raise ValueError(f"Invalid update operation. Must be one of {valid_operations}")
|
||||
|
||||
# Update cache first
|
||||
for mode, action in data.items():
|
||||
for key, value in action.items():
|
||||
cursors = key.split(".")
|
||||
nested = cache
|
||||
|
||||
# Ensure path exists
|
||||
for c in cursors[:-1]:
|
||||
if not isinstance(nested, dict):
|
||||
raise ValueError(f"Invalid path: {key}")
|
||||
nested = nested.setdefault(c, {})
|
||||
|
||||
field = cursors[-1]
|
||||
|
||||
try:
|
||||
if mode == "$set":
|
||||
nested[field] = value
|
||||
elif mode == "$unset":
|
||||
nested.pop(field, None)
|
||||
elif mode == "$inc":
|
||||
if not isinstance(nested.get(field, 0), (int, float)):
|
||||
raise ValueError(f"Cannot increment non-numeric field: {field}")
|
||||
nested[field] = nested.get(field, 0) + value
|
||||
elif mode == "$push":
|
||||
arr = nested.setdefault(field, [])
|
||||
if not isinstance(arr, list):
|
||||
raise ValueError(f"Cannot push to non-array field: {field}")
|
||||
if isinstance(value, dict) and "$each" in value:
|
||||
arr.extend(value["$each"])
|
||||
if "$slice" in value:
|
||||
arr[:] = arr[value["$slice"]:]
|
||||
else:
|
||||
arr.append(value)
|
||||
elif mode == "$pull":
|
||||
if field in nested:
|
||||
if not isinstance(nested[field], list):
|
||||
raise ValueError(f"Cannot pull from non-array field: {field}")
|
||||
values = value.get("$in", []) if isinstance(value, dict) else [value]
|
||||
nested[field] = [item for item in nested[field] if item not in values]
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error updating {key}: {str(e)}")
|
||||
|
||||
# Then update database
|
||||
result = await db.update_one(filter_, data)
|
||||
|
||||
# Update last access time
|
||||
if '_id' in filter_:
|
||||
cls._last_access[filter_['_id']] = time.time()
|
||||
|
||||
return result.modified_count > 0
|
||||
|
||||
except Exception as e:
|
||||
# Rollback cache if database update fails
|
||||
if '_id' in filter_:
|
||||
cls._settings_buffer.pop(filter_['_id'], None)
|
||||
cls._users_buffer.pop(filter_['_id'], None)
|
||||
raise Exception(f"Update failed: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def get_cached_settings(
|
||||
cls,
|
||||
guild_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve settings for a guild with caching.
|
||||
|
||||
Args:
|
||||
guild_id: The Discord guild ID
|
||||
|
||||
Returns:
|
||||
Dict containing guild settings or empty dict if not found
|
||||
"""
|
||||
try:
|
||||
if guild_id not in cls._settings_buffer:
|
||||
return {}
|
||||
|
||||
return copy.deepcopy(cls._settings_buffer[guild_id])
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to retrieve settings: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def get_settings(
|
||||
cls,
|
||||
guild_id: int,
|
||||
*,
|
||||
deep_copy: bool = True,
|
||||
force_refresh: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve settings for a guild with caching.
|
||||
|
||||
Args:
|
||||
guild_id: The Discord guild ID
|
||||
force_refresh: If True, bypass cache and fetch fresh data
|
||||
|
||||
Returns:
|
||||
Dict containing guild settings
|
||||
|
||||
Raises:
|
||||
ConnectionError: If database operation fails
|
||||
"""
|
||||
try:
|
||||
async with cls._lock:
|
||||
# Check if we need fresh data
|
||||
if force_refresh or guild_id not in cls._settings_buffer:
|
||||
settings = await cls._settings_db.find_one({"_id": guild_id})
|
||||
if not settings:
|
||||
settings = {"_id": guild_id}
|
||||
try:
|
||||
await cls._settings_db.insert_one(settings)
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to create settings: {str(e)}")
|
||||
|
||||
cls._settings_buffer[guild_id] = settings
|
||||
cls._last_access[guild_id] = time.time()
|
||||
|
||||
buffer = cls._settings_buffer[guild_id]
|
||||
return copy.deepcopy(buffer) if deep_copy else buffer
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to retrieve settings: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def update_settings(
|
||||
cls,
|
||||
guild_id: int,
|
||||
data: Dict[UpdateOperationType, Dict[str, Any]],
|
||||
*,
|
||||
upsert: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Update settings for a guild.
|
||||
|
||||
Args:
|
||||
guild_id: The Discord guild ID
|
||||
data: Update operations to perform
|
||||
upsert: If True, create document if it doesn't exist
|
||||
|
||||
Returns:
|
||||
bool: True if update was successful
|
||||
|
||||
Raises:
|
||||
ValueError: If invalid update data is provided
|
||||
ConnectionError: If database operation fails
|
||||
"""
|
||||
try:
|
||||
settings = await cls.get_settings(guild_id, deep_copy=False)
|
||||
result = await cls._update_db(
|
||||
cls._settings_db,
|
||||
settings,
|
||||
{"_id": guild_id},
|
||||
data
|
||||
)
|
||||
|
||||
if not result and upsert:
|
||||
# Try to insert if update failed and upsert is True
|
||||
settings = {"_id": guild_id, **data.get("$set", {})}
|
||||
await cls._settings_db.insert_one(settings)
|
||||
cls._settings_buffer[guild_id] = settings
|
||||
return True
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to update settings: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def get_user(
|
||||
cls,
|
||||
user_id: int,
|
||||
*,
|
||||
d_type: Optional[str] = None,
|
||||
need_copy: bool = True,
|
||||
force_refresh: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve user data with caching and type-specific data.
|
||||
|
||||
Args:
|
||||
user_id: The Discord user ID
|
||||
d_type: Specific data type to retrieve
|
||||
need_copy: If True, return a deep copy of the data
|
||||
force_refresh: If True, bypass cache and fetch fresh data
|
||||
|
||||
Returns:
|
||||
Dict containing user data
|
||||
|
||||
Raises:
|
||||
ConnectionError: If database operation fails
|
||||
ValueError: If invalid d_type is provided
|
||||
"""
|
||||
try:
|
||||
async with cls._lock:
|
||||
# Check if we need fresh data
|
||||
if force_refresh or user_id not in cls._users_buffer:
|
||||
user = await cls._users_db.find_one({"_id": user_id})
|
||||
if not user:
|
||||
user = {**copy.deepcopy(cls._user_base), "_id": user_id}
|
||||
user["playlist"]["200"]["name"] = Config().get_playlist_config()[2]
|
||||
try:
|
||||
await cls._users_db.insert_one(user)
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to create user: {str(e)}")
|
||||
|
||||
cls._users_buffer[user_id] = user
|
||||
cls._last_access[user_id] = time.time()
|
||||
|
||||
user = cls._users_buffer[user_id]
|
||||
|
||||
if d_type:
|
||||
if d_type not in cls._user_base:
|
||||
raise ValueError(f"Invalid data type: {d_type}")
|
||||
user = user.setdefault(d_type, copy.deepcopy(cls._user_base.get(d_type)))
|
||||
|
||||
return copy.deepcopy(user) if need_copy else user
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to retrieve user data: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def update_user(
|
||||
cls,
|
||||
user_id: int,
|
||||
data: Dict[UpdateOperationType, Dict[str, Any]],
|
||||
*,
|
||||
upsert: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Update user data.
|
||||
|
||||
Args:
|
||||
user_id: The Discord user ID
|
||||
data: Update operations to perform
|
||||
upsert: If True, create user if doesn't exist
|
||||
|
||||
Returns:
|
||||
bool: True if update was successful
|
||||
|
||||
Raises:
|
||||
ValueError: If invalid update data is provided
|
||||
ConnectionError: If database operation fails
|
||||
"""
|
||||
try:
|
||||
user = await cls.get_user(user_id, need_copy=False)
|
||||
result = await cls._update_db(
|
||||
cls._users_db,
|
||||
user,
|
||||
{"_id": user_id},
|
||||
data
|
||||
)
|
||||
|
||||
if not result and upsert:
|
||||
# Try to insert if update failed and upsert is True
|
||||
user_data = {"_id": user_id, **data.get("$set", {})}
|
||||
await cls._users_db.insert_one(user_data)
|
||||
cls._users_buffer[user_id] = user_data
|
||||
return True
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to update user: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def delete_user(cls, user_id: int) -> bool:
|
||||
"""
|
||||
Delete a user's data completely.
|
||||
|
||||
Args:
|
||||
user_id: The Discord user ID
|
||||
|
||||
Returns:
|
||||
bool: True if deletion was successful
|
||||
|
||||
Raises:
|
||||
ConnectionError: If database operation fails
|
||||
"""
|
||||
try:
|
||||
async with cls._lock:
|
||||
result = await cls._users_db.delete_one({"_id": user_id})
|
||||
if result.deleted_count > 0:
|
||||
cls._users_buffer.pop(user_id, None)
|
||||
cls._last_access.pop(user_id, None)
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to delete user: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def get_users_by_criteria(
|
||||
cls,
|
||||
criteria: Dict[str, Any],
|
||||
*,
|
||||
limit: Optional[int] = None,
|
||||
skip: int = 0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve multiple users matching specific criteria.
|
||||
|
||||
Args:
|
||||
criteria: MongoDB query criteria
|
||||
limit: Maximum number of users to return
|
||||
skip: Number of matching users to skip
|
||||
|
||||
Returns:
|
||||
List of matching user data
|
||||
|
||||
Raises:
|
||||
ConnectionError: If database operation fails
|
||||
"""
|
||||
try:
|
||||
cursor = cls._users_db.find(criteria).skip(skip)
|
||||
if limit:
|
||||
cursor = cursor.limit(limit)
|
||||
|
||||
users = await cursor.to_list(length=None)
|
||||
|
||||
# Update cache with fetched users
|
||||
async with cls._lock:
|
||||
current_time = time.time()
|
||||
for user in users:
|
||||
user_id = user["_id"]
|
||||
cls._users_buffer[user_id] = user
|
||||
cls._last_access[user_id] = current_time
|
||||
|
||||
return users
|
||||
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to retrieve users: {str(e)}")
|
||||
@@ -21,22 +21,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from discord import Member
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
from tldextract import extract
|
||||
from discord import Member
|
||||
|
||||
from .enums import SearchType
|
||||
from function import (
|
||||
get_source,
|
||||
time as ctime
|
||||
)
|
||||
from .enums import SearchType, TrackRecType
|
||||
from .config import Config
|
||||
from .utils import format_ms
|
||||
from .transformer import encode, decode
|
||||
|
||||
from .transformer import encode
|
||||
|
||||
YOUTUBE_REGEX = re.compile(r'(https?://)?(www\.)?youtube\.(com|nl)/watch\?v=([-\w]+)')
|
||||
if TYPE_CHECKING:
|
||||
from .pool import Node
|
||||
|
||||
|
||||
class Track:
|
||||
"""The base track object. Returns critical track information needed for parsing by Lavalink.
|
||||
You can also pass in commands.Context to get a discord.py Context object in your track.
|
||||
@@ -67,8 +66,11 @@ class Track:
|
||||
track_id: str = None,
|
||||
info: dict,
|
||||
requester: Member,
|
||||
search_type: SearchType = SearchType.YOUTUBE,
|
||||
search_type: SearchType = None,
|
||||
):
|
||||
if not search_type:
|
||||
search_type = Config().search_platform
|
||||
|
||||
self._track_id: Optional[str] = track_id
|
||||
self.info: dict = info
|
||||
|
||||
@@ -80,10 +82,7 @@ class Track:
|
||||
self._search_type: SearchType = search_type
|
||||
|
||||
self.thumbnail: str = info.get("artworkUrl")
|
||||
if not self.thumbnail and YOUTUBE_REGEX.match(self.uri):
|
||||
self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/maxresdefault.jpg"
|
||||
|
||||
self.emoji: str = get_source(self.source, "emoji")
|
||||
self.emoji: str = Config().get_source_config(self.source, "emoji")
|
||||
self.length: float = info.get("length")
|
||||
|
||||
self.requester: Member = requester
|
||||
@@ -94,17 +93,38 @@ class Track:
|
||||
self.end_time: Optional[int] = None
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Track):
|
||||
return False
|
||||
|
||||
return other.track_id == self.track_id
|
||||
"""Checks equality between two tracks."""
|
||||
if isinstance(other, Track):
|
||||
return other.track_id == self.track_id
|
||||
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of the track."""
|
||||
return self.title
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Voicelink.track title={self.title!r} uri=<{self.uri!r}> length={self.length}>"
|
||||
|
||||
async def get_recommendations(self, node: Node) -> List[Track]:
|
||||
"""Fetches recommended tracks based on the current track."""
|
||||
if not node or not node._available:
|
||||
return []
|
||||
|
||||
rec_type = TrackRecType.from_platform(self.source)
|
||||
if not rec_type:
|
||||
return []
|
||||
|
||||
query = rec_type.format(track_id=self.identifier)
|
||||
tracks = await node.get_tracks(query=query, requester=node.bot.user)
|
||||
if not tracks:
|
||||
return []
|
||||
|
||||
if isinstance(tracks, Playlist):
|
||||
tracks = tracks.tracks
|
||||
|
||||
return tracks
|
||||
|
||||
@property
|
||||
def track_id(self) -> str:
|
||||
if not self._track_id:
|
||||
@@ -114,8 +134,8 @@ class Track:
|
||||
|
||||
@property
|
||||
def formatted_length(self) -> str:
|
||||
return ctime(self.length)
|
||||
|
||||
return format_ms(self.length)
|
||||
|
||||
@property
|
||||
def data(self) -> dict:
|
||||
return {
|
||||
@@ -123,6 +143,14 @@ class Track:
|
||||
"requester_id": self.requester.id
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def decode(cls, track_id: str) -> dict:
|
||||
return decode(track_id)
|
||||
|
||||
@classmethod
|
||||
def encode(cls, track_info: dict) -> 'Track':
|
||||
return encode(track_info)
|
||||
|
||||
class Playlist:
|
||||
"""The base playlist object.
|
||||
Returns critical playlist information needed for parsing by Lavalink.
|
||||
@@ -162,4 +190,4 @@ class Playlist:
|
||||
|
||||
@property
|
||||
def track_count(self) -> int:
|
||||
return len(self.tracks)
|
||||
return len(self.tracks)
|
||||
@@ -24,27 +24,30 @@ SOFTWARE.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import function as func
|
||||
import discord
|
||||
|
||||
from discord import Embed, Client
|
||||
from discord.ext import commands
|
||||
from typing import TYPE_CHECKING, List, Callable
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from .config import Config
|
||||
from .utils import format_ms
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .player import Player
|
||||
from .objects import Track
|
||||
|
||||
def ensure_track(func) -> callable:
|
||||
def wrapper(self: Placeholders, *args, **kwargs):
|
||||
from .pool import NodePool
|
||||
|
||||
def ensure_track(func) -> Callable:
|
||||
def wrapper(self: PlayerPlaceholder, *args, **kwargs):
|
||||
current = self.get_current()
|
||||
if not current:
|
||||
return "None"
|
||||
return func(self, current, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
class Placeholders:
|
||||
def __init__(self, bot: Client, player: Player = None) -> None:
|
||||
self.bot: Client = bot
|
||||
class PlayerPlaceholder:
|
||||
def __init__(self, bot: commands.Bot, player: Player = None) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.player: Player = player
|
||||
|
||||
self.variables = {
|
||||
@@ -67,7 +70,7 @@ class Placeholders:
|
||||
"loop_mode": self.loop_mode,
|
||||
"default_embed_color": self.default_embed_color,
|
||||
"bot_icon": self.bot_icon,
|
||||
"server_invite_link": func.settings.invite_link,
|
||||
"server_invite_link": Config().invite_link,
|
||||
"invite_link": f"https://discord.com/oauth2/authorize?client_id={self.bot.user.id}&permissions=2184260928&scope=bot%20applications.commands"
|
||||
}
|
||||
|
||||
@@ -97,7 +100,7 @@ class Placeholders:
|
||||
|
||||
@ensure_track
|
||||
def track_duration(self, track: Track) -> str:
|
||||
return self.player.get_msg("live") if track.is_stream else func.time(track.length)
|
||||
return self.player.get_msg("common.status.live") if track.is_stream else format_ms(track.length)
|
||||
|
||||
@ensure_track
|
||||
def track_requester_id(self, track: Track) -> str:
|
||||
@@ -117,7 +120,7 @@ class Placeholders:
|
||||
|
||||
@ensure_track
|
||||
def track_color(self, track: Track) -> int:
|
||||
return int(func.get_source(track.source, "color"), 16)
|
||||
return int(Config().get_source_config(track.source, "color"), 16)
|
||||
|
||||
@ensure_track
|
||||
def track_source_name(self, track: Track) -> str:
|
||||
@@ -152,7 +155,7 @@ class Placeholders:
|
||||
return self.player.queue.repeat if self.player else "Off"
|
||||
|
||||
def default_embed_color(self) -> int:
|
||||
return func.settings.embed_color
|
||||
return Config().embed_color
|
||||
|
||||
def bot_icon(self) -> str:
|
||||
return self.bot.user.display_avatar.url if self.player else "https://i.imgur.com/dIFBwU7.png"
|
||||
@@ -197,41 +200,78 @@ class Placeholders:
|
||||
text = re.sub(r'@@(.*?)@@', lambda x: str(variables.get(x.group(1), '')), text)
|
||||
return text
|
||||
|
||||
def build_embed(embed_form: dict[str, dict], placeholder: Placeholders) -> Embed:
|
||||
embed = Embed()
|
||||
try:
|
||||
rv = {key: func() if callable(func) else func for key, func in placeholder.variables.items()}
|
||||
if author := embed_form.get("author"):
|
||||
embed.set_author(
|
||||
name = placeholder.replace(author.get("name"), rv),
|
||||
url = placeholder.replace(author.get("url"), rv),
|
||||
icon_url = placeholder.replace(author.get("icon_url"), rv)
|
||||
)
|
||||
|
||||
if title := embed_form.get("title"):
|
||||
embed.title = placeholder.replace(title.get("name"), rv)
|
||||
embed.url = placeholder.replace(title.get("url"), rv)
|
||||
@classmethod
|
||||
def build_embed(cls, embed_form: dict[str, dict], placeholder: PlayerPlaceholder) -> discord.Embed:
|
||||
embed = discord.Embed()
|
||||
try:
|
||||
rv = {key: func() if callable(func) else func for key, func in placeholder.variables.items()}
|
||||
if author := embed_form.get("author"):
|
||||
embed.set_author(
|
||||
name = placeholder.replace(author.get("name"), rv),
|
||||
url = placeholder.replace(author.get("url"), rv),
|
||||
icon_url = placeholder.replace(author.get("icon_url"), rv)
|
||||
)
|
||||
|
||||
if title := embed_form.get("title"):
|
||||
embed.title = placeholder.replace(title.get("name"), rv)
|
||||
embed.url = placeholder.replace(title.get("url"), rv)
|
||||
|
||||
if fields := embed_form.get("fields", []):
|
||||
for f in fields:
|
||||
embed.add_field(name=placeholder.replace(f.get("name"), rv), value=placeholder.replace(f.get("value", ""), rv), inline=f.get("inline", False))
|
||||
if fields := embed_form.get("fields", []):
|
||||
for f in fields:
|
||||
embed.add_field(name=placeholder.replace(f.get("name"), rv), value=placeholder.replace(f.get("value", ""), rv), inline=f.get("inline", False))
|
||||
|
||||
if footer := embed_form.get("footer"):
|
||||
embed.set_footer(
|
||||
text = placeholder.replace(footer.get("text"), rv),
|
||||
icon_url = placeholder.replace(footer.get("icon_url"), rv)
|
||||
)
|
||||
if footer := embed_form.get("footer"):
|
||||
embed.set_footer(
|
||||
text = placeholder.replace(footer.get("text"), rv),
|
||||
icon_url = placeholder.replace(footer.get("icon_url"), rv)
|
||||
)
|
||||
|
||||
if thumbnail := embed_form.get("thumbnail"):
|
||||
embed.set_thumbnail(url = placeholder.replace(thumbnail, rv))
|
||||
|
||||
if image := embed_form.get("image"):
|
||||
embed.set_image(url = placeholder.replace(image, rv))
|
||||
if thumbnail := embed_form.get("thumbnail"):
|
||||
embed.set_thumbnail(url = placeholder.replace(thumbnail, rv))
|
||||
|
||||
if image := embed_form.get("image"):
|
||||
embed.set_image(url = placeholder.replace(image, rv))
|
||||
|
||||
embed.description = placeholder.replace(embed_form.get("description"), rv)
|
||||
embed.color = int(placeholder.replace(embed_form.get("color"), rv))
|
||||
embed.description = placeholder.replace(embed_form.get("description"), rv)
|
||||
embed.color = int(placeholder.replace(embed_form.get("color"), rv))
|
||||
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
return embed
|
||||
return embed
|
||||
|
||||
class BotPlaceholder:
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
self.variables = {
|
||||
"guilds": self.guilds_count,
|
||||
"users": self.users_count,
|
||||
"players": self.players_count,
|
||||
"nodes": self.nodes_count
|
||||
}
|
||||
|
||||
def guilds_count(self) -> int:
|
||||
return len(self.bot.guilds)
|
||||
|
||||
def users_count(self) -> int:
|
||||
return len(self.bot.users)
|
||||
|
||||
def players_count(self) -> int:
|
||||
count = 0
|
||||
for node in NodePool._nodes.values():
|
||||
count += len(node._players)
|
||||
|
||||
return count
|
||||
|
||||
def nodes_count(self):
|
||||
return len(NodePool._nodes)
|
||||
|
||||
def replace(self, msg: str) -> str:
|
||||
keys: List[str] = re.findall(r'@@(.*?)@@', msg)
|
||||
|
||||
for key in keys:
|
||||
value = self.variables.get(key.lower(), None)
|
||||
if value:
|
||||
msg = msg.replace(f"@@{key}@@", str(value()))
|
||||
|
||||
return msg
|
||||
@@ -21,13 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import time, logging
|
||||
import function as func
|
||||
from __future__ import annotations
|
||||
|
||||
import time, logging, asyncio
|
||||
|
||||
from math import ceil
|
||||
from asyncio import sleep
|
||||
from views import InteractiveController
|
||||
from typing import Any, Dict, List, Optional, Union, Tuple
|
||||
from random import shuffle, choice
|
||||
from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING
|
||||
|
||||
from discord import (
|
||||
Client,
|
||||
@@ -45,18 +45,25 @@ from discord import (
|
||||
from discord.ext import commands
|
||||
|
||||
from . import events
|
||||
from .config import Config
|
||||
from .pool import Node, NodePool
|
||||
from .objects import Track, Playlist
|
||||
from .filters import Filter, Filters
|
||||
from .enums import SearchType, LoopType, RequestMethod
|
||||
from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent, TrackExceptionEvent
|
||||
from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, FilterTagAlreadyInUse, DuplicateTrack
|
||||
from .filters import Filter, Filters
|
||||
from .objects import Track, Playlist
|
||||
from .pool import Node, NodePool
|
||||
from .placeholders import Placeholders, build_embed
|
||||
from .placeholders import PlayerPlaceholder
|
||||
from .queue import Queue, QUEUE_TYPES
|
||||
from random import shuffle, choice
|
||||
from .mongodb import MongoDBHandler
|
||||
from .language import LangHandler
|
||||
from .views import InteractiveController
|
||||
from .utils import format_ms, dispatch_message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .ipc import IPCClient
|
||||
|
||||
async def connect_channel(ctx: Union[commands.Context, Interaction], channel: VoiceChannel = None):
|
||||
texts = await func.get_lang(ctx.guild.id, "noChannel", "noPermission")
|
||||
texts = await LangHandler.get_lang(ctx.guild.id, "voice.connection.noChannel", "voice.connection.noPermission")
|
||||
try:
|
||||
channel = channel or ctx.author.voice.channel if isinstance(ctx, commands.Context) else ctx.user.voice.channel
|
||||
except:
|
||||
@@ -66,7 +73,7 @@ async def connect_channel(ctx: Union[commands.Context, Interaction], channel: Vo
|
||||
if check.connect == False or check.speak == False:
|
||||
raise VoicelinkException(texts[1])
|
||||
|
||||
settings = await func.get_settings(channel.guild.id)
|
||||
settings = await MongoDBHandler.get_settings(channel.guild.id)
|
||||
player: Player = await channel.connect(
|
||||
cls=Player(
|
||||
ctx.bot if isinstance(ctx, commands.Context) else ctx.client,
|
||||
@@ -76,7 +83,7 @@ async def connect_channel(ctx: Union[commands.Context, Interaction], channel: Vo
|
||||
if player.volume != 100:
|
||||
await player.set_volume(player.volume)
|
||||
|
||||
if ctx.bot.ipc.is_connected:
|
||||
if player.is_ipc_connected:
|
||||
await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]})
|
||||
|
||||
return player
|
||||
@@ -104,8 +111,8 @@ class Player(VoiceProtocol):
|
||||
):
|
||||
self.client: Client = client
|
||||
self._bot: Client = client
|
||||
self._ipc = self._bot.ipc
|
||||
self._ipc_connection = False
|
||||
self._ipc_client: IPCClient = self._bot.ipc_client
|
||||
self._ipc_connection: bool = False
|
||||
|
||||
self.context = ctx
|
||||
self.dj: Member = ctx.user if isinstance(ctx, Interaction) else ctx.author
|
||||
@@ -116,7 +123,7 @@ class Player(VoiceProtocol):
|
||||
self.joinTime: float = round(time.time())
|
||||
self._volume: int = self.settings.get('volume', 100)
|
||||
self.queue: Queue = QUEUE_TYPES.get(self.settings.get("queue_type", "queue").lower())(
|
||||
self.settings.get("max_queue", func.settings.max_queue),
|
||||
self.settings.get("max_queue", Config().max_queue),
|
||||
self.settings.get("duplicate_track", True), self.get_msg
|
||||
)
|
||||
|
||||
@@ -145,8 +152,9 @@ class Player(VoiceProtocol):
|
||||
self.shuffle_votes = set()
|
||||
self.stop_votes = set()
|
||||
|
||||
self._ph = Placeholders(client, self)
|
||||
self._ph = PlayerPlaceholder(client, self)
|
||||
self._logger: Optional[logging.Logger] = self._node._logger
|
||||
self._inactive_cleanup_task: Optional[asyncio.Task[None]] = None
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
@@ -230,10 +238,12 @@ class Player(VoiceProtocol):
|
||||
|
||||
@property
|
||||
def autoplay(self) -> bool:
|
||||
"""Indicates whether the player is set to autoplay."""
|
||||
return self.settings.get("autoplay", False)
|
||||
|
||||
@property
|
||||
def data(self) -> dict:
|
||||
"""Returns a dictionary containing the player's data."""
|
||||
return {
|
||||
"guild_id": self._guild.id,
|
||||
"channel_id": self.channel.id,
|
||||
@@ -252,15 +262,15 @@ class Player(VoiceProtocol):
|
||||
@property
|
||||
def is_ipc_connected(self) -> bool:
|
||||
"""Indicates whether the Inter-Process Communication (IPC) connection is active."""
|
||||
return self._ipc._is_connected and self._ipc_connection
|
||||
return self._ipc_client._is_connected and self._ipc_connection
|
||||
|
||||
def get_msg(self, *keys) -> Union[list[str], str]:
|
||||
"""Retrieves a localized message or list of messages based on the given keys
|
||||
for the guild associated with this player.
|
||||
"""
|
||||
return func.get_lang_non_async(self.guild.id, *keys)
|
||||
return LangHandler._get_lang(self.settings.get("lang"), *keys)
|
||||
|
||||
def required(self, leave=False):
|
||||
def required(self, leave: bool = False):
|
||||
"""
|
||||
Calculates the number of votes required for a specific action in the voice channel.
|
||||
|
||||
@@ -271,8 +281,7 @@ class Player(VoiceProtocol):
|
||||
|
||||
required = ceil((len(self.channel.members) - 1) / 2.5)
|
||||
if leave:
|
||||
if len(self.channel.members) == 3:
|
||||
required = 2
|
||||
required += 1
|
||||
|
||||
return required
|
||||
|
||||
@@ -291,12 +300,12 @@ class Player(VoiceProtocol):
|
||||
has 'Manage Server' permission, or meets the DJ role criteria in the settings.
|
||||
Raises an exception if `check_user_join` is True and the user is not in the channel.
|
||||
"""
|
||||
if user.id in func.settings.bot_access_user:
|
||||
if user.id in Config().bot_access_user:
|
||||
return True
|
||||
|
||||
manage_perm = user.guild_permissions.manage_guild
|
||||
if check_user_join and not self.is_user_join(user):
|
||||
raise VoicelinkException(self.get_msg('notInChannel').format(user.mention, self.channel.mention))
|
||||
raise VoicelinkException(self.get_msg('voice.connection.notInChannel').format(user.mention, self.channel.mention))
|
||||
|
||||
if 'dj' in self.settings and self.settings['dj']:
|
||||
return manage_perm or (self.settings['dj'] in [role.id for role in user.roles])
|
||||
@@ -304,10 +313,10 @@ class Player(VoiceProtocol):
|
||||
|
||||
def build_embed(self, current_track: Track = None):
|
||||
"""Builds an embed based on the current track state."""
|
||||
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
|
||||
controller = self.settings.get("default_controller", Config().controller).get("embeds", {})
|
||||
embed_form = controller.get("active" if current_track else "inactive", {})
|
||||
|
||||
return build_embed(embed_form, self._ph)
|
||||
return PlayerPlaceholder.build_embed(embed_form, self._ph)
|
||||
|
||||
async def send(self, method: RequestMethod, query: str = None, data: Union[Dict, str] = {}) -> Dict:
|
||||
"""Sends an HTTP request to the node with the given method, query, and data."""
|
||||
@@ -343,6 +352,7 @@ class Player(VoiceProtocol):
|
||||
"token": state['event']['token'],
|
||||
"endpoint": state['event']['endpoint'],
|
||||
"sessionId": state['sessionId'],
|
||||
"channelId": str(self.channel.id),
|
||||
}
|
||||
|
||||
await self.send(method=RequestMethod.PATCH, data={"voice": data})
|
||||
@@ -397,7 +407,7 @@ class Player(VoiceProtocol):
|
||||
self._paused = False
|
||||
|
||||
if self._track_is_stuck:
|
||||
await sleep(10)
|
||||
await asyncio.sleep(10)
|
||||
self._track_is_stuck = False
|
||||
|
||||
if not self.guild.me.voice:
|
||||
@@ -415,16 +425,18 @@ class Player(VoiceProtocol):
|
||||
if not track:
|
||||
if self.autoplay and await self.get_recommendations():
|
||||
return await self.do_next()
|
||||
if self.queue.is_empty:
|
||||
self._schedule_inactive_cleanup_timer()
|
||||
else:
|
||||
try:
|
||||
await self.play(track, start=track.position)
|
||||
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)
|
||||
await asyncio.sleep(5)
|
||||
return await self.do_next()
|
||||
|
||||
if not track.requester.bot:
|
||||
self._bot.loop.create_task(func.update_user(track.requester.id, {
|
||||
self._bot.loop.create_task(MongoDBHandler.update_user(track.requester.id, {
|
||||
"$push": {"history": {"$each": [track.track_id], "$slice": -25}}
|
||||
}))
|
||||
|
||||
@@ -441,10 +453,7 @@ class Player(VoiceProtocol):
|
||||
|
||||
async def invoke_controller(self):
|
||||
"""Sends or updates the music controller message in the designated channel."""
|
||||
if not self.settings.get('controller', True):
|
||||
return
|
||||
|
||||
if self._updating or not self.channel:
|
||||
if not self.settings.get('controller', True) or self._updating or not self.channel:
|
||||
return
|
||||
|
||||
self._updating = True
|
||||
@@ -453,8 +462,7 @@ class Player(VoiceProtocol):
|
||||
embed, view = self.build_embed(self.current), InteractiveController(self)
|
||||
if not self.controller:
|
||||
if request_channel_data := self.settings.get("music_request_channel"):
|
||||
channel = self.bot.get_channel(request_channel_data.get("text_channel_id"))
|
||||
if channel:
|
||||
if channel := self.bot.get_channel(request_channel_data.get("text_channel_id")):
|
||||
try:
|
||||
self.controller = await channel.fetch_message(request_channel_data.get("controller_msg_id"))
|
||||
await self.controller.edit(embed=embed, view=view)
|
||||
@@ -463,11 +471,15 @@ class Player(VoiceProtocol):
|
||||
|
||||
# Send a new controller message if none exists
|
||||
if not self.controller:
|
||||
self.controller = await func.send(self.context, content=embed, view=view, requires_fetch=True)
|
||||
self.controller = await dispatch_message(self.context, content=embed, view=view, delete_after=None, requires_fetch=True)
|
||||
|
||||
elif not await self.is_position_fresh():
|
||||
await self.controller.delete()
|
||||
self.controller = await func.send(self.context, content=embed, view=view, requires_fetch=True)
|
||||
try:
|
||||
await self.controller.delete()
|
||||
except errors.NotFound:
|
||||
self.controller = None
|
||||
|
||||
self.controller = await dispatch_message(self.context, content=embed, view=view, delete_after=None, requires_fetch=True)
|
||||
|
||||
else:
|
||||
await self.controller.edit(embed=embed, view=view)
|
||||
@@ -495,7 +507,7 @@ class Player(VoiceProtocol):
|
||||
async def teardown(self):
|
||||
"""Cleans up the player and associated resources."""
|
||||
try:
|
||||
await func.update_settings(self.guild.id, {"$set": {
|
||||
await MongoDBHandler.update_settings(self.guild.id, {"$set": {
|
||||
"last_active": (timeNow := round(time.time())),
|
||||
"played_time": round(self.settings.get("played_time", 0) + ((timeNow - self.joinTime) / 60), 2)
|
||||
}})
|
||||
@@ -507,10 +519,12 @@ class Player(VoiceProtocol):
|
||||
|
||||
try:
|
||||
await self.update_voice_status(remove_status=True)
|
||||
if self.controller and self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"):
|
||||
await self.controller.edit(embed=self.build_embed(), view=None)
|
||||
else:
|
||||
await self.controller.delete()
|
||||
self._cancel_inactive_cleanup_timer()
|
||||
if self.controller:
|
||||
if self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"):
|
||||
await self.controller.edit(embed=self.build_embed(), view=None)
|
||||
else:
|
||||
await self.controller.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -524,13 +538,16 @@ class Player(VoiceProtocol):
|
||||
query: str,
|
||||
*,
|
||||
requester: Member,
|
||||
search_type: SearchType = SearchType.YOUTUBE
|
||||
search_type: SearchType = None
|
||||
) -> Union[List[Track], Playlist]:
|
||||
"""Fetches tracks from the node's REST api to parse into Lavalink.
|
||||
|
||||
You can also pass in a discord.py Context object to get a
|
||||
Context object on any track you search.
|
||||
"""
|
||||
if not search_type:
|
||||
search_type = Config().search_platform
|
||||
|
||||
return await self._node.get_tracks(query, requester=requester, search_type=search_type)
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = True, self_mute: bool = False):
|
||||
@@ -607,17 +624,32 @@ class Player(VoiceProtocol):
|
||||
end_time = track.length
|
||||
|
||||
if start_time >= end_time:
|
||||
raise VoicelinkException(self.get_msg("invalidStartTime"))
|
||||
raise VoicelinkException(self.get_msg("time.invalidStartTime"))
|
||||
|
||||
track_length = track.length
|
||||
if not 0 <= start_time <= track_length:
|
||||
raise VoicelinkException(self.get_msg("invalidStartTime", func.time(track_length)))
|
||||
raise VoicelinkException(self.get_msg("time.invalidStartTime", format_ms(track_length)))
|
||||
if not 0 <= end_time <= track_length:
|
||||
raise VoicelinkException(self.get_msg("invalidEndTime", func.time(track_length)))
|
||||
raise VoicelinkException(self.get_msg("time.invalidEndTime", format_ms(track_length)))
|
||||
|
||||
track.position = start_time
|
||||
track.end_time = end_time
|
||||
|
||||
def _cancel_inactive_cleanup_timer(self) -> None:
|
||||
"""Cancels the per-player inactivity cleanup timer (if any)."""
|
||||
task: Optional[asyncio.Task] = getattr(self, "_inactive_cleanup_task", None)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
self._inactive_cleanup_task = None
|
||||
|
||||
def _schedule_inactive_cleanup_timer(self) -> None:
|
||||
"""Schedules per-player cleanup after inactivity."""
|
||||
self._cancel_inactive_cleanup_timer()
|
||||
seconds = Config().timer_settings.get("inactive_player_cleanup", 600)
|
||||
self._inactive_cleanup_task = self.bot.loop.create_task(
|
||||
self._inactive_cleanup_timer_worker(seconds)
|
||||
)
|
||||
|
||||
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:
|
||||
"""Adds one or more tracks to the queue."""
|
||||
tracks: List[Track] = []
|
||||
@@ -636,7 +668,7 @@ class Player(VoiceProtocol):
|
||||
_duplicate_tracks.append(track.uri)
|
||||
else:
|
||||
if raw_tracks.uri in _duplicate_tracks:
|
||||
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
|
||||
raise DuplicateTrack(self.get_msg("queue.errors.duplicateTrack"))
|
||||
|
||||
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)
|
||||
@@ -644,6 +676,8 @@ class Player(VoiceProtocol):
|
||||
|
||||
finally:
|
||||
if tracks:
|
||||
if self.channel.members and len([m for m in self.channel.members if not m.bot]) > 0:
|
||||
self._cancel_inactive_cleanup_timer()
|
||||
if self.is_ipc_connected:
|
||||
await self.send_ws({"op": "addTrack", "tracks": [track.track_id for track in tracks], "position": -1 if is_list else position}, tracks[0].requester)
|
||||
|
||||
@@ -705,7 +739,7 @@ class Player(VoiceProtocol):
|
||||
"""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'))
|
||||
raise VoicelinkException(self.get_msg('player.controls.shuffle.error'))
|
||||
|
||||
shuffle(replacement)
|
||||
self.queue.replace(queue_type, replacement)
|
||||
@@ -760,7 +794,7 @@ class Player(VoiceProtocol):
|
||||
try:
|
||||
self._filters.add_filter(filter=filter)
|
||||
except FilterTagAlreadyInUse:
|
||||
raise FilterTagAlreadyInUse(self.get_msg("filterTagAlreadyInUse"))
|
||||
raise FilterTagAlreadyInUse(self.get_msg("effects.tagInUse"))
|
||||
|
||||
payload = self._filters.get_all_payloads()
|
||||
await self.send(method=RequestMethod.PATCH, data={"filters": payload})
|
||||
@@ -847,14 +881,14 @@ class Player(VoiceProtocol):
|
||||
await self.set_pause(True)
|
||||
|
||||
async def get_recommendations(self, *, track: Optional[Track] = None) -> bool:
|
||||
"""Get recommendations from Youtube or Spotify."""
|
||||
"""Fetches and adds recommended tracks based on the provided track or recent history."""
|
||||
if not track:
|
||||
try:
|
||||
track = choice(self.queue.history(incTrack=True)[-5:])
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
tracks = await self._node.get_recommendations(track)
|
||||
|
||||
tracks = await track.get_recommendations(self._node)
|
||||
if tracks:
|
||||
await self.add_track(tracks, duplicate=False)
|
||||
|
||||
@@ -864,7 +898,7 @@ class Player(VoiceProtocol):
|
||||
|
||||
async def update_voice_status(self, remove_status: bool = False) -> None:
|
||||
"""Updates the voice status of the channel based on the specified template."""
|
||||
template = self.settings.get("stage_announce_template", func.settings.voice_status_template)
|
||||
template = self.settings.get("stage_announce_template", Config().voice_status_template)
|
||||
if not template or not self.channel:
|
||||
return
|
||||
|
||||
@@ -888,4 +922,39 @@ class Player(VoiceProtocol):
|
||||
payload['guildId'] = str(self.guild.id)
|
||||
if requester:
|
||||
payload['requesterId'] = str(requester.id)
|
||||
await self.bot.ipc.send(payload)
|
||||
await self._ipc_client.send(payload)
|
||||
|
||||
async def _inactive_cleanup_timer_worker(self, seconds: int) -> None:
|
||||
try:
|
||||
await asyncio.sleep(seconds)
|
||||
if not self._guild or not self._node:
|
||||
return
|
||||
if self._guild.id not in self._node._players:
|
||||
return
|
||||
|
||||
members = self.channel.members if self.channel else []
|
||||
has_non_bot = any(not m.bot or not m.voice.self_deaf for m in members)
|
||||
# Empty channel: always pause/teardown. If people remain: only when idle (not playing + empty queue).
|
||||
if has_non_bot and (self.is_playing or not self.queue.is_empty):
|
||||
return
|
||||
|
||||
self._inactive_cleanup_task = None
|
||||
|
||||
if self.settings.get("24/7", False):
|
||||
if not self.is_paused:
|
||||
await self.set_pause(True)
|
||||
else:
|
||||
await self.teardown()
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as e:
|
||||
# Logger is best-effort; timer cleanup should never crash the loop.
|
||||
try:
|
||||
guild_id = self._guild.id if self._guild else "unknown"
|
||||
except Exception:
|
||||
guild_id = "unknown"
|
||||
if self._logger:
|
||||
self._logger.error(
|
||||
f"Inactive cleanup timer failed for guild {guild_id}",
|
||||
exc_info=e
|
||||
)
|
||||
@@ -51,6 +51,7 @@ from .objects import Playlist, Track
|
||||
from .utils import ExponentialBackoff, NodeStats, NodeInfo, Ping
|
||||
from .enums import RequestMethod
|
||||
from .ratelimit import YTRatelimit, YTToken, STRATEGY
|
||||
from .config import Config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .player import Player
|
||||
@@ -91,6 +92,7 @@ class Node:
|
||||
self._heartbeat: int = heartbeat
|
||||
self._secure: bool = secure
|
||||
self._logger: Optional[logging.Logger] = logger
|
||||
self._stats: Optional[NodeStats] = None
|
||||
|
||||
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}"
|
||||
@@ -353,7 +355,7 @@ class Node:
|
||||
query: str,
|
||||
*,
|
||||
requester: Member,
|
||||
search_type: SearchType = SearchType.YOUTUBE
|
||||
search_type: SearchType = None
|
||||
) -> Union[List[Track], Playlist]:
|
||||
"""
|
||||
Fetches tracks from the node's REST api to parse into Lavalink.
|
||||
@@ -361,7 +363,9 @@ class Node:
|
||||
You can also pass in a discord.py Context object to get a
|
||||
Context object on any track you search.
|
||||
"""
|
||||
|
||||
if not search_type:
|
||||
search_type = Config().search_platform
|
||||
|
||||
if not URL_REGEX.match(query) and ':' not in query:
|
||||
query = f"{search_type}:{query}"
|
||||
|
||||
@@ -386,26 +390,6 @@ class Node:
|
||||
|
||||
elif load_type == "track":
|
||||
return [Track(track_id=data["encoded"], info=data["info"], requester=requester)]
|
||||
|
||||
async def get_recommendations(self, track: Track, limit: int = 20) -> List[Optional[Track]]:
|
||||
query = ""
|
||||
if track.source == "youtube":
|
||||
query = f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}"
|
||||
|
||||
elif track.source == "spotify":
|
||||
query = f"sprec:seed_tracks={track.identifier}"
|
||||
|
||||
if not query:
|
||||
return []
|
||||
|
||||
tracks = await self.get_tracks(query=query, requester=self.bot.user)
|
||||
if not track:
|
||||
return []
|
||||
|
||||
if isinstance(tracks, Playlist):
|
||||
tracks = tracks.tracks
|
||||
|
||||
return tracks[:limit] if limit else tracks
|
||||
|
||||
async def update_refresh_yt_access_token(self, token: YTToken) -> dict:
|
||||
if not self._available:
|
||||
@@ -509,7 +493,7 @@ class NodePool:
|
||||
raise NodeCreationError(f"A node with identifier '{identifier}' already exists.")
|
||||
|
||||
if not logger:
|
||||
logger = logging.getLogger("voicelink")
|
||||
logger = logging.getLogger("vocard")
|
||||
|
||||
node = Node(
|
||||
pool=cls, bot=bot, host=host, port=port, password=password,
|
||||
|
||||
@@ -21,14 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from itertools import cycle
|
||||
from typing import Optional, Tuple, Callable, Dict, List
|
||||
|
||||
from .exceptions import QueueFull, OutofList
|
||||
from .objects import Track
|
||||
from .enums import LoopType
|
||||
|
||||
from typing import Optional, Tuple, Callable, Dict, List
|
||||
from itertools import cycle
|
||||
from discord import Member
|
||||
|
||||
class LoopTypeCycle:
|
||||
def __init__(self) -> None:
|
||||
self._cycle = cycle(LoopType)
|
||||
@@ -87,33 +88,33 @@ class Queue:
|
||||
|
||||
def put(self, item: Track) -> int:
|
||||
if self.count >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
raise QueueFull(self.get_msg("queue.errors.queueFull").format(self._size))
|
||||
|
||||
self._queue.append(item)
|
||||
return self.count
|
||||
|
||||
def put_at_front(self, item: Track) -> int:
|
||||
if self.count >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
raise QueueFull(self.get_msg("queue.errors.queueFull").format(self._size))
|
||||
|
||||
self._queue.insert(self._position, item)
|
||||
return 1
|
||||
|
||||
def put_at_index(self, index: int, item: Track) -> None:
|
||||
if self.count >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
raise QueueFull(self.get_msg("queue.errors.queueFull").format(self._size))
|
||||
|
||||
return self._queue.insert(self._position - 1 + index, item)
|
||||
|
||||
def skipto(self, index: int) -> None:
|
||||
if not 0 < index <= self.count:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
raise OutofList(self.get_msg("queue.errors.outOfList"))
|
||||
else:
|
||||
self._position += index - 1
|
||||
|
||||
def backto(self, index: int) -> None:
|
||||
if not self._position - index >= 0:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
raise OutofList(self.get_msg("queue.errors.outOfList"))
|
||||
else:
|
||||
self._position -= index
|
||||
|
||||
@@ -137,11 +138,11 @@ class Queue:
|
||||
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"))
|
||||
raise OutofList(self.get_msg("queue.errors.outOfList"))
|
||||
|
||||
def move(self, target: int, to: int) -> Optional[Track]:
|
||||
if not 0 < target <= self.count or not 0 < to:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
raise OutofList(self.get_msg("queue.errors.outOfList"))
|
||||
|
||||
try:
|
||||
item = self._queue[self._position + target - 1]
|
||||
@@ -149,9 +150,9 @@ class Queue:
|
||||
self.put_at_index(to, item)
|
||||
return item
|
||||
except:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
raise OutofList(self.get_msg("queue.errors.outOfList"))
|
||||
|
||||
def remove(self, index: int, index2: int = None, member: Member = None) -> Dict[int, Track]:
|
||||
def remove(self, index: int, index2: int = None, member: discord.Member = None) -> Dict[int, Track]:
|
||||
pos = self._position - 1
|
||||
|
||||
if index2 is None:
|
||||
@@ -171,7 +172,7 @@ class Queue:
|
||||
|
||||
return removed_tracks
|
||||
except:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
raise OutofList(self.get_msg("queue.errors.outOfList"))
|
||||
|
||||
def history(self, incTrack: bool = False) -> List[Track]:
|
||||
if incTrack:
|
||||
@@ -206,7 +207,7 @@ class FairQueue(Queue):
|
||||
|
||||
def put(self, item: Track) -> int:
|
||||
if len(self._queue) >= self._size:
|
||||
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
|
||||
raise QueueFull(self.get_msg("queue.errors.queueFull").format(self._size))
|
||||
|
||||
tracks = self.tracks(incTrack=True)
|
||||
lastIndex = len(tracks)
|
||||
|
||||
@@ -22,8 +22,9 @@ SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, TYPE_CHECKING, Any
|
||||
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .pool import Node
|
||||
|
||||
@@ -24,19 +24,24 @@ SOFTWARE.
|
||||
import random
|
||||
import time
|
||||
import socket
|
||||
from timeit import default_timer as timer
|
||||
import discord
|
||||
|
||||
from itertools import zip_longest
|
||||
from typing import Dict, Optional, Union
|
||||
from timeit import default_timer as timer
|
||||
from discord.ext import commands
|
||||
|
||||
from typing import Dict, Optional
|
||||
from .mongodb import MongoDBHandler
|
||||
from .language import LangHandler
|
||||
|
||||
__all__ = [
|
||||
"ExponentialBackoff",
|
||||
"NodeStats",
|
||||
"NodeInfoVersion",
|
||||
"NodeInfo",
|
||||
"Plugin",
|
||||
"Ping"
|
||||
]
|
||||
# __all__ = [
|
||||
# "ExponentialBackoff",
|
||||
# "NodeStats",
|
||||
# "NodeInfoVersion",
|
||||
# "NodeInfo",
|
||||
# "Plugin",
|
||||
# "Ping"
|
||||
# ]
|
||||
|
||||
class ExponentialBackoff:
|
||||
"""
|
||||
@@ -203,4 +208,200 @@ class Ping:
|
||||
((self._host, self._port), None))
|
||||
s_runtime = 1000 * (cost_time)
|
||||
|
||||
return s_runtime
|
||||
return s_runtime
|
||||
|
||||
class TempCtx():
|
||||
def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None:
|
||||
self.author: discord.Member = author
|
||||
self.channel: discord.VoiceChannel = channel
|
||||
self.guild: discord.Guild = channel.guild
|
||||
|
||||
def format_to_ms(time_str: str) -> int:
|
||||
"""
|
||||
Converts a time string in one of the formats ('HH:MM:SS', 'MM:SS', 'SS') to milliseconds.
|
||||
|
||||
Args:
|
||||
time_str (str): The input time string.
|
||||
|
||||
Returns:
|
||||
int: Time in milliseconds, or 0 if parsing fails.
|
||||
"""
|
||||
formats = ['%H:%M:%S', '%M:%S', '%S']
|
||||
|
||||
for fmt in formats:
|
||||
try:
|
||||
parsed = time.strptime(time_str, fmt)
|
||||
total_seconds = parsed.tm_hour * 3600 + parsed.tm_min * 60 + parsed.tm_sec
|
||||
return total_seconds * 1000
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return 0
|
||||
|
||||
def format_ms(milliseconds: Union[float, int]) -> str:
|
||||
"""
|
||||
Converts milliseconds to a formatted time string.
|
||||
|
||||
Args:
|
||||
milliseconds (int): The time in milliseconds.
|
||||
|
||||
Returns:
|
||||
str: The formatted time string.
|
||||
|
||||
Examples:
|
||||
65000 -> "01:05"
|
||||
3723000 -> "1:02:03"
|
||||
90061000 -> "1 days, 01:01:01"
|
||||
"""
|
||||
if isinstance(milliseconds, float):
|
||||
milliseconds = int(milliseconds)
|
||||
|
||||
seconds = (milliseconds // 1000) % 60
|
||||
minutes = (milliseconds // 60_000) % 60
|
||||
hours = (milliseconds // 3_600_000) % 24
|
||||
days = milliseconds // 86_400_000
|
||||
|
||||
if days:
|
||||
return f"{days} days, {hours:02}:{minutes:02}:{seconds:02}"
|
||||
if hours:
|
||||
return f"{hours}:{minutes:02}:{seconds:02}"
|
||||
return f"{minutes:02}:{seconds:02}"
|
||||
|
||||
def format_bytes(bytes: int, unit: bool = False):
|
||||
"""
|
||||
Converts bytes to a human-readable string in MB or GB.
|
||||
Args:
|
||||
bytes (int): The number of bytes.
|
||||
unit (bool): If True, appends the unit (MB or GB) to the result.
|
||||
|
||||
Returns:
|
||||
str: The formatted string.
|
||||
"""
|
||||
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 truncate_string(text: str, length: int = 40) -> str:
|
||||
"""
|
||||
Truncates a string to a specified maximum length, appending '...' if the string exceeds that length.
|
||||
|
||||
Args:
|
||||
text (str): The input string to truncate.
|
||||
length (int, optional): The maximum allowed length of the output string, including the ellipsis. Defaults to 40.
|
||||
|
||||
Returns:
|
||||
str: The truncated string with '...' appended if it was longer than the specified length.
|
||||
"""
|
||||
return text[:length - 3] + "..." if len(text) > length else text
|
||||
|
||||
async def dispatch_message(
|
||||
ctx: Union[commands.Context, discord.Interaction, TempCtx],
|
||||
content: Union[str, discord.Embed] = None,
|
||||
*params,
|
||||
view: Optional[discord.ui.View] = None,
|
||||
file: Optional[discord.File] = None,
|
||||
delete_after: Optional[float] = discord.utils.MISSING,
|
||||
ephemeral: bool = False,
|
||||
requires_fetch: bool = False
|
||||
) -> Optional[discord.Message]:
|
||||
"""
|
||||
Dispatches a message or embed to the appropriate Discord context.
|
||||
|
||||
Args:
|
||||
ctx: The command or interaction context.
|
||||
content: The message content or embed to send.
|
||||
*params: Parameters to format the content string.
|
||||
view: Optional UI view.
|
||||
file: Optional file to attach.
|
||||
delete_after: Optional auto-delete duration.
|
||||
ephemeral: Whether the message should be ephemeral.
|
||||
requires_fetch: Whether to fetch the message after sending.
|
||||
|
||||
Returns:
|
||||
The sent message object, or None.
|
||||
"""
|
||||
if not content:
|
||||
content = "No content provided."
|
||||
|
||||
# Determine the text to send
|
||||
embed = content if isinstance(content, discord.Embed) else None
|
||||
text = None if embed else str(content).format(*params) if params else str(content)
|
||||
|
||||
# Determine the sending function
|
||||
send_func = (
|
||||
ctx.send if isinstance(ctx, commands.Context) else
|
||||
ctx.channel.send if isinstance(ctx, TempCtx) else
|
||||
ctx.followup.send if ctx.response.is_done() else
|
||||
ctx.response.send_message
|
||||
)
|
||||
|
||||
# Check settings for delete_after duration
|
||||
settings = await MongoDBHandler.get_settings(ctx.guild.id)
|
||||
send_kwargs = {
|
||||
"content": text,
|
||||
"embed": embed,
|
||||
"allowed_mentions": discord.AllowedMentions().none(),
|
||||
"silent": settings.get("silent_msg", False),
|
||||
}
|
||||
|
||||
if file:
|
||||
send_kwargs["file"] = file
|
||||
|
||||
if view:
|
||||
send_kwargs["view"] = view
|
||||
|
||||
if "delete_after" in send_func.__code__.co_varnames:
|
||||
if delete_after is discord.utils.MISSING and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"):
|
||||
delete_after = 10
|
||||
send_kwargs["delete_after"] = delete_after if delete_after is not discord.utils.MISSING else None
|
||||
|
||||
if "ephemeral" in send_func.__code__.co_varnames:
|
||||
send_kwargs["ephemeral"] = ephemeral
|
||||
|
||||
# Send the message or embed
|
||||
message = await send_func(**send_kwargs)
|
||||
|
||||
if isinstance(message, discord.InteractionCallbackResponse):
|
||||
message = message.resource
|
||||
|
||||
if requires_fetch and isinstance(message, (discord.WebhookMessage, discord.InteractionMessage)):
|
||||
message = await message.fetch()
|
||||
|
||||
return message
|
||||
|
||||
async def send_localized_message(
|
||||
ctx: Union[commands.Context, discord.Interaction, TempCtx],
|
||||
content_key: str,
|
||||
*params,
|
||||
language: str = None,
|
||||
**kwargs
|
||||
) -> Optional[discord.Message]:
|
||||
"""
|
||||
Sends a localized message using a language key and optional formatting parameters.
|
||||
|
||||
Args:
|
||||
ctx (Union[commands.Context, discord.Interaction]): The Discord context or interaction.
|
||||
content_key (str): The key used to retrieve the localized message.
|
||||
language (str, optional): Language code to override guild default. Must exist in LangHandler.get_all_languages().
|
||||
*params: Optional parameters to format the localized message.
|
||||
**kwargs: Additional keyword arguments passed to dispatch_message (e.g., view, file, delete_after, ephemeral, requires_fetch).
|
||||
|
||||
Returns:
|
||||
Optional[discord.Message]: The sent message object, or None if sending failed.
|
||||
"""
|
||||
if language and language in LangHandler.get_all_languages():
|
||||
localized_text = LangHandler._get_lang(language, content_key)
|
||||
else:
|
||||
localized_text = await LangHandler.get_lang(ctx.guild.id, content_key)
|
||||
|
||||
if localized_text:
|
||||
try:
|
||||
formatted_text = localized_text.format(*params)
|
||||
except (IndexError, KeyError):
|
||||
formatted_text = localized_text
|
||||
else:
|
||||
formatted_text = "Translation not found."
|
||||
|
||||
return await dispatch_message(ctx, content=formatted_text, **kwargs)
|
||||
@@ -21,19 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from .embed_builder import EmbedBuilderView
|
||||
from .controller import InteractiveController
|
||||
from .debug import DebugView
|
||||
from .link import LinkView
|
||||
from .inbox import InboxView
|
||||
from .playlist import PlaylistViewManager
|
||||
from .lyrics import LyricsView
|
||||
from .queue import QueueView
|
||||
from .help import HelpView
|
||||
from .search import SearchView
|
||||
from discord.ext import commands
|
||||
|
||||
class ButtonOnCooldown(commands.CommandError):
|
||||
def __init__(self, retry_after: float) -> None:
|
||||
self.retry_after = retry_after
|
||||
|
||||
from .controller import InteractiveController
|
||||
from .search import SearchView
|
||||
from .help import HelpView
|
||||
from .list import ListView
|
||||
from .lyrics import LyricsView
|
||||
from .playlist import PlaylistView
|
||||
from .inbox import InboxView
|
||||
from .link import LinkView
|
||||
from .debug import DebugView
|
||||
from .embedBuilder import EmbedBuilderView
|
||||
@@ -24,16 +24,22 @@ SOFTWARE.
|
||||
import discord
|
||||
import re
|
||||
import voicelink
|
||||
import addons
|
||||
import views
|
||||
import function as func
|
||||
|
||||
from discord.ext import commands
|
||||
from typing import Optional, Dict, Type, Union, Any
|
||||
|
||||
from ..config import Config
|
||||
from ..utils import format_ms, send_localized_message, dispatch_message
|
||||
from ..language import LangHandler
|
||||
from ..mongodb import MongoDBHandler
|
||||
|
||||
def key(interaction: discord.Interaction):
|
||||
return interaction.user
|
||||
|
||||
|
||||
class ButtonOnCooldown(commands.CommandError):
|
||||
def __init__(self, retry_after: float) -> None:
|
||||
self.retry_after = retry_after
|
||||
|
||||
class ControlButton(discord.ui.Button):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -74,7 +80,35 @@ class ControlButton(discord.ui.Button):
|
||||
|
||||
async def send(self, interaction: discord.Interaction, key: str, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None:
|
||||
stay = self.player.settings.get("controller_msg", True)
|
||||
return await func.send(
|
||||
return await send_localized_message(
|
||||
interaction, key, *params,
|
||||
view=view,
|
||||
delete_after=None if ephemeral or stay else 10,
|
||||
ephemeral=ephemeral
|
||||
)
|
||||
|
||||
async def send_embed(self, interaction: discord.Interaction, embed: discord.Embed, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None:
|
||||
stay = self.player.settings.get("controller_msg", True)
|
||||
return await dispatch_message(
|
||||
interaction, embed, *params,
|
||||
view=view,
|
||||
delete_after=None if ephemeral or stay else 10,
|
||||
ephemeral=ephemeral
|
||||
)
|
||||
|
||||
class ControlSelect(discord.ui.Select):
|
||||
def __init__(
|
||||
self,
|
||||
player: "voicelink.Player",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.player: voicelink.Player = player
|
||||
|
||||
async def send(self, interaction: discord.Interaction, key: str, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None:
|
||||
stay = self.player.settings.get("controller_msg", True)
|
||||
return await send_localized_message(
|
||||
interaction, key, *params,
|
||||
view=view,
|
||||
delete_after=None if ephemeral or stay else 10,
|
||||
@@ -91,13 +125,13 @@ class Back(ControlButton):
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.previous_votes:
|
||||
return await self.send(interaction, "voted", ephemeral=True)
|
||||
return await self.send(interaction, "voting.voted", ephemeral=True)
|
||||
else:
|
||||
self.player.previous_votes.add(interaction.user)
|
||||
if len(self.player.previous_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await self.send(interaction, "backVote", interaction.user, len(self.player.previous_votes), required)
|
||||
return await self.send(interaction, "player.controls.back.vote", interaction.user, len(self.player.previous_votes), required)
|
||||
|
||||
if not self.player.is_playing:
|
||||
self.player.queue.backto(1)
|
||||
@@ -106,7 +140,7 @@ class Back(ControlButton):
|
||||
self.player.queue.backto(2)
|
||||
await self.player.stop()
|
||||
|
||||
await self.send(interaction, "backed", interaction.user)
|
||||
await self.send(interaction, "player.controls.back.success", interaction.user)
|
||||
|
||||
if self.player.queue._repeat.mode == voicelink.LoopType.TRACK:
|
||||
await self.player.set_repeat(voicelink.LoopType.OFF)
|
||||
@@ -128,7 +162,7 @@ class PlayPause(ControlButton):
|
||||
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
if interaction.user in votes:
|
||||
return await self.send(interaction, "voted", ephemeral=True)
|
||||
return await self.send(interaction, "voting.voted", ephemeral=True)
|
||||
else:
|
||||
votes.add(interaction.user)
|
||||
if len(votes) < (required := self.player.required()):
|
||||
@@ -149,15 +183,15 @@ class Skip(ControlButton):
|
||||
if interaction.user == self.player.current.requester:
|
||||
pass
|
||||
elif interaction.user in self.player.skip_votes:
|
||||
return await self.send(interaction, "voted", ephemeral=True)
|
||||
return await self.send(interaction, "voting.voted", ephemeral=True)
|
||||
else:
|
||||
self.player.skip_votes.add(interaction.user)
|
||||
if len(self.player.skip_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await self.send(interaction, "skipVote", interaction.user, len(self.player.skip_votes), required)
|
||||
return await self.send(interaction, "player.controls.skip.vote", interaction.user, len(self.player.skip_votes), required)
|
||||
|
||||
await self.send(interaction, "skipped", interaction.user)
|
||||
await self.send(interaction, "player.controls.skip.success", interaction.user)
|
||||
|
||||
if self.player.queue._repeat.mode == voicelink.LoopType.TRACK:
|
||||
await self.player.set_repeat(voicelink.LoopType.OFF)
|
||||
@@ -170,15 +204,15 @@ class Stop(ControlButton):
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.stop_votes:
|
||||
return await self.send(interaction, "voted", ephemeral=True)
|
||||
return await self.send(interaction, "voting.voted", ephemeral=True)
|
||||
else:
|
||||
self.player.stop_votes.add(interaction.user)
|
||||
if len(self.player.stop_votes) >= (required := self.player.required(leave=True)):
|
||||
pass
|
||||
else:
|
||||
return await self.send(interaction, "leaveVote", interaction.user, len(self.player.stop_votes), required)
|
||||
return await self.send(interaction, "player.controls.leave.vote", interaction.user, len(self.player.stop_votes), required)
|
||||
|
||||
await self.send(interaction, "left", interaction.user)
|
||||
await self.send(interaction, "player.controls.leave.success", interaction.user)
|
||||
await self.player.teardown()
|
||||
|
||||
class AddFav(ControlButton):
|
||||
@@ -188,21 +222,21 @@ class AddFav(ControlButton):
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
track = self.player.current
|
||||
if not track:
|
||||
return await self.send(interaction, "noTrackPlaying")
|
||||
return await self.send(interaction, "player.errors.noTrackPlaying")
|
||||
if track.is_stream:
|
||||
return await self.send(interaction, "playlistAddError")
|
||||
user = await func.get_user(interaction.user.id, 'playlist')
|
||||
rank, max_p, max_t = func.check_roles()
|
||||
return await self.send(interaction, "playlist.errors.streamNotAllowed")
|
||||
user = await MongoDBHandler.get_user(interaction.user.id, d_type='playlist')
|
||||
_, max_t, _ = Config().get_playlist_config()
|
||||
if len(user['200']['tracks']) >= max_t:
|
||||
return await self.send(interaction, "playlistLimited", max_t, ephemeral=True)
|
||||
return await self.send(interaction, "playlist.errors.trackLimited", max_t, ephemeral=True)
|
||||
|
||||
if track.track_id in user['200']['tracks']:
|
||||
return await self.send(interaction, "playlistRepeated", ephemeral=True)
|
||||
respond = await func.update_user(interaction.user.id, {"$push": {'playlist.200.tracks': track.track_id}})
|
||||
return await self.send(interaction, "playlist.errors.trackRepeated", ephemeral=True)
|
||||
respond = await MongoDBHandler.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)
|
||||
await self.send(interaction, "playlist.actions.trackAdded", track.title, interaction.user.mention, user['200']['name'], ephemeral=True)
|
||||
else:
|
||||
await self.send(interaction, "playlistAddError2", ephemeral=True)
|
||||
await self.send(interaction, "playlist.errors.track", ephemeral=True)
|
||||
|
||||
class Loop(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -213,7 +247,7 @@ class Loop(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, 'missingModePerm', ephemeral=True)
|
||||
return await self.send(interaction, 'permissions.missingMode', ephemeral=True)
|
||||
|
||||
await self.player.set_repeat(requester=interaction.user)
|
||||
self.change_states(self.player.queue._repeat.peek_next().name)
|
||||
@@ -226,12 +260,12 @@ class VolumeUp(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, "missingFunctionPerm")
|
||||
return await self.send(interaction, "permissions.missingFunction")
|
||||
|
||||
value = value if (value := self.player.volume + 20) <= 150 else 150
|
||||
await self.player.set_volume(value, interaction.user)
|
||||
|
||||
await self.send(interaction, 'setVolume', value, ephemeral=True)
|
||||
await self.send(interaction, 'settings.actions.volumeSet', value, ephemeral=True)
|
||||
|
||||
class VolumeDown(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -239,12 +273,12 @@ class VolumeDown(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, "missingFunctionPerm")
|
||||
return await self.send(interaction, "permissions.missingFunction")
|
||||
|
||||
value = value if (value := self.player.volume - 20) >= 0 else 0
|
||||
await self.player.set_volume(value, interaction.user)
|
||||
|
||||
await self.send(interaction, 'setVolume', value, ephemeral=True)
|
||||
await self.send(interaction, 'settings.actions.volumeSet', value, ephemeral=True)
|
||||
|
||||
class VolumeMute(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -255,7 +289,7 @@ class VolumeMute(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, "missingFunctionPerm")
|
||||
return await self.send(interaction, "permissions.missingFunction")
|
||||
|
||||
is_muted = self.player.volume != 0
|
||||
value = 0 if is_muted else self.player.settings.get("volume", 100)
|
||||
@@ -269,11 +303,11 @@ class AutoPlay(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, "missingAutoPlayPerm", ephemeral=True)
|
||||
return await self.send(interaction, "permissions.missingAutoPlay", ephemeral=True)
|
||||
|
||||
check = not self.player.settings.get("autoplay", False)
|
||||
self.player.settings['autoplay'] = check
|
||||
await self.send(interaction, 'autoplay', await func.get_lang(interaction.guild_id, 'enabled' if check else "disabled"))
|
||||
await self.send(interaction, 'player.controls.autoplay', await LangHandler.get_lang(interaction.guild_id, 'common.status.enabled' if check else "common.status.disabled"))
|
||||
|
||||
if not self.player.is_playing:
|
||||
await self.player.do_next()
|
||||
@@ -285,16 +319,16 @@ class Shuffle(ControlButton):
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
if interaction.user in self.player.shuffle_votes:
|
||||
return await self.send(interaction, 'voted', ephemeral=True)
|
||||
return await self.send(interaction, 'voting.voted', ephemeral=True)
|
||||
else:
|
||||
self.player.shuffle_votes.add(interaction.user)
|
||||
if len(self.player.shuffle_votes) >= (required := self.player.required()):
|
||||
pass
|
||||
else:
|
||||
return await self.send(interaction, 'shuffleVote', interaction.user, len(self.player.shuffle_votes), required)
|
||||
return await self.send(interaction, 'player.controls.shuffle.vote', interaction.user, len(self.player.shuffle_votes), required)
|
||||
|
||||
await self.player.shuffle("queue", interaction.user)
|
||||
await self.send(interaction, 'shuffled')
|
||||
await self.send(interaction, 'player.controls.shuffle.success')
|
||||
|
||||
class Forward(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -305,15 +339,15 @@ class Forward(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, 'missingPosPerm', ephemeral=True)
|
||||
return await self.send(interaction, 'permissions.missingPosition', ephemeral=True)
|
||||
|
||||
if not self.player.current:
|
||||
return await self.send(interaction, 'noTrackPlaying', ephemeral=True)
|
||||
return await self.send(interaction, 'player.errors.noTrackPlaying', ephemeral=True)
|
||||
|
||||
position = int(self.player.position + 10000)
|
||||
|
||||
await self.player.seek(position)
|
||||
await self.send(interaction, 'forward', func.time(position))
|
||||
await self.send(interaction, 'player.controls.forward', format_ms(position))
|
||||
|
||||
class Rewind(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -324,15 +358,15 @@ class Rewind(ControlButton):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await self.send(interaction, 'missingPosPerm', ephemeral=True)
|
||||
return await self.send(interaction, 'permissions.missingPosition', ephemeral=True)
|
||||
|
||||
if not self.player.current:
|
||||
return await self.send(interaction, 'noTrackPlaying', ephemeral=True)
|
||||
return await self.send(interaction, 'player.errors.noTrackPlaying', ephemeral=True)
|
||||
|
||||
position = 0 if (value := int(self.player.position - 30000)) <= 0 else value
|
||||
|
||||
await self.player.seek(position)
|
||||
await self.send(interaction, 'rewind', func.time(position))
|
||||
await self.send(interaction, 'player.controls.rewind', format_ms(position))
|
||||
|
||||
class Lyrics(ControlButton):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -342,36 +376,38 @@ class Lyrics(ControlButton):
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
from . import LyricsView
|
||||
if not self.player or not self.player.is_playing:
|
||||
return await self.send(interaction, "noTrackPlaying", ephemeral=True)
|
||||
return await self.send(interaction, "player.errors.noTrackPlaying", ephemeral=True)
|
||||
|
||||
await interaction.response.defer()
|
||||
|
||||
title = self.player.current.title
|
||||
artist = self.player.current.author
|
||||
|
||||
lyrics_platform = addons.LYRICS_PLATFORMS.get(func.settings.lyrics_platform)
|
||||
lyrics_platform = voicelink.LYRICS_PLATFORMS.get(Config().lyrics_platform)
|
||||
if lyrics_platform:
|
||||
lyrics = await lyrics_platform().get_lyrics(title, artist)
|
||||
if not lyrics:
|
||||
return await self.send(interaction, "lyricsNotFound", ephemeral=True)
|
||||
return await self.send(interaction, "lyrics.notFound", ephemeral=True)
|
||||
|
||||
view = views.LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user)
|
||||
view.response = await self.send(interaction, view.build_embed(), view=view, ephemeral=True)
|
||||
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user)
|
||||
view.response = await self.send_embed(interaction, await view.build_embed(), view=view, ephemeral=True)
|
||||
|
||||
class Tracks(discord.ui.Select):
|
||||
class Tracks(ControlSelect):
|
||||
def __init__(self, player: "voicelink.Player", btn_data, **kwargs):
|
||||
self.player: voicelink.Player = player
|
||||
|
||||
if player.queue.is_empty:
|
||||
raise ValueError("Player queue is empty, cannot create Tracks row instance.")
|
||||
|
||||
options = []
|
||||
for index, track in enumerate(self.player.queue.tracks(), start=1):
|
||||
for index, track in enumerate(player.queue.tracks(), start=1):
|
||||
if index > min(max(btn_data.get("max_options", 10), 1), 25):
|
||||
break
|
||||
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=self.player._ph.replace(btn_data.get("label"), {}),
|
||||
placeholder=player._ph.replace(btn_data.get("label"), {}),
|
||||
player=player,
|
||||
options=options,
|
||||
disabled=player.queue.is_empty,
|
||||
**kwargs
|
||||
@@ -379,47 +415,49 @@ class Tracks(discord.ui.Select):
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await func.send(interaction, "missingFunctionPerm", ephemeral=True)
|
||||
return await self.send(interaction, "permissions.missingFunction", ephemeral=True)
|
||||
|
||||
await interaction.response.defer()
|
||||
|
||||
self.player.queue.skipto(int(self.values[0].split(". ")[0]))
|
||||
await self.player.stop()
|
||||
|
||||
if self.player.settings.get("controller_msg", True):
|
||||
await func.send(interaction, "skipped", interaction.user)
|
||||
await self.send(interaction, "player.controls.skip.success", interaction.user)
|
||||
|
||||
class Effects(discord.ui.Select):
|
||||
class Effects(ControlSelect):
|
||||
def __init__(self, player: "voicelink.Player", btn_data, 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._ph.replace(btn_data.get("label"), {}),
|
||||
placeholder=player._ph.replace(btn_data.get("label"), {}),
|
||||
player=player,
|
||||
options=options,
|
||||
row=row
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
if not self.player.is_privileged(interaction.user):
|
||||
return await func.send(interaction, "missingFunctionPerm", ephemeral=True)
|
||||
return await self.send(interaction, "permissions.missingFunction", ephemeral=True)
|
||||
|
||||
await interaction.response.defer()
|
||||
|
||||
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")
|
||||
return await self.send(interaction, "effects.cleared")
|
||||
|
||||
selected_filter = avalibable_filters.get(self.values[0].lower())()
|
||||
selected_filter: voicelink.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")
|
||||
await self.send(interaction, "effects.cleared")
|
||||
else:
|
||||
await self.player.add_filter(selected_filter, requester=interaction.user)
|
||||
await func.send(interaction, "addEffect", selected_filter.tag)
|
||||
await self.send(interaction, "effects.added", selected_filter.tag)
|
||||
|
||||
BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = {
|
||||
BUTTON_TYPE: Dict[str, Type[Union[ControlButton, ControlSelect]]] = {
|
||||
"back": Back,
|
||||
"play-pause": PlayPause,
|
||||
"skip": Skip,
|
||||
@@ -439,11 +477,11 @@ BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = {
|
||||
}
|
||||
|
||||
class InteractiveController(discord.ui.View):
|
||||
def __init__(self, player):
|
||||
def __init__(self, player: "voicelink.Player"):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
self.player: voicelink.Player = player
|
||||
for row_num, btn_row in enumerate(func.settings.controller.get("buttons")):
|
||||
for row_num, btn_row in enumerate(Config().controller.get("buttons")):
|
||||
for btn_name, btn_data in btn_row.items():
|
||||
btn_class = BUTTON_TYPE.get(btn_name.lower())
|
||||
if not btn_class:
|
||||
@@ -458,27 +496,27 @@ class InteractiveController(discord.ui.View):
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction):
|
||||
if not self.player.node._available:
|
||||
await func.send(interaction, "nodeReconnect", ephemeral=True)
|
||||
await send_localized_message(interaction, "player.errors.nodeReconnect", ephemeral=True)
|
||||
return False
|
||||
|
||||
if interaction.user.id in func.settings.bot_access_user:
|
||||
if interaction.user.id in Config().bot_access_user:
|
||||
return True
|
||||
|
||||
if self.player.channel and self.player.is_user_join(interaction.user):
|
||||
retry_after = self.cooldown.update_rate_limit(interaction)
|
||||
if retry_after:
|
||||
raise views.ButtonOnCooldown(retry_after)
|
||||
raise ButtonOnCooldown(retry_after)
|
||||
return True
|
||||
else:
|
||||
await func.send(interaction, "notInChannel", interaction.user.mention, self.player.channel.mention, ephemeral=True)
|
||||
await send_localized_message(interaction, "voice.connection.notInChannel", interaction.user.mention, self.player.channel.mention, ephemeral=True)
|
||||
return False
|
||||
|
||||
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
|
||||
if isinstance(error, views.ButtonOnCooldown):
|
||||
if isinstance(error, ButtonOnCooldown):
|
||||
sec = int(error.retry_after)
|
||||
await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True)
|
||||
return await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True)
|
||||
|
||||
elif isinstance(error, Exception):
|
||||
await interaction.response.send_message(error)
|
||||
|
||||
return
|
||||
if isinstance(error, voicelink.VoicelinkException):
|
||||
return await interaction.response.send_message(error, ephemeral=True)
|
||||
|
||||
await super().on_error(interaction, error, item)
|
||||
@@ -24,96 +24,20 @@ SOFTWARE.
|
||||
import discord
|
||||
import io
|
||||
import os
|
||||
import json
|
||||
import contextlib
|
||||
import textwrap
|
||||
import traceback
|
||||
import voicelink
|
||||
import function as func
|
||||
|
||||
from typing import Optional
|
||||
from discord.ext import commands
|
||||
|
||||
class ExecuteModal(discord.ui.Modal):
|
||||
def __init__(self, code: str, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code: str = code
|
||||
from .utils import BaseModal
|
||||
from ..config import Config
|
||||
from ..utils import format_ms, format_bytes
|
||||
|
||||
self.add_item(
|
||||
discord.ui.TextInput(
|
||||
label="Code Runner",
|
||||
placeholder="Input Your Code",
|
||||
style=discord.TextStyle.long,
|
||||
default=self.code
|
||||
)
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
await interaction.response.defer()
|
||||
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,
|
||||
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
|
||||
@@ -207,11 +131,21 @@ class ExecutePanel(discord.ui.View):
|
||||
await self.message.edit(view=self)
|
||||
|
||||
async def execute(self, interaction: discord.Interaction):
|
||||
modal = ExecuteModal(self.code, title="Enter Your Code")
|
||||
modal = BaseModal(
|
||||
title="Enter Your Code",
|
||||
custom_id="execute_code_modal",
|
||||
items=[discord.ui.TextInput(
|
||||
label="Code Runner",
|
||||
placeholder="Input Your Code",
|
||||
style=discord.TextStyle.long,
|
||||
custom_id="code_runner",
|
||||
default=self.code
|
||||
)]
|
||||
)
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
if not (code := modal.code):
|
||||
if not (code := modal.values.get("code_runner")):
|
||||
return
|
||||
|
||||
self._error = None
|
||||
@@ -278,7 +212,7 @@ class NodesPanel(discord.ui.View):
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
self.update_btn_status()
|
||||
embed = discord.Embed(title="📡 Nodes Panel", color=func.settings.embed_color)
|
||||
embed = discord.Embed(title="📡 Nodes Panel", color=Config().embed_color)
|
||||
|
||||
if not voicelink.NodePool._nodes:
|
||||
embed.description = "```There are no nodes are connected!```"
|
||||
@@ -288,16 +222,16 @@ class NodesPanel(discord.ui.View):
|
||||
if self.selected_node and self.selected_node._identifier != node._identifier:
|
||||
continue
|
||||
|
||||
if node._available:
|
||||
if node._available and node.stats:
|
||||
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"• 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)}```"
|
||||
f"• UPTIME: {format_ms(node.stats.uptime)}```"
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
@@ -313,9 +247,61 @@ class NodesPanel(discord.ui.View):
|
||||
|
||||
@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")
|
||||
modal = BaseModal(
|
||||
title="Create Node",
|
||||
custom_id="add_node_modal",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label="Host",
|
||||
placeholder="Enter the lavalink host e.g 0.0.0.0",
|
||||
custom_id="host"
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Port",
|
||||
placeholder="Enter the lavalink port e.g 2333",
|
||||
custom_id="port"
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Password",
|
||||
placeholder="Enter the lavalink password",
|
||||
custom_id="password"
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Secure",
|
||||
placeholder="Specify if your Lavalink uses SSL. Enter 'true' or 'false'",
|
||||
custom_id="secure",
|
||||
default="false"
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Identifier",
|
||||
placeholder="Enter a name for your lavalink server",
|
||||
custom_id="identifier"
|
||||
)
|
||||
]
|
||||
)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
try:
|
||||
config = {
|
||||
"host": v["host"],
|
||||
"port": int(v["port"]),
|
||||
"password": v["password"],
|
||||
"secure": v["secure"].startswith("t"),
|
||||
"identifier": v["identifier"]
|
||||
}
|
||||
except Exception:
|
||||
return await interaction.response.send_message("Some of your input is invalid! Please try again.", ephemeral=True)
|
||||
|
||||
try:
|
||||
await voicelink.NodePool.create_node(bot=interaction.client, **config)
|
||||
await interaction.followup.send(f"Node {v['identifier']} is connected!", ephemeral=True)
|
||||
await self.message.edit(embed=self.build_embed(), view=self)
|
||||
|
||||
except Exception as e:
|
||||
return await interaction.followup.send(e, ephemeral=True)
|
||||
|
||||
@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:
|
||||
@@ -405,9 +391,9 @@ class DebugView(discord.ui.View):
|
||||
except:
|
||||
pass
|
||||
|
||||
session_file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
|
||||
if os.path.exists(session_file_path):
|
||||
os.remove(session_file_path)
|
||||
if os.path.exists(Config.LAST_SESSION_FILE_DIR):
|
||||
os.remove(Config.LAST_SESSION_FILE_DIR)
|
||||
|
||||
func.update_json(func.LAST_SESSION_FILE_NAME, player_data)
|
||||
with open(Config.LAST_SESSION_FILE_DIR, "w", encoding="utf8") as f:
|
||||
json.dump(player_data, f, ensure_ascii=False, indent=4)
|
||||
await interaction.client.close()
|
||||
359
voicelink/views/embed_builder.py
Normal file
359
voicelink/views/embed_builder.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import copy
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from .utils import BaseModal
|
||||
from ..mongodb import MongoDBHandler
|
||||
from ..placeholders import PlayerPlaceholder
|
||||
|
||||
|
||||
class Dropdown(discord.ui.Select):
|
||||
def __init__(self):
|
||||
options = [
|
||||
discord.SelectOption(label='Active', description='The controller embed when music is playing', emoji='🟩'),
|
||||
discord.SelectOption(label='Inactive', description='The controller embed when music is not playing', emoji='🟥'),
|
||||
]
|
||||
super().__init__(placeholder='Select a embed to edit...', min_values=1, max_values=1, options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
self.view.embed_type = self.values[0].lower()
|
||||
if self.view.embed_type not in self.view.data:
|
||||
self.view.data[self.view.embed_type] = {}
|
||||
|
||||
await interaction.response.edit_message(embed=self.view.build_embed())
|
||||
|
||||
class EmbedBuilderView(discord.ui.View):
|
||||
def __init__(self, context: commands.Context, placeholder: PlayerPlaceholder, data: dict) -> None:
|
||||
super().__init__(timeout=300)
|
||||
self.add_item(Dropdown())
|
||||
|
||||
self.author: discord.Member = context.author
|
||||
self.ph: PlayerPlaceholder = placeholder
|
||||
self.response: discord.Message = None
|
||||
|
||||
self.original_data: dict = copy.deepcopy(data)
|
||||
self.data: dict = copy.deepcopy(data)
|
||||
self.embed_type: str = "active"
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
return PlayerPlaceholder.build_embed(self.data.get(self.embed_type, {}), self.ph)
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction):
|
||||
return interaction.user == self.author
|
||||
|
||||
@discord.ui.button(label="Edit Content", style=discord.ButtonStyle.blurple)
|
||||
async def edit_content(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embed_type, {})
|
||||
modal = BaseModal(
|
||||
title="Edit Content",
|
||||
custom_id="edit_content",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label="Title",
|
||||
placeholder="The title of the embed",
|
||||
style=discord.TextStyle.paragraph,
|
||||
custom_id="title",
|
||||
max_length=1000,
|
||||
default=data.get("title", {}).get("name"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Url",
|
||||
placeholder="The url of the title",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="url",
|
||||
max_length=100,
|
||||
default=data.get("title", {}).get("url"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Color",
|
||||
placeholder="The color of the embed",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="color",
|
||||
max_length=100,
|
||||
default=data.get("color"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Description",
|
||||
placeholder="The description of the title",
|
||||
style=discord.TextStyle.paragraph,
|
||||
custom_id="description",
|
||||
max_length=200,
|
||||
default=data.get("description"),
|
||||
required=False
|
||||
)
|
||||
])
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
try:
|
||||
data["description"] = v["description"]
|
||||
data["color"] = int(v["color"], 16)
|
||||
|
||||
if "title" not in data:
|
||||
data["title"] = {}
|
||||
|
||||
data["title"]["name"] = v['title']
|
||||
data["title"]["url"] = v['url']
|
||||
except:
|
||||
pass
|
||||
|
||||
return await self.response.edit(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Edit Author",)
|
||||
async def edit_author(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embed_type, {})
|
||||
modal = BaseModal(
|
||||
title="Edit Author",
|
||||
custom_id="edit_author",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label="Name",
|
||||
placeholder="The name of the author",
|
||||
style=discord.TextStyle.paragraph,
|
||||
custom_id="name",
|
||||
max_length=200,
|
||||
default=data.get("author", {}).get("name"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Url",
|
||||
placeholder="The url of the author",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="url",
|
||||
max_length=100,
|
||||
default=data.get("author", {}).get("url"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Icon Url",
|
||||
placeholder="The icon url of the author",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="icon_url",
|
||||
max_length=100,
|
||||
default=data.get("author", {}).get("icon_url"),
|
||||
required=False
|
||||
),
|
||||
])
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
if v['name'] != "":
|
||||
if "author" not in data:
|
||||
data["author"] = {}
|
||||
|
||||
data["author"]["name"] = v['name']
|
||||
data["author"]["url"] = v['url']
|
||||
data["author"]["icon_url"] = v['icon_url']
|
||||
else:
|
||||
del data["author"]
|
||||
|
||||
return await self.response.edit(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Edit Image")
|
||||
async def edit_image(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embed_type, {})
|
||||
modal = BaseModal(
|
||||
title="Edit Image",
|
||||
custom_id="edit_image",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label="Thumbnail",
|
||||
placeholder="The url of the thumbnail",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="thumbnail",
|
||||
max_length=200,
|
||||
default=data.get("thumbnail"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Image",
|
||||
placeholder="The url of the image",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="image",
|
||||
max_length=100,
|
||||
default=data.get("image"),
|
||||
required=False
|
||||
)
|
||||
])
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
data["thumbnail"] = v['thumbnail']
|
||||
data["image"] = v['image']
|
||||
|
||||
return await self.response.edit(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Edit Footer")
|
||||
async def edit_footer(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embed_type, {})
|
||||
modal = BaseModal(
|
||||
title="Edit Footer",
|
||||
custom_id="edit_footer",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label="Text",
|
||||
placeholder="The text of the footer",
|
||||
style=discord.TextStyle.paragraph,
|
||||
custom_id="text",
|
||||
max_length=200,
|
||||
default=data.get("footer", {}).get("text"),
|
||||
required=False
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Icon Url",
|
||||
placeholder="The url of the icon",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="icon_url",
|
||||
max_length=100,
|
||||
default=data.get("footer", {}).get("icon_url"),
|
||||
required=False
|
||||
)
|
||||
])
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
if "footer" not in data:
|
||||
data["footer"] = {}
|
||||
|
||||
data["footer"]["text"] = v['text']
|
||||
data["footer"]["icon_url"] = v['icon_url']
|
||||
|
||||
return await self.response.edit(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Add Field", style=discord.ButtonStyle.green, row=1)
|
||||
async def add_field(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embed_type)
|
||||
if "fields" not in data:
|
||||
data["fields"] = []
|
||||
|
||||
if len(data["fields"]) >= 25:
|
||||
return await interaction.response.send_message("You have already reached the maximum of fields!", ephemeral=True)
|
||||
|
||||
modal = BaseModal(
|
||||
title="Add Field",
|
||||
custom_id="add_field",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label="Name",
|
||||
placeholder="The name of the field",
|
||||
style=discord.TextStyle.paragraph,
|
||||
custom_id="name",
|
||||
max_length=256
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Value",
|
||||
placeholder="The value of the field",
|
||||
style=discord.TextStyle.long,
|
||||
custom_id="value",
|
||||
max_length=1024
|
||||
),
|
||||
discord.ui.TextInput(
|
||||
label="Inline",
|
||||
placeholder="The inline of the field, e.g. True or False",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="inline",
|
||||
)
|
||||
])
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
v = modal.values
|
||||
data["fields"].append({
|
||||
"name": v["name"],
|
||||
"value": v["value"],
|
||||
"inline": True if v["inline"].lower() == "true" else False
|
||||
})
|
||||
|
||||
return await self.response.edit(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Remove Field", style=discord.ButtonStyle.red, row=1)
|
||||
async def remove_field(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
data = self.data.get(self.embed_type)
|
||||
if "fields" not in data:
|
||||
data["fields"] = []
|
||||
|
||||
if len(data["fields"]) == 0:
|
||||
return await interaction.response.send_message("There are no fields to remove!", ephemeral=True)
|
||||
|
||||
modal = BaseModal(
|
||||
title="Remove Field",
|
||||
custom_id="remove_field",
|
||||
items = [
|
||||
discord.ui.TextInput(
|
||||
label="Index",
|
||||
placeholder="The number of fields to remove, e.g. 1",
|
||||
style=discord.TextStyle.short,
|
||||
custom_id="index",
|
||||
)
|
||||
])
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
try:
|
||||
del data["fields"][int(modal.values["index"])]
|
||||
except:
|
||||
return await interaction.followup.send("Can't found the field", ephemeral=True)
|
||||
|
||||
return await self.response.edit(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(label="Apply", style=discord.ButtonStyle.green, row=1)
|
||||
async def apply(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await MongoDBHandler.update_settings(
|
||||
interaction.guild_id,
|
||||
{"$set": {"default_controller.embeds": self.data}},
|
||||
)
|
||||
|
||||
await self.on_timeout()
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Reset", style=discord.ButtonStyle.red, row=1)
|
||||
async def reset(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
self.data.update(copy.deepcopy(self.original_data))
|
||||
return await interaction.response.edit_message(embed=self.build_embed())
|
||||
|
||||
@discord.ui.button(emoji='🗑️', row=1)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ SOFTWARE.
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import function as func
|
||||
from ..config import Config
|
||||
|
||||
class HelpDropdown(discord.ui.Select):
|
||||
def __init__(self, categories:list):
|
||||
def __init__(self, categories: list[str]) -> None:
|
||||
self.view: HelpView
|
||||
|
||||
super().__init__(
|
||||
@@ -61,9 +61,6 @@ class HelpView(discord.ui.View):
|
||||
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.categories))
|
||||
|
||||
async def on_error(self, error, item, interaction) -> None:
|
||||
return
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for child in self.children:
|
||||
@@ -80,20 +77,20 @@ class HelpView(discord.ui.View):
|
||||
def build_embed(self, category: str) -> discord.Embed:
|
||||
category = category.lower()
|
||||
if category == "news":
|
||||
embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=func.settings.embed_color)
|
||||
embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=Config().embed_color)
|
||||
embed.add_field(
|
||||
name=f"Available Categories: [{2 + len(self.categories)}]",
|
||||
value="```py\n👉 News\n2. Tutorial\n{}```".format("".join(f"{i}. {c}\n" for i, c in enumerate(self.categories, start=3))),
|
||||
inline=True
|
||||
)
|
||||
|
||||
update = "Vocard is a simple music bot. It leads to a comfortable experience which is user-friendly, It supports YouTube, Soundcloud, Spotify, Twitch and more!"
|
||||
update = "Vocard is a simple music bot. It leads to a comfortable experience which is user-friendly, It supports Soundcloud, Spotify, Twitch and more!"
|
||||
embed.add_field(name="📰 Information:", value=update, inline=True)
|
||||
embed.add_field(name="Get Started", value="```Join a voice channel and /play {Song/URL} a song. (Names, Youtube Video Links or Playlist links or Spotify links are supported on Vocard)```", inline=False)
|
||||
embed.add_field(name="Get Started", value="```Join a voice channel and /play {Song/URL} a song. (Names, Video Links or Playlist links are supported on Vocard)```", inline=False)
|
||||
|
||||
return embed
|
||||
|
||||
embed = discord.Embed(title=f"Category: {category.capitalize()}", color=func.settings.embed_color)
|
||||
embed = discord.Embed(title=f"Category: {category.capitalize()}", color=Config().embed_color)
|
||||
embed.add_field(name=f"Categories: [{2 + len(self.categories)}]", value="```py\n" + "\n".join(("👉 " if c == category.capitalize() else f"{i}. ") + c for i, c in enumerate(['News', 'Tutorial'] + self.categories, start=1)) + "```", inline=True)
|
||||
|
||||
if category == 'tutorial':
|
||||
@@ -21,11 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord, time
|
||||
import function as func
|
||||
import discord
|
||||
import time
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..config import Config
|
||||
|
||||
class Select_message(discord.ui.Select):
|
||||
def __init__(self, inbox):
|
||||
self.view: InboxView
|
||||
@@ -59,7 +61,7 @@ class InboxView(discord.ui.View):
|
||||
embed=discord.Embed(
|
||||
title=f"📭 All {self.author.display_name}'s Inbox",
|
||||
description=f'Max Messages: {len(self.inbox)}/10' + '```%0s %2s %20s\n' % (" ", "ID:", "Title:") + '\n'.join('%0s %2s. %35s'% ('✉️' if mail['type'] == 'invite' else '📢', index, mail['title'][:35] + "...") for index, mail in enumerate(self.inbox, start=1)) + '```',
|
||||
color=func.settings.embed_color
|
||||
color=Config().embed_color
|
||||
)
|
||||
|
||||
if self.current:
|
||||
@@ -26,4 +26,4 @@ import discord
|
||||
class LinkView(discord.ui.View):
|
||||
def __init__(self, label=None, emoji=None, url=None):
|
||||
super().__init__(timeout=60)
|
||||
self.add_item(discord.ui.Button(label=label, emoji=emoji, url=url))
|
||||
self.add_item(discord.ui.Button(label=label, emoji=emoji, url=url))
|
||||
81
voicelink/views/lyrics.py
Normal file
81
voicelink/views/lyrics.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from .utils import Pagination
|
||||
from .pagination import PaginationView
|
||||
from ..config import Config
|
||||
from ..language import LangHandler
|
||||
|
||||
# class LyricsDropdown(discord.ui.Select):
|
||||
# def __init__(self, langs: list[str]) -> None:
|
||||
# self.view: LyricsView
|
||||
|
||||
# super().__init__(
|
||||
# placeholder="Select A Lyrics Translation",
|
||||
# min_values=1, max_values=1,
|
||||
# options=[discord.SelectOption(label=lang) for lang in langs],
|
||||
# custom_id="selectLyricsLangs"
|
||||
# )
|
||||
|
||||
# async def callback(self, interaction: discord.Interaction) -> None:
|
||||
# self.view.lang = self.values[0]
|
||||
# self.view.current_page = 1
|
||||
# self.view.pages = len(self.view.source.get(self.values[0]))
|
||||
# await interaction.response.edit_message(embed=self.view.build_embed())
|
||||
|
||||
class LyricsView(PaginationView):
|
||||
def __init__(self, name: str, source: dict, author: discord.Member) -> None:
|
||||
self.name: str = name
|
||||
self.author: discord.Member = author
|
||||
self.response: discord.Message = None
|
||||
|
||||
super().__init__(
|
||||
Pagination[str](source.get("default"), page_size=1),
|
||||
author
|
||||
)
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def build_embed(self) -> discord.Embed:
|
||||
page = self.pagination.get_current_page_items()
|
||||
text = await LangHandler.get_lang(self.author.guild.id, "search.title")
|
||||
embed=discord.Embed(description=page[0], color=Config().embed_color)
|
||||
embed.set_author(name=text.format(self.name), icon_url=self.author.display_avatar.url)
|
||||
return embed
|
||||
|
||||
async def update_message(self, interaction: discord.Interaction) -> None:
|
||||
"""Update the view and edit the message."""
|
||||
self.update_view()
|
||||
|
||||
if interaction.response.is_done():
|
||||
await interaction.followup.edit_message(self.response.id, embed=await self.build_embed(), view=self)
|
||||
else:
|
||||
await interaction.response.edit_message(embed=await self.build_embed(), view=self)
|
||||
129
voicelink/views/pagination.py
Normal file
129
voicelink/views/pagination.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from .utils import Pagination, BaseModal
|
||||
from ..language import LangHandler
|
||||
from ..mongodb import MongoDBHandler
|
||||
|
||||
class PaginationView(discord.ui.View):
|
||||
def __init__(self, pagination: Pagination, author: discord.Member, timeout: float = 300):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.pagination: Pagination = pagination
|
||||
self.author: discord.Member = author
|
||||
self.lang: str = MongoDBHandler.get_cached_settings(author.guild.id).get("lang")
|
||||
self.update_view()
|
||||
|
||||
def update_view(self, extra_states: Optional[Dict[str, Dict[str, str]]] = None) -> None:
|
||||
"""Update button states and labels based on the current pagination state."""
|
||||
texts = LangHandler._get_lang(self.lang, "pagination.prev", "pagination.next")
|
||||
button_states = {
|
||||
"fast_back": {
|
||||
"disabled": self.pagination.current_page <= 1,
|
||||
"label": "<<",
|
||||
},
|
||||
"back": {
|
||||
"disabled": not self.pagination.has_previous_page,
|
||||
"label": texts[0],
|
||||
},
|
||||
"next": {
|
||||
"disabled": not self.pagination.has_next_page,
|
||||
"label": texts[1],
|
||||
},
|
||||
"fast_next": {
|
||||
"disabled": self.pagination.current_page >= self.pagination.total_pages - 1,
|
||||
"label": ">>",
|
||||
},
|
||||
"page_number": {
|
||||
"disabled": False,
|
||||
"label": f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}",
|
||||
},
|
||||
}
|
||||
|
||||
if extra_states:
|
||||
button_states.update(extra_states)
|
||||
|
||||
# Update button states and labels
|
||||
for child in self.children:
|
||||
if (state := button_states.get(child.custom_id)):
|
||||
child.disabled = state.get("disabled", False)
|
||||
child.label = state.get("label")
|
||||
|
||||
async def update_message(self, interaction: discord.Interaction) -> None:
|
||||
"""Update the view and edit the message."""
|
||||
|
||||
@discord.ui.button(label='<<', custom_id="fast_back")
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Jump to the first page."""
|
||||
self.pagination.go_page(0)
|
||||
await self.update_message(interaction)
|
||||
|
||||
@discord.ui.button(label='Back', custom_id="back", style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Go to the previous page if it exists."""
|
||||
self.pagination.go_back()
|
||||
await self.update_message(interaction)
|
||||
|
||||
@discord.ui.button(label="--/--", custom_id="page_number", style=discord.ButtonStyle.blurple)
|
||||
async def page_number(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Display current page number."""
|
||||
texts = LangHandler._get_lang(self.lang, "pagination.page.title", "pagination.page.field")
|
||||
modal = BaseModal(
|
||||
title=texts[0],
|
||||
custom_id="page_number_modal",
|
||||
items=[
|
||||
discord.ui.TextInput(
|
||||
label=texts[1],
|
||||
custom_id="page_number",
|
||||
placeholder="e.g. 1",
|
||||
default=str(self.pagination.current_page),
|
||||
max_length=5,
|
||||
required=True
|
||||
)
|
||||
]
|
||||
)
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
page_number = modal.values.get("page_number")
|
||||
if not page_number or not page_number.isdigit():
|
||||
return
|
||||
|
||||
self.pagination.go_page(int(page_number) - 1)
|
||||
await self.update_message(interaction)
|
||||
|
||||
@discord.ui.button(label='Next', custom_id="next", style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Go to the next page if it exists."""
|
||||
self.pagination.go_next()
|
||||
await self.update_message(interaction)
|
||||
|
||||
@discord.ui.button(label='>>', custom_id="fast_next")
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Jump to the last page."""
|
||||
self.pagination.go_page(self.pagination.total_pages - 1)
|
||||
await self.update_message(interaction)
|
||||
300
voicelink/views/playlist.py
Normal file
300
voicelink/views/playlist.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
import unicodedata
|
||||
|
||||
from tldextract import extract
|
||||
from discord.ext import commands
|
||||
from typing import Any
|
||||
|
||||
from .utils import DynamicViewManager, Pagination, BaseModal
|
||||
from .pagination import PaginationView
|
||||
from ..config import Config
|
||||
from ..utils import format_ms, truncate_string, dispatch_message
|
||||
from ..mongodb import MongoDBHandler
|
||||
from ..language import LangHandler
|
||||
from ..exceptions import VoicelinkException
|
||||
|
||||
class PlaylistDropdown(discord.ui.Select):
|
||||
def __init__(self, results: list[dict[str, Any]], lang: str) -> None:
|
||||
self.view: PlaylistViewManager
|
||||
|
||||
super().__init__(
|
||||
placeholder=LangHandler._get_lang(lang, "playlist.view.playlistSelect"),
|
||||
custom_id="selector",
|
||||
options=[
|
||||
discord.SelectOption(
|
||||
emoji=playlist['emoji'],
|
||||
label=f'{index}. {playlist["name"]}',
|
||||
value=playlist["id"],
|
||||
description=f"{playlist['time']} · {playlist['type']}"
|
||||
) for index, playlist in enumerate(results, start=1) if playlist['type'] != 'error'
|
||||
]
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
view: PlaylistView = self.view.change_view(self.values[0])
|
||||
await interaction.response.edit_message(embed=view.build_embed(), view=view)
|
||||
|
||||
|
||||
class PlaylistView(PaginationView):
|
||||
def __init__(
|
||||
self,
|
||||
primary_view: "PlaylistViewManager",
|
||||
playlist_data: dict[str, Any]
|
||||
) -> None:
|
||||
self.primary_view: PlaylistViewManager = primary_view
|
||||
self.author: discord.Member = primary_view.ctx.author
|
||||
|
||||
self.playlist_id: str = playlist_data.get("id")
|
||||
self.emoji: str = playlist_data.get("emoji")
|
||||
self.name: str = playlist_data.get("name")
|
||||
self.time: str = playlist_data.get("time")
|
||||
self.type: str = playlist_data.get("type")
|
||||
self.owner_id: int = playlist_data.get("owner")
|
||||
self.perms: dict[str, list[int]] = playlist_data.get("perms")
|
||||
|
||||
super().__init__(Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7), primary_view.ctx.author)
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
"""Build the embed for the current page of tracks."""
|
||||
tracks = self.pagination.get_current_page_items()
|
||||
texts = LangHandler._get_lang(
|
||||
self.primary_view.lang,
|
||||
"playlist.view.detailTitle", "playlist.view.detailDesc", "settings.permissions.title",
|
||||
"playlist.view.permsValue", "playlist.view.permsValue2",
|
||||
"playlist.view.trackList", "playlist.errors.noTrack", "playlist.view.footer2"
|
||||
)
|
||||
|
||||
embed = discord.Embed(title=texts[0], color=Config().embed_color)
|
||||
embed.description = texts[1].format(
|
||||
self.name,
|
||||
self.playlist_id,
|
||||
self.pagination.total_items,
|
||||
self.primary_view.ctx.bot.get_user(self.owner_id),
|
||||
self.type.upper()
|
||||
) + "\n"
|
||||
|
||||
embed.description += texts[2] + "\n"
|
||||
if self.type == 'share':
|
||||
write_perm = '✓' if 'write' in self.perms and self.author.id in self.perms['write'] else '✘'
|
||||
remove_perm = '✓' if 'remove' in self.perms and self.author.id in self.perms['remove'] else '✘'
|
||||
embed.description += texts[3].format(write_perm, remove_perm)
|
||||
else:
|
||||
readable_users = ', '.join(f'<@{user}>' for user in self.perms['read'])
|
||||
embed.description += texts[4].format(readable_users)
|
||||
|
||||
# Add track information
|
||||
embed.description += f"\n\n**{texts[5]}:**\n"
|
||||
if tracks:
|
||||
for index, track in enumerate(tracks, start=self.pagination.start_index + 1):
|
||||
if isinstance(track, dict):
|
||||
source_emoji = Config().get_source_config(track['sourceName'], 'emoji')
|
||||
track_info = f"{source_emoji} `{index:>2}.` `[{format_ms(track['length'])}]` [{truncate_string(track['title'])}]({track['uri']})"
|
||||
else:
|
||||
source_emoji = Config().get_source_config(extract(track.info['uri']).domain, 'emoji')
|
||||
track_info = f"{source_emoji} `{index:>2}.` `[{format_ms(track.length)}]` [{truncate_string(track.title)}]({track.uri})"
|
||||
embed.description += track_info + "\n"
|
||||
else:
|
||||
embed.description += texts[6].format(self.name)
|
||||
|
||||
# Set the footer
|
||||
embed.set_footer(text=texts[7].format(self.time))
|
||||
return embed
|
||||
|
||||
def update_view(self, extra_states = None):
|
||||
texts = LangHandler._get_lang(self.lang, "pagination.play", "pagination.share", "pagination.export", "pagination.delete")
|
||||
extra_states = {
|
||||
"play": {
|
||||
"label": texts[0],
|
||||
},
|
||||
"share": {
|
||||
"label": texts[1],
|
||||
},
|
||||
"export": {
|
||||
"label": texts[2],
|
||||
},
|
||||
"delete": {
|
||||
"label": texts[3],
|
||||
},
|
||||
}
|
||||
return super().update_view(extra_states)
|
||||
|
||||
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
|
||||
if isinstance(error, VoicelinkException):
|
||||
return await dispatch_message(interaction, content=getattr(error, 'original', error), ephemeral=True)
|
||||
|
||||
return await super().on_error(interaction, error, item)
|
||||
|
||||
async def update_message(self, interaction: discord.Interaction) -> None:
|
||||
"""Update the view and edit the message with the new embed."""
|
||||
self.update_view()
|
||||
|
||||
if interaction.response.is_done():
|
||||
await interaction.followup.edit_message(self.primary_view.response.id, embed=self.build_embed(), view=self)
|
||||
else:
|
||||
await interaction.response.edit_message(embed=self.build_embed(), view=self)
|
||||
|
||||
@discord.ui.button(label="<")
|
||||
async def back_to_home(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Return to the main playlist view."""
|
||||
view: PlaylistViewManager = self.primary_view.change_view("home")
|
||||
await interaction.response.edit_message(embed=view.build_embed(), view=view)
|
||||
|
||||
@discord.ui.button(label="Play", custom_id="play", style=discord.ButtonStyle.green)
|
||||
async def play_all(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None:
|
||||
await interaction.response.defer()
|
||||
cmd = interaction.client.get_command("playlist play")
|
||||
await cmd(self.primary_view.ctx, self.name)
|
||||
|
||||
@discord.ui.button(label="Share", custom_id="share", style=discord.ButtonStyle.gray)
|
||||
async def share(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None:
|
||||
modal = BaseModal(
|
||||
title="Share Playlist",
|
||||
custom_id="share_modal",
|
||||
items=[
|
||||
discord.ui.Label(
|
||||
text="User to share with",
|
||||
description="Select a user to share with",
|
||||
component=discord.ui.UserSelect(
|
||||
custom_id="user_select",
|
||||
placeholder="Select a user to share with",
|
||||
required=True
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await interaction.response.send_modal(modal)
|
||||
await modal.wait()
|
||||
|
||||
user = modal.values.get("user_select")
|
||||
if not user:
|
||||
return
|
||||
|
||||
await interaction.client.get_command("playlist share")(self.primary_view.ctx, user[0], self.name)
|
||||
|
||||
@discord.ui.button(label="Export", custom_id="export", style=discord.ButtonStyle.gray)
|
||||
async def export(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None:
|
||||
await interaction.response.defer()
|
||||
cmd = interaction.client.get_command("playlist export")
|
||||
await cmd(self.primary_view.ctx, self.name)
|
||||
|
||||
@discord.ui.button(label="Delete", custom_id="delete", style=discord.ButtonStyle.red)
|
||||
async def delete(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None:
|
||||
await interaction.response.defer()
|
||||
cmd = interaction.client.get_command("playlist delete")
|
||||
await cmd(self.primary_view.ctx, name=self.name)
|
||||
|
||||
if self.playlist_id != "200":
|
||||
view: PlaylistViewManager = self.primary_view.change_view("home")
|
||||
view.results = [item for item in view.results if item.get("id") != self.playlist_id]
|
||||
view.clear_items()
|
||||
view.add_item(PlaylistDropdown(view.results, view.lang))
|
||||
self.primary_view.remove_view(self.playlist_id)
|
||||
await view.response.edit(embed=view.build_embed(), view=view)
|
||||
|
||||
class PlaylistViewManager(DynamicViewManager):
|
||||
def __init__(self, ctx: commands.Context, results: list[dict[str, Any]]):
|
||||
self.ctx: commands.Context[commands.Bot] = ctx
|
||||
self.results: list[dict[str, Any]] = results
|
||||
self.lang: str = MongoDBHandler.get_cached_settings(ctx.guild.id).get("lang")
|
||||
|
||||
views = {"home": self}
|
||||
views.update({result["id"]: PlaylistView(self, result) for result in results})
|
||||
|
||||
super().__init__(views=views, timeout=None)
|
||||
|
||||
self.response: discord.Message = None
|
||||
self.add_item(PlaylistDropdown(results, self.lang))
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for view in self._views.values():
|
||||
view.stop()
|
||||
|
||||
for child in self.current_view.children:
|
||||
child.disabled = True
|
||||
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_width(self, s):
|
||||
width = 0
|
||||
for char in str(s):
|
||||
if unicodedata.east_asian_width(char) in ('F', 'W'):
|
||||
width += 2
|
||||
else:
|
||||
width += 1
|
||||
return width
|
||||
|
||||
def pad_string(self, s, width):
|
||||
s = str(s)
|
||||
current_width = self.get_width(s)
|
||||
padding = width - current_width
|
||||
return s + " " * padding
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
"""
|
||||
Build the embed for the playlist overview.
|
||||
|
||||
Returns:
|
||||
discord.Embed: The constructed embed with playlist details.
|
||||
"""
|
||||
max_p, _, _ = Config().get_playlist_config()
|
||||
text = LangHandler._get_lang(self.lang, "playlist.view.title", "playlist.view.headers", "playlist.view.footer")
|
||||
|
||||
headers = text[1].split(",")
|
||||
headers.insert(0, "")
|
||||
content = [headers]
|
||||
|
||||
description = ""
|
||||
for index in range(max_p):
|
||||
info = self.results[index] if index < len(self.results) else {}
|
||||
if info:
|
||||
content.append([
|
||||
info.get('emoji', ' '),
|
||||
info.get('id', "-" * 3),
|
||||
f"[{info.get('time', '--:--')}]",
|
||||
truncate_string(info.get('name', "-" * 6), 12),
|
||||
len(info.get('tracks', []))
|
||||
])
|
||||
|
||||
column_widths = [max(self.get_width(str(item)) for item in column) for column in zip(*content)]
|
||||
|
||||
for row in content:
|
||||
formatted_row = " ".join(self.pad_string(item, width) for item, width in zip(row, column_widths))
|
||||
description += formatted_row + "\n"
|
||||
|
||||
embed = discord.Embed(
|
||||
description=f'```{description}```',
|
||||
color=Config().embed_color
|
||||
)
|
||||
|
||||
embed.set_author(
|
||||
name=text[0].format(self.ctx.author.display_name),
|
||||
icon_url=self.ctx.author.display_avatar.url
|
||||
)
|
||||
embed.set_footer(text=text[2])
|
||||
return embed
|
||||
130
voicelink/views/queue.py
Normal file
130
voicelink/views/queue.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import discord
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .utils import Pagination
|
||||
from .pagination import PaginationView
|
||||
|
||||
from ..language import LangHandler
|
||||
from ..config import Config
|
||||
from ..utils import format_ms, truncate_string
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..player import Player
|
||||
from ..objects import Track
|
||||
|
||||
class QueueView(PaginationView):
|
||||
"""
|
||||
A Discord UI view for displaying and interacting with a paginated list of tracks.
|
||||
|
||||
Attributes:
|
||||
player (Player): The player containing the track queue or history.
|
||||
author (discord.Member): The member who initiated the view.
|
||||
is_queue (bool): Indicates if the list is a queue or history.
|
||||
pagination (Pagination[Track]): Manages pagination of track lists.
|
||||
response (discord.Message): The message containing the view.
|
||||
total_duration (str): The total duration of the tracks.
|
||||
"""
|
||||
|
||||
def __init__(self, player: "Player", author: discord.Member, is_queue: bool = True) -> None:
|
||||
super().__init__(
|
||||
Pagination["Track"](
|
||||
items=player.queue.tracks() if is_queue else list(reversed(player.queue.history(not player.current))),
|
||||
page_size=7,
|
||||
),
|
||||
author
|
||||
)
|
||||
|
||||
self.player: Player = player
|
||||
self.is_queue: bool = is_queue
|
||||
self.total_duration = self.calculate_total_duration()
|
||||
self.response: discord.Message = None
|
||||
|
||||
def calculate_total_duration(self) -> str:
|
||||
"""Calculate the total duration of the tracks."""
|
||||
try:
|
||||
return format_ms(sum(track.length for track in self.pagination.items))
|
||||
except Exception:
|
||||
return "∞"
|
||||
|
||||
def format_description(self, tracks: list["Track"], texts: list[str]) -> str:
|
||||
"""Format the description for the embed based on current tracks."""
|
||||
now_playing = (
|
||||
texts[1].format(self.player.current.uri, f"```{self.player.current.title}```")
|
||||
if self.player.current else texts[2].format("None")
|
||||
)
|
||||
|
||||
track_list = "\n".join([
|
||||
f"{track.emoji} `{i:>2}.` `[{texts[3] if track.is_stream else format_ms(track.length)}]` "
|
||||
f"[{truncate_string(track.title)}]({track.uri}) {track.requester.mention}"
|
||||
for i, track in enumerate(tracks, start=self.pagination.start_index + 1)
|
||||
])
|
||||
|
||||
return f"{now_playing}\n**{texts[4] if self.is_queue else texts[5]}**\n{track_list}"
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""Disable all buttons when the view times out."""
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
async def build_embed(self) -> discord.Embed:
|
||||
"""Build the embed for the current page of tracks."""
|
||||
tracks = self.pagination.get_current_page_items()
|
||||
texts = await LangHandler.get_lang(
|
||||
self.author.guild.id,
|
||||
"queue.view.title",
|
||||
"queue.view.desc",
|
||||
"player.playback.nowplayingDesc",
|
||||
"common.status.live",
|
||||
"queue.management.title",
|
||||
"queue.view.historyTitle",
|
||||
"playlist.view.footer2",
|
||||
)
|
||||
|
||||
embed = discord.Embed(title=texts[0], color=Config().embed_color)
|
||||
embed.description = self.format_description(tracks, texts)
|
||||
|
||||
if self.player.current:
|
||||
embed.set_thumbnail(url=self.player.current.thumbnail)
|
||||
|
||||
embed.set_footer(text=texts[6].format(self.total_duration))
|
||||
|
||||
return embed
|
||||
|
||||
async def update_message(self, interaction: discord.Interaction) -> None:
|
||||
"""Update the view and edit the message with the new embed."""
|
||||
super().update_view()
|
||||
|
||||
if interaction.response.is_done():
|
||||
await interaction.followup.edit_message(self.response.id, embed=await self.build_embed(), view=self)
|
||||
else:
|
||||
await interaction.response.edit_message(embed=await self.build_embed(), view=self)
|
||||
@@ -20,13 +20,10 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import discord
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from voicelink import Track
|
||||
from ..objects import Track
|
||||
|
||||
class SearchDropdown(discord.ui.Select):
|
||||
def __init__(self, tracks: list[Track], texts: list[str]) -> None:
|
||||
@@ -57,9 +54,6 @@ class SearchView(discord.ui.View):
|
||||
self.values: list[str] = None
|
||||
self.add_item(SearchDropdown(tracks, texts))
|
||||
|
||||
async def on_error(self, error, item, interaction):
|
||||
return
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
26
voicelink/views/utils/__init__.py
Normal file
26
voicelink/views/utils/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from .pagination import Pagination
|
||||
from .modal import BaseModal
|
||||
from .dynamic_view_manager import DynamicViewManager
|
||||
90
voicelink/views/utils/dynamic_view_manager.py
Normal file
90
voicelink/views/utils/dynamic_view_manager.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class DynamicViewManager(discord.ui.View):
|
||||
def __init__(self, views: dict[str, discord.ui.View], *, timeout: float = 180):
|
||||
"""A manager for multiple dynamic views."""
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self._views: dict[str, discord.ui.View] = views
|
||||
self._current_view: discord.ui.View | None = views.get("home")
|
||||
|
||||
def add_view(self, name: str, view: discord.ui.View) -> None:
|
||||
"""
|
||||
Add a view to the manager.
|
||||
|
||||
Args:
|
||||
name (str): The name of the view.
|
||||
view (discord.ui.View): The view instance to add.
|
||||
"""
|
||||
if name in self._views:
|
||||
raise ValueError(f"A view with the name '{name}' already exists.")
|
||||
|
||||
self._views[name] = view
|
||||
|
||||
def get_view(self, key: str) -> discord.ui.View | None:
|
||||
"""
|
||||
Get a view from the manager.
|
||||
|
||||
Args:
|
||||
key (str): The name of the view to retrieve.
|
||||
|
||||
Returns:
|
||||
discord.ui.View | None: The requested view or None if not found.
|
||||
"""
|
||||
return self._views.get(key)
|
||||
|
||||
def remove_view(self, key: str) -> None:
|
||||
"""
|
||||
Remove a view from the manager.
|
||||
|
||||
Args:
|
||||
key (str): The name of the view to remove.
|
||||
"""
|
||||
if key in self._views:
|
||||
del self._views[key]
|
||||
|
||||
def change_view(self, name: str) -> discord.ui.View:
|
||||
"""
|
||||
Change the current active view.
|
||||
|
||||
Args:
|
||||
name (str): The name of the view to switch to.
|
||||
|
||||
Returns:
|
||||
discord.ui.View: The newly activated view.
|
||||
"""
|
||||
view = self.get_view(name)
|
||||
if not view:
|
||||
raise ValueError(f"No view found with the name '{name}'.")
|
||||
|
||||
self._current_view = view
|
||||
return view
|
||||
|
||||
@property
|
||||
def current_view(self) -> discord.ui.View | None:
|
||||
"""Get the current active view."""
|
||||
return self._current_view
|
||||
46
voicelink/views/utils/modal.py
Normal file
46
voicelink/views/utils/modal.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class BaseModal(discord.ui.Modal):
|
||||
def __init__(self, title: str, items: list[discord.ui.Item], timeout: float = None, custom_id: str = None) -> None:
|
||||
super().__init__(title=title, timeout=timeout, custom_id=custom_id)
|
||||
self._add_items(items)
|
||||
self.values: dict[str, str] = {}
|
||||
|
||||
def _add_items(self, items: list[discord.ui.Item]) -> None:
|
||||
"""Add items to the modal."""
|
||||
for item in items:
|
||||
self.add_item(item)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
"""Handle the modal submission."""
|
||||
await interaction.response.defer()
|
||||
|
||||
for item in self.walk_children():
|
||||
if isinstance(item, discord.ui.TextInput):
|
||||
self.values[item.custom_id] = item.value
|
||||
elif isinstance(item, (discord.ui.Select, discord.ui.UserSelect, discord.ui.RoleSelect, discord.ui.ChannelSelect, discord.ui.MentionableSelect)):
|
||||
self.values[item.custom_id] = item.values
|
||||
178
voicelink/views/utils/pagination.py
Normal file
178
voicelink/views/utils/pagination.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""MIT License
|
||||
|
||||
Copyright (c) 2023 - present Vocard Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from typing import List, Generic, TypeVar
|
||||
from math import ceil
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Pagination(Generic[T]):
|
||||
"""
|
||||
A class to manage pagination for a list of items.
|
||||
|
||||
Attributes:
|
||||
items (List[T]): The list of items to paginate.
|
||||
page_size (int): The number of items per page.
|
||||
current_page (int): The current page index (0-based).
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
def __init__(self, items: List[T], page_size: int):
|
||||
"""
|
||||
Initializes the Pagination class.
|
||||
|
||||
Args:
|
||||
items (List[T]): The list of items to paginate.
|
||||
page_size (int): The number of items per page.
|
||||
|
||||
Raises:
|
||||
ValueError: If page_size is less than or equal to 0.
|
||||
"""
|
||||
if page_size <= 0:
|
||||
raise ValueError("page_size must be greater than 0")
|
||||
|
||||
self._items: List[T] = items
|
||||
self._page_size: int = page_size
|
||||
self._current_page: int = 0
|
||||
self.total_pages: int = ceil(len(items) / page_size)
|
||||
|
||||
def add_item(self, item: T) -> None:
|
||||
"""
|
||||
Adds an item to the pagination and updates total pages.
|
||||
|
||||
Args:
|
||||
item (T): The item to add.
|
||||
"""
|
||||
self._items.append(item)
|
||||
self.total_pages = ceil(len(self._items) / self._page_size)
|
||||
|
||||
def remove_item(self, item: T) -> None:
|
||||
"""
|
||||
Removes an item from the pagination and updates total pages.
|
||||
|
||||
Args:
|
||||
item (T): The item to remove.
|
||||
"""
|
||||
self._items.remove(item)
|
||||
self.total_pages = ceil(len(self._items) / self._page_size)
|
||||
if self._current_page >= self.total_pages:
|
||||
self._current_page = max(0, self.total_pages - 1)
|
||||
|
||||
def get_current_page_items(self) -> List[T]:
|
||||
"""
|
||||
Retrieves the items for the current page.
|
||||
|
||||
Returns:
|
||||
List[T]: The items on the current page.
|
||||
"""
|
||||
return self._items[self.start_index:self.end_index]
|
||||
|
||||
def go_back(self) -> None:
|
||||
"""Moves to the previous page if available."""
|
||||
if self.has_previous_page:
|
||||
self._current_page -= 1
|
||||
|
||||
def go_next(self) -> None:
|
||||
"""Moves to the next page if available."""
|
||||
if self.has_next_page:
|
||||
self._current_page += 1
|
||||
|
||||
def go_page(self, page_number: int) -> None:
|
||||
"""
|
||||
Navigate to a specific page, clamped between 0 and total_pages.
|
||||
|
||||
Args:
|
||||
page_number (int): The page number to navigate to (0-based).
|
||||
"""
|
||||
self._current_page = max(0, min(page_number, self.total_pages - 1))
|
||||
|
||||
@property
|
||||
def has_next_page(self) -> bool:
|
||||
"""
|
||||
Checks if there is a next page.
|
||||
|
||||
Returns:
|
||||
bool: True if there is a next page, False otherwise.
|
||||
"""
|
||||
return self._current_page < self.total_pages - 1
|
||||
|
||||
@property
|
||||
def has_previous_page(self) -> bool:
|
||||
"""
|
||||
Checks if there is a previous page.
|
||||
|
||||
Returns:
|
||||
bool: True if there is a previous page, False otherwise.
|
||||
"""
|
||||
return self._current_page > 0
|
||||
|
||||
@property
|
||||
def start_index(self) -> int:
|
||||
"""
|
||||
Gets the start index of the items for the current page.
|
||||
|
||||
Returns:
|
||||
int: The start index (0-based) of the current page.
|
||||
"""
|
||||
return self._current_page * self._page_size
|
||||
|
||||
@property
|
||||
def end_index(self) -> int:
|
||||
"""
|
||||
Gets the end index of the items for the current page.
|
||||
|
||||
Returns:
|
||||
int: The end index (exclusive) of the current page.
|
||||
"""
|
||||
return min(self.start_index + self._page_size, len(self._items))
|
||||
|
||||
@property
|
||||
def current_page(self) -> int:
|
||||
"""
|
||||
Returns the current page number (1-based).
|
||||
|
||||
Returns:
|
||||
int: The current page number, starting from 1.
|
||||
"""
|
||||
return self._current_page + 1
|
||||
|
||||
@property
|
||||
def total_items(self) -> int:
|
||||
"""
|
||||
Gets the total number of items in the pagination.
|
||||
|
||||
Returns:
|
||||
int: The total number of items across all pages.
|
||||
"""
|
||||
return len(self._items)
|
||||
|
||||
@property
|
||||
def items(self) -> List[T]:
|
||||
"""
|
||||
Gets the list of all items in the pagination.
|
||||
|
||||
Returns:
|
||||
List[T]: The list of all items.
|
||||
"""
|
||||
return self._items
|
||||
Reference in New Issue
Block a user