diff --git a/README.md b/README.md index 55a5870..ebf50c0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/addons/__init__.py b/addons/__init__.py deleted file mode 100644 index 9bcfb4e..0000000 --- a/addons/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .lyrics import LYRICS_PLATFORMS -from .placeholders import Placeholders -from .settings import Settings \ No newline at end of file diff --git a/addons/placeholders.py b/addons/placeholders.py deleted file mode 100644 index a3a2a64..0000000 --- a/addons/placeholders.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/addons/settings.py b/addons/settings.py deleted file mode 100644 index ff79807..0000000 --- a/addons/settings.py +++ /dev/null @@ -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", "") \ No newline at end of file diff --git a/cogs/basic.py b/cogs/basic.py index 0a10f96..2984f10 100644 --- a/cogs/basic.py +++ b/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)) \ No newline at end of file diff --git a/cogs/effect.py b/cogs/effect.py index 5953a9c..9055322 100644 --- a/cogs/effect.py +++ b/cogs/effect.py @@ -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)) diff --git a/cogs/listeners.py b/cogs/listeners.py index f8a5583..391e1ce 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -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), diff --git a/cogs/playlist.py b/cogs/playlist.py index a7f3f6b..9c56596 100644 --- a/cogs/playlist.py +++ b/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) diff --git a/cogs/settings.py b/cogs/settings.py index b3834fd..4815334 100644 --- a/cogs/settings.py +++ b/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( diff --git a/cogs/task.py b/cogs/task.py index fe31273..8975d5e 100644 --- a/cogs/task.py +++ b/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)) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 0fc8e39..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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 diff --git a/function.py b/function.py index b8a1b5a..6c2ae98 100644 --- a/function.py +++ b/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) \ No newline at end of file + return voicelink.Config().aliases_settings.get(name, []) \ No newline at end of file diff --git a/langs/CH.json b/langs/CH.json deleted file mode 100644 index 29b890a..0000000 --- a/langs/CH.json +++ /dev/null @@ -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 ็งป้™คๅฎƒใ€‚", - "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 ้–‹ๅง‹่ฆๆฑ‚ไปปไฝ•ๆญŒๆ›ฒ๏ผŒ่€Œ็„ก้œ€ไฝฟ็”จๆฉŸๅ™จไบบๅ‰็ถดใ€‚" -} \ No newline at end of file diff --git a/langs/DE.json b/langs/DE.json index f33ab0c..11a35a4 100644 --- a/langs/DE.json +++ b/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 , 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 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." -} \ No newline at end of file + "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 , 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 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" + } + } +} diff --git a/langs/EN.json b/langs/EN.json index 378a207..0e4e975 100644 --- a/langs/EN.json +++ b/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 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 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." -} \ No newline at end of file + "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 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 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" + } + } +} diff --git a/langs/ES.json b/langs/ES.json index b2ec7bb..fa58491 100644 --- a/langs/ES.json +++ b/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 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 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." -} \ No newline at end of file + "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 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 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" + } + } +} diff --git a/langs/FR.json b/langs/FR.json index fb1df9e..d713d53 100644 --- a/langs/FR.json +++ b/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 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 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." -} \ No newline at end of file + "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 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 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" + } + } +} diff --git a/langs/JA.json b/langs/JA.json index 88660fe..2200968 100644 --- a/langs/JA.json +++ b/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 ใ‚’ไฝฟ็”จใ—ใฆใใ ใ•ใ„ใ€‚", - "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 ใ‚’ไฝฟ็”จใ—ใฆไปปๆ„ใฎๆ›ฒใ‚’ใƒชใ‚ฏใ‚จใ‚นใƒˆใงใใพใ™ใ€‚ใƒœใƒƒใƒˆใฎใƒ—ใƒฌใƒ•ใ‚ฃใƒƒใ‚ฏใ‚นใฏๅฟ…่ฆใ‚ใ‚Šใพใ›ใ‚“ใ€‚" -} \ No newline at end of file + "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 ใ‚’ไฝฟ็”จใ—ใฆใใ ใ•ใ„ใ€‚" + }, + "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": "ใƒšใƒผใ‚ธ็•ชๅท" + } + } +} diff --git a/langs/KO.json b/langs/KO.json index fe49dfc..26bb2c9 100644 --- a/langs/KO.json +++ b/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 ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.", - "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๋กœ ์›ํ•˜๋Š” ๋…ธ๋ž˜๋ฅผ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ด‡ ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." -} \ No newline at end of file + "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 ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค." + }, + "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": "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ" + } + } +} diff --git a/langs/PL.json b/langs/PL.json index 1e0ff06..fc6d935 100644 --- a/langs/PL.json +++ b/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 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 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" -} \ No newline at end of file + "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 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 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" + } + } +} diff --git a/langs/RU.json b/langs/RU.json index 2eb340b..5555245 100644 --- a/langs/RU.json +++ b/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 ะฒ ัั‚ะพะผ ะบะฐะฝะฐะปะต ะฑะตะท ะธัะฟะพะปัŒะทะพะฒะฐะฝะธั ะฟั€ะตั„ะธะบัะฐ ะฑะพั‚ะฐ." -} \ No newline at end of file + "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": "ะะพะผะตั€ ัั‚ั€ะฐะฝะธั†ั‹" + } + } +} diff --git a/langs/UA.json b/langs/UA.json index cecbf4b..fae0a28 100644 --- a/langs/UA.json +++ b/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 ัƒ ั†ัŒะพะผัƒ ะบะฐะฝะฐะปั– ะฑะตะท ะฝะตะพะฑั…ั–ะดะฝะพัั‚ั– ะฒะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ะฟั€ะตั„ั–ะบั ะฑะพั‚ะฐ." -} \ No newline at end of file + "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": "ะะพะผะตั€ ัั‚ะพั€ั–ะฝะบะธ" + } + } +} diff --git a/langs/VN.json b/langs/VN.json index 6fef020..fe3a483 100644 --- a/langs/VN.json +++ b/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 ฤ‘แปƒ 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รฌ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." -} \ No newline at end of file + "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 ฤ‘แปƒ 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รฌ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" + } + } +} diff --git a/langs/ZHCN.json b/langs/ZHCN.json new file mode 100644 index 0000000..1714f2c --- /dev/null +++ b/langs/ZHCN.json @@ -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": "้กต็ " + } + } +} diff --git a/langs/ZHTW.json b/langs/ZHTW.json new file mode 100644 index 0000000..84462fd --- /dev/null +++ b/langs/ZHTW.json @@ -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": "้ ็ขผ" + } + } +} diff --git a/lavalink/Dockerfile-lavalink b/lavalink/Dockerfile-lavalink deleted file mode 100644 index eaa9ca8..0000000 --- a/lavalink/Dockerfile-lavalink +++ /dev/null @@ -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"] diff --git a/lavalink/application.yml b/lavalink/application.yml deleted file mode 100644 index 059f6dd..0000000 --- a/lavalink/application.yml +++ /dev/null @@ -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 diff --git a/local_langs/es-419.json b/local_langs/es-419.json index a047101..24465ec 100644 --- a/local_langs/es-419.json +++ b/local_langs/es-419.json @@ -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." } diff --git a/local_langs/es-ES.json b/local_langs/es-ES.json index e8cfd59..6b3e561 100644 --- a/local_langs/es-ES.json +++ b/local_langs/es-ES.json @@ -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." } diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json new file mode 100644 index 0000000..34b5d64 --- /dev/null +++ b/local_langs/zh-CN.json @@ -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.": "็งป้™คไธ€ไธชๆœๅŠกๅ™จ่ฎพ็ฝฎ่ฆ†็›–๏ผŒไฝฟๆœบๅ™จไบบไฝฟ็”จๅ…ถ้ป˜่ฎคๅ€ผใ€‚" +} \ No newline at end of file diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json index eecde64..d6e354e 100644 --- a/local_langs/zh-TW.json +++ b/local_langs/zh-TW.json @@ -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.": "็งป้™คไธ€ๅ€‹ไผบๆœๅ™จ่จญๅฎš่ฆ†่“‹๏ผŒไฝฟๆฉŸๅ™จไบบไฝฟ็”จๅ…ถ้ ่จญๅ€ผใ€‚" } \ No newline at end of file diff --git a/main.py b/main.py index d8111cf..a977f28 100644 --- a/main.py +++ b/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) diff --git a/requirements.txt b/requirements.txt index 30dac47..acd6509 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/settings Example.json b/settings Example.json index 3a359b6..5ca8afd 100644 --- a/settings Example.json +++ b/settings Example.json @@ -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 } } diff --git a/update.py b/update.py index a6452ba..db1c989 100644 --- a/update.py +++ b/update.py @@ -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) diff --git a/views/embedBuilder.py b/views/embedBuilder.py deleted file mode 100644 index 1e93c91..0000000 --- a/views/embedBuilder.py +++ /dev/null @@ -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() - - \ No newline at end of file diff --git a/views/list.py b/views/list.py deleted file mode 100644 index 7ee4469..0000000 --- a/views/list.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/views/lyrics.py b/views/lyrics.py deleted file mode 100644 index b520313..0000000 --- a/views/lyrics.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/views/playlist.py b/views/playlist.py deleted file mode 100644 index d78ad31..0000000 --- a/views/playlist.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/voicelink/__init__.py b/voicelink/__init__.py index bedf3f3..09495d8 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -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 diff --git a/voicelink/config.py b/voicelink/config.py new file mode 100644 index 0000000..efe6214 --- /dev/null +++ b/voicelink/config.py @@ -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") \ No newline at end of file diff --git a/voicelink/enums.py b/voicelink/enums.py index ef74c58..084cbbd 100644 --- a/voicelink/enums.py +++ b/voicelink/enums.py @@ -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 """ diff --git a/ipc/__init__.py b/voicelink/ipc/__init__.py similarity index 100% rename from ipc/__init__.py rename to voicelink/ipc/__init__.py diff --git a/ipc/client.py b/voicelink/ipc/client.py similarity index 97% rename from ipc/client.py rename to voicelink/ipc/client.py index de0c460..cf1936d 100644 --- a/ipc/client.py +++ b/voicelink/ipc/client.py @@ -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: diff --git a/ipc/methods.py b/voicelink/ipc/methods.py similarity index 87% rename from ipc/methods.py rename to voicelink/ipc/methods.py index 9a93a17..0fcb766 100644 --- a/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -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", diff --git a/voicelink/language.py b/voicelink/language.py new file mode 100644 index 0000000..5115ebe --- /dev/null +++ b/voicelink/language.py @@ -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() \ No newline at end of file diff --git a/addons/lyrics.py b/voicelink/lyrics.py similarity index 99% rename from addons/lyrics.py rename to voicelink/lyrics.py index e05168a..45775ce 100644 --- a/addons/lyrics.py +++ b/voicelink/lyrics.py @@ -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) diff --git a/voicelink/mongodb.py b/voicelink/mongodb.py new file mode 100644 index 0000000..d531c25 --- /dev/null +++ b/voicelink/mongodb.py @@ -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)}") diff --git a/voicelink/objects.py b/voicelink/objects.py index 2852fd6..f9e38fb 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -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" 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) \ No newline at end of file diff --git a/voicelink/placeholders.py b/voicelink/placeholders.py index 407225c..5f5ef5d 100644 --- a/voicelink/placeholders.py +++ b/voicelink/placeholders.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/voicelink/player.py b/voicelink/player.py index 752162b..a60de2f 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -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) \ No newline at end of file + 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 + ) \ No newline at end of file diff --git a/voicelink/pool.py b/voicelink/pool.py index 427fcfd..d415a47 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -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, diff --git a/voicelink/queue.py b/voicelink/queue.py index 58092e2..dc05ea0 100644 --- a/voicelink/queue.py +++ b/voicelink/queue.py @@ -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) diff --git a/voicelink/ratelimit.py b/voicelink/ratelimit.py index 171a512..c168048 100644 --- a/voicelink/ratelimit.py +++ b/voicelink/ratelimit.py @@ -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 diff --git a/voicelink/utils.py b/voicelink/utils.py index fbb2689..7d63192 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/views/__init__.py b/voicelink/views/__init__.py similarity index 84% rename from views/__init__.py rename to voicelink/views/__init__.py index 883cfe5..3b124a5 100644 --- a/views/__init__.py +++ b/voicelink/views/__init__.py @@ -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 diff --git a/views/controller.py b/voicelink/views/controller.py similarity index 67% rename from views/controller.py rename to voicelink/views/controller.py index 9bfc8b8..b15685c 100644 --- a/views/controller.py +++ b/voicelink/views/controller.py @@ -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 \ No newline at end of file + if isinstance(error, voicelink.VoicelinkException): + return await interaction.response.send_message(error, ephemeral=True) + + await super().on_error(interaction, error, item) \ No newline at end of file diff --git a/views/debug.py b/voicelink/views/debug.py similarity index 82% rename from views/debug.py rename to voicelink/views/debug.py index 4f76e10..f1adab0 100644 --- a/views/debug.py +++ b/voicelink/views/debug.py @@ -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() \ No newline at end of file diff --git a/voicelink/views/embed_builder.py b/voicelink/views/embed_builder.py new file mode 100644 index 0000000..8d092da --- /dev/null +++ b/voicelink/views/embed_builder.py @@ -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() + + \ No newline at end of file diff --git a/views/help.py b/voicelink/views/help.py similarity index 91% rename from views/help.py rename to voicelink/views/help.py index fcb4005..0e0237f 100644 --- a/views/help.py +++ b/voicelink/views/help.py @@ -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': diff --git a/views/inbox.py b/voicelink/views/inbox.py similarity index 98% rename from views/inbox.py rename to voicelink/views/inbox.py index 7524b7b..db23cc0 100644 --- a/views/inbox.py +++ b/voicelink/views/inbox.py @@ -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: diff --git a/views/link.py b/voicelink/views/link.py similarity index 99% rename from views/link.py rename to voicelink/views/link.py index 0ceb3d5..9c1fada 100644 --- a/views/link.py +++ b/voicelink/views/link.py @@ -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)) \ No newline at end of file + self.add_item(discord.ui.Button(label=label, emoji=emoji, url=url)) diff --git a/voicelink/views/lyrics.py b/voicelink/views/lyrics.py new file mode 100644 index 0000000..f23f38e --- /dev/null +++ b/voicelink/views/lyrics.py @@ -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) \ No newline at end of file diff --git a/voicelink/views/pagination.py b/voicelink/views/pagination.py new file mode 100644 index 0000000..fb5d193 --- /dev/null +++ b/voicelink/views/pagination.py @@ -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) \ No newline at end of file diff --git a/voicelink/views/playlist.py b/voicelink/views/playlist.py new file mode 100644 index 0000000..a913bb1 --- /dev/null +++ b/voicelink/views/playlist.py @@ -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 \ No newline at end of file diff --git a/voicelink/views/queue.py b/voicelink/views/queue.py new file mode 100644 index 0000000..a3cfbbe --- /dev/null +++ b/voicelink/views/queue.py @@ -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) \ No newline at end of file diff --git a/views/search.py b/voicelink/views/search.py similarity index 92% rename from views/search.py rename to voicelink/views/search.py index 3a77cb8..f841943 100644 --- a/views/search.py +++ b/voicelink/views/search.py @@ -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 diff --git a/voicelink/views/utils/__init__.py b/voicelink/views/utils/__init__.py new file mode 100644 index 0000000..a0d3fda --- /dev/null +++ b/voicelink/views/utils/__init__.py @@ -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 diff --git a/voicelink/views/utils/dynamic_view_manager.py b/voicelink/views/utils/dynamic_view_manager.py new file mode 100644 index 0000000..8b6d6b1 --- /dev/null +++ b/voicelink/views/utils/dynamic_view_manager.py @@ -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 diff --git a/voicelink/views/utils/modal.py b/voicelink/views/utils/modal.py new file mode 100644 index 0000000..ead346d --- /dev/null +++ b/voicelink/views/utils/modal.py @@ -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 \ No newline at end of file diff --git a/voicelink/views/utils/pagination.py b/voicelink/views/utils/pagination.py new file mode 100644 index 0000000..339dab3 --- /dev/null +++ b/voicelink/views/utils/pagination.py @@ -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 \ No newline at end of file