diff --git a/cogs/basic.py b/cogs/basic.py index 80b07a2..fa885dd 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -67,7 +67,7 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player): 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) - return await ctx.send(embed=embed, view=LinkView(texts[2].format(track.source), track.emoji, track.uri)) + return await send(ctx, embed, view=LinkView(texts[2].format(track.source), track.emoji, track.uri)) class Basic(commands.Cog): def __init__(self, bot: commands.Bot) -> None: @@ -148,9 +148,16 @@ class Basic(commands.Cog): 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") - await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False) - except voicelink.QueueFull as e: - await ctx.send(e) + + 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( + ctx, + stream_content + additional_content, + tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, + position if position >= 1 and player.is_playing else None + ) finally: if not player.is_playing: await player.do_next() @@ -189,10 +196,16 @@ class Basic(commands.Cog): else: position = await player.add_track(tracks[0]) texts = await get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad") - await interaction.followup.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False) - except voicelink.QueueFull as e: - await interaction.followup.send(e) + 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( + interaction, + stream_content + additional_content, + tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, + position if position >= 1 and player.is_playing else None + ) finally: if not player.is_playing: await player.do_next() @@ -227,7 +240,7 @@ class Basic(commands.Cog): 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(platform, "emoji"), platform, len(tracks[0:10]), query_track), color=settings.embed_color) view = SearchView(tracks=tracks[0:10], texts=[texts[5], texts[6]]) - view.response = await ctx.send(embed=embed, view=view, ephemeral=True) + view.response = await send(ctx, embed, view=view, ephemeral=True) await view.wait() if view.values is not None: @@ -236,7 +249,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 ctx.send(msg, allowed_mentions=False) + await send(ctx, msg) if not player.is_playing: await player.do_next() @@ -272,11 +285,16 @@ class Basic(commands.Cog): 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") - await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + (texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else texts[2].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False) - - except voicelink.QueueFull as e: - await ctx.send(e) + 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( + ctx, + stream_content + additional_content, + tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, + position if position >= 1 and player.is_playing else None + ) finally: if not player.is_playing: await player.do_next() @@ -311,11 +329,14 @@ class Basic(commands.Cog): 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) - await ctx.send((f"`{texts[0]}`" if tracks[0].is_stream else "") + texts[1].format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length), allowed_mentions=False) - except voicelink.QueueFull as e: - await ctx.send(e) + stream_content = f"`{texts[0]}`" if tracks[0].is_stream else "" + await send( + ctx, + stream_content + texts[1], + tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, + ) finally: if player.queue._repeat.mode == voicelink.LoopType.TRACK: await player.set_repeat(voicelink.LoopType.OFF) @@ -471,7 +492,7 @@ class Basic(commands.Cog): if player.queue.is_empty: return await nowplay(ctx, player) view = ListView(player=player, author=ctx.author) - view.response = await ctx.send(embed=await view.build_embed(), view=view) + view.response = await send(ctx, await view.build_embed(), view=view) @queue.command(name="export", aliases=get_aliases("export")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -532,10 +553,6 @@ class Basic(commands.Cog): index = await player.add_track(tracks) await send(ctx, "playlistLoad", attachment.filename, index) - - except voicelink.QueueFull as e: - return await ctx.send(e, ephemeral=True) - except Exception as e: logger.error("error", exc_info=e) raise e @@ -559,7 +576,7 @@ class Basic(commands.Cog): return await nowplay(ctx, player) view = ListView(player=player, author=ctx.author, is_queue=False) - view.response = await ctx.send(embed=await view.build_embed(), view=view) + view.response = await send(ctx, await view.build_embed(), view=view) @commands.hybrid_command(name="leave", aliases=get_aliases("leave")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -653,7 +670,7 @@ class Basic(commands.Cog): await send(ctx, "removed", len(removed_tracks.keys())) @commands.hybrid_command(name="forward", aliases=get_aliases("forward")) - @app_commands.describe(position="Input a amount that you to forward to. Exmaple: 1:20") + @app_commands.describe(position="Input an amount that you to forward to. Exmaple: 1:20") @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def forward(self, ctx: commands.Context, position: str = "10"): "Forwards by a certain amount of time in the current track. The default is 10 seconds." @@ -674,7 +691,7 @@ class Basic(commands.Cog): await send(ctx, "forward", ctime(player.position + num)) @commands.hybrid_command(name="rewind", aliases=get_aliases("rewind")) - @app_commands.describe(position="Input a amount that you to rewind to. Exmaple: 1:20") + @app_commands.describe(position="Input an amount that you to rewind to. Exmaple: 1:20") @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def rewind(self, ctx: commands.Context, position: str = "10"): "Rewind by a certain amount of time in the current track. The default is 10 seconds." @@ -785,7 +802,7 @@ class Basic(commands.Cog): return await send(ctx, "lyricsNotFound", ephemeral=True) view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=ctx.author) - view.response = await ctx.send(embed=view.build_embed(), view=view) + view.response = await send(ctx, 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.") @@ -841,7 +858,7 @@ class Basic(commands.Cog): category = "News" view = HelpView(self.bot, ctx.author) embed = view.build_embed(category) - view.response = await ctx.send(embed=embed, view=view) + view.response = await send(ctx, embed, view=view) @commands.hybrid_command(name="ping", aliases=get_aliases("ping")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -866,7 +883,7 @@ class Basic(commands.Cog): inline=False ) - await ctx.send(embed=embed) + await send(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/playlist.py b/cogs/playlist.py index 4abf0b7..bec4135 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -107,7 +107,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 = await ctx.send(embed=embed, view=view) + view.response = send(ctx, embed, view=view) @playlist.command(name="play", aliases=get_aliases("play")) @app_commands.describe( @@ -212,7 +212,7 @@ class Playlists(commands.Cog, name="playlist"): embed.set_footer(text=text[2]) view = PlaylistView(embed, results, ctx.author) - view.response = await ctx.send(embed=embed, view=view, ephemeral=True) + view.response = await send(ctx, embed, view=view, ephemeral=True) @playlist.command(name="create", aliases=get_aliases("create")) @app_commands.describe( @@ -344,7 +344,7 @@ class Playlists(commands.Cog, name="playlist"): inbox = user['inbox'].copy() view = InboxView(ctx.author, user['inbox']) - view.response = await ctx.send(embed=view.build_embed(), view=view, ephemeral=True) + view.response = await send(ctx, view.build_embed(), view=view, ephemeral=True) await view.wait() if inbox == user['inbox']: diff --git a/cogs/settings.py b/cogs/settings.py index 543089f..ed47037 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -58,13 +58,16 @@ 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 ctx.send(embed=embed, view=view) + view.response = await send(ctx, embed, view=view) @settings.command(name="prefix", aliases=get_aliases("prefix")) @commands.has_permissions(manage_guild=True) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) 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) + await update_settings(ctx.guild.id, {"$set": {"prefix": prefix}}) await send(ctx, "setPrefix", prefix, prefix) @@ -170,7 +173,7 @@ class Settings(commands.Cog, name="settings"): ), inline=False ) - await ctx.send(embed=embed) + await send(ctx, embed) @settings.command(name="volume", aliases=get_aliases("volume")) @app_commands.describe(value="Input a integer.") @@ -226,7 +229,7 @@ class Settings(commands.Cog, name="settings"): controller_settings = settings.get("default_controller", func.settings.controller) view = EmbedBuilderView(ctx, controller_settings.get("embeds").copy()) - view.response = await ctx.send(embed=view.build_embed(), view=view) + view.response = await send(ctx, view.build_embed(), view=view) @settings.command(name="controllermsg", aliases=get_aliases("controllermsg")) @commands.has_permissions(manage_guild=True) @@ -243,9 +246,46 @@ class Settings(commands.Cog, name="settings"): @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""" + "Customize the channel topic template" await update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}}) - await send(ctx, "SetStageAnnounceTemplate") + await send(ctx, "setStageAnnounceTemplate") + + @settings.command(name="setupchannel", aliases=get_aliases("setupchannel")) + @app_commands.describe( + channel="Provide a request channel. If not, a text channel will be generated." + ) + @commands.has_permissions(manage_guild=True) + @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) + 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) + + if not channel: + try: + overwrites = { + ctx.guild.me: discord.PermissionOverwrite( + read_messages=True, + manage_messages=True + ) + } + channel = await ctx.guild.create_text_channel("vocard-song-requests", overwrites=overwrites) + except: + return await send(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") + + 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))) + + await 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) @app_commands.command(name="debug") async def debug(self, interaction: discord.Interaction): @@ -268,7 +308,7 @@ class Settings(commands.Cog, name="settings"): value=f"```• VERSION: {func.settings.version}\n" \ f"• LATENCY: {self.bot.latency:.2f}ms\n" \ f"• GUILDS: {len(self.bot.guilds)}\n" \ - f"• USERS: {sum([guild.member_count for guild in self.bot.guilds])}\n" \ + f"• USERS: {sum([guild.member_count or 0 for guild in self.bot.guilds])}\n" \ f"• PLAYERS: {len(self.bot.voice_clients)}```", inline=False ) diff --git a/function.py b/function.py index 6e09f5e..da6a72f 100644 --- a/function.py +++ b/function.py @@ -34,6 +34,8 @@ LOCAL_LANGS: dict[str, dict[str, str]] = {} #Stores all the localization languag 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': { @@ -106,7 +108,7 @@ def format_time(number:str) -> int: 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 = settings.sources_settings.get(source.lower(), {}) + source_settings: dict = settings.sources_settings.get(source.lower(), settings.sources_settings.get("others")) return source_settings.get(type, ("🔗" if type == "emoji" else settings.embed_color)) def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]: @@ -143,30 +145,55 @@ def format_bytes(bytes: int, unit: bool = False): else: return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "") -async def get_lang(guild_id:int, *keys) -> Union[list[str], str]: +async def get_lang(guild_id:int, *keys) -> Optional[Union[list[str], str]]: settings = await get_settings(guild_id) lang = settings.get("lang", "EN") if lang in LANGS and not LANGS[lang]: LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) if len(keys) == 1: - return LANGS.get(lang, {}).get(keys[0], "Language pack not found!") - return [LANGS.get(lang, {}).get(key, "Language pack not found!") for key in keys] + return LANGS.get(lang, {}).get(keys[0]) + return [LANGS.get(lang, {}).get(key) for key in keys] -async def send(ctx: Union[commands.Context, discord.Interaction], key: str, *params, delete_after: float = None, ephemeral: bool = False) -> Optional[discord.Message]: - text = await get_lang(ctx.guild.id, key) - text = text.format(*params) +async def send( + ctx: Union[commands.Context, discord.Interaction], + content: Union[str, discord.Embed] = None, + *params, + view: discord.ui.View = None, + delete_after: float = None, + ephemeral: bool = False +) -> Optional[discord.Message]: + if content is None: + content = "No content provided." - if isinstance(ctx, commands.Context): - send_func = ctx.send + # Determine the text to send + if isinstance(content, discord.Embed): + embed = content + text = None else: - if not ctx.response.is_done(): - send_func = ctx.response.send_message - + text = await get_lang(ctx.guild.id, content) + if text: + text = text.format(*params) else: - return await ctx.followup.send(text, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS) + text = content.format(*params) + embed = None - return await send_func(text, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS) + # Determine the sending function + send_func = ( + ctx.send if isinstance(ctx, commands.Context) else + ctx.response.send_message if not ctx.response.is_done() else + ctx.followup.send + ) + + # Check settings for delete_after duration + settings = await get_settings(ctx.guild.id) + if settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"): + delete_after = 10 + + # Send the message or embed + if view: + return await send_func(text, embed=embed, view=view, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS) + return await send_func(text, embed=embed, delete_after=delete_after, ephemeral=ephemeral, allowed_mentions=ALLOWED_MENTIONS) async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool: for mode, action in data.items(): diff --git a/langs/CH.json b/langs/CH.json index d096e80..ff6d4be 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -7,9 +7,11 @@ "noChannel": "沒有語音頻道可供連接。請提供一個語音頻道或加入一個語音頻道。", "alreadyConnected": "已經連接到語音頻道。", "noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。", + "noCreatePermission": "抱歉!我沒有權限建立歌曲請求頻道。", "noPlaySource": "找不到任何可播放的來源!", "noPlayer": "在此伺服器上找不到播放器。", "notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。", + "missingIntents": "抱歉,此命令無法執行,因為機器人缺少所需的請求意圖:`({0})`.", "languageNotFound": "找不到語言包!請選擇一個現有的語言包。", "changedLanguage": "已成功切換到 `{0}` 語言包。", "setPrefix": "完成!我的前綴在您的伺服器中現在是 `{0}`。嘗試運行 `{1}ping` 來測試它。", @@ -186,5 +188,5 @@ "invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。", "invalidTimeOrder": "結束時間不能小於或等於開始時間。", - "SetStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。" + "setStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。" } \ No newline at end of file diff --git a/langs/DE.json b/langs/DE.json index 37bd9a0..5aa9880 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -7,9 +7,11 @@ "noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stellen Sie entweder einen zur Verfügung oder schließen Sie sich einem an.", "alreadyConnected": "Bereits mit einem Sprachkanal verbunden.", "noPermission": "Es tut uns leid! Ich bin nicht berechtigt, Ihrem Sprachkanal beizutreten oder darin zu sprechen.", + "noCreatePermission": "Entschuldigung! Ich habe keine Berechtigung, einen Songanforderungskanal zu erstellen.", "noPlaySource": "Kann keine abspielbaren Quellen finden!", "noPlayer": "Auf diesem Server wurde kein Spieler gefunden.", "notVote": "Dieser Befehl erfordert Ihre Stimme! Geben Sie `/vote` ein, um weitere Informationen zu erhalten.", + "missingIntents": "Es tut mir leid, dieser Befehl kann nicht ausgeführt werden, da dem Bot die erforderliche Anforderungsabsicht fehlt: `({0})`.", "languageNotFound": "Kein Sprachpaket gefunden. Bitte wählen Sie ein vorhandenes Sprachpaket aus.", "changedLanguage": "Erfolgreich auf das Sprachpaket `{0}` geändert.", "setPrefix": "Erledigt! Mein Präfix auf Ihrem Server ist jetzt `{0}`. Versuchen Sie, `{1}ping` auszuführen, um es zu testen.", @@ -186,5 +188,5 @@ "invalidEndTime": "Ungültiger Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.", "invalidTimeOrder": "Der Endzeit darf nicht kleiner oder gleich dem Startzeit sein.", - "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Sie sich gerade befinden, gemäß Ihrer Vorlage benannt. Sie sollten in wenigen Sekunden eine Aktualisierung sehen." + "setStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Sie sich gerade befinden, gemäß Ihrer Vorlage benannt. Sie sollten in wenigen Sekunden eine Aktualisierung sehen." } \ No newline at end of file diff --git a/langs/EN.json b/langs/EN.json index 1552ed7..e11767f 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -7,9 +7,11 @@ "noChannel": "No voice channel to connect. Please either provide one or join one.", "alreadyConnected": "Already connected to a voice channel.", "noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.", + "noCreatePermission": "Sorry! i don't have permissions to create a song requesting channel.", "noPlaySource": "Can't found any playable sources!", "noPlayer": "No player has found on this server.", "notVote": "This command requires your vote! Type `/vote` for more info.", + "missingIntents": "Sorry, this command cannot be executed because the bot is missing the required request intent: `({0})`.", "languageNotFound": "No language pack found! please select an existing language pack.", "changedLanguage": "Successfully changed to `{0}` language pack.", "setPrefix": "Done! My prefix in your server is now `{0}`. Try running `{1}ping` to test it out.", @@ -186,5 +188,6 @@ "invalidEndTime": "Invalid end time, it must be between `00:00` and `{0}`", "invalidTimeOrder": "End time cannot be less than or equal to start time", - "SetStageAnnounceTemplate": "Done! From now on, voice status like the one you're in now will be named according to your template. You should see it update in a few seconds." + "setStageAnnounceTemplate": "Done! From now on, voice status like the one you're in now will be named according to your template. You should see it update in a few seconds.", + "createSongRequestChannel": "A song request channel ({0}) has been created! You can start requesting any song by name or URL in that channel, without needing to use the bot prefix." } \ No newline at end of file diff --git a/langs/ES.json b/langs/ES.json index f2a3fcd..990cdea 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -7,9 +7,11 @@ "noChannel": "No hay canal de voz al que conectarse. Por favor, proporcione uno o únase a uno.", "alreadyConnected": "Ya conectado a un canal de voz.", "noPermission": "¡Lo siento! No tengo permisos para unirme o hablar en su canal de voz.", + "noCreatePermission": "¡Lo siento! No tengo permisos para crear un canal de solicitud de canciones.", "noPlaySource": "¡No se puede encontrar ninguna fuente reproducible!", "noPlayer": "No se ha encontrado ningún reproductor en este servidor.", "notVote": "¡Este comando requiere su voto! Escriba `/vote` para obtener más información.", + "missingIntents": "Lo siento, este comando no se puede ejecutar porque el bot carece de la intención de solicitud requerida: `({0})`.", "languageNotFound": "¡No se encontró paquete de idioma! por favor seleccione un paquete de idioma existente.", "changedLanguage": "Cambiado con éxito al paquete de idioma `{0}`.", "setPrefix": "¡Listo! Mi prefijo en tu servidor ahora es `{0}`. Intenta ejecutar `{1}ping` para probarlo.", @@ -186,5 +188,5 @@ "invalidEndTime": "Tiempo de finalización inválido! El tiempo debe estar entre `00:00` y `{0}`.", "invalidTimeOrder": "El tiempo final no puede ser menor o igual que el tiempo de inicio.", - "SetStageAnnounceTemplate": "¡Hecho! A partir de ahora, el estado de voz como el que tienes ahora se nombrará según tu plantilla. Deberías verlo actualizarse en unos segundos." + "setStageAnnounceTemplate": "¡Hecho! A partir de ahora, el estado de voz como el que tienes ahora se nombrará según tu plantilla. Deberías verlo actualizarse en unos segundos." } \ No newline at end of file diff --git a/langs/JA.json b/langs/JA.json index 4f008f6..9544615 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -7,9 +7,11 @@ "noChannel": "接続する音声チャンネルがありません。提供するか、参加してください。", "alreadyConnected": "すでに音声チャンネルに接続しています。", "noPermission": "申し訳ありません!私はあなたの音声チャンネルに参加または話すための許可がありません。", + "noCreatePermission": "ごめんなさい!曲リクエストチャンネルを作成する権限がありません。", "noPlaySource": "再生可能なソースが見つかりません!", "noPlayer": "このサーバーにプレイヤーが見つかりません。", "notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。", + "missingIntents": "申し訳ありませんが、このコマンドは実行できません。ボットに必要なリクエストインテントが不足しています:`({0})`.", "languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。", "changedLanguage": "「{0}」言語パックに正常に変更しました。", "setPrefix": "完了!あなたのサーバーのプレフィックスは今や「{0}」です。 `{1}ping`を実行してテストしてみてください。", @@ -186,5 +188,5 @@ "invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。", "invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。", - "SetStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。" + "setStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。" } \ No newline at end of file diff --git a/langs/KO.json b/langs/KO.json index ccb7fc2..0dfd6c6 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -7,9 +7,11 @@ "noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 참여하십시오.", "alreadyConnected": "이미 음성 채널에 연결되어 있습니다.", "noPermission": "죄송합니다! 음성 채널에 참여하거나 말할 권한이 없습니다.", + "noCreatePermission": "죄송합니다! 노래 요청 채널을 생성할 권한이 없습니다.", "noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!", "noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.", "notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.", + "missingIntents": "죄송하지만 이 명령을 실행할 수 없습니다. 봇에 필요한 요청 의도가 없습니다:`({0})`.", "languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.", "changedLanguage": "성공적으로 `{0}` 언어 팩으로 변경되었습니다.", "setPrefix": "완료되었습니다! 이 서버에서 내 접두사는 이제 `{0}`입니다. `{1}ping`을 실행하여 테스트해보세요.", @@ -186,5 +188,5 @@ "invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.", "invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다.", - "SetStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다." + "setStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다." } \ No newline at end of file diff --git a/langs/RU.json b/langs/RU.json index 636794d..bbc9cca 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -7,9 +7,11 @@ "noChannel": "Нет голосового канала для подключения. Пожалуйста, укажите или присоединитесь к одному.", "alreadyConnected": "Уже подключен к голосовому каналу.", "noPermission": "Извините! У меня нет разрешения на подключение или разговор в вашем голосовом канале.", + "noCreatePermission": "Извините! У меня нет прав на создание канала для запроса песен.", "noPlaySource": "Не получилось найти рабочие источники!", "noPlayer": "На этом сервере не найдено ни одного активного плеера.", "notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.", + "missingIntents": "Извините, но эту команду нельзя выполнить, так как у бота отсутствует необходимый запрос намерения: `({0})`.", "languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.", "changedLanguage": "Язык успешно изменен на `{0}`.", "setPrefix": "Готово! Мой префикс на вашем сервере теперь `{0}`. Попробуйте запустить `{1}ping`, чтобы проверить его.", @@ -185,5 +187,5 @@ "invalidEndTime": "Невозможное время конца! Вход времени должен быть внутри `00:00` и `{0}`.", "invalidTimeOrder": "Время конца не может быть меньше или равно времени начала.", - "SetStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд." + "setStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд." } \ No newline at end of file diff --git a/langs/UA.json b/langs/UA.json index b31b613..df3a9e0 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -7,9 +7,11 @@ "noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.", "alreadyConnected": "Уже підключений до голосового каналу.", "noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.", + "noCreatePermission": "Вибачте! У мене немає прав для створення каналу запитів на пісні.", "noPlaySource": "Неможливо знайти робочі джерела!", "noPlayer": "На цьому сервері не знайдено жодного активного плеєра.", "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", + "missingIntents": "Вибачте, але цю команду не можна виконати, оскільки у бота відсутній необхідний запит на інтенцію: `({0})`.", "languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.", "changedLanguage": "Успішно змінено на мовний пакет `{0}`.", "setPrefix": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.", @@ -185,5 +187,5 @@ "invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.", "invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.", - "SetStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд." + "setStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд." } \ No newline at end of file diff --git a/local_langs/zh-TW.json b/local_langs/zh-TW.json index 443b026..d4ae83e 100644 --- a/local_langs/zh-TW.json +++ b/local_langs/zh-TW.json @@ -69,10 +69,10 @@ "Remove tracks requested by a specific member.": "刪除指定成員所要求的歌曲。", "forward": "前進", "Forwards by a certain amount of time in the current track. The default is 10 seconds.": "在目前歌曲中前進一定的時間。預設為 10 秒。", - "Input a amount that you to forward to. Exmaple: 1: 20": "輸入您要前進到的時間。範例:1:20", + "Input an amount that you to forward to. Exmaple: 1:20": "輸入您要前進到的時間。範例:1:20", "rewind": "倒退", "Rewind by a certain amount of time in the current track. The default is 10 seconds.": "在目前歌曲中倒退一定的時間。預設為 10 秒。", - "Input a amount that you to rewind to. Exmaple: 1: 20": "輸入您要倒退到的時間。範例:1:20", + "Input an amount that you to rewind to. Exmaple: 1:20": "輸入您要倒退到的時間。範例:1:20", "replay": "重新播放", "Reset the progress of the current song.": "重設目前歌曲的進度。", "shuffle": "隨機播放", @@ -210,5 +210,22 @@ "cleareffect": "清除效果", "Clear all or specific sound effects.": "清除所有或指定的音效。", "effect": "效果", - "Remove a specific sound effects.": "刪除指定的音效。" + "Remove a specific sound effects.": "刪除指定的音效。", + "start": "開始", + "end": "結束", + "Specify a time you would like to start, e.g. 1:00": "指定您希望開始的時間,例如:1:00。", + "Specify a time you would like to end, e.g. 4:00": "指定您希望結束的時間,例如:4:00。", + "list": "列表", + "Customize the channel topic template": "自訂頻道主題模板", + "template": "模板", + "setupchannel": "設置頻道", + "Sets up a dedicated channel for song requests in your server.": "為您的伺服器設置一個專用的歌曲請求頻道。", + "Provide a request channel. If not, a text channel will be generated.": "提供請求頻道。如果沒有,將生成一個文本頻道。", + "ping": "ping", + "…": "...", + "8d": "8d", + "dj": "dj", + "247": "247", + "stageannounce": "舞台公告", + "Soundcloud": "Soundcloud" } \ No newline at end of file diff --git a/main.py b/main.py index 3c959bd..d2ff78c 100644 --- a/main.py +++ b/main.py @@ -21,8 +21,18 @@ class Translator(discord.app_commands.Translator): func.logger.info("Unload Translator") async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext): - if str(locale) in func.LOCAL_LANGS: - return func.LOCAL_LANGS[str(locale)].get(string.message, None) + 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) + + return translated_text + return None class Vocard(commands.Bot): @@ -32,15 +42,37 @@ class Vocard(commands.Bot): self.ipc: IPCClient async def on_message(self, message: discord.Message, /) -> None: + # Ignore messages from bots or DMs if message.author.bot or not message.guild: return False + # Check if the bot is directly mentioned if self.user.id in message.raw_mentions and not message.mention_everyone: prefix = await self.command_prefix(self, message) if not prefix: return await message.channel.send("I don't have a bot prefix set.") 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) + 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) + try: + cmd = self.get_command("play") + if message.content: + await cmd(ctx, query=message.content) + + elif message.attachments: + for attachment in message.attachments: + await cmd(ctx, query=attachment.url) + + except Exception as e: + await func.send(ctx, str(e), ephemeral=True) + + finally: + return await message.delete() + await self.process_commands(message) async def connect_db(self) -> None: @@ -65,6 +97,9 @@ class Vocard(commands.Bot): # Connecting to MongoDB await self.connect_db() + # Set translator + await self.tree.set_translator(Translator()) + # Loading all the module in `cogs` folder for module in os.listdir(func.ROOT_DIR + '/cogs'): if module.endswith('.py'): @@ -82,10 +117,10 @@ class Vocard(commands.Bot): func.logger.error(f"Cannot connected to dashboard! - Reason: {e}") if not func.settings.version or func.settings.version != update.__version__: - func.update_json("settings.json", new_data={"version": update.__version__}) - - await self.tree.set_translator(Translator()) await self.tree.sync() + func.update_json("settings.json", new_data={"version": update.__version__}) + for locale_key, values in func.MISSING_TRANSLATOR.items(): + func.logger.warning(f"Missing translation for '{", ".join(values)}' in '{locale_key}'") async def on_ready(self): func.logger.info("------------------") @@ -98,6 +133,7 @@ class Vocard(commands.Bot): func.settings.client_id = self.user.id func.LOCAL_LANGS.clear() + func.MISSING_TRANSLATOR.clear() async def on_command_error(self, ctx: commands.Context, exception, /) -> None: error = getattr(exception, 'original', exception) diff --git a/settings Example.json b/settings Example.json index bf586ce..5a26452 100644 --- a/settings Example.json +++ b/settings Example.json @@ -82,6 +82,10 @@ "tiktok": { "emoji": "<:tiktok:996007689798811698>", "color": "0x74ECE9" + }, + "others": { + "emoji": "🌎", + "color": "0xb3b3b3" } }, "default_controller": { diff --git a/update.py b/update.py index 6e71ae9..81f53df 100644 --- a/update.py +++ b/update.py @@ -2,7 +2,7 @@ import requests, zipfile, os, shutil, argparse from io import BytesIO ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.7.0b1" +__version__ = "v2.7.0b2" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" diff --git a/views/controller.py b/views/controller.py index 7a9c409..d142b3e 100644 --- a/views/controller.py +++ b/views/controller.py @@ -45,11 +45,11 @@ class ControlButton(discord.ui.Button): self.disable_button_text: bool = func.settings.controller.get("disableButtonText", False) super().__init__(label=self.player.get_msg(label) if label and not self.disable_button_text else None, **kwargs) - async def send(self, interaction: discord.Interaction, key:str, *params, ephemeral: bool = False) -> None: + async def send(self, interaction: discord.Interaction, key: str, *params, ephemeral: bool = False) -> None: stay = self.player.settings.get("controller_msg", True) return await func.send( interaction, key, *params, - delete_after=None if ephemeral or stay is True else 10, + delete_after=None if ephemeral or stay else 10, ephemeral=ephemeral ) diff --git a/voicelink/player.py b/voicelink/player.py index 1671e15..ac166a3 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -36,6 +36,7 @@ from discord import ( VoiceProtocol, Member, Message, + PartialMessage, Interaction, errors ) @@ -112,7 +113,7 @@ class Player(VoiceProtocol): self.queue: Queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg) self._node = NodePool.get_node() - self._current: Track = None + self._current: Optional[Track] = None self._filters: Filters = Filters() self._paused: bool = False self._is_connected: bool = False @@ -126,8 +127,8 @@ class Player(VoiceProtocol): self._voice_state: dict = {} - self.controller: Message = None - self.updating: bool = False + self.controller: Union[Message, PartialMessage] = None + self._updating: bool = False self.pause_votes = set() self.resume_votes = set() @@ -180,7 +181,7 @@ class Player(VoiceProtocol): return self._is_connected and self._paused @property - def current(self) -> Track: + def current(self) -> Optional[Track]: """Property which returns the currently playing track""" return self._current @@ -218,12 +219,26 @@ class Player(VoiceProtocol): @property def ping(self) -> float: + """Calculates and returns the player's current ping in seconds.""" return round(self._ping / 1000, 2) - + + @property + def is_ipc_connected(self) -> bool: + """Indicates whether the Inter-Process Communication (IPC) connection is active.""" + return self._ipc._is_connected and self._ipc_connection + def get_msg(self, *keys) -> Union[list[str], str]: + """Retrieves a localized message or list of messages based on the given keys + for the guild associated with this player. + """ return func.get_lang_non_async(self.guild.id, *keys) def required(self, leave=False): + """ + Calculates the number of votes required for a specific action in the voice channel. + + If `leave` is True and the channel has three members, the requirement adjusts to 2 votes. + """ if self.settings.get('votedisable'): return 0 @@ -233,18 +248,22 @@ class Player(VoiceProtocol): required = 2 return required - - @property - def is_ipc_connected(self) -> bool: - return self._ipc._is_connected and self._ipc_connection def is_user_join(self, user: Member): + """Checks if a user is present in the voice channel or has 'Manage Server' permission.""" if user not in self.channel.members: if not user.guild_permissions.manage_guild: return False return True def is_privileged(self, user: Member, check_user_join: bool = True) -> bool: + """ + Determines if a user has privileged access. + + Privileged access is granted if the user is in the bot access list, + 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: return True @@ -256,11 +275,20 @@ class Player(VoiceProtocol): return manage_perm or (self.settings['dj'] in [role.id for role in user.roles]) return self.dj.id == user.id or manage_perm + 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", {}) + raw = controller.get("active" if current_track else "inactive", {}) + + return build_embed(raw, 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.""" uri: str = f"sessions/{self._node._session_id}/players/{self._guild.id}" + (f"?{query}" if query else "") return await self._node.send(method, query=uri, data=data) async def _update_state(self, data: dict) -> None: + """Updates the player's state based on the provided data.""" state: dict = data.get("state") self._last_update = time.time() * 1000 self._is_connected = state.get("connected") @@ -277,6 +305,7 @@ class Player(VoiceProtocol): }) async def _dispatch_voice_update(self, voice_data: Dict[str, Any] = None): + """Dispatches a voice update to the node.""" if {"sessionId", "event"} != self._voice_state.keys(): self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update failed {voice_data}") return @@ -293,10 +322,12 @@ class Player(VoiceProtocol): self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched voice update to {state['event']['endpoint']} with data {data}") async def on_voice_server_update(self, data: dict): + """Handles a voice server update event.""" self._voice_state.update({"event": data}) await self._dispatch_voice_update(self._voice_state) async def on_voice_state_update(self, data: dict): + """Handles a voice state update event.""" self._voice_state.update({"sessionId": data.get("session_id")}) if not (channel_id := data.get("channel_id")): @@ -312,6 +343,7 @@ class Player(VoiceProtocol): await self._dispatch_voice_update({**self._voice_state, "event": data}) async def _dispatch_event(self, data: dict): + """Dispatches an event based on the type of event data received.""" event_type = data.get("type") event: VoicelinkEvent = getattr(events, event_type)(data, self) @@ -326,6 +358,7 @@ class Player(VoiceProtocol): self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) dispatched event {event_type}.") async def do_next(self): + """Processes the next track in the queue.""" if self._current or self.is_playing or not self.channel: return @@ -378,42 +411,48 @@ class Player(VoiceProtocol): }) async def invoke_controller(self): - if self.updating or not self.channel: + """Sends or updates the music controller message in the designated channel.""" + if self._updating or not self.channel: return - self.updating = True + self._updating = True - try: - embed, view = await self.build_embed(), InteractiveController(self) + try: + embed, view = self.build_embed(self.current), InteractiveController(self) if not self.controller: - self.controller = await self.context.channel.send(embed=embed, view=view) + if request_channel_data := self.settings.get("music_request_channel"): + channel = self.bot.get_channel(request_channel_data.get("text_channel_id")) + if channel: + self.controller = channel.get_partial_message(request_channel_data.get("controller_msg_id")) + try: + await self.controller.edit(embed=embed, view=view) + except errors.NotFound: + self.controller = None + + # Send a new controller message if none exists + if not self.controller: + self.controller = await self.context.channel.send(embed=embed, view=view) elif not await self.is_position_fresh(): try: await self.controller.delete() - except: - pass + except Exception as e: + self._logger.warning( + f"Failed to delete outdated controller in {self.guild.name}({self.guild.id}): {e}" + ) self.controller = await self.context.channel.send(embed=embed, view=view) else: await self.controller.edit(embed=embed, view=view) - except errors.Forbidden: - pass - except Exception as e: self._logger.error(f"Something went wrong while sending music controller to {self.guild.name}({self.guild.id})", exc_info=e) - pass - - self.updating = False - - async def build_embed(self): - controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {}) - raw = controller.get("active" if self.current else "inactive", {}) - return build_embed(raw, self._ph) + finally: + self._updating = False async def is_position_fresh(self): + """Checks if the current controller message is among the most recent messages.""" try: async for message in self.context.channel.history(limit=5): if message.id == self.controller.id: @@ -424,19 +463,24 @@ class Player(VoiceProtocol): return False async def teardown(self): - await func.update_settings( - self.guild.id, - {"$set": { + """Cleans up the player and associated resources.""" + try: + await func.update_settings(self.guild.id, {"$set": { "lastActice": (timeNow := round(time.time())), "playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2) - }} - ) - await self.update_voice_status(remove_status=True) - if self.is_ipc_connected: - await self.send_ws({"op": "playerClose"}) + }}) + + if self.is_ipc_connected: + await self.send_ws({"op": "playerClose"}) + except: + pass try: - await self.controller.delete() + await self.update_voice_status(remove_status=True) + if self.controller and self.controller.id == self.settings.get("music_request_channel", {}).get("controller_msg_id"): + await self.controller.edit(embed=self.build_embed(), view=None) + else: + await self.controller.delete() except: pass @@ -464,6 +508,7 @@ class Player(VoiceProtocol): return await self._node.get_tracks(query, requester=requester, search_type=search_type) async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = True, self_mute: bool = False): + """Connects the player to a voice channel.""" await self.guild.change_voice_state(channel=self.channel, self_deaf=True, self_mute=self_mute) self._node._players[self.guild.id] = self self._is_connected = True @@ -538,6 +583,7 @@ class Player(VoiceProtocol): return self._current def _validate_time(self, track: Track, start_time: int, end_time: int) -> None: + """Validates the start and end times for a track.""" if start_time or end_time: if not end_time: end_time = track.length @@ -555,6 +601,7 @@ class Player(VoiceProtocol): track.end_time = end_time async def add_track(self, raw_tracks: Union[Track, List[Track]], *, start_time: int = 0, end_time: int = 0, at_front: bool = False, duplicate: bool = True) -> int: + """Adds one or more tracks to the queue.""" tracks: List[Track] = [] _duplicate_tracks = [] if self.queue._allow_duplicate and duplicate else [track.uri for track in self.queue._queue] raw_tracks = raw_tracks[0] if isinstance(raw_tracks, List) and len(raw_tracks) == 1 else raw_tracks @@ -586,6 +633,7 @@ class Player(VoiceProtocol): return len(tracks) if is_list else position async def remove_track(self, index: int, index2: int = None, remove_target: Member = None, requester: Member = None) -> Dict[int, Track]: + """Removes one or more tracks from the queue.""" removed_tracks = self.queue.remove(index, index2, remove_target) if removed_tracks and self.is_ipc_connected: await self.send_ws({ @@ -651,6 +699,7 @@ class Player(VoiceProtocol): self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been shuffled the queue.") async def swap_track(self, index1: int, index2: int, requester: Member = None) -> Tuple[Track, Track]: + """Swaps two tracks in the queue at the specified indices.""" track1, track2 = self.queue.swap(index1, index2) if self.is_ipc_connected: await self.send_ws({ @@ -661,6 +710,7 @@ class Player(VoiceProtocol): return track1, track2 async def move_track(self, index: int, new_index: int, requester: Member = None) -> Optional[Track]: + """Moves a track from its current position to a new position in the queue.""" moved_track = self.queue.move(index, new_index) if self.is_ipc_connected: @@ -669,6 +719,7 @@ class Player(VoiceProtocol): return moved_track async def set_repeat(self, mode: LoopType = None, requester: Member = None) -> LoopType: + """Sets the repeat mode for the queue.""" if not mode: mode = self.queue._repeat.next() @@ -684,6 +735,7 @@ class Player(VoiceProtocol): return mode async def add_filter(self, filter: Filter, requester: Member = None, fast_apply: bool = False) -> Filters: + """Adds a filter to the player's audio stream.""" try: self._filters.add_filter(filter=filter) except FilterTagAlreadyInUse: @@ -705,6 +757,7 @@ class Player(VoiceProtocol): return self._filters async def clear_queue(self, queue_type: str, requester: Member = None) -> None: + """Clears the queue or the history of tracks.""" queue_type = queue_type.lower() if queue_type == 'history': self.queue.history_clear(self.is_playing) @@ -735,6 +788,7 @@ class Player(VoiceProtocol): return self._filters async def reset_filter(self, *, requester: Member = None, fast_apply=False) -> None: + """Resets all filters applied to the player's audio stream.""" if not self._filters: raise FilterInvalidArgument("You must have filters applied first in order to use this method.") @@ -752,7 +806,7 @@ class Player(VoiceProtocol): self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been removed all filters.") async def change_node(self, identifier: str = None) -> None: - """Change node.""" + """Changes the audio processing node for the guild..""" try: node = NodePool.get_node(identifier=identifier) except: @@ -763,7 +817,7 @@ class Player(VoiceProtocol): self._node._players[self.guild.id] = self await self._dispatch_voice_update(self._voice_state) - + if self.current: await self.play(self.current, start=self.position) self._last_update = time.time() * 1000 @@ -791,6 +845,7 @@ class Player(VoiceProtocol): return False 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) if not template or not self.channel: return @@ -810,6 +865,7 @@ class Player(VoiceProtocol): ) async def send_ws(self, payload, requester: Member = None): + """Sends a WebSocket payload to the bot's IPC (Inter-Process Communication) system.""" payload['guild_id'] = str(self.guild.id) if requester: payload['requester_id'] = str(requester.id)