From ed823bbdcd8cfa29a6bb7ad5c9351819e8e356aa Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:37:07 +0800 Subject: [PATCH 01/22] Supported unprefixed responses --- main.py | 2 +- requirements.txt | 3 ++- update.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index e2c6b9e..25647e9 100644 --- a/main.py +++ b/main.py @@ -164,7 +164,7 @@ if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True): # Setup the bot object intents = discord.Intents.default() -intents.message_content = True if func.settings.bot_prefix else False +intents.message_content = False if func.settings.bot_prefix is None else True intents.members = func.settings.ipc_client.get("enable", False) intents.voice_states = True diff --git a/requirements.txt b/requirements.txt index 9ba1748..386fa6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ tldextract==3.2.1 validators==0.18.2 humanize==4.0.0 beautifulsoup4==4.11.1 -psutil==5.9.8 \ No newline at end of file +psutil==5.9.8 +aiohttp==3.9.5 \ No newline at end of file diff --git a/update.py b/update.py index 0c0c4b2..6e71ae9 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.6.9" +__version__ = "v2.7.0b1" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" From 2ddf4ad55c73520e15ff48e677b21fb783b186f7 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:47:43 +0800 Subject: [PATCH 02/22] Allowed owner to use the bot without a prefix --- main.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 25647e9..38495a0 100644 --- a/main.py +++ b/main.py @@ -139,9 +139,15 @@ class CommandCheck(discord.app_commands.CommandTree): return True -async def get_prefix(bot, message: discord.Message): +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) + prefix = settings.get("prefix", func.settings.bot_prefix) + + # Allow owner to use the bot without a prefix + if await bot.is_owner(message.author) and not message.content.startswith(prefix): + return "" + + return prefix # Loading settings and logger func.settings = Settings(func.open_json("settings.json")) From 47fe8a9af43952042de7e26a29bf40fb47920f3a Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:13:45 +0800 Subject: [PATCH 03/22] Allowed bot access user use command without prefix --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 38495a0..3ec48b3 100644 --- a/main.py +++ b/main.py @@ -144,7 +144,7 @@ async def get_prefix(bot: commands.Bot, message: discord.Message) -> str: prefix = settings.get("prefix", func.settings.bot_prefix) # Allow owner to use the bot without a prefix - if await bot.is_owner(message.author) and not message.content.startswith(prefix): + if not message.content.startswith(prefix) and (await bot.is_owner(message.author) or message.author.id in func.settings.bot_access_user): return "" return prefix From 59e021d3755f1c51a4e1af2f8141ac7fcc512980 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:44:00 +0800 Subject: [PATCH 04/22] Fixed bugs --- main.py | 2 +- voicelink/placeholders.py | 2 +- voicelink/player.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 3ec48b3..3c959bd 100644 --- a/main.py +++ b/main.py @@ -144,7 +144,7 @@ async def get_prefix(bot: commands.Bot, message: discord.Message) -> str: prefix = settings.get("prefix", func.settings.bot_prefix) # Allow owner to use the bot without a prefix - if not message.content.startswith(prefix) and (await bot.is_owner(message.author) or message.author.id in func.settings.bot_access_user): + if prefix and not message.content.startswith(prefix) and (await bot.is_owner(message.author) or message.author.id in func.settings.bot_access_user): return "" return prefix diff --git a/voicelink/placeholders.py b/voicelink/placeholders.py index d2d8632..92c577d 100644 --- a/voicelink/placeholders.py +++ b/voicelink/placeholders.py @@ -34,7 +34,7 @@ class Placeholders: "track_color": self.track_color, "track_requester_id": self.track_requester_id, "track_requester_name": self.track_requester_name, - "track_requester_metion": self.track_requester_mention, + "track_requester_mention": self.track_requester_mention, "track_requester_avatar": self.track_requester_avatar, "track_source_name": self.track_source_name, "track_source_emoji": self.track_source_emoji, diff --git a/voicelink/player.py b/voicelink/player.py index d58dfbb..48fc399 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -514,7 +514,7 @@ class Player(VoiceProtocol): if track.spotify: if not track.original: - search_results = await self._node.get_tracks(f"ytmsearch:{track.author} - {track.title}", requester=track.requester) + search_results = await self._node.get_tracks(f"ytsearch:{track.author} - {track.title}", requester=track.requester) if not search_results: raise TrackLoadError("Can't find a playable source!") track.original = search_results[0] From 8b583b52c6734f4a55f2471d70824df0645197ad Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:46:46 +0800 Subject: [PATCH 05/22] Fixed bugs --- Dockerfile | 12 ++++-------- langs/EN.json | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index d500667..91db733 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ -# Use an official Python runtime as a base image FROM python:3.12-slim -# Install build dependencies and supervisor -RUN apt-get update -y && apt-get install -y gcc python3-dev supervisor +# Install build dependencies +RUN apt-get update -y && apt-get install -y gcc python3-dev # Set the working directory to /app WORKDIR /app @@ -13,8 +12,5 @@ COPY . /app # Install any needed packages specified in requirements.txt RUN pip install --no-cache-dir -r requirements.txt -# Copy the supervisor configuration file -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Run supervisor to manage both processes main.py and webapp.py -CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file +# Run main.py when the container launches +CMD ["python", "-u", "main.py"] \ No newline at end of file diff --git a/langs/EN.json b/langs/EN.json index 00dad17..1552ed7 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -124,7 +124,7 @@ "live": "LIVE", "playlistLoad": " 🎶 Added the playlist **{0}** with `{1}` songs to the queue.", "trackLoad": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to begin playing.\n", - "trackLoad_pos": "Added **[{0}](<{1}>)** by **{3}** (`{3}`) to the queue at position **{4}**\n", + "trackLoad_pos": "Added **[{0}](<{1}>)** by **{2}** (`{3}`) to the queue at position **{4}**\n", "searchTitle": "Search Query: {0}", "searchDesc": "➥ Platform: {0} **{1}**\n➥ Results: **{2}**\n\n{3}", From 9d505caf7f28a5490c563009d49b54ac76ed6ff4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:32:02 +0800 Subject: [PATCH 06/22] Updated voicelink enum --- cogs/basic.py | 66 +++++++++++++++++--------------------------- ipc/methods.py | 4 +-- views/controller.py | 10 +++---- voicelink/enums.py | 47 +++++++++++++++++-------------- voicelink/objects.py | 4 +-- voicelink/player.py | 45 ++++++++++++++---------------- voicelink/pool.py | 18 +++++++----- voicelink/queue.py | 10 +++---- 8 files changed, 97 insertions(+), 107 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index f25b39d..80b07a2 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -40,17 +40,11 @@ from function import ( logger ) +from voicelink import SearchType, LoopType from addons import lyricsPlatform from views import SearchView, ListView, LinkView, LyricsView, HelpView from validators import url -searchPlatform = { - "youtube": "ytsearch", - "youtubemusic": "ytmsearch", - "soundcloud": "scsearch", - "apple": "amsearch", -} - async def nowplay(ctx: commands.Context, player: voicelink.Player): track = player.current if not track: @@ -94,22 +88,22 @@ class Basic(commands.Cog): async def play_autocomplete(self, interaction: discord.Interaction, current: str) -> list: if voicelink.pool.URL_REGEX.match(current): return [app_commands.Choice(name=current, value=current)] + if current: + node = voicelink.NodePool.get_node() + if node and node.spotify_client: + try: + tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user) + 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] + except voicelink.TrackLoadError: + return [] + history: dict[str, str] = {} for track_id in reversed(await get_user(interaction.user.id, "history")): track_dict = voicelink.decode(track_id) history[track_dict["identifier"]] = track_dict history_tracks = [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] - if not current: - return history_tracks - - node = voicelink.NodePool.get_node() - if node and node.spotify_client: - try: - tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user) - 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] - except voicelink.TrackLoadError: - return [] + return history_tracks @commands.hybrid_command(name="connect", aliases=get_aliases("connect")) @app_commands.describe(channel="Provide a channel to connect.") @@ -209,14 +203,11 @@ class Basic(commands.Cog): platform="Select the platform you want to search." ) @app_commands.choices(platform=[ - app_commands.Choice(name="Youtube", value="Youtube"), - app_commands.Choice(name="Youtube Music", value="YoutubeMusic"), - app_commands.Choice(name="Spotify", value="Spotify"), - app_commands.Choice(name="SoundCloud", value="SoundCloud"), - app_commands.Choice(name="Apple Music", value="Apple") + app_commands.Choice(name=search_type.name.replace("_", " ").title(), value=search_type.name) + for search_type in SearchType ]) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) - async def search(self, ctx: commands.Context, *, query: str, platform: str = "Youtube"): + async def search(self, ctx: commands.Context, *, query: str, platform: str = SearchType.YOUTUBE.name): "Loads your input and added it to the queue." player: voicelink.Player = ctx.guild.voice_client if not player: @@ -227,14 +218,8 @@ class Basic(commands.Cog): if url(query): return await send(ctx, "noLinkSupport", ephemeral=True) - - platform = platform.lower() - if platform != 'spotify': - query_platform = searchPlatform.get(platform, 'ytsearch') + f":{query}" - tracks = await player.get_tracks(query=query_platform, requester=ctx.author) - else: - tracks = await player.node.spotifySearch(query=query, requester=ctx.author) - + + tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=SearchType[platform] if platform in SearchType.__members__ else SearchType.YOUTUBE) if not tracks: return await send(ctx, "noTrackFound") @@ -332,8 +317,8 @@ class Basic(commands.Cog): await ctx.send(e) finally: - if player.queue._repeat.mode == voicelink.LoopType.track: - await player.set_repeat(voicelink.LoopType.off.name) + if player.queue._repeat.mode == voicelink.LoopType.TRACK: + await player.set_repeat(voicelink.LoopType.OFF) await player.stop() if player.is_playing else await player.do_next() @@ -410,8 +395,8 @@ class Basic(commands.Cog): player.queue.skipto(index) await send(ctx, "skipped", ctx.author) - if player.queue._repeat.mode == voicelink.LoopType.track: - await player.set_repeat(voicelink.LoopType.off.name) + if player.queue._repeat.mode == voicelink.LoopType.TRACK: + await player.set_repeat(voicelink.LoopType.OFF) await player.stop() @@ -443,8 +428,8 @@ class Basic(commands.Cog): await player.stop() await send(ctx, "backed", ctx.author) - if player.queue._repeat.mode == voicelink.LoopType.track: - await player.set_repeat(voicelink.LoopType.off.name) + if player.queue._repeat.mode == voicelink.LoopType.TRACK: + await player.set_repeat(voicelink.LoopType.OFF) @commands.hybrid_command(name="seek", aliases=get_aliases("seek")) @app_commands.describe(position="Input position. Exmaple: 1:20.") @@ -613,9 +598,8 @@ class Basic(commands.Cog): @commands.hybrid_command(name="loop", aliases=get_aliases("loop")) @app_commands.describe(mode="Choose a looping mode.") @app_commands.choices(mode=[ - app_commands.Choice(name='Off', value='off'), - app_commands.Choice(name='Track', value='track'), - app_commands.Choice(name='Queue', value='queue') + app_commands.Choice(name=loop_type.name.title(), value=loop_type.name) + for loop_type in LoopType ]) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def loop(self, ctx: commands.Context, mode: str): @@ -627,7 +611,7 @@ class Basic(commands.Cog): if not player.is_privileged(ctx.author): return await send(ctx, "missingPerms_mode", ephemeral=True) - await player.set_repeat(mode, ctx.author) + await player.set_repeat(LoopType[mode] if mode in LoopType.__members__ else LoopType.OFF, ctx.author) await send(ctx, "repeat", mode.capitalize()) @commands.hybrid_command(name="clear", aliases=get_aliases("clear")) diff --git a/ipc/methods.py b/ipc/methods.py index 9522d5e..22493eb 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -178,8 +178,8 @@ async def skipTo(player: Player, member: Member, data: Dict) -> None: if index > 1: player.queue.skipto(index) - if player.queue._repeat.mode == LoopType.track: - await player.set_repeat(LoopType.off.name) + if player.queue._repeat.mode == LoopType.TRACK: + await player.set_repeat(LoopType.OFF) await player.stop() async def backTo(player: Player, member: Member, data: Dict) -> None: diff --git a/views/controller.py b/views/controller.py index 3c75aa5..7a9c409 100644 --- a/views/controller.py +++ b/views/controller.py @@ -82,8 +82,8 @@ class Back(ControlButton): await self.send(interaction, "backed", interaction.user) - if self.player.queue._repeat.mode == voicelink.LoopType.track: - await self.player.set_repeat(voicelink.LoopType.off.name) + if self.player.queue._repeat.mode == voicelink.LoopType.TRACK: + await self.player.set_repeat(voicelink.LoopType.OFF) class Resume(ControlButton): def __init__(self, **kwargs): @@ -140,8 +140,8 @@ class Skip(ControlButton): await self.send(interaction, "skipped", interaction.user) - if self.player.queue._repeat.mode == voicelink.LoopType.track: - await self.player.set_repeat(voicelink.LoopType.off.name) + if self.player.queue._repeat.mode == voicelink.LoopType.TRACK: + await self.player.set_repeat(voicelink.LoopType.OFF) await self.player.stop() class Stop(ControlButton): @@ -222,7 +222,7 @@ class Loop(ControlButton): self.emoji = self.get_next_loop_emoji(self.player) await interaction.response.edit_message(view=self.view) - await self.send(interaction, 'repeat', mode.capitalize()) + await self.send(interaction, 'repeat', mode.name.capitalize()) class VolumeUp(ControlButton): def __init__(self, **kwargs): diff --git a/voicelink/enums.py b/voicelink/enums.py index c8c53fa..9fb9578 100644 --- a/voicelink/enums.py +++ b/voicelink/enums.py @@ -26,35 +26,42 @@ from enum import Enum, auto class LoopType(Enum): """The enum for the different loop types for Voicelink - LoopType.off: 1 - LoopType.track: 2 - LoopType.queue: 3 + LoopType.OFF: 1 + LoopType.TRACK: 2 + LoopType.QUEUE: 3 """ - off = auto() - track = auto() - queue = auto() + OFF = auto() + TRACK = auto() + QUEUE = auto() class SearchType(Enum): """The enum for the different search types for Voicelink. This feature is exclusively for the Spotify search feature of Voicelink. If you are not using this feature, this class is not necessary. - SearchType.ytsearch searches using regular Youtube, + SearchType.YOUTUBE searches using regular Youtube, which is best for all scenarios. - SearchType.ytmsearch searches using YouTube Music, + SearchType.YOUTUBE_MUSIC searches using YouTube Music, which is best for getting audio-only results. + + SearchType.SPOTIFY searches using Spotify, + which is an alternative to YouTube or YouTube Music. - SearchType.scsearch searches using SoundCloud, + SearchType.SOUNDCLOUD searches using SoundCloud, + which is an alternative to YouTube or YouTube Music. + + SearchType.APPLE_MUSIC searches using Apple Music, which is an alternative to YouTube or YouTube Music. """ - ytsearch = "ytsearch" - ytmsearch = "ytmsearch" - scsearch = "scsearch" - amsearch = "amsearch" + YOUTUBE = "ytsearch" + YOUTUBE_MUSIC = "ytmsearch" + SPOTIFY = "spsearch" + SOUNDCLOUD = "scsearch" + APPLE_MUSIC = "amsearch" def __str__(self) -> str: return self.value @@ -62,10 +69,10 @@ class SearchType(Enum): class RequestMethod(Enum): """The enum for the different request methods in Voicelink """ - get = "get" - patch = "patch" - delete = "delete" - post = "post" + GET = "get" + PATCH = "patch" + DELETE = "delete" + POST = "post" def __str__(self) -> str: return self.value @@ -86,9 +93,9 @@ class NodeAlgorithm(Enum): """ # We don't have to define anything special for these, since these just serve as flags - by_ping = auto() - by_region = auto() - by_players = auto() + BY_PING = auto() + BY_REGION = auto() + BY_PLAYERS = auto() def __str__(self) -> str: return self.value \ No newline at end of file diff --git a/voicelink/objects.py b/voicelink/objects.py index de5e1bd..baa9927 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -72,7 +72,7 @@ class Track: track_id: str = None, info: dict, requester: Member, - search_type: SearchType = SearchType.ytsearch, + search_type: SearchType = SearchType.YOUTUBE, spotify_track = None, ): self._track_id: Optional[str] = track_id @@ -88,7 +88,7 @@ class Track: self.artist_id: Optional[list] = info.get("artist_id") self.original: Optional[Track] = None if self.spotify else self - self._search_type: SearchType = SearchType.ytsearch if self.spotify else search_type + self._search_type: SearchType = SearchType.YOUTUBE if self.spotify else search_type self.spotify_track: Track = spotify_track self.thumbnail: str = info.get("artworkUrl") diff --git a/voicelink/player.py b/voicelink/player.py index 48fc399..1671e15 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -289,7 +289,7 @@ class Player(VoiceProtocol): "sessionId": state['sessionId'], } - await self.send(method=RequestMethod.patch, data={"voice": data}) + await self.send(method=RequestMethod.PATCH, data={"voice": data}) 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): @@ -450,7 +450,7 @@ class Player(VoiceProtocol): query: str, *, requester: Member, - search_type: SearchType = SearchType.ytsearch + search_type: SearchType = SearchType.YOUTUBE ) -> Union[List[Track], Playlist]: """Fetches tracks from the node's REST api to parse into Lavalink. @@ -474,7 +474,7 @@ class Player(VoiceProtocol): async def stop(self): """Stops the currently playing track.""" self._current = None - await self.send(method=RequestMethod.patch, data={'encodedTrack': None}) + await self.send(method=RequestMethod.PATCH, data={'encodedTrack': None}) async def disconnect(self, *, force: bool = False): """Disconnects the player from voice.""" @@ -498,7 +498,7 @@ class Player(VoiceProtocol): assert self.channel is None and not self.is_connected self._node._players.pop(self.guild.id) - await self.send(method=RequestMethod.delete) + await self.send(method=RequestMethod.DELETE) async def play( self, @@ -514,7 +514,7 @@ class Player(VoiceProtocol): if track.spotify: if not track.original: - search_results = await self._node.get_tracks(f"ytsearch:{track.author} - {track.title}", requester=track.requester) + search_results = await self._node.get_tracks(f"{track.author} - {track.title}", requester=track.requester) if not search_results: raise TrackLoadError("Can't find a playable source!") track.original = search_results[0] @@ -527,7 +527,7 @@ class Player(VoiceProtocol): if end or track.end_time: data["endTime"] = str(end if end else track.end_time) - await self.send(method=RequestMethod.patch, query=f"noReplace={ignore_if_playing}", data=data) + await self.send(method=RequestMethod.PATCH, query=f"noReplace={ignore_if_playing}", data=data) self._current = track @@ -601,7 +601,7 @@ class Player(VoiceProtocol): if position < 0 or position > self._current.original.length: raise TrackInvalidPosition("Seek position must be between 0 and the track length") - await self.send(method=RequestMethod.patch, data={"position": position}) + await self.send(method=RequestMethod.PATCH, data={"position": position}) if self.is_ipc_connected: await self.send_ws({"op": "updatePosition", "position": position}, requester) @@ -613,7 +613,7 @@ class Player(VoiceProtocol): self._paused = pause self.pause_votes.clear() if pause else self.resume_votes.clear() - await self.send(method=RequestMethod.patch, data={"paused": pause}) + await self.send(method=RequestMethod.PATCH, data={"paused": pause}) if self.is_ipc_connected: await self.send_ws({"op": "updatePause", "pause": pause}, requester) @@ -623,7 +623,7 @@ class Player(VoiceProtocol): async def set_volume(self, volume: int, requester: Member = None) -> int: """Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500.""" - await self.send(method=RequestMethod.patch, data={"volume": volume}) + await self.send(method=RequestMethod.PATCH, data={"volume": volume}) self._volume = volume if self.is_ipc_connected: @@ -668,24 +668,19 @@ class Player(VoiceProtocol): return moved_track - async def set_repeat(self, mode: str = None, requester: Member = None) -> str: + async def set_repeat(self, mode: LoopType = None, requester: Member = None) -> LoopType: if not mode: - mode = self.queue._repeat.next().name - - is_found = False - for type in LoopType: - if type.name.lower() == mode.lower(): - self.queue._repeat.set_mode(type) - is_found = True - break - - if not is_found: + mode = self.queue._repeat.next() + + if not isinstance(mode, LoopType): raise VoicelinkException("Invalid repeat mode.") + self.queue._repeat.set_mode(mode) + if self.is_ipc_connected: - await self.send_ws({"op": "repeatTrack", "repeatMode": mode}, requester) + await self.send_ws({"op": "repeatTrack", "repeatMode": mode.name.lower()}, requester) - self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been update the repeat mode to {mode}.") + self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been update the repeat mode to {mode.name.lower()}.") return mode async def add_filter(self, filter: Filter, requester: Member = None, fast_apply: bool = False) -> Filters: @@ -695,7 +690,7 @@ class Player(VoiceProtocol): raise FilterTagAlreadyInUse(self.get_msg("FilterTagAlreadyInUse")) payload = self._filters.get_all_payloads() - await self.send(method=RequestMethod.patch, data={"filters": payload}) + await self.send(method=RequestMethod.PATCH, data={"filters": payload}) if fast_apply: await self.seek(self.position) @@ -725,7 +720,7 @@ class Player(VoiceProtocol): async def remove_filter(self, filter_tag: str, requester: Member = None, fast_apply: bool = False) -> Filters: self._filters.remove_filter(filter_tag=filter_tag) payload = self._filters.get_all_payloads() - await self.send(method=RequestMethod.patch, data={"filters": payload}) + await self.send(method=RequestMethod.PATCH, data={"filters": payload}) if fast_apply: await self.seek(self.position) @@ -744,7 +739,7 @@ class Player(VoiceProtocol): raise FilterInvalidArgument("You must have filters applied first in order to use this method.") self._filters.reset_filters() - await self.send(method=RequestMethod.patch, data={"filters": {}}) + await self.send(method=RequestMethod.PATCH, data={"filters": {}}) if fast_apply: await self.seek(self.position) diff --git a/voicelink/pool.py b/voicelink/pool.py index 5b258f8..518750e 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -271,7 +271,7 @@ class Node: if resp.status >= 300: raise NodeException(f"Getting errors from Lavalink REST api") - if method == RequestMethod.delete: + if method == RequestMethod.DELETE: return await resp.json(content_type=None) return await resp.json() @@ -286,7 +286,7 @@ class Node: self._task = self._bot.loop.create_task(self._listen()) self._available = True - self._info = NodeInfo(await self.send(RequestMethod.get, query="info")) + self._info = NodeInfo(await self.send(RequestMethod.GET, query="info")) self._logger.info(f"Node [{self._identifier}] is connected!") @@ -369,7 +369,7 @@ class Node: query: str, *, requester: Member, - search_type: SearchType = SearchType.ytsearch + search_type: SearchType = SearchType.YOUTUBE ) -> Union[List[Track], Playlist]: """Fetches tracks from the node's REST api to parse into Lavalink. @@ -380,8 +380,12 @@ class Node: Context object on any track you search. """ - if not URL_REGEX.match(query) and not re.match(r"(?:ytm?|sc)search:.", query): - query = f"{search_type}:{query}" + if not URL_REGEX.match(query): + if search_type == SearchType.SPOTIFY: + return await self.spotifySearch(query=query, requester=requester) + + else: + query = f"{search_type}:{query}" if SPOTIFY_URL_REGEX.match(query): try: @@ -509,7 +513,7 @@ class Node: Track( track_id=None, requester=requester, - search_type=SearchType.ytsearch, + search_type=SearchType.YOUTUBE, spotify_track=track, info=track.to_dict() ) @@ -525,7 +529,7 @@ class Node: tracks = [ Track( track_id=None, - search_type=SearchType.ytsearch, + search_type=SearchType.YOUTUBE, spotify_track=track, info=track.to_dict(), requester=self.bot.user diff --git a/voicelink/queue.py b/voicelink/queue.py index 79bd41a..10beee0 100644 --- a/voicelink/queue.py +++ b/voicelink/queue.py @@ -25,7 +25,7 @@ from .exceptions import QueueFull, OutofList from .objects import Track from .enums import LoopType -from typing import Optional, Tuple, List, Callable, Dict +from typing import Optional, Tuple, Callable, Dict, List from itertools import cycle from discord import Member @@ -65,16 +65,16 @@ class Queue: def get(self) -> Optional[Track]: track = None try: - track = self._queue[self._position - 1 if self._repeat.mode == LoopType.track else self._position] - if self._repeat.mode != LoopType.track: + track = self._queue[self._position - 1 if self._repeat.mode == LoopType.TRACK else self._position] + if self._repeat.mode != LoopType.TRACK: self._position += 1 except: - if self._repeat.mode == LoopType.queue: + if self._repeat.mode == LoopType.QUEUE: try: track = self._queue[self._repeat_position] self._position = self._repeat_position + 1 except IndexError: - self._repeat.set_mode(LoopType.off) + self._repeat.set_mode(LoopType.OFF) return track From f75c67353904a11b53eb9d1fbcc7b91daf66d3fe Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:44:12 +0800 Subject: [PATCH 07/22] Create bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..d89720d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,52 @@ +name: Bug Report +description: Report broken or incorrect behaviour +labels: unconfirmed bug +body: + - type: markdown + attributes: + value: > + Thank you for submitting a bug report! For real-time support, please join our [Discord community](https://discord.gg/wRCgB7vBQv). + This form is specifically for reporting bugs, and we appreciate your understanding! + + **Note:** This form is for bugs only! + - type: input + attributes: + label: Summary + description: A simple summary of your bug report + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: What you did to make it happen. + validations: + required: true + - type: textarea + attributes: + label: System Information + description: > + Run `python -m discord -v` and paste this information below. This command requires v1.1.0 or higher of the library. + If this errors out, please provide basic information about your system, such as your operating system and Python version. + validations: + required: true + - type: textarea + attributes: + label: Error Logs + description: Paste the loggings from your console. Include only relevant errors or warnings. + validations: + required: true + - type: checkboxes + attributes: + label: Checklist + description: Let's ensure you've done your due diligence when reporting this issue! + options: + - label: I have searched the open issues for duplicates. + required: true + - label: I have included the entire traceback, if possible. + required: true + - label: I have removed my token from display, if visible. + required: true + - type: textarea + attributes: + label: Additional Context + description: If there is anything else to say, please do so here. \ No newline at end of file From 94c5fd4c27c0224ddd9d8ca1f14d05fbce3ac7c4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:33:29 +0800 Subject: [PATCH 08/22] Added request song channel feature --- .github/workflows/docker-image.yml | 10 +++++ cogs/basic.py | 67 +++++++++++++++++++----------- cogs/playlist.py | 6 +-- cogs/settings.py | 41 +++++++++++++++--- function.py | 51 +++++++++++++++++------ langs/CH.json | 2 +- langs/DE.json | 2 +- langs/EN.json | 4 +- langs/ES.json | 2 +- langs/JA.json | 2 +- langs/KO.json | 2 +- langs/RU.json | 2 +- langs/UA.json | 2 +- main.py | 15 +++++++ views/controller.py | 4 +- 15 files changed, 155 insertions(+), 57 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 14bc7d5..3a99ae8 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -17,3 +17,13 @@ jobs: - uses: actions/checkout@v4 - name: Build the Docker image run: docker build . --file Dockerfile --tag vocard:latest + + - name: Log in to GitHub Docker Registry + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push the Docker image + run: | + docker tag vocard:latest ghcr.io/chocomeow/vocard:latest + docker push ghcr.io/chocomeow/vocard:latest diff --git a/cogs/basic.py b/cogs/basic.py index 80b07a2..ac876dd 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) @@ -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..2e99ec4 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -58,7 +58,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 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) @@ -170,7 +170,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 +226,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 +243,38 @@ 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 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") + + await update_settings(ctx.guild.id, {"$set": {'music_request_channel': { + "text_channel_id": channel.id + }}}) + await send(ctx, "createSongRequestChannel", channel.mention) @app_commands.command(name="debug") async def debug(self, interaction: discord.Interaction): @@ -268,7 +297,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..bed42e0 100644 --- a/function.py +++ b/function.py @@ -143,30 +143,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..ead501f 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -186,5 +186,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..13d1397 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -186,5 +186,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..7f27e86 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -7,6 +7,7 @@ "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.", @@ -186,5 +187,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..272de87 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -186,5 +186,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..3108898 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -186,5 +186,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..42424e6 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -186,5 +186,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..b9a9118 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -185,5 +185,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..fae103b 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -185,5 +185,5 @@ "invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.", "invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.", - "SetStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд." + "setStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд." } \ No newline at end of file diff --git a/main.py b/main.py index 3c959bd..9c4e55b 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ import os import aiohttp import update import logging +import voicelink import function as func from discord.ext import commands @@ -41,6 +42,20 @@ class Vocard(commands.Bot): return await message.channel.send("I don't have a bot prefix set.") await message.channel.send(f"My prefix is `{prefix}`") + 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"): + try: + ctx = await self.get_context(message) + cmd = self.get_command("play") + await cmd(ctx, query=message.content) + + 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: 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 ) From 1576ba79572fe1d897d047de214b893d074f3e89 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:34:08 +0800 Subject: [PATCH 09/22] Update update.py --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/" From 3d377fd5ab476b98f5b68465bf3e71b0de57ed65 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:42:55 +0800 Subject: [PATCH 10/22] Added music controller into request channel --- .github/workflows/docker-image.yml | 4 +- cogs/settings.py | 7 +- voicelink/player.py | 128 ++++++++++++++++++++--------- 3 files changed, 97 insertions(+), 42 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 3a99ae8..c96d10f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -2,11 +2,9 @@ name: Docker Image CI on: push: - branches: [ "**" ] + branches: [ "main" ] paths-ignore: - '**.md' - pull_request: - branches: [ "**" ] jobs: build: diff --git a/cogs/settings.py b/cogs/settings.py index 2e99ec4..6a88aef 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -270,9 +270,14 @@ class Settings(commands.Cog, name="settings"): 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 + "text_channel_id": channel.id, + "controller_msg_id": message.id, }}}) await send(ctx, "createSongRequestChannel", channel.mention) diff --git a/voicelink/player.py b/voicelink/player.py index 1671e15..9756d5e 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -36,8 +36,8 @@ from discord import ( VoiceProtocol, Member, Message, + PartialMessage, Interaction, - errors ) from discord.ext import commands @@ -112,7 +112,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 +126,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 +180,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 +218,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 +247,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 +274,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 +304,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 +321,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 +342,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 +357,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 +410,45 @@ 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")) + await self.controller.edit(embed=embed, view=view) + + # 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 +459,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 +504,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 +579,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 +597,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 +629,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 +695,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 +706,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 +715,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 +731,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 +753,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 +784,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 +802,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: @@ -791,6 +841,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 +861,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) From cdd5313293b7799de99a7d6c1cff4b35f9cf40d6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:49:22 +0800 Subject: [PATCH 11/22] Fixed controller not found in request channel --- voicelink/player.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/voicelink/player.py b/voicelink/player.py index 9756d5e..ac166a3 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -38,6 +38,7 @@ from discord import ( Message, PartialMessage, Interaction, + errors ) from discord.ext import commands @@ -423,7 +424,10 @@ class Player(VoiceProtocol): 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")) - await self.controller.edit(embed=embed, view=view) + 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: @@ -813,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 From 7d83c02cf3b49804cea22668a35c10d5b67706b3 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:02:11 +0800 Subject: [PATCH 12/22] Added missing intent error message --- cogs/settings.py | 6 ++++++ langs/CH.json | 1 + langs/DE.json | 1 + langs/EN.json | 1 + langs/ES.json | 1 + langs/JA.json | 1 + langs/KO.json | 1 + langs/RU.json | 1 + langs/UA.json | 1 + 9 files changed, 14 insertions(+) diff --git a/cogs/settings.py b/cogs/settings.py index 6a88aef..ed47037 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -65,6 +65,9 @@ class Settings(commands.Cog, name="settings"): @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) @@ -255,6 +258,9 @@ class Settings(commands.Cog, name="settings"): @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 = { diff --git a/langs/CH.json b/langs/CH.json index ead501f..ca8b4aa 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -10,6 +10,7 @@ "noPlaySource": "找不到任何可播放的來源!", "noPlayer": "在此伺服器上找不到播放器。", "notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。", + "missingIntents": "抱歉,此命令無法執行,因為機器人缺少所需的請求意圖:`({0})`.", "languageNotFound": "找不到語言包!請選擇一個現有的語言包。", "changedLanguage": "已成功切換到 `{0}` 語言包。", "setPrefix": "完成!我的前綴在您的伺服器中現在是 `{0}`。嘗試運行 `{1}ping` 來測試它。", diff --git a/langs/DE.json b/langs/DE.json index 13d1397..99d1d6e 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -10,6 +10,7 @@ "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.", diff --git a/langs/EN.json b/langs/EN.json index 7f27e86..e11767f 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -11,6 +11,7 @@ "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.", diff --git a/langs/ES.json b/langs/ES.json index 272de87..f9b1161 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -10,6 +10,7 @@ "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.", diff --git a/langs/JA.json b/langs/JA.json index 3108898..b00070f 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -10,6 +10,7 @@ "noPlaySource": "再生可能なソースが見つかりません!", "noPlayer": "このサーバーにプレイヤーが見つかりません。", "notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。", + "missingIntents": "申し訳ありませんが、このコマンドは実行できません。ボットに必要なリクエストインテントが不足しています:`({0})`.", "languageNotFound": "言語パックが見つかりません。既存の言語パックを選択してください。", "changedLanguage": "「{0}」言語パックに正常に変更しました。", "setPrefix": "完了!あなたのサーバーのプレフィックスは今や「{0}」です。 `{1}ping`を実行してテストしてみてください。", diff --git a/langs/KO.json b/langs/KO.json index 42424e6..204fa37 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -10,6 +10,7 @@ "noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!", "noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.", "notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.", + "missingIntents": "죄송하지만 이 명령을 실행할 수 없습니다. 봇에 필요한 요청 의도가 없습니다:`({0})`.", "languageNotFound": "언어 팩을 찾을 수 없습니다! 기존 언어 팩을 선택하십시오.", "changedLanguage": "성공적으로 `{0}` 언어 팩으로 변경되었습니다.", "setPrefix": "완료되었습니다! 이 서버에서 내 접두사는 이제 `{0}`입니다. `{1}ping`을 실행하여 테스트해보세요.", diff --git a/langs/RU.json b/langs/RU.json index b9a9118..b39f43d 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -10,6 +10,7 @@ "noPlaySource": "Не получилось найти рабочие источники!", "noPlayer": "На этом сервере не найдено ни одного активного плеера.", "notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.", + "missingIntents": "Извините, но эту команду нельзя выполнить, так как у бота отсутствует необходимый запрос намерения: `({0})`.", "languageNotFound": "Языковой пакет не найден! Пожалуйста, выберите существующий языковой пакет.", "changedLanguage": "Язык успешно изменен на `{0}`.", "setPrefix": "Готово! Мой префикс на вашем сервере теперь `{0}`. Попробуйте запустить `{1}ping`, чтобы проверить его.", diff --git a/langs/UA.json b/langs/UA.json index fae103b..d6d6912 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -10,6 +10,7 @@ "noPlaySource": "Неможливо знайти робочі джерела!", "noPlayer": "На цьому сервері не знайдено жодного активного плеєра.", "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", + "missingIntents": "Вибачте, але цю команду не можна виконати, оскільки у бота відсутній необхідний запит на інтенцію: `({0})`.", "languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.", "changedLanguage": "Успішно змінено на мовний пакет `{0}`.", "setPrefix": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.", From 11fcbfa222c9863535adb3f5b81fa6d45698c422 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:59:46 +0800 Subject: [PATCH 13/22] Added warning logs if missing translation --- cogs/basic.py | 4 ++-- function.py | 2 ++ local_langs/zh-TW.json | 23 ++++++++++++++++++++--- main.py | 19 ++++++++++++++++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index ac876dd..fa885dd 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -670,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." @@ -691,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." diff --git a/function.py b/function.py index bed42e0..f9a1d0b 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': { 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 9c4e55b..59b52c3 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,6 @@ import os import aiohttp import update import logging -import voicelink import function as func from discord.ext import commands @@ -22,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): @@ -102,6 +111,9 @@ class Vocard(commands.Bot): await self.tree.set_translator(Translator()) await self.tree.sync() + 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("------------------") func.logger.info(f"Logging As {self.user}") @@ -113,6 +125,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) From f14f623fe18c6c8ce5a17daf94d14e9e4e1b0739 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:16:57 +0800 Subject: [PATCH 14/22] Update main.py --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 59b52c3..511aa41 100644 --- a/main.py +++ b/main.py @@ -89,6 +89,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'): @@ -106,11 +109,8 @@ 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}'") From eea026a112ad6b155b7cf73ea68a3783b72c41ce Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:57:11 +0800 Subject: [PATCH 15/22] Added support for audio attachment in request channel --- main.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 511aa41..d2ff78c 100644 --- a/main.py +++ b/main.py @@ -42,23 +42,31 @@ 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: - ctx = await self.get_context(message) cmd = self.get_command("play") - await cmd(ctx, query=message.content) + 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) From bd598c3ea1c39eac0fcc3dcf3de3ec6a2cd8fcca Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:57:46 +0800 Subject: [PATCH 16/22] Added others source settings --- function.py | 2 +- settings Example.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/function.py b/function.py index f9a1d0b..da6a72f 100644 --- a/function.py +++ b/function.py @@ -108,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]: 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": { From dc919ecca224aad3b25933c410272b8750a0ff3c Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:10:21 +0800 Subject: [PATCH 17/22] Added 'noCreatePermission' for additional translations --- langs/CH.json | 1 + langs/DE.json | 1 + langs/ES.json | 1 + langs/JA.json | 1 + langs/KO.json | 1 + langs/RU.json | 1 + langs/UA.json | 1 + 7 files changed, 7 insertions(+) diff --git a/langs/CH.json b/langs/CH.json index ca8b4aa..ff6d4be 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -7,6 +7,7 @@ "noChannel": "沒有語音頻道可供連接。請提供一個語音頻道或加入一個語音頻道。", "alreadyConnected": "已經連接到語音頻道。", "noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。", + "noCreatePermission": "抱歉!我沒有權限建立歌曲請求頻道。", "noPlaySource": "找不到任何可播放的來源!", "noPlayer": "在此伺服器上找不到播放器。", "notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。", diff --git a/langs/DE.json b/langs/DE.json index 99d1d6e..5aa9880 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -7,6 +7,7 @@ "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.", diff --git a/langs/ES.json b/langs/ES.json index f9b1161..990cdea 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -7,6 +7,7 @@ "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.", diff --git a/langs/JA.json b/langs/JA.json index b00070f..9544615 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -7,6 +7,7 @@ "noChannel": "接続する音声チャンネルがありません。提供するか、参加してください。", "alreadyConnected": "すでに音声チャンネルに接続しています。", "noPermission": "申し訳ありません!私はあなたの音声チャンネルに参加または話すための許可がありません。", + "noCreatePermission": "ごめんなさい!曲リクエストチャンネルを作成する権限がありません。", "noPlaySource": "再生可能なソースが見つかりません!", "noPlayer": "このサーバーにプレイヤーが見つかりません。", "notVote": "このコマンドにはあなたの投票が必要です!詳細については、/voteを入力してください。", diff --git a/langs/KO.json b/langs/KO.json index 204fa37..0dfd6c6 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -7,6 +7,7 @@ "noChannel": "연결할 음성 채널이 없습니다. 하나를 제공하거나 참여하십시오.", "alreadyConnected": "이미 음성 채널에 연결되어 있습니다.", "noPermission": "죄송합니다! 음성 채널에 참여하거나 말할 권한이 없습니다.", + "noCreatePermission": "죄송합니다! 노래 요청 채널을 생성할 권한이 없습니다.", "noPlaySource": "재생 가능한 소스를 찾을 수 없습니다!", "noPlayer": "이 서버에서 플레이어를 찾을 수 없습니다.", "notVote": "이 명령어를 실행하려면 투표해야합니다! 자세한 내용은 `/vote`를 입력하십시오.", diff --git a/langs/RU.json b/langs/RU.json index b39f43d..bbc9cca 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -7,6 +7,7 @@ "noChannel": "Нет голосового канала для подключения. Пожалуйста, укажите или присоединитесь к одному.", "alreadyConnected": "Уже подключен к голосовому каналу.", "noPermission": "Извините! У меня нет разрешения на подключение или разговор в вашем голосовом канале.", + "noCreatePermission": "Извините! У меня нет прав на создание канала для запроса песен.", "noPlaySource": "Не получилось найти рабочие источники!", "noPlayer": "На этом сервере не найдено ни одного активного плеера.", "notVote": "Эта команда требует вашего голоса! Введите `/vote` для получения дополнительной информации.", diff --git a/langs/UA.json b/langs/UA.json index d6d6912..df3a9e0 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -7,6 +7,7 @@ "noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.", "alreadyConnected": "Уже підключений до голосового каналу.", "noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.", + "noCreatePermission": "Вибачте! У мене немає прав для створення каналу запитів на пісні.", "noPlaySource": "Неможливо знайти робочі джерела!", "noPlayer": "На цьому сервері не знайдено жодного активного плеєра.", "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", From 19cb19676356fe895d625c213f6c98794739ab52 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:18:26 +0800 Subject: [PATCH 18/22] Added Lrclib lyrics support --- addons/lyrics.py | 21 ++++++++++++++++++++- voicelink/__init__.py | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/addons/lyrics.py b/addons/lyrics.py index 34de9f9..a1be965 100644 --- a/addons/lyrics.py +++ b/addons/lyrics.py @@ -49,6 +49,7 @@ Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTM Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5''' LYRIST_ENDPOINT = "https://lyrist.vercel.app/api/" +LRCLIB_ENDPOINT = "https://lrclib.net/api/" class LyricsPlatform(ABC): @abstractmethod @@ -196,8 +197,26 @@ class Lyrist(LyricsPlatform): except: return None +class Lrclib(LyricsPlatform): + async def get(self, url, params: dict = None) -> list[dict]: + try: + async with aiohttp.ClientSession() as session: + resp = await session.get(url=url, headers={'User-Agent': random.choice(userAgents)}, params=params) + if resp.status != 200: + return None + return await resp.json() + except: + return [] + + async def get_lyrics(self, title, artist): + params = {"q": f"{title} - {artist}"} + result = await self.get(LRCLIB_ENDPOINT + "search", params) + lyrics = result[0].get("plainLyrics", "") if result else "" + return {"default": lyrics} + lyricsPlatform: dict[str, LyricsPlatform] = { "a_zlyrics": A_ZLyrics, "genius": Genius, - "lyrist": Lyrist + "lyrist": Lyrist, + "lrclib": Lrclib } \ No newline at end of file diff --git a/voicelink/__init__.py b/voicelink/__init__.py index d48084f..e14e65a 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -24,7 +24,7 @@ SOFTWARE. __version__ = "1.4" __author__ = 'Vocard Development, Choco' __license__ = "MIT" -__copyright__ = "Copyright 2023 (c) Vocard Development, Choco" +__copyright__ = "Copyright 2023 - present (c) Vocard Development, Choco" from .enums import SearchType, LoopType from .events import * From 212450452fd638193e6796bb869c3d7f23e06592 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:47:35 +0800 Subject: [PATCH 19/22] Update lyrics.py --- addons/lyrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/lyrics.py b/addons/lyrics.py index a1be965..aa71d53 100644 --- a/addons/lyrics.py +++ b/addons/lyrics.py @@ -211,8 +211,8 @@ class Lrclib(LyricsPlatform): async def get_lyrics(self, title, artist): params = {"q": f"{title} - {artist}"} result = await self.get(LRCLIB_ENDPOINT + "search", params) - lyrics = result[0].get("plainLyrics", "") if result else "" - return {"default": lyrics} + if result: + return {"default": result[0].get("plainLyrics", "")} lyricsPlatform: dict[str, LyricsPlatform] = { "a_zlyrics": A_ZLyrics, From 749cc2d926d9457809cc1b4ef03687fe7ca04338 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:49:38 +0800 Subject: [PATCH 20/22] Merge branch 'beta' into request-song-channel --- .github/workflows/docker-image.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index c96d10f..938abca 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -5,10 +5,12 @@ on: branches: [ "main" ] paths-ignore: - '**.md' + +permissions: + packages: write + jobs: - build: - runs-on: ubuntu-latest steps: @@ -23,5 +25,5 @@ jobs: - name: Push the Docker image run: | - docker tag vocard:latest ghcr.io/chocomeow/vocard:latest - docker push ghcr.io/chocomeow/vocard:latest + docker tag vocard:latest ghcr.io/chocomeow/vocard:latest # Ensure lowercase + docker push ghcr.io/chocomeow/vocard:latest # Ensure lowercase From 5a22a930f551bfc0b3d47c28e0a16205b0fd50a0 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:33:49 +0800 Subject: [PATCH 21/22] Fixed loading issue for some Spotify playlists --- update.py | 2 +- voicelink/pool.py | 10 ---------- voicelink/spotify/client.py | 35 ++++++++++++++++++++++++----------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/update.py b/update.py index 81f53df..ddb060a 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.0b2" +__version__ = "v2.7.0b3" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" diff --git a/voicelink/pool.py b/voicelink/pool.py index 518750e..3902025 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -144,9 +144,6 @@ class Node: @property def spotify_client(self) -> Optional[spotify.Client]: if not self._spotify_client: - if not self._spotify_client_id or not self._spotify_client_secret: - return None - self._spotify_client = spotify.Client( self._spotify_client_id, self._spotify_client_secret ) @@ -389,13 +386,6 @@ class Node: if SPOTIFY_URL_REGEX.match(query): try: - if not self.spotify_client: - raise InvalidSpotifyClientAuthorization( - "You did not provide proper Spotify client authorization credentials. " - "If you would like to use the Spotify searching feature, " - "please obtain Spotify API credentials here: https://developer.spotify.com/" - ) - spotify_results = await self.spotify_client.search(query=query) except Exception as _: raise TrackLoadError("Not able to find the provided Spotify entity, is it private?") diff --git a/voicelink/spotify/client.py b/voicelink/spotify/client.py index 3d1dff2..c880e2f 100644 --- a/voicelink/spotify/client.py +++ b/voicelink/spotify/client.py @@ -26,12 +26,19 @@ import time import aiohttp from base64 import b64encode -from typing import List, Union, Dict, Any +from typing import ( + List, + Dict, + Union, + Optional +) + from .objects import Track, Album, Artist, Playlist, Category from .exceptions import InvalidSpotifyURL, SpotifyRequestException BASE_URL = "https://api.spotify.com/v1/" GRANT_URL = "https://accounts.spotify.com/api/token" +ANONYMOUS_GRANT_URL = "https://open.spotify.com/get_access_token" REQUEST_URL = BASE_URL + "{type}s/{id}" SEARCH_URL = BASE_URL + "search?q={query}&type={type}&limit={limit}" SUGGESTION_URL = BASE_URL + "recommendations?limit={limit}&seed_tracks={seed_tracks}" @@ -46,32 +53,40 @@ class Client: """ def __init__(self, client_id: str, client_secret: str) -> None: - self._client_id: str = client_id - self._client_secret: str = client_secret + self._client_id: Optional[str] = client_id + self._client_secret: Optional[str] = client_secret self.session: aiohttp.ClientSession = aiohttp.ClientSession() self._bearer_token: str = None self._expiry: int = 0 - self._auth_token: str = b64encode(f"{self._client_id}:{self._client_secret}".encode()) + self._auth_token: bytes = b64encode(f"{self._client_id}:{self._client_secret}".encode()) self._grant_headers: Dict[str, str] = {"Authorization": f"Basic {self._auth_token.decode()}"} self._bearer_headers: Dict[str, str] = None self._categories: List[Category] = [] async def _fetch_bearer_token(self) -> None: - _data = {"grant_type": "client_credentials"} + if self._client_id and self._client_secret: + url, data = GRANT_URL, {"grant_type": "client_credentials"} + else: + url, data = ANONYMOUS_GRANT_URL, None - async with self.session.post(GRANT_URL, data=_data, headers=self._grant_headers) as resp: + async with self.session.post(url, data=data, headers=self._grant_headers) if data else self.session.get(url) as resp: if resp.status != 200: raise SpotifyRequestException( f"Error fetching bearer token: {resp.status} {resp.reason}" ) - data: Dict = await resp.json() + response_data: Dict = await resp.json() + + if self._client_id and self._client_secret: + self._bearer_token = response_data["access_token"] + self._expiry = time.time() + int(response_data["expires_in"]) - 10 + else: + self._bearer_token = response_data["accessToken"] + self._expiry = response_data["accessTokenExpirationTimestampMs"] / 1000 - self._bearer_token = data["access_token"] - self._expiry = time.time() + (int(data["expires_in"]) - 10) self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"} async def get_request(self, url: str) -> Dict: @@ -109,7 +124,6 @@ class Client: request_url += "/top-tracks?market=US" data = await self.get_request(request_url) - if spotify_type == "track": return Track(data) elif spotify_type == "album": @@ -121,7 +135,6 @@ class Client: Track(track["track"]) for track in data["tracks"]["items"] if track["track"] is not None ] - if not tracks: raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.") From 6dfedf0e44990adcc30f0fb22f3f6d8a50feaa95 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:13:06 +0800 Subject: [PATCH 22/22] Fixed bugs and optimized code --- voicelink/player.py | 3 -- voicelink/pool.py | 8 ++--- voicelink/spotify/client.py | 64 ++++++++++++++++++++---------------- voicelink/spotify/objects.py | 2 +- 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/voicelink/player.py b/voicelink/player.py index ac166a3..d8a6f52 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -824,9 +824,6 @@ class Player(VoiceProtocol): if self.is_paused: await self.set_pause(True) - - if self.volume != 100: - await self.set_volume(self.volume) async def get_recommendations(self, *, track: Optional[Track] = None) -> bool: """Get recommendations from Youtube or Spotify.""" diff --git a/voicelink/pool.py b/voicelink/pool.py index 3902025..997a079 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -566,12 +566,12 @@ class NodePool: This option is preferred if you want to choose the best node from a multi-node setup using either the node's latency or the node's voice region. - Use NodeAlgorithm.by_ping if you want to get the best node + Use NodeAlgorithm.BY_PING if you want to get the best node based on the node's latency. Use NodeAlgorithm.by_region if you want to get the best node based on the node's voice region. This method will only work if you set a voice region when you create a node. - Use NodeAlgorithm.by_players if you want to get the best node + Use NodeAlgorithm.BY_PLAYERS if you want to get the best node based on how players it has. This method will return a node with the least amount of players """ @@ -580,11 +580,11 @@ class NodePool: if not available_nodes: raise NoNodesAvailable("There are no nodes available.") - if algorithm == NodeAlgorithm.by_ping: + if algorithm == NodeAlgorithm.BY_PING: tested_nodes = {node: node.latency for node in available_nodes} return min(tested_nodes, key=tested_nodes.get) - elif algorithm == NodeAlgorithm.by_players: + elif algorithm == NodeAlgorithm.BY_PLAYERS: tested_nodes = {node: len(node.players.keys()) for node in available_nodes} return min(tested_nodes, key=tested_nodes.get) diff --git a/voicelink/spotify/client.py b/voicelink/spotify/client.py index c880e2f..c247b9c 100644 --- a/voicelink/spotify/client.py +++ b/voicelink/spotify/client.py @@ -67,6 +67,7 @@ class Client: self._categories: List[Category] = [] async def _fetch_bearer_token(self) -> None: + """Fetches and stores a bearer token for API authentication.""" if self._client_id and self._client_secret: url, data = GRANT_URL, {"grant_type": "client_credentials"} else: @@ -90,6 +91,7 @@ class Client: self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"} async def get_request(self, url: str) -> Dict: + """Performs a GET request to the specified URL with authorization headers.""" if not self._bearer_token or time.time() >= self._expiry: await self._fetch_bearer_token() @@ -102,68 +104,72 @@ class Client: return await resp.json() async def track_search(self, query: str, track: str = "track", limit: int = 10) -> List[Track]: + """Searches for tracks based on the provided query and returns a list of Track objects.""" request_url = SEARCH_URL.format(query=query, type=track, limit=limit) data = await self.get_request(request_url) return [ Track(track) for track in data['tracks']['items'] ] async def similar_track(self, seed_tracks: str, *, limit: int = 10) -> List[Track]: + """Retrieves tracks similar to the provided seed tracks and returns them as Track objects.""" request_url = SUGGESTION_URL.format(limit=limit, seed_tracks=seed_tracks) data = await self.get_request(request_url) return [ Track(track) for track in data['tracks'] ] async def search(self, *, query: str) -> Union[Track, Album, Playlist]: + """Searches for an item (track, album, artist, or playlist) by query and returns the corresponding object.""" result = SPOTIFY_URL_REGEX.match(query) - spotify_type = result.group("type") - spotify_id = result.group("id") - if not result: raise InvalidSpotifyURL("The Spotify link provided is not valid.") + spotify_type = result.group("type") + spotify_id = result.group("id") request_url = REQUEST_URL.format(type=spotify_type, id=spotify_id) + if isArtist := (spotify_type == "artist"): request_url += "/top-tracks?market=US" data = await self.get_request(request_url) + if spotify_type == "track": return Track(data) elif spotify_type == "album": return Album(data) elif isArtist: return Artist(data) - else: - tracks = [ + + tracks = [ + Track(track["track"]) + for track in data["tracks"]["items"] if track.get("track") is not None + ] + + if not tracks: + raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.") + + next_page_url = data["tracks"].get("next") + + while next_page_url: + next_data = await self.get_request(next_page_url) + tracks.extend([ Track(track["track"]) - for track in data["tracks"]["items"] if track["track"] is not None - ] - if not tracks: - raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.") - - next_page_url = data["tracks"]["next"] + for track in next_data.get("items", []) if track.get("track") is not None + ]) + next_page_url = next_data.get("next") - while next_page_url is not None: - async with self.session.get(next_page_url, headers=self._bearer_headers) as resp: - if resp.status != 200: - raise SpotifyRequestException( - f"Error while fetching results: {resp.status} {resp.reason}" - ) - - next_data: Dict = await resp.json() - - tracks += [ - Track(track["track"]) - for track in next_data["items"] if track["track"] is not None - ] - next_page_url = next_data["next"] - - return Playlist(data, tracks) + return Playlist(data, tracks) async def get_categories(self) -> List[Category]: + """Fetches and returns available music categories from the Spotify API.""" if not self._categories: request_url = f"{BASE_URL}browse/categories" - data = await self.get_request(request_url) - self._categories = [Category(item) for item in data.get("items", [])] + + while request_url: + data = await self.get_request(request_url) + items = data.get("categories", {}).get("items", []) + self._categories.extend(Category(item) for item in items) + request_url = data.get("categories", {}).get("next") return self._categories async def close(self) -> None: + """Closes the HTTP session used for making API requests.""" await self.session.close() \ No newline at end of file diff --git a/voicelink/spotify/objects.py b/voicelink/spotify/objects.py index 9a26062..59a645d 100644 --- a/voicelink/spotify/objects.py +++ b/voicelink/spotify/objects.py @@ -129,7 +129,7 @@ class Category: self.href: str = data.get("href") self.id: str = data.get("id") self.name: str = data.get("name") - self.icon: str = data.get("icon", [])[0].get("url") + self.icon: str = data.get("icons", [{}])[0].get("url") def __repr__(self) -> str: return (f"