From 2c9aae7861e20f7ff46820e0ca611033807c7f62 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:53:22 +0800 Subject: [PATCH 01/10] Refactor queue view and add pagination utilities Replaced ListView with QueueView for queue/history display, introducing improved pagination and modal support via new utilities in views/utils/. Updated imports and references in cogs/basic.py and views/__init__.py. Removed views/list.py and added views/queue.py, views/utils/pagination.py, and views/utils/modal.py for modular pagination and modal handling. --- cogs/basic.py | 6 +- views/__init__.py | 2 +- views/help.py | 2 +- views/list.py | 120 ----------------------- views/queue.py | 199 ++++++++++++++++++++++++++++++++++++++ views/utils/__init__.py | 25 +++++ views/utils/modal.py | 44 +++++++++ views/utils/pagination.py | 131 +++++++++++++++++++++++++ 8 files changed, 404 insertions(+), 125 deletions(-) delete mode 100644 views/list.py create mode 100644 views/queue.py create mode 100644 views/utils/__init__.py create mode 100644 views/utils/modal.py create mode 100644 views/utils/pagination.py diff --git a/cogs/basic.py b/cogs/basic.py index 0a10f96..d6aca4b 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -42,7 +42,7 @@ from function import ( from voicelink import SearchType, LoopType from addons import LYRICS_PLATFORMS -from views import SearchView, ListView, LinkView, LyricsView, HelpView +from views import SearchView, QueueView, LinkView, LyricsView, HelpView from validators import url async def nowplay(ctx: commands.Context, player: voicelink.Player): @@ -493,7 +493,7 @@ class Basic(commands.Cog): if player.queue.is_empty: return await nowplay(ctx, player) - view = ListView(player=player, author=ctx.author) + view = QueueView(player=player, author=ctx.author) view.response = await send(ctx, await view.build_embed(), view=view) @queue.command(name="export", aliases=get_aliases("export")) @@ -577,7 +577,7 @@ class Basic(commands.Cog): if not player.queue.history(): return await nowplay(ctx, player) - view = ListView(player=player, author=ctx.author, is_queue=False) + view = QueueView(player=player, author=ctx.author, is_queue=False) view.response = await send(ctx, await view.build_embed(), view=view) @commands.hybrid_command(name="leave", aliases=get_aliases("leave")) diff --git a/views/__init__.py b/views/__init__.py index 883cfe5..db8e424 100644 --- a/views/__init__.py +++ b/views/__init__.py @@ -30,7 +30,7 @@ class ButtonOnCooldown(commands.CommandError): from .controller import InteractiveController from .search import SearchView from .help import HelpView -from .list import ListView +from .queue import QueueView from .lyrics import LyricsView from .playlist import PlaylistView from .inbox import InboxView diff --git a/views/help.py b/views/help.py index fcb4005..bb9edf2 100644 --- a/views/help.py +++ b/views/help.py @@ -27,7 +27,7 @@ from discord.ext import commands import function as func class HelpDropdown(discord.ui.Select): - def __init__(self, categories:list): + def __init__(self, categories: list[str]) -> None: self.view: HelpView super().__init__( 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/queue.py b/views/queue.py new file mode 100644 index 0000000..ff984c5 --- /dev/null +++ b/views/queue.py @@ -0,0 +1,199 @@ +"""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 typing import TYPE_CHECKING + +from .utils import Pagination, BaseModal + +if TYPE_CHECKING: + from voicelink import Player, Track + + +class QueueView(discord.ui.View): + """ + 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__(timeout=60) + self.player: Player = player + self.author: discord.Member = author + self.is_queue: bool = is_queue + self.response: discord.Message = None + + self.pagination = Pagination["Track"]( + items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())), + page_size=7, + ) + + self.total_duration = self.calculate_total_duration() + self.update_view() + + def calculate_total_duration(self) -> str: + """Calculate the total duration of the tracks.""" + try: + return func.time(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 func.time(track.length)}]` " + f"[{func.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}" + + def update_view(self) -> None: + """Update button states and page number display based on current pagination state.""" + button_states = { + "fast_back": self.pagination.current_page <= 2, + "back": not self.pagination.has_previous_page, + "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, + "next": not self.pagination.has_next_page, + } + + for child in self.children: + if child.custom_id in button_states: + child.disabled = button_states[child.custom_id] + if child.custom_id == "page_number": + child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" + + 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 interaction_check(self, interaction: discord.Interaction) -> bool: + """Ensure only the author of the view can interact with it.""" + return interaction.user == self.author + + 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 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 = self.format_description(tracks, texts) + + embed.set_footer( + text=texts[6].format( + self.pagination.current_page, + self.pagination.total_pages, + self.total_duration, + ) + ) + + return embed + + async def update_and_edit_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.response.id, embed=await self.build_embed(), view=self) + else: + await interaction.response.edit_message(embed=await self.build_embed(), view=self) + + @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_and_edit_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_and_edit_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.""" + modal = BaseModal( + title="Page Number", + custom_id="page_number_modal", + items=[ + discord.ui.TextInput( + label="Page Number", + custom_id="page_number", + placeholder="Enter the page number to navigate.", + 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_and_edit_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_and_edit_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_and_edit_message(interaction) diff --git a/views/utils/__init__.py b/views/utils/__init__.py new file mode 100644 index 0000000..c2902b7 --- /dev/null +++ b/views/utils/__init__.py @@ -0,0 +1,25 @@ +"""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 \ No newline at end of file diff --git a/views/utils/modal.py b/views/utils/modal.py new file mode 100644 index 0000000..ea143be --- /dev/null +++ b/views/utils/modal.py @@ -0,0 +1,44 @@ +"""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 diff --git a/views/utils/pagination.py b/views/utils/pagination.py new file mode 100644 index 0000000..7bfdbd1 --- /dev/null +++ b/views/utils/pagination.py @@ -0,0 +1,131 @@ +"""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 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.""" + 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 From 1633f14b64665a7c0cc77dfdf11104876311f101 Mon Sep 17 00:00:00 2001 From: Choco Date: Fri, 19 Sep 2025 23:29:42 +0800 Subject: [PATCH 02/10] Refactor playlist view and update language files Refactored playlist viewing logic for improved modularity and maintainability, introducing PlaylistViewManager and DynamicViewManager. Updated language files to standardize playlist view titles and descriptions, and removed unnecessary emojis. Updated requirements.txt to discord.py 2.6.3. Enhanced send function to support file attachments. --- cogs/playlist.py | 230 ++++++++++++------- function.py | 2 + langs/CH.json | 11 +- langs/DE.json | 7 +- langs/EN.json | 8 +- langs/ES.json | 7 +- langs/FR.json | 7 +- langs/JA.json | 7 +- langs/KO.json | 7 +- langs/PL.json | 7 +- langs/RU.json | 7 +- langs/UA.json | 7 +- langs/VN.json | 7 +- requirements.txt | 2 +- views/__init__.py | 2 +- views/playlist.py | 332 ++++++++++++++++++++-------- views/queue.py | 2 +- views/utils/__init__.py | 3 +- views/utils/dynamic_view_manager.py | 89 ++++++++ views/utils/pagination.py | 39 +++- 20 files changed, 566 insertions(+), 217 deletions(-) create mode 100644 views/utils/dynamic_view_manager.py diff --git a/cogs/playlist.py b/cogs/playlist.py index a7f3f6b..5a8fe17 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -32,60 +32,163 @@ from function import ( get_user, update_user, check_roles, - get_lang, - settings, get_aliases, cooldown_check, logger ) -from views import PlaylistView, InboxView, HelpView +from views import PlaylistViewManager, InboxView, HelpView 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) +async def check_playlist_perms(user_id: int, author_id: int, playlist_id: str) -> dict: + """Check if user has read permissions for a specific playlist.""" + user_data = await get_user(author_id, 'playlist') + playlist = user_data.get(playlist_id) + if not playlist or user_id not in playlist['perms']['read']: return {} + return playlist 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') + """Get user's playlist data with various filtering options.""" + user_playlists = await get_user(ctx.author.id, 'playlist') - await ctx.defer() + if not ctx.interaction.response.is_done(): + await ctx.defer() + 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"} + + 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} + shared_playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) + + if not shared_playlist or ctx.author.id not in shared_playlist['perms']['read']: + return {'playlist': None, 'position': index, 'id': playlist_id} + + return {'playlist': shared_playlist, 'position': index, 'id': playlist_id} + + return {'playlist': playlist, 'position': index, 'id': playlist_id} + return {'playlist': None, 'position': None, 'id': 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"] = ctime(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 = await check_playlist_perms( + ctx.author.id, + playlist_data['user'], + playlist_data['referId'] + ) + + if not shared_playlist: + await 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.decode(track) + total_time += decoded_track.get("length", 0) + decoded_tracks.append(decoded_track) + + return { + 'emoji': emoji, + 'id': playlist_id, + 'time': ctime(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.decode(track) + total_time += decoded_track.get("length", 0) + decoded_tracks.append(decoded_track) + + return { + 'emoji': emoji, + 'id': playlist_id, + 'time': ctime(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: @@ -156,63 +259,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_playlists, _ = check_roles() - 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_playlists < 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 send(ctx, content=await view.build_embed(), view=view, ephemeral=True) @playlist.command(name="create", aliases=get_aliases("create")) @app_commands.describe( diff --git a/function.py b/function.py index b8a1b5a..980b92a 100644 --- a/function.py +++ b/function.py @@ -191,6 +191,7 @@ async def send( content: Union[str, discord.Embed] = None, *params, view: discord.ui.View = None, + file: discord.File = None, delete_after: float = None, ephemeral: bool = False, requires_fetch: bool = False @@ -223,6 +224,7 @@ async def send( send_kwargs = { "content": text, "embed": embed, + "file": file, "allowed_mentions": ALLOWED_MENTIONS, "silent": settings.get("silent_msg", False), } diff --git a/langs/CH.json b/langs/CH.json index 29b890a..160bc34 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -38,7 +38,7 @@ "addEffect": "套用音效`{0}`濾鏡。", "clearEffect": "聲音效果已清除!", "filterTagAlreadyInUse": "此聲音效果已在使用中!請使用 /cleareffect 移除它。", - "playlistViewTitle": "📜 所有 {0} 的播放清單", + "playlistViewTitle": "所有 {0} 的播放清單", "playlistViewHeaders": "ID:,時間:,名稱:,曲目數:", "playlistFooter": "輸入 /playlist play [播放清單] 加入此播放清單至隊列中。", "playlistNotFound": "找不到播放清單 [`{0}`]。輸入 /playlist view 查看所有播放清單。", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 已從 {1} 的播放清單 [`{2}`] 中刪除 **{0}**。", "playlistClear": "你已成功清除播放清單 [`{0}`]。", "playlistView": "播放清單檢視器", - "playlistViewDesc": "```名稱 | ID: {0} | {1}\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```", - "playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}", - "playlistViewPermsValue2": "📖 讀取: {0}", + "playlistViewDesc": "```名稱: {0} [{1}]\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```", + "playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}", + "playlistViewPermsValue2": "📖 讀取: {0}", "playlistViewTrack": "音軌", - "playlistViewPage": "頁面: {0}/{1} | 總長度: {2}", + "playlistViewFooter": "總長度: {0}", "inboxFull": "抱歉!{0} 的收件匣已滿。", "inboxNoMsg": "您的收件匣中沒有任何訊息。", "invitationSent": "已發送邀請給 {0}。", @@ -129,7 +129,6 @@ "historyTitle": "歷史隊列:", "viewTitle": "音樂隊列", "viewDesc": "**現正播放:[點擊我]({0}) ⮯**\n{1}", - "viewFooter": "頁數:{0}/{1} | 總長度:{2}", "pauseError": "播放器已經暫停。", "pauseVote": "{0} 已投票暫停歌曲。[{1}/{2}]", "paused": "播放器已被 `{0}` 暫停。", diff --git a/langs/DE.json b/langs/DE.json index f33ab0c..a0bf86a 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -38,7 +38,7 @@ "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}", + "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.", @@ -68,11 +68,11 @@ "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```", + "playlistViewDesc": "```Name: {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}", + "playlistViewFooter": "Gesamtdauer: {0}", "inboxFull": "Es tut mir leid! Der Posteingang von {0} ist voll.", "inboxNoMsg": "Es sind keine Nachrichten in Deinem Posteingang.", "invitationSent": "Einladung an {0} gesendet.", @@ -129,7 +129,6 @@ "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.", diff --git a/langs/EN.json b/langs/EN.json index 378a207..a05f7cb 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -38,9 +38,10 @@ "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", + "playlistViewTitle": "All of {0}'s Playlists", "playlistViewHeaders": "ID:,Time:,Name:,Tracks:", "playlistFooter": "Type /playlist play [playlist] to add a playlist the into the queue.", + "emptyPlaylistMessage": "🎶 No playlists found. Create your first one with `/playlist create`!", "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}`].", @@ -68,11 +69,11 @@ "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```", + "playlistViewDesc": "```Name: {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}", + "playlistViewFooter": "Total Duration: {0}", "inboxFull": "Sorry! {0}'s inbox is full.", "inboxNoMsg": "There are no messages in your inbox.", "invitationSent": "Invitation sent to {0}.", @@ -129,7 +130,6 @@ "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.", diff --git a/langs/ES.json b/langs/ES.json index b2ec7bb..1b5c11d 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -38,7 +38,7 @@ "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}", + "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.", @@ -68,11 +68,11 @@ "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```", + "playlistViewDesc": "```Nombre: {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}", + "playlistViewFooter": "Duración total: {0}", "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}.", @@ -129,7 +129,6 @@ "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.", diff --git a/langs/FR.json b/langs/FR.json index fb1df9e..663f967 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -38,7 +38,7 @@ "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}", + "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.", @@ -68,11 +68,11 @@ "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```", + "playlistViewDesc": "```Nom: {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}", + "playlistViewFooter": "Durée totale : {0}", "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}.", @@ -129,7 +129,6 @@ "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.", diff --git a/langs/JA.json b/langs/JA.json index 88660fe..0df1caa 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -38,7 +38,7 @@ "addEffect": "`{0}` フィルターを適用します。", "clearEffect": "効果音がクリアされました!", "filterTagAlreadyInUse": "このサウンドエフェクトはすでに使用されています!削除するには/cleareffect を使用してください。", - "playlistViewTitle": "📜 {0}のすべてのプレイリスト", + "playlistViewTitle": "{0}のすべてのプレイリスト", "playlistViewHeaders": "ID:,時間:,名前:,トラック:", "playlistFooter": "プレイリストをキューに追加するには、/playlist play [playlist]を入力してください。", "playlistNotFound": "プレイリスト[{0}]が見つかりません。すべてのプレイリストを表示するには、/playlist viewを入力してください。", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 {1}さんのプレイリスト[{2}]から**{0}**を削除しました。", "playlistClear": "プレイリスト[{0}]を正常にクリアしました。", "playlistView": "プレイリストビューアー", - "playlistViewDesc": "```名前 | ID: {0} | {1}\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```", + "playlistViewDesc": "```名前: {0} [{1}]\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```", "playlistViewPermsValue": "📖 読み込み: ✓ ✍🏽 書き込み: {0} 🗑️ 削除: {1}", "playlistViewPermsValue2": "📖 読み込み: {0}", "playlistViewTrack": "トラック", - "playlistViewPage": "ページ: {0}/{1} | トータルダレーション: {2}", + "playlistViewFooter": "トータルダレーション: {0}", "inboxFull": "申し訳ありません!{0}さんの受信トレイはいっぱいです。", "inboxNoMsg": "受信トレイにメッセージはありません。", "invitationSent": "{0}さんに招待状を送信しました。", @@ -129,7 +129,6 @@ "historyTitle": "再生履歴:", "viewTitle": "音楽キュー", "viewDesc": "**現在再生中:[ここをクリックして聴く]({0}) ⮯**\n{1}", - "viewFooter": "ページ:{0}/{1} | 合計再生時間:{2}", "pauseError": "プレイヤーはすでに一時停止しています。", "pauseVote": "{0}が曲を一時停止することに賛成しました。[{1}/{2}]", "paused": "{0}がプレイヤーを一時停止しました。", diff --git a/langs/KO.json b/langs/KO.json index fe49dfc..6223ef1 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -38,7 +38,7 @@ "addEffect": "`{0}` 필터를 적용하세요.", "clearEffect": "효과가 삭제되었습니다!", "filterTagAlreadyInUse": "이 필터는 이미 사용 중입니다! 삭제하려면 /cleareffect 를 사용하십시오.", - "playlistViewTitle": "📜 {0}의 모든 재생 목록", + "playlistViewTitle": "{0}의 모든 재생 목록", "playlistViewHeaders": "ID:,시간:,이름:,트랙:", "playlistFooter": "/playlist play [재생 목록]을 입력하여 재생 목록을 대기열에 추가하세요.", "playlistNotFound": "재생 목록 [{0}]을(를) 찾을 수 없습니다. 모든 재생 목록을 보려면 /playlist view를 입력하세요.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 {1}님의 재생 목록 [{2}]에서 **{0}**을(를) 제거했습니다.", "playlistClear": "재생 목록 [{0}]을(를) 성공적으로 지웠습니다.", "playlistView": "재생 목록 뷰어", - "playlistViewDesc": "```이름 | ID: {0} | {1}\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```", + "playlistViewDesc": "```이름: {0} [{1}]\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```", "playlistViewPermsValue": "📖 읽기: ✓ ✍🏽 쓰기: {0} 🗑️ 삭제: {1}", "playlistViewPermsValue2": "📖 읽기: {0}", "playlistViewTrack": "트랙", - "playlistViewPage": "페이지: {0}/{1} | 총 길이: {2}", + "playlistViewFooter": "총 길이: {0}", "inboxFull": "죄송합니다! {0}님의 받은 편지함이 가득 찼습니다.", "inboxNoMsg": "받은 편지함에 메시지가 없습니다.", "invitationSent": "{0}님에게 초대장을 보냈습니다.", @@ -129,7 +129,6 @@ "historyTitle": "이전 곡 대기열:", "viewTitle": "음악 대기열", "viewDesc": "**현재 재생중: [여기를 클릭하여 듣기]({0}) ⮯**\n{1}", - "viewFooter": "페이지: {0}/{1} | 총 재생 시간: {2}", "pauseError": "플레이어가 이미 일시정지되었습니다.", "pauseVote": "{0}님이 노래 일시정지 투표를 했습니다. [{1}/{2}]", "paused": "{0}님이 플레이어를 일시정지했습니다.", diff --git a/langs/PL.json b/langs/PL.json index 1e0ff06..68c0bfe 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -38,7 +38,7 @@ "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}:", + "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.", @@ -68,11 +68,11 @@ "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```", + "playlistViewDesc": "```Nazwa: {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}", + "playlistViewFooter": "Całkowity czas trwania: {0}", "inboxFull": "Błąd! Skrzynka odbiorcza {0} jest pełna.", "inboxNoMsg": "W twojej skrzynce odbiorczej nie ma wiadomości.", "invitationSent": "Zaproszenie wysłane do {0}.", @@ -129,7 +129,6 @@ "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.", diff --git a/langs/RU.json b/langs/RU.json index 2eb340b..5b9a21b 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -38,7 +38,7 @@ "addEffect": "Применен `{0}` эффект.", "clearEffect": "Звуковые эффекты были очищены!", "filterTagAlreadyInUse": "Этот звуковой эффект уже используется! Пожалуйста, используйте /cleareffect <Тег>, чтобы удалить его.", - "playlistViewTitle": "📜 Все плейлисты пользователя {0}", + "playlistViewTitle": "Все плейлисты пользователя {0}", "playlistViewHeaders": "ID:,Время:,Название:,Треки:", "playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.", "playlistNotFound": "Плейлист [`{0}`] не найден. Введите /playlist view, чтобы посмотреть все ваши плейлисты.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Удален **{0}** из плейлиста {1} [`{2}`].", "playlistClear": "Вы успешно очистили свой плейлист [`{0}`].", "playlistView": "Просмотр плейлистов", - "playlistViewDesc": "```Имя | ID: {0} | {1}\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```", + "playlistViewDesc": "```Имя: {0} [{1}]\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```", "playlistViewPermsValue": "📖 Чтение: ✓ ✍🏽 Запись: {0} 🗑️ Удаление: {1}", "playlistViewPermsValue2": "📖 Чтение: {0}", "playlistViewTrack": "Треки", - "playlistViewPage": "Страница: {0}/{1} | Общая продолжительность: {2}", + "playlistViewFooter": "Общая продолжительность: {0}", "inboxFull": "Извините! Почтовый ящик {0} переполнен.", "inboxNoMsg": "В вашем почтовом ящике нет сообщений.", "invitationSent": "Приглашение отправлено {0}.", @@ -129,7 +129,6 @@ "historyTitle": "История очереди:", "viewTitle": "Текущая очередь", "viewDesc": "**Сейчас играет: [*ссылка*]({0}) ⮯**\n{1}", - "viewFooter": "Страница: {0}/{1} | Общая продолжительность: {2}", "pauseError": "Плеер уже на паузе.", "pauseVote": "Пользователь `{0}` проголосовал за паузу плеера. [{1}/{2}]", "paused": "Пользователь `{0}` поставил плеер на паузу.", diff --git a/langs/UA.json b/langs/UA.json index cecbf4b..db0641a 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -38,7 +38,7 @@ "addEffect": "Застосуйте ефект `{0}` фільтр.", "clearEffect": "Звукові ефекти були очищені!", "filterTagAlreadyInUse": "Цей звуковий ефект уже використовується! Будь ласка, використовуйте /cleareffect <Тег>, щоб видалити його.", - "playlistViewTitle": "📜 Усі плейлисти користувача {0}", + "playlistViewTitle": "Усі плейлисти користувача {0}", "playlistViewHeaders": "ID:,Час:,Назва:,Треки:", "playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.", "playlistNotFound": "Плейлист [`{0}`] не знайдено. Введіть /playlist view, щоб подивитися всі ваші плейлисти.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Видалено **{0}** з плейлиста {1} [`{2}`].", "playlistClear": "Ви успішно очистили свій плейлист [`{0}`].", "playlistView": "Перегляд плейлистів", - "playlistViewDesc": "```Ім'я | ID: {0} | {1}\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```", + "playlistViewDesc": "```Ім'я: {0} [{1}]\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```", "playlistViewPermsValue": "📖 Читання: ✓ ✍🏽 Запис: {0} 🗑️ Видалення: {1}", "playlistViewPermsValue2": "📖 Читання: {0}", "playlistViewTrack": "Треки", - "playlistViewPage": "Сторінка: {0}/{1} | Загальна тривалість: {2}", + "playlistViewFooter": "Загальна тривалість: {0}", "inboxFull": "Вибачте! Поштова скринька {0} переповнена.", "inboxNoMsg": "У вашій поштовій скриньці немає повідомлень.", "invitationSent": "Запрошення надіслано {0}.", @@ -129,7 +129,6 @@ "historyTitle": "Історія черги:", "viewTitle": "Поточна черга", "viewDesc": "**Заразом грає: [*посилання*]({0}) ⮯**\n{1}", - "viewFooter": "Сторінка: {0}/{1} | Загальна тривалість: {2}", "pauseError": "Плеєр уже на паузі.", "pauseVote": "{0} проголосував за паузу пісні. [{1}/{2}]", "paused": "`{0}` Поставив плеєр на паузу.", diff --git a/langs/VN.json b/langs/VN.json index 6fef020..2440917 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -38,7 +38,7 @@ "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}", + "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.", @@ -68,11 +68,11 @@ "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```", + "playlistViewDesc": "```Tên: {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}", + "playlistViewFooter": "Tổng Thời Lượng: {0}", "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}.", @@ -129,7 +129,6 @@ "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.", diff --git a/requirements.txt b/requirements.txt index 30dac47..0cbe8f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.5.2 +discord.py==2.6.3 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 diff --git a/views/__init__.py b/views/__init__.py index db8e424..d8bca40 100644 --- a/views/__init__.py +++ b/views/__init__.py @@ -32,7 +32,7 @@ from .search import SearchView from .help import HelpView from .queue import QueueView from .lyrics import LyricsView -from .playlist import PlaylistView +from .playlist import PlaylistViewManager from .inbox import InboxView from .link import LinkView from .debug import DebugView diff --git a/views/playlist.py b/views/playlist.py index d78ad31..3c22448 100644 --- a/views/playlist.py +++ b/views/playlist.py @@ -26,139 +26,297 @@ import function as func from math import ceil from tldextract import extract -from typing import Any, TYPE_CHECKING +from discord.ext import commands +from typing import Any, TYPE_CHECKING, reveal_type + +from .utils import DynamicViewManager, Pagination, BaseModal if TYPE_CHECKING: from voicelink import Track + class Select_playlist(discord.ui.Select): - def __init__(self, results): - self.view: PlaylistView + def __init__(self, results: list[dict[str, Any]]) -> None: + self.view: PlaylistViewManager 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' - ] + 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: - 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) + view: PlaylistView = self.view.change_view(self.values[0]) + await interaction.response.edit_message(embed=await view.build_embed(), view=view) + class PlaylistView(discord.ui.View): def __init__( - self, - viewEmbed: discord.Embed, - results: list[dict[str, Any]], - author: discord.Message + self, + primary_view: "PlaylistViewManager", + playlist_data: dict[str, Any] ) -> None: - super().__init__(timeout=60) + super().__init__(timeout=180) - self.viewEmbed: discord.Embed = viewEmbed - self.results: list[dict[str, Any]] = results - self.author: discord.Member = author - self.response: discord.Message = None + self.primary_view: PlaylistViewManager = primary_view + self.author: discord.Member = primary_view.ctx.author - self.current: dict[str, Any] = None - self.page: int = 0 - self.current_page: int = 1 + self.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") + self.pagination: Pagination = Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7) - self.add_item(Select_playlist(results)) + self.update_view() + + def update_view(self) -> None: + """Update button states and page number display based on current pagination state.""" + button_states = { + "fast_back": self.pagination.current_page <= 2, + "back": not self.pagination.has_previous_page, + "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, + "next": not self.pagination.has_next_page, + } + + for child in self.children: + if child.custom_id in button_states: + child.disabled = button_states[child.custom_id] + if child.custom_id == "page_number": + child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" 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") + """Build the embed for the current page of tracks.""" + tracks = self.pagination.get_current_page_items() + texts = await func.get_lang( + self.author.guild.id, + "playlistView", "playlistViewDesc", "settingsPermTitle", + "playlistViewPermsValue", "playlistViewPermsValue2", + "playlistViewTrack", "playlistNoTrack", "playlistViewFooter" + ) 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 = texts[1].format( + self.name, + self.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: - 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)) + for index, track in enumerate(tracks, start=self.pagination.start_index + 1): + if self.type == "playlist": + source_emoji = func.get_source(track['sourceName'], 'emoji') + track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track['length'])}]` [{func.truncate_string(track['title'])}]({track['uri']})" + else: + source_emoji = func.get_source(extract(track.info['uri']).domain, 'emoji') + track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track.length)}]` [{func.truncate_string(track.title)}]({track.uri})" + embed.description += track_info + "\n" else: - embed.description += texts[6].format(self.current['name']) + embed.description += texts[6].format(self.name) - embed.set_footer(text=texts[7].format(self.current_page, self.page, self.current['time'])) + # Set the footer + embed.set_footer(text=texts[7].format(self.time)) return embed async def on_timeout(self) -> None: for child in self.children: child.disabled = True try: - await self.response.edit(view=self) + await self.primary_view.response.edit(view=self) except: pass - @discord.ui.button(label='<<', style=discord.ButtonStyle.grey, disabled=True) + async def update_and_edit_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=await self.build_embed(), view=self) + else: + await interaction.response.edit_message(embed=await self.build_embed(), view=self) + + async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None: + """Handle errors that occur during interaction.""" + func.logger.error(f"Error in PlaylistView: {error}", exc_info=error) + + @discord.ui.button(label='<<', custom_id="fast_back") 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() + """Jump to the first page.""" + self.pagination.go_page(0) + await self.update_and_edit_message(interaction) - @discord.ui.button(label='Back', style=discord.ButtonStyle.blurple, disabled=True) + @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: - 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() + """Go to the previous page if it exists.""" + self.pagination.go_back() + await self.update_and_edit_message(interaction) - @discord.ui.button(label='Next', style=discord.ButtonStyle.blurple, disabled=True) + @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.""" + modal = BaseModal( + title="Page Number", + custom_id="page_number_modal", + items=[ + discord.ui.TextInput( + label="Page Number", + custom_id="page_number", + placeholder="Enter the page number to navigate.", + 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_and_edit_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: - 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() + """Go to the next page if it exists.""" + self.pagination.go_next() + await self.update_and_edit_message(interaction) - @discord.ui.button(label='>>', style=discord.ButtonStyle.grey, disabled=True) + @discord.ui.button(label='>>', custom_id="fast_next") 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()) + """Jump to the last page.""" + self.pagination.go_page(self.pagination.total_pages - 1) + await self.update_and_edit_message(interaction) + + @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=await view.build_embed(), view=view) + + @discord.ui.button(label="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) + # Need to handle error + + @discord.ui.button(label="Share", style=discord.ButtonStyle.blurple) + async def share(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: 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 + @discord.ui.button(label="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", 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) + + +class PlaylistViewManager(DynamicViewManager): + def __init__(self, ctx: commands.Context, results: list[dict[str, Any]]): + self.ctx: commands.Context = ctx + self.results: list[dict[str, Any]] = results + + 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(Select_playlist(results)) + + def get_width(self, s): + import unicodedata + 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 + + async def build_embed(self) -> discord.Embed: + """ + Build the embed for the playlist overview. + + Returns: + discord.Embed: The constructed embed with playlist details. + """ + _, max_p, _ = func.check_roles() + text = await func.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter") + + 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', '--:--')}]", + info.get('name', "-" * 6), + 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=func.settings.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/views/queue.py b/views/queue.py index ff984c5..ad921c3 100644 --- a/views/queue.py +++ b/views/queue.py @@ -122,7 +122,7 @@ class QueueView(discord.ui.View): "live", "queueTitle", "historyTitle", - "viewFooter", + "playlistViewFooter", ) embed = discord.Embed(title=texts[0], color=func.settings.embed_color) diff --git a/views/utils/__init__.py b/views/utils/__init__.py index c2902b7..a0d3fda 100644 --- a/views/utils/__init__.py +++ b/views/utils/__init__.py @@ -22,4 +22,5 @@ SOFTWARE. """ from .pagination import Pagination -from .modal import BaseModal \ No newline at end of file +from .modal import BaseModal +from .dynamic_view_manager import DynamicViewManager diff --git a/views/utils/dynamic_view_manager.py b/views/utils/dynamic_view_manager.py new file mode 100644 index 0000000..2a9bb4c --- /dev/null +++ b/views/utils/dynamic_view_manager.py @@ -0,0 +1,89 @@ +"""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 = None + + def current_view(self) -> discord.ui.View | None: + """Get the current active view.""" + return self._current_view + + 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 diff --git a/views/utils/pagination.py b/views/utils/pagination.py index 7bfdbd1..3a48070 100644 --- a/views/utils/pagination.py +++ b/views/utils/pagination.py @@ -57,6 +57,28 @@ class Pagination(Generic[T]): 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. @@ -77,7 +99,12 @@ class Pagination(Generic[T]): self._current_page += 1 def go_page(self, page_number: int) -> None: - """Navigate to a specific page, clamped between 0 and total_pages.""" + """ + 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 @@ -129,3 +156,13 @@ class Pagination(Generic[T]): 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) \ No newline at end of file From ef54f96f51de5ce8bd5bc0412b70d8767b542c84 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:39:19 +0800 Subject: [PATCH 03/10] Migrate scripts to voicelink, rewrite handlers and views --- addons/__init__.py | 3 - addons/placeholders.py | 63 --- addons/settings.py | 59 -- cogs/basic.py | 315 ++++++----- cogs/effect.py | 32 +- cogs/listeners.py | 26 +- cogs/playlist.py | 185 ++++--- cogs/settings.py | 133 +++-- cogs/task.py | 10 +- function.py | 283 +--------- ipc/client.py | 4 +- ipc/methods.py | 65 ++- langs/CH.json | 171 ------ langs/ZHCN.json | 4 +- langs/ZHTW.json | 4 +- local_langs/es-419.json | 2 +- local_langs/es-ES.json | 2 +- local_langs/zh-CN.json | 2 +- local_langs/zh-TW.json | 2 +- main.py | 100 ++-- settings Example.json | 5 + update.py | 2 +- views/lyrics.py | 110 ---- views/queue.py | 199 ------- voicelink/__init__.py | 11 +- voicelink/config.py | 134 +++++ voicelink/language.py | 133 +++++ {addons => voicelink}/lyrics.py | 11 +- voicelink/mongodb.py | 514 ++++++++++++++++++ voicelink/objects.py | 28 +- voicelink/placeholders.py | 130 +++-- voicelink/player.py | 47 +- voicelink/pool.py | 2 +- voicelink/queue.py | 11 +- voicelink/ratelimit.py | 3 +- voicelink/utils.py | 224 +++++++- {views => voicelink/views}/__init__.py | 24 +- {views => voicelink/views}/controller.py | 59 +- {views => voicelink/views}/debug.py | 25 +- .../views/embed_builder.py | 40 +- {views => voicelink/views}/help.py | 6 +- {views => voicelink/views}/inbox.py | 8 +- {views => voicelink/views}/link.py | 2 +- voicelink/views/lyrics.py | 81 +++ voicelink/views/pagination.py | 107 ++++ {views => voicelink/views}/playlist.py | 163 ++---- voicelink/views/queue.py | 132 +++++ {views => voicelink/views}/search.py | 5 +- {views => voicelink/views}/utils/__init__.py | 0 .../views}/utils/dynamic_view_manager.py | 9 +- {views => voicelink/views}/utils/modal.py | 0 .../views}/utils/pagination.py | 0 52 files changed, 2048 insertions(+), 1642 deletions(-) delete mode 100644 addons/__init__.py delete mode 100644 addons/placeholders.py delete mode 100644 addons/settings.py delete mode 100644 langs/CH.json delete mode 100644 views/lyrics.py delete mode 100644 views/queue.py create mode 100644 voicelink/config.py create mode 100644 voicelink/language.py rename {addons => voicelink}/lyrics.py (99%) create mode 100644 voicelink/mongodb.py rename {views => voicelink/views}/__init__.py (88%) rename {views => voicelink/views}/controller.py (89%) rename {views => voicelink/views}/debug.py (95%) rename views/embedBuilder.py => voicelink/views/embed_builder.py (92%) rename {views => voicelink/views}/help.py (97%) rename {views => voicelink/views}/inbox.py (98%) rename {views => voicelink/views}/link.py (99%) create mode 100644 voicelink/views/lyrics.py create mode 100644 voicelink/views/pagination.py rename {views => voicelink/views}/playlist.py (63%) create mode 100644 voicelink/views/queue.py rename {views => voicelink/views}/search.py (95%) rename {views => voicelink/views}/utils/__init__.py (100%) rename {views => voicelink/views}/utils/dynamic_view_manager.py (99%) rename {views => voicelink/views}/utils/modal.py (100%) rename {views => voicelink/views}/utils/pagination.py (100%) 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 6454ad7..2475d6a 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, QueueView, 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, 'noTrackPlaying', ephemeral=True) - texts = await get_lang(ctx.guild.id, "nowplayingDesc", "nowplayingField", "nowplayingLink") + texts = await LangHandler.get_lang(ctx.guild.id, "nowplayingDesc", "nowplayingField", "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: @@ -103,7 +97,7 @@ class Basic(commands.Cog): 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] - history = {track["identifier"]: track for track_id in reversed(await get_user(interaction.user.id, "history")) if (track := voicelink.decode(track_id))["uri"]} + 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"🕒 {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")) @@ -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, "alreadyConnected") - await send(ctx, 'connect', player.channel) + await send_localized_message(ctx, 'connect', player.channel) @commands.hybrid_command(name="play", aliases=get_aliases("play")) @app_commands.describe( @@ -133,27 +127,26 @@ 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, "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, "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, "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, "live", "trackLoad_pos", "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 +169,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, "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, "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, "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, "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, "live", "trackLoad_pos", "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 +211,31 @@ 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): + async def search(self, ctx: commands.Context, *, query: str, platform: str = voicelink.SearchType.YOUTUBE.name): "Searches your query and displays the results." 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, "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, "noLinkSupport", ephemeral=True) - search_type: SearchType = SearchType.match(platform) or SearchType.YOUTUBE + search_type: voicelink.SearchType = voicelink.SearchType.match(platform) or voicelink.SearchType.YOUTUBE 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, "noTrackFound") - texts = await get_lang(ctx.guild.id, "searchTitle", "searchDesc", "live", "trackLoad_pos", "trackLoad", "searchWait", "searchSuccess") + texts = await LangHandler.get_lang(ctx.guild.id, "searchTitle", "searchDesc", "live", "trackLoad_pos", "trackLoad", "searchWait", "searchSuccess") 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 +244,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() @@ -271,27 +264,27 @@ 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, "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, "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, "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, "live", "trackLoad_pos", "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 +308,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, "missingFunctionPerm", 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, "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, "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, "live", "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 +344,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, "noPlayer", ephemeral=True) if player.is_paused: - return await send(ctx, "pauseError", ephemeral=True) + return await send_localized_message(ctx, "pauseError", 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, "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, "pauseVote", 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, "paused", ctx.author) @commands.hybrid_command(name="resume", aliases=get_aliases("resume")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -373,21 +366,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, "noPlayer", ephemeral=True) if not player.is_paused: - return await send(ctx, "resumeError") + return await send_localized_message(ctx, "resumeError") 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, "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, "resumeVote", 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, "resumed", 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 +389,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, "noPlayer", ephemeral=True) if not player.node._available: - return await send(ctx, "nodeReconnect") + return await send_localized_message(ctx, "nodeReconnect") if not player.is_playing: - return await send(ctx, "skipError", ephemeral=True) + return await send_localized_message(ctx, "skipError", 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, "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, "skipVote", ctx.author, len(player.skip_votes), required) if index: player.queue.skipto(index) - await send(ctx, "skipped", ctx.author) + await send_localized_message(ctx, "skipped", ctx.author) if player.queue._repeat.mode == voicelink.LoopType.TRACK: await player.set_repeat(voicelink.LoopType.OFF) @@ -430,18 +423,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, "noPlayer", ephemeral=True) if not player.node._available: - return await send(ctx, "nodeReconnectode") + return await send_localized_message(ctx, "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, "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, "backVote", ctx.author, len(player.previous_votes), required) if not player.is_playing: player.queue.backto(index) @@ -450,7 +443,7 @@ class Basic(commands.Cog): player.queue.backto(index + 1) await player.stop() - await send(ctx, "backed", ctx.author) + await send_localized_message(ctx, "backed", ctx.author) if player.queue._repeat.mode == voicelink.LoopType.TRACK: await player.set_repeat(voicelink.LoopType.OFF) @@ -461,19 +454,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "missingPosPerm", ephemeral=True) if not player.current or player.position == 0: - return await send(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "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, "timeFormatError", ephemeral=True) await player.seek(num, ctx.author) - await send(ctx, "seek", position) + await send_localized_message(ctx, "seek", position) @commands.hybrid_group( name="queue", @@ -486,15 +479,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, "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, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True) if player.queue.is_empty: return await nowplay(ctx, player) view = QueueView(player=player, author=ctx.author) - view.response = await send(ctx, await view.build_embed(), view=view) + 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 +495,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, "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, "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, "noTrackPlaying", ephemeral=True) await ctx.defer() @@ -518,7 +511,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 +520,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 +535,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, "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, "noTrackFound") index = await player.add_track(tracks) - await send(ctx, "playlistLoad", attachment.filename, index) + await send_localized_message(ctx, "playlistLoad", attachment.filename, index) except Exception as e: logger.error("error", exc_info=e) raise e @@ -569,16 +562,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, "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, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True) if not player.queue.history(): return await nowplay(ctx, player) view = QueueView(player=player, author=ctx.author, is_queue=False) - view.response = await send(ctx, await view.build_embed(), view=view) + 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 +579,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, "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, "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, "leaveVote", ctx.author, len(player.stop_votes), required) - await send(ctx, "left", ctx.author) + await send_localized_message(ctx, "left", ctx.author) await player.teardown() @commands.hybrid_command(name="nowplaying", aliases=get_aliases("nowplaying")) @@ -607,10 +600,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, "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, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True) await nowplay(ctx, player) @@ -618,20 +611,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingModePerm", ephemeral=True) + return await send_localized_message(ctx, "missingModePerm", 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, "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 +637,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingQueuePerm", ephemeral=True) + return await send_localized_message(ctx, "missingQueuePerm", ephemeral=True) await player.clear_queue(queue, ctx.author) - await send(ctx, "cleared", queue.capitalize()) + await send_localized_message(ctx, "cleared", queue.capitalize()) @commands.hybrid_command(name="remove", aliases=get_aliases("remove")) @app_commands.describe( @@ -663,13 +656,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingQueuePerm", ephemeral=True) + return await send_localized_message(ctx, "missingQueuePerm", 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, "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 +671,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "missingPosPerm", ephemeral=True) if not player.current: - return await send(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "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, "timeFormatError", ephemeral=True) await player.seek(int(player.position + num)) - await send(ctx, "forward", ctime(player.position + num)) + await send_localized_message(ctx, "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 +692,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "missingPosPerm", ephemeral=True) if not player.current: - return await send(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "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, "timeFormatError", ephemeral=True) await player.seek(int(player.position - num)) - await send(ctx, "rewind", ctime(player.position - num)) + await send_localized_message(ctx, "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 +712,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "missingPosPerm", ephemeral=True) if not player.current: - return await send(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True) await player.seek(0) - await send(ctx, "replay") + await send_localized_message(ctx, "replay") @commands.hybrid_command(name="shuffle", aliases=get_aliases("shuffle")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -736,18 +729,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, "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, "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, "shuffleVote", ctx.author, len(player.shuffle_votes), required) await player.shuffle("queue", ctx.author) - await send(ctx, "shuffled") + await send_localized_message(ctx, "shuffled") @commands.hybrid_command(name="swap", aliases=get_aliases("swap")) @app_commands.describe( @@ -759,13 +752,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "missingPosPerm", 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, "swapped", track1.title, track2.title) @commands.hybrid_command(name="move", aliases=get_aliases("move")) @app_commands.describe( @@ -777,13 +770,13 @@ 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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "missingPosPerm", ephemeral=True) moved_track = await player.move_track(target, to, ctx.author) - await send(ctx, "moved", moved_track, to) + await send_localized_message(ctx, "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.") @@ -793,20 +786,20 @@ class Basic(commands.Cog): 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, "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, "lyricsNotFound", 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 +808,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, "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, "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, "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, "djToMe", ephemeral=True) if member not in player.channel.members: - return await send(ctx, "djNotInChannel", member, ephemeral=True) + return await send_localized_message(ctx, "djNotInChannel", member, ephemeral=True) player.dj = member - await send(ctx, "djswap", member) + await send_localized_message(ctx, "djswap", member) @commands.hybrid_command(name="autoplay", aliases=get_aliases("autoplay")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -838,14 +831,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, "noPlayer", ephemeral=True) if not player.is_privileged(ctx.author): - return await send(ctx, "missingAutoPlayPerm", ephemeral=True) + return await send_localized_message(ctx, "missingAutoPlayPerm", 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, "autoplay", await LangHandler.get_lang(ctx.guild.id, "enabled" if check else "disabled")) if not player.is_playing: await player.do_next() @@ -862,7 +855,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 +863,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, "pingTitle1", "pingField1", "pingTitle2", "pingField2") - 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 +880,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..48fb338 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, "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, "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, "addEffect", 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, "addEffect", 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, "addEffect", 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, "addEffect", effect.tag) @commands.hybrid_command(name="rotation", aliases=get_aliases("rotation")) @app_commands.describe(hertz="The hertz of the rotation. Default is `0.2`") @@ -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, "addEffect", 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, "addEffect", effect.tag) @commands.hybrid_command(name="lowpass", aliases=get_aliases("lowpass")) @app_commands.describe(smoothing="The level of the lowPass. Default is `20.0`") @@ -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, "addEffect", 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, "addEffect", 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, "addEffect", 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, "addEffect", 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, "addEffect", 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, "clearEffect") 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..a423920 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, _): diff --git a/cogs/playlist.py b/cogs/playlist.py index 5a8fe17..2189251 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -21,23 +21,22 @@ 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 discord import app_commands from discord.ext import commands from function import ( - send, - time as ctime, - get_user, - update_user, - check_roles, get_aliases, cooldown_check, logger ) -from views import PlaylistViewManager, 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): @@ -46,7 +45,7 @@ def assign_playlist_id(existed: list) -> str: async def check_playlist_perms(user_id: int, author_id: int, playlist_id: str) -> dict: """Check if user has read permissions for a specific playlist.""" - user_data = await get_user(author_id, 'playlist') + user_data = await MongoDBHandler.get_user(author_id, d_type='playlist') playlist = user_data.get(playlist_id) if not playlist or user_id not in playlist['perms']['read']: @@ -56,7 +55,7 @@ async def check_playlist_perms(user_id: int, author_id: int, playlist_id: str) - async def check_playlist(ctx: commands.Context, name: str = None, full: bool = False, share: bool = True) -> dict: """Get user's playlist data with various filtering options.""" - user_playlists = await get_user(ctx.author.id, 'playlist') + user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist') if not ctx.interaction.response.is_done(): await ctx.defer() @@ -90,7 +89,7 @@ async def search_playlist(url: str, requester: discord.Member, time_needed: bool result = {"name": tracks.name, "tracks": tracks.tracks} if time_needed: - result["time"] = ctime(sum(track.length for track in tracks.tracks)) + result["time"] = format_ms(sum(track.length for track in tracks.tracks)) return result except Exception: @@ -135,7 +134,7 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist ) if not shared_playlist: - await update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}}) + await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}}) return None if shared_playlist['type'] == 'link': @@ -157,14 +156,14 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist decoded_tracks = [] total_time = 0 for track in shared_playlist['tracks']: - decoded_track = voicelink.decode(track) + 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': ctime(total_time), + 'time': format_ms(total_time), 'name': playlist_data['name'], 'tracks': decoded_tracks, 'perms': shared_playlist['perms'], @@ -175,14 +174,14 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist decoded_tracks = [] total_time = 0 for track in playlist_data['tracks']: - decoded_track = voicelink.decode(track) + 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': ctime(total_time), + 'time': format_ms(total_time), 'name': playlist_data['name'], 'tracks': decoded_tracks, 'perms': playlist_data['perms'], @@ -196,7 +195,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] @@ -210,7 +209,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( @@ -224,10 +223,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, 'playlistNotFound', 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, 'playlistNotAccess', ephemeral=True) player: voicelink.Player = ctx.guild.voice_client if not player: @@ -237,21 +236,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, 'playlistNoTrack', 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, 'playlistNoTrack', 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, 'playlistPlay', result['playlist']['name'], len(tracks['tracks'][:max_t])) if not player.is_playing: await player.do_next() @@ -261,13 +260,13 @@ class Playlists(commands.Cog, name="playlist"): async def view(self, ctx: commands.Context) -> None: """List all your playlists and all songs in your favourite playlist.""" user_playlists = await check_playlist(ctx, full=True) - _, max_playlists, _ = check_roles() + max_p, _, _ = Config().get_playlist_config() playlist_results = [] for index, playlist_id in enumerate(user_playlists, start=1): playlist_data = user_playlists[playlist_id] - is_locked = max_playlists < index + is_locked = max_p < index try: result = await _process_playlist(ctx, playlist_data, playlist_id, is_locked) @@ -284,7 +283,7 @@ class Playlists(commands.Cog, name="playlist"): }) view = PlaylistViewManager(ctx, playlist_results) - view.response = await send(ctx, content=await view.build_embed(), view=view, ephemeral=True) + view.response = await dispatch_message(ctx, content=await view.build_embed(), view=view, ephemeral=True) @playlist.command(name="create", aliases=get_aliases("create")) @app_commands.describe( @@ -295,25 +294,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, 'playlistOverText', 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, 'overPlaylistCreation', 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, 'playlistExists', 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, "playlistNotInvalidUrl", 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, "playlistCreated", name) @playlist.command(name="delete", aliases=get_aliases("delete")) @app_commands.describe(name="The name of the playlist.") @@ -323,15 +322,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, "playlistNotFound", name, ephemeral=True) if result['id'] == "200": - return await send(ctx, "playlistDeleteError", ephemeral=True) + return await send_localized_message(ctx, "playlistDeleteError", 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, "playlistRemove", result["playlist"]["name"]) @playlist.command(name="share", aliases=get_aliases("share")) @app_commands.describe( @@ -343,28 +342,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, 'playlistSendErrorPlayer', ephemeral=True) if member.bot: - return await send(ctx, 'playlistSendErrorBot', ephemeral=True) + return await send_localized_message(ctx, 'playlistSendErrorBot', 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, 'playlistNotFound', name, ephemeral=True) if result['playlist']['type'] == 'share': - return await send(ctx, 'playlistBelongs', result['playlist']['user'], ephemeral=True) + return await send_localized_message(ctx, 'playlistBelongs', 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, 'playlistShare', 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, 'noPlaylistAcc', 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, 'playlistSent', ephemeral=True) if len(receiver['inbox']) >= 10: - return await send(ctx.guild.id, 'inboxFull', member, ephemeral=True) + return await send_localized_message(ctx.guild.id, 'inboxFull', member, ephemeral=True) - await update_user( + await MongoDBHandler.update_user( member.id, {"$push": {"inbox": { 'sender': ctx.author.id, @@ -375,7 +374,7 @@ class Playlists(commands.Cog, name="playlist"): 'type': 'invite' }}} ) - return await send(ctx, "invitationSent", member) + return await send_localized_message(ctx, "invitationSent", member) @playlist.command(name="rename", aliases=get_aliases("rename")) @app_commands.describe( @@ -387,36 +386,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, 'playlistOverText', ephemeral=True) if name.lower() == newname.lower(): - return await send(ctx, 'playlistSameName', ephemeral=True) + return await send_localized_message(ctx, 'playlistSameName', 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, 'playlistExists', ephemeral=True) if not found: - return await send(ctx.guild.id, 'playlistNotFound', name, ephemeral=True) + return await send_localized_message(ctx.guild.id, 'playlistNotFound', 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, 'playlistRenamed', 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, "inboxNoMsg", 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']: @@ -425,7 +424,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'])))}", @@ -435,7 +434,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) @@ -448,26 +447,26 @@ class Playlists(commands.Cog, name="playlist"): "Add tracks in to your custom playlist." 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, 'playlistNotFound', name, ephemeral=True) if result['playlist']['type'] in ['share', 'link']: - return await send(ctx, 'playlistNotAllow', ephemeral=True) + return await send_localized_message(ctx, 'playlistNotAllow', 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, 'playlistLimitTrack', 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, 'noTrackFound') if isinstance(results, voicelink.Playlist): - return await send(ctx, 'playlistPlaylistLink', ephemeral=True) + return await send_localized_message(ctx, 'playlistPlaylistLink', ephemeral=True) if results[0].is_stream: - return await send(ctx, 'playlistStream', ephemeral=True) + return await send_localized_message(ctx, 'playlistStream', 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']) + await MongoDBHandler.update_user(ctx.author.id, {"$push": {f'playlist.{result["id"]}.tracks': results[0].track_id}}) + await send_localized_message(ctx, 'playlistAdded', results[0].title, ctx.author, result['playlist']['name']) @playlist.command(name="remove", aliases=get_aliases("remove")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -480,16 +479,16 @@ class Playlists(commands.Cog, name="playlist"): "Remove song from your favorite playlist." 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, 'playlistNotFound', name, ephemeral=True) if result['playlist']['type'] in ['link', 'share']: - return await send(ctx, 'playlistNotAllow', ephemeral=True) + return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True) if not 0 < position <= len(result['playlist']['tracks']): - return await send(ctx, 'playlistPositionNotFound', position, name) + return await send_localized_message(ctx, 'playlistPositionNotFound', position, name) - await update_user(ctx.author.id, {"$pull": {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}}) + await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f'playlist.{result["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]) + await send_localized_message(ctx, 'playlistRemoved', track.get("title"), ctx.author, name) @playlist.command(name="clear", aliases=get_aliases("clear")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -498,13 +497,13 @@ class Playlists(commands.Cog, name="playlist"): "Remove all songs from your favorite playlist." 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, 'playlistNotFound', name, ephemeral=True) if result['playlist']['type'] in ['link', 'share']: - return await send(ctx, 'playlistNotAllow', ephemeral=True) + return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True) - await update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}}) - await send(ctx, 'playlistClear', name) + await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}}) + await send_localized_message(ctx, 'playlistClear', name) @playlist.command(name="export", aliases=get_aliases("export")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -513,29 +512,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, 'playlistNotFound', 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, 'playlistNoTrack', 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, 'playlistNoTrack', 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 += "," @@ -544,7 +543,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 @@ -556,17 +555,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, 'playlistOverText', 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, 'overPlaylistCreation', 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, 'playlistExists', name, ephemeral=True) try: bytes = await attachment.read() @@ -574,8 +573,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, 'playlistCreated', name) except Exception as e: logger.error("Decode Error", exc_info=e) diff --git a/cogs/settings.py b/cogs/settings.py index b3834fd..e38c8e0 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -29,18 +29,14 @@ 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 def status_icon(status: bool) -> str: return "✅" if status else "❌" @@ -58,7 +54,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 +62,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, "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, "setPrefix", prefix, prefix) @settings.command(name="language", aliases=get_aliases("language")) @commands.has_permissions(manage_guild=True) @@ -77,25 +73,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, "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, 'changedLanguage', 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, 'setDJ', f"<@&{role.id}>" if role else "None") @settings.command(name="queue", aliases=get_aliases("queue")) @app_commands.choices(mode=[ @@ -107,57 +103,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, "setQueue", 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, '247', await LangHandler.get_lang(ctx.guild.id, "enabled" if not toggle else "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, 'bypassVote', await LangHandler.get_lang(ctx.guild.id, "enabled" if not toggle else "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, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsTitle3", "settingsPermTitle", "settingsPermValue") + 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('prefix', voicelink.Config().bot_prefix) or 'None', settings.get('lang', 'EN'), 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 +169,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 +181,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, 'setVolume', 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 +199,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, 'toggleController', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "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, "toggleDuplicateTrack", await LangHandler.get_lang(ctx.guild.id, "disabled" if toggle else "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, 'toggleControllerMsg', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "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, 'toggleSilentMsg', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled")) @settings.command(name="stageannounce", aliases=get_aliases("stageannounce")) @commands.has_permissions(manage_guild=True) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def stageannounce(self, ctx: commands.Context, template: str = None): "Customize the channel topic template" - await update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}}) - await send(ctx, "setStageAnnounceTemplate") + await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}}) + await send_localized_message(ctx, "setStageAnnounceTemplate") @settings.command(name="setupchannel", aliases=get_aliases("setupchannel")) @app_commands.describe( @@ -270,7 +267,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, "missingIntents", "MESSAGE_CONTENT", ephemeral=True) if not channel: try: @@ -282,25 +279,25 @@ 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, "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, "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, "createSongRequestChannel", channel.mention) @app_commands.command(name="debug") async def debug(self, interaction: discord.Interaction): - if interaction.user.id not in func.settings.bot_access_user: + 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!") memory = psutil.virtual_memory() @@ -308,7 +305,7 @@ class Settings(commands.Cog, name="settings"): 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 +313,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 +332,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..fd0f030 100644 --- a/cogs/task.py +++ b/cogs/task.py @@ -26,7 +26,6 @@ import discord import function as func from discord.ext import commands, tasks -from addons import Placeholders class Task(commands.Cog): def __init__(self, bot: commands.Bot): @@ -36,7 +35,7 @@ class Task(commands.Cog): 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() @@ -48,7 +47,7 @@ class Task(commands.Cog): await self.bot.wait_until_ready() try: - act_data = func.settings.activity[(self.current_act + 1) % len(func.settings.activity) - 1] + act_data = voicelink.Config().activity[(self.current_act + 1) % len(voicelink.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,7 +57,7 @@ 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(voicelink.Config().activity) func.logger.info(f"Changed the bot status to {act_name}") @@ -104,8 +103,7 @@ class Task(commands.Cog): @tasks.loop(hours=12.0) 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/function.py b/function.py index 68dd2f9..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 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 "") - -def _get_lang(lang: str, *keys) -> Optional[Union[list[str], str]]: - if lang not in LANGS: - lang = "EN" - - if not LANGS[lang]: - LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) - - lang_dict = LANGS[lang] - - if len(keys) == 1: - return lang_dict.get(keys[0], "Not found!") - return [lang_dict.get(key, "Not found!") for key in keys] - -async def get_lang(guild_id:int, *keys) -> Optional[Union[list[str], str]]: - settings = await get_settings(guild_id) - return _get_lang(settings.get("lang"), *keys) - -async def send( - ctx: Union[commands.Context, discord.Interaction], - content: Union[str, discord.Embed] = None, - *params, - view: discord.ui.View = None, - file: discord.File = 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, - "file": file, - "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/ipc/client.py b/ipc/client.py index de0c460..b1b079c 100644 --- a/ipc/client.py +++ b/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 @@ -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/ipc/methods.py index 9a93a17..4f5c86e 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -1,12 +1,11 @@ import time, re -import function as func from typing import List, Dict, Union, Optional -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 RATELIMIT_COUNTER: Dict[int, Dict[str, float]] = {} SCOPES = { @@ -62,8 +61,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 +85,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,7 +145,7 @@ 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) @@ -212,7 +211,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", [])] @@ -274,7 +273,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: @@ -358,13 +357,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 +390,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 +402,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", @@ -436,7 +435,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: 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 +449,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 +472,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 +483,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 +503,15 @@ 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() + max_p, max_t, _ = Config().get_playlist_config() 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 +539,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 +553,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 +570,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 +581,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 +597,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 +630,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 +640,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 +655,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 +696,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), diff --git a/langs/CH.json b/langs/CH.json deleted file mode 100644 index 160bc34..0000000 --- a/langs/CH.json +++ /dev/null @@ -1,171 +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": "```名稱: {0} [{1}]\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```", - "playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}", - "playlistViewPermsValue2": "📖 讀取: {0}", - "playlistViewTrack": "音軌", - "playlistViewFooter": "總長度: {0}", - "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}", - "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/ZHCN.json b/langs/ZHCN.json index 4aae7f3..b86c964 100644 --- a/langs/ZHCN.json +++ b/langs/ZHCN.json @@ -38,7 +38,7 @@ "addEffect": "已应用 `{0}` 音效。", "clearEffect": "音效已清除!", "filterTagAlreadyInUse": "该音效已在使用中!请使用 /cleareffect <标签> 移除。", - "playlistViewTitle": "📜 {0} 的所有播放列表", + "playlistViewTitle": "{0} 的所有播放列表", "playlistViewHeaders": "ID:,时长:,名称:,曲目数:", "playlistFooter": "输入 /playlist play [播放列表] 将其添加到队列中。", "playlistNotFound": "未找到播放列表 [`{0}`]。输入 /playlist view 查看所有播放列表。", @@ -72,6 +72,7 @@ "playlistViewPermsValue": "📖 读取: ✓ ✍🏽 写入: {0} 🗑️ 删除: {1}", "playlistViewPermsValue2": "📖 读取: {0}", "playlistViewTrack": "曲目列表", + "playlistViewFooter": "总持续时间:{0}", "playlistViewPage": "第 {0}/{1} 页 | 总时长: {2}", "inboxFull": "抱歉!{0} 的收件箱已满。", "inboxNoMsg": "您的收件箱中没有消息。", @@ -129,7 +130,6 @@ "historyTitle": "播放历史:", "viewTitle": "音乐队列", "viewDesc": "**正在播放: [点我]({0}) ⮯**\n{1}", - "viewFooter": "第 {0}/{1} 页 | 总时长: {2}", "pauseError": "播放器已暂停。", "pauseVote": "{0} 投票暂停了歌曲。[{1}/{2}]", "paused": "`{0}` 已暂停播放器。", diff --git a/langs/ZHTW.json b/langs/ZHTW.json index 7c5de56..69f991b 100644 --- a/langs/ZHTW.json +++ b/langs/ZHTW.json @@ -38,7 +38,7 @@ "addEffect": "已套用 `{0}` 效果。", "clearEffect": "音效已清除!", "filterTagAlreadyInUse": "該音效已在使用中!請使用 /cleareffect <標籤> 移除。", - "playlistViewTitle": "📜 {0} 的所有播放清單", + "playlistViewTitle": "{0} 的所有播放清單", "playlistViewHeaders": "ID:,時長:,名稱:,曲目數:", "playlistFooter": "輸入 /playlist play [播放清單] 將其加入隊列。", "playlistNotFound": "未找到播放清單 [`{0}`]。輸入 /playlist view 查看所有播放清單。", @@ -72,6 +72,7 @@ "playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 寫入: {0} 🗑️ 移除: {1}", "playlistViewPermsValue2": "📖 讀取: {0}", "playlistViewTrack": "曲目列表", + "playlistViewFooter": "總持續時間:{0}", "playlistViewPage": "第 {0}/{1} 頁 | 總時長: {2}", "inboxFull": "抱歉!{0} 的收件匣已滿。", "inboxNoMsg": "您的收件匣中沒有訊息。", @@ -129,7 +130,6 @@ "historyTitle": "播放歷史:", "viewTitle": "音樂隊列", "viewDesc": "**正在播放: [點我]({0}) ⮯**\n{1}", - "viewFooter": "第 {0}/{1} 頁 | 總時長: {2}", "pauseError": "播放器已暫停。", "pauseVote": "{0} 投票暫停了歌曲。[{1}/{2}]", "paused": "`{0}` 已暫停播放器。", diff --git a/local_langs/es-419.json b/local_langs/es-419.json index 6aed14d..c5f616e 100644 --- a/local_langs/es-419.json +++ b/local_langs/es-419.json @@ -109,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.", diff --git a/local_langs/es-ES.json b/local_langs/es-ES.json index 4580dba..831a9a3 100644 --- a/local_langs/es-ES.json +++ b/local_langs/es-ES.json @@ -109,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.", diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json index fabe021..43e37a6 100644 --- a/local_langs/zh-CN.json +++ b/local_langs/zh-CN.json @@ -109,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.": "为您的播放列表命名。", diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json index 35fbdae..b0ee859 100644 --- a/local_langs/zh-TW.json +++ b/local_langs/zh-TW.json @@ -109,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.": "給您的播放列表命名。", diff --git a/main.py b/main.py index ed9a483..f16290f 100644 --- a/main.py +++ b/main.py @@ -27,35 +27,42 @@ 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, 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): @@ -77,7 +84,7 @@ 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) @@ -91,34 +98,16 @@ class Vocard(commands.Bot): 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 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 = IPCClient(self, **bot_config.ipc_client) + if bot_config.ipc_client.get("enable", False): try: await self.ipc.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, "unknownException") + 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: @@ -206,13 +197,14 @@ class CommandCheck(discord.app_commands.CommandTree): return True async def get_prefix(bot: commands.Bot, message: discord.Message) -> str: - settings = await func.get_settings(message.guild.id) - return settings.get("prefix", func.settings.bot_prefix) + settings = await MongoDBHandler.get_settings(message.guild.id) + return settings.get("prefix", bot_config.bot_prefix) # 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): @@ -229,8 +221,8 @@ 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 bot = Vocard( @@ -245,4 +237,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/settings Example.json b/settings Example.json index 3a359b6..937df06 100644 --- a/settings Example.json +++ b/settings Example.json @@ -48,6 +48,11 @@ "secure": false, "enable": false }, + "playlist_settings": { + "max_playlist": 5, + "max_tracks_per_playlist": 500, + "default_playlist_name": "Favourite" + }, "sources_settings": { "youtube": { "emoji": "<:youtube:826661982760992778>", diff --git a/update.py b/update.py index a6452ba..330125e 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.3b1" # URLs for update and migration PYTHON_CMD_NAME = os.path.basename(sys.executable) 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/queue.py b/views/queue.py deleted file mode 100644 index ad921c3..0000000 --- a/views/queue.py +++ /dev/null @@ -1,199 +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 typing import TYPE_CHECKING - -from .utils import Pagination, BaseModal - -if TYPE_CHECKING: - from voicelink import Player, Track - - -class QueueView(discord.ui.View): - """ - 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__(timeout=60) - self.player: Player = player - self.author: discord.Member = author - self.is_queue: bool = is_queue - self.response: discord.Message = None - - self.pagination = Pagination["Track"]( - items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())), - page_size=7, - ) - - self.total_duration = self.calculate_total_duration() - self.update_view() - - def calculate_total_duration(self) -> str: - """Calculate the total duration of the tracks.""" - try: - return func.time(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 func.time(track.length)}]` " - f"[{func.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}" - - def update_view(self) -> None: - """Update button states and page number display based on current pagination state.""" - button_states = { - "fast_back": self.pagination.current_page <= 2, - "back": not self.pagination.has_previous_page, - "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, - "next": not self.pagination.has_next_page, - } - - for child in self.children: - if child.custom_id in button_states: - child.disabled = button_states[child.custom_id] - if child.custom_id == "page_number": - child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" - - 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 interaction_check(self, interaction: discord.Interaction) -> bool: - """Ensure only the author of the view can interact with it.""" - return interaction.user == self.author - - 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 func.get_lang( - self.author.guild.id, - "viewTitle", - "viewDesc", - "nowplayingDesc", - "live", - "queueTitle", - "historyTitle", - "playlistViewFooter", - ) - - embed = discord.Embed(title=texts[0], color=func.settings.embed_color) - embed.description = self.format_description(tracks, texts) - - embed.set_footer( - text=texts[6].format( - self.pagination.current_page, - self.pagination.total_pages, - self.total_duration, - ) - ) - - return embed - - async def update_and_edit_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.response.id, embed=await self.build_embed(), view=self) - else: - await interaction.response.edit_message(embed=await self.build_embed(), view=self) - - @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_and_edit_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_and_edit_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.""" - modal = BaseModal( - title="Page Number", - custom_id="page_number_modal", - items=[ - discord.ui.TextInput( - label="Page Number", - custom_id="page_number", - placeholder="Enter the page number to navigate.", - 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_and_edit_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_and_edit_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_and_edit_message(interaction) diff --git a/voicelink/__init__.py b/voicelink/__init__.py index bedf3f3..a9df3de 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -21,18 +21,21 @@ 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 .config import Config from .enums import SearchType, LoopType 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 diff --git a/voicelink/config.py b/voicelink/config.py new file mode 100644 index 0000000..b93a44c --- /dev/null +++ b/voicelink/config.py @@ -0,0 +1,134 @@ +"""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 +) + +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.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.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/language.py b/voicelink/language.py new file mode 100644 index 0000000..01d7839 --- /dev/null +++ b/voicelink/language.py @@ -0,0 +1,133 @@ +"""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 + +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]] = {} + + @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] = 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 _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. + """ + lang = lang.upper() + if lang not in cls._langs: + lang = "EN" + + 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", "EN") + 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..0340400 --- /dev/null +++ b/voicelink/mongodb.py @@ -0,0 +1,514 @@ +"""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 + +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 + async def get_settings( + cls, + guild_id: int, + *, + 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() + + return copy.deepcopy(cls._settings_buffer[guild_id]) + + 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) + 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 = {"_id": user_id, **copy.deepcopy(cls._user_base)} + 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..6b51301 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -22,18 +22,16 @@ SOFTWARE. """ import re -from typing import Optional +import discord -from discord import Member +from typing import Optional from tldextract import extract +from discord import Member from .enums import SearchType -from function import ( - get_source, - time as ctime -) - -from .transformer import encode +from .config import Config +from .utils import format_ms +from .transformer import encode, decode YOUTUBE_REGEX = re.compile(r'(https?://)?(www\.)?youtube\.(com|nl)/watch\?v=([-\w]+)') @@ -83,7 +81,7 @@ class Track: 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 @@ -114,7 +112,7 @@ class Track: @property def formatted_length(self) -> str: - return ctime(self.length) + return format_ms(self.length) @property def data(self) -> dict: @@ -123,6 +121,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 +168,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..030a821 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("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 5a07c80..01dfe2c 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -22,11 +22,10 @@ SOFTWARE. """ import time, logging -import function as func from math import ceil from asyncio import sleep -from views import InteractiveController +from random import shuffle, choice from typing import Any, Dict, List, Optional, Union, Tuple from discord import ( @@ -45,18 +44,22 @@ 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 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, "noChannel", "noPermission") try: channel = channel or ctx.author.voice.channel if isinstance(ctx, commands.Context) else ctx.user.voice.channel except: @@ -66,7 +69,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, @@ -116,7 +119,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,7 +148,7 @@ 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 def __repr__(self): @@ -258,7 +261,7 @@ class Player(VoiceProtocol): """Retrieves a localized message or list of messages based on the given keys for the guild associated with this player. """ - return func._get_lang(self.settings.get("lang"), *keys) + return LangHandler._get_lang(self.settings.get("lang"), *keys) def required(self, leave=False): """ @@ -291,7 +294,7 @@ 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 @@ -304,10 +307,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.""" @@ -424,7 +427,7 @@ class Player(VoiceProtocol): 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}} })) @@ -463,11 +466,11 @@ 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, 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) + self.controller = await dispatch_message(self.context, content=embed, view=view, requires_fetch=True) else: await self.controller.edit(embed=embed, view=view) @@ -495,7 +498,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) }}) @@ -611,9 +614,9 @@ class Player(VoiceProtocol): 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("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("invalidEndTime", format_ms(track_length))) track.position = start_time track.end_time = end_time @@ -864,7 +867,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 diff --git a/voicelink/pool.py b/voicelink/pool.py index ec25f31..8197e3e 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -509,7 +509,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..f7f4efc 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) @@ -151,7 +152,7 @@ class Queue: except: raise OutofList(self.get_msg("voicelinkOutofList")) - 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: 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..eecd454 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -24,19 +24,25 @@ 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 +from timeit import default_timer as timer +from discord.ext import commands +from typing import Union -__all__ = [ - "ExponentialBackoff", - "NodeStats", - "NodeInfoVersion", - "NodeInfo", - "Plugin", - "Ping" -] +from .mongodb import MongoDBHandler +from .language import LangHandler + +# __all__ = [ +# "ExponentialBackoff", +# "NodeStats", +# "NodeInfoVersion", +# "NodeInfo", +# "Plugin", +# "Ping" +# ] class ExponentialBackoff: """ @@ -203,4 +209,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: discord.ui.View = None, + file: discord.File = None, + delete_after: float = None, + 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 content is None: + content = "No content provided." + + # Determine the text to send + embed = content if isinstance(content, discord.Embed) else None + text = None if embed else content.format(*params) + + # 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 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 + + # 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 88% rename from views/__init__.py rename to voicelink/views/__init__.py index d8bca40..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 .queue import QueueView -from .lyrics import LyricsView -from .playlist import PlaylistViewManager -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 89% rename from views/controller.py rename to voicelink/views/controller.py index 9bfc8b8..46b32cb 100644 --- a/views/controller.py +++ b/voicelink/views/controller.py @@ -24,16 +24,23 @@ SOFTWARE. import discord import re import voicelink -import addons -import views -import function as func +import traceback 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 +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 +81,7 @@ 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, @@ -191,14 +198,14 @@ class AddFav(ControlButton): return await self.send(interaction, "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() + user = await MongoDBHandler.get_user(interaction.user.id, 'playlist') + _, max_t, _ = Config().get_playlist_config() if len(user['200']['tracks']) >= max_t: return await self.send(interaction, "playlistLimited", max_t, ephemeral=True) if track.track_id in user['200']['tracks']: return await self.send(interaction, "playlistRepeated", ephemeral=True) - respond = await func.update_user(interaction.user.id, {"$push": {'playlist.200.tracks': track.track_id}}) + 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) else: @@ -273,7 +280,7 @@ class AutoPlay(ControlButton): 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, 'autoplay', await LangHandler.get_lang(interaction.guild_id, 'enabled' if check else "disabled")) if not self.player.is_playing: await self.player.do_next() @@ -313,7 +320,7 @@ class Forward(ControlButton): position = int(self.player.position + 10000) await self.player.seek(position) - await self.send(interaction, 'forward', func.time(position)) + await self.send(interaction, 'forward', format_ms(position)) class Rewind(ControlButton): def __init__(self, **kwargs): @@ -332,7 +339,7 @@ class Rewind(ControlButton): 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, 'rewind', format_ms(position)) class Lyrics(ControlButton): def __init__(self, **kwargs): @@ -342,19 +349,20 @@ 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) 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) - view = views.LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user) + 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(interaction, view.build_embed(), view=view, ephemeral=True) class Tracks(discord.ui.Select): @@ -379,13 +387,13 @@ 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 send_localized_message(interaction, "missingFunctionPerm", ephemeral=True) 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 send_localized_message(interaction, "skipped", interaction.user) class Effects(discord.ui.Select): def __init__(self, player: "voicelink.Player", btn_data, row): @@ -404,20 +412,20 @@ class Effects(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 send_localized_message(interaction, "missingFunctionPerm", ephemeral=True) avalibable_filters = voicelink.Filters.get_available_filters() if self.values[0] == "None": await self.player.reset_filter(requester=interaction.user) - return await func.send(interaction, "clearEffect") + return await send_localized_message(interaction, "clearEffect") selected_filter = avalibable_filters.get(self.values[0].lower())() if self.player.filters.has_filter(filter_tag=selected_filter.tag): await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user) - await func.send(interaction, "clearEffect") + await send_localized_message(interaction, "clearEffect") else: await self.player.add_filter(selected_filter, requester=interaction.user) - await func.send(interaction, "addEffect", selected_filter.tag) + await send_localized_message(interaction, "addEffect", selected_filter.tag) BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = { "back": Back, @@ -443,7 +451,7 @@ class InteractiveController(discord.ui.View): 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 +466,28 @@ 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, "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, "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) elif isinstance(error, Exception): + traceback.print_exception(error) await interaction.response.send_message(error) return \ No newline at end of file diff --git a/views/debug.py b/voicelink/views/debug.py similarity index 95% rename from views/debug.py rename to voicelink/views/debug.py index 4f76e10..fac7ebc 100644 --- a/views/debug.py +++ b/voicelink/views/debug.py @@ -24,15 +24,18 @@ 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 +from ..config import Config +from ..utils import format_ms, format_bytes + class ExecuteModal(discord.ui.Modal): def __init__(self, code: str, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -103,11 +106,7 @@ class AddNodeModal(discord.ui.Modal): await interaction.response.defer() try: - await voicelink.NodePool.create_node( - bot=interaction.client, - logger=func.logger, - **config - ) + await voicelink.NodePool.create_node(bot=interaction.client, **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) @@ -278,7 +277,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!```" @@ -295,9 +294,9 @@ class NodesPanel(discord.ui.View): 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( @@ -405,9 +404,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/views/embedBuilder.py b/voicelink/views/embed_builder.py similarity index 92% rename from views/embedBuilder.py rename to voicelink/views/embed_builder.py index 1e93c91..0a4294d 100644 --- a/views/embedBuilder.py +++ b/voicelink/views/embed_builder.py @@ -21,12 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import discord, copy -import function as func +import discord +import copy from typing import List from discord.ext import commands +from ..mongodb import MongoDBHandler +from ..placeholders import PlayerPlaceholder + class Modal(discord.ui.Modal): def __init__(self, items: List[discord.ui.Item], *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -51,28 +54,27 @@ class Dropdown(discord.ui.Select): 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] = {} + 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, data: dict) -> None: - from voicelink import Placeholders, build_embed - + 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.embedType: str = "active" - - self.ph: Placeholders = Placeholders(context.bot) - self.build_embed = lambda: build_embed(self.data.get(self.embedType, {}), self.ph) + 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: @@ -87,7 +89,7 @@ class EmbedBuilderView(discord.ui.View): @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, {}) + data = self.data.get(self.embed_type, {}) items = [ discord.ui.TextInput( label="Title", @@ -144,7 +146,7 @@ class EmbedBuilderView(discord.ui.View): @discord.ui.button(label="Edit Author",) async def edit_author(self, interaction: discord.Interaction, button: discord.ui.Button): - data = self.data.get(self.embedType, {}) + data = self.data.get(self.embed_type, {}) items = [ discord.ui.TextInput( label="Name", @@ -192,7 +194,7 @@ class EmbedBuilderView(discord.ui.View): @discord.ui.button(label="Edit Image") async def edit_image(self, interaction: discord.Interaction, button: discord.ui.Button): - data = self.data.get(self.embedType, {}) + data = self.data.get(self.embed_type, {}) items = [ discord.ui.TextInput( label="Thumbnail", @@ -225,7 +227,7 @@ class EmbedBuilderView(discord.ui.View): @discord.ui.button(label="Edit Footer") async def edit_footer(self, interaction: discord.Interaction, button: discord.ui.Button): - data = self.data.get(self.embedType, {}) + data = self.data.get(self.embed_type, {}) items = [ discord.ui.TextInput( label="Text", @@ -260,7 +262,7 @@ class EmbedBuilderView(discord.ui.View): @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) + data = self.data.get(self.embed_type) items = [ discord.ui.TextInput( label="Name", @@ -310,7 +312,7 @@ class EmbedBuilderView(discord.ui.View): ) ] - data = self.data.get(self.embedType) + data = self.data.get(self.embed_type) if "fields" not in data: data["fields"] = [] @@ -330,7 +332,7 @@ class EmbedBuilderView(discord.ui.View): @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( + await MongoDBHandler.update_settings( interaction.guild_id, {"$set": {"default_controller.embeds": self.data}}, ) diff --git a/views/help.py b/voicelink/views/help.py similarity index 97% rename from views/help.py rename to voicelink/views/help.py index bb9edf2..f24dc3a 100644 --- a/views/help.py +++ b/voicelink/views/help.py @@ -24,7 +24,7 @@ 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[str]) -> None: @@ -80,7 +80,7 @@ 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))), @@ -93,7 +93,7 @@ class HelpView(discord.ui.View): 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..1ef9e1f --- /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, "searchTitle") + 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..de3753e --- /dev/null +++ b/voicelink/views/pagination.py @@ -0,0 +1,107 @@ +"""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 traceback + +from .utils import Pagination, BaseModal + +class PaginationView(discord.ui.View): + def __init__(self, pagination: Pagination, author: discord.Member, timeout = 300): + super().__init__(timeout=timeout) + + self.pagination: Pagination = pagination + self.author: discord.Member = author + self.update_view() + + def update_view(self) -> None: + """Update button states and page number display based on current pagination state.""" + button_states = { + "fast_back": self.pagination.current_page <= 2, + "back": not self.pagination.has_previous_page, + "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, + "next": not self.pagination.has_next_page, + } + + for child in self.children: + if child.custom_id in button_states: + child.disabled = button_states[child.custom_id] + if child.custom_id == "page_number": + child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" + + async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None: + traceback.print_exception(error) + + 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.""" + modal = BaseModal( + title="Page Number", + custom_id="page_number_modal", + items=[ + discord.ui.TextInput( + label="Page Number", + custom_id="page_number", + placeholder="Enter the page number to navigate.", + 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/views/playlist.py b/voicelink/views/playlist.py similarity index 63% rename from views/playlist.py rename to voicelink/views/playlist.py index 3c22448..81f7709 100644 --- a/views/playlist.py +++ b/voicelink/views/playlist.py @@ -22,20 +22,19 @@ SOFTWARE. """ import discord -import function as func +import unicodedata -from math import ceil from tldextract import extract from discord.ext import commands -from typing import Any, TYPE_CHECKING, reveal_type +from typing import Any -from .utils import DynamicViewManager, Pagination, BaseModal +from .utils import DynamicViewManager, Pagination +from .pagination import PaginationView +from ..config import Config +from ..utils import format_ms, truncate_string +from ..language import LangHandler -if TYPE_CHECKING: - from voicelink import Track - - -class Select_playlist(discord.ui.Select): +class PlaylistDropdown(discord.ui.Select): def __init__(self, results: list[dict[str, Any]]) -> None: self.view: PlaylistViewManager @@ -57,63 +56,39 @@ class Select_playlist(discord.ui.Select): await interaction.response.edit_message(embed=await view.build_embed(), view=view) -class PlaylistView(discord.ui.View): +class PlaylistView(PaginationView): def __init__( self, primary_view: "PlaylistViewManager", playlist_data: dict[str, Any] ) -> None: - super().__init__(timeout=180) - self.primary_view: PlaylistViewManager = primary_view self.author: discord.Member = primary_view.ctx.author - self.id: str = playlist_data.get("id") + 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") - self.pagination: Pagination = Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7) - self.update_view() - - def update_view(self) -> None: - """Update button states and page number display based on current pagination state.""" - button_states = { - "fast_back": self.pagination.current_page <= 2, - "back": not self.pagination.has_previous_page, - "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, - "next": not self.pagination.has_next_page, - } - - for child in self.children: - if child.custom_id in button_states: - child.disabled = button_states[child.custom_id] - if child.custom_id == "page_number": - child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" - - async def interaction_check(self, interaction: discord.Interaction) -> bool: - return interaction.user == self.author - - async def on_error(self, error, item, interaction) -> None: - return + super().__init__(Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7), primary_view.ctx.author) 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 func.get_lang( + texts = await LangHandler.get_lang( self.author.guild.id, "playlistView", "playlistViewDesc", "settingsPermTitle", "playlistViewPermsValue", "playlistViewPermsValue2", "playlistViewTrack", "playlistNoTrack", "playlistViewFooter" ) - embed = discord.Embed(title=texts[0], color=func.settings.embed_color) + embed = discord.Embed(title=texts[0], color=Config().embed_color) embed.description = texts[1].format( self.name, - self.id, + self.playlist_id, self.pagination.total_items, self.primary_view.ctx.bot.get_user(self.owner_id), self.type.upper() @@ -133,11 +108,11 @@ class PlaylistView(discord.ui.View): if tracks: for index, track in enumerate(tracks, start=self.pagination.start_index + 1): if self.type == "playlist": - source_emoji = func.get_source(track['sourceName'], 'emoji') - track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track['length'])}]` [{func.truncate_string(track['title'])}]({track['uri']})" + 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 = func.get_source(extract(track.info['uri']).domain, 'emoji') - track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track.length)}]` [{func.truncate_string(track.title)}]({track.uri})" + 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) @@ -146,15 +121,7 @@ class PlaylistView(discord.ui.View): embed.set_footer(text=texts[7].format(self.time)) return embed - async def on_timeout(self) -> None: - for child in self.children: - child.disabled = True - try: - await self.primary_view.response.edit(view=self) - except: - pass - - async def update_and_edit_message(self, interaction: discord.Interaction) -> None: + async def update_message(self, interaction: discord.Interaction) -> None: """Update the view and edit the message with the new embed.""" self.update_view() @@ -162,62 +129,7 @@ class PlaylistView(discord.ui.View): await interaction.followup.edit_message(self.primary_view.response.id, embed=await self.build_embed(), view=self) else: await interaction.response.edit_message(embed=await self.build_embed(), view=self) - - async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None: - """Handle errors that occur during interaction.""" - func.logger.error(f"Error in PlaylistView: {error}", exc_info=error) - - @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_and_edit_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_and_edit_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.""" - modal = BaseModal( - title="Page Number", - custom_id="page_number_modal", - items=[ - discord.ui.TextInput( - label="Page Number", - custom_id="page_number", - placeholder="Enter the page number to navigate.", - 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_and_edit_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_and_edit_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_and_edit_message(interaction) - + @discord.ui.button(label="<") async def back_to_home(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: """Return to the main playlist view.""" @@ -229,7 +141,6 @@ class PlaylistView(discord.ui.View): await interaction.response.defer() cmd = interaction.client.get_command("playlist play") await cmd(self.primary_view.ctx, self.name) - # Need to handle error @discord.ui.button(label="Share", style=discord.ButtonStyle.blurple) async def share(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: @@ -246,11 +157,18 @@ class PlaylistView(discord.ui.View): 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)) + self.primary_view.remove_view(self.playlist_id) + await view.response.edit(embed=await view.build_embed(), view=view) class PlaylistViewManager(DynamicViewManager): def __init__(self, ctx: commands.Context, results: list[dict[str, Any]]): - self.ctx: commands.Context = ctx + self.ctx: commands.Context[commands.Bot] = ctx self.results: list[dict[str, Any]] = results views = {"home": self} @@ -259,10 +177,9 @@ class PlaylistViewManager(DynamicViewManager): super().__init__(views=views, timeout=None) self.response: discord.Message = None - self.add_item(Select_playlist(results)) + self.add_item(PlaylistDropdown(results)) def get_width(self, s): - import unicodedata width = 0 for char in str(s): if unicodedata.east_asian_width(char) in ('F', 'W'): @@ -276,6 +193,18 @@ class PlaylistViewManager(DynamicViewManager): current_width = self.get_width(s) padding = width - current_width return s + " " * padding + + 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 async def build_embed(self) -> discord.Embed: """ @@ -284,8 +213,8 @@ class PlaylistViewManager(DynamicViewManager): Returns: discord.Embed: The constructed embed with playlist details. """ - _, max_p, _ = func.check_roles() - text = await func.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter") + max_p, _, _ = Config().get_playlist_config() + text = await LangHandler.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter") headers = text[1].split(",") headers.insert(0, "") @@ -299,7 +228,7 @@ class PlaylistViewManager(DynamicViewManager): info.get('emoji', ' '), info.get('id', "-" * 3), f"[{info.get('time', '--:--')}]", - info.get('name', "-" * 6), + truncate_string(info.get('name', "-" * 6), 12), len(info.get('tracks', [])) ]) @@ -311,7 +240,7 @@ class PlaylistViewManager(DynamicViewManager): embed = discord.Embed( description=f'```{description}```', - color=func.settings.embed_color + color=Config().embed_color ) embed.set_author( diff --git a/voicelink/views/queue.py b/voicelink/views/queue.py new file mode 100644 index 0000000..fc45e22 --- /dev/null +++ b/voicelink/views/queue.py @@ -0,0 +1,132 @@ +"""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, BaseModal +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: + + self.player: Player = player + self.is_queue: bool = is_queue + self.total_duration = self.calculate_total_duration() + self.response: discord.Message = None + + super().__init__( + Pagination[Track]( + items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())), + page_size=7, + ), + author + ) + + 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, + "viewTitle", + "viewDesc", + "nowplayingDesc", + "live", + "queueTitle", + "historyTitle", + "playlistViewFooter", + ) + + 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 95% rename from views/search.py rename to voicelink/views/search.py index 3a77cb8..39e9023 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: diff --git a/views/utils/__init__.py b/voicelink/views/utils/__init__.py similarity index 100% rename from views/utils/__init__.py rename to voicelink/views/utils/__init__.py diff --git a/views/utils/dynamic_view_manager.py b/voicelink/views/utils/dynamic_view_manager.py similarity index 99% rename from views/utils/dynamic_view_manager.py rename to voicelink/views/utils/dynamic_view_manager.py index 2a9bb4c..d8f1afa 100644 --- a/views/utils/dynamic_view_manager.py +++ b/voicelink/views/utils/dynamic_view_manager.py @@ -32,10 +32,6 @@ class DynamicViewManager(discord.ui.View): self._views: dict[str, discord.ui.View] = views self._current_view: discord.ui.View | None = None - def current_view(self) -> discord.ui.View | None: - """Get the current active view.""" - return self._current_view - def add_view(self, name: str, view: discord.ui.View) -> None: """ Add a view to the manager. @@ -87,3 +83,8 @@ class DynamicViewManager(discord.ui.View): 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/views/utils/modal.py b/voicelink/views/utils/modal.py similarity index 100% rename from views/utils/modal.py rename to voicelink/views/utils/modal.py diff --git a/views/utils/pagination.py b/voicelink/views/utils/pagination.py similarity index 100% rename from views/utils/pagination.py rename to voicelink/views/utils/pagination.py From f6722e538a7798e91de1d914f73c2019e5d95757 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:54:51 +0800 Subject: [PATCH 04/10] Moved ipc_client into voicelink --- cogs/listeners.py | 4 ++-- main.py | 9 ++++----- voicelink/__init__.py | 1 + {ipc => voicelink/ipc}/__init__.py | 0 {ipc => voicelink/ipc}/client.py | 2 +- {ipc => voicelink/ipc}/methods.py | 9 +++++++-- voicelink/player.py | 17 +++++++++++------ 7 files changed, 26 insertions(+), 16 deletions(-) rename {ipc => voicelink/ipc}/__init__.py (100%) rename {ipc => voicelink/ipc}/client.py (98%) rename {ipc => voicelink/ipc}/methods.py (99%) diff --git a/cogs/listeners.py b/cogs/listeners.py index a423920..44b2da3 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -176,8 +176,8 @@ class Listeners(commands.Cog): if player.is_paused and len([m for m in player.channel.members if not m.bot]) == 1: await player.set_pause(False, member) - if self.bot.ipc._is_connected: - await self.bot.ipc.send({ + if player.is_ipc_connected: + await player._ipc_client.send({ "op": "updateGuild", "user": { "userId": str(member.id), diff --git a/main.py b/main.py index f16290f..156d263 100644 --- a/main.py +++ b/main.py @@ -30,9 +30,8 @@ import logging import function as func from discord.ext import commands -from ipc import IPCClient from logging.handlers import TimedRotatingFileHandler -from voicelink import Config, LangHandler, MongoDBHandler, VoicelinkException +from voicelink import Config, LangHandler, MongoDBHandler, IPCClient, VoicelinkException from voicelink.utils import dispatch_message class Translator(discord.app_commands.Translator): @@ -69,7 +68,7 @@ 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 @@ -121,10 +120,10 @@ 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, **bot_config.ipc_client) + 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}") diff --git a/voicelink/__init__.py b/voicelink/__init__.py index a9df3de..c839b37 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -39,3 +39,4 @@ 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/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 98% rename from ipc/client.py rename to voicelink/ipc/client.py index b1b079c..cf1936d 100644 --- a/ipc/client.py +++ b/voicelink/ipc/client.py @@ -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 diff --git a/ipc/methods.py b/voicelink/ipc/methods.py similarity index 99% rename from ipc/methods.py rename to voicelink/ipc/methods.py index 4f5c86e..14d1d10 100644 --- a/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import time, re -from typing import List, Dict, Union, Optional +from typing import List, Dict, Union, Optional, TYPE_CHECKING from discord import User, Member from discord.ext import commands 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, @@ -727,7 +732,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")): diff --git a/voicelink/player.py b/voicelink/player.py index 01dfe2c..5d01e33 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -21,12 +21,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + import time, logging from math import ceil from asyncio import sleep from random import shuffle, choice -from typing import Any, Dict, List, Optional, Union, Tuple +from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING from discord import ( Client, @@ -58,6 +60,9 @@ 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 LangHandler.get_lang(ctx.guild.id, "noChannel", "noPermission") try: @@ -79,7 +84,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 @@ -107,8 +112,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 @@ -255,7 +260,7 @@ 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 @@ -891,4 +896,4 @@ 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) \ No newline at end of file From 3831c87f9bda31ca59689ea502a9887919d6bdcb Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:15:23 +0800 Subject: [PATCH 05/10] Add default_search_platform option for customizable search platform --- cogs/basic.py | 4 ++-- settings Example.json | 1 + voicelink/config.py | 3 +++ voicelink/enums.py | 55 ++++++++++++++++++++++++++++++++----------- voicelink/objects.py | 6 +++-- voicelink/player.py | 5 +++- voicelink/pool.py | 7 ++++-- 7 files changed, 60 insertions(+), 21 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 2475d6a..52f505b 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -214,7 +214,7 @@ class Basic(commands.Cog): 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 = voicelink.SearchType.YOUTUBE.name): + async def search(self, ctx: commands.Context, *, query: str, platform: str = Config().search_platform.name): "Searches your query and displays the results." player: voicelink.Player = ctx.guild.voice_client if not player: @@ -226,7 +226,7 @@ class Basic(commands.Cog): if url(query): return await send_localized_message(ctx, "noLinkSupport", ephemeral=True) - search_type: voicelink.SearchType = voicelink.SearchType.match(platform) or voicelink.SearchType.YOUTUBE + search_type: voicelink.SearchType = voicelink.SearchType.match(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_localized_message(ctx, "noTrackFound") diff --git a/settings Example.json b/settings Example.json index 937df06..7f874ed 100644 --- a/settings Example.json +++ b/settings Example.json @@ -40,6 +40,7 @@ "bot_access_user": [], "embed_color":"0xb3b3b3", "default_max_queue": 1000, + "default_search_platform": "youtube", "lyrics_platform": "lrclib", "ipc_client": { "host": "127.0.0.1", diff --git a/voicelink/config.py b/voicelink/config.py index b93a44c..7c4a6f3 100644 --- a/voicelink/config.py +++ b/voicelink/config.py @@ -33,6 +33,8 @@ from typing import ( Optional ) +from .enums import SearchType + load_dotenv() class Config: @@ -82,6 +84,7 @@ class Config: 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.match(settings.get("default_search_platform")) 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", {}) diff --git a/voicelink/enums.py b/voicelink/enums.py index ef74c58..bc92915 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,6 +81,12 @@ 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 diff --git a/voicelink/objects.py b/voicelink/objects.py index 6b51301..44d7f16 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -22,7 +22,6 @@ SOFTWARE. """ import re -import discord from typing import Optional from tldextract import extract @@ -65,8 +64,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 diff --git a/voicelink/player.py b/voicelink/player.py index 5d01e33..de07517 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -532,13 +532,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): diff --git a/voicelink/pool.py b/voicelink/pool.py index 8197e3e..2e87dcd 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 @@ -353,7 +354,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 +362,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}" From aee2ec5f3b3d98befc9b524c0b922b249af84d8e Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 2 Oct 2025 00:05:26 +0800 Subject: [PATCH 06/10] fixed some bugs and remove presences intents --- main.py | 1 + voicelink/config.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 156d263..d321c6a 100644 --- a/main.py +++ b/main.py @@ -223,6 +223,7 @@ intents = discord.Intents.default() 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, diff --git a/voicelink/config.py b/voicelink/config.py index 7c4a6f3..55e3c1a 100644 --- a/voicelink/config.py +++ b/voicelink/config.py @@ -84,7 +84,7 @@ class Config: 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.match(settings.get("default_search_platform")) or SearchType.YOUTUBE + self.search_platform: SearchType = SearchType.match(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", {}) From d43920414669efbf1c14e45a23c01c09961ce781 Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 2 Oct 2025 00:06:09 +0800 Subject: [PATCH 07/10] Added get cached settings method in mongodbhandler --- voicelink/mongodb.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/voicelink/mongodb.py b/voicelink/mongodb.py index 0340400..8bb8eb9 100644 --- a/voicelink/mongodb.py +++ b/voicelink/mongodb.py @@ -265,6 +265,29 @@ class MongoDBHandler: 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, From 20df3703c337c61df2159942c6843cd5c7de724f Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 2 Oct 2025 00:10:25 +0800 Subject: [PATCH 08/10] Refactor localization keys --- cogs/basic.py | 224 ++++++++--------- cogs/effect.py | 28 +-- cogs/playlist.py | 102 ++++---- cogs/settings.py | 44 ++-- langs/DE.json | 439 ++++++++++++++++++++------------- langs/EN.json | 441 +++++++++++++++++++++------------- langs/ES.json | 439 ++++++++++++++++++++------------- langs/FR.json | 439 ++++++++++++++++++++------------- langs/JA.json | 439 ++++++++++++++++++++------------- langs/KO.json | 439 ++++++++++++++++++++------------- langs/PL.json | 439 ++++++++++++++++++++------------- langs/RU.json | 439 ++++++++++++++++++++------------- langs/UA.json | 439 ++++++++++++++++++++------------- langs/VN.json | 438 ++++++++++++++++++++------------- langs/ZHCN.json | 440 ++++++++++++++++++++------------- langs/ZHTW.json | 440 ++++++++++++++++++++------------- language.py | 365 ++++++++++++++++++++++++++++ main.py | 2 +- settings Example.json | 12 +- voicelink/ipc/methods.py | 22 +- voicelink/language.py | 29 ++- voicelink/placeholders.py | 2 +- voicelink/player.py | 21 +- voicelink/queue.py | 20 +- voicelink/views/controller.py | 82 +++---- voicelink/views/lyrics.py | 2 +- voicelink/views/pagination.py | 57 +++-- voicelink/views/playlist.py | 62 +++-- voicelink/views/queue.py | 14 +- 29 files changed, 3989 insertions(+), 2370 deletions(-) create mode 100644 language.py diff --git a/cogs/basic.py b/cogs/basic.py index 52f505b..a66f70c 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -42,9 +42,9 @@ from voicelink.utils import format_ms, format_to_ms, truncate_string, dispatch_m async def nowplay(ctx: commands.Context, player: voicelink.Player): track = player.current if not track: - return await send_localized_message(ctx, 'noTrackPlaying', ephemeral=True) + return await send_localized_message(ctx, 'player.errors.noTrackPlaying', ephemeral=True) - texts = await LangHandler.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=Config().embed_color) @@ -108,9 +108,9 @@ class Basic(commands.Cog): try: player = await voicelink.connect_channel(ctx, channel) except discord.errors.ClientException: - return await send_localized_message(ctx, "alreadyConnected") + return await send_localized_message(ctx, "voice.connection.alreadyConnected") - await send_localized_message(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,22 +127,22 @@ class Basic(commands.Cog): player = await voicelink.connect_channel(ctx) if not player.is_user_join(ctx.author): - return await send_localized_message(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_localized_message(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_to_ms(start), end_time=format_to_ms(end)) - await send_localized_message(ctx, "playlistLoad", tracks.name, index) + await send_localized_message(ctx, "player.playback.playlistLoad", tracks.name, index) else: 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, "live", "trackLoad_pos", "trackLoad") + 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] @@ -169,27 +169,27 @@ class Basic(commands.Cog): query = message.attachments[0].url if not query: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(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 LangHandler.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] @@ -221,17 +221,17 @@ class Basic(commands.Cog): player = await voicelink.connect_channel(ctx) if not player.is_user_join(ctx.author): - return await send_localized_message(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_localized_message(ctx, "noLinkSupport", ephemeral=True) + return await send_localized_message(ctx, "search.noLinkSupport", ephemeral=True) search_type: voicelink.SearchType = voicelink.SearchType.match(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_localized_message(ctx, "noTrackFound") + return await send_localized_message(ctx, "player.errors.noTrackFound") - texts = await LangHandler.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(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]]) @@ -264,22 +264,22 @@ class Basic(commands.Cog): player = await voicelink.connect_channel(ctx) if not player.is_user_join(ctx.author): - return await send_localized_message(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_localized_message(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_to_ms(start), end_time=format_to_ms(end), at_front=True) - await send_localized_message(ctx, "playlistLoad", tracks.name, index) + await send_localized_message(ctx, "player.playback.playlistLoad", tracks.name, index) else: 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, "live", "trackLoad_pos", "trackLoad") + 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] @@ -308,21 +308,21 @@ class Basic(commands.Cog): player = await voicelink.connect_channel(ctx) if not player.is_privileged(ctx.author): - return await send_localized_message(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_localized_message(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_to_ms(start), end_time=format_to_ms(end), at_front=True) - await send_localized_message(ctx, "playlistLoad", tracks.name, index) + await send_localized_message(ctx, "player.playback.playlistLoad", tracks.name, index) else: - texts = await LangHandler.get_lang(ctx.guild.id, "live", "trackLoad") + 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 "" @@ -344,21 +344,21 @@ class Basic(commands.Cog): "Pause the music." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(ctx, "noPlayer", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True) if player.is_paused: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(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) @@ -366,21 +366,21 @@ class Basic(commands.Cog): "Resume the music." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(ctx, "noPlayer", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True) if not player.is_paused: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(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.") @@ -389,28 +389,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_localized_message(ctx, "noPlayer", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True) if not player.node._available: - return await send_localized_message(ctx, "nodeReconnect") + return await send_localized_message(ctx, "player.errors.nodeReconnect") if not player.is_playing: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(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) @@ -423,18 +423,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_localized_message(ctx, "noPlayer", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noPlayer", ephemeral=True) if not player.node._available: - return await send_localized_message(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_localized_message(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_localized_message(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) @@ -443,7 +443,7 @@ class Basic(commands.Cog): player.queue.backto(index + 1) await player.stop() - await send_localized_message(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) @@ -454,19 +454,19 @@ class Basic(commands.Cog): "Change the player position." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(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_localized_message(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_localized_message(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True) if not (num := format_to_ms(position)): - return await send_localized_message(ctx, "timeFormatError", ephemeral=True) + return await send_localized_message(ctx, "time.formatError", ephemeral=True) await player.seek(num, ctx.author) - await send_localized_message(ctx, "seek", position) + await send_localized_message(ctx, "player.controls.seek", position) @commands.hybrid_group( name="queue", @@ -479,10 +479,10 @@ 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_localized_message(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_localized_message(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) @@ -495,13 +495,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_localized_message(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_localized_message(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_localized_message(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True) await ctx.defer() @@ -535,7 +535,7 @@ class Basic(commands.Cog): player = await voicelink.connect_channel(ctx) if not player.is_user_join(ctx.author): - return await send_localized_message(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() @@ -544,10 +544,10 @@ class Basic(commands.Cog): 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_localized_message(ctx, "noTrackFound") + return await send_localized_message(ctx, "player.errors.noTrackFound") index = await player.add_track(tracks) - await send_localized_message(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 @@ -562,10 +562,10 @@ 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_localized_message(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_localized_message(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) @@ -579,19 +579,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_localized_message(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_localized_message(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_localized_message(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_localized_message(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")) @@ -600,10 +600,10 @@ class Basic(commands.Cog): "Shows details of the current track." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(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_localized_message(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,13 +618,13 @@ class Basic(commands.Cog): "Changes Loop mode." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(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_localized_message(ctx, "missingModePerm", ephemeral=True) + return await send_localized_message(ctx, "permissions.missingMode", ephemeral=True) await player.set_repeat(voicelink.LoopType[mode] if mode in voicelink.LoopType.__members__ else voicelink.LoopType.OFF, ctx.author) - await send_localized_message(ctx, "repeat", mode.capitalize()) + 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.") @@ -637,13 +637,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_localized_message(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_localized_message(ctx, "missingQueuePerm", ephemeral=True) + return await send_localized_message(ctx, "permissions.missingQueue", ephemeral=True) await player.clear_queue(queue, ctx.author) - await send_localized_message(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( @@ -656,13 +656,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_localized_message(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_localized_message(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_localized_message(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") @@ -671,19 +671,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_localized_message(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_localized_message(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True) if not player.current: - return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True) if not (num := format_to_ms(position)): - return await send_localized_message(ctx, "timeFormatError", ephemeral=True) + return await send_localized_message(ctx, "time.formatError", ephemeral=True) await player.seek(int(player.position + num)) - await send_localized_message(ctx, "forward", format_ms(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") @@ -692,19 +692,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_localized_message(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_localized_message(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True) if not player.current: - return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True) if not (num := format_to_ms(position)): - return await send_localized_message(ctx, "timeFormatError", ephemeral=True) + return await send_localized_message(ctx, "time.formatError", ephemeral=True) await player.seek(int(player.position - num)) - await send_localized_message(ctx, "rewind", format_ms(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) @@ -712,16 +712,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_localized_message(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_localized_message(ctx, "missingPosPerm", ephemeral=True) + return await send_localized_message(ctx, "permissions.missingPosition", ephemeral=True) if not player.current: - return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True) await player.seek(0) - await send_localized_message(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) @@ -729,18 +729,18 @@ class Basic(commands.Cog): "Randomizes the tracks in the queue." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(ctx, "shuffled") + await send_localized_message(ctx, "player.controls.shuffle.success") @commands.hybrid_command(name="swap", aliases=get_aliases("swap")) @app_commands.describe( @@ -752,13 +752,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_localized_message(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_localized_message(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_localized_message(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( @@ -770,13 +770,13 @@ 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_localized_message(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_localized_message(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_localized_message(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.") @@ -786,7 +786,7 @@ class Basic(commands.Cog): if not title: player: voicelink.Player = ctx.guild.voice_client if not player or not player.is_playing: - return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True) + return await send_localized_message(ctx, "player.errors.noTrackPlaying", ephemeral=True) title = player.current.title artist = player.current.author @@ -796,7 +796,7 @@ class Basic(commands.Cog): if lyrics_platform: lyrics = await lyrics_platform().get_lyrics(title, artist) if not lyrics: - return await send_localized_message(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 dispatch_message(ctx, await view.build_embed(), view=view) @@ -808,22 +808,22 @@ class Basic(commands.Cog): "Transfer dj to another." player: voicelink.Player = ctx.guild.voice_client if not player: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(ctx, "djToMe", ephemeral=True) + return await send_localized_message(ctx, "permissions.djToSelf", ephemeral=True) if member not in player.channel.members: - return await send_localized_message(ctx, "djNotInChannel", member, ephemeral=True) + return await send_localized_message(ctx, "permissions.djNotInChannel", member, ephemeral=True) player.dj = member - await send_localized_message(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) @@ -831,14 +831,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_localized_message(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_localized_message(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_localized_message(ctx, "autoplay", await LangHandler.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() @@ -863,7 +863,7 @@ 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 LangHandler.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=Config().embed_color) embed.add_field( diff --git a/cogs/effect.py b/cogs/effect.py index 48fb338..9055322 100644 --- a/cogs/effect.py +++ b/cogs/effect.py @@ -37,12 +37,12 @@ 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 LangHandler.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 LangHandler.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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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_localized_message(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/playlist.py b/cogs/playlist.py index 2189251..948d700 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -57,7 +57,7 @@ async def check_playlist(ctx: commands.Context, name: str = None, full: bool = F """Get user's playlist data with various filtering options.""" user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist') - if not ctx.interaction.response.is_done(): + if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done(): await ctx.defer() if full: @@ -223,10 +223,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_localized_message(ctx, 'playlistNotFound', name, ephemeral=True) + 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_localized_message(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: @@ -236,7 +236,7 @@ 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_localized_message(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]: @@ -245,12 +245,12 @@ class Playlists(commands.Cog, name="playlist"): tracks = {"name": result['playlist']['name'], "tracks": _tracks} if not tracks: - return await send_localized_message(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_localized_message(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() @@ -283,7 +283,7 @@ class Playlists(commands.Cog, name="playlist"): }) view = PlaylistViewManager(ctx, playlist_results) - view.response = await dispatch_message(ctx, content=await view.build_embed(), view=view, ephemeral=True) + 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( @@ -294,25 +294,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_localized_message(ctx, 'playlistOverText', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.nameOverLimit', ephemeral=True) max_p, _, _ = Config().get_playlist_config() user = await check_playlist(ctx, full=True) if len(user) >= max_p: - return await send_localized_message(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_localized_message(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_localized_message(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 MongoDBHandler.update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}}) - await send_localized_message(ctx, "playlistCreated", name) + await send_localized_message(ctx, "playlist.actions.create", name) @playlist.command(name="delete", aliases=get_aliases("delete")) @app_commands.describe(name="The name of the playlist.") @@ -322,15 +322,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 send_localized_message(ctx, "playlistNotFound", name, ephemeral=True) + return await send_localized_message(ctx, "playlist.errors.notFound", name, ephemeral=True) if result['id'] == "200": - return await send_localized_message(ctx, "playlistDeleteError", ephemeral=True) + return await send_localized_message(ctx, "playlist.errors.deleteDefault", ephemeral=True) if result['playlist']['type'] == 'share': await MongoDBHandler.update_user(result['playlist']['user'], {"$pull": {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}}) await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{result['id']}": 1}}) - return await send_localized_message(ctx, "playlistRemove", result["playlist"]["name"]) + return await send_localized_message(ctx, "playlist.actions.remove", result["playlist"]["name"]) @playlist.command(name="share", aliases=get_aliases("share")) @app_commands.describe( @@ -342,26 +342,26 @@ 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_localized_message(ctx, 'playlistSendErrorPlayer', ephemeral=True) + return await send_localized_message(ctx, 'playlist.sharing.sendErrorPlayer', ephemeral=True) if member.bot: - return await send_localized_message(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_localized_message(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_localized_message(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_localized_message(ctx, 'playlistShare', member, ephemeral=True) + return await send_localized_message(ctx, 'playlist.sharing.alreadyShared', member, ephemeral=True) receiver = await MongoDBHandler.get_user(member.id) if not receiver: - return await send_localized_message(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_localized_message(ctx, 'playlistSent', ephemeral=True) + return await send_localized_message(ctx, 'playlist.sharing.alreadySent', ephemeral=True) if len(receiver['inbox']) >= 10: - return await send_localized_message(ctx.guild.id, 'inboxFull', member, ephemeral=True) + return await send_localized_message(ctx, 'playlist.inbox.full', member, ephemeral=True) await MongoDBHandler.update_user( member.id, @@ -374,7 +374,7 @@ class Playlists(commands.Cog, name="playlist"): 'type': 'invite' }}} ) - return await send_localized_message(ctx, "invitationSent", member) + return await send_localized_message(ctx, "playlist.sharing.invitationSent", member) @playlist.command(name="rename", aliases=get_aliases("rename")) @app_commands.describe( @@ -386,22 +386,22 @@ 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_localized_message(ctx, 'playlistOverText', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.nameOverLimit', ephemeral=True) if name.lower() == newname.lower(): - return await send_localized_message(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_localized_message(ctx, 'playlistExists', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.exists', ephemeral=True) if not found: - return await send_localized_message(ctx.guild.id, 'playlistNotFound', name, ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{id}.name': newname}}) - await send_localized_message(ctx, 'playlistRenamed', 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) @@ -411,7 +411,7 @@ class Playlists(commands.Cog, name="playlist"): max_p, _, _ = Config().get_playlist_config() if not user['inbox']: - return await send_localized_message(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']) @@ -447,26 +447,26 @@ class Playlists(commands.Cog, name="playlist"): "Add tracks in to your custom playlist." result = await check_playlist(ctx, name.lower(), share=False) if not result['playlist']: - return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True) + 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, 'playlistNotAllow', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) _, max_t, _ = Config().get_playlist_config() if len(result['playlist']['tracks']) >= max_t: - return await send_localized_message(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_localized_message(ctx, 'noTrackFound') + return await send_localized_message(ctx, 'player.errors.noTrackFound') if isinstance(results, voicelink.Playlist): - return await send_localized_message(ctx, 'playlistPlaylistLink', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.playlistLinkNotAllowed', ephemeral=True) if results[0].is_stream: - return await send_localized_message(ctx, 'playlistStream', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.streamNotAllowed', ephemeral=True) await MongoDBHandler.update_user(ctx.author.id, {"$push": {f'playlist.{result["id"]}.tracks': results[0].track_id}}) - await send_localized_message(ctx, 'playlistAdded', results[0].title, ctx.author, result['playlist']['name']) + await send_localized_message(ctx, 'playlist.actions.trackAdded', results[0].title, ctx.author, result['playlist']['name']) @playlist.command(name="remove", aliases=get_aliases("remove")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -479,16 +479,16 @@ class Playlists(commands.Cog, name="playlist"): "Remove song from your favorite playlist." result = await check_playlist(ctx, name.lower(), share=False) if not result['playlist']: - return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) if result['playlist']['type'] in ['link', 'share']: - return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) if not 0 < position <= len(result['playlist']['tracks']): - return await send_localized_message(ctx, 'playlistPositionNotFound', position, name) + return await send_localized_message(ctx, 'playlist.errors.positionNotFound', position, name) await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}}) track = voicelink.Track.decode(result['playlist']['tracks'][position - 1]) - await send_localized_message(ctx, 'playlistRemoved', track.get("title"), ctx.author, name) + await send_localized_message(ctx, 'playlist.actions.trackRemoved', track.get("title"), ctx.author, name) @playlist.command(name="clear", aliases=get_aliases("clear")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -497,13 +497,13 @@ class Playlists(commands.Cog, name="playlist"): "Remove all songs from your favorite playlist." result = await check_playlist(ctx, name.lower(), share=False) if not result['playlist']: - return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) if result['playlist']['type'] in ['link', 'share']: - return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}}) - await send_localized_message(ctx, 'playlistClear', name) + 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) @@ -512,13 +512,13 @@ 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_localized_message(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_localized_message(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']: @@ -527,7 +527,7 @@ class Playlists(commands.Cog, name="playlist"): tracks = {"name": result['playlist']['name'], "tracks": _tracks} if not tracks: - return await send_localized_message(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" @@ -555,17 +555,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_localized_message(ctx, 'playlistOverText', ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.nameOverLimit', ephemeral=True) max_p, _, _ = Config().get_playlist_config() user = await check_playlist(ctx, full=True) if len(user) >= max_p: - return await send_localized_message(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_localized_message(ctx, 'playlistExists', name, ephemeral=True) + return await send_localized_message(ctx, 'playlist.errors.exists', name, ephemeral=True) try: bytes = await attachment.read() @@ -574,7 +574,7 @@ class Playlists(commands.Cog, name="playlist"): data = {'tracks': track_ids, 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} await MongoDBHandler.update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}}) - await send_localized_message(ctx, 'playlistCreated', name) + 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 e38c8e0..2be80b5 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -62,10 +62,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_localized_message(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True) + return await send_localized_message(ctx, "common.error.missingIntents", "MESSAGE_CONTENT", ephemeral=True) await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {"prefix": prefix}}) - await send_localized_message(ctx, "setPrefix", 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) @@ -74,10 +74,10 @@ class Settings(commands.Cog, name="settings"): "You can choose your preferred language, the bot message will change to the language you set." language = language.upper() if language not in voicelink.LangHandler.get_all_languages(): - return await send_localized_message(ctx, "languageNotFound") + return await send_localized_message(ctx, "settings.actions.languageNotFound") await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'lang': language}}) - await send_localized_message(ctx, 'changedLanguage', language) + await send_localized_message(ctx, 'settings.actions.languageChanged', language) @language.autocomplete('language') async def autocomplete_callback(self, interaction: discord.Interaction, current: str) -> list: @@ -91,7 +91,7 @@ class Settings(commands.Cog, name="settings"): async def dj(self, ctx: commands.Context, role: discord.Role = None): "Set a DJ role or remove DJ role." await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'dj': role.id}} if role else {"$unset": {'dj': None}}) - await send_localized_message(ctx, 'setDJ', f"<@&{role.id}>" if role else "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=[ @@ -104,7 +104,7 @@ class Settings(commands.Cog, name="settings"): "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 MongoDBHandler.update_settings(ctx.guild.id, {"$set": {"queue_type": mode}}) - await send_localized_message(ctx, "setQueue", mode.capitalize()) + await send_localized_message(ctx, "settings.actions.queueModeSet", mode.capitalize()) @settings.command(name="247", aliases=get_aliases("247")) @commands.has_permissions(manage_guild=True) @@ -114,7 +114,7 @@ class Settings(commands.Cog, name="settings"): settings = await MongoDBHandler.get_settings(ctx.guild.id) toggle = settings.get('24/7', False) await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'24/7': not toggle}}) - await send_localized_message(ctx, '247', await LangHandler.get_lang(ctx.guild.id, "enabled" if not toggle else "disabled")) + 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) @@ -122,9 +122,9 @@ class Settings(commands.Cog, name="settings"): async def bypassvote(self, ctx: commands.Context): "Toggles voting system." settings = await MongoDBHandler.get_settings(ctx.guild.id) - toggle = settings.get('disabled_vote', True) - await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'disabled_vote': not toggle}}) - await send_localized_message(ctx, 'bypassVote', await LangHandler.get_lang(ctx.guild.id, "enabled" if not toggle else "disabled")) + toggle = settings.get('common.status.disabled_vote', True) + await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'common.status.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) @@ -133,7 +133,7 @@ class Settings(commands.Cog, name="settings"): "Show all the bot settings in your server." settings = await MongoDBHandler.get_settings(ctx.guild.id) - texts = await LangHandler.get_lang(ctx.guild.id, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsTitle3", "settingsPermTitle", "settingsPermValue") + 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: @@ -145,7 +145,7 @@ class Settings(commands.Cog, name="settings"): settings.get('lang', 'EN'), settings.get('controller', True), dj_role.name if dj_role else 'None', - settings.get('disabled_vote', False), + settings.get('common.status.disabled_vote', False), settings.get('24/7', False), settings.get('volume', 100), format_ms(settings.get('played_time', 0) * 60 * 1000), @@ -182,7 +182,7 @@ class Settings(commands.Cog, name="settings"): await player.set_volume(value, ctx.author) await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'volume': value}}) - await send_localized_message(ctx, 'setVolume', value) + await send_localized_message(ctx, 'settings.actions.volumeSet', value) @settings.command(name="togglecontroller", aliases=get_aliases("togglecontroller")) @commands.has_permissions(manage_guild=True) @@ -200,7 +200,7 @@ class Settings(commands.Cog, name="settings"): discord.ui.View.from_message(player.controller).stop() await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'controller': toggle}}) - await send_localized_message(ctx, 'toggleController', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled")) + 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) @@ -214,7 +214,7 @@ class Settings(commands.Cog, name="settings"): player.queue._allow_duplicate = toggle await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'duplicate_track': toggle}}) - return await send_localized_message(ctx, "toggleDuplicateTrack", await LangHandler.get_lang(ctx.guild.id, "disabled" if toggle else "enabled")) + 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) @@ -237,7 +237,7 @@ class Settings(commands.Cog, name="settings"): toggle = not settings.get('controller_msg', True) await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'controller_msg': toggle}}) - await send_localized_message(ctx, 'toggleControllerMsg', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled")) + 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) @@ -248,7 +248,7 @@ class Settings(commands.Cog, name="settings"): toggle = not settings.get('silent_msg', False) await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'silent_msg': toggle}}) - await send_localized_message(ctx, 'toggleSilentMsg', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled")) + 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) @@ -256,7 +256,7 @@ class Settings(commands.Cog, name="settings"): async def stageannounce(self, ctx: commands.Context, template: str = None): "Customize the channel topic template" await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}}) - await send_localized_message(ctx, "setStageAnnounceTemplate") + await send_localized_message(ctx, "voice.stageChannel.setAnnounceTemplate") @settings.command(name="setupchannel", aliases=get_aliases("setupchannel")) @app_commands.describe( @@ -267,7 +267,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_localized_message(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True) + return await send_localized_message(ctx, "common.error.missingIntents", "MESSAGE_CONTENT", ephemeral=True) if not channel: try: @@ -279,11 +279,11 @@ class Settings(commands.Cog, name="settings"): } channel = await ctx.guild.create_text_channel("vocard-song-requests", overwrites=overwrites) except: - return await send_localized_message(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_localized_message(ctx, "noCreatePermission") + return await send_localized_message(ctx, "permissions.noCreatePermission") settings = await MongoDBHandler.get_settings(ctx.guild.id) controller = settings.get("default_controller", voicelink.Config().controller).get("embeds", {}).get("inactive", {}) @@ -293,7 +293,7 @@ class Settings(commands.Cog, name="settings"): "text_channel_id": channel.id, "controller_msg_id": message.id, }}}) - await send_localized_message(ctx, "createSongRequestChannel", channel.mention) + await send_localized_message(ctx, "songRequest.channelCreated", channel.mention) @app_commands.command(name="debug") async def debug(self, interaction: discord.Interaction): diff --git a/langs/DE.json b/langs/DE.json index a0bf86a..bb79248 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -1,171 +1,270 @@ { - "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: {0} [{1}]\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```", - "playlistViewPermsValue": "📖 Lesen: ✓ ✍🏽 Schreiben: {0} 🗑️ Entfernen: {1}", - "playlistViewPermsValue2": "📖 Lesen: {0}", - "playlistViewTrack": "Tracks", - "playlistViewFooter": "Gesamtdauer: {0}", - "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}", - "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 ..." + }, + "buttons": { + "back": "Zurück", + "pause": "Pause", + "resume": "Weiter", + "skip": "Überspringen", + "leave": "Verlassen", + "loop": "Endlosschleife", + "volumeUp": "Lauter", + "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." + }, + "unmapped": { + "notVote": "Dieser Befehl erfordert Deine Stimme! Gebe `/vote` ein, um weitere Informationen zu erhalten.", + "missingTrackInfo": "Einige Track-Informationen fehlen.", + "noVoiceChannel": "Dieser Sprachkanal wurde nicht gefunden!", + "playerFilter": "Wähle einen Filter aus, um ihn anzuwenden ...", + "searchTimeout": "Die Suche wurde abgebrochen. Bitte versuche es später erneut.", + "decodeError": "Beim Dekodieren der Datei ist etwas schief gelaufen!", + "invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein." + }, + "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." + }, + "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 gebe einen gültigen Link oder öffentlichen Spotify/YouTube-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 a05f7cb..1d6ea7d 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -1,172 +1,271 @@ { - "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.", - "emptyPlaylistMessage": "🎶 No playlists found. Create your first one with `/playlist create`!", - "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: {0} [{1}]\nTotal Tracks: {2}\nOwner: {3}\nType: {4}\n```", - "playlistViewPermsValue": "📖 Read: ✓ ✍🏽 Write: {0} 🗑️ Remove: {1}", - "playlistViewPermsValue2": "📖 Read: {0}", - "playlistViewTrack": "Tracks", - "playlistViewFooter": "Total Duration: {0}", - "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}", - "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 ..." + }, + "buttons": { + "back": "Back", + "pause": "Pause", + "resume": "Resume", + "skip": "Skip", + "leave": "Leave", + "loop": "Loop", + "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}`." + }, + "unmapped": { + "notVote": "This command requires your vote! Type `/vote` for more info.", + "emptyPlaylistMessage": "🎶 No playlists found. Create your first one with `/playlist create`!", + "missingTrackInfo": "Some track details are missing.", + "noVoiceChannel": "Voice Channel Not Found!", + "playerFilter": "Select a filter to apply ...", + "searchTimeout": "Search timed out. Please try again later.", + "decodeError": "Something went wrong while decoding the file!", + "invalidTimeOrder": "End time cannot be less than or equal to start time" + }, + "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." + }, + "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 spotify or youtube 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." + } + }, + "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 1b5c11d..e0c6d60 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -1,171 +1,270 @@ { - "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: {0} [{1}]\nTotal de pistas: {2}\nPropietario: {3}\nTipo: {4}\n```", - "playlistViewPermsValue": "📖 Leer: ✓ ✍🏽 Escribir: {0} 🗑️ Eliminar: {1}", - "playlistViewPermsValue2": "📖 Leer: {0}", - "playlistViewTrack": "Pistas", - "playlistViewFooter": "Duración total: {0}", - "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}", - "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 ..." + }, + "buttons": { + "back": "Atrás", + "pause": "Pausa", + "resume": "Reanudar", + "skip": "Saltar", + "leave": "Salir", + "loop": "Bucle", + "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}`." + }, + "unmapped": { + "notVote": "¡Este comando requiere su voto! Escriba `/vote` para obtener más información.", + "missingTrackInfo": "Falta información de la canción.", + "noVoiceChannel": "¡Canal de voz no encontrado!", + "playerFilter": "Seleccione un filtro para aplicar ...", + "searchTimeout": "La búsqueda ha expirado. Por favor, inténtelo de nuevo más tarde.", + "decodeError": "¡Algo salió mal al decodificar el archivo!", + "invalidTimeOrder": "El tiempo final no puede ser menor o igual que el tiempo de inicio." + }, + "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." + }, + "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": "Ingrese un enlace válido o un enlace público de lista de reproducción de Spotify o YouTube.", + "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 663f967..af5d78f 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -1,171 +1,270 @@ { - "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: {0} [{1}]\nTotal Morceaux : {2}\nPropriétaire : {3}\nType : {4}\n```", - "playlistViewPermsValue": "📖 Lecture : ✓ ✍🏽 Écriture : {0} 🗑️ Suppression : {1}", - "playlistViewPermsValue2": "📖 Lecture : {0}", - "playlistViewTrack": "Morceaux", - "playlistViewFooter": "Durée totale : {0}", - "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}", - "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..." + }, + "buttons": { + "back": "Précédent", + "pause": "Pause", + "resume": "Reprendre", + "skip": "Passer", + "leave": "Quitter", + "loop": "Boucle", + "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}`." + }, + "unmapped": { + "notVote": "Cette commande nécessite votre vote ! Tapez `/vote` pour en savoir plus.", + "missingTrackInfo": "Certaines informations sur le morceau sont manquantes.", + "noVoiceChannel": "Aucun salon vocal trouvé !", + "playerFilter": "Sélectionnez un filtre à appliquer...", + "searchTimeout": "Recherche expirée. Veuillez réessayer plus tard.", + "decodeError": "Une erreur s'est produite lors du décodage du fichier !", + "invalidTimeOrder": "L'heure de fin ne peut pas être inférieure ou égale à celle de début" + }, + "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." + }, + "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 Spotify ou YouTube.", + "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 0df1caa..eeef9fd 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -1,171 +1,270 @@ { - "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": "```名前: {0} [{1}]\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```", - "playlistViewPermsValue": "📖 読み込み: ✓ ✍🏽 書き込み: {0} 🗑️ 削除: {1}", - "playlistViewPermsValue2": "📖 読み込み: {0}", - "playlistViewTrack": "トラック", - "playlistViewFooter": "トータルダレーション: {0}", - "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}", - "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": "スキップする曲を選択してください..." + }, + "buttons": { + "back": "戻る", + "pause": "一時停止", + "resume": "再開", + "skip": "スキップ", + "leave": "退出", + "loop": "ループ", + "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}`に移行しました。" + }, + "unmapped": { + "notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。", + "missingTrackInfo": "一部のトラック情報が欠落しています。", + "noVoiceChannel": "音声チャンネルが見つかりません!", + "playerFilter": "適用するフィルターを選択してください...", + "searchTimeout": "検索がタイムアウトしました。後でもう一度お試しください。", + "decodeError": "ファイルのデコード中に問題が発生しました!", + "invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。" + }, + "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}サイレントメッセージを持っています。" + }, + "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": "有効なリンクまたは公開SpotifyまたはYouTubeプレイリストリンクを入力してください。", + "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 6223ef1..858eac6 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -1,171 +1,270 @@ { - "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": "```이름: {0} [{1}]\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```", - "playlistViewPermsValue": "📖 읽기: ✓ ✍🏽 쓰기: {0} 🗑️ 삭제: {1}", - "playlistViewPermsValue2": "📖 읽기: {0}", - "playlistViewTrack": "트랙", - "playlistViewFooter": "총 길이: {0}", - "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}", - "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": "건너뛰기 할 노래를 선택하세요..." + }, + "buttons": { + "back": "이전", + "pause": "일시정지", + "resume": "재개", + "skip": "스킵", + "leave": "나가기", + "loop": "반복", + "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}`님에게 이전했습니다." + }, + "unmapped": { + "notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.", + "missingTrackInfo": "일부 트랙 정보가 누락되었습니다.", + "noVoiceChannel": "음성 채널을 찾을 수 없습니다!", + "playerFilter": "적용할 필터를 선택하세요...", + "searchTimeout": "검색이 시간 초과되었습니다. 나중에 다시 시도해주세요.", + "decodeError": "파일 디코딩 중 문제가 발생했습니다!", + "invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다." + }, + "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} 침묵 메시지를 사용하고 있습니다." + }, + "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": "유효한 링크 또는 공개 Spotify 또는 YouTube 재생 목록 링크를 입력하세요.", + "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 68c0bfe..01ba020 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -1,171 +1,270 @@ { - "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: {0} [{1}]\nLiczba utworów: {2}\nWłaściciel: {3}\nTyp: {4}\n```", - "playlistViewPermsValue": "📖 Odczyt: ✓ ✍🏽 Zapis: {0} 🗑️ Usuwanie: {1}", - "playlistViewPermsValue2": "📖 Odczyt: {0}", - "playlistViewTrack": "Utwory", - "playlistViewFooter": "Całkowity czas trwania: {0}", - "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}", - "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ąć ..." + }, + "buttons": { + "back": "Cofnij", + "pause": "Pauza", + "resume": "Wznów", + "skip": "Pomiń", + "leave": "Opuść", + "loop": "Pętla", + "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}`." + }, + "unmapped": { + "notVote": "To polecenie wymaga głosowania! Wpisz `/vote`, aby uzyskać więcej informacji.", + "missingTrackInfo": "Brakuje niektórych informacji o utworze.", + "noVoiceChannel": "Kanał nie został znaleziony", + "playerFilter": "Wybierz efekt który chesz nałożyć ...", + "searchTimeout": "Upłynął czas wyszukiwania. Spróbuj ponownie później.", + "decodeError": "Coś poszło nie tak podczas dekodowania pliku!", + "invalidTimeOrder": "Czas zakończenia odtwarzania nie może być mniejszy lub równy czasowi startu" + }, + "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." + }, + "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": "Podaj poprawny link do playlisty z Spotify lub Youtube.", + "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 5b9a21b..972e0a0 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -1,171 +1,270 @@ { - "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": "```Имя: {0} [{1}]\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```", - "playlistViewPermsValue": "📖 Чтение: ✓ ✍🏽 Запись: {0} 🗑️ Удаление: {1}", - "playlistViewPermsValue2": "📖 Чтение: {0}", - "playlistViewTrack": "Треки", - "playlistViewFooter": "Общая продолжительность: {0}", - "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}", - "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": "Выберите трек для перехода ..." + }, + "buttons": { + "back": "Назад", + "pause": "Пауза", + "resume": "Пуск", + "skip": "Вперед", + "leave": "СТОП", + "loop": "Зациклить", + "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}`." + }, + "unmapped": { + "notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.", + "missingTrackInfo": "Некоторая информация о треке отсутствует.", + "noVoiceChannel": "Голосовой канал не найден!", + "playerFilter": "Выберите фильтр для применения ...", + "searchTimeout": "Поиск прерван: превышено время ожидания. Пожалуйста, попробуйте позже.", + "decodeError": "Что-то пошло не так при декодировании файла!", + "invalidTimeOrder": "Время конца трека не может быть меньше или равно времени начала." + }, + "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} тихих сообщений." + }, + "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": "Пожалуйста, введите действительную ссылку или публичную плейлист-ссылку на Spotify или YouTube.", + "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 db0641a..c02b3b1 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -1,171 +1,270 @@ { - "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": "```Ім'я: {0} [{1}]\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```", - "playlistViewPermsValue": "📖 Читання: ✓ ✍🏽 Запис: {0} 🗑️ Видалення: {1}", - "playlistViewPermsValue2": "📖 Читання: {0}", - "playlistViewTrack": "Треки", - "playlistViewFooter": "Загальна тривалість: {0}", - "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}", - "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": "Виберіть пісню для переходу ..." + }, + "buttons": { + "back": "Назад", + "pause": "Призупинити", + "resume": "Продовжити", + "skip": "Скіпнути трек", + "leave": "СТОП", + "loop": "Зациклити", + "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}`." + }, + "unmapped": { + "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", + "missingTrackInfo": "Деяка інформація про трек відсутня.", + "noVoiceChannel": "Голосовий канал не знайдено!", + "playerFilter": "Виберіть фільтр для застосування ...", + "searchTimeout": "Пошук перервано: перевищено час очікування. Будь ласка, спробуйте пізніше.", + "decodeError": "Щось пішло не так під час декодування файлу!", + "invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку." + }, + "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} тихих повідомлень." + }, + "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": "Будь ласка, введіть дійсне посилання або публічне плейлист-посилання на Spotify або YouTube.", + "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 2440917..8bd6a9b 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -1,171 +1,269 @@ { - "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: {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", - "playlistViewFooter": "Tổng Thời Lượng: {0}", - "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}", - "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 ..." + }, + "buttons": { + "back": "Quay Lại", + "pause": "Tạm Dừng", + "resume": "Tiếp Tục", + "skip": "Bỏ Qua", + "leave": "Rời Khỏi", + "loop": "Lặp Lạ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}`." + }, + "unmapped": { + "notVote": "Lệnh này yêu cầu bạn bình chọn! Gõ `/vote` để biết thêm thông tin.", + "missingTrackInfo": "Một số chi tiết bài hát bị thiếu.", + "noVoiceChannel": "Không Tìm Thấy Kênh Voice!", + "playerFilter": "Chọn một bộ lọc để áp dụng ...", + "searchTimeout": "Tìm kiếm hết thời gian. Vui lòng thử lại sau.", + "decodeError": "Đã xảy ra lỗi khi giải mã tệp!", + "invalidTimeOrder": "Thời gian kết thúc không thể nhỏ hơn hoặc bằng thời gian bắt đầu" + }, + "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." + }, + "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", + "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 playlist Spotify hoặc Youtube công khai hợp lệ.", + "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 index b86c964..9c70855 100644 --- a/langs/ZHCN.json +++ b/langs/ZHCN.json @@ -1,172 +1,270 @@ { - "unknownException": "⚠️ 执行命令时出错!请稍后再试,或加入我们的 Discord 服务器获取支持。", - "enabled": "已启用", - "disabled": "已禁用", - "nodeReconnect": "请在节点重连后再试!", - "noChannel": "未检测到语音频道,请提供一个或加入语音频道。", - "alreadyConnected": "已连接到语音频道。", - "noPermission": "抱歉!我没有权限加入或在您的语音频道中发言。", - "noCreatePermission": "抱歉!我没有权限创建点歌频道。", - "noPlaySource": "找不到可播放的资源!", - "noPlayer": "该服务器上没有找到播放器。", - "notVote": "该命令需要您先投票!输入 `/vote` 了解更多。", - "missingIntents": "抱歉,无法执行此命令,因为缺少所需的请求 intent:`({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": "曲目列表", - "playlistViewFooter": "总持续时间:{0}", - "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}", - "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})!您可以在该频道输入歌曲名称或链接来点歌,无需使用机器人前缀。" -} \ No newline at end of file + "common": { + "errors": { + "unknown": "⚠️ 执行命令时出错!请稍后再试,或加入我们的 Discord 服务器获取支持。", + "missingIntents": "抱歉,无法执行此命令,因为缺少所需的请求 intent:`({0})`。" + }, + "status": { + "enabled": "已启用", + "disabled": "已禁用", + "live": "直播" + } + }, + "player": { + "errors": { + "nodeReconnect": "请在节点重连后再试!", + "noPlaySource": "找不到可播放的资源!", + "noPlayer": "该服务器上没有找到播放器。", + "noTrackPlaying": "当前没有正在播放的歌曲", + "noTrackFound": "未找到对应的歌曲,请提供有效链接。" + }, + "dropdown": { + "trackSelect": "选择要跳转的歌曲 ..." + }, + "buttons": { + "back": "上一首", + "pause": "暂停", + "resume": "继续", + "skip": "下一首", + "leave": "退出", + "loop": "循环", + "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}`。" + }, + "unmapped": { + "notVote": "该命令需要您先投票!输入 `/vote` 了解更多。", + "missingTrackInfo": "部分曲目信息缺失。", + "noVoiceChannel": "未找到语音频道!", + "playerFilter": "选择要应用的音效 ...", + "searchTimeout": "搜索超时,请稍后重试。", + "decodeError": "解码文件时出错!", + "invalidTimeOrder": "结束时间不能小于或等于开始时间" + }, + "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} 静默消息。" + }, + "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": "请输入有效的公共 Spotify 或 YouTube 播放列表链接。", + "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 index 69f991b..eb0216c 100644 --- a/langs/ZHTW.json +++ b/langs/ZHTW.json @@ -1,172 +1,270 @@ { - "unknownException": "⚠️ 執行指令時發生錯誤!請稍後再試,或加入我們的 Discord 伺服器取得協助。", - "enabled": "已啟用", - "disabled": "已停用", - "nodeReconnect": "請在節點重新連線後再試!", - "noChannel": "未偵測到語音頻道,請提供一個或先加入語音頻道。", - "alreadyConnected": "已連線至語音頻道。", - "noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。", - "noCreatePermission": "抱歉!我沒有權限建立點歌頻道。", - "noPlaySource": "找不到可播放的來源!", - "noPlayer": "此伺服器上沒有找到播放器。", - "notVote": "此指令需要您先投票!輸入 `/vote` 了解更多。", - "missingIntents": "抱歉,無法執行此指令,因為缺少必要的請求 intent:`({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": "曲目列表", - "playlistViewFooter": "總持續時間:{0}", - "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}", - "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})!您可以在該頻道輸入歌曲名稱或連結來點歌,無需使用機器人前綴。" -} \ No newline at end of file + "common": { + "errors": { + "unknown": "⚠️ 執行指令時發生錯誤!請稍後再試,或加入我們的 Discord 伺服器取得協助。", + "missingIntents": "抱歉,無法執行此指令,因為缺少必要的請求 intent:`({0})`。" + }, + "status": { + "enabled": "已啟用", + "disabled": "已停用", + "live": "直播" + } + }, + "player": { + "errors": { + "nodeReconnect": "請在節點重新連線後再試!", + "noPlaySource": "找不到可播放的來源!", + "noPlayer": "此伺服器上沒有找到播放器。", + "noTrackPlaying": "目前沒有正在播放的歌曲", + "noTrackFound": "未找到對應的歌曲,請提供有效連結。" + }, + "dropdown": { + "trackSelect": "選擇要跳轉的歌曲 ..." + }, + "buttons": { + "back": "上一首", + "pause": "暫停", + "resume": "繼續", + "skip": "下一首", + "leave": "離開", + "loop": "循環", + "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}`。" + }, + "unmapped": { + "notVote": "此指令需要您先投票!輸入 `/vote` 了解更多。", + "missingTrackInfo": "部分曲目資訊缺失。", + "noVoiceChannel": "未找到語音頻道!", + "playerFilter": "選擇要套用的音效 ...", + "searchTimeout": "搜尋逾時,請稍後再試。", + "decodeError": "解碼檔案時發生錯誤!", + "invalidTimeOrder": "結束時間不能小於或等於開始時間" + }, + "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} 靜音訊息。" + }, + "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": "請輸入有效的公開 Spotify 或 YouTube 播放清單連結。", + "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/language.py b/language.py new file mode 100644 index 0000000..d6945ae --- /dev/null +++ b/language.py @@ -0,0 +1,365 @@ +import json +import os +from pathlib import Path + +# Define the categorization mapping +CATEGORY_MAPPING = { + # Common + "enabled": "common.status.enabled", + "disabled": "common.status.disabled", + "live": "common.status.live", + "unknownException": "common.errors.unknown", + "missingIntents": "common.errors.missingIntents", + # "decodeError": "common.errors.decodeError", + + # Voice + "connect": "voice.connection.connect", + "noChannel": "voice.connection.noChannel", + "alreadyConnected": "voice.connection.alreadyConnected", + "noPermission": "voice.connection.noPermission", + "notInChannel": "voice.connection.notInChannel", + # "noVoiceChannel": "voice.connection.noVoiceChannel", + "setStageAnnounceTemplate": "voice.stageChannel.setAnnounceTemplate", + + # Player errors + "nodeReconnect": "player.errors.nodeReconnect", + "noPlayer": "player.errors.noPlayer", + "noPlaySource": "player.errors.noPlaySource", + "noTrackPlaying": "player.errors.noTrackPlaying", + "noTrackFound": "player.errors.noTrackFound", + # "missingTrackInfo": "player.errors.missingTrackInfo", + + # Player playback + "trackLoad": "player.playback.trackLoad", + "trackLoad_pos": "player.playback.trackLoadPos", + "playlistLoad": "player.playback.playlistLoad", + "nowplayingDesc": "player.playback.nowplayingDesc", + "nowplayingField": "player.playback.nowplayingField", + "nowplayingLink": "player.playback.nowplayingLink", + + # Player controls - pause + "pauseError": "player.controls.pause.error", + "pauseVote": "player.controls.pause.vote", + "paused": "player.controls.pause.success", + + # Player controls - resume + "resumeError": "player.controls.resume.error", + "resumeVote": "player.controls.resume.vote", + "resumed": "player.controls.resume.success", + + # Player controls - skip + "skipError": "player.controls.skip.error", + "skipVote": "player.controls.skip.vote", + "skipped": "player.controls.skip.success", + + # Player controls - back + "backVote": "player.controls.back.vote", + "backed": "player.controls.back.success", + + # Player controls - leave + "leaveVote": "player.controls.leave.vote", + "left": "player.controls.leave.success", + + # Player controls - shuffle + "shuffleError": "player.controls.shuffle.error", + "shuffleVote": "player.controls.shuffle.vote", + "shuffled": "player.controls.shuffle.success", + + # Player controls - other + "seek": "player.controls.seek", + "forward": "player.controls.forward", + "rewind": "player.controls.rewind", + "replay": "player.controls.replay", + "repeat": "player.controls.repeat", + "autoplay": "player.controls.autoplay", + + # Player buttons + "buttonBack": "player.buttons.back", + "buttonPause": "player.buttons.pause", + "buttonResume": "player.buttons.resume", + "buttonSkip": "player.buttons.skip", + "buttonLeave": "player.buttons.leave", + "buttonLoop": "player.buttons.loop", + "buttonVolumeUp": "player.buttons.volumeUp", + "buttonVolumeDown": "player.buttons.volumeDown", + "buttonVolumeMute": "player.buttons.volumeMute", + "buttonVolumeUnmute": "player.buttons.volumeUnmute", + "buttonAutoPlay": "player.buttons.autoPlay", + "buttonShuffle": "player.buttons.shuffle", + "buttonForward": "player.buttons.forward", + "buttonRewind": "player.buttons.rewind", + "buttonLyrics": "player.buttons.lyrics", + + # Player dropdown + "playerDropdown": "player.dropdown.trackSelect", + # "playerFilter": "player.dropdown.filterSelect", + + # Queue management + "queueTitle": "queue.management.title", + "cleared": "queue.management.cleared", + "removed": "queue.management.removed", + "swapped": "queue.management.swapped", + "moved": "queue.management.moved", + + # Queue view + "viewTitle": "queue.view.title", + "viewDesc": "queue.view.desc", + "historyTitle": "queue.view.historyTitle", + + # Queue errors + "voicelinkQueueFull": "queue.errors.queueFull", + "voicelinkOutofList": "queue.errors.outOfList", + "voicelinkDuplicateTrack": "queue.errors.duplicateTrack", + + # Search + "searchTitle": "search.title", + "searchDesc": "search.desc", + "searchWait": "search.wait", + # "searchTimeout": "search.timeout", + "searchSuccess": "search.success", + "noLinkSupport": "search.noLinkSupport", + + # Playlist view + "playlistViewTitle": "playlist.view.title", + "playlistViewHeaders": "playlist.view.headers", + "playlistFooter": "playlist.view.footer", + "playlistView": "playlist.view.detailTitle", + "playlistViewDesc": "playlist.view.detailDesc", + "playlistViewPermsValue": "playlist.view.permsValue", + "playlistViewPermsValue2": "playlist.view.permsValue2", + "playlistViewTrack": "playlist.view.trackList", + "playlistViewFooter": "playlist.view.footer2", + # "playlistViewPage": "playlist.view.page", + + # Playlist actions + "playlistPlay": "playlist.actions.play", + "playlistCreated": "playlist.actions.created", + "playlistRenamed": "playlist.actions.renamed", + "playlistRemove": "playlist.actions.removed", + "playlistRemoved": "playlist.actions.trackRemoved", + "playlistClear": "playlist.actions.cleared", + "playlistAdded": "playlist.actions.trackAdded", + + # Playlist errors + "playlistNotFound": "playlist.errors.notFound", + "playlistNotAccess": "playlist.errors.noAccess", + "playlistNoTrack": "playlist.errors.noTrack", + "playlistNotAllow": "playlist.errors.notAllowed", + "playlistOverText": "playlist.errors.nameOverLimit", + "playlistSameName": "playlist.errors.sameName", + "playlistDeleteError": "playlist.errors.deleteDefault", + "playlistPositionNotFound": "playlist.errors.positionNotFound", + "playlistNotInvalidUrl": "playlist.errors.invalidUrl", + "playlistExists": "playlist.errors.exists", + "overPlaylistCreation": "playlist.errors.limitReached", + "playlistLimitTrack": "playlist.errors.trackLimitReached", + "playlistPlaylistLink": "playlist.errors.playlistLinkNotAllowed", + "playlistStream": "playlist.errors.streamNotAllowed", + "playlistAddError": "playlist.errors.streaming", + "playlistAddError2": "playlist.errors.track", + "playlistLimited": "playlist.errors.trackLimited", + "playlistRepeated": "playlist.errors.trackRepeated", + # "emptyPlaylistMessage": "playlist.errors.emptyPlaylist", + + # Playlist sharing + "playlistSendErrorPlayer": "playlist.sharing.sendErrorPlayer", + "playlistSendErrorBot": "playlist.sharing.sendErrorBot", + "playlistBelongs": "playlist.sharing.belongs", + "playlistShare": "playlist.sharing.alreadyShared", + "playlistSent": "playlist.sharing.alreadySent", + "noPlaylistAcc": "playlist.sharing.noAccount", + "invitationSent": "playlist.sharing.invitationSent", + + # Playlist inbox + "inboxFull": "playlist.inbox.full", + "inboxNoMsg": "playlist.inbox.noMessages", + + # Effects + "addEffect": "effects.added", + "clearEffect": "effects.cleared", + "filterTagAlreadyInUse": "effects.tagInUse", + + # Lyrics + "lyricsNotFound": "lyrics.notFound", + + # Settings menu + "settingsMenu": "settings.menu", + "settingsTitle": "settings.basic.title", + "settingsValue": "settings.basic.value", + "settingsTitle2": "settings.queue.title", + "settingsValue2": "settings.queue.value", + "settingsTitle3": "settings.voice.title", + "settingsPermTitle": "settings.permissions.title", + "settingsPermValue": "settings.permissions.value", + + # Settings actions + "languageNotFound": "settings.actions.languageNotFound", + "changedLanguage": "settings.actions.languageChanged", + "setPrefix": "settings.actions.prefixSet", + "setDJ": "settings.actions.djSet", + "setQueue": "settings.actions.queueModeSet", + "247": "settings.actions.mode247", + "bypassVote": "settings.actions.bypassVote", + "setVolume": "settings.actions.volumeSet", + "toggleController": "settings.actions.controllerToggled", + "toggleDuplicateTrack": "settings.actions.duplicateTrackToggled", + "toggleControllerMsg": "settings.actions.controllerMsgToggled", + "toggleSilentMsg": "settings.actions.silentMsgToggled", + + # Permissions + "notdj": "permissions.notDj", + "djToMe": "permissions.djToSelf", + "djNotInChannel": "permissions.djNotInChannel", + "djswap": "permissions.djSwapped", + "missingPosPerm": "permissions.missingPosition", + "missingModePerm": "permissions.missingMode", + "missingQueuePerm": "permissions.missingQueue", + "missingAutoPlayPerm": "permissions.missingAutoPlay", + "missingFunctionPerm": "permissions.missingFunction", + "noCreatePermission": "permissions.noCreatePermission", + + # Voting + # "notVote": "voting.notVoted", + "voted": "voting.voted", + + # Ping + "pingTitle1": "ping.title1", + "pingTitle2": "ping.title2", + "pingField1": "ping.field1", + "pingField2": "ping.field2", + + # Song Request + "createSongRequestChannel": "songRequest.channelCreated", + + # Time + "timeFormatError": "time.formatError", + "invalidStartTime": "time.invalidStartTime", + "invalidEndTime": "time.invalidEndTime", + # "invalidTimeOrder": "time.invalidTimeOrder", +} + + +def unflatten_json(flat_json, separator='.'): + """Convert a flattened JSON object back to nested structure.""" + nested = {} + + for key, value in flat_json.items(): + keys = key.split(separator) + current = nested + + for i, k in enumerate(keys[:-1]): + if k not in current: + current[k] = {} + current = current[k] + + current[keys[-1]] = value + + return nested + + +def categorize_language_json(input_data, mapping=CATEGORY_MAPPING): + """ + Categorize a flat language JSON using the mapping. + + Args: + input_data (dict): The flat language JSON + mapping (dict): The categorization mapping + + Returns: + dict: Categorized nested JSON + """ + categorized_flat = {} + unmapped_keys = [] + + for old_key, value in input_data.items(): + if old_key in mapping: + new_key = mapping[old_key] + categorized_flat[new_key] = value + else: + unmapped_keys.append(old_key) + # Keep unmapped keys in a special category + categorized_flat[f"unmapped.{old_key}"] = value + + if unmapped_keys: + print(f"⚠️ Warning: {len(unmapped_keys)} unmapped keys found:") + for key in unmapped_keys[:10]: # Show first 10 + print(f" - {key}") + if len(unmapped_keys) > 10: + print(f" ... and {len(unmapped_keys) - 10} more") + + return unflatten_json(categorized_flat) + + +def process_language_file(input_file, output_file): + """ + Process a single language file and categorize it. + + Args: + input_file (str): Path to input JSON file + output_file (str): Path to output JSON file + """ + try: + with open(input_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + categorized = categorize_language_json(data) + + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(categorized, f, ensure_ascii=False, indent=4) + + print(f"✓ Successfully processed: {input_file} -> {output_file}") + + except FileNotFoundError: + print(f"✗ Error: File '{input_file}' not found") + except json.JSONDecodeError: + print(f"✗ Error: Invalid JSON in file '{input_file}'") + except Exception as e: + print(f"✗ Error processing {input_file}: {str(e)}") + + +def batch_process_languages(input_dir, output_dir, pattern="*.json"): + """ + Process all language JSON files in a directory. + + Args: + input_dir (str): Directory containing input JSON files + output_dir (str): Directory to save categorized JSON files + pattern (str): File pattern to match (default: "*.json") + """ + input_path = Path(input_dir) + output_path = Path(output_dir) + + # Create output directory if it doesn't exist + output_path.mkdir(parents=True, exist_ok=True) + + # Find all matching files + files = list(input_path.glob(pattern)) + + if not files: + print(f"No files found matching pattern '{pattern}' in '{input_dir}'") + return + + print(f"Found {len(files)} file(s) to process\n") + + success_count = 0 + for file in files: + output_file = output_path / file.name + try: + process_language_file(str(file), str(output_file)) + success_count += 1 + except Exception as e: + print(f"✗ Failed to process {file.name}: {str(e)}") + + print(f"\n{'='*50}") + print(f"Processing complete: {success_count}/{len(files)} files successfully processed") + + +# Example usage +if __name__ == "__main__": + print("Language JSON Categorizer") + print("="*50) + + code = "ZHCN" + input_file = f'langs/{code}.json' + output_file = f'langs/{code}.json' + process_language_file(f'{input_file}', f'{output_file}') \ No newline at end of file diff --git a/main.py b/main.py index d321c6a..a813856 100644 --- a/main.py +++ b/main.py @@ -173,7 +173,7 @@ class Vocard(commands.Bot): return await ctx.reply(embed=embed) elif not issubclass(error.__class__, VoicelinkException): - error = await Lang_handler.get_lang(ctx.guild.id, "unknownException") + bot_config.invite_link + 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: diff --git a/settings Example.json b/settings Example.json index 7f874ed..af423cb 100644 --- a/settings Example.json +++ b/settings Example.json @@ -127,27 +127,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": { @@ -157,7 +157,7 @@ }, { "tracks": { - "label": "@@t_playerDropdown@@", + "label": "@@t_player.dropdown.trackSelect@@", "max_options": 10 } } diff --git a/voicelink/ipc/methods.py b/voicelink/ipc/methods.py index 14d1d10..146197c 100644 --- a/voicelink/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -167,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: @@ -188,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: @@ -253,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) @@ -286,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) @@ -309,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 diff --git a/voicelink/language.py b/voicelink/language.py index 01d7839..deb8029 100644 --- a/voicelink/language.py +++ b/voicelink/language.py @@ -25,7 +25,7 @@ import os import json import logging -from typing import Optional, Union, List +from typing import Optional, Union, List, Dict, Any from .config import Config from .mongodb import MongoDBHandler @@ -74,13 +74,38 @@ class LangHandler: filepath = os.path.join(directory, file) try: with open(filepath, encoding="utf8") as f: - storage[lang_code] = json.load(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]]: """ diff --git a/voicelink/placeholders.py b/voicelink/placeholders.py index 030a821..5f5ef5d 100644 --- a/voicelink/placeholders.py +++ b/voicelink/placeholders.py @@ -100,7 +100,7 @@ class PlayerPlaceholder: @ensure_track def track_duration(self, track: Track) -> str: - return self.player.get_msg("live") if track.is_stream else format_ms(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: diff --git a/voicelink/player.py b/voicelink/player.py index de07517..b2c85ab 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -64,7 +64,7 @@ if TYPE_CHECKING: from .ipc import IPCClient async def connect_channel(ctx: Union[commands.Context, Interaction], channel: VoiceChannel = None): - texts = await LangHandler.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: @@ -268,7 +268,7 @@ class Player(VoiceProtocol): """ 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. @@ -279,8 +279,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 @@ -304,7 +303,7 @@ class Player(VoiceProtocol): 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]) @@ -618,13 +617,13 @@ 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", format_ms(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", format_ms(track_length))) + raise VoicelinkException(self.get_msg("time.invalidEndTime", format_ms(track_length))) track.position = start_time track.end_time = end_time @@ -647,7 +646,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) @@ -716,7 +715,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) @@ -771,7 +770,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}) diff --git a/voicelink/queue.py b/voicelink/queue.py index f7f4efc..dc05ea0 100644 --- a/voicelink/queue.py +++ b/voicelink/queue.py @@ -88,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 @@ -138,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] @@ -150,7 +150,7 @@ 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: discord.Member = None) -> Dict[int, Track]: pos = self._position - 1 @@ -172,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: @@ -207,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/views/controller.py b/voicelink/views/controller.py index 46b32cb..2d3d903 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -98,13 +98,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) @@ -113,7 +113,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) @@ -135,7 +135,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()): @@ -156,15 +156,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) @@ -177,15 +177,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): @@ -195,21 +195,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") + return await self.send(interaction, "playlist.errors.streamNotAllowed") user = await MongoDBHandler.get_user(interaction.user.id, '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) + 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): @@ -220,7 +220,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) @@ -233,12 +233,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): @@ -246,12 +246,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): @@ -262,7 +262,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) @@ -276,11 +276,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 LangHandler.get_lang(interaction.guild_id, 'enabled' if check else "disabled")) + await self.send(interaction, '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() @@ -292,16 +292,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): @@ -312,10 +312,10 @@ 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) @@ -331,10 +331,10 @@ 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 @@ -351,7 +351,7 @@ 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) title = self.player.current.title artist = self.player.current.author @@ -360,7 +360,7 @@ class Lyrics(ControlButton): 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 = 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) @@ -387,13 +387,13 @@ class Tracks(discord.ui.Select): async def callback(self, interaction: discord.Interaction): if not self.player.is_privileged(interaction.user): - return await send_localized_message(interaction, "missingFunctionPerm", ephemeral=True) + return await send_localized_message(interaction, "permissions.missingFunction", ephemeral=True) self.player.queue.skipto(int(self.values[0].split(". ")[0])) await self.player.stop() if self.player.settings.get("controller_msg", True): - await send_localized_message(interaction, "skipped", interaction.user) + await send_localized_message(interaction, "player.controls.skip.success", interaction.user) class Effects(discord.ui.Select): def __init__(self, player: "voicelink.Player", btn_data, row): @@ -412,20 +412,20 @@ class Effects(discord.ui.Select): async def callback(self, interaction: discord.Interaction): if not self.player.is_privileged(interaction.user): - return await send_localized_message(interaction, "missingFunctionPerm", ephemeral=True) + return await send_localized_message(interaction, "permissions.missingFunction", ephemeral=True) avalibable_filters = voicelink.Filters.get_available_filters() if self.values[0] == "None": await self.player.reset_filter(requester=interaction.user) - return await send_localized_message(interaction, "clearEffect") + return await send_localized_message(interaction, "effects.cleared") selected_filter = avalibable_filters.get(self.values[0].lower())() if self.player.filters.has_filter(filter_tag=selected_filter.tag): await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user) - await send_localized_message(interaction, "clearEffect") + await send_localized_message(interaction, "effects.cleared") else: await self.player.add_filter(selected_filter, requester=interaction.user) - await send_localized_message(interaction, "addEffect", selected_filter.tag) + await send_localized_message(interaction, "effects.added", selected_filter.tag) BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = { "back": Back, @@ -466,7 +466,7 @@ class InteractiveController(discord.ui.View): async def interaction_check(self, interaction: discord.Interaction): if not self.player.node._available: - await send_localized_message(interaction, "nodeReconnect", ephemeral=True) + await send_localized_message(interaction, "player.errors.nodeReconnect", ephemeral=True) return False if interaction.user.id in Config().bot_access_user: @@ -478,7 +478,7 @@ class InteractiveController(discord.ui.View): raise ButtonOnCooldown(retry_after) return True else: - await send_localized_message(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: diff --git a/voicelink/views/lyrics.py b/voicelink/views/lyrics.py index 1ef9e1f..f23f38e 100644 --- a/voicelink/views/lyrics.py +++ b/voicelink/views/lyrics.py @@ -66,7 +66,7 @@ class LyricsView(PaginationView): async def build_embed(self) -> discord.Embed: page = self.pagination.get_current_page_items() - text = await LangHandler.get_lang(self.author.guild.id, "searchTitle") + 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 diff --git a/voicelink/views/pagination.py b/voicelink/views/pagination.py index de3753e..16742b2 100644 --- a/voicelink/views/pagination.py +++ b/voicelink/views/pagination.py @@ -22,35 +22,59 @@ SOFTWARE. """ import discord -import traceback + +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 = 300): + 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", "EN") self.update_view() - def update_view(self) -> None: - """Update button states and page number display based on current pagination state.""" + 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": self.pagination.current_page <= 2, - "back": not self.pagination.has_previous_page, - "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, - "next": not self.pagination.has_next_page, + "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 child.custom_id in button_states: - child.disabled = button_states[child.custom_id] - if child.custom_id == "page_number": - child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" + if (state := button_states.get(child.custom_id)): + child.disabled = state.get("disabled", False) + child.label = state.get("label") async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None: - traceback.print_exception(error) + return async def update_message(self, interaction: discord.Interaction) -> None: """Update the view and edit the message.""" @@ -70,14 +94,15 @@ class PaginationView(discord.ui.View): @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="Page Number", + title=texts[0], custom_id="page_number_modal", items=[ discord.ui.TextInput( - label="Page Number", + label=texts[1], custom_id="page_number", - placeholder="Enter the page number to navigate.", + placeholder="e.g. 1", default=str(self.pagination.current_page), max_length=5, required=True diff --git a/voicelink/views/playlist.py b/voicelink/views/playlist.py index 81f7709..a4230e4 100644 --- a/voicelink/views/playlist.py +++ b/voicelink/views/playlist.py @@ -32,14 +32,15 @@ from .utils import DynamicViewManager, Pagination from .pagination import PaginationView from ..config import Config from ..utils import format_ms, truncate_string +from ..mongodb import MongoDBHandler from ..language import LangHandler class PlaylistDropdown(discord.ui.Select): - def __init__(self, results: list[dict[str, Any]]) -> None: + def __init__(self, results: list[dict[str, Any]], lang: str) -> None: self.view: PlaylistViewManager super().__init__( - placeholder="Select a playlist to view ..", + placeholder=LangHandler._get_lang(lang, "playlist.view.playlistSelect"), custom_id="selector", options=[ discord.SelectOption( @@ -53,7 +54,7 @@ class PlaylistDropdown(discord.ui.Select): async def callback(self, interaction: discord.Interaction) -> None: view: PlaylistView = self.view.change_view(self.values[0]) - await interaction.response.edit_message(embed=await view.build_embed(), view=view) + await interaction.response.edit_message(embed=view.build_embed(), view=view) class PlaylistView(PaginationView): @@ -75,14 +76,14 @@ class PlaylistView(PaginationView): super().__init__(Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7), primary_view.ctx.author) - async def build_embed(self) -> discord.Embed: + 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, - "playlistView", "playlistViewDesc", "settingsPermTitle", - "playlistViewPermsValue", "playlistViewPermsValue2", - "playlistViewTrack", "playlistNoTrack", "playlistViewFooter" + 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) @@ -121,38 +122,56 @@ class PlaylistView(PaginationView): 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 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=await self.build_embed(), view=self) + await interaction.followup.edit_message(self.primary_view.response.id, embed=self.build_embed(), view=self) else: - await interaction.response.edit_message(embed=await self.build_embed(), view=self) + 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=await view.build_embed(), view=view) + await interaction.response.edit_message(embed=view.build_embed(), view=view) - @discord.ui.button(label="Play", style=discord.ButtonStyle.green) + @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", style=discord.ButtonStyle.blurple) + @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: await interaction.response.defer() - @discord.ui.button(label="Export", style=discord.ButtonStyle.gray) + @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", style=discord.ButtonStyle.red) + @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") @@ -162,14 +181,15 @@ class PlaylistView(PaginationView): 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.add_item(PlaylistDropdown(view.results, view.lang)) self.primary_view.remove_view(self.playlist_id) - await view.response.edit(embed=await view.build_embed(), view=view) + 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", "EN") views = {"home": self} views.update({result["id"]: PlaylistView(self, result) for result in results}) @@ -177,7 +197,7 @@ class PlaylistViewManager(DynamicViewManager): super().__init__(views=views, timeout=None) self.response: discord.Message = None - self.add_item(PlaylistDropdown(results)) + self.add_item(PlaylistDropdown(results, self.lang)) def get_width(self, s): width = 0 @@ -206,7 +226,7 @@ class PlaylistViewManager(DynamicViewManager): except: pass - async def build_embed(self) -> discord.Embed: + def build_embed(self) -> discord.Embed: """ Build the embed for the playlist overview. @@ -214,7 +234,7 @@ class PlaylistViewManager(DynamicViewManager): discord.Embed: The constructed embed with playlist details. """ max_p, _, _ = Config().get_playlist_config() - text = await LangHandler.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter") + text = LangHandler._get_lang(self.lang, "playlist.view.title", "playlist.view.headers", "playlist.view.footer") headers = text[1].split(",") headers.insert(0, "") diff --git a/voicelink/views/queue.py b/voicelink/views/queue.py index fc45e22..2f339d2 100644 --- a/voicelink/views/queue.py +++ b/voicelink/views/queue.py @@ -103,13 +103,13 @@ class QueueView(PaginationView): tracks = self.pagination.get_current_page_items() texts = await LangHandler.get_lang( self.author.guild.id, - "viewTitle", - "viewDesc", - "nowplayingDesc", - "live", - "queueTitle", - "historyTitle", - "playlistViewFooter", + "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) From de2a79ff9397455b458366f19b55af59b56ebfd9 Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 2 Oct 2025 00:21:35 +0800 Subject: [PATCH 09/10] Delete language.py --- language.py | 365 ---------------------------------------------------- 1 file changed, 365 deletions(-) delete mode 100644 language.py diff --git a/language.py b/language.py deleted file mode 100644 index d6945ae..0000000 --- a/language.py +++ /dev/null @@ -1,365 +0,0 @@ -import json -import os -from pathlib import Path - -# Define the categorization mapping -CATEGORY_MAPPING = { - # Common - "enabled": "common.status.enabled", - "disabled": "common.status.disabled", - "live": "common.status.live", - "unknownException": "common.errors.unknown", - "missingIntents": "common.errors.missingIntents", - # "decodeError": "common.errors.decodeError", - - # Voice - "connect": "voice.connection.connect", - "noChannel": "voice.connection.noChannel", - "alreadyConnected": "voice.connection.alreadyConnected", - "noPermission": "voice.connection.noPermission", - "notInChannel": "voice.connection.notInChannel", - # "noVoiceChannel": "voice.connection.noVoiceChannel", - "setStageAnnounceTemplate": "voice.stageChannel.setAnnounceTemplate", - - # Player errors - "nodeReconnect": "player.errors.nodeReconnect", - "noPlayer": "player.errors.noPlayer", - "noPlaySource": "player.errors.noPlaySource", - "noTrackPlaying": "player.errors.noTrackPlaying", - "noTrackFound": "player.errors.noTrackFound", - # "missingTrackInfo": "player.errors.missingTrackInfo", - - # Player playback - "trackLoad": "player.playback.trackLoad", - "trackLoad_pos": "player.playback.trackLoadPos", - "playlistLoad": "player.playback.playlistLoad", - "nowplayingDesc": "player.playback.nowplayingDesc", - "nowplayingField": "player.playback.nowplayingField", - "nowplayingLink": "player.playback.nowplayingLink", - - # Player controls - pause - "pauseError": "player.controls.pause.error", - "pauseVote": "player.controls.pause.vote", - "paused": "player.controls.pause.success", - - # Player controls - resume - "resumeError": "player.controls.resume.error", - "resumeVote": "player.controls.resume.vote", - "resumed": "player.controls.resume.success", - - # Player controls - skip - "skipError": "player.controls.skip.error", - "skipVote": "player.controls.skip.vote", - "skipped": "player.controls.skip.success", - - # Player controls - back - "backVote": "player.controls.back.vote", - "backed": "player.controls.back.success", - - # Player controls - leave - "leaveVote": "player.controls.leave.vote", - "left": "player.controls.leave.success", - - # Player controls - shuffle - "shuffleError": "player.controls.shuffle.error", - "shuffleVote": "player.controls.shuffle.vote", - "shuffled": "player.controls.shuffle.success", - - # Player controls - other - "seek": "player.controls.seek", - "forward": "player.controls.forward", - "rewind": "player.controls.rewind", - "replay": "player.controls.replay", - "repeat": "player.controls.repeat", - "autoplay": "player.controls.autoplay", - - # Player buttons - "buttonBack": "player.buttons.back", - "buttonPause": "player.buttons.pause", - "buttonResume": "player.buttons.resume", - "buttonSkip": "player.buttons.skip", - "buttonLeave": "player.buttons.leave", - "buttonLoop": "player.buttons.loop", - "buttonVolumeUp": "player.buttons.volumeUp", - "buttonVolumeDown": "player.buttons.volumeDown", - "buttonVolumeMute": "player.buttons.volumeMute", - "buttonVolumeUnmute": "player.buttons.volumeUnmute", - "buttonAutoPlay": "player.buttons.autoPlay", - "buttonShuffle": "player.buttons.shuffle", - "buttonForward": "player.buttons.forward", - "buttonRewind": "player.buttons.rewind", - "buttonLyrics": "player.buttons.lyrics", - - # Player dropdown - "playerDropdown": "player.dropdown.trackSelect", - # "playerFilter": "player.dropdown.filterSelect", - - # Queue management - "queueTitle": "queue.management.title", - "cleared": "queue.management.cleared", - "removed": "queue.management.removed", - "swapped": "queue.management.swapped", - "moved": "queue.management.moved", - - # Queue view - "viewTitle": "queue.view.title", - "viewDesc": "queue.view.desc", - "historyTitle": "queue.view.historyTitle", - - # Queue errors - "voicelinkQueueFull": "queue.errors.queueFull", - "voicelinkOutofList": "queue.errors.outOfList", - "voicelinkDuplicateTrack": "queue.errors.duplicateTrack", - - # Search - "searchTitle": "search.title", - "searchDesc": "search.desc", - "searchWait": "search.wait", - # "searchTimeout": "search.timeout", - "searchSuccess": "search.success", - "noLinkSupport": "search.noLinkSupport", - - # Playlist view - "playlistViewTitle": "playlist.view.title", - "playlistViewHeaders": "playlist.view.headers", - "playlistFooter": "playlist.view.footer", - "playlistView": "playlist.view.detailTitle", - "playlistViewDesc": "playlist.view.detailDesc", - "playlistViewPermsValue": "playlist.view.permsValue", - "playlistViewPermsValue2": "playlist.view.permsValue2", - "playlistViewTrack": "playlist.view.trackList", - "playlistViewFooter": "playlist.view.footer2", - # "playlistViewPage": "playlist.view.page", - - # Playlist actions - "playlistPlay": "playlist.actions.play", - "playlistCreated": "playlist.actions.created", - "playlistRenamed": "playlist.actions.renamed", - "playlistRemove": "playlist.actions.removed", - "playlistRemoved": "playlist.actions.trackRemoved", - "playlistClear": "playlist.actions.cleared", - "playlistAdded": "playlist.actions.trackAdded", - - # Playlist errors - "playlistNotFound": "playlist.errors.notFound", - "playlistNotAccess": "playlist.errors.noAccess", - "playlistNoTrack": "playlist.errors.noTrack", - "playlistNotAllow": "playlist.errors.notAllowed", - "playlistOverText": "playlist.errors.nameOverLimit", - "playlistSameName": "playlist.errors.sameName", - "playlistDeleteError": "playlist.errors.deleteDefault", - "playlistPositionNotFound": "playlist.errors.positionNotFound", - "playlistNotInvalidUrl": "playlist.errors.invalidUrl", - "playlistExists": "playlist.errors.exists", - "overPlaylistCreation": "playlist.errors.limitReached", - "playlistLimitTrack": "playlist.errors.trackLimitReached", - "playlistPlaylistLink": "playlist.errors.playlistLinkNotAllowed", - "playlistStream": "playlist.errors.streamNotAllowed", - "playlistAddError": "playlist.errors.streaming", - "playlistAddError2": "playlist.errors.track", - "playlistLimited": "playlist.errors.trackLimited", - "playlistRepeated": "playlist.errors.trackRepeated", - # "emptyPlaylistMessage": "playlist.errors.emptyPlaylist", - - # Playlist sharing - "playlistSendErrorPlayer": "playlist.sharing.sendErrorPlayer", - "playlistSendErrorBot": "playlist.sharing.sendErrorBot", - "playlistBelongs": "playlist.sharing.belongs", - "playlistShare": "playlist.sharing.alreadyShared", - "playlistSent": "playlist.sharing.alreadySent", - "noPlaylistAcc": "playlist.sharing.noAccount", - "invitationSent": "playlist.sharing.invitationSent", - - # Playlist inbox - "inboxFull": "playlist.inbox.full", - "inboxNoMsg": "playlist.inbox.noMessages", - - # Effects - "addEffect": "effects.added", - "clearEffect": "effects.cleared", - "filterTagAlreadyInUse": "effects.tagInUse", - - # Lyrics - "lyricsNotFound": "lyrics.notFound", - - # Settings menu - "settingsMenu": "settings.menu", - "settingsTitle": "settings.basic.title", - "settingsValue": "settings.basic.value", - "settingsTitle2": "settings.queue.title", - "settingsValue2": "settings.queue.value", - "settingsTitle3": "settings.voice.title", - "settingsPermTitle": "settings.permissions.title", - "settingsPermValue": "settings.permissions.value", - - # Settings actions - "languageNotFound": "settings.actions.languageNotFound", - "changedLanguage": "settings.actions.languageChanged", - "setPrefix": "settings.actions.prefixSet", - "setDJ": "settings.actions.djSet", - "setQueue": "settings.actions.queueModeSet", - "247": "settings.actions.mode247", - "bypassVote": "settings.actions.bypassVote", - "setVolume": "settings.actions.volumeSet", - "toggleController": "settings.actions.controllerToggled", - "toggleDuplicateTrack": "settings.actions.duplicateTrackToggled", - "toggleControllerMsg": "settings.actions.controllerMsgToggled", - "toggleSilentMsg": "settings.actions.silentMsgToggled", - - # Permissions - "notdj": "permissions.notDj", - "djToMe": "permissions.djToSelf", - "djNotInChannel": "permissions.djNotInChannel", - "djswap": "permissions.djSwapped", - "missingPosPerm": "permissions.missingPosition", - "missingModePerm": "permissions.missingMode", - "missingQueuePerm": "permissions.missingQueue", - "missingAutoPlayPerm": "permissions.missingAutoPlay", - "missingFunctionPerm": "permissions.missingFunction", - "noCreatePermission": "permissions.noCreatePermission", - - # Voting - # "notVote": "voting.notVoted", - "voted": "voting.voted", - - # Ping - "pingTitle1": "ping.title1", - "pingTitle2": "ping.title2", - "pingField1": "ping.field1", - "pingField2": "ping.field2", - - # Song Request - "createSongRequestChannel": "songRequest.channelCreated", - - # Time - "timeFormatError": "time.formatError", - "invalidStartTime": "time.invalidStartTime", - "invalidEndTime": "time.invalidEndTime", - # "invalidTimeOrder": "time.invalidTimeOrder", -} - - -def unflatten_json(flat_json, separator='.'): - """Convert a flattened JSON object back to nested structure.""" - nested = {} - - for key, value in flat_json.items(): - keys = key.split(separator) - current = nested - - for i, k in enumerate(keys[:-1]): - if k not in current: - current[k] = {} - current = current[k] - - current[keys[-1]] = value - - return nested - - -def categorize_language_json(input_data, mapping=CATEGORY_MAPPING): - """ - Categorize a flat language JSON using the mapping. - - Args: - input_data (dict): The flat language JSON - mapping (dict): The categorization mapping - - Returns: - dict: Categorized nested JSON - """ - categorized_flat = {} - unmapped_keys = [] - - for old_key, value in input_data.items(): - if old_key in mapping: - new_key = mapping[old_key] - categorized_flat[new_key] = value - else: - unmapped_keys.append(old_key) - # Keep unmapped keys in a special category - categorized_flat[f"unmapped.{old_key}"] = value - - if unmapped_keys: - print(f"⚠️ Warning: {len(unmapped_keys)} unmapped keys found:") - for key in unmapped_keys[:10]: # Show first 10 - print(f" - {key}") - if len(unmapped_keys) > 10: - print(f" ... and {len(unmapped_keys) - 10} more") - - return unflatten_json(categorized_flat) - - -def process_language_file(input_file, output_file): - """ - Process a single language file and categorize it. - - Args: - input_file (str): Path to input JSON file - output_file (str): Path to output JSON file - """ - try: - with open(input_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - categorized = categorize_language_json(data) - - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(categorized, f, ensure_ascii=False, indent=4) - - print(f"✓ Successfully processed: {input_file} -> {output_file}") - - except FileNotFoundError: - print(f"✗ Error: File '{input_file}' not found") - except json.JSONDecodeError: - print(f"✗ Error: Invalid JSON in file '{input_file}'") - except Exception as e: - print(f"✗ Error processing {input_file}: {str(e)}") - - -def batch_process_languages(input_dir, output_dir, pattern="*.json"): - """ - Process all language JSON files in a directory. - - Args: - input_dir (str): Directory containing input JSON files - output_dir (str): Directory to save categorized JSON files - pattern (str): File pattern to match (default: "*.json") - """ - input_path = Path(input_dir) - output_path = Path(output_dir) - - # Create output directory if it doesn't exist - output_path.mkdir(parents=True, exist_ok=True) - - # Find all matching files - files = list(input_path.glob(pattern)) - - if not files: - print(f"No files found matching pattern '{pattern}' in '{input_dir}'") - return - - print(f"Found {len(files)} file(s) to process\n") - - success_count = 0 - for file in files: - output_file = output_path / file.name - try: - process_language_file(str(file), str(output_file)) - success_count += 1 - except Exception as e: - print(f"✗ Failed to process {file.name}: {str(e)}") - - print(f"\n{'='*50}") - print(f"Processing complete: {success_count}/{len(files)} files successfully processed") - - -# Example usage -if __name__ == "__main__": - print("Language JSON Categorizer") - print("="*50) - - code = "ZHCN" - input_file = f'langs/{code}.json' - output_file = f'langs/{code}.json' - process_language_file(f'{input_file}', f'{output_file}') \ No newline at end of file From b6edd24171421567efa6c322b1809474eb379531 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:09:51 +0800 Subject: [PATCH 10/10] Updated the language pack --- langs/DE.json | 12 ++---------- langs/EN.json | 13 ++----------- langs/ES.json | 12 ++---------- langs/FR.json | 12 ++---------- langs/JA.json | 12 ++---------- langs/KO.json | 12 ++---------- langs/PL.json | 12 ++---------- langs/RU.json | 12 ++---------- langs/UA.json | 12 ++---------- langs/VN.json | 13 +++---------- langs/ZHCN.json | 12 ++---------- langs/ZHTW.json | 12 ++---------- 12 files changed, 25 insertions(+), 121 deletions(-) diff --git a/langs/DE.json b/langs/DE.json index bb79248..593b21e 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -19,7 +19,8 @@ "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 ..." + "trackSelect": "Wähle einen Song aus, um zu überspringen ...", + "filterSelect": "Wähle einen Filter aus, um ihn anzuwenden ..." }, "buttons": { "back": "Zurück", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` ist nicht im Sprachkanal.", "djSwapped": "Du hast die DJ-Rolle auf `{0}` übertragen." }, - "unmapped": { - "notVote": "Dieser Befehl erfordert Deine Stimme! Gebe `/vote` ein, um weitere Informationen zu erhalten.", - "missingTrackInfo": "Einige Track-Informationen fehlen.", - "noVoiceChannel": "Dieser Sprachkanal wurde nicht gefunden!", - "playerFilter": "Wähle einen Filter aus, um ihn anzuwenden ...", - "searchTimeout": "Die Suche wurde abgebrochen. Bitte versuche es später erneut.", - "decodeError": "Beim Dekodieren der Datei ist etwas schief gelaufen!", - "invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein." - }, "settings": { "actions": { "languageNotFound": "Kein Sprachpaket gefunden. Bitte wähle ein vorhandenes Sprachpaket aus.", diff --git a/langs/EN.json b/langs/EN.json index 1d6ea7d..28dc5a9 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -19,7 +19,8 @@ "noTrackFound": "No songs were found with that query! Please provide a valid url." }, "dropdown": { - "trackSelect": "Select a song to skip to ..." + "trackSelect": "Select a song to skip to ...", + "filterSelect": "Select a filter to apply ..." }, "buttons": { "back": "Back", @@ -107,16 +108,6 @@ "djNotInChannel": "`{0}` is not in the voice channel.", "djSwapped": "You have transferred the role of dj to `{0}`." }, - "unmapped": { - "notVote": "This command requires your vote! Type `/vote` for more info.", - "emptyPlaylistMessage": "🎶 No playlists found. Create your first one with `/playlist create`!", - "missingTrackInfo": "Some track details are missing.", - "noVoiceChannel": "Voice Channel Not Found!", - "playerFilter": "Select a filter to apply ...", - "searchTimeout": "Search timed out. Please try again later.", - "decodeError": "Something went wrong while decoding the file!", - "invalidTimeOrder": "End time cannot be less than or equal to start time" - }, "settings": { "actions": { "languageNotFound": "No language pack found! please select an existing language pack.", diff --git a/langs/ES.json b/langs/ES.json index e0c6d60..24460b2 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -19,7 +19,8 @@ "noTrackFound": "No se encontraron canciones con esa consulta. Proporcione una URL válida." }, "dropdown": { - "trackSelect": "Seleccione una canción para saltar a ..." + "trackSelect": "Seleccione una canción para saltar a ...", + "filterSelect": "Seleccione un filtro para aplicar ..." }, "buttons": { "back": "Atrás", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` no está en el canal de voz.", "djSwapped": "Has transferido el rol de DJ a `{0}`." }, - "unmapped": { - "notVote": "¡Este comando requiere su voto! Escriba `/vote` para obtener más información.", - "missingTrackInfo": "Falta información de la canción.", - "noVoiceChannel": "¡Canal de voz no encontrado!", - "playerFilter": "Seleccione un filtro para aplicar ...", - "searchTimeout": "La búsqueda ha expirado. Por favor, inténtelo de nuevo más tarde.", - "decodeError": "¡Algo salió mal al decodificar el archivo!", - "invalidTimeOrder": "El tiempo final no puede ser menor o igual que el tiempo de inicio." - }, "settings": { "actions": { "languageNotFound": "¡No se encontró paquete de idioma! por favor seleccione un paquete de idioma existente.", diff --git a/langs/FR.json b/langs/FR.json index af5d78f..90d6441 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -19,7 +19,8 @@ "noTrackFound": "Aucune chanson trouvée pour cette requête ! Veuillez fournir une URL valide." }, "dropdown": { - "trackSelect": "Sélectionnez une chanson vers laquelle passer..." + "trackSelect": "Sélectionnez une chanson vers laquelle passer...", + "filterSelect": "Sélectionnez un filtre à appliquer..." }, "buttons": { "back": "Précédent", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` n'est pas dans le salon vocal.", "djSwapped": "Vous avez transféré le rôle de DJ à `{0}`." }, - "unmapped": { - "notVote": "Cette commande nécessite votre vote ! Tapez `/vote` pour en savoir plus.", - "missingTrackInfo": "Certaines informations sur le morceau sont manquantes.", - "noVoiceChannel": "Aucun salon vocal trouvé !", - "playerFilter": "Sélectionnez un filtre à appliquer...", - "searchTimeout": "Recherche expirée. Veuillez réessayer plus tard.", - "decodeError": "Une erreur s'est produite lors du décodage du fichier !", - "invalidTimeOrder": "L'heure de fin ne peut pas être inférieure ou égale à celle de début" - }, "settings": { "actions": { "languageNotFound": "Aucun pack de langue trouvé ! Veuillez sélectionner un pack de langue existant.", diff --git a/langs/JA.json b/langs/JA.json index eeef9fd..56133a1 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -19,7 +19,8 @@ "noTrackFound": "そのクエリで曲が見つかりませんでした!有効なURLを入力してください。" }, "dropdown": { - "trackSelect": "スキップする曲を選択してください..." + "trackSelect": "スキップする曲を選択してください...", + "filterSelect": "適用するフィルターを選択してください..." }, "buttons": { "back": "戻る", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}`はボイスチャンネルにいません。", "djSwapped": "DJの役割を`{0}`に移行しました。" }, - "unmapped": { - "notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。", - "missingTrackInfo": "一部のトラック情報が欠落しています。", - "noVoiceChannel": "音声チャンネルが見つかりません!", - "playerFilter": "適用するフィルターを選択してください...", - "searchTimeout": "検索がタイムアウトしました。後でもう一度お試しください。", - "decodeError": "ファイルのデコード中に問題が発生しました!", - "invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。" - }, "settings": { "actions": { "languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。", diff --git a/langs/KO.json b/langs/KO.json index 858eac6..e105467 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -19,7 +19,8 @@ "noTrackFound": "해당 쿼리로 곡을 찾을 수 없습니다. 유효한 URL을 제공해주세요." }, "dropdown": { - "trackSelect": "건너뛰기 할 노래를 선택하세요..." + "trackSelect": "건너뛰기 할 노래를 선택하세요...", + "filterSelect": "적용할 필터를 선택하세요..." }, "buttons": { "back": "이전", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}`님이 음성 채널에 없습니다.", "djSwapped": "당신은 DJ 권한을 `{0}`님에게 이전했습니다." }, - "unmapped": { - "notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.", - "missingTrackInfo": "일부 트랙 정보가 누락되었습니다.", - "noVoiceChannel": "음성 채널을 찾을 수 없습니다!", - "playerFilter": "적용할 필터를 선택하세요...", - "searchTimeout": "검색이 시간 초과되었습니다. 나중에 다시 시도해주세요.", - "decodeError": "파일 디코딩 중 문제가 발생했습니다!", - "invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다." - }, "settings": { "actions": { "languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.", diff --git a/langs/PL.json b/langs/PL.json index 01ba020..ac7fc50 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -19,7 +19,8 @@ "noTrackFound": "Nie znaleziono utworu! Podaj poprawny adres URL." }, "dropdown": { - "trackSelect": "Wybierz pozycję do której pominąć ..." + "trackSelect": "Wybierz pozycję do której pominąć ...", + "filterSelect": "Wybierz efekt który chesz nałożyć ..." }, "buttons": { "back": "Cofnij", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` nie jest na kanale głosowym.", "djSwapped": "Przekazałeś rolę DJ'a użytkownikowi `{0}`." }, - "unmapped": { - "notVote": "To polecenie wymaga głosowania! Wpisz `/vote`, aby uzyskać więcej informacji.", - "missingTrackInfo": "Brakuje niektórych informacji o utworze.", - "noVoiceChannel": "Kanał nie został znaleziony", - "playerFilter": "Wybierz efekt który chesz nałożyć ...", - "searchTimeout": "Upłynął czas wyszukiwania. Spróbuj ponownie później.", - "decodeError": "Coś poszło nie tak podczas dekodowania pliku!", - "invalidTimeOrder": "Czas zakończenia odtwarzania nie może być mniejszy lub równy czasowi startu" - }, "settings": { "actions": { "languageNotFound": "Nie znaleziono pakietu językowego! wybierz istniejący pakiet językowy.", diff --git a/langs/RU.json b/langs/RU.json index 972e0a0..804d4c6 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -19,7 +19,8 @@ "noTrackFound": "Треки по такому запросу не найдены! Пожалуйста, укажите действительную ссылку." }, "dropdown": { - "trackSelect": "Выберите трек для перехода ..." + "trackSelect": "Выберите трек для перехода ...", + "filterSelect": "Выберите фильтр для применения ..." }, "buttons": { "back": "Назад", @@ -107,15 +108,6 @@ "djNotInChannel": "Пользователь `{0}` не находится в голосовом канале.", "djSwapped": "Вы передали роль DJ пользователю `{0}`." }, - "unmapped": { - "notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.", - "missingTrackInfo": "Некоторая информация о треке отсутствует.", - "noVoiceChannel": "Голосовой канал не найден!", - "playerFilter": "Выберите фильтр для применения ...", - "searchTimeout": "Поиск прерван: превышено время ожидания. Пожалуйста, попробуйте позже.", - "decodeError": "Что-то пошло не так при декодировании файла!", - "invalidTimeOrder": "Время конца трека не может быть меньше или равно времени начала." - }, "settings": { "actions": { "languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.", diff --git a/langs/UA.json b/langs/UA.json index c02b3b1..919b0c6 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -19,7 +19,8 @@ "noTrackFound": "Пісні з таким запитом не знайдено! Будь ласка, вкажіть дійсне посилання." }, "dropdown": { - "trackSelect": "Виберіть пісню для переходу ..." + "trackSelect": "Виберіть пісню для переходу ...", + "filterSelect": "Виберіть фільтр для застосування ..." }, "buttons": { "back": "Назад", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` не знаходиться в голосовому каналі.", "djSwapped": "Ви передали роль DJ `{0}`." }, - "unmapped": { - "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", - "missingTrackInfo": "Деяка інформація про трек відсутня.", - "noVoiceChannel": "Голосовий канал не знайдено!", - "playerFilter": "Виберіть фільтр для застосування ...", - "searchTimeout": "Пошук перервано: перевищено час очікування. Будь ласка, спробуйте пізніше.", - "decodeError": "Щось пішло не так під час декодування файлу!", - "invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку." - }, "settings": { "actions": { "languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.", diff --git a/langs/VN.json b/langs/VN.json index 8bd6a9b..78e4612 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -19,7 +19,8 @@ "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 ..." + "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", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` không ở trong kênh voice.", "djSwapped": "Bạn đã chuyển vai trò DJ cho `{0}`." }, - "unmapped": { - "notVote": "Lệnh này yêu cầu bạn bình chọn! Gõ `/vote` để biết thêm thông tin.", - "missingTrackInfo": "Một số chi tiết bài hát bị thiếu.", - "noVoiceChannel": "Không Tìm Thấy Kênh Voice!", - "playerFilter": "Chọn một bộ lọc để áp dụng ...", - "searchTimeout": "Tìm kiếm hết thời gian. Vui lòng thử lại sau.", - "decodeError": "Đã xảy ra lỗi khi giải mã tệp!", - "invalidTimeOrder": "Thời gian kết thúc không thể nhỏ hơn hoặc bằng thời gian bắt đầu" - }, "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ó.", @@ -169,6 +161,7 @@ "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": { diff --git a/langs/ZHCN.json b/langs/ZHCN.json index 9c70855..57f6e82 100644 --- a/langs/ZHCN.json +++ b/langs/ZHCN.json @@ -19,7 +19,8 @@ "noTrackFound": "未找到对应的歌曲,请提供有效链接。" }, "dropdown": { - "trackSelect": "选择要跳转的歌曲 ..." + "trackSelect": "选择要跳转的歌曲 ...", + "filterSelect": "选择要应用的音效 ..." }, "buttons": { "back": "上一首", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` 不在语音频道中。", "djSwapped": "您已将 DJ 身份转移给 `{0}`。" }, - "unmapped": { - "notVote": "该命令需要您先投票!输入 `/vote` 了解更多。", - "missingTrackInfo": "部分曲目信息缺失。", - "noVoiceChannel": "未找到语音频道!", - "playerFilter": "选择要应用的音效 ...", - "searchTimeout": "搜索超时,请稍后重试。", - "decodeError": "解码文件时出错!", - "invalidTimeOrder": "结束时间不能小于或等于开始时间" - }, "settings": { "actions": { "languageNotFound": "未找到语言包!请选择一个已有的语言包。", diff --git a/langs/ZHTW.json b/langs/ZHTW.json index eb0216c..1b9d148 100644 --- a/langs/ZHTW.json +++ b/langs/ZHTW.json @@ -19,7 +19,8 @@ "noTrackFound": "未找到對應的歌曲,請提供有效連結。" }, "dropdown": { - "trackSelect": "選擇要跳轉的歌曲 ..." + "trackSelect": "選擇要跳轉的歌曲 ...", + "filterSelect": "選擇要套用的音效 ..." }, "buttons": { "back": "上一首", @@ -107,15 +108,6 @@ "djNotInChannel": "`{0}` 不在語音頻道中。", "djSwapped": "您已將 DJ 身分轉移給 `{0}`。" }, - "unmapped": { - "notVote": "此指令需要您先投票!輸入 `/vote` 了解更多。", - "missingTrackInfo": "部分曲目資訊缺失。", - "noVoiceChannel": "未找到語音頻道!", - "playerFilter": "選擇要套用的音效 ...", - "searchTimeout": "搜尋逾時,請稍後再試。", - "decodeError": "解碼檔案時發生錯誤!", - "invalidTimeOrder": "結束時間不能小於或等於開始時間" - }, "settings": { "actions": { "languageNotFound": "未找到語言包!請選擇一個現有的語言包。",