From f1fa4606edff7291e84030901627849cec790f72 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:49:52 +0800 Subject: [PATCH 01/52] Fix track existence check in Node.get_tracks Corrects a variable name in the track existence check from 'track' to 'tracks', ensuring the method properly returns an empty list when no tracks are found. --- voicelink/pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicelink/pool.py b/voicelink/pool.py index 427fcfd..ec25f31 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -399,7 +399,7 @@ class Node: return [] tracks = await self.get_tracks(query=query, requester=self.bot.user) - if not track: + if not tracks: return [] if isinstance(tracks, Playlist): 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 02/52] 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 260af1d44731b58bc5d06780b5e4dcdfe4648e24 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:05:20 +0800 Subject: [PATCH 03/52] Remove Lavalink Docker setup and config files Deleted docker-compose.yml, lavalink/Dockerfile-lavalink, and lavalink/application.yml to remove support for running Lavalink via Docker. This change may indicate a migration away from Docker-based deployment or a refactor of the service architecture. --- docker-compose.yml | 106 ------------------- lavalink/Dockerfile-lavalink | 16 --- lavalink/application.yml | 197 ----------------------------------- 3 files changed, 319 deletions(-) delete mode 100644 docker-compose.yml delete mode 100644 lavalink/Dockerfile-lavalink delete mode 100644 lavalink/application.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 0fc8e39..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,106 +0,0 @@ -# ------------------------------------------------------------------------------------------------------------ # - -# For installation instructions please visit - https://docs.vocard.xyz/latest/bot/setup/docker-linux/ -# or for Windows Docker - https://docs.vocard.xyz/latest/bot/setup/docker-windows/ - -# ------------------------------------------ THANK YOU FOR READING! ------------------------------------------ # -name: vocard -services: - lavalink: - container_name: lavalink - # image: ghcr.io/lavalink-devs/lavalink:latest - # Use build to run Lavalink on the latest JRE 23 version (for better performance) - build: - context: ./lavalink - dockerfile: ./Dockerfile-lavalink - restart: unless-stopped - environment: - - _JAVA_OPTIONS=-Xmx1G - - SERVER_PORT=2333 - # there is no point in changing the password here, since the container is available only in docker network - - LAVALINK_SERVER_PASSWORD=youshallnotpass # Change password if needed (don't forget to change it in healthcheck below and settings.json) - volumes: - - ./lavalink/application.yml:/opt/Lavalink/application.yml - networks: - - vocard - expose: - - "2333" - healthcheck: - test: nc -z -v localhost 2333 - interval: 10s - timeout: 5s - retries: 5 - - spotify-tokener: - image: ghcr.io/topi314/spotify-tokener:master - container_name: spotify-tokener - restart: unless-stopped - environment: - - SPOTIFY_TOKENER_ADDR=0.0.0.0:49152 - networks: - - vocard - ports: - - 49152:49152 - healthcheck: - test: nc -z -v localhost 49152 - interval: 10s - timeout: 5s - retries: 5 - - vocard-db: - container_name: vocard-db - image: mongo:8 - restart: unless-stopped - volumes: - - ./data/mongo/db:/data/db - - ./data/mongo/conf:/data/configdb - environment: - - MONGO_INITDB_ROOT_USERNAME=admin # For your MongoDB URL use "mongodb://admin:admin@vocard-db:27017" - - MONGO_INITDB_ROOT_PASSWORD=admin - expose: - - "27017" - networks: - - vocard - command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"] - healthcheck: - test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - # vocard-dashboard: - # container_name: vocard-dashboard - # image: ghcr.io/chocomeow/vocard-dashboard:latest - # restart: unless-stopped - # # If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line. - # # build: - # # context: ./dashboard - # # dockerfile: ./Dockerfile - # volumes: - # - ./dashboard/settings.json:/app/settings.json - # ports: - # - 8000:8000 - # networks: - # - vocard - - vocard: - container_name: vocard - restart: unless-stopped - # If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line. - image: ghcr.io/chocomeow/vocard:latest - # build: - # dockerfile: ./Dockerfile - volumes: - - ./settings.json:/app/settings.json - networks: - - vocard - depends_on: - lavalink: - condition: service_healthy - vocard-db: - condition: service_healthy - -networks: - vocard: - name: vocard diff --git a/lavalink/Dockerfile-lavalink b/lavalink/Dockerfile-lavalink deleted file mode 100644 index eaa9ca8..0000000 --- a/lavalink/Dockerfile-lavalink +++ /dev/null @@ -1,16 +0,0 @@ -FROM eclipse-temurin:24-jre-noble - -RUN groupadd -g 322 lavalink && \ - useradd -u 322 -g lavalink -m -d /opt/Lavalink lavalink - -WORKDIR /opt/Lavalink - -ADD --chown=lavalink:lavalink https://github.com/lavalink-devs/Lavalink/releases/latest/download/Lavalink.jar . - -RUN apt-get update && \ - apt-get install -y --no-install-recommends netcat-openbsd && \ - rm -rf /var/lib/apt/lists/* - -USER lavalink - -ENTRYPOINT ["java", "-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2", "-jar", "Lavalink.jar"] diff --git a/lavalink/application.yml b/lavalink/application.yml deleted file mode 100644 index 059f6dd..0000000 --- a/lavalink/application.yml +++ /dev/null @@ -1,197 +0,0 @@ -server: # REST and WS server - port: 2333 - address: 0.0.0.0 - http2: - enabled: true # Whether to enable HTTP/2 support -plugins: - youtube: - enabled: true # Whether this source can be used. - allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used. - allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded. - allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded. - # The clients to use for track loading. See below for a list of valid clients. - # Clients are queried in the order they are given (so the first client is queried first and so on...) - clients: - - MUSIC - - ANDROID_VR - - ANDROID_MUSIC - - WEB - - WEBEMBEDDED - - TVHTML5EMBEDDED - # The below section of the config allows setting specific options for each client, such as the requests they will handle. - # If an option, or client, is unspecified, then the default option value/client values will be used instead. - # If a client is configured, but is not registered above, the options for that client will be ignored. - # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT - # WARNING!: UNDERSTANDING WHAT IT DOES. MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. - - # Write the names of clients as they are specified under the heading "Available Clients". - clientOptions: - WEB: - # Example: Disabling a client's playback capabilities. - playback: false - videoLoading: false # Disables loading of videos for this client. A client may still be used for playback even if this is set to 'false'. - WEBEMBEDDED: - # Example: Configuring a client to exclusively be used for video loading and playback. - playlistLoading: false # Disables loading of playlists and mixes. - searching: false # Disables the ability to search for videos. - lavasrc: - providers: # Custom providers for track loading. This is the default - # - "dzisrc:%ISRC%" # Deezer ISRC provider - # - "dzsearch:%QUERY%" # Deezer search provider - - "ytsearch:\"%ISRC%\"" # Will be ignored if track does not have an ISRC. See https://en.wikipedia.org/wiki/International_Standard_Recording_Code - - "ytsearch:%QUERY%" # Will be used if track has no ISRC or no track could be found for the ISRC - # you can add multiple other fallback sources here - sources: - spotify: true # Enable Spotify source - applemusic: false # Enable Apple Music source - deezer: false # Enable Deezer source - yandexmusic: false # Enable Yandex Music source - flowerytts: false # Enable Flowery TTS source - youtube: false # Enable YouTube search source (https://github.com/topi314/LavaSearch) - vkmusic: false # Enable Vk Music source - lyrics-sources: - spotify: false # Enable Spotify lyrics source - deezer: false # Enable Deezer lyrics source - youtube: false # Enable YouTube lyrics source - yandexmusic: false # Enable Yandex Music lyrics source - vkmusic: false # Enable Vk Music lyrics source - spotify: - clientId: "" - clientSecret: "" - # spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api - countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - playlistLoadLimit: 6 # The number of pages at 100 tracks each - albumLoadLimit: 6 # The number of pages at 50 tracks each - resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow) - localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"` - preferAnonymousToken: true # Whether to use the anonymous token for resolving tracks, artists and albums. Spotify generated playlists are always resolved with the anonymous tokens since they do not work otherwise. This requires the customTokenEndpoint to be set. - customTokenEndpoint: "http://spotify-tokener:49152/api/token" # Optional custom endpoint for getting the anonymous token. If not set, spotify's default endpoint will be used which might not work. The response must match spotify's anonymous token response format. - applemusic: - countryCode: "US" # the country code you want to use for filtering the artists top tracks and language. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - mediaAPIToken: "your apple music api token" # apple music api token - # or specify an apple music key - keyID: "your key id" - teamID: "your team id" - musicKitKey: | - -----BEGIN PRIVATE KEY----- - your key - -----END PRIVATE KEY----- - playlistLoadLimit: 6 # The number of pages at 300 tracks each - albumLoadLimit: 6 # The number of pages at 300 tracks each - deezer: - masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else) - # arl: "your deezer arl" # the arl cookie used for accessing the deezer api this is optional but required for formats above MP3_128 - formats: [ "FLAC", "MP3_320", "MP3_256", "MP3_128", "MP3_64", "AAC_64" ] # the formats you want to use for the deezer tracks. "FLAC", "MP3_320", "MP3_256" & "AAC_64" are only available for premium users and require a valid arl - yandexmusic: - accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music - playlistLoadLimit: 1 # The number of pages at 100 tracks each - albumLoadLimit: 1 # The number of pages at 50 tracks each - artistLoadLimit: 1 # The number of pages at 10 tracks each - flowerytts: - voice: "default voice" # (case-sensitive) get default voice from here https://api.flowery.pw/v1/tts/voices - translate: false # whether to translate the text to the native language of voice - silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0. - speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.) - audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3 - youtube: - countryCode: "US" # the country code you want to use for searching lyrics via ISRC. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - vkmusic: - userToken: "your user token" # This token is needed for authorization in the api. Guide: https://github.com/topi314/LavaSrc#vk-music - playlistLoadLimit: 1 # The number of pages at 50 tracks each - artistLoadLimit: 1 # The number of pages at 10 tracks each - recommendationsLoadLimit: 10 # Number of tracks -lavalink: - plugins: - - dependency: "dev.lavalink.youtube:youtube-plugin:1.13.5" # Please check the latest version at https://github.com/lavalink-devs/youtube-source/releases - snapshot: false - - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.7.3" # Please check the latest version at https://github.com/topi314/LavaSrc/releases - snapshot: false -# - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin -# repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default -# snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository -# pluginsDir: "./plugins" # optional, defaults to "./plugins" -# defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository -# defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository - server: - password: "youshallnotpass" - sources: - # The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead. - youtube: false - bandcamp: true - soundcloud: true - twitch: true - vimeo: true - nico: true - http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address. - local: false - filters: # All filters are enabled by default - volume: true - equalizer: true - karaoke: true - timescale: true - tremolo: true - vibrato: true - distortion: true - rotation: true - channelMix: true - lowPass: true - nonAllocatingFrameBuffer: false # Setting to true reduces the number of allocations made by each player at the expense of frame rebuilding (e.g. non-instantaneous volume changes) - bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses. - frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered - opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. - resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU. - trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data. - useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready. - youtubePlaylistLoadLimit: 6 # Number of pages at 100 each - playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds - youtubeSearchEnabled: true - soundcloudSearchEnabled: true - gc-warnings: true - #ratelimit: - #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks - #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink - #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch - #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing - #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times - #youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without. - #email: "" # Email of Google account - #password: "" # Password of Google account - #httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked - #proxyHost: "localhost" # Hostname of the proxy, (ip or domain) - #proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy - #proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth - #proxyPassword: "" # Password for basic authentication - -metrics: - prometheus: - enabled: false - endpoint: /metrics - -sentry: - dsn: "" - environment: "" -# tags: -# some_key: some_value -# another_key: another_value - -logging: - file: - path: ./logs/ - - level: - root: INFO - lavalink: INFO - - request: - enabled: true - includeClientInfo: true - includeHeaders: false - includeQueryString: true - includePayload: true - maxPayloadLength: 10000 - - - logback: - rollingpolicy: - max-file-size: 1GB - max-history: 30 From 851871e123b092bc7a3a81fc8e51bd5644657a12 Mon Sep 17 00:00:00 2001 From: Choco Date: Fri, 19 Sep 2025 21:39:51 +0800 Subject: [PATCH 04/52] Update play and search command descriptions and translations Revised the docstrings for the play and search commands in basic.py for clarity. Updated corresponding translation strings in es-419.json, es-ES.json, and zh-TW.json to match the new descriptions. --- cogs/basic.py | 4 ++-- local_langs/es-419.json | 3 ++- local_langs/es-ES.json | 3 ++- local_langs/zh-TW.json | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 0a10f96..d09149d 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -127,7 +127,7 @@ class Basic(commands.Cog): @app_commands.autocomplete(query=play_autocomplete) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def play(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0") -> None: - "Loads your input and added it to the queue." + "Loads your input into the queue" player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) @@ -222,7 +222,7 @@ class Basic(commands.Cog): ]) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def search(self, ctx: commands.Context, *, query: str, platform: str = SearchType.YOUTUBE.name): - "Loads your input and added it to the queue." + "Loads your search query and shows the results." player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) diff --git a/local_langs/es-419.json b/local_langs/es-419.json index a047101..6cd73f2 100644 --- a/local_langs/es-419.json +++ b/local_langs/es-419.json @@ -4,7 +4,8 @@ "channel": "canal", "Provide a channel to connect.": "Proporciona un canal para conectar.", "play": "reproducir", - "Loads your input and added it to the queue.": "Carga tu entrada y la agrega a la cola.", + "Loads your input into the queue": "Carga tu entrada en la cola.", + "Searches your query and displays the results.": "Busca tu consulta y muestra los resultados.", "query": "consulta", "Input a query or a searchable link.": "Ingresa una consulta o un enlace de búsqueda.", "search": "buscar", diff --git a/local_langs/es-ES.json b/local_langs/es-ES.json index e8cfd59..8d38429 100644 --- a/local_langs/es-ES.json +++ b/local_langs/es-ES.json @@ -4,7 +4,8 @@ "channel": "canal", "Provide a channel to connect.": "Proporciona un canal para conectar.", "play": "reproducir", - "Loads your input and added it to the queue.": "Carga tu entrada y la añade a la cola.", + "Loads your input into the queue.": "Carga tu entrada en la cola.", + "Searches your query and displays the results.": "Busca tu consulta y muestra los resultados.", "query": "consulta", "Input a query or a searchable link.": "Introduce una consulta o un enlace de búsqueda.", "search": "buscar", diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json index eecde64..35fbdae 100644 --- a/local_langs/zh-TW.json +++ b/local_langs/zh-TW.json @@ -4,7 +4,8 @@ "channel": "頻道", "Provide a channel to connect.": "提供要連線的頻道。", "play": "播放", - "Loads your input and added it to the queue.": "載入您的輸入並將其加入排隊。", + "Loads your input into the queue.": "將您的輸入加載到隊列中。", + "Searches your query and displays the results.": "搜尋您的查詢並顯示結果。", "query": "查詢", "Input a query or a searchable link.": "輸入查詢或可搜尋的連結。", "search": "搜尋", From 1633f14b64665a7c0cc77dfdf11104876311f101 Mon Sep 17 00:00:00 2001 From: Choco Date: Fri, 19 Sep 2025 23:29:42 +0800 Subject: [PATCH 05/52] 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 c556f0fe7bd3b17cd134b27f02ab64e7f49479a7 Mon Sep 17 00:00:00 2001 From: TomStilson <92690997+TomStilson@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:58:48 -0400 Subject: [PATCH 06/52] Add Chinese simplified and traditional translations --- langs/ZHCN.json | 172 ++++++++++++++++++++++++++++++ langs/ZHTW.json | 172 ++++++++++++++++++++++++++++++ local_langs/zh-CN.json | 235 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 579 insertions(+) create mode 100644 langs/ZHCN.json create mode 100644 langs/ZHTW.json create mode 100644 local_langs/zh-CN.json diff --git a/langs/ZHCN.json b/langs/ZHCN.json new file mode 100644 index 0000000..4aae7f3 --- /dev/null +++ b/langs/ZHCN.json @@ -0,0 +1,172 @@ +{ + "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": "曲目列表", + "playlistViewPage": "第 {0}/{1} 页 | 总时长: {2}", + "inboxFull": "抱歉!{0} 的收件箱已满。", + "inboxNoMsg": "您的收件箱中没有消息。", + "invitationSent": "邀请已发送给 {0}。", + "notInChannel": "{0},您必须在 {1} 才能使用语音命令。如果您已在频道,请重新加入!", + "noTrackPlaying": "当前没有正在播放的歌曲", + "noTrackFound": "未找到对应的歌曲,请提供有效链接。", + "noLinkSupport": "搜索命令不支持链接!", + "voted": "您已投票!", + "missingPosPerm": "只有 DJ 或管理员才能更改位置。", + "missingModePerm": "只有 DJ 或管理员才能切换循环模式。", + "missingQueuePerm": "只有 DJ 或管理员才能从队列中移除曲目。", + "missingAutoPlayPerm": "只有 DJ 或管理员才能启用或禁用自动播放!", + "missingFunctionPerm": "只有 DJ 或管理员才能使用此功能。", + "timeFormatError": "时间格式错误。示例: 2:42 或 12:39:31", + "lyricsNotFound": "未找到歌词。输入 /lyrics <歌曲名> <作者> 查找歌词。", + "missingTrackInfo": "部分曲目信息缺失。", + "noVoiceChannel": "未找到语音频道!", + "playlistAddError": "不允许将流媒体视频添加到播放列表!", + "playlistAddError2": "添加曲目到播放列表时出现问题!", + "playlistLimited": "已达到上限!您最多只能在播放列表中添加 {0} 首歌。", + "playlistRepeated": "该曲目已在播放列表中!", + "playlistAdded": "❤️ 已将 **{0}** 添加到 {1} 的播放列表 [`{2}`]!", + "playerDropdown": "选择要跳转的歌曲 ...", + "playerFilter": "选择要应用的音效 ...", + "buttonBack": "上一首", + "buttonPause": "暂停", + "buttonResume": "继续", + "buttonSkip": "跳过", + "buttonLeave": "退出", + "buttonLoop": "循环", + "buttonVolumeUp": "音量+", + "buttonVolumeDown": "音量-", + "buttonVolumeMute": "静音", + "buttonVolumeUnmute": "取消静音", + "buttonAutoPlay": "自动播放", + "buttonShuffle": "随机播放", + "buttonForward": "快进", + "buttonRewind": "快退", + "buttonLyrics": "歌词", + "nowplayingDesc": "**正在播放:**\n```{0}```", + "nowplayingField": "即将播放:", + "nowplayingLink": "在 {0} 收听", + "connect": "已连接到 {0}", + "live": "直播", + "playlistLoad": " 🎶 已将播放列表 **{0}** 的 `{1}` 首歌添加到队列中。", + "trackLoad": "已添加 **[{0}](<{1}>)** - **{2}** (`{3}`) 并开始播放。\n", + "trackLoad_pos": "已添加 **[{0}](<{1}>)** - **{2}** (`{3}`) 到队列位置 **{4}**\n", + "searchTitle": "搜索关键词: {0}", + "searchDesc": "➥ 平台: {0} **{1}**\n➥ 结果数: **{2}**\n\n{3}", + "searchWait": "请选择要添加到队列的歌曲。", + "searchTimeout": "搜索超时,请稍后重试。", + "searchSuccess": "歌曲已添加到队列。", + "queueTitle": "即将播放队列:", + "historyTitle": "播放历史:", + "viewTitle": "音乐队列", + "viewDesc": "**正在播放: [点我]({0}) ⮯**\n{1}", + "viewFooter": "第 {0}/{1} 页 | 总时长: {2}", + "pauseError": "播放器已暂停。", + "pauseVote": "{0} 投票暂停了歌曲。[{1}/{2}]", + "paused": "`{0}` 已暂停播放器。", + "resumeError": "播放器未暂停。", + "resumeVote": "{0} 投票继续播放。[{1}/{2}]", + "resumed": "`{0}` 已继续播放。", + "shuffleError": "请在队列中添加更多歌曲后再随机播放。", + "shuffleVote": "{0} 投票随机播放队列。[{1}/{2}]", + "shuffled": "队列已随机排序。", + "skipError": "没有歌曲可跳过。", + "skipVote": "{0} 投票跳过歌曲。[{1}/{2}]", + "skipped": "`{0}` 已跳过歌曲。", + "backVote": "{0} 投票播放上一首歌。[{1}/{2}]", + "backed": "`{0}` 已切换到上一首歌曲。", + "leaveVote": "{0} 投票停止播放。[{1}/{2}]", + "left": "`{0}` 已停止播放器。", + "seek": "已将播放器定位到 **{0}**", + "repeat": "循环模式已设置为 `{0}`", + "cleared": "已清空 `{0}` 中的所有曲目", + "removed": "已从队列中移除 `{0}` 首歌。", + "forward": "已快进到 **{0}**", + "rewind": "已快退到 **{0}**", + "replay": "正在重播当前歌曲。", + "swapped": "`{0}` 和 `{1}` 已交换位置", + "moved": "已将 `{0}` 移动到 `{1}`", + "autoplay": "自动播放模式现已 **{0}**", + "notdj": "您不是 DJ。当前 DJ 是 {0}。", + "djToMe": "您不能将 DJ 身份转移给自己或机器人。", + "djNotInChannel": "`{0}` 不在语音频道中。", + "djswap": "您已将 DJ 身份转移给 `{0}`。", + "voicelinkQueueFull": "抱歉,队列已达到最大 `{0}` 首歌!", + "voicelinkOutofList": "请输入有效的曲目序号!", + "voicelinkDuplicateTrack": "抱歉,该曲目已在队列中。", + "decodeError": "解码文件时出错!", + "invalidStartTime": "无效的开始时间,必须在 `00:00` 和 `{0}` 之间", + "invalidEndTime": "无效的结束时间,必须在 `00:00` 和 `{0}` 之间", + "invalidTimeOrder": "结束时间不能小于或等于开始时间", + "setStageAnnounceTemplate": "完成!从现在起,您所在的语音状态将按照模板命名。几秒后即可看到更新。", + "createSongRequestChannel": "已创建点歌频道 ({0})!您可以在该频道输入歌曲名称或链接来点歌,无需使用机器人前缀。" +} \ No newline at end of file diff --git a/langs/ZHTW.json b/langs/ZHTW.json new file mode 100644 index 0000000..7c5de56 --- /dev/null +++ b/langs/ZHTW.json @@ -0,0 +1,172 @@ +{ + "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": "曲目列表", + "playlistViewPage": "第 {0}/{1} 頁 | 總時長: {2}", + "inboxFull": "抱歉!{0} 的收件匣已滿。", + "inboxNoMsg": "您的收件匣中沒有訊息。", + "invitationSent": "邀請已發送給 {0}。", + "notInChannel": "{0},您必須在 {1} 才能使用語音指令。如果您已在頻道,請重新加入!", + "noTrackPlaying": "目前沒有正在播放的歌曲", + "noTrackFound": "未找到對應的歌曲,請提供有效連結。", + "noLinkSupport": "搜尋指令不支援連結!", + "voted": "您已投票!", + "missingPosPerm": "只有 DJ 或管理員能更改位置。", + "missingModePerm": "只有 DJ 或管理員能切換循環模式。", + "missingQueuePerm": "只有 DJ 或管理員能從隊列中移除曲目。", + "missingAutoPlayPerm": "只有 DJ 或管理員能啟用或停用自動播放!", + "missingFunctionPerm": "只有 DJ 或管理員能使用此功能。", + "timeFormatError": "時間格式錯誤。範例: 2:42 或 12:39:31", + "lyricsNotFound": "未找到歌詞。輸入 /lyrics <歌曲名稱> <作者> 查找歌詞。", + "missingTrackInfo": "部分曲目資訊缺失。", + "noVoiceChannel": "未找到語音頻道!", + "playlistAddError": "不允許將串流影片加入播放清單!", + "playlistAddError2": "新增曲目到播放清單時發生問題!", + "playlistLimited": "已達到上限!播放清單最多只能新增 {0} 首歌曲。", + "playlistRepeated": "此曲目已在播放清單中!", + "playlistAdded": "❤️ 已將 **{0}** 加入 {1} 的播放清單 [`{2}`]!", + "playerDropdown": "選擇要跳轉的歌曲 ...", + "playerFilter": "選擇要套用的音效 ...", + "buttonBack": "上一首", + "buttonPause": "暫停", + "buttonResume": "繼續", + "buttonSkip": "跳過", + "buttonLeave": "離開", + "buttonLoop": "循環", + "buttonVolumeUp": "音量+", + "buttonVolumeDown": "音量-", + "buttonVolumeMute": "靜音", + "buttonVolumeUnmute": "取消靜音", + "buttonAutoPlay": "自動播放", + "buttonShuffle": "隨機播放", + "buttonForward": "快轉", + "buttonRewind": "倒轉", + "buttonLyrics": "歌詞", + "nowplayingDesc": "**正在播放:**\n```{0}```", + "nowplayingField": "接下來:", + "nowplayingLink": "在 {0} 收聽", + "connect": "已連線至 {0}", + "live": "直播", + "playlistLoad": " 🎶 已將播放清單 **{0}** 的 `{1}` 首歌曲加入隊列。", + "trackLoad": "已新增 **[{0}](<{1}>)** - **{2}** (`{3}`) 並開始播放。\n", + "trackLoad_pos": "已新增 **[{0}](<{1}>)** - **{2}** (`{3}`) 到隊列位置 **{4}**\n", + "searchTitle": "搜尋關鍵字: {0}", + "searchDesc": "➥ 平台: {0} **{1}**\n➥ 結果數: **{2}**\n\n{3}", + "searchWait": "請選擇要加入隊列的歌曲。", + "searchTimeout": "搜尋逾時,請稍後再試。", + "searchSuccess": "歌曲已加入隊列。", + "queueTitle": "即將播放隊列:", + "historyTitle": "播放歷史:", + "viewTitle": "音樂隊列", + "viewDesc": "**正在播放: [點我]({0}) ⮯**\n{1}", + "viewFooter": "第 {0}/{1} 頁 | 總時長: {2}", + "pauseError": "播放器已暫停。", + "pauseVote": "{0} 投票暫停了歌曲。[{1}/{2}]", + "paused": "`{0}` 已暫停播放器。", + "resumeError": "播放器未暫停。", + "resumeVote": "{0} 投票繼續播放。[{1}/{2}]", + "resumed": "`{0}` 已繼續播放。", + "shuffleError": "請在隊列中新增更多歌曲後再隨機播放。", + "shuffleVote": "{0} 投票隨機播放隊列。[{1}/{2}]", + "shuffled": "隊列已隨機排序。", + "skipError": "沒有歌曲可跳過。", + "skipVote": "{0} 投票跳過歌曲。[{1}/{2}]", + "skipped": "`{0}` 已跳過歌曲。", + "backVote": "{0} 投票播放上一首歌。[{1}/{2}]", + "backed": "`{0}` 已切換到上一首歌曲。", + "leaveVote": "{0} 投票停止播放。[{1}/{2}]", + "left": "`{0}` 已停止播放器。", + "seek": "已將播放器定位至 **{0}**", + "repeat": "循環模式已設定為 `{0}`", + "cleared": "已清空 `{0}` 中的所有曲目", + "removed": "已從隊列中移除 `{0}` 首歌曲。", + "forward": "已快轉至 **{0}**", + "rewind": "已倒轉至 **{0}**", + "replay": "正在重播目前的歌曲。", + "swapped": "`{0}` 與 `{1}` 已交換位置", + "moved": "已將 `{0}` 移動到 `{1}`", + "autoplay": "自動播放模式現在是 **{0}**", + "notdj": "您不是 DJ。目前 DJ 是 {0}。", + "djToMe": "您不能將 DJ 身分轉移給自己或機器人。", + "djNotInChannel": "`{0}` 不在語音頻道中。", + "djswap": "您已將 DJ 身分轉移給 `{0}`。", + "voicelinkQueueFull": "抱歉,隊列已達最大 `{0}` 首歌曲!", + "voicelinkOutofList": "請輸入有效的曲目序號!", + "voicelinkDuplicateTrack": "抱歉,此曲目已在隊列中。", + "decodeError": "解碼檔案時發生錯誤!", + "invalidStartTime": "無效的開始時間,必須在 `00:00` 和 `{0}` 之間", + "invalidEndTime": "無效的結束時間,必須在 `00:00` 和 `{0}` 之間", + "invalidTimeOrder": "結束時間不能小於或等於開始時間", + "setStageAnnounceTemplate": "完成!從現在起,您所在的語音狀態將依照模板命名。幾秒後即可看到更新。", + "createSongRequestChannel": "已建立點歌頻道 ({0})!您可以在該頻道輸入歌曲名稱或連結來點歌,無需使用機器人前綴。" +} \ No newline at end of file diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json new file mode 100644 index 0000000..274bf9d --- /dev/null +++ b/local_langs/zh-CN.json @@ -0,0 +1,235 @@ +{ + "connect": "连接", + "Connect to a voice channel.": "连接到语音频道。", + "channel": "频道", + "Provide a channel to connect.": "提供要连接的频道。", + "play": "播放", + "Loads your input and added it to the queue.": "加载您的输入并将其加入队列。", + "query": "查询", + "Input a query or a searchable link.": "输入查询或可搜索的链接。", + "search": "搜索", + "Input the name of the song.": "输入歌曲名称。", + "platform": "平台", + "Select the platform you want to search.": "选择您要搜索的平台。", + "Youtube": "YouTube", + "Youtube Music": "YouTube 音乐", + "Spotify": "Spotify", + "SoundCloud": "SoundCloud", + "Apple Music": "Apple 音乐", + "playtop": "顶部播放", + "Adds a song with the given url or query on the top of the queue.": "将指定的 URL 或查询加入队列顶部。", + "forceplay": "强制播放", + "Enforce playback using the given URL or query.": "使用指定的 URL 或查询强制播放。", + "pause": "暂停", + "Pause the music.": "暂停音乐。", + "resume": "继续", + "Resume the music.": "继续音乐。", + "skip": "跳过", + "Skips to the next song or skips to the specified song.": "跳到下一首歌曲或跳到指定歌曲。", + "index": "索引", + "Enter a index that you want to skip to.": "输入您要跳转到的索引。", + "back": "返回", + "Skips back to the previous song or skips to the specified previous song.": "返回上一首歌曲或返回到指定的上一首歌曲。", + "Enter a index that you want to skip back to.": "输入您要返回到的索引。", + "seek": "定位", + "Change the player position.": "更改播放器位置。", + "position": "位置", + "Input position. Exmaple: 1:20.": "输入位置。例如:1:20。", + "queue": "队列", + "Display the players queue songs in your queue.": "显示队列中的歌曲。", + "export": "导出", + "Exports the entire queue to a text file": "将整个队列导出为文本文件。", + "import": "导入", + "Imports the text file and adds the track to the current queue.": "导入文本文件并将歌曲加入当前队列。", + "attachment": "附件", + "history": "历史", + "Display the players queue songs in your history queue.": "显示历史队列中的歌曲。", + "leave": "离开", + "Disconnects the bot from your voice channel and chears the queue.": "断开机器人与语音频道的连接并清空队列。", + "nowplaying": "正在播放", + "Shows details of the current track.": "显示当前歌曲的详细信息。", + "loop": "循环", + "Changes Loop mode.": "更改循环模式。", + "mode": "模式", + "Choose a looping mode.": "选择循环模式。", + "Off": "关闭", + "Track": "单曲", + "Queue": "队列", + "clear": "清空", + "Remove all the tracks in your queue or history queue.": "清空队列或历史队列中的所有歌曲。", + "Choose a queue that you want to clear.": "选择要清空的队列。", + "History": "历史", + "remove": "移除", + "Removes specified track or a range of tracks from the queue.": "删除队列中的指定歌曲或一系列歌曲。", + "position1": "位置1", + "Input a position from the queue to be removed.": "输入要删除的歌曲位置。", + "position2": "位置2", + "Set the range of the queue to be removed.": "设置要删除的队列范围。", + "member": "成员", + "Remove tracks requested by a specific member.": "删除指定成员点的歌曲。", + "forward": "快进", + "Forwards by a certain amount of time in the current track. The default is 10 seconds.": "在当前歌曲中快进指定时间,默认 10 秒。", + "Input an amount that you to forward to. Exmaple: 1:20": "输入要快进的时间。例如:1:20", + "rewind": "后退", + "Rewind by a certain amount of time in the current track. The default is 10 seconds.": "在当前歌曲中后退指定时间,默认 10 秒。", + "Input an amount that you to rewind to. Exmaple: 1:20": "输入要后退的时间。例如:1:20", + "replay": "重播", + "Reset the progress of the current song.": "重置当前歌曲进度。", + "shuffle": "随机播放", + "Randomizes the tracks in the queue.": "随机打乱队列中的歌曲。", + "swap": "交换", + "Swaps the specified song to the specified song.": "交换队列中两首歌曲的位置。", + "The track to swap. Example: 2": "要交换的歌曲。例如:2", + "The track to swap with position1. Exmaple: 1": "与位置1交换的歌曲。例如:1", + "move": "移动", + "Moves the specified song to the specified position.": "将指定歌曲移动到指定位置。", + "target": "目标", + "The track to move. Example: 2": "要移动的歌曲。例如:2", + "to": "到", + "The new position to move the track to. Exmaple: 1": "要移动到的新位置。例如:1", + "lyrics": "歌词", + "Displays lyrics for the playing track.": "显示正在播放歌曲的歌词。", + "title": "标题", + "Searches for your query and displays the reutned lyrics.": "搜索您的查询并显示返回的歌词。", + "artist": "歌手", + "swapdj": "转移DJ", + "Transfer dj to another.": "将 DJ 转移给其他人。", + "Choose a member to transfer the dj role.": "选择要转移 DJ 身份的成员。", + "autoplay": "自动播放", + "Toggles autoplay mode, it will automatically queue the best songs to play.": "切换自动播放模式,将自动推荐歌曲加入队列。", + "help": "帮助", + "Lists all the commands in Vocard.": "列出 Vocard 的所有命令。", + "category": "分类", + "Test if the bot is alive, and see the delay between your commands and my response.": "测试机器人是否运行,并查看您的命令与我的响应之间的延迟。", + "playlist": "播放列表", + "Play all songs from your favorite playlist.": "播放您最喜欢的播放列表中的所有歌曲。", + "name": "名称", + "Input the name of your custom playlist": "输入您自定义播放列表的名称", + "value": "值", + "Play the specific track from your custom playlist.": "播放您自定义播放列表中的指定歌曲。", + "view": "查看", + "List all your playlist and all songs in your favourite playlist.": "列出您所有的播放列表和最喜欢的播放列表中的所有歌曲。", + "create": "创建", + "Create your custom playlist.": "创建自定义播放列表。", + "Give a name to your playlist.": "为您的播放列表命名。", + "link": "链接", + "Provide a playlist link if you are creating link playlist.": "如果您正在创建链接播放列表,请提供播放列表链接。", + "delete": "删除", + "Delete your custom playlist.": "删除自定义播放列表。", + "The name of the playlist.": "播放列表名称。", + "share": "分享", + "Share your custom playlist with your friends.": "与好友分享自定义播放列表。", + "The user id of your friend.": "您好友的用户 ID。", + "The name of the playlist that you want to share.": "您想分享的播放列表名称。", + "rename": "重命名", + "Rename your custom playlist.": "重命名自定义播放列表。", + "The name of your playlist.": "您的播放列表名称。", + "newname": "新名称", + "The new name of your playlist.": "您的播放列表新名称。", + "inbox": "收件箱", + "Show your playlist invitation.": "显示播放列表邀请。", + "add": "添加", + "Add tracks in to your custom playlist.": "将歌曲添加到自定义播放列表。", + "Remove song from your favorite playlist.": "从最喜欢的播放列表中删除歌曲。", + "Input a position from the playlist to be removed.": "输入要删除的歌曲位置。", + "Remove all songs from your favorite playlist.": "删除最喜欢的播放列表中的所有歌曲。", + "Exports the entire playlist to a text file": "将整个播放列表导出为文本文件。", + "settings": "设置", + "prefix": "前缀", + "Change the default prefix for message commands.": "更改消息命令的默认前缀。", + "language": "语言", + "You can choose your preferred language, the bot message will change to the language you set.": "您可以选择偏好的语言,机器人消息将切换为您设置的语言。", + "Set a DJ role or remove DJ role.": "设置或移除 DJ 身份。", + "role": "角色", + "Change to another type of queue mode.": "更改为另一种队列模式。", + "FairQueue": "公平队列", + "Toggles 24/7 mode, which disables automatic inactivity-based disconnects.": "切换 24/7 模式,禁用自动闲置断开。", + "bypassvote": "跳过投票", + "Toggles voting system.": "切换投票系统。", + "Show all the bot settings in your server.": "显示机器人在服务器中的所有设置。", + "volume": "音量", + "Set the player's volume.": "设置播放器音量。", + "Input a integer.": "输入一个整数。", + "toggleController": "切换控制器", + "Toggles the music controller.": "切换音乐控制器。", + "duplicatetrack": "重复歌曲", + "Toggle Vocard to prevent duplicate songs from queuing.": "切换以防止重复歌曲加入队列。", + "customcontroller": "自定义控制器", + "Customizes music controller embeds.": "自定义音乐控制器嵌入。", + "controllermsg": "控制器消息", + "silentmsg": "静默消息", + "Toggles to send a message when clicking the button in the music controller.": "切换在点击音乐控制器按钮时是否发送消息。", + "Toggle silent messaging to send discreet messages without alerting recipients.": "切换静默消息以私密发送,不提醒接收者。", + "debug": "调试", + "speed": "速度", + "Sets the player's playback speed": "设置播放器播放速度。", + "The value to set the speed to. Default is `1.0`": "设置速度值,默认 `1.0`。", + "karaoke": "卡拉OK", + "Uses equalization to eliminate part of a band, usually targeting vocals.": "使用均衡器消除部分频带,通常针对人声。", + "level": "级别", + "The level of the karaoke. Default is `1.0`": "卡拉OK级别,默认 `1.0`。", + "monolevel": "单声道级别", + "The monolevel of the karaoke. Default is `1.0`": "卡拉OK单声道级别,默认 `1.0`。", + "filterband": "滤波频带", + "The filter band of the karaoke. Default is `220.0`": "卡拉OK滤波频带,默认 `220.0`。", + "filterwidth": "滤波宽度", + "The filter band of the karaoke. Default is `100.0`": "卡拉OK滤波宽度,默认 `100.0`。", + "tremolo": "颤音", + "Uses amplification to create a shuddering effect, where the volume quickly oscillates.": "通过放大制造颤音效果,音量快速振荡。", + "frequency": "频率", + "The frequency of the tremolo. Default is `2.0`": "颤音频率,默认 `2.0`。", + "depth": "深度", + "The depth of the tremolo. Default is `0.5`": "颤音深度,默认 `0.5`。", + "vibrato": "颤音高低", + "Similar to tremolo. While tremolo oscillates the volume, vibrato oscillates the pitch.": "与颤音相似。颤音影响音量,颤音高低影响音高。", + "The frequency of the vibrato. Default is `2.0`": "颤音高低的频率,默认 `2.0`。", + "The Depth of the vibrato. Default is `0.5`": "颤音高低的深度,默认 `0.5`。", + "rotation": "旋转", + "Rotates the sound around the stereo channels/user headphones aka Audio Panning.": "让声音在立体声/耳机中旋转,也叫声像。", + "hertz": "赫兹", + "The hertz of the rotation. Default is `0.2`": "旋转赫兹,默认 `0.2`。", + "distortion": "失真", + "Distortion effect. It can generate some pretty unique audio effects.": "失真效果,可以产生独特的音效。", + "lowpass": "低通", + "Filter which supresses higher frequencies and allows lower frequencies to pass.": "低通滤波器,抑制高频,让低频通过。", + "smoothing": "平滑", + "The level of the lowPass. Default is `20.0`": "低通平滑级别,默认 `20.0`。", + "channelmix": "声道混合", + "Filter which manually adjusts the panning of the audio.": "手动调整音频声像的滤波器。", + "left_to_left": "左到左", + "Sounds from left to left. Default is `1.0`": "左声道到左声道,默认 `1.0`。", + "right_to_right": "右到右", + "Sounds from right to right. Default is `1.0`": "右声道到右声道,默认 `1.0`。", + "left_to_right": "左到右", + "Sounds from left to right. Default is `0.0`": "左声道到右声道,默认 `0.0`。", + "right_to_left": "右到左", + "Sounds from right to left. Default is `0.0`": "右声道到左声道,默认 `0.0`。", + "nightcore": "夜核", + "Add nightcore filter into your player.": "为播放器添加夜核滤波器。", + "Add 8D filter into your player.": "为播放器添加 8D 滤波器。", + "vaporwave": "蒸汽波", + "Add vaporwave filter into your player.": "为播放器添加蒸汽波滤波器。", + "cleareffect": "清除效果", + "Clear all or specific sound effects.": "清除所有或指定音效。", + "effect": "效果", + "Remove a specific sound effects.": "删除指定音效。", + "start": "开始", + "end": "结束", + "Specify a time you would like to start, e.g. 1:00": "指定开始时间,例如:1:00。", + "Specify a time you would like to end, e.g. 4:00": "指定结束时间,例如:4:00。", + "list": "列表", + "Customize the channel topic template": "自定义频道主题模板", + "template": "模板", + "setupchannel": "设置频道", + "Sets up a dedicated channel for song requests in your server.": "在服务器中设置专用点歌频道。", + "Provide a request channel. If not, a text channel will be generated.": "提供点歌频道。如果没有,将生成文本频道。", + "ping": "ping", + "…": "...", + "8d": "8d", + "dj": "dj", + "247": "247", + "stageannounce": "舞台公告", + "Soundcloud": "SoundCloud", + "Fairqueue": "公平队列", + "togglecontroller": "切换控制器" +} From eac92b9d731f637c9cbf6a3fddc4ed47b8217a43 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:24:24 +0800 Subject: [PATCH 07/52] Update zh-CN.json --- local_langs/zh-CN.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json index 274bf9d..9394155 100644 --- a/local_langs/zh-CN.json +++ b/local_langs/zh-CN.json @@ -4,7 +4,8 @@ "channel": "频道", "Provide a channel to connect.": "提供要连接的频道。", "play": "播放", - "Loads your input and added it to the queue.": "加载您的输入并将其加入队列。", + "Loads your input into the queue.": "将您的输入加载到队列中。", + "Searches your query and displays the results.": "搜索您的查询并显示结果。", "query": "查询", "Input a query or a searchable link.": "输入查询或可搜索的链接。", "search": "搜索", @@ -232,4 +233,4 @@ "Soundcloud": "SoundCloud", "Fairqueue": "公平队列", "togglecontroller": "切换控制器" -} +} \ No newline at end of file From 355d0e118622ba0faa5c84c694737c41e7b71e67 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:47:44 +0800 Subject: [PATCH 08/52] Refactor language retrieval functions Removed get_lang_non_async and replaced its usage with a new internal _get_lang function. Updated get_lang to use _get_lang for consistency. Updated Player class to use _get_lang directly, improving code clarity and reducing duplication. --- function.py | 30 ++++++++++++++---------------- voicelink/player.py | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/function.py b/function.py index b8a1b5a..8a00327 100644 --- a/function.py +++ b/function.py @@ -159,15 +159,6 @@ def check_roles() -> tuple[str, int, int]: def truncate_string(text: str, length: int = 40) -> str: return text[:length - 3] + "..." if len(text) > length else text -def get_lang_non_async(guild_id: int, *keys) -> Union[list[str], str]: - settings = SETTINGS_BUFFER.get(guild_id, {}) - lang = settings.get("lang", "EN") - if lang in LANGS and not LANGS[lang]: - LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) - - if len(keys) == 1: - return LANGS.get(lang, {}).get(keys[0], "Not found!") - return [LANGS.get(lang, {}).get(key, "Not found!") for key in keys] def format_bytes(bytes: int, unit: bool = False): if bytes <= 1_000_000_000: @@ -175,16 +166,23 @@ def format_bytes(bytes: int, unit: bool = False): else: return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "") - -async def get_lang(guild_id:int, *keys) -> Optional[Union[list[str], str]]: - settings = await get_settings(guild_id) - lang = settings.get("lang", "EN") - if lang in LANGS and not LANGS[lang]: + +def _get_lang(lang: str, *keys) -> Optional[Union[list[str], str]]: + if lang not in LANGS: + return "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 LANGS.get(lang, {}).get(keys[0]) - return [LANGS.get(lang, {}).get(key) for key in keys] + 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], diff --git a/voicelink/player.py b/voicelink/player.py index 752162b..5a07c80 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -258,7 +258,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_non_async(self.guild.id, *keys) + return func._get_lang(self.settings.get("lang"), *keys) def required(self, leave=False): """ From 2c98dfd76335e33ce5ad37a6703ad419d9ea3d37 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:58:15 +0800 Subject: [PATCH 09/52] Update command docstrings and localization keys Improved docstrings for 'play' and 'search' commands in basic.py for clarity. Updated localization keys and translations in es-419.json, es-ES.json, and zh-CN.json to fix punctuation, naming consistency, and minor translation errors. --- cogs/basic.py | 4 ++-- local_langs/es-419.json | 4 ++-- local_langs/es-ES.json | 2 +- local_langs/zh-CN.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index d09149d..af1fea9 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -127,7 +127,7 @@ class Basic(commands.Cog): @app_commands.autocomplete(query=play_autocomplete) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def play(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0") -> None: - "Loads your input into the queue" + "Loads your input into the queue." player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) @@ -222,7 +222,7 @@ class Basic(commands.Cog): ]) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def search(self, ctx: commands.Context, *, query: str, platform: str = SearchType.YOUTUBE.name): - "Loads your search query and shows the results." + "Searches your query and displays the results." player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) diff --git a/local_langs/es-419.json b/local_langs/es-419.json index 6cd73f2..6aed14d 100644 --- a/local_langs/es-419.json +++ b/local_langs/es-419.json @@ -4,7 +4,7 @@ "channel": "canal", "Provide a channel to connect.": "Proporciona un canal para conectar.", "play": "reproducir", - "Loads your input into the queue": "Carga tu entrada en la cola.", + "Loads your input into the queue.": "Carga tu entrada en la cola.", "Searches your query and displays the results.": "Busca tu consulta y muestra los resultados.", "query": "consulta", "Input a query or a searchable link.": "Ingresa una consulta o un enlace de búsqueda.", @@ -93,7 +93,7 @@ "title": "titulo", "Searches for your query and displays the reutned lyrics.": "Busca tu consulta y muestra la letra encontrada.", "artist": "artista", - "swapdj": "cambiar_dj", + "swapdj": "cambiardj", "Transfer dj to another.": "Transfiere el DJ a otro.", "Choose a member to transfer the dj role.": "Elige un miembro para transferir el rol de DJ.", "autoplay": "reproduccion_auto", diff --git a/local_langs/es-ES.json b/local_langs/es-ES.json index 8d38429..4580dba 100644 --- a/local_langs/es-ES.json +++ b/local_langs/es-ES.json @@ -93,7 +93,7 @@ "title": "titulo", "Searches for your query and displays the reutned lyrics.": "Busca tu consulta y muestra la letra encontrada.", "artist": "artista", - "swapdj": "cambiar_dj", + "swapdj": "cambiardj", "Transfer dj to another.": "Transfiere el DJ a otro.", "Choose a member to transfer the dj role.": "Elige un miembro para transferir el rol de DJ.", "autoplay": "reproduccion_auto", diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json index 9394155..fabe021 100644 --- a/local_langs/zh-CN.json +++ b/local_langs/zh-CN.json @@ -93,7 +93,7 @@ "title": "标题", "Searches for your query and displays the reutned lyrics.": "搜索您的查询并显示返回的歌词。", "artist": "歌手", - "swapdj": "转移DJ", + "swapdj": "转移dj", "Transfer dj to another.": "将 DJ 转移给其他人。", "Choose a member to transfer the dj role.": "选择要转移 DJ 身份的成员。", "autoplay": "自动播放", @@ -165,7 +165,7 @@ "speed": "速度", "Sets the player's playback speed": "设置播放器播放速度。", "The value to set the speed to. Default is `1.0`": "设置速度值,默认 `1.0`。", - "karaoke": "卡拉OK", + "karaoke": "卡拉ok", "Uses equalization to eliminate part of a band, usually targeting vocals.": "使用均衡器消除部分频带,通常针对人声。", "level": "级别", "The level of the karaoke. Default is `1.0`": "卡拉OK级别,默认 `1.0`。", From 44b4ba68d12a78f69b789892874bd5ac0df71676 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 25 Sep 2025 23:13:07 +0800 Subject: [PATCH 10/52] Fix language fallback logic in _get_lang function Previously, _get_lang returned 'EN' if the language was not found, which could cause issues. Now, it sets lang to 'EN' and continues processing, ensuring consistent behavior. --- function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/function.py b/function.py index 8a00327..7f4444c 100644 --- a/function.py +++ b/function.py @@ -169,7 +169,7 @@ def format_bytes(bytes: int, unit: bool = False): def _get_lang(lang: str, *keys) -> Optional[Union[list[str], str]]: if lang not in LANGS: - return "EN" + lang = "EN" if not LANGS[lang]: LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) From b0bf23ae2af1b4427be9d10ed363d1630a7264b1 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:26:02 +0800 Subject: [PATCH 11/52] Simplify bot prefix retrieval logic Removed special handling for bot owner and access users in get_prefix. The function now always returns the configured prefix or the default. --- main.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/main.py b/main.py index d8111cf..ed9a483 100644 --- a/main.py +++ b/main.py @@ -207,13 +207,7 @@ class CommandCheck(discord.app_commands.CommandTree): async def get_prefix(bot: commands.Bot, message: discord.Message) -> str: settings = await func.get_settings(message.guild.id) - prefix = settings.get("prefix", func.settings.bot_prefix) - - # Allow owner to use the bot without a prefix - if prefix and not message.content.startswith(prefix) and (await bot.is_owner(message.author) or message.author.id in func.settings.bot_access_user): - return "" - - return prefix + return settings.get("prefix", func.settings.bot_prefix) # Loading settings and logger func.settings = Settings(func.open_json("settings.json")) 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 12/52] 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 13/52] 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 14/52] 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 15/52] 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 16/52] 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 17/52] 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 18/52] 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 19/52] 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": "未找到語言包!請選擇一個現有的語言包。", From 81a8603e1da24000fb4d2a6365ed8994fbcc8b3c Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:28:50 +0800 Subject: [PATCH 20/52] Added default language in language handler --- cogs/settings.py | 2 +- voicelink/language.py | 8 ++++++-- voicelink/views/pagination.py | 2 +- voicelink/views/playlist.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index 2be80b5..46939fc 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -142,7 +142,7 @@ class Settings(commands.Cog, name="settings"): dj_role = ctx.guild.get_role(settings.get('dj', 0)) embed.add_field(name=texts[1], value=texts[2].format( settings.get('prefix', voicelink.Config().bot_prefix) or 'None', - settings.get('lang', 'EN'), + settings.get('lang', LangHandler._default_lang), settings.get('controller', True), dj_role.name if dj_role else 'None', settings.get('common.status.disabled_vote', False), diff --git a/voicelink/language.py b/voicelink/language.py index deb8029..5115ebe 100644 --- a/voicelink/language.py +++ b/voicelink/language.py @@ -40,6 +40,7 @@ class LangHandler: _langs: dict[str, dict[str, str]] = {} _local_langs: dict[str, dict[str, str]] = {} + _default_lang: str = "EN" @classmethod def init( @@ -118,9 +119,12 @@ class LangHandler: Returns: str | list[str] | None: The requested string(s), or "Not found!" if missing. """ + if not lang: + lang = cls._default_lang + lang = lang.upper() if lang not in cls._langs: - lang = "EN" + lang = cls._default_lang lang_dict = cls._langs.get(lang, {}) if len(keys) == 1: @@ -144,7 +148,7 @@ class LangHandler: str | list[str] | None: The requested string(s). """ settings = await MongoDBHandler.get_settings(guild_id) - lang = settings.get("lang", "EN") + lang = settings.get("lang", LangHandler._default_lang) return cls._get_lang(lang, *keys) @classmethod diff --git a/voicelink/views/pagination.py b/voicelink/views/pagination.py index 16742b2..3d131fd 100644 --- a/voicelink/views/pagination.py +++ b/voicelink/views/pagination.py @@ -35,7 +35,7 @@ class PaginationView(discord.ui.View): self.pagination: Pagination = pagination self.author: discord.Member = author - self.lang: str = MongoDBHandler.get_cached_settings(author.guild.id).get("lang", "EN") + self.lang: str = MongoDBHandler.get_cached_settings(author.guild.id).get("lang") self.update_view() def update_view(self, extra_states: Optional[Dict[str, Dict[str, str]]] = None) -> None: diff --git a/voicelink/views/playlist.py b/voicelink/views/playlist.py index a4230e4..068fc8f 100644 --- a/voicelink/views/playlist.py +++ b/voicelink/views/playlist.py @@ -189,7 +189,7 @@ 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") + self.lang: str = MongoDBHandler.get_cached_settings(ctx.guild.id).get("lang") views = {"home": self} views.update({result["id"]: PlaylistView(self, result) for result in results}) From 50298d27096e5e2fb56ff1e2cd6c41018b87e90c Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:55:29 +0800 Subject: [PATCH 21/52] Resolve MongoDB handler not updating cache --- voicelink/mongodb.py | 12 +++++++----- voicelink/views/controller.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/voicelink/mongodb.py b/voicelink/mongodb.py index 8bb8eb9..5fb8346 100644 --- a/voicelink/mongodb.py +++ b/voicelink/mongodb.py @@ -293,7 +293,8 @@ class MongoDBHandler: cls, guild_id: int, *, - force_refresh: bool = False + deep_copy: bool = True, + force_refresh: bool = False, ) -> Dict[str, Any]: """ Retrieve settings for a guild with caching. @@ -323,8 +324,9 @@ class MongoDBHandler: cls._settings_buffer[guild_id] = settings cls._last_access[guild_id] = time.time() - return copy.deepcopy(cls._settings_buffer[guild_id]) - + buffer = cls._settings_buffer[guild_id] + return copy.deepcopy(buffer) if deep_copy else buffer + except Exception as e: raise ConnectionError(f"Failed to retrieve settings: {str(e)}") @@ -352,7 +354,7 @@ class MongoDBHandler: ConnectionError: If database operation fails """ try: - settings = await cls.get_settings(guild_id) + settings = await cls.get_settings(guild_id, deep_copy=False) result = await cls._update_db( cls._settings_db, settings, @@ -403,7 +405,7 @@ class MongoDBHandler: 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)} + user = {**copy.deepcopy(cls._user_base), "_id": user_id} try: await cls._users_db.insert_one(user) except Exception as e: diff --git a/voicelink/views/controller.py b/voicelink/views/controller.py index 2d3d903..b6e4b2f 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -198,7 +198,7 @@ class AddFav(ControlButton): return await self.send(interaction, "player.errors.noTrackPlaying") if track.is_stream: return await self.send(interaction, "playlist.errors.streamNotAllowed") - user = await MongoDBHandler.get_user(interaction.user.id, 'playlist') + user = await MongoDBHandler.get_user(interaction.user.id, d_type='playlist') _, max_t, _ = Config().get_playlist_config() if len(user['200']['tracks']) >= max_t: return await self.send(interaction, "playlist.errors.trackLimited", max_t, ephemeral=True) From 90c2f224967c6053e42b3fcac1ad73f115afba88 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:56:30 +0800 Subject: [PATCH 22/52] Fix music controller being removed after song playback --- voicelink/player.py | 4 ++-- voicelink/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/voicelink/player.py b/voicelink/player.py index b2c85ab..dc3e36f 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -470,11 +470,11 @@ class Player(VoiceProtocol): # Send a new controller message if none exists if not self.controller: - self.controller = await dispatch_message(self.context, content=embed, view=view, requires_fetch=True) + self.controller = await dispatch_message(self.context, content=embed, view=view, delete_after=None, requires_fetch=True) elif not await self.is_position_fresh(): await self.controller.delete() - self.controller = await dispatch_message(self.context, content=embed, view=view, requires_fetch=True) + self.controller = await dispatch_message(self.context, content=embed, view=view, delete_after=None, requires_fetch=True) else: await self.controller.edit(embed=embed, view=view) diff --git a/voicelink/utils.py b/voicelink/utils.py index eecd454..b5793b8 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -354,7 +354,7 @@ async def dispatch_message( 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"): + if not delete_after and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): delete_after = 10 send_kwargs["delete_after"] = delete_after From 8c68686497040812132e573e05b32534c940dde0 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:57:11 +0800 Subject: [PATCH 23/52] Fix bot command not working in music request channel --- main.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index a813856..d2b43ca 100644 --- a/main.py +++ b/main.py @@ -86,21 +86,22 @@ class Vocard(commands.Bot): settings = await MongoDBHandler.get_settings(message.guild.id) if settings and (request_channel := settings.get("music_request_channel")): if message.channel.id == request_channel.get("text_channel_id"): - ctx = await self.get_context(message) + ctx = await self.get_context(message) try: - cmd = self.get_command("play") - if message.content: - await cmd(ctx, query=message.content) + if not ctx.prefix: + cmd = self.get_command("play") + if message.content: + await cmd(ctx, query=message.content) - elif message.attachments: - for attachment in message.attachments: - await cmd(ctx, query=attachment.url) + elif message.attachments: + for attachment in message.attachments: + await cmd(ctx, query=attachment.url) except Exception as e: await dispatch_message(ctx, str(e), ephemeral=True) finally: - return await message.delete() + await message.delete() await self.process_commands(message) From df05a5b1bd95f1e1426c3bfbed3df6170ee82c8d Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 4 Oct 2025 22:48:14 +0800 Subject: [PATCH 24/52] Handle None prefix in get_prefix function Ensures that get_prefix returns an empty string if the prefix is None, preventing potential issues with command parsing when no prefix is set in the settings. --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index d2b43ca..417437a 100644 --- a/main.py +++ b/main.py @@ -198,7 +198,8 @@ class CommandCheck(discord.app_commands.CommandTree): async def get_prefix(bot: commands.Bot, message: discord.Message) -> str: settings = await MongoDBHandler.get_settings(message.guild.id) - return settings.get("prefix", bot_config.bot_prefix) + prefix = settings.get("prefix", bot_config.bot_prefix) + return prefix if prefix is not None else "" # Loading settings and logger bot_config = Config(func.open_json("settings.json")) From 5c98e60f91f854658e38be8269b55b9bab837c2b Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 5 Oct 2025 22:28:01 +0800 Subject: [PATCH 25/52] Set default current view in DynamicViewManager Initializes _current_view to the 'home' view if present, instead of None. Also removes unused BaseModal import and fixes type annotation for Pagination in QueueView. --- voicelink/views/queue.py | 4 ++-- voicelink/views/utils/dynamic_view_manager.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/voicelink/views/queue.py b/voicelink/views/queue.py index 2f339d2..5a8dc23 100644 --- a/voicelink/views/queue.py +++ b/voicelink/views/queue.py @@ -27,7 +27,7 @@ import discord from typing import TYPE_CHECKING -from .utils import Pagination, BaseModal +from .utils import Pagination from .pagination import PaginationView from ..language import LangHandler @@ -59,7 +59,7 @@ class QueueView(PaginationView): self.response: discord.Message = None super().__init__( - Pagination[Track]( + Pagination["Track"]( items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())), page_size=7, ), diff --git a/voicelink/views/utils/dynamic_view_manager.py b/voicelink/views/utils/dynamic_view_manager.py index d8f1afa..8b6d6b1 100644 --- a/voicelink/views/utils/dynamic_view_manager.py +++ b/voicelink/views/utils/dynamic_view_manager.py @@ -30,7 +30,7 @@ class DynamicViewManager(discord.ui.View): super().__init__(timeout=timeout) self._views: dict[str, discord.ui.View] = views - self._current_view: discord.ui.View | None = None + self._current_view: discord.ui.View | None = views.get("home") def add_view(self, name: str, view: discord.ui.View) -> None: """ From 7f6135801d951ed68c9241dd6458f5885105ccc6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 5 Oct 2025 22:39:07 +0800 Subject: [PATCH 26/52] Add items property to Pagination and refactor QueueView Introduced an 'items' property to the Pagination class for direct access to the underlying items list. Refactored QueueView to use this new property, improving code clarity and consistency. --- voicelink/views/queue.py | 16 +++++++--------- voicelink/views/utils/pagination.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/voicelink/views/queue.py b/voicelink/views/queue.py index 5a8dc23..69e1521 100644 --- a/voicelink/views/queue.py +++ b/voicelink/views/queue.py @@ -52,12 +52,6 @@ class QueueView(PaginationView): """ 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())), @@ -66,18 +60,22 @@ class QueueView(PaginationView): author ) + self.player: Player = player + self.is_queue: bool = is_queue + self.total_duration = self.calculate_total_duration() + self.response: discord.Message = None + def calculate_total_duration(self) -> str: """Calculate the total duration of the tracks.""" try: - return format_ms(sum(track.length for track in self.pagination._items)) + 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}```") + texts[1].format(self.player.current.uri, f"```{self.player.current.title}```") if self.player.current else texts[2].format("None") ) diff --git a/voicelink/views/utils/pagination.py b/voicelink/views/utils/pagination.py index 3a48070..339dab3 100644 --- a/voicelink/views/utils/pagination.py +++ b/voicelink/views/utils/pagination.py @@ -165,4 +165,14 @@ class Pagination(Generic[T]): Returns: int: The total number of items across all pages. """ - return len(self._items) \ No newline at end of file + return len(self._items) + + @property + def items(self) -> List[T]: + """ + Gets the list of all items in the pagination. + + Returns: + List[T]: The list of all items. + """ + return self._items \ No newline at end of file From b503385542b76054005de329329729d880d09692 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:56:30 +0800 Subject: [PATCH 27/52] Refactor modal usage and error handling in views Replaced custom Modal class with BaseModal in embed_builder.py for consistency and maintainability. Updated modal instantiation to use custom_id and improved value handling. Removed redundant on_error methods from help.py, pagination.py, and search.py, and delegated error handling to superclass in controller.py for cleaner error management. --- voicelink/views/controller.py | 8 +- voicelink/views/embed_builder.py | 323 ++++++++++++++++--------------- voicelink/views/help.py | 3 - voicelink/views/pagination.py | 3 - voicelink/views/search.py | 3 - 5 files changed, 166 insertions(+), 174 deletions(-) diff --git a/voicelink/views/controller.py b/voicelink/views/controller.py index b6e4b2f..1879dfc 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -484,10 +484,6 @@ class InteractiveController(discord.ui.View): async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: if isinstance(error, ButtonOnCooldown): sec = int(error.retry_after) - await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True) + return await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True) - elif isinstance(error, Exception): - traceback.print_exception(error) - await interaction.response.send_message(error) - - return \ No newline at end of file + super().on_error(interaction, error, item) \ No newline at end of file diff --git a/voicelink/views/embed_builder.py b/voicelink/views/embed_builder.py index 0a4294d..8d092da 100644 --- a/voicelink/views/embed_builder.py +++ b/voicelink/views/embed_builder.py @@ -24,26 +24,12 @@ SOFTWARE. import discord import copy -from typing import List from discord.ext import commands +from .utils import BaseModal 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) - for item in items: - self.add_item(item) - - self.values: dict = {} - - async def on_submit(self, interaction: discord.Interaction) -> None: - await interaction.response.defer() - for children in self.children: - self.values[children.label.lower()] = children.value - - self.stop() class Dropdown(discord.ui.Select): def __init__(self): @@ -90,42 +76,47 @@ 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.embed_type, {}) - items = [ - discord.ui.TextInput( - label="Title", - placeholder="The title of the embed", - style=discord.TextStyle.paragraph, - max_length=1000, - default=data.get("title", {}).get("name"), - required=False - ), - discord.ui.TextInput( - label="Url", - placeholder="The url of the title", - style=discord.TextStyle.short, - max_length=100, - default=data.get("title", {}).get("url"), - required=False - ), - discord.ui.TextInput( - label="Color", - placeholder="The color of the embed", - style=discord.TextStyle.short, - max_length=100, - default=data.get("color"), - required=False - ), - discord.ui.TextInput( - label="Description", - placeholder="The description of the title", - style=discord.TextStyle.paragraph, - max_length=200, - default=data.get("description"), - required=False - ) - ] - - modal = Modal(items, title="Edit Content") + modal = BaseModal( + title="Edit Content", + custom_id="edit_content", + items=[ + discord.ui.TextInput( + label="Title", + placeholder="The title of the embed", + style=discord.TextStyle.paragraph, + custom_id="title", + max_length=1000, + default=data.get("title", {}).get("name"), + required=False + ), + discord.ui.TextInput( + label="Url", + placeholder="The url of the title", + style=discord.TextStyle.short, + custom_id="url", + max_length=100, + default=data.get("title", {}).get("url"), + required=False + ), + discord.ui.TextInput( + label="Color", + placeholder="The color of the embed", + style=discord.TextStyle.short, + custom_id="color", + max_length=100, + default=data.get("color"), + required=False + ), + discord.ui.TextInput( + label="Description", + placeholder="The description of the title", + style=discord.TextStyle.paragraph, + custom_id="description", + max_length=200, + default=data.get("description"), + required=False + ) + ]) await interaction.response.send_modal(modal) await modal.wait() @@ -142,112 +133,120 @@ class EmbedBuilderView(discord.ui.View): except: pass - return await interaction.edit_original_response(embed=self.build_embed()) + return await self.response.edit(embed=self.build_embed()) @discord.ui.button(label="Edit Author",) async def edit_author(self, interaction: discord.Interaction, button: discord.ui.Button): data = self.data.get(self.embed_type, {}) - items = [ - discord.ui.TextInput( - label="Name", - placeholder="The name of the author", - style=discord.TextStyle.paragraph, - max_length=200, - default=data.get("author", {}).get("name"), - required=False - ), - discord.ui.TextInput( - label="Url", - placeholder="The url of the author", - style=discord.TextStyle.short, - max_length=100, - default=data.get("author", {}).get("url"), - required=False - ), - discord.ui.TextInput( - label="Icon Url", - placeholder="The icon url of the author", - style=discord.TextStyle.short, - max_length=100, - default=data.get("author", {}).get("icon_url"), - required=False - ), - ] - - modal = Modal(items, title="Edit Author") + modal = BaseModal( + title="Edit Author", + custom_id="edit_author", + items=[ + discord.ui.TextInput( + label="Name", + placeholder="The name of the author", + style=discord.TextStyle.paragraph, + custom_id="name", + max_length=200, + default=data.get("author", {}).get("name"), + required=False + ), + discord.ui.TextInput( + label="Url", + placeholder="The url of the author", + style=discord.TextStyle.short, + custom_id="url", + max_length=100, + default=data.get("author", {}).get("url"), + required=False + ), + discord.ui.TextInput( + label="Icon Url", + placeholder="The icon url of the author", + style=discord.TextStyle.short, + custom_id="icon_url", + max_length=100, + default=data.get("author", {}).get("icon_url"), + required=False + ), + ]) await interaction.response.send_modal(modal) await modal.wait() v = modal.values - if v['name'] != "": if "author" not in data: data["author"] = {} data["author"]["name"] = v['name'] data["author"]["url"] = v['url'] - data["author"]["icon_url"] = v['icon url'] + data["author"]["icon_url"] = v['icon_url'] else: del data["author"] - return await interaction.edit_original_response(embed=self.build_embed()) + return await self.response.edit(embed=self.build_embed()) @discord.ui.button(label="Edit Image") async def edit_image(self, interaction: discord.Interaction, button: discord.ui.Button): data = self.data.get(self.embed_type, {}) - items = [ - discord.ui.TextInput( - label="Thumbnail", - placeholder="The url of the thumbnail", - style=discord.TextStyle.short, - max_length=200, - default=data.get("thumbnail"), - required=False - ), - discord.ui.TextInput( - label="Image", - placeholder="The url of the image", - style=discord.TextStyle.short, - max_length=100, - default=data.get("image"), - required=False - ) - ] - - modal = Modal(items, title="Edit Image") + modal = BaseModal( + title="Edit Image", + custom_id="edit_image", + items=[ + discord.ui.TextInput( + label="Thumbnail", + placeholder="The url of the thumbnail", + style=discord.TextStyle.short, + custom_id="thumbnail", + max_length=200, + default=data.get("thumbnail"), + required=False + ), + discord.ui.TextInput( + label="Image", + placeholder="The url of the image", + style=discord.TextStyle.short, + custom_id="image", + max_length=100, + default=data.get("image"), + required=False + ) + ]) await interaction.response.send_modal(modal) await modal.wait() v = modal.values - data["thumbnail"] = v['thumbnail'] data["image"] = v['image'] - return await interaction.edit_original_response(embed=self.build_embed()) + return await self.response.edit(embed=self.build_embed()) @discord.ui.button(label="Edit Footer") async def edit_footer(self, interaction: discord.Interaction, button: discord.ui.Button): data = self.data.get(self.embed_type, {}) - items = [ - discord.ui.TextInput( - label="Text", - placeholder="The text of the footer", - style=discord.TextStyle.paragraph, - max_length=200, - default=data.get("footer", {}).get("text"), - required=False - ), - discord.ui.TextInput( - label="Icon Url", - placeholder="The url of the icon", - style=discord.TextStyle.short, - max_length=100, - default=data.get("footer", {}).get("icon_url"), - required=False - ) - ] - - modal = Modal(items, title="Edit Footer") + modal = BaseModal( + title="Edit Footer", + custom_id="edit_footer", + items=[ + discord.ui.TextInput( + label="Text", + placeholder="The text of the footer", + style=discord.TextStyle.paragraph, + custom_id="text", + max_length=200, + default=data.get("footer", {}).get("text"), + required=False + ), + discord.ui.TextInput( + label="Icon Url", + placeholder="The url of the icon", + style=discord.TextStyle.short, + custom_id="icon_url", + max_length=100, + default=data.get("footer", {}).get("icon_url"), + required=False + ) + ]) await interaction.response.send_modal(modal) await modal.wait() @@ -256,40 +255,44 @@ class EmbedBuilderView(discord.ui.View): data["footer"] = {} data["footer"]["text"] = v['text'] - data["footer"]["icon_url"] = v['icon url'] + data["footer"]["icon_url"] = v['icon_url'] - return await interaction.edit_original_response(embed=self.build_embed()) + return await self.response.edit(embed=self.build_embed()) @discord.ui.button(label="Add Field", style=discord.ButtonStyle.green, row=1) async def add_field(self, interaction: discord.Interaction, button: discord.ui.Button): data = self.data.get(self.embed_type) - items = [ - discord.ui.TextInput( - label="Name", - placeholder="The name of the field", - style=discord.TextStyle.paragraph, - max_length=256 - ), - discord.ui.TextInput( - label="Value", - placeholder="The value of the field", - style=discord.TextStyle.long, - max_length=1024 - ), - discord.ui.TextInput( - label="Inline", - placeholder="The inline of the field, e.g. True or False", - style=discord.TextStyle.short - ) - ] - if "fields" not in data: data["fields"] = [] if len(data["fields"]) >= 25: return await interaction.response.send_message("You have already reached the maximum of fields!", ephemeral=True) - modal = Modal(items, title="Add Field") + modal = BaseModal( + title="Add Field", + custom_id="add_field", + items=[ + discord.ui.TextInput( + label="Name", + placeholder="The name of the field", + style=discord.TextStyle.paragraph, + custom_id="name", + max_length=256 + ), + discord.ui.TextInput( + label="Value", + placeholder="The value of the field", + style=discord.TextStyle.long, + custom_id="value", + max_length=1024 + ), + discord.ui.TextInput( + label="Inline", + placeholder="The inline of the field, e.g. True or False", + style=discord.TextStyle.short, + custom_id="inline", + ) + ]) await interaction.response.send_modal(modal) await modal.wait() @@ -300,18 +303,10 @@ class EmbedBuilderView(discord.ui.View): "inline": True if v["inline"].lower() == "true" else False }) - return await interaction.edit_original_response(embed=self.build_embed()) + return await self.response.edit(embed=self.build_embed()) @discord.ui.button(label="Remove Field", style=discord.ButtonStyle.red, row=1) async def remove_field(self, interaction: discord.Interaction, button: discord.ui.Button): - items = [ - discord.ui.TextInput( - label="Index", - placeholder="The number of fields to remove, e.g. 1", - style=discord.TextStyle.short - ) - ] - data = self.data.get(self.embed_type) if "fields" not in data: data["fields"] = [] @@ -319,7 +314,17 @@ class EmbedBuilderView(discord.ui.View): if len(data["fields"]) == 0: return await interaction.response.send_message("There are no fields to remove!", ephemeral=True) - modal = Modal(items, title="Remove Field") + modal = BaseModal( + title="Remove Field", + custom_id="remove_field", + items = [ + discord.ui.TextInput( + label="Index", + placeholder="The number of fields to remove, e.g. 1", + style=discord.TextStyle.short, + custom_id="index", + ) + ]) await interaction.response.send_modal(modal) await modal.wait() @@ -328,7 +333,7 @@ class EmbedBuilderView(discord.ui.View): except: return await interaction.followup.send("Can't found the field", ephemeral=True) - return await interaction.edit_original_response(embed=self.build_embed()) + return await self.response.edit(embed=self.build_embed()) @discord.ui.button(label="Apply", style=discord.ButtonStyle.green, row=1) async def apply(self, interaction: discord.Interaction, button: discord.ui.Button): diff --git a/voicelink/views/help.py b/voicelink/views/help.py index f24dc3a..5857c24 100644 --- a/voicelink/views/help.py +++ b/voicelink/views/help.py @@ -61,9 +61,6 @@ class HelpView(discord.ui.View): self.add_item(discord.ui.Button(label='Github', emoji=':github:1098265017268322406', url='https://github.com/ChocoMeow/Vocard')) self.add_item(discord.ui.Button(label='Donate', emoji=':patreon:913397909024800878', url='https://www.patreon.com/Vocard')) self.add_item(HelpDropdown(self.categories)) - - async def on_error(self, error, item, interaction) -> None: - return async def on_timeout(self) -> None: for child in self.children: diff --git a/voicelink/views/pagination.py b/voicelink/views/pagination.py index 3d131fd..fb5d193 100644 --- a/voicelink/views/pagination.py +++ b/voicelink/views/pagination.py @@ -73,9 +73,6 @@ class PaginationView(discord.ui.View): 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: - return - async def update_message(self, interaction: discord.Interaction) -> None: """Update the view and edit the message.""" diff --git a/voicelink/views/search.py b/voicelink/views/search.py index 39e9023..f841943 100644 --- a/voicelink/views/search.py +++ b/voicelink/views/search.py @@ -54,9 +54,6 @@ class SearchView(discord.ui.View): self.values: list[str] = None self.add_item(SearchDropdown(tracks, texts)) - async def on_error(self, error, item, interaction): - return - async def on_timeout(self): for child in self.children: child.disabled = True From 7d8bec787c988dfd40ccc1d69903e5181e888c92 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:10:53 +0800 Subject: [PATCH 28/52] Refactor modals to use BaseModal and update node stats Replaced custom ExecuteModal and AddNodeModal classes with a generic BaseModal for code execution and node creation. Updated Node class to initialize _stats attribute. Adjusted logic in NodesPanel to check for node stats before displaying memory info. --- voicelink/pool.py | 1 + voicelink/views/debug.py | 149 ++++++++++++++++++--------------------- 2 files changed, 69 insertions(+), 81 deletions(-) diff --git a/voicelink/pool.py b/voicelink/pool.py index 2e87dcd..0c51068 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -92,6 +92,7 @@ class Node: self._heartbeat: int = heartbeat self._secure: bool = secure self._logger: Optional[logging.Logger] = logger + self._stats: Optional[NodeStats] = None self._websocket_uri: str = f"{'wss' if self._secure else 'ws'}://{self._host}:{self._port}/" + NODE_VERSION + "/websocket" self._rest_uri: str = f"{'https' if self._secure else 'http'}://{self._host}:{self._port}" diff --git a/voicelink/views/debug.py b/voicelink/views/debug.py index fac7ebc..f1adab0 100644 --- a/voicelink/views/debug.py +++ b/voicelink/views/debug.py @@ -33,86 +33,11 @@ import voicelink from typing import Optional from discord.ext import commands +from .utils import BaseModal 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) - self.code: str = code - self.add_item( - discord.ui.TextInput( - label="Code Runner", - placeholder="Input Your Code", - style=discord.TextStyle.long, - default=self.code - ) - ) - - async def on_submit(self, interaction: discord.Interaction): - await interaction.response.defer() - self.code = self.children[0].value - self.stop() - -class AddNodeModal(discord.ui.Modal): - def __init__(self, view, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - self.view: NodesPanel = view - - self.add_item( - discord.ui.TextInput( - label="Host", - placeholder="Enter the lavalink host e.g 0.0.0.0" - ) - ) - self.add_item( - discord.ui.TextInput( - label="Port", - placeholder="Enter the lavalink port e.g 2333" - ) - ) - self.add_item( - discord.ui.TextInput( - label="Password", - placeholder="Enter the lavalink password" - ) - ) - self.add_item( - discord.ui.TextInput( - label="Secure", - placeholder="Specify if your Lavalink uses SSL. Enter 'true' or 'false'" - ) - ) - self.add_item( - discord.ui.TextInput( - label="Identifier", - placeholder="Enter a name for your lavalink server" - ) - ) - - async def on_submit(self, interaction: discord.Interaction): - try: - config = { - "host": self.children[0].value, - "port": int(self.children[1].value), - "password": self.children[2].value, - "secure": self.children[3].value.lower() == "true", - "identifier": self.children[4].value - } - except Exception: - return await interaction.response.send_message("Some of your input is invalid! Please try again.", ephemeral=True) - - await interaction.response.defer() - try: - await voicelink.NodePool.create_node(bot=interaction.client, **config) - await interaction.followup.send(f"Node {self.children[4].value} is connected!", ephemeral=True) - await self.view.message.edit(embed=self.view.build_embed(), view=self.view) - - except Exception as e: - return await interaction.followup.send(e, ephemeral=True) - class CogsDropdown(discord.ui.Select): def __init__(self, bot: commands.Bot): self.bot: commands.Bot = bot @@ -206,11 +131,21 @@ class ExecutePanel(discord.ui.View): await self.message.edit(view=self) async def execute(self, interaction: discord.Interaction): - modal = ExecuteModal(self.code, title="Enter Your Code") + modal = BaseModal( + title="Enter Your Code", + custom_id="execute_code_modal", + items=[discord.ui.TextInput( + label="Code Runner", + placeholder="Input Your Code", + style=discord.TextStyle.long, + custom_id="code_runner", + default=self.code + )] + ) await interaction.response.send_modal(modal) await modal.wait() - if not (code := modal.code): + if not (code := modal.values.get("code_runner")): return self._error = None @@ -287,7 +222,7 @@ class NodesPanel(discord.ui.View): if self.selected_node and self.selected_node._identifier != node._identifier: continue - if node._available: + if node._available and node.stats: total_memory = node.stats.used + node.stats.free embed.add_field( name=f"{name} Node - 🟢 Connected", @@ -312,9 +247,61 @@ class NodesPanel(discord.ui.View): @discord.ui.button(label="Add", emoji="➕", style=discord.ButtonStyle.green) async def add(self, interaction: discord.Interaction, button: discord.ui.Button): - modal = AddNodeModal(self, title="Create Node") + modal = BaseModal( + title="Create Node", + custom_id="add_node_modal", + items=[ + discord.ui.TextInput( + label="Host", + placeholder="Enter the lavalink host e.g 0.0.0.0", + custom_id="host" + ), + discord.ui.TextInput( + label="Port", + placeholder="Enter the lavalink port e.g 2333", + custom_id="port" + ), + discord.ui.TextInput( + label="Password", + placeholder="Enter the lavalink password", + custom_id="password" + ), + discord.ui.TextInput( + label="Secure", + placeholder="Specify if your Lavalink uses SSL. Enter 'true' or 'false'", + custom_id="secure", + default="false" + ), + discord.ui.TextInput( + label="Identifier", + placeholder="Enter a name for your lavalink server", + custom_id="identifier" + ) + ] + ) await interaction.response.send_modal(modal) - + await modal.wait() + + v = modal.values + try: + config = { + "host": v["host"], + "port": int(v["port"]), + "password": v["password"], + "secure": v["secure"].startswith("t"), + "identifier": v["identifier"] + } + except Exception: + return await interaction.response.send_message("Some of your input is invalid! Please try again.", ephemeral=True) + + try: + await voicelink.NodePool.create_node(bot=interaction.client, **config) + await interaction.followup.send(f"Node {v['identifier']} is connected!", ephemeral=True) + await self.message.edit(embed=self.build_embed(), view=self) + + except Exception as e: + return await interaction.followup.send(e, ephemeral=True) + @discord.ui.button(label="Remove", emoji="➖", style=discord.ButtonStyle.red, disabled=True) async def remove(self, interaction: discord.Interaction, button: discord.ui.Button): if not self.selected_node: From 12be1ede6481ba417bb242f1c4a4d507943382be Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:24:16 +0800 Subject: [PATCH 29/52] Improve error logging in process_methods Replaces traceback printing with structured logging using ipc_client._logger.error, providing better context and exception details when method processing fails. --- voicelink/ipc/methods.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/voicelink/ipc/methods.py b/voicelink/ipc/methods.py index 146197c..a900ac6 100644 --- a/voicelink/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -792,8 +792,7 @@ async def process_methods(ipc_client: IPCClient, bot: commands.Bot, data: Dict) await ipc_client.send(resp) except Exception as e: - import traceback - traceback.print_exc() + ipc_client._logger.error(f"Failed to process method '{op}' by user {user_id}", exc_info=e) payload = { "op": "errorMsg", "level": "error", From 7479f9bfe253ddb9db654850e34b0b4249916d9a Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:19:40 +0800 Subject: [PATCH 30/52] Show track length in autocomplete choices Track autocomplete choices now display the formatted track length for both current tracks and history, improving user clarity when selecting tracks. --- cogs/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index a66f70c..3017b8e 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -95,10 +95,10 @@ class Basic(commands.Cog): if isinstance(tracks, voicelink.Playlist): tracks = tracks.tracks - return [app_commands.Choice(name=truncate_string(f"🎵 {track.author} - {track.title}", 100), value=truncate_string(f"{track.author} - {track.title}", 100)) for track in tracks] + return [app_commands.Choice(name=truncate_string(f"🎵 [{track.formatted_length}] {track.author} - {track.title}", 100), value=track.uri) for track in tracks] history = {track["identifier"]: track for track_id in reversed(await 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] + return [app_commands.Choice(name=truncate_string(f"🕒 [{format_ms(track['length'])}] {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25] @commands.hybrid_command(name="connect", aliases=get_aliases("connect")) @app_commands.describe(channel="Provide a channel to connect.") From c28f17ea43aa3df2131d8b6df446bc909089fefa Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:19:56 +0800 Subject: [PATCH 31/52] Update playlist link validation and info text Standardized playlist link validation messages across all language files and code, removing specific references to Spotify and YouTube. Updated README and help view to reflect broader platform support and clarified instructions. Removed unused YouTube regex and related thumbnail logic from objects.py. --- README.md | 3 ++- langs/DE.json | 2 +- langs/EN.json | 2 +- langs/ES.json | 2 +- langs/FR.json | 2 +- langs/JA.json | 2 +- langs/KO.json | 2 +- langs/PL.json | 2 +- langs/RU.json | 2 +- langs/UA.json | 2 +- langs/VN.json | 2 +- langs/ZHCN.json | 2 +- langs/ZHTW.json | 2 +- voicelink/ipc/methods.py | 2 +- voicelink/objects.py | 6 +----- voicelink/views/controller.py | 1 - voicelink/views/help.py | 4 ++-- 17 files changed, 18 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 55a5870..ebf50c0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Vocard is a highly customizable Discord music bot, designed to deliver a user-fr * Lightweight design * Smooth playback * Clean and nice interface -* Supports many music platforms (YouTube, SoundCloud, etc.) +* Supports many music platforms (YouTube, SoundCloud, Spotify, Apple Music etc.) * Built-in playlist support * Fully customizable settings * Lyrics support @@ -19,6 +19,7 @@ Vocard is a highly customizable Discord music bot, designed to deliver a user-fr * Multiple languages available * Easy to update * Supports docker +* [One Click Installer](https://github.com/ChocoMeow/Vocard-Installer) * [Premium dashboard](https://github.com/ChocoMeow/Vocard-Dashboard) ## Screenshot diff --git a/langs/DE.json b/langs/DE.json index 593b21e..2eae5e3 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -174,7 +174,7 @@ "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.", + "invalidUrl": "Bitte geben Sie einen gültigen öffentlichen Playlist-Link ein.", "trackLimitReached": "Du hast die Grenze erreicht! Du kannst deiner Playlist nur noch `{0}` Songs hinzufügen.", "playlistLinkNotAllowed": "Du kannst keine Playlist-Links verwenden.", "streamNotAllowed": "Du darfst deiner Playlist keine aktiven Streams hinzufügen.", diff --git a/langs/EN.json b/langs/EN.json index 28dc5a9..2672819 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -174,7 +174,7 @@ "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.", + "invalidUrl": "Please enter a valid public playlist link.", "trackLimitReached": "You have reached the limit! You can only add `{0}` songs to your playlist.", "playlistLinkNotAllowed": "You are not allowed to use playlist links.", "streamNotAllowed": "You are not allowed to add streaming videos to your playlist.", diff --git a/langs/ES.json b/langs/ES.json index 24460b2..d3b5d41 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -174,7 +174,7 @@ "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.", + "invalidUrl": "Por favor, introduzca un enlace válido de lista de reproducción pública.", "trackLimitReached": "¡Ha alcanzado el límite! Solo puede agregar `{0}` canciones a su lista de reproducción.", "playlistLinkNotAllowed": "No está autorizado para usar enlaces de lista de reproducción.", "streamNotAllowed": "No está autorizado para agregar videos de transmisión a su lista de reproducción.", diff --git a/langs/FR.json b/langs/FR.json index 90d6441..813f389 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -174,7 +174,7 @@ "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.", + "invalidUrl": "Veuillez entrer un lien valide de playlist publique.", "trackLimitReached": "Vous avez atteint la limite ! Vous ne pouvez ajouter que `{0}` chansons à votre playlist.", "playlistLinkNotAllowed": "Vous n'êtes pas autorisé à utiliser des liens de playlist.", "streamNotAllowed": "Vous n'êtes pas autorisé à ajouter des vidéos en streaming à votre playlist.", diff --git a/langs/JA.json b/langs/JA.json index 56133a1..891a329 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -174,7 +174,7 @@ "deleteDefault": "デフォルトのプレイリストを削除することはできません。", "limitReached": " {0}個以上のプレイリストを作成することはできません!", "exists": "プレイリスト[{0}]はすでに存在します。", - "invalidUrl": "有効なリンクまたは公開SpotifyまたはYouTubeプレイリストリンクを入力してください。", + "invalidUrl": "有効な公開プレイリストのリンクを入力してください。", "trackLimitReached": "この制限に達しました!プレイリストには{0}曲しか追加できません。", "playlistLinkNotAllowed": "プレイリストリンクを使用することはできません。", "streamNotAllowed": "ストリーミングビデオをプレイリストに追加することはできません。", diff --git a/langs/KO.json b/langs/KO.json index e105467..e3445a1 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -174,7 +174,7 @@ "deleteDefault": "기본 재생 목록은 삭제할 수 없습니다.", "limitReached": "더 이상 {0}개 이상의 재생 목록을 만들 수 없습니다!", "exists": "재생 목록 [{0}]이(가) 이미 있습니다.", - "invalidUrl": "유효한 링크 또는 공개 Spotify 또는 YouTube 재생 목록 링크를 입력하세요.", + "invalidUrl": "유효한 공개 재생목록 링크를 입력하세요.", "trackLimitReached": "죄송합니다! 재생 목록에 추가할 수 있는 노래는 {0}곡까지입니다.", "playlistLinkNotAllowed": "재생 목록 링크를 사용할 수 없습니다.", "streamNotAllowed": "스트리밍 비디오를 재생 목록에 추가할 수 없습니다.", diff --git a/langs/PL.json b/langs/PL.json index ac7fc50..2b43c8a 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -174,7 +174,7 @@ "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.", + "invalidUrl": "Proszę podać prawidłowy link do publicznej playlisty.", "trackLimitReached": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.", "playlistLinkNotAllowed": "Linki do playlist są nie obsługiwane.", "streamNotAllowed": "Nie możesz dodać transmisji na żywo do playlisty.", diff --git a/langs/RU.json b/langs/RU.json index 804d4c6..ab51b37 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -174,7 +174,7 @@ "deleteDefault": "Вы не можете удалить плейлист по умолчанию.", "limitReached": "Вы не можете создавать больше `{0}` плейлистов!", "exists": "Плейлист [`{0}`] уже существует.", - "invalidUrl": "Пожалуйста, введите действительную ссылку или публичную плейлист-ссылку на Spotify или YouTube.", + "invalidUrl": "Пожалуйста, введите действительную ссылку на публичный плейлист.", "trackLimitReached": "Вы достигли лимита! Вы можете добавить только `{0}` треков в свой плейлист.", "playlistLinkNotAllowed": "Вам не разрешено использовать ссылку на плейлист.", "streamNotAllowed": "Вам не разрешено добавлять потоковые видео в свой плейлист.", diff --git a/langs/UA.json b/langs/UA.json index 919b0c6..0b5e385 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -174,7 +174,7 @@ "deleteDefault": "Ви не можете видалити плейлист за замовчуванням.", "limitReached": "Ви не можете створювати більше `{0}` плейлистів!", "exists": "Плейлист [`{0}`] вже існує.", - "invalidUrl": "Будь ласка, введіть дійсне посилання або публічне плейлист-посилання на Spotify або YouTube.", + "invalidUrl": "Будь ласка, введіть дійсне посилання на публічний плейлист.", "trackLimitReached": "Ви досягли ліміту! Ви можете додати тільки `{0}` пісень до свого плейлиста.", "playlistLinkNotAllowed": "Вам не дозволено використовувати посилання на плейлист.", "streamNotAllowed": "Вам не дозволено додавати потокові відео у свій плейлист.", diff --git a/langs/VN.json b/langs/VN.json index 78e4612..2574761 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -174,7 +174,7 @@ "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ệ.", + "invalidUrl": "Vui lòng nhập liên kết hợp lệ đến danh sách phát công khai.", "trackLimitReached": "Bạn đã đạt giới hạn! Bạn chỉ có thể thêm `{0}` bài hát vào playlist của mình.", "playlistLinkNotAllowed": "Bạn không được phép sử dụng liên kết playlist.", "streamNotAllowed": "Bạn không được phép thêm video trực tiếp vào playlist của mình.", diff --git a/langs/ZHCN.json b/langs/ZHCN.json index 57f6e82..a506635 100644 --- a/langs/ZHCN.json +++ b/langs/ZHCN.json @@ -174,7 +174,7 @@ "deleteDefault": "不允许删除默认播放列表。", "limitReached": "您最多只能创建 `{0}` 个播放列表!", "exists": "播放列表 [`{0}`] 已存在。", - "invalidUrl": "请输入有效的公共 Spotify 或 YouTube 播放列表链接。", + "invalidUrl": "请输入有效的公共播放列表链接。", "trackLimitReached": "已达到上限!每个播放列表最多只能添加 `{0}` 首歌。", "playlistLinkNotAllowed": "不允许使用播放列表链接。", "streamNotAllowed": "不允许将流媒体视频添加到播放列表。", diff --git a/langs/ZHTW.json b/langs/ZHTW.json index 1b9d148..5293eeb 100644 --- a/langs/ZHTW.json +++ b/langs/ZHTW.json @@ -174,7 +174,7 @@ "deleteDefault": "不允許刪除預設播放清單。", "limitReached": "您最多只能建立 `{0}` 個播放清單!", "exists": "播放清單 [`{0}`] 已存在。", - "invalidUrl": "請輸入有效的公開 Spotify 或 YouTube 播放清單連結。", + "invalidUrl": "請輸入有效的公開播放清單連結。", "trackLimitReached": "已達到上限!每個播放清單最多只能新增 `{0}` 首歌曲。", "playlistLinkNotAllowed": "不允許使用播放清單連結。", "streamNotAllowed": "不允許將串流影片加入播放清單。", diff --git a/voicelink/ipc/methods.py b/voicelink/ipc/methods.py index a900ac6..01c60d1 100644 --- a/voicelink/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -433,7 +433,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: return { "op": "updatePlaylist", "status": "error", - "msg": f"Please enter a valid link or public spotify or youtube playlist link.", + "msg": f"Please enter a valid link or public playlist link.", "field": "playlistUrl", "userId": str(user_id) } diff --git a/voicelink/objects.py b/voicelink/objects.py index 44d7f16..5176bc2 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -32,7 +32,6 @@ 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]+)') class Track: """The base track object. Returns critical track information needed for parsing by Lavalink. @@ -80,9 +79,6 @@ class Track: self._search_type: SearchType = search_type self.thumbnail: str = info.get("artworkUrl") - if not self.thumbnail and YOUTUBE_REGEX.match(self.uri): - self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/maxresdefault.jpg" - self.emoji: str = Config().get_source_config(self.source, "emoji") self.length: float = info.get("length") @@ -115,7 +111,7 @@ class Track: @property def formatted_length(self) -> str: return format_ms(self.length) - + @property def data(self) -> dict: return { diff --git a/voicelink/views/controller.py b/voicelink/views/controller.py index 1879dfc..5e3677f 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -24,7 +24,6 @@ SOFTWARE. import discord import re import voicelink -import traceback from discord.ext import commands from typing import Optional, Dict, Type, Union, Any diff --git a/voicelink/views/help.py b/voicelink/views/help.py index 5857c24..0e0237f 100644 --- a/voicelink/views/help.py +++ b/voicelink/views/help.py @@ -84,9 +84,9 @@ class HelpView(discord.ui.View): inline=True ) - update = "Vocard is a simple music bot. It leads to a comfortable experience which is user-friendly, It supports YouTube, Soundcloud, Spotify, Twitch and more!" + update = "Vocard is a simple music bot. It leads to a comfortable experience which is user-friendly, It supports Soundcloud, Spotify, Twitch and more!" embed.add_field(name="📰 Information:", value=update, inline=True) - embed.add_field(name="Get Started", value="```Join a voice channel and /play {Song/URL} a song. (Names, Youtube Video Links or Playlist links or Spotify links are supported on Vocard)```", inline=False) + embed.add_field(name="Get Started", value="```Join a voice channel and /play {Song/URL} a song. (Names, Video Links or Playlist links are supported on Vocard)```", inline=False) return embed From df5373353cb5502fe11b652ebbf574efd378efa3 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:12:02 +0800 Subject: [PATCH 32/52] Refactor track recommendation logic and add TrackRecType enum Moved recommendation query generation from Node to Track using the new TrackRecType enum, simplifying and centralizing platform-specific recommendation formatting. Updated related methods to use Track.get_recommendations, removed Node.get_recommendations, and improved enum handling for search platform matching. Also exposed TrackRecType in voicelink.__init__. --- cogs/basic.py | 2 +- voicelink/__init__.py | 2 +- voicelink/config.py | 2 +- voicelink/enums.py | 77 +++++++++++++++++++++++++++++++++++++++- voicelink/ipc/methods.py | 2 +- voicelink/objects.py | 38 ++++++++++++++++---- voicelink/player.py | 6 ++-- voicelink/pool.py | 20 ----------- 8 files changed, 114 insertions(+), 35 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 3017b8e..47ac547 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -226,7 +226,7 @@ class Basic(commands.Cog): if url(query): return await send_localized_message(ctx, "search.noLinkSupport", ephemeral=True) - search_type: voicelink.SearchType = voicelink.SearchType.match(platform) or Config().search_platform + search_type: voicelink.SearchType = voicelink.SearchType.from_platform(platform) or Config().search_platform tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=search_type) if not tracks: return await send_localized_message(ctx, "player.errors.noTrackFound") diff --git a/voicelink/__init__.py b/voicelink/__init__.py index c839b37..09495d8 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -27,7 +27,7 @@ __license__ = "MIT" __copyright__ = "Copyright 2023 - present (c) Vocard Development, ChocoMeow" from .config import Config -from .enums import SearchType, LoopType +from .enums import SearchType, LoopType, TrackRecType from .events import * from .exceptions import * from .filters import * diff --git a/voicelink/config.py b/voicelink/config.py index 55e3c1a..ff6de26 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", "youtube")) or SearchType.YOUTUBE + self.search_platform: SearchType = SearchType.from_platform(settings.get("default_search_platform", "youtube")) or SearchType.YOUTUBE self.bot_prefix: str = settings.get("prefix", "") self.activity: List[Dict[str, str]] = settings.get("activity", [{"listen": "/help"}]) self.logging: Dict[Union[str, Dict[str, Union[str, bool]]]] = settings.get("logging", {}) diff --git a/voicelink/enums.py b/voicelink/enums.py index bc92915..084cbbd 100644 --- a/voicelink/enums.py +++ b/voicelink/enums.py @@ -92,7 +92,7 @@ class SearchType(Enum): return self.value @classmethod - def match(cls, value: str): + def from_platform(cls, value: str): """find an enum based on a search string.""" normalized_value = value.lower().replace("_", "").replace(" ", "") @@ -106,6 +106,81 @@ class SearchType(Enum): def display_name(self) -> str: return self.name.replace("_", " ").title() +class TrackRecType(Enum): + """Enum representing track recommendation key formats for various platforms. + + Each key format is used to generate a recommendation link or identifier + for a given track ID on the respective platform. + + - RecommendationType.SPOTIFY: + Generates a Spotify recommendation key in the format 'sprec:mix:track:{track_id}'. + + - RecommendationType.YOUTUBE: + Generates a YouTube recommendation URL with a playlist context. + + - RecommendationType.YOUTUBE_MUSIC: + Same as YouTube, generates a YouTube Music recommendation URL. + + - RecommendationType.DEEZER: + Generates a Deezer recommendation key in the format 'dzrec:{track_id}'. + + - RecommendationType.YANDEX_MUSIC: + Generates a Yandex Music recommendation key in the format 'ymrec:{track_id}'. + + - RecommendationType.VK_MUSIC: + Generates a VK Music recommendation key in the format 'vkrec:{track_id}'. + + - RecommendationType.TIDAL: + Generates a Tidal recommendation key in the format 'tdrec:{track_id}'. + + - RecommendationType.QOBUZ: + Generates a Qobuz recommendation key in the format 'qbrec:{track_id}'. + + - RecommendationType.JIOSAAVN: + Generates a JioSaavn recommendation key in the format 'jsrec:{track_id}' + """ + + YOUTUBE = "https://www.youtube.com/watch?v={track_id}&list=RD{track_id}" + YOUTUBE_MUSIC = YOUTUBE + SPOTIFY = "sprec:mix:track:{track_id}" + DEEZER = "dzrec:{track_id}" + YANDEX_MUSIC = "ymrec:{track_id}" + VK_MUSIC = "vkrec:{track_id}" + TIDAL = "tdrec:{track_id}" + QOBUZ = "qbrec:{track_id}" + JIOSAAVN = "jsrec:{track_id}" + + def __str__(self) -> str: + return self.name + + def format(self, track_id: str) -> str: + """Format the recommendation key using the provided track ID. + + Args: + track_id (str): The ID of the track to format. + + Returns: + str: The formatted recommendation link. + """ + return self.value.format(track_id=track_id) + + @classmethod + def from_platform(cls, platform: str) -> 'TrackRecType': + """Find the enum member based on a platform name. + + Args: + platform (str): The name of the platform. + + Returns: + TrackRecType: The corresponding enum member, or None if not found. + """ + normalized = platform.lower().replace("_", "").replace(" ", "") + for member in cls: + if member.name.lower().replace("_", "") == normalized: + return member + + return None + class RequestMethod(Enum): """The enum for the different request methods in Voicelink """ diff --git a/voicelink/ipc/methods.py b/voicelink/ipc/methods.py index 01c60d1..886bc41 100644 --- a/voicelink/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -152,7 +152,7 @@ async def getRecommendation(bot: commands.Bot, data: Dict) -> None: track_data = Track.decode(track_id := data.get("trackId")) track = Track(track_id=track_id, info=track_data, requester=bot.user) - tracks: List[Track] = await node.get_recommendations(track, limit=60) + tracks: List[Track] = await track.get_recommendations(node) return { "op": "getRecommendation", diff --git a/voicelink/objects.py b/voicelink/objects.py index 5176bc2..f9e38fb 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -21,18 +21,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import re +from __future__ import annotations -from typing import Optional +from typing import Optional, List, TYPE_CHECKING from tldextract import extract from discord import Member -from .enums import SearchType +from .enums import SearchType, TrackRecType from .config import Config from .utils import format_ms from .transformer import encode, decode +if TYPE_CHECKING: + from .pool import Node + class Track: """The base track object. Returns critical track information needed for parsing by Lavalink. You can also pass in commands.Context to get a discord.py Context object in your track. @@ -90,17 +93,38 @@ class Track: self.end_time: Optional[int] = None def __eq__(self, other) -> bool: - if not isinstance(other, Track): - return False - - return other.track_id == self.track_id + """Checks equality between two tracks.""" + if isinstance(other, Track): + return other.track_id == self.track_id + + return False def __str__(self) -> str: + """String representation of the track.""" return self.title def __repr__(self) -> str: return f" length={self.length}>" + async def get_recommendations(self, node: Node) -> List[Track]: + """Fetches recommended tracks based on the current track.""" + if not node or not node._available: + return [] + + rec_type = TrackRecType.from_platform(self.source) + if not rec_type: + return [] + + query = rec_type.format(track_id=self.identifier) + tracks = await node.get_tracks(query=query, requester=node.bot.user) + if not tracks: + return [] + + if isinstance(tracks, Playlist): + tracks = tracks.tracks + + return tracks + @property def track_id(self) -> str: if not self._track_id: diff --git a/voicelink/player.py b/voicelink/player.py index dc3e36f..2f7efc8 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -857,14 +857,14 @@ class Player(VoiceProtocol): await self.set_pause(True) async def get_recommendations(self, *, track: Optional[Track] = None) -> bool: - """Get recommendations from Youtube or Spotify.""" + """Fetches and adds recommended tracks based on the provided track or recent history.""" if not track: try: track = choice(self.queue.history(incTrack=True)[-5:]) except IndexError: return False - - tracks = await self._node.get_recommendations(track) + + tracks = await track.get_recommendations(self._node) if tracks: await self.add_track(tracks, duplicate=False) diff --git a/voicelink/pool.py b/voicelink/pool.py index 0c51068..d415a47 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -390,26 +390,6 @@ class Node: elif load_type == "track": return [Track(track_id=data["encoded"], info=data["info"], requester=requester)] - - async def get_recommendations(self, track: Track, limit: int = 20) -> List[Optional[Track]]: - query = "" - if track.source == "youtube": - query = f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}" - - elif track.source == "spotify": - query = f"sprec:seed_tracks={track.identifier}" - - if not query: - return [] - - tracks = await self.get_tracks(query=query, requester=self.bot.user) - if not tracks: - return [] - - if isinstance(tracks, Playlist): - tracks = tracks.tracks - - return tracks[:limit] if limit else tracks async def update_refresh_yt_access_token(self, token: YTToken) -> dict: if not self._available: From 87f22b5589dd1c2d9f5eec675f631a56020cfcf6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:12:17 +0800 Subject: [PATCH 33/52] Improve controller message handling and dispatch logic Refactored Player.invoke_controller to combine controller setting and update checks, and added error handling for message deletion. Updated dispatch_message to use Optional types and MISSING for default values, improving clarity and handling of delete_after logic. --- voicelink/player.py | 14 +++++++------- voicelink/utils.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/voicelink/player.py b/voicelink/player.py index 2f7efc8..e08d349 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -448,10 +448,7 @@ class Player(VoiceProtocol): async def invoke_controller(self): """Sends or updates the music controller message in the designated channel.""" - if not self.settings.get('controller', True): - return - - if self._updating or not self.channel: + if not self.settings.get('controller', True) or self._updating or not self.channel: return self._updating = True @@ -460,8 +457,7 @@ class Player(VoiceProtocol): embed, view = self.build_embed(self.current), InteractiveController(self) if not self.controller: if request_channel_data := self.settings.get("music_request_channel"): - channel = self.bot.get_channel(request_channel_data.get("text_channel_id")) - if channel: + if channel := self.bot.get_channel(request_channel_data.get("text_channel_id")): try: self.controller = await channel.fetch_message(request_channel_data.get("controller_msg_id")) await self.controller.edit(embed=embed, view=view) @@ -473,7 +469,11 @@ class Player(VoiceProtocol): self.controller = await dispatch_message(self.context, content=embed, view=view, delete_after=None, requires_fetch=True) elif not await self.is_position_fresh(): - await self.controller.delete() + try: + await self.controller.delete() + except errors.NotFound: + self.controller = None + self.controller = await dispatch_message(self.context, content=embed, view=view, delete_after=None, requires_fetch=True) else: diff --git a/voicelink/utils.py b/voicelink/utils.py index b5793b8..daca3aa 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -27,10 +27,10 @@ import socket import discord from itertools import zip_longest -from typing import Dict, Optional +from typing import Dict, Optional, Union from timeit import default_timer as timer from discord.ext import commands -from typing import Union +from discord.utils import MISSING from .mongodb import MongoDBHandler from .language import LangHandler @@ -301,9 +301,9 @@ 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, + view: Optional[discord.ui.View] = None, + file: Optional[discord.File] = None, + delete_after: Optional[float] = MISSING, ephemeral: bool = False, requires_fetch: bool = False ) -> Optional[discord.Message]: @@ -354,7 +354,7 @@ async def dispatch_message( send_kwargs["view"] = view if "delete_after" in send_func.__code__.co_varnames: - if not delete_after and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): + if delete_after is MISSING and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): delete_after = 10 send_kwargs["delete_after"] = delete_after From c1ad86563b603864d42c2590653c9d30f11b3b80 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:12:29 +0800 Subject: [PATCH 34/52] Fix queue history display logic in QueueView Updates the QueueView to pass 'not player.current' to player.queue.history(), ensuring the history is correctly reversed and displayed when not showing the current queue. --- voicelink/views/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicelink/views/queue.py b/voicelink/views/queue.py index 69e1521..a3cfbbe 100644 --- a/voicelink/views/queue.py +++ b/voicelink/views/queue.py @@ -54,7 +54,7 @@ class QueueView(PaginationView): def __init__(self, player: "Player", author: discord.Member, is_queue: bool = True) -> None: super().__init__( Pagination["Track"]( - items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())), + items=player.queue.tracks() if is_queue else list(reversed(player.queue.history(not player.current))), page_size=7, ), author From e99aefefcf50416bdb0ed03d2c55111eb111b6b5 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:33:27 +0800 Subject: [PATCH 35/52] Handle MISSING sentinel in delete_after assignment --- requirements.txt | 2 +- voicelink/player.py | 9 +++++---- voicelink/utils.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0cbe8f1..19417d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.6.3 +discord.py==2.6.4 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 diff --git a/voicelink/player.py b/voicelink/player.py index e08d349..e79e137 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -514,10 +514,11 @@ class Player(VoiceProtocol): try: await self.update_voice_status(remove_status=True) - if self.controller and self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"): - await self.controller.edit(embed=self.build_embed(), view=None) - else: - await self.controller.delete() + if self.controller: + if self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"): + await self.controller.edit(embed=self.build_embed(), view=None) + else: + await self.controller.delete() except: pass diff --git a/voicelink/utils.py b/voicelink/utils.py index daca3aa..cc3a092 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -356,11 +356,11 @@ async def dispatch_message( if "delete_after" in send_func.__code__.co_varnames: if delete_after is MISSING and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): delete_after = 10 - send_kwargs["delete_after"] = delete_after + send_kwargs["delete_after"] = delete_after if delete_after != MISSING else None if "ephemeral" in send_func.__code__.co_varnames: send_kwargs["ephemeral"] = ephemeral - + # Send the message or embed message = await send_func(**send_kwargs) From e2ba8b472ab40243fd124aa642cc86bdac4dd669 Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 20 Nov 2025 23:34:34 +0800 Subject: [PATCH 36/52] Refactor message dispatch and playlist view error handling Updated dispatch_message to use discord.utils.MISSING and improved content formatting. Added error handling in PlaylistView for VoicelinkException and refactored on_timeout logic in PlaylistViewManager to avoid duplication. Minor improvements to playlist track rendering. --- voicelink/utils.py | 13 ++++++------- voicelink/views/playlist.py | 33 +++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/voicelink/utils.py b/voicelink/utils.py index cc3a092..7d63192 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -30,7 +30,6 @@ from itertools import zip_longest from typing import Dict, Optional, Union from timeit import default_timer as timer from discord.ext import commands -from discord.utils import MISSING from .mongodb import MongoDBHandler from .language import LangHandler @@ -303,7 +302,7 @@ async def dispatch_message( *params, view: Optional[discord.ui.View] = None, file: Optional[discord.File] = None, - delete_after: Optional[float] = MISSING, + delete_after: Optional[float] = discord.utils.MISSING, ephemeral: bool = False, requires_fetch: bool = False ) -> Optional[discord.Message]: @@ -323,12 +322,12 @@ async def dispatch_message( Returns: The sent message object, or None. """ - if content is None: + if not content: content = "No content provided." # Determine the text to send embed = content if isinstance(content, discord.Embed) else None - text = None if embed else content.format(*params) + text = None if embed else str(content).format(*params) if params else str(content) # Determine the sending function send_func = ( @@ -354,13 +353,13 @@ async def dispatch_message( send_kwargs["view"] = view if "delete_after" in send_func.__code__.co_varnames: - if delete_after is MISSING and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): + if delete_after is discord.utils.MISSING and settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): delete_after = 10 - send_kwargs["delete_after"] = delete_after if delete_after != MISSING else None + send_kwargs["delete_after"] = delete_after if delete_after is not discord.utils.MISSING else None if "ephemeral" in send_func.__code__.co_varnames: send_kwargs["ephemeral"] = ephemeral - + # Send the message or embed message = await send_func(**send_kwargs) diff --git a/voicelink/views/playlist.py b/voicelink/views/playlist.py index 068fc8f..a418955 100644 --- a/voicelink/views/playlist.py +++ b/voicelink/views/playlist.py @@ -31,9 +31,10 @@ from typing import Any from .utils import DynamicViewManager, Pagination from .pagination import PaginationView from ..config import Config -from ..utils import format_ms, truncate_string +from ..utils import format_ms, truncate_string, dispatch_message from ..mongodb import MongoDBHandler from ..language import LangHandler +from ..exceptions import VoicelinkException class PlaylistDropdown(discord.ui.Select): def __init__(self, results: list[dict[str, Any]], lang: str) -> None: @@ -108,7 +109,7 @@ class PlaylistView(PaginationView): embed.description += f"\n\n**{texts[5]}:**\n" if tracks: for index, track in enumerate(tracks, start=self.pagination.start_index + 1): - if self.type == "playlist": + if isinstance(track, dict): source_emoji = Config().get_source_config(track['sourceName'], 'emoji') track_info = f"{source_emoji} `{index:>2}.` `[{format_ms(track['length'])}]` [{truncate_string(track['title'])}]({track['uri']})" else: @@ -140,6 +141,10 @@ class PlaylistView(PaginationView): } return super().update_view(extra_states) + async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: + if isinstance(error, VoicelinkException): + return await dispatch_message(interaction, content=getattr(error, 'original', error), ephemeral=True) + async def update_message(self, interaction: discord.Interaction) -> None: """Update the view and edit the message with the new embed.""" self.update_view() @@ -199,6 +204,18 @@ class PlaylistViewManager(DynamicViewManager): self.response: discord.Message = None self.add_item(PlaylistDropdown(results, self.lang)) + async def on_timeout(self) -> None: + for view in self._views.values(): + view.stop() + + for child in self.current_view.children: + child.disabled = True + + try: + await self.response.edit(view=self) + except: + pass + def get_width(self, s): width = 0 for char in str(s): @@ -214,18 +231,6 @@ class PlaylistViewManager(DynamicViewManager): 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 - def build_embed(self) -> discord.Embed: """ Build the embed for the playlist overview. From d5186319a028f25698483945d038ea1dc4a20dc6 Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 20 Nov 2025 23:34:51 +0800 Subject: [PATCH 37/52] Add playlist permission management commands Introduces a new 'permission' command to grant or revoke read, write, or remove permissions for playlists. Refactors playlist access checks to support granular permissions and shared playlist operations, and updates localized messages for permission actions. --- cogs/playlist.py | 211 +++++++++++++++++++++++++++++++++++++++++------ langs/EN.json | 9 ++ 2 files changed, 193 insertions(+), 27 deletions(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index 948d700..4fe56c5 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -26,6 +26,7 @@ import discord import voicelink from io import StringIO +from typing import Optional, Tuple from discord import app_commands from discord.ext import commands from function import ( @@ -43,17 +44,47 @@ def assign_playlist_id(existed: list) -> str: if str(i) not in existed: return str(i) -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.""" +def resolve_owner_display(ctx: commands.Context[commands.Bot], owner_id: int): + if owner_id == ctx.author.id: + return ctx.author + if ctx.guild: + member = ctx.guild.get_member(owner_id) + if member: + return member + user = ctx.bot.get_user(owner_id) + return user if user else f"<@{owner_id}>" + +async def check_playlist_perms( + user_id: int, + author_id: int, + playlist_id: str, + *, + required_perm: Optional[str] = "read" +) -> Tuple[Optional[dict], Optional[str]]: + """Check if user has the requested permissions for a specific playlist.""" user_data = await MongoDBHandler.get_user(author_id, d_type='playlist') playlist = user_data.get(playlist_id) - if not playlist or user_id not in playlist['perms']['read']: - return {} + if not playlist: + return None, "not_found" - return playlist + perms = playlist.get('perms', {}) + if user_id not in perms.get('read', []): + return None, "no_read" + + if required_perm and required_perm != "read": + if user_id not in perms.get(required_perm, []): + return None, "no_permission" + + return playlist, None -async def check_playlist(ctx: commands.Context, name: str = None, full: bool = False, share: bool = True) -> dict: +async def check_playlist( + ctx: commands.Context, + name: str = None, + full: bool = False, + share: bool = True, + share_perm: Optional[str] = None +) -> dict: """Get user's playlist data with various filtering options.""" user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist') @@ -64,23 +95,70 @@ async def check_playlist(ctx: commands.Context, name: str = None, full: bool = F return user_playlists if not name: - return {'playlist': user_playlists['200'], 'position': 1, 'id': "200"} + return { + 'playlist': user_playlists['200'], + 'position': 1, + 'id': "200", + 'is_shared': False, + 'owner_id': ctx.author.id, + 'owner_playlist_id': "200", + 'error': None + } for index, playlist_id in enumerate(user_playlists, start=1): playlist = user_playlists[playlist_id] if playlist['name'].lower() == name.lower(): if playlist['type'] == 'share' and share: - shared_playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) + shared_playlist, error = await check_playlist_perms( + ctx.author.id, + playlist['user'], + playlist['referId'], + required_perm=share_perm or "read" + ) - if not shared_playlist or ctx.author.id not in shared_playlist['perms']['read']: - return {'playlist': None, 'position': index, 'id': playlist_id} + if not shared_playlist: + if error == "not_found": + await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}}) + return { + 'playlist': None, + 'position': index, + 'id': playlist_id, + 'is_shared': True, + 'owner_id': playlist['user'], + 'owner_playlist_id': playlist['referId'], + 'error': 'permission' if error in {'no_read', 'no_permission'} else None + } - return {'playlist': shared_playlist, 'position': index, 'id': playlist_id} + return { + 'playlist': shared_playlist, + 'position': index, + 'id': playlist_id, + 'is_shared': True, + 'owner_id': playlist['user'], + 'owner_playlist_id': playlist['referId'], + 'error': None + } - return {'playlist': playlist, 'position': index, 'id': playlist_id} + return { + 'playlist': playlist, + 'position': index, + 'id': playlist_id, + 'is_shared': False, + 'owner_id': ctx.author.id, + 'owner_playlist_id': playlist_id, + 'error': None + } - return {'playlist': None, 'position': None, 'id': None} + return { + 'playlist': None, + 'position': None, + 'id': None, + 'is_shared': False, + 'owner_id': ctx.author.id, + 'owner_playlist_id': None, + 'error': None + } async def search_playlist(url: str, requester: discord.Member, time_needed: bool = True) -> dict: """Search for playlist tracks from a URL.""" @@ -127,14 +205,15 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist # Handle shared playlist if playlist_type == 'share': - shared_playlist = await check_playlist_perms( + shared_playlist, error = await check_playlist_perms( ctx.author.id, playlist_data['user'], playlist_data['referId'] ) if not shared_playlist: - await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}}) + if error == "not_found": + await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}}) return None if shared_playlist['type'] == 'link': @@ -330,7 +409,7 @@ class Playlists(commands.Cog, name="playlist"): 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, "playlist.actions.remove", result["playlist"]["name"]) + return await send_localized_message(ctx, "playlist.actions.removed", result["playlist"]["name"]) @playlist.command(name="share", aliases=get_aliases("share")) @app_commands.describe( @@ -376,6 +455,70 @@ class Playlists(commands.Cog, name="playlist"): ) return await send_localized_message(ctx, "playlist.sharing.invitationSent", member) + @playlist.command(name="permission", aliases=get_aliases("permission")) + @app_commands.describe( + name="The name of the playlist.", + member="The user to grant or revoke permissions for.", + permission="The permission type: read, write, or remove.", + action="Whether to grant or revoke the permission." + ) + @app_commands.choices(permission=[ + app_commands.Choice(name="read", value="read"), + app_commands.Choice(name="write", value="write"), + app_commands.Choice(name="remove", value="remove") + ]) + @app_commands.choices(action=[ + app_commands.Choice(name="grant", value="grant"), + app_commands.Choice(name="revoke", value="revoke") + ]) + @app_commands.autocomplete(name=playlist_autocomplete) + @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) + async def permission(self, ctx: commands.Context, name: str, member: discord.Member, permission: str, action: str): + "Grant or revoke permissions for a playlist." + if member.id == ctx.author.id: + return await send_localized_message(ctx, 'playlist.permissions.cannotModifySelf', ephemeral=True) + if member.bot: + return await send_localized_message(ctx, 'playlist.sharing.sendErrorBot', ephemeral=True) + + result = await check_playlist(ctx, name.lower(), share=False) + if not result['playlist']: + return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) + + if result['playlist']['type'] in ['share', 'link']: + return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) + + perm_type = result['playlist'].get('perms', {}) + if permission not in perm_type: + return await send_localized_message(ctx, 'playlist.permissions.invalidPermission', ephemeral=True) + + perm_list = perm_type.get(permission, []) + if action == "grant": + if member.id in perm_list: + return await send_localized_message(ctx, 'playlist.permissions.alreadyGranted', member, permission, ephemeral=True) + + # Ensure user has read access first + if permission != 'read' and member.id not in perm_type.get('read', []): + return await send_localized_message(ctx, f'You haven\'t shared your playlist to {member.mention}', member, ephemeral=True) + + await MongoDBHandler.update_user(ctx.author.id, {"$push": {f"playlist.{result['id']}.perms.{permission}": member.id}}) + return await send_localized_message(ctx, 'playlist.permissions.granted', member, permission, result['playlist']['name']) + + elif action == "revoke": + if member.id not in perm_list: + return await send_localized_message(ctx, 'playlist.permissions.notGranted', member, permission, ephemeral=True) + + await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f"playlist.{result['id']}.perms.{permission}": member.id}}) + + # If revoking read, also revoke write and remove + if permission == 'read': + await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f"playlist.{result['id']}.perms.write": member.id}}) + await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f"playlist.{result['id']}.perms.remove": member.id}}) + + return await send_localized_message(ctx, 'playlist.permissions.revoked', member, permission, result['playlist']['name']) + + else: + return await send_localized_message(ctx, 'playlist.permissions.invalidAction', ephemeral=True) + @playlist.command(name="rename", aliases=get_aliases("rename")) @app_commands.describe( name="The name of your playlist.", @@ -445,10 +588,12 @@ class Playlists(commands.Cog, name="playlist"): @app_commands.autocomplete(name=playlist_autocomplete) async def add(self, ctx: commands.Context, name: str, query: str) -> None: "Add tracks in to your custom playlist." - result = await check_playlist(ctx, name.lower(), share=False) + result = await check_playlist(ctx, name.lower(), share=True, share_perm='write') if not result['playlist']: + if result.get('error') == 'permission': + return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True) return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) - if result['playlist']['type'] in ['share', 'link']: + if result['playlist']['type'] == 'link': return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) _, max_t, _ = Config().get_playlist_config() @@ -465,8 +610,11 @@ class Playlists(commands.Cog, name="playlist"): if results[0].is_stream: 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, 'playlist.actions.trackAdded', results[0].title, ctx.author, result['playlist']['name']) + owner_id = result.get('owner_id', ctx.author.id) + owner_playlist_id = result.get('owner_playlist_id', result['id']) + await MongoDBHandler.update_user(owner_id, {"$push": {f'playlist.{owner_playlist_id}.tracks': results[0].track_id}}) + owner_display = resolve_owner_display(ctx, owner_id) + await send_localized_message(ctx, 'playlist.actions.trackAdded', results[0].title, owner_display, result['playlist']['name']) @playlist.command(name="remove", aliases=get_aliases("remove")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -477,32 +625,41 @@ class Playlists(commands.Cog, name="playlist"): @app_commands.autocomplete(name=playlist_autocomplete) async def remove(self, ctx: commands.Context, name: str, position: int): "Remove song from your favorite playlist." - result = await check_playlist(ctx, name.lower(), share=False) + result = await check_playlist(ctx, name.lower(), share=True, share_perm='remove') if not result['playlist']: + if result.get('error') == 'permission': + return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True) return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) - if result['playlist']['type'] in ['link', 'share']: + if result['playlist']['type'] == 'link': return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) if not 0 < position <= len(result['playlist']['tracks']): return await send_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]}}) + owner_id = result.get('owner_id', ctx.author.id) + owner_playlist_id = result.get('owner_playlist_id', result['id']) + await MongoDBHandler.update_user(owner_id, {"$pull": {f'playlist.{owner_playlist_id}.tracks': result['playlist']['tracks'][position - 1]}}) track = voicelink.Track.decode(result['playlist']['tracks'][position - 1]) - await send_localized_message(ctx, 'playlist.actions.trackRemoved', track.get("title"), ctx.author, name) + owner_display = resolve_owner_display(ctx, owner_id) + await send_localized_message(ctx, 'playlist.actions.trackRemoved', track.get("title"), owner_display, name) @playlist.command(name="clear", aliases=get_aliases("clear")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @app_commands.autocomplete(name=playlist_autocomplete) async def clear(self, ctx: commands.Context, name: str) -> None: "Remove all songs from your favorite playlist." - result = await check_playlist(ctx, name.lower(), share=False) + result = await check_playlist(ctx, name.lower(), share=True, share_perm='remove') if not result['playlist']: + if result.get('error') == 'permission': + return await send_localized_message(ctx, 'playlist.errors.noAccess', ephemeral=True) return await send_localized_message(ctx, 'playlist.errors.notFound', name, ephemeral=True) - if result['playlist']['type'] in ['link', 'share']: + if result['playlist']['type'] == 'link': return await send_localized_message(ctx, 'playlist.errors.notAllowed', ephemeral=True) - await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}}) + owner_id = result.get('owner_id', ctx.author.id) + owner_playlist_id = result.get('owner_playlist_id', result['id']) + await MongoDBHandler.update_user(owner_id, {"$set": {f'playlist.{owner_playlist_id}.tracks': []}}) await send_localized_message(ctx, 'playlist.actions.cleared', name) @playlist.command(name="export", aliases=get_aliases("export")) diff --git a/langs/EN.json b/langs/EN.json index 2672819..ed3654d 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -205,6 +205,15 @@ "inbox": { "full": "Sorry! {0}'s inbox is full.", "noMessages": "There are no messages in your inbox." + }, + "permissions": { + "granted": "Granted **{1}** permission to {0} for playlist [`{2}`].", + "revoked": "Revoked **{1}** permission from {0} for playlist [`{2}`].", + "alreadyGranted": "{0} already has **{1}** permission for this playlist.", + "notGranted": "{0} does not have **{1}** permission for this playlist.", + "cannotModifySelf": "You cannot modify your own permissions.", + "invalidPermission": "Invalid permission type. Must be: read, write, or remove.", + "invalidAction": "Invalid action. Must be: grant or revoke." } }, "search": { From 9201ee730f7133570870685c8614b2cfc6b0dd56 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:33:15 +0800 Subject: [PATCH 38/52] fix: use correct disabled_vote key Updated references to 'common.status.disabled_vote' to use 'disabled_vote' directly in the settings cog. This simplifies the settings structure and ensures consistency when toggling and displaying the voting system status. --- cogs/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index 46939fc..c0c7663 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -122,8 +122,8 @@ 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('common.status.disabled_vote', True) - await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'common.status.disabled_vote': not toggle}}) + toggle = settings.get('disabled_vote', True) + await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'disabled_vote': not toggle}}) await send_localized_message(ctx, 'settings.actions.bypassVote', await LangHandler.get_lang(ctx.guild.id, "common.status.enabled" if not toggle else "common.status.disabled")) @settings.command(name="view", aliases=get_aliases("view")) @@ -145,7 +145,7 @@ class Settings(commands.Cog, name="settings"): settings.get('lang', LangHandler._default_lang), settings.get('controller', True), dj_role.name if dj_role else 'None', - settings.get('common.status.disabled_vote', False), + settings.get('disabled_vote', False), settings.get('24/7', False), settings.get('volume', 100), format_ms(settings.get('played_time', 0) * 60 * 1000), From 8fdceef4dbe777684d1726348e65ff68922dea8e Mon Sep 17 00:00:00 2001 From: Choco Date: Sat, 29 Nov 2025 11:20:21 +0800 Subject: [PATCH 39/52] Add user selection modal for playlist sharing Introduces a modal dialog to select a user when sharing a playlist, improving the user experience. Updates the PlaylistView to use the new BaseModal for sharing, and enhances BaseModal to support select components and store their values. --- cogs/playlist.py | 2 +- voicelink/views/playlist.py | 30 +++++++++++++++++++++++++++--- voicelink/views/utils/modal.py | 2 ++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index 4fe56c5..d0d8f20 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -473,7 +473,7 @@ class Playlists(commands.Cog, name="playlist"): ]) @app_commands.autocomplete(name=playlist_autocomplete) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) - async def permission(self, ctx: commands.Context, name: str, member: discord.Member, permission: str, action: str): + async def permission(self, ctx: commands.Context, member: discord.Member, name: str, permission: str, action: str): "Grant or revoke permissions for a playlist." if member.id == ctx.author.id: return await send_localized_message(ctx, 'playlist.permissions.cannotModifySelf', ephemeral=True) diff --git a/voicelink/views/playlist.py b/voicelink/views/playlist.py index a418955..a913bb1 100644 --- a/voicelink/views/playlist.py +++ b/voicelink/views/playlist.py @@ -28,7 +28,7 @@ from tldextract import extract from discord.ext import commands from typing import Any -from .utils import DynamicViewManager, Pagination +from .utils import DynamicViewManager, Pagination, BaseModal from .pagination import PaginationView from ..config import Config from ..utils import format_ms, truncate_string, dispatch_message @@ -144,7 +144,9 @@ class PlaylistView(PaginationView): async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: if isinstance(error, VoicelinkException): return await dispatch_message(interaction, content=getattr(error, 'original', error), ephemeral=True) - + + return await super().on_error(interaction, error, item) + async def update_message(self, interaction: discord.Interaction) -> None: """Update the view and edit the message with the new embed.""" self.update_view() @@ -168,7 +170,29 @@ class PlaylistView(PaginationView): @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() + modal = BaseModal( + title="Share Playlist", + custom_id="share_modal", + items=[ + discord.ui.Label( + text="User to share with", + description="Select a user to share with", + component=discord.ui.UserSelect( + custom_id="user_select", + placeholder="Select a user to share with", + required=True + ), + ) + ] + ) + await interaction.response.send_modal(modal) + await modal.wait() + + user = modal.values.get("user_select") + if not user: + return + + await interaction.client.get_command("playlist share")(self.primary_view.ctx, user[0], self.name) @discord.ui.button(label="Export", custom_id="export", style=discord.ButtonStyle.gray) async def export(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: diff --git a/voicelink/views/utils/modal.py b/voicelink/views/utils/modal.py index ea143be..ead346d 100644 --- a/voicelink/views/utils/modal.py +++ b/voicelink/views/utils/modal.py @@ -42,3 +42,5 @@ class BaseModal(discord.ui.Modal): for item in self.walk_children(): if isinstance(item, discord.ui.TextInput): self.values[item.custom_id] = item.value + elif isinstance(item, (discord.ui.Select, discord.ui.UserSelect, discord.ui.RoleSelect, discord.ui.ChannelSelect, discord.ui.MentionableSelect)): + self.values[item.custom_id] = item.values \ No newline at end of file From dfa06162718211bf80c26aa9de7640452d394573 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:56:11 +0800 Subject: [PATCH 40/52] Fix playlist creation message key Corrects the localization key from 'playlist.actions.create' to 'playlist.actions.created' when sending a message after playlist creation. --- cogs/playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index d0d8f20..4ad9614 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -391,7 +391,7 @@ class Playlists(commands.Cog, name="playlist"): 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, "playlist.actions.create", name) + await send_localized_message(ctx, "playlist.actions.created", name) @playlist.command(name="delete", aliases=get_aliases("delete")) @app_commands.describe(name="The name of the playlist.") From 88906b60eda295185443fa6981316ec3c91a19a0 Mon Sep 17 00:00:00 2001 From: Choco Date: Sun, 1 Mar 2026 22:26:08 +0800 Subject: [PATCH 41/52] Add channelId to voice state; upgrade discord.py for DAVE protocol Include channelId in the voice session payload to ensure correct voice state. Upgrade discord.py from 2.6.4 to 2.7.0 to comply with Discord's DAVE protocol requirements. --- requirements.txt | 2 +- voicelink/player.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 19417d4..c93bb82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.6.4 +discord.py==2.7.0 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 diff --git a/voicelink/player.py b/voicelink/player.py index e79e137..c54626c 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -350,6 +350,7 @@ class Player(VoiceProtocol): "token": state['event']['token'], "endpoint": state['event']['endpoint'], "sessionId": state['sessionId'], + "channelId": str(self.channel.id), } await self.send(method=RequestMethod.PATCH, data={"voice": data}) From 3492846699717ca4faedd3cc705c7178deadddf8 Mon Sep 17 00:00:00 2001 From: Choco Date: Fri, 20 Mar 2026 00:06:57 +0800 Subject: [PATCH 42/52] Use configured default playlist name Set the new-user default playlist name from default_playlist_name in voicelink/config.py (fallback to Favourite) instead of hardcoding it. --- voicelink/mongodb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/voicelink/mongodb.py b/voicelink/mongodb.py index 5fb8346..d531c25 100644 --- a/voicelink/mongodb.py +++ b/voicelink/mongodb.py @@ -29,6 +29,8 @@ import logging from typing import Any, Dict, Optional, Literal, TypedDict, List from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection +from .config import Config + logger: logging.Logger = logging.getLogger("vocard.db") # Type definitions for better code clarity @@ -406,6 +408,7 @@ class MongoDBHandler: user = await cls._users_db.find_one({"_id": user_id}) if not user: user = {**copy.deepcopy(cls._user_base), "_id": user_id} + user["playlist"]["200"]["name"] = Config().get_playlist_config()[2] try: await cls._users_db.insert_one(user) except Exception as e: From 19618a1726927fa25cd0e139c09b881a2640c687 Mon Sep 17 00:00:00 2001 From: Choco Date: Sat, 21 Mar 2026 23:40:12 +0800 Subject: [PATCH 43/52] Add per-player inactive cleanup timer and config Adds configurable inactive cleanup timing and per-player timer logic so idle players disconnect or pause according to 24/7 and voice-channel state, replacing immediate teardown when the queue finishes. --- cogs/listeners.py | 15 ++++++++-- cogs/task.py | 48 ++++--------------------------- settings Example.json | 5 ++++ voicelink/config.py | 1 + voicelink/player.py | 67 +++++++++++++++++++++++++++++++++++++++---- 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index 44b2da3..391e1ce 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -173,9 +173,20 @@ class Listeners(commands.Cog): is_joined = False if is_joined and player.settings.get("24/7", False): - if player.is_paused and len([m for m in player.channel.members if not m.bot]) == 1: + if player.is_paused and len([m for m in player.channel.members if not m.bot or not m.voice.self_deaf]) == 1: await player.set_pause(False, member) - + + if not is_joined: + if not player.is_paused and len([m for m in player.channel.members if not m.bot or not m.voice.self_deaf]) == 0: + player._schedule_inactive_cleanup_timer() + + # if dj is not in the channel, find a new DJ + if player.dj not in player.channel.members: + for m in player.channel.members: + if not m.bot or not m.voice.self_deaf: + player.dj = m + break + if player.is_ipc_connected: await player._ipc_client.send({ "op": "updateGuild", diff --git a/cogs/task.py b/cogs/task.py index fd0f030..8975d5e 100644 --- a/cogs/task.py +++ b/cogs/task.py @@ -25,13 +25,13 @@ import voicelink import discord import function as func +from voicelink.config import Config from discord.ext import commands, tasks class Task(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.activity_update.start() - self.player_check.start() self.cache_cleaner.start() self.current_act = 0 @@ -39,15 +39,14 @@ class Task(commands.Cog): def cog_unload(self): self.activity_update.cancel() - self.player_check.cancel() self.cache_cleaner.cancel() - @tasks.loop(minutes=10.0) + @tasks.loop(seconds=Config().timer_settings.get("bot_activity_update", 600)) async def activity_update(self): await self.bot.wait_until_ready() try: - act_data = voicelink.Config().activity[(self.current_act + 1) % len(voicelink.Config().activity) - 1] + act_data = Config().activity[(self.current_act + 1) % len(Config().activity) - 1] act_original = self.bot.activity act_type = getattr(discord.ActivityType, act_data.get("type", "").lower(), discord.ActivityType.playing) act_name = self.placeholder.replace(act_data.get("name", "")) @@ -57,51 +56,14 @@ 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(voicelink.Config().activity) + self.current_act = (self.current_act + 1) % len(Config().activity) func.logger.info(f"Changed the bot status to {act_name}") except Exception as e: func.logger.error("Error occurred while changing the bot status!", exc_info=e) - @tasks.loop(minutes=5.0) - async def player_check(self): - for identifier, node in voicelink.NodePool._nodes.items(): - for guild_id, player in node._players.copy().items(): - try: - if not player.channel or not player.context or not player.guild: - await player.teardown() - continue - except: - await player.teardown() - continue - - try: - members = player.channel.members - if (not player.is_playing and player.queue.is_empty) or not any(False if member.bot or member.voice.self_deaf else True for member in members): - if not player.settings.get('24/7', False): - await player.teardown() - continue - else: - if not player.is_paused: - await player.set_pause(True) - else: - if not player.guild.me: - await player.teardown() - continue - elif not player.guild.me.voice: - await player.connect(timeout=0.0, reconnect=True) - - if player.dj not in members: - for m in members: - if not m.bot: - player.dj = m - break - - except Exception as e: - func.logger.error("Error occurred while checking the player!", exc_info=e) - - @tasks.loop(hours=12.0) + @tasks.loop(seconds=Config().timer_settings.get("cache_cleanup", 43200)) async def cache_cleaner(self): await voicelink.MongoDBHandler.cleanup_cache() diff --git a/settings Example.json b/settings Example.json index af423cb..8ac45ed 100644 --- a/settings Example.json +++ b/settings Example.json @@ -49,6 +49,11 @@ "secure": false, "enable": false }, + "timer_settings": { + "bot_activity_update": 600, + "inactive_player_cleanup": 600, + "cache_cleanup": 43200 + }, "playlist_settings": { "max_playlist": 5, "max_tracks_per_playlist": 500, diff --git a/voicelink/config.py b/voicelink/config.py index ff6de26..efe6214 100644 --- a/voicelink/config.py +++ b/voicelink/config.py @@ -98,6 +98,7 @@ class Config: self.lyrics_platform: str = settings.get("lyrics_platform", "A_ZLyrics").lower() self.ipc_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {}) self.playlist_settings: Dict[str, Union[str, int]] = settings.get("playlist_settings", {}) + self.timer_settings: Dict[str, int] = settings.get("timer_settings", {}) self.version: str = settings.get("version", "") self.initialized = True diff --git a/voicelink/player.py b/voicelink/player.py index c54626c..a60de2f 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -23,10 +23,9 @@ SOFTWARE. from __future__ import annotations -import time, logging +import time, logging, asyncio from math import ceil -from asyncio import sleep from random import shuffle, choice from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING @@ -155,6 +154,7 @@ class Player(VoiceProtocol): self._ph = PlayerPlaceholder(client, self) self._logger: Optional[logging.Logger] = self._node._logger + self._inactive_cleanup_task: Optional[asyncio.Task[None]] = None def __repr__(self): return ( @@ -238,10 +238,12 @@ class Player(VoiceProtocol): @property def autoplay(self) -> bool: + """Indicates whether the player is set to autoplay.""" return self.settings.get("autoplay", False) @property def data(self) -> dict: + """Returns a dictionary containing the player's data.""" return { "guild_id": self._guild.id, "channel_id": self.channel.id, @@ -405,7 +407,7 @@ class Player(VoiceProtocol): self._paused = False if self._track_is_stuck: - await sleep(10) + await asyncio.sleep(10) self._track_is_stuck = False if not self.guild.me.voice: @@ -423,12 +425,14 @@ class Player(VoiceProtocol): if not track: if self.autoplay and await self.get_recommendations(): return await self.do_next() + if self.queue.is_empty: + self._schedule_inactive_cleanup_timer() else: try: await self.play(track, start=track.position) except Exception as e: self._logger.error(f"Something went wrong while playing music in {self.guild.name}({self.guild.id})", exc_info=e) - await sleep(5) + await asyncio.sleep(5) return await self.do_next() if not track.requester.bot: @@ -515,6 +519,7 @@ class Player(VoiceProtocol): try: await self.update_voice_status(remove_status=True) + self._cancel_inactive_cleanup_timer() if self.controller: if self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"): await self.controller.edit(embed=self.build_embed(), view=None) @@ -630,6 +635,21 @@ class Player(VoiceProtocol): track.position = start_time track.end_time = end_time + def _cancel_inactive_cleanup_timer(self) -> None: + """Cancels the per-player inactivity cleanup timer (if any).""" + task: Optional[asyncio.Task] = getattr(self, "_inactive_cleanup_task", None) + if task and not task.done(): + task.cancel() + self._inactive_cleanup_task = None + + def _schedule_inactive_cleanup_timer(self) -> None: + """Schedules per-player cleanup after inactivity.""" + self._cancel_inactive_cleanup_timer() + seconds = Config().timer_settings.get("inactive_player_cleanup", 600) + self._inactive_cleanup_task = self.bot.loop.create_task( + self._inactive_cleanup_timer_worker(seconds) + ) + async def add_track(self, raw_tracks: Union[Track, List[Track]], *, start_time: int = 0, end_time: int = 0, at_front: bool = False, duplicate: bool = True) -> int: """Adds one or more tracks to the queue.""" tracks: List[Track] = [] @@ -656,6 +676,8 @@ class Player(VoiceProtocol): finally: if tracks: + if self.channel.members and len([m for m in self.channel.members if not m.bot]) > 0: + self._cancel_inactive_cleanup_timer() if self.is_ipc_connected: await self.send_ws({"op": "addTrack", "tracks": [track.track_id for track in tracks], "position": -1 if is_list else position}, tracks[0].requester) @@ -900,4 +922,39 @@ class Player(VoiceProtocol): payload['guildId'] = str(self.guild.id) if requester: payload['requesterId'] = str(requester.id) - await self._ipc_client.send(payload) \ No newline at end of file + await self._ipc_client.send(payload) + + async def _inactive_cleanup_timer_worker(self, seconds: int) -> None: + try: + await asyncio.sleep(seconds) + if not self._guild or not self._node: + return + if self._guild.id not in self._node._players: + return + + members = self.channel.members if self.channel else [] + has_non_bot = any(not m.bot or not m.voice.self_deaf for m in members) + # Empty channel: always pause/teardown. If people remain: only when idle (not playing + empty queue). + if has_non_bot and (self.is_playing or not self.queue.is_empty): + return + + self._inactive_cleanup_task = None + + if self.settings.get("24/7", False): + if not self.is_paused: + await self.set_pause(True) + else: + await self.teardown() + except asyncio.CancelledError: + return + except Exception as e: + # Logger is best-effort; timer cleanup should never crash the loop. + try: + guild_id = self._guild.id if self._guild else "unknown" + except Exception: + guild_id = "unknown" + if self._logger: + self._logger.error( + f"Inactive cleanup timer failed for guild {guild_id}", + exc_info=e + ) \ No newline at end of file From 28cb2a910e88bdf1026162f1ab0126289b6b2210 Mon Sep 17 00:00:00 2001 From: Choco Date: Sat, 21 Mar 2026 23:40:29 +0800 Subject: [PATCH 44/52] Fix typo: max_playlists Corrects the playlist settings key so it matches the code and config (max_playlists), avoiding mismatched or ignored limits when reading playlist settings. --- settings Example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings Example.json b/settings Example.json index 8ac45ed..a504077 100644 --- a/settings Example.json +++ b/settings Example.json @@ -55,7 +55,7 @@ "cache_cleanup": 43200 }, "playlist_settings": { - "max_playlist": 5, + "max_playlists": 5, "max_tracks_per_playlist": 500, "default_playlist_name": "Favourite" }, From 8bb34326d10de3a5427c444fa149c477f33bef78 Mon Sep 17 00:00:00 2001 From: Choco Date: Sun, 22 Mar 2026 12:09:34 +0800 Subject: [PATCH 45/52] Add settings reset command for per-setting defaults Introduces a settings reset subcommand with choices so admins can clear one guild override at a time via MongoDB $unset, with live player sync for volume and duplicate-track defaults. Adds settings.actions.resetDone across language files and optional reset aliases (restore, defaults) in settings examples. --- cogs/settings.py | 39 +++++++++++++++++++++++++++++++++++++++ langs/DE.json | 4 +++- langs/EN.json | 4 +++- langs/ES.json | 4 +++- langs/FR.json | 4 +++- langs/JA.json | 4 +++- langs/KO.json | 4 +++- langs/PL.json | 4 +++- langs/RU.json | 4 +++- langs/UA.json | 4 +++- langs/VN.json | 4 +++- langs/ZHCN.json | 4 +++- langs/ZHTW.json | 4 +++- local_langs/es-419.json | 5 ++++- local_langs/es-ES.json | 5 ++++- local_langs/zh-CN.json | 5 ++++- local_langs/zh-TW.json | 5 ++++- update.py | 2 +- 18 files changed, 92 insertions(+), 17 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index c0c7663..7eaaa62 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -38,6 +38,23 @@ from voicelink.views import DebugView, HelpView, EmbedBuilderView from voicelink.placeholders import PlayerPlaceholder from voicelink.utils import format_ms, format_bytes, dispatch_message, send_localized_message +_RESET_LABELS: dict[str, str] = { + "prefix": "prefix", + "language": "lang", + "dj role": "dj", + "queue mode": "queue_type", + "24/7 mode": "24/7", + "vote bypass": "disabled_vote", + "default volume": "volume", + "music controller": "controller", + "duplicate tracks": "duplicate_track", + "controller messages": "controller_msg", + "silent messages": "silent_msg", + "stage announce template": "stage_announce_template", + "song request channel": "music_request_channel", + "custom controller embeds": "default_controller" +} + def status_icon(status: bool) -> str: return "✅" if status else "❌" @@ -295,6 +312,28 @@ class Settings(commands.Cog, name="settings"): }}}) await send_localized_message(ctx, "songRequest.channelCreated", channel.mention) + @settings.command(name="reset", aliases=get_aliases("reset")) + @app_commands.describe(setting="Which setting to restore to defaults (removes this guild override)") + @app_commands.choices(setting=[ + app_commands.Choice(name=label.capitalize(), value=label) + for label in _RESET_LABELS.keys() + ]) + @commands.has_permissions(manage_guild=True) + @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) + async def reset_setting(self, ctx: commands.Context, *, setting: str): + "Remove one guild setting override so the bot uses its default again." + setting_key = _RESET_LABELS.get(setting.lower()) + if not setting_key: + return await send_localized_message(ctx, "settings.actions.resetNotFound", ", ".join(_RESET_LABELS.keys())) + + await MongoDBHandler.update_settings(ctx.guild.id, {"$unset": {setting_key: ""}}) + player: voicelink.Player = ctx.guild.voice_client + if setting_key == "volume" and player: + await player.set_volume(100, ctx.author) + elif setting_key == "duplicate_track" and player: + player.queue._allow_duplicate = True + await send_localized_message(ctx, "settings.actions.resetDone", setting.capitalize()) + @app_commands.command(name="debug") async def debug(self, interaction: discord.Interaction): if interaction.user.id not in voicelink.Config().bot_access_user: diff --git a/langs/DE.json b/langs/DE.json index 2eae5e3..62c4223 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -121,7 +121,9 @@ "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." + "silentMsgToggled": "Sie haben {0} stille Nachrichten.", + "resetDone": "`{0}` wurde auf die Standardwerte des Servers zurückgesetzt.", + "resetNotFound": "Ungültiger Einstellungsname. Bitte verwende eine der folgenden Einstellungen: {0}" }, "menu": "Servereinstellungen | {0}", "basic": { diff --git a/langs/EN.json b/langs/EN.json index ed3654d..ba729ed 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -121,7 +121,9 @@ "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." + "silentMsgToggled": "You have {0} silent messaging.", + "resetDone": "Reset `{0}` to server defaults.", + "resetNotFound": "Invalid setting name. Please use one of the following: {0}" }, "menu": "Server Settings | {0}", "basic": { diff --git a/langs/ES.json b/langs/ES.json index d3b5d41..8f6dc38 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -121,7 +121,9 @@ "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." + "silentMsgToggled": "Tienes {0} mensajes silenciosos.", + "resetDone": "Reset `{0}` to server defaults.", + "resetNotFound": "Nombre de opción inválido. Por favor, use una de las siguientes opciones: {0}" }, "menu": "Configuración del servidor | {0}", "basic": { diff --git a/langs/FR.json b/langs/FR.json index 813f389..08383d2 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -121,7 +121,9 @@ "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." + "silentMsgToggled": "Vous avez {0} messages silencieux.", + "resetDone": "`{0}` a été réinitialisé aux paramètres par défaut du serveur.", + "resetNotFound": "Nom d'option invalide. Veuillez utiliser une des options suivantes: {0}" }, "menu": "Paramètres du serveur | {0}", "basic": { diff --git a/langs/JA.json b/langs/JA.json index 891a329..3552488 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -121,7 +121,9 @@ "controllerToggled": "今、音楽コントローラーは「{0}」です。", "duplicateTrackToggled": "今、キュー内の重複トラックを防止するための設定は「{0}」です。", "controllerMsgToggled": "現在、音楽コントローラーから `{0}` 件のメッセージがあります。", - "silentMsgToggled": "あなたは{0}サイレントメッセージを持っています。" + "silentMsgToggled": "あなたは{0}サイレントメッセージを持っています。", + "resetDone": "`{0}`をサーバーのデフォルトにリセットしました。", + "resetNotFound": "無効な設定名です。次のいずれかを使用してください: {0}" }, "menu": "サーバー設定| {0}", "basic": { diff --git a/langs/KO.json b/langs/KO.json index e3445a1..bd1d813 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -121,7 +121,9 @@ "controllerToggled": "이제 음악 컨트롤러가 `{0}`(으)로 설정되었습니다.", "duplicateTrackToggled": "이제 대기열에서 중복된 트랙을 방지하기 위해 `{0}`(으)로 설정되었습니다.", "controllerMsgToggled": "이제 음악 컨트롤러로부터 `{0}` 메시지가 있습니다.", - "silentMsgToggled": "당신은 {0} 침묵 메시지를 사용하고 있습니다." + "silentMsgToggled": "당신은 {0} 침묵 메시지를 사용하고 있습니다.", + "resetDone": "`{0}`을(를) 서버 기본값으로 초기화했습니다.", + "resetNotFound": "잘못된 설정 이름입니다. 다음 중 하나를 사용하세요: {0}" }, "menu": "서버 설정 | {0}", "basic": { diff --git a/langs/PL.json b/langs/PL.json index 2b43c8a..8b97d2a 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -121,7 +121,9 @@ "controllerToggled": "Kontroler muzyki: `{0}`", "duplicateTrackToggled": "Unikanie duplikowania pozycji w kolejce: `{0}`", "controllerMsgToggled": "Wiadomości od kontrolera muzyki: `{0}`", - "silentMsgToggled": "Masz {0} ciche wiadomości." + "silentMsgToggled": "Masz {0} ciche wiadomości.", + "resetDone": "`{0}` zostało zresetowane na domyślne ustawienia serwera.", + "resetNotFound": "Nieprawidłowa nazwa ustawienia. Proszę użyć jednej z następujących opcji: {0}" }, "menu": "Ustawnienia serwera | {0}", "basic": { diff --git a/langs/RU.json b/langs/RU.json index ab51b37..b4957d3 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -121,7 +121,9 @@ "controllerToggled": "Теперь у вас `{0}` контроллер музыки.", "duplicateTrackToggled": "Теперь у вас `{0}` фильтр дубликатов в очереди.", "controllerMsgToggled": "Теперь у вас `{0}` сообщения от плеера.", - "silentMsgToggled": "У вас {0} тихих сообщений." + "silentMsgToggled": "У вас {0} тихих сообщений.", + "resetDone": "`{0}` установлено на значения по умолчанию сервера.", + "resetNotFound": "Неверное имя настройки. Пожалуйста, используйте одно из следующих значений: {0}" }, "menu": "Настройки сервера | {0}", "basic": { diff --git a/langs/UA.json b/langs/UA.json index 0b5e385..bbdf7cb 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -121,7 +121,9 @@ "controllerToggled": "Тепер у вас `{0}` контролер музики.", "duplicateTrackToggled": "Тепер у вас `{0}` запобігання дублюванню треку в черзі.", "controllerMsgToggled": "Тепер у вас `{0}` повідомлення від контролера музики.", - "silentMsgToggled": "У вас {0} тихих повідомлень." + "silentMsgToggled": "У вас {0} тихих повідомлень.", + "resetDone": "`{0}` встановлено на значення за замовчуванням сервера.", + "resetNotFound": "Неправильна назва налаштування. Будь ласка, використовуйте одне з наведених нижче значень: {0}" }, "menu": "Налаштування сервера | {0}", "basic": { diff --git a/langs/VN.json b/langs/VN.json index 2574761..4112bc3 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -121,7 +121,9 @@ "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." + "silentMsgToggled": "Bạn có {0} tin nhắn im lặng.", + "resetDone": "Reset `{0}` to server defaults.", + "resetNotFound": "Tên cài đặt không hợp lệ. Vui lòng sử dụng một trong các tùy chọn sau: {0}" }, "menu": "Cài Đặt Máy Chủ | {0}", "basic": { diff --git a/langs/ZHCN.json b/langs/ZHCN.json index a506635..a361a59 100644 --- a/langs/ZHCN.json +++ b/langs/ZHCN.json @@ -121,7 +121,9 @@ "controllerToggled": "您现在已 `{0}` 音乐控制器。", "duplicateTrackToggled": "您已 `{0}` 重复曲目预防功能。", "controllerMsgToggled": "您现在已 `{0}` 来自音乐控制器的消息。", - "silentMsgToggled": "您已 {0} 静默消息。" + "silentMsgToggled": "您已 {0} 静默消息。", + "resetDone": "`{0}` 已重置为服务器默认值。", + "resetNotFound": "无效的设置名称。请使用以下之一: {0}" }, "menu": "服务器设置 | {0}", "basic": { diff --git a/langs/ZHTW.json b/langs/ZHTW.json index 5293eeb..27d45e5 100644 --- a/langs/ZHTW.json +++ b/langs/ZHTW.json @@ -121,7 +121,9 @@ "controllerToggled": "您現在已 `{0}` 音樂控制器。", "duplicateTrackToggled": "您已 `{0}` 重複曲目防止功能。", "controllerMsgToggled": "您現在已 `{0}` 來自音樂控制器的訊息。", - "silentMsgToggled": "您已 {0} 靜音訊息。" + "silentMsgToggled": "您已 {0} 靜音訊息。", + "resetDone": "`{0}` 已重置為伺服器預設值。", + "resetNotFound": "無效的設定名稱。請使用以下之一: {0}" }, "menu": "伺服器設定 | {0}", "basic": { diff --git a/local_langs/es-419.json b/local_langs/es-419.json index c5f616e..08342be 100644 --- a/local_langs/es-419.json +++ b/local_langs/es-419.json @@ -232,5 +232,8 @@ "stageannounce": "anuncio_escenario", "Soundcloud": "Soundcloud", "Fairqueue":"ColaJusta", - "togglecontroller":"cambiarcontrolador" + "togglecontroller":"cambiarcontrolador", + "reset": "reiniciar", + "Which setting to restore to defaults (removes this guild override)": "Qué configuración restaurar a los valores por defecto (elimina esta sobreescritura de servidor)", + "Remove one guild setting override so the bot uses its default again.": "Elimina una sobreescritura de configuración de servidor para que el bot use sus valores por defecto nuevamente." } diff --git a/local_langs/es-ES.json b/local_langs/es-ES.json index 831a9a3..ac5fdb9 100644 --- a/local_langs/es-ES.json +++ b/local_langs/es-ES.json @@ -232,5 +232,8 @@ "stageannounce": "anuncio_escenario", "Soundcloud": "Soundcloud", "Fairqueue":"ColaJusta", - "togglecontroller":"cambiarcontrolador" + "togglecontroller":"cambiarcontrolador", + "reset": "reiniciar", + "Which setting to restore to defaults (removes this guild override)": "Qué configuración restaurar a los valores por defecto (elimina esta sobreescritura de servidor)", + "Remove one guild setting override so the bot uses its default again.": "Elimina una sobreescritura de configuración de servidor para que el bot use sus valores por defecto nuevamente." } diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json index 43e37a6..b8d1bcb 100644 --- a/local_langs/zh-CN.json +++ b/local_langs/zh-CN.json @@ -232,5 +232,8 @@ "stageannounce": "舞台公告", "Soundcloud": "SoundCloud", "Fairqueue": "公平队列", - "togglecontroller": "切换控制器" + "togglecontroller": "切换控制器", + "reset": "重置", + "Which setting to restore to defaults (removes this guild override)": "要重置为默认值的设置(移除此服务器覆盖)", + "Remove one guild setting override so the bot uses its default again.": "移除一个服务器设置覆盖,使机器人使用其默认值。" } \ No newline at end of file diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json index b0ee859..70de513 100644 --- a/local_langs/zh-TW.json +++ b/local_langs/zh-TW.json @@ -232,5 +232,8 @@ "stageannounce": "舞台公告", "Soundcloud": "Soundcloud", "Fairqueue": "公平隊列", - "togglecontroller": "切換控制器" + "togglecontroller": "切換控制器", + "reset": "重置", + "Which setting to restore to defaults (removes this guild override)": "要重置為預設值的設定(移除此伺服器覆蓋)", + "Remove one guild setting override so the bot uses its default again.": "移除一個伺服器設定覆蓋,使機器人使用其預設值。" } \ No newline at end of file diff --git a/update.py b/update.py index 330125e..424bf56 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.3b1" +__version__ = "v2.7.3b2" # URLs for update and migration PYTHON_CMD_NAME = os.path.basename(sys.executable) From ae8dfbc1b6c596f2bff981f9e6b9ab1c3da3fab5 Mon Sep 17 00:00:00 2001 From: Choco Date: Sun, 22 Mar 2026 13:58:40 +0800 Subject: [PATCH 46/52] Fixed translation to meet the discord requirements --- cogs/settings.py | 2 +- local_langs/es-419.json | 4 ++-- local_langs/es-ES.json | 4 ++-- local_langs/zh-CN.json | 2 +- local_langs/zh-TW.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index 7eaaa62..888f0e0 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -313,7 +313,7 @@ class Settings(commands.Cog, name="settings"): await send_localized_message(ctx, "songRequest.channelCreated", channel.mention) @settings.command(name="reset", aliases=get_aliases("reset")) - @app_commands.describe(setting="Which setting to restore to defaults (removes this guild override)") + @app_commands.describe(setting="Which setting to restore to defaults.") @app_commands.choices(setting=[ app_commands.Choice(name=label.capitalize(), value=label) for label in _RESET_LABELS.keys() diff --git a/local_langs/es-419.json b/local_langs/es-419.json index 08342be..24465ec 100644 --- a/local_langs/es-419.json +++ b/local_langs/es-419.json @@ -234,6 +234,6 @@ "Fairqueue":"ColaJusta", "togglecontroller":"cambiarcontrolador", "reset": "reiniciar", - "Which setting to restore to defaults (removes this guild override)": "Qué configuración restaurar a los valores por defecto (elimina esta sobreescritura de servidor)", - "Remove one guild setting override so the bot uses its default again.": "Elimina una sobreescritura de configuración de servidor para que el bot use sus valores por defecto nuevamente." + "Which setting to restore to defaults": "Qué configuración restaurar a los valores por defecto.", + "Remove one guild setting override so the bot uses its default again.": "Restablece una opción del servidor a su valor predeterminado." } diff --git a/local_langs/es-ES.json b/local_langs/es-ES.json index ac5fdb9..6b3e561 100644 --- a/local_langs/es-ES.json +++ b/local_langs/es-ES.json @@ -234,6 +234,6 @@ "Fairqueue":"ColaJusta", "togglecontroller":"cambiarcontrolador", "reset": "reiniciar", - "Which setting to restore to defaults (removes this guild override)": "Qué configuración restaurar a los valores por defecto (elimina esta sobreescritura de servidor)", - "Remove one guild setting override so the bot uses its default again.": "Elimina una sobreescritura de configuración de servidor para que el bot use sus valores por defecto nuevamente." + "Which setting to restore to defaults": "Qué configuración restaurar a los valores por defecto.", + "Remove one guild setting override so the bot uses its default again.": "Restablece una opción del servidor a su valor predeterminado." } diff --git a/local_langs/zh-CN.json b/local_langs/zh-CN.json index b8d1bcb..34b5d64 100644 --- a/local_langs/zh-CN.json +++ b/local_langs/zh-CN.json @@ -234,6 +234,6 @@ "Fairqueue": "公平队列", "togglecontroller": "切换控制器", "reset": "重置", - "Which setting to restore to defaults (removes this guild override)": "要重置为默认值的设置(移除此服务器覆盖)", + "Which setting to restore to defaults": "要重置为默认值的设置。", "Remove one guild setting override so the bot uses its default again.": "移除一个服务器设置覆盖,使机器人使用其默认值。" } \ No newline at end of file diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json index 70de513..d6e354e 100644 --- a/local_langs/zh-TW.json +++ b/local_langs/zh-TW.json @@ -234,6 +234,6 @@ "Fairqueue": "公平隊列", "togglecontroller": "切換控制器", "reset": "重置", - "Which setting to restore to defaults (removes this guild override)": "要重置為預設值的設定(移除此伺服器覆蓋)", + "Which setting to restore to defaults": "要重置為預設值的設定。", "Remove one guild setting override so the bot uses its default again.": "移除一個伺服器設定覆蓋,使機器人使用其預設值。" } \ No newline at end of file From 56a6b67470b10514d8a8ad878b4ac445ca3d86e4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:01:53 +0800 Subject: [PATCH 47/52] Make messages ephemeral and bump discord.py Make certain bot responses ephemeral to avoid spamming channels (debug command and missing-permission reply). Bump discord.py from 2.7.0 to 2.7.1 in requirements. Remove an unused Config().get_playlist_config() assignment in updatePlaylist to clean up dead code. --- cogs/settings.py | 2 +- main.py | 2 +- requirements.txt | 2 +- voicelink/ipc/methods.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index 888f0e0..4815334 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -337,7 +337,7 @@ class Settings(commands.Cog, name="settings"): @app_commands.command(name="debug") async def debug(self, interaction: discord.Interaction): 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!") + return await interaction.response.send_message("You are not able to use this command!", ephemeral=True) memory = psutil.virtual_memory() disk = psutil.disk_usage(func.ROOT_DIR) diff --git a/main.py b/main.py index 417437a..b2b29b9 100644 --- a/main.py +++ b/main.py @@ -191,7 +191,7 @@ class CommandCheck(discord.app_commands.CommandTree): channel_perm = interaction.channel.permissions_for(interaction.guild.me) if not channel_perm.read_messages or not channel_perm.send_messages: - await interaction.response.send_message("I don't have permission to read or send messages in this channel.") + await interaction.response.send_message("I don't have permission to read or send messages in this channel.", ephemeral=True) return False return True diff --git a/requirements.txt b/requirements.txt index c93bb82..acd6509 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.7.0 +discord.py==2.7.1 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 diff --git a/voicelink/ipc/methods.py b/voicelink/ipc/methods.py index 886bc41..0fcb766 100644 --- a/voicelink/ipc/methods.py +++ b/voicelink/ipc/methods.py @@ -508,7 +508,6 @@ 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') - 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) From ce6ef5d11ee61f803687f2f4b9ccb92933bf1925 Mon Sep 17 00:00:00 2001 From: Choco Date: Sun, 5 Apr 2026 23:55:50 +0800 Subject: [PATCH 48/52] Defer interaction responses in commands Call await ctx.defer() for discord.Interaction in play, search and playtop to avoid interaction timeouts during long operations. Also move the MongoDB get_user playlist fetch in check_playlist to after the defer so DB work doesn't occur before the interaction is deferred. (cogs/basic.py, cogs/playlist.py) --- cogs/basic.py | 9 +++++++++ cogs/playlist.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 47ac547..dc98a71 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -122,6 +122,9 @@ class Basic(commands.Cog): @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def play(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0") -> None: "Loads your input into the queue." + if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done(): + await ctx.defer() + player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) @@ -216,6 +219,9 @@ class Basic(commands.Cog): @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def search(self, ctx: commands.Context, *, query: str, platform: str = Config().search_platform.name): "Searches your query and displays the results." + if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done(): + await ctx.defer() + player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) @@ -259,6 +265,9 @@ class Basic(commands.Cog): @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def playtop(self, ctx: commands.Context, *, query: str, start: str = "0", end: str = "0"): "Adds a song with the given url or query on the top of the queue." + if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done(): + await ctx.defer() + player: voicelink.Player = ctx.guild.voice_client if not player: player = await voicelink.connect_channel(ctx) diff --git a/cogs/playlist.py b/cogs/playlist.py index 4ad9614..9c56596 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -86,11 +86,11 @@ async def check_playlist( share_perm: Optional[str] = None ) -> dict: """Get user's playlist data with various filtering options.""" - user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist') - if isinstance(ctx, discord.Interaction) and not ctx.interaction.response.is_done(): await ctx.defer() + user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist') + if full: return user_playlists From 629eb041f61454c2fd67629a0116f6fc8f98ba8b Mon Sep 17 00:00:00 2001 From: Choco Date: Mon, 13 Apr 2026 22:59:20 +0800 Subject: [PATCH 49/52] Use player.controls key for autoplay Replace the 'autoplay' translation key with 'player.controls.autoplay' when sending the autoplay status message. Behavior is unchanged: the setting is toggled, a localized enabled/disabled message is sent, and do_next() is called if the player isn't playing. --- voicelink/views/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicelink/views/controller.py b/voicelink/views/controller.py index 5e3677f..7ac2189 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -279,7 +279,7 @@ class AutoPlay(ControlButton): 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, 'common.status.enabled' if check else "common.status.disabled")) + await self.send(interaction, 'player.controls.autoplay', await LangHandler.get_lang(interaction.guild_id, 'common.status.enabled' if check else "common.status.disabled")) if not self.player.is_playing: await self.player.do_next() From 47ec3d212a71b978cff3fd41f8b96e47cbf4e9e6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:57:08 +0800 Subject: [PATCH 50/52] Refactor controller UI and bump version Refactor interactive controller components and tweak command signatures. - Make lyrics command accept keyword-only args (adds a leading * for title/artist). - Bump version to v2.7.3b3. - Import dispatch_message and add send_embed helper to ControlButton for embed responses. - Introduce ControlSelect (base for selects) and refactor Tracks/Effects to inherit it, pass player explicitly and fix placeholder/player usage. - Namespace localization keys (e.g. player.controls.forward/rewind) and defer interaction responses where appropriate. - Replace direct send_localized_message calls with ControlSelect/ControlButton send wrappers to honor controller message settings. - Add additional on_error handling for VoicelinkException and ensure non-ephemeral message deletion behavior follows controller settings. These changes improve consistency for UI components, interaction handling, and localization usage. --- cogs/basic.py | 2 +- update.py | 2 +- voicelink/views/controller.py | 82 +++++++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index dc98a71..2984f10 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -790,7 +790,7 @@ class Basic(commands.Cog): @commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics")) @app_commands.describe(title="Searches for your query and displays the reutned lyrics.") @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) - async def lyrics(self, ctx: commands.Context, title: str = "", artist: str = ""): + async def lyrics(self, ctx: commands.Context, *, title: str = "", artist: str = ""): "Displays lyrics for the playing track." if not title: player: voicelink.Player = ctx.guild.voice_client diff --git a/update.py b/update.py index 424bf56..d14586f 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.3b2" +__version__ = "v2.7.3b3" # URLs for update and migration PYTHON_CMD_NAME = os.path.basename(sys.executable) diff --git a/voicelink/views/controller.py b/voicelink/views/controller.py index 7ac2189..b15685c 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -29,7 +29,7 @@ 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 ..utils import format_ms, send_localized_message, dispatch_message from ..language import LangHandler from ..mongodb import MongoDBHandler @@ -87,6 +87,34 @@ class ControlButton(discord.ui.Button): ephemeral=ephemeral ) + async def send_embed(self, interaction: discord.Interaction, embed: discord.Embed, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None: + stay = self.player.settings.get("controller_msg", True) + return await dispatch_message( + interaction, embed, *params, + view=view, + delete_after=None if ephemeral or stay else 10, + ephemeral=ephemeral + ) + +class ControlSelect(discord.ui.Select): + def __init__( + self, + player: "voicelink.Player", + **kwargs + ): + super().__init__(**kwargs) + + self.player: voicelink.Player = player + + async def send(self, interaction: discord.Interaction, key: str, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None: + stay = self.player.settings.get("controller_msg", True) + return await send_localized_message( + interaction, key, *params, + view=view, + delete_after=None if ephemeral or stay else 10, + ephemeral=ephemeral + ) + class Back(ControlButton): def __init__(self, **kwargs): super().__init__( @@ -319,7 +347,7 @@ class Forward(ControlButton): position = int(self.player.position + 10000) await self.player.seek(position) - await self.send(interaction, 'forward', format_ms(position)) + await self.send(interaction, 'player.controls.forward', format_ms(position)) class Rewind(ControlButton): def __init__(self, **kwargs): @@ -338,7 +366,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', format_ms(position)) + await self.send(interaction, 'player.controls.rewind', format_ms(position)) class Lyrics(ControlButton): def __init__(self, **kwargs): @@ -352,6 +380,8 @@ class Lyrics(ControlButton): if not self.player or not self.player.is_playing: return await self.send(interaction, "player.errors.noTrackPlaying", ephemeral=True) + await interaction.response.defer() + title = self.player.current.title artist = self.player.current.author @@ -362,23 +392,22 @@ class Lyrics(ControlButton): 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) + view.response = await self.send_embed(interaction, await view.build_embed(), view=view, ephemeral=True) -class Tracks(discord.ui.Select): +class Tracks(ControlSelect): def __init__(self, player: "voicelink.Player", btn_data, **kwargs): - self.player: voicelink.Player = player - if player.queue.is_empty: raise ValueError("Player queue is empty, cannot create Tracks row instance.") options = [] - for index, track in enumerate(self.player.queue.tracks(), start=1): + for index, track in enumerate(player.queue.tracks(), start=1): if index > min(max(btn_data.get("max_options", 10), 1), 25): break options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatted_length), emoji=track.emoji)) super().__init__( - placeholder=self.player._ph.replace(btn_data.get("label"), {}), + placeholder=player._ph.replace(btn_data.get("label"), {}), + player=player, options=options, disabled=player.queue.is_empty, **kwargs @@ -386,47 +415,49 @@ class Tracks(discord.ui.Select): async def callback(self, interaction: discord.Interaction): if not self.player.is_privileged(interaction.user): - return await send_localized_message(interaction, "permissions.missingFunction", ephemeral=True) + return await self.send(interaction, "permissions.missingFunction", ephemeral=True) + await interaction.response.defer() + self.player.queue.skipto(int(self.values[0].split(". ")[0])) await self.player.stop() if self.player.settings.get("controller_msg", True): - await send_localized_message(interaction, "player.controls.skip.success", interaction.user) + await self.send(interaction, "player.controls.skip.success", interaction.user) -class Effects(discord.ui.Select): +class Effects(ControlSelect): def __init__(self, player: "voicelink.Player", btn_data, row): - - self.player: voicelink.Player = player - options = [discord.SelectOption(label="None", value="None")] for name in voicelink.Filters.get_available_filters(): options.append(discord.SelectOption(label=name.capitalize(), value=name)) super().__init__( - placeholder=self.player._ph.replace(btn_data.get("label"), {}), + placeholder=player._ph.replace(btn_data.get("label"), {}), + player=player, options=options, row=row ) async def callback(self, interaction: discord.Interaction): if not self.player.is_privileged(interaction.user): - return await send_localized_message(interaction, "permissions.missingFunction", ephemeral=True) + return await self.send(interaction, "permissions.missingFunction", ephemeral=True) + await interaction.response.defer() + avalibable_filters = voicelink.Filters.get_available_filters() if self.values[0] == "None": await self.player.reset_filter(requester=interaction.user) - return await send_localized_message(interaction, "effects.cleared") + return await self.send(interaction, "effects.cleared") - selected_filter = avalibable_filters.get(self.values[0].lower())() + selected_filter: voicelink.Filter = avalibable_filters.get(self.values[0].lower())() if self.player.filters.has_filter(filter_tag=selected_filter.tag): await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user) - await send_localized_message(interaction, "effects.cleared") + await self.send(interaction, "effects.cleared") else: await self.player.add_filter(selected_filter, requester=interaction.user) - await send_localized_message(interaction, "effects.added", selected_filter.tag) + await self.send(interaction, "effects.added", selected_filter.tag) -BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = { +BUTTON_TYPE: Dict[str, Type[Union[ControlButton, ControlSelect]]] = { "back": Back, "play-pause": PlayPause, "skip": Skip, @@ -446,7 +477,7 @@ BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = { } class InteractiveController(discord.ui.View): - def __init__(self, player): + def __init__(self, player: "voicelink.Player"): super().__init__(timeout=None) self.player: voicelink.Player = player @@ -485,4 +516,7 @@ class InteractiveController(discord.ui.View): sec = int(error.retry_after) return await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True) - super().on_error(interaction, error, item) \ No newline at end of file + if isinstance(error, voicelink.VoicelinkException): + return await interaction.response.send_message(error, ephemeral=True) + + await super().on_error(interaction, error, item) \ No newline at end of file From b6a61cbfb534f007a08e1b9b0d10937b20f5e5e6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 9 May 2026 12:30:08 +0800 Subject: [PATCH 51/52] Expand loop translations and rename max_history Replace the single "loop" translation with three explicit keys ("loopOff", "loopTrack", "loopQueue") across all language files to support distinct loop modes. Also standardize the settings key from "max-history" to "max_history" and update main.py to read LOG_SETTINGS.get("max_history", 30) for the TimedRotatingFileHandler backupCount. Files changed: langs/*.json, main.py, settings Example.json. --- langs/DE.json | 5 +++-- langs/EN.json | 4 +++- langs/ES.json | 4 +++- langs/FR.json | 4 +++- langs/JA.json | 4 +++- langs/KO.json | 4 +++- langs/PL.json | 4 +++- langs/RU.json | 4 +++- langs/UA.json | 4 +++- langs/VN.json | 4 +++- langs/ZHCN.json | 4 +++- langs/ZHTW.json | 4 +++- main.py | 2 +- settings Example.json | 2 +- 14 files changed, 38 insertions(+), 15 deletions(-) diff --git a/langs/DE.json b/langs/DE.json index 62c4223..11a35a4 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -28,8 +28,9 @@ "resume": "Weiter", "skip": "Überspringen", "leave": "Verlassen", - "loop": "Endlosschleife", - "volumeUp": "Lauter", + "loopOff": "Aus", + "loopTrack": "Track", + "loopQueue": "Warteschlange", "volumeDown": "Leiser", "volumeMute": "Stummschalten", "volumeUnmute": "Stummschaltung aufheben", diff --git a/langs/EN.json b/langs/EN.json index ba729ed..0e4e975 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -28,7 +28,9 @@ "resume": "Resume", "skip": "Skip", "leave": "Leave", - "loop": "Loop", + "loopOff": "Off", + "loopTrack": "Track", + "loopQueue": "Queue", "volumeUp": "Volume Up", "volumeDown": "Volume Down", "volumeMute": "Mute", diff --git a/langs/ES.json b/langs/ES.json index 8f6dc38..fa58491 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -28,7 +28,9 @@ "resume": "Reanudar", "skip": "Saltar", "leave": "Salir", - "loop": "Bucle", + "loopOff": "Apagar", + "loopTrack": "Pista", + "loopQueue": "Cola", "volumeUp": "Subir Volumen", "volumeDown": "Bajar Volumen", "volumeMute": "Silenciar", diff --git a/langs/FR.json b/langs/FR.json index 08383d2..d713d53 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -28,7 +28,9 @@ "resume": "Reprendre", "skip": "Passer", "leave": "Quitter", - "loop": "Boucle", + "loopOff": "Désactivé", + "loopTrack": "Piste", + "loopQueue": "File d'attente", "volumeUp": "Volume +", "volumeDown": "Volume -", "volumeMute": "Muet", diff --git a/langs/JA.json b/langs/JA.json index 3552488..2200968 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -28,7 +28,9 @@ "resume": "再開", "skip": "スキップ", "leave": "退出", - "loop": "ループ", + "loopOff": "オフ", + "loopTrack": "トラック", + "loopQueue": "キュー", "volumeUp": "音量を上げる", "volumeDown": "音量を下げる", "volumeMute": "消音", diff --git a/langs/KO.json b/langs/KO.json index bd1d813..26bb2c9 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -28,7 +28,9 @@ "resume": "재개", "skip": "스킵", "leave": "나가기", - "loop": "반복", + "loopOff": "끄기", + "loopTrack": "트랙", + "loopQueue": "대기열", "volumeUp": "볼륨 높이기", "volumeDown": "볼륨 낮추기", "volumeMute": "음소거", diff --git a/langs/PL.json b/langs/PL.json index 8b97d2a..fc6d935 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -28,7 +28,9 @@ "resume": "Wznów", "skip": "Pomiń", "leave": "Opuść", - "loop": "Pętla", + "loopOff": "Wyłącz", + "loopTrack": "Ścieżka", + "loopQueue": "Kolejka", "volumeUp": "Głośność +", "volumeDown": "Głośność -", "volumeMute": "Wycisz", diff --git a/langs/RU.json b/langs/RU.json index b4957d3..5555245 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -28,7 +28,9 @@ "resume": "Пуск", "skip": "Вперед", "leave": "СТОП", - "loop": "Зациклить", + "loopOff": "Выключить", + "loopTrack": "Трек", + "loopQueue": "Очередь", "volumeUp": "Громкость +", "volumeDown": "Громкость -", "volumeMute": "Выключить звук", diff --git a/langs/UA.json b/langs/UA.json index bbdf7cb..fae0a28 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -28,7 +28,9 @@ "resume": "Продовжити", "skip": "Скіпнути трек", "leave": "СТОП", - "loop": "Зациклити", + "loopOff": "Вимкнути", + "loopTrack": "Трек", + "loopQueue": "Черга", "volumeUp": "Гучність +", "volumeDown": "Гучність -", "volumeMute": "Вимкнути звук", diff --git a/langs/VN.json b/langs/VN.json index 4112bc3..fe3a483 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -28,7 +28,9 @@ "resume": "Tiếp Tục", "skip": "Bỏ Qua", "leave": "Rời Khỏi", - "loop": "Lặp Lại", + "loopOff": "Tắt", + "loopTrack": "Bài hát", + "loopQueue": "Hàng đợi", "volumeUp": "Tăng Âm Lượng", "volumeDown": "Giảm Âm Lượng", "volumeMute": "Tắt Tiếng", diff --git a/langs/ZHCN.json b/langs/ZHCN.json index a361a59..1714f2c 100644 --- a/langs/ZHCN.json +++ b/langs/ZHCN.json @@ -28,7 +28,9 @@ "resume": "继续", "skip": "下一首", "leave": "退出", - "loop": "循环", + "loopOff": "关闭", + "loopTrack": "音轨", + "loopQueue": "队列", "volumeUp": "音量+", "volumeDown": "音量-", "volumeMute": "静音", diff --git a/langs/ZHTW.json b/langs/ZHTW.json index 27d45e5..84462fd 100644 --- a/langs/ZHTW.json +++ b/langs/ZHTW.json @@ -28,7 +28,9 @@ "resume": "繼續", "skip": "下一首", "leave": "離開", - "loop": "循環", + "loopOff": "關閉", + "loopTrack": "音軌", + "loopQueue": "隊列", "volumeUp": "音量+", "volumeDown": "音量-", "volumeMute": "靜音", diff --git a/main.py b/main.py index b2b29b9..a977f28 100644 --- a/main.py +++ b/main.py @@ -211,7 +211,7 @@ if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True): if not os.path.exists(log_path): os.makedirs(log_path) - file_handler = TimedRotatingFileHandler(filename=f'{log_path}/vocard.log', encoding="utf-8", backupCount=LOG_SETTINGS.get("max-history", 30), when="d") + file_handler = TimedRotatingFileHandler(filename=f'{log_path}/vocard.log', encoding="utf-8", backupCount=LOG_SETTINGS.get("max_history", 30), when="d") file_handler.namer = lambda name: name.replace(".log", "") + ".log" file_handler.setFormatter(logging.Formatter('{asctime} [{levelname:<8}] {name}: {message}', '%Y-%m-%d %H:%M:%S', style='{')) logging.getLogger().addHandler(file_handler) diff --git a/settings Example.json b/settings Example.json index a504077..5ca8afd 100644 --- a/settings Example.json +++ b/settings Example.json @@ -35,7 +35,7 @@ "vocard": "INFO", "ipc_client": "INFO" }, - "max-history": 30 + "max_history": 30 }, "bot_access_user": [], "embed_color":"0xb3b3b3", From c45bef5779f72211d964ea74465210843be6cfb3 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 9 May 2026 12:35:41 +0800 Subject: [PATCH 52/52] dump the version to 2.7.3 --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index d14586f..db1c989 100644 --- a/update.py +++ b/update.py @@ -31,7 +31,7 @@ import subprocess from io import BytesIO ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.7.3b3" +__version__ = "v2.7.3" # URLs for update and migration PYTHON_CMD_NAME = os.path.basename(sys.executable)