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] 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