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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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" Date: Sat, 11 Jan 2025 00:12:24 +0100 Subject: [PATCH 23/65] DE.json correctly translated into German Translated DE.json into proper German --- langs/DE.json | 184 +++++++++++++++++++++++++------------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/langs/DE.json b/langs/DE.json index 37bd9a0..aafd0eb 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -1,82 +1,82 @@ { - "unknownException": "⚠️ Etwas ist beim Ausführen des Befehls schiefgelaufen! Bitte versuchen Sie es später erneut oder treten Sie unserem Discord-Server bei, um weitere Unterstützung zu erhalten.", + "unknownException": "⚠️ Beim Ausführen des Befehls ist etwas schiefgelaufen! Bitte versuche es später erneut oder tritt unserem Discord-Server bei, um weiteren Support zu erhalten.", "enabled": "aktiviert", "disabled": "deaktiviert", - "nodeReconnect": "Bitte versuche es erneut, nachdem sich der Knoten wieder verbunden hat.", - "noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stellen Sie entweder einen zur Verfügung oder schließen Sie sich einem an.", + "nodeReconnect": "Bitte versuche es erneut, nachdem sich die Node wieder verbunden hat.", + "noChannel": "Kein Sprachkanal zum Verbinden gefunden. Bitte stelle entweder einen zur Verfügung oder trete einem bei.", "alreadyConnected": "Bereits mit einem Sprachkanal verbunden.", - "noPermission": "Es tut uns leid! Ich bin nicht berechtigt, Ihrem Sprachkanal beizutreten oder darin zu sprechen.", - "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.", - "languageNotFound": "Kein Sprachpaket gefunden. Bitte wählen Sie ein vorhandenes Sprachpaket aus.", + "noPermission": "Es tut mir leid, ich bin nicht berechtigt, dem Sprachkanal beizutreten oder darin zu sprechen.", + "noPlaySource": "Ich kann keine abspielbaren Quellen finden!", + "noPlayer": "Auf diesem Server wurden kein Player gefunden.", + "notVote": "Dieser Befehl erfordert Deine Stimme! Gebe `/vote` ein, um weitere Informationen zu erhalten.", + "languageNotFound": "Kein Sprachpaket gefunden. Bitte wähle 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.", - "setDJ": "Stellen Sie den DJ auf {0}.", - "setqueue": "Stellen Sie den Warteschlangenmodus auf `{0}` ein.", - "247": "Jetzt haben Sie den 24/7-Modus von `{0}`.", - "bypassVote": "Jetzt haben Sie das Abstimmungssystem `{0}` umgangen.", - "setVolume": "Stellen Sie die Lautstärke auf `{0}` % ein.", - "togglecontroller": "Jetzt haben Sie `{0}` den Musikcontroller.", - "toggleDuplicateTrack": "Jetzt haben Sie `{0}`, um doppelte Tracks in der Warteschlange zu verhindern.", - "toggleControllerMsg": "Sie haben jetzt `{0}` Nachrichten vom Musik-Controller.", + "setPrefix": "Erledigt! Mein Präfix ist jetzt `{0}` auf deinem Server. Versuche, `{1}ping` auszuführen, um es zu testen.", + "setDJ": "Stelle den DJ auf {0}.", + "setqueue": "Stelle den Warteschlangenmodus auf `{0}` ein.", + "247": "Der 24/7-Modus wurde erfolgreich `{0}`.", + "bypassVote": "Das Abstimmungssystem wurde `{0}`", + "setVolume": "Stelle die Lautstärke auf `{0}`%.", + "togglecontroller": "Der Musikcontroller wurde erfolgreich `{0}`", + "toggleDuplicateTrack": "Du hast das hinzufügen von doppelten Tracks in der Warteschlange `{0}`", + "toggleControllerMsg": "Nachrichten vom Musik-Controller wurden erfolgreich `{0}`", "settingsMenu": "Servereinstellungen | {0}", "settingsTitle": "❤️ Grundlegende Informationen:", - "settingsValue": "```Präfix: {0}`\nSprache: {1}\nMusik-Controller: {2}\nDJ-Rolle: @{3}\nAbstimmungsumgehung: {4}\nRund um die Uhr: {5}\nStandardvolumen: {6}%\nSpielzeit: {7}```", + "settingsValue": "```Präfix: {0}`\nSprache: {1}\nMusik-Controller: {2}\nDJ-Rolle: @{3}\nAbstimmungsumgehung: {4}\n24/7 Play: {5}\nStandard Lautstärke: {6}%\nSpielzeit: {7}```", "settingsTitle2": "🔗 Warteschlangeninformationen:", - "settingsValue2": "```Warteschlangenmodus: {0}\nMax Lied: {1}\nDoppelte Spur zulassen: {2}```", + "settingsValue2": "```Warteschlangenmodus: {0}\nMaximale Songs: {1}\nDoppelte Tracks zulassen: {2}```", "settingsTitle3": "🎤 Sprachstatusinfo:", "settingsPermTitle": "✨ Berechtigungen:", - "settingsPermValue": "```{0} Administrator\n{1} Gilde verwalten\n{2} Kanal verwalten\n{3} Manage_Messages```", + "settingsPermValue": "```{0} Administrator\n{1} Guild verwalten\n{2} Kanal verwalten\n{3} Manage_Messages```", "pingTitle1": "Bot-Info:", - "pingTitle2": "Spielerinfo:", + "pingTitle2": "Player Info:", "pingfield1": "```Shard-ID: {0}/{1}\nShard-Latenz: {2:.3f} s {3}\nRegion: {4}```", - "pingfield2": "```Knoten: {0} - {1:.3f}s\nSpieler: {2}\nSprachregion: {3}```", + "pingfield2": "```Node: {0} - {1:.3f}s\nPlayer: {2}\nSprachregion: {3}```", "addEffect": "Wende den Effekt `{0}` Filter an.", "clearEffect": "Die Soundeffekte wurden gelöscht!", - "FilterTagAlreadyInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwenden Sie /cleareffect , um sie zu entfernen.", + "FilterTagAlreadyInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwende /cleareffect , um sie zu entfernen.", "playlistViewTitle": "📜 Alle Playlists von {0}", - "playlistViewHeaders": "ID:,Zeit:,Name:,Spuren:", - "playlistFooter": "Geben Sie /playlist play [playlist] ein, um die Playlist in die Warteschlange einzufügen.", - "playlistNotFound": "Wiedergabeliste [`{0}`] nicht gefunden. Geben Sie /playlist view ein, um Ihre gesamte Wiedergabeliste anzuzeigen.", - "playlistNotAccess": "Es tut uns leid! Du bist nicht berechtigt, auf diese Playlist zuzugreifen!", + "playlistViewHeaders": "ID:,Zeit:,Name:,Tracks:", + "playlistFooter": "Gebe /playlist play [playlist] ein, um die Playlist in die Warteschlange einzufügen.", + "playlistNotFound": "Die Wiedergabeliste [`{0}`] wurde nicht gefunden. Gebe /playlist view ein, um Deine gesamte Wiedergabeliste anzuzeigen.", + "playlistNotAccess": "Es tut mir leid! Du bist nicht berechtigt, auf diese Playlist zuzugreifen!", "playlistNoTrack": "Es tut uns leid! Es gibt keine Titel in der Wiedergabeliste [`{0}`].", "playlistNotAllow": "Dieser Befehl ist für verknüpfte und freigegebene Wiedergabelisten nicht zulässig.", - "playlistPlay": "Playlist [`{0}`] mit `{1}` Songs zur Warteschlange hinzugefügt.", - "playlistOverText": "Es tut uns leid! Der Name der Playlist darf 10 Zeichen nicht überschreiten.", - "playlistSameName": "Es tut uns leid! Dieser Name darf nicht mit Ihrem neuen Namen identisch sein.", - "playlistDeleteError": "Sie dürfen die Standard-Wiedergabeliste nicht löschen.", - "playlistRemove": "Sie haben die Wiedergabeliste [`{0}`] entfernt.", - "playlistSendErrorPlayer": "Es tut uns leid! Sie können keine Einladung an sich selbst senden.", - "playlistSendErrorBot": "Es tut uns leid! Sie können keine Einladung an einen Bot senden.", - "playlistBelongs": "Es tut uns leid! Diese Playlist gehört <@{0}>.", - "playlistShare": "Es tut uns leid! Diese Playlist wurde mit {0} geteilt.", - "playlistSent": "Es tut uns leid! Sie haben bereits eine Einladung gesendet.", - "noPlaylistAcc": "{0} hat kein Playlist-Konto erstellt.", - "overPlaylistCreation": "Sie können nicht mehr als `{0}` Wiedergabelisten erstellen!", - "playlistExists": "Playlist [`{0}`] existiert bereits.", - "playlistNotInvaildUrl": "Bitte geben Sie einen gültigen Link oder öffentlichen Spotify- oder YouTube-Playlist-Link ein.", - "playlistCreated": "Sie haben die Wiedergabeliste `{0}` erstellt. Geben Sie /playlist view ein, um weitere Informationen zu erhalten.", - "playlistRenamed": "Sie haben `{0}` in `{1}` umbenannt.", - "playlistLimitTrack": "Sie haben die Grenze erreicht! Du kannst deiner Playlist nur `{0}` Songs hinzufügen.", - "playlistPlaylistLink": "Sie dürfen keinen Playlist-Link verwenden.", - "playlistStream": "Du darfst deiner Playlist keine Streaming-Videos hinzufügen.", - "playlistPositionNotFound": "Position `{0}` kann nicht in Ihrer Playlist [`{1}`] gefunden werden!", + "playlistPlay": "Die Playlist [`{0}`] mit `{1}` Songs wurde zur Warteschlange hinzugefügt.", + "playlistOverText": "Verzeihung, der Name der Playlist darf nicht mehr als 10 Zeichen enthalten.", + "playlistSameName": "Verzeihung, dieser Name darf nicht mit Deinem neuen Namen identisch sein.", + "playlistDeleteError": "Du kannst die Standard-Wiedergabeliste nicht löschen.", + "playlistRemove": "Du hast die Wiedergabeliste [`{0}`] entfernt.", + "playlistSendErrorPlayer": "Entschuldigung, du kannst keine Einladung an dich selber senden.", + "playlistSendErrorBot": "Entschuldigung, Du kannst keine Einladung an eine App senden.", + "playlistBelongs": "Verzeihung, diese Playlist gehört <@{0}>.", + "playlistShare": "Verzeigung, Diese Playlist wurde bereits mit {0} geteilt.", + "playlistSent": "Verzeihung, Du hast bereits eine Einladung gesendet.", + "noPlaylistAcc": "{0} hat noch kein Playlist-Konto erstellt.", + "overPlaylistCreation": "Du kannst nicht mehr als `{0}` Wiedergabelisten erstellen!", + "playlistExists": "Die Playlist [`{0}`] existiert bereits.", + "playlistNotInvaildUrl": "Bitte gebe einen gültigen Link oder öffentlichen Spotify/YouTube-Playlist-Link ein.", + "playlistCreated": "Du hast die Wiedergabeliste `{0}` erstellt. Gebe /playlist view ein, um weitere Informationen zu erhalten.", + "playlistRenamed": "Du hast `{0}` zu `{1}` umbenannt.", + "playlistLimitTrack": "Du hast die Grenze erreicht! Du kannst deiner Playlist nur noch `{0}` Songs hinzufügen.", + "playlistPlaylistLink": "Du kannst keine Playlist-Links verwenden.", + "playlistStream": "Du darfst deiner Playlist keine aktiven Streams hinzufügen.", + "playlistPositionNotFound": "Die Position `{0}` kann in Deiner Playlist [`{1}`] nicht gefunden werden.", "playlistRemoved": "👋 {0} aus {1}s Playlist [{2}] entfernt.", - "playlistClear": "Sie haben Ihre Wiedergabeliste [`{0}`] erfolgreich gelöscht.", + "playlistClear": "Du hast Deine Wiedergabenliste [`{0}`] erfolgreich gelöscht.", "playlistView": "Playlist-Viewer", "playlistViewDesc": "```Name | ID: {0} | {1}\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```", "playlistViewPermsValue": "📖 Lesen: ✓ ✍🏽 Schreiben: {0} 🗑️ Entfernen: {1}", "playlistViewPermsValue2": "📖 Lesen: {0}", - "playlistViewTrack": "Spuren", + "playlistViewTrack": "Tracks", "playlistViewPage": "Seite: {0}/{1} | Gesamtdauer: {2}", - "inboxFull": "Es tut uns leid! Der Posteingang von {0} ist voll.", - "inboxNoMsg": "Es sind keine Nachrichten in Ihrem Posteingang.", + "inboxFull": "Es tut mir leid! Der Posteingang von {0} ist voll.", + "inboxNoMsg": "Es sind keine Nachrichten in Deinem Posteingang.", "invitationSent": "Einladung an {0} gesendet.", - "notInChannel": "{0}, du musst in {1} sein, um Sprachbefehle zu nutzen. Bitte betrete den Sprachkanal, wenn du dich in Sprache befindest!", + "notInChannel": "{0}, Du musst in {1} sein, um Sprachbefehle nutzen zu können. Bitte betrete den Sprachkanal erneut bei, wenn Du dich im Voice Channel befindest!", "noTrackPlaying": "Es werden derzeit keine Songs abgespielt", "noTrackFound": "Es wurden keine Songs mit dieser Abfrage gefunden! Bitte gib eine gültige URL an.", "noLinkSupport": "Der Suchbefehl unterstützt keine Links!", @@ -85,37 +85,37 @@ "missingPerms_mode": "Nur der DJ oder Admins können den Wiederholungsmodus wechseln.", "missingPerms_queue": "Nur der DJ oder Admins können Tracks aus der Warteschlange entfernen.", "missingPerms_autoplay": "Nur der DJ oder Admins können den Autoplay-Modus aktivieren oder deaktivieren!", - "missingPerms_function": "Nur DJ oder Admin können diese Funktion verwenden.", + "missingPerms_function": "Nur DJ oder Admins können diese Funktion verwenden.", "timeFormatError": "Falsches Zeitformat. Beispiel: 2:42 oder 12:39:31", - "lyricsNotFound": "Songtexte nicht gefunden. Geben Sie /lyrics ein, um die Songtexte zu finden.", + "lyricsNotFound": "Es wurden keine Songtexte gefunden. Gebe /lyrics ein, um die Songtexte zu finden.", "missingTrackInfo": "Einige Track-Informationen fehlen.", - "noVoiceChannel": "Sprachkanal nicht gefunden!", + "noVoiceChannel": "Dieser Sprachkanal wurde nicht gefunden!", - "playlistAddError": "Sie dürfen Ihrer Wiedergabeliste keine Streaming-Videos hinzufügen!", - "playlistAddError2": "Es gab ein Problem beim Hinzufügen von Tracks zur Wiedergabeliste!", - "playlistlimited": "Sie haben das Limit erreicht! Sie können nur {0} Songs zu Ihrer Wiedergabeliste hinzufügen.", - "playlistrepeated": "In Ihrer Wiedergabeliste gibt es bereits den gleichen Track!", - "playlistAdded": "❤️ Hinzugefügt **{0}** in {1}'s Wiedergabeliste [`{2}`]!", + "playlistAddError": "Du darfst Deiner Wiedergabenliste keine aktiven Streams hinzufügen!", + "playlistAddError2": "Es gab ein Problem beim Hinzufügen von Tracks zur Wiedergabenliste!", + "playlistlimited": "Du hast das Limit erreicht! Du kannst nur noch {0} Songs zu Deiner Wiedergabenliste hinzufügen.", + "playlistrepeated": "In Deiner Wiedergabenliste gibt es bereits den gleichen Track!", + "playlistAdded": "❤️ **{0}** wurde in der Wiedergabenliste [`{2}`] von {1} hinzugefügt.", - "playerDropdown": "Wählen Sie einen Song aus, um zu überspringen ...", - "playerFilter": "Wählen Sie einen Filter aus, um ihn anzuwenden ...", + "playerDropdown": "Wähle einen Song aus, um zu überspringen ...", + "playerFilter": "Wähle einen Filter aus, um ihn anzuwenden ...", "buttonBack": "Zurück", "buttonPause": "Pause", - "buttonResume": "Fortsetzen", + "buttonResume": "Weiter", "buttonSkip": "Überspringen", "buttonLeave": "Verlassen", - "buttonLoop": "Schleife", + "buttonLoop": "Endlosschleife", "buttonVolumeUp": "Lauter", "buttonVolumeDown": "Leiser", "buttonVolumeMute": "Stummschalten", "buttonVolumeUnmute": "Stummschaltung aufheben", "buttonAutoPlay": "Autoplay", - "buttonShuffle": "Mischen", - "buttonForward": "Vorwärts", - "buttonRewind": "Rückwärts", + "buttonShuffle": "Zufall", + "buttonForward": "Vorspulen", + "buttonRewind": "Zurückspulen", - "nowplayingDesc": "**Jetzt abspielen:**\n```{0}```", + "nowplayingDesc": "**Jetzt wird abgespielt:**\n```{0}```", "nowplayingField": "Als nächstes:", "nowplayingLink": "Auf {0} anhören", @@ -124,31 +124,31 @@ "live": "LIVE", "playlistLoad": " 🎶 Die Wiedergabeliste **{0}** mit `{1}` Songs wurde zur Warteschlange hinzugefügt.", "trackLoad": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde zum Abspielen hinzugefügt.\n", - "trackLoad_pos": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde in der Warteschlange an Position **{4}** hinzugefügt.\n", + "trackLoad_pos": "**[{0}](<{1}>)** von **{2}** (`{3}`) wurde in der Warteschlange zu Position **{4}** hinzugefügt.\n", "searchTitle": "Suchabfrage: {0}", "searchDesc": "➥ Plattform: {0} **{1}**\n➥ Ergebnisse: **{2}**\n\n{3}", - "searchWait": "Wählen Sie den Song aus, den Sie zur Warteschlange hinzufügen möchten.", - "searchTimeout": "Die Suche wurde abgebrochen. Bitte versuchen Sie es später erneut.", - "searchSuccess": "Song wurde zur Warteschlange hinzugefügt.", + "searchWait": "Wähle den Song aus, den Du zur Warteschlange hinzufügen möchtest.", + "searchTimeout": "Die Suche wurde abgebrochen. Bitte versuche es später erneut.", + "searchSuccess": "Der Song wurde zur Warteschlange hinzugefügt.", "queueTitle": "Kommende Warteschlange:", - "historyTitle": "Verlaufswarteschlange:", + "historyTitle": "Vorherige Warteschlange:", "viewTitle": "Musik-Warteschlange", - "viewDesc": "**Jetzt abspielen: [Hier klicken]({0}) ⮯**\n{1}", + "viewDesc": "**Jetzt wird abgespielt: [Hier klicken]({0}) ⮯**\n{1}", "viewFooter": "Seite: {0}/{1} | Gesamtdauer: {2}", "pauseError": "Der Player ist bereits pausiert.", - "pauseVote": "{0} hat für eine Pause des Songs gestimmt. [{1}/{2}]", + "pauseVote": "{0} hat für eine Pause des Songs abgestimmt. [{1}/{2}]", "paused": "`{0}` hat den Player pausiert.", "resumeError": "Der Player ist nicht pausiert.", - "resumeVote": "{0} hat für das Fortsetzen des Songs gestimmt. [{1}/{2}]", + "resumeVote": "{0} hat für das Fortsetzen des Songs abgestimmt. [{1}/{2}]", "resumed": "`{0}` hat den Player fortgesetzt.", - "shuffleError": "Fügen Sie mehr Songs zur Warteschlange hinzu, bevor Sie mischen.", + "shuffleError": "Füge mehr Songs zur Warteschlange hinzu, bevor Du die Songs mischst.", "shuffleVote": "{0} hat für das Mischen der Warteschlange gestimmt. [{1}/{2}]", "shuffled": "Die Warteschlange wurde gemischt.", "skipError": "Es gibt keine Songs, die übersprungen werden können.", - "skipVote": "{0} hat für das Überspringen des Songs gestimmt. [{1}/{2}]", + "skipVote": "{0} hat für das Überspringen des Songs abgestimmt. [{1}/{2}]", "skipped": "`{0}` hat den Song übersprungen.", "backVote": "{0} hat für das Überspringen zum vorherigen Song gestimmt. [{1}/{2}]", @@ -157,34 +157,34 @@ "leaveVote": "{0} hat für das Anhalten des Players gestimmt. [{1}/{2}]", "left": "`{0}` hat den Player angehalten.", - "seek": "Setze den Player auf **{0}**", + "seek": "Der Player wurde auf **{0}** gesetzt.", "repeat": "Der Wiederholungsmodus wurde auf `{0}` gesetzt", "cleared": "Alle Tracks in `{0}` wurden gelöscht", "removed": "`{0}` Tracks wurden aus der Warteschlange entfernt.", - "forward": "Spule den Player vor auf **{0}**", - "rewind": "Spule den Player zurück auf **{0}**", - "replay": "Wiederhole den aktuellen Song.", + "forward": "Der Player wurde auf **{0}** vorgespult.", + "rewind": "Der Player wurde auf **{0}** zurückgespult", + "replay": "Der aktuelle Song wird wiederholt.", "swapped": "`{0}` und `{1}` wurden ausgetauscht.", - "moved": "Verschoben `{0}` zu `{1}`", + "moved": "`{0}` zu `{1}` verschoben", "autoplay": "Der Autoplay-Modus ist jetzt **{0}**", - "notdj": "Du bist kein DJ. Der aktuelle DJ ist {0}.", - "djToMe": "Du kannst den DJ nicht an dich selbst oder einen Bot übertragen.", + "notdj": "Du bist kein DJ, der aktuelle DJ ist {0}.", + "djToMe": "Du kannst den DJ nicht an dich selbst oder einer App übertragen.", "djnotinchannel": "`{0}` ist nicht im Sprachkanal.", "djswap": "Du hast die DJ-Rolle auf `{0}` übertragen.", "chaptersDropdown": "Wähle ein Kapitel zum Überspringen aus...", "noChaptersFound": "Es wurden keine Kapitel gefunden!", - "chatpersNotSupport": "Dieser Befehl unterstützt nur Youtube-Videos!", + "chatpersNotSupport": "Dieser Befehl unterstützt nur YouTube-Videos!", - "voicelinkQueueFull": "Entschuldigung, du hast das Maximum von `{0}` Tracks in der Warteschlange erreicht!", + "voicelinkQueueFull": "Entschuldigung, Du hast das Maximum von `{0}` Tracks in der Warteschlange erreicht.", "voicelinkOutofList": "Bitte gib einen gültigen Track-Index an!", "voicelinkDuplicateTrack": "Entschuldigung, dieser Track ist bereits in der Warteschlange.", "deocdeError": "Beim Dekodieren der Datei ist etwas schief gelaufen!", - "invalidStartTime": "Ungültiger Startzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.", - "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.", + "invalidStartTime": "Ungültige Startzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.", + "invalidEndTime": "Ungültige Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.", + "invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein.", - "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Sie sich gerade befinden, gemäß Ihrer Vorlage benannt. Sie sollten in wenigen Sekunden eine Aktualisierung sehen." -} \ No newline at end of file + "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen.", +} From 9e96ac7478e5af2d16f2b464b5e51ad08a43ebdd Mon Sep 17 00:00:00 2001 From: Dominik <64731738+TheDomCraft@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:15:26 +0100 Subject: [PATCH 24/65] Update DE.json unnecessary comma removed --- langs/DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/DE.json b/langs/DE.json index aafd0eb..52cecc3 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -186,5 +186,5 @@ "invalidEndTime": "Ungültige Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.", "invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein.", - "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen.", + "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen." } From 4f20cd8c5e3ce112802d92a92e78d7cdfa92e312 Mon Sep 17 00:00:00 2001 From: Dominik <64731738+TheDomCraft@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:57:21 +0100 Subject: [PATCH 25/65] Update DE.json added the createSongRequestChannel key and edited the setStageAnnounceTemplate key --- langs/DE.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/langs/DE.json b/langs/DE.json index e715ea1..c863567 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -33,7 +33,7 @@ "settingsPermValue": "```{0} Administrator\n{1} Guild verwalten\n{2} Kanal verwalten\n{3} Manage_Messages```", "pingTitle1": "Bot-Info:", "pingTitle2": "Player Info:", - "pingfield1": "```Shard-ID: {0}/{1}\nShard-Latenz: {2:.3f} s {3}\nRegion: {4}```", + "pingfield1": "```Shard-ID: {0}/{1}\nShard-Latenz: {2:.3f}s {3}\nRegion: {4}```", "pingfield2": "```Node: {0} - {1:.3f}s\nPlayer: {2}\nSprachregion: {3}```", "addEffect": "Wende den Effekt `{0}` Filter an.", "clearEffect": "Die Soundeffekte wurden gelöscht!", @@ -188,5 +188,6 @@ "invalidEndTime": "Ungültige Endzeit! Die Zeit muss innerhalb von `00:00` und `{0}` liegen.", "invalidTimeOrder": "Die Endzeit darf nicht kleiner oder gleich dem Startzeit sein.", - "SetStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen." -} \ No newline at end of file + "setStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen.", + "createSongRequestChannel": "Der Song Request Channel ({0}) wurde erstellt! Du kannst jeden Song nach Namen oder URL in diesem Kanal anfordern, ohne den Bot-Präfix verwenden zu müssen." +} From 1bf3f406f033e7cd7aaacec6028bf27c4cb45564 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:29:25 +0800 Subject: [PATCH 26/65] Refactor IPC client payload change naming style from snake_case to camelCase --- cogs/listeners.py | 10 +-- ipc/methods.py | 175 +++++++++++++++++++++++--------------------- voicelink/player.py | 26 +++---- 3 files changed, 111 insertions(+), 100 deletions(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index 9aef1e7..4db2495 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -102,13 +102,13 @@ class Listeners(commands.Cog): await self.bot.ipc.send({ "op": "updateGuild", "user": { - "user_id": str(member.id), - "avatar_url": member.display_avatar.url, + "userId": str(member.id), + "avatarUrl": member.display_avatar.url, "name": member.name, }, - "channel_name": member.voice.channel.name if is_joined else "", - "guild_id": str(member.guild.id), - "is_joined": is_joined + "channelName": member.voice.channel.name if is_joined else "", + "guildId": str(member.guild.id), + "isJoined": is_joined }) async def setup(bot: commands.Bot) -> None: diff --git a/ipc/methods.py b/ipc/methods.py index 22493eb..ed97f4c 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -54,9 +54,9 @@ def require_permission(only_admin: bool = False): def error_msg(msg: str, *, user_id: int = None, guild_id: int = None, level: str = "info") -> Dict: payload = {"op": "errorMsg", "level": level, "msg": msg} if user_id: - payload["user_id"] = str(user_id) + payload["userId"] = str(user_id) if guild_id: - payload["guild_id"] = str(guild_id) + payload["guildId"] = str(guild_id) return payload @@ -68,13 +68,13 @@ async def connect_channel(member: Member, bot: commands.Bot) -> Player: try: settings = await func.get_settings(channel.guild.id) player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings)) - await player.send_ws({"op": "createPlayer", "member_ids": [str(member.id) for member in channel.members]}) + await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]}) return player except: return async def initBot(bot: commands.Bot, data: Dict) -> Dict: - user_id = int(data.get("user_id")) + user_id = int(data.get("userId")) user = bot.get_user(user_id) if not user: user = await bot.fetch_user(user_id) @@ -82,14 +82,14 @@ async def initBot(bot: commands.Bot, data: Dict) -> Dict: if user: return { "op": "initBot", - "user_id": str(user_id), - "bot_name": bot.user.display_name, - "bot_avatar": bot.user.display_avatar.url, - "bot_id": str(bot.user.id) + "userId": str(user_id), + "botName": bot.user.display_name, + "botAvatar": bot.user.display_avatar.url, + "botId": str(bot.user.id) } async def initUser(bot: commands.Bot, data: Dict) -> Dict: - user_id = int(data.get("user_id")) + user_id = int(data.get("userId")) data = await func.get_user(user_id) for mail in data.get("inbox"): @@ -100,11 +100,11 @@ async def initUser(bot: commands.Bot, data: Dict) -> Dict: if not sender: data.get("inbox").remove(mail) - mail["sender"] = {"avatar_url": sender.display_avatar.url, "name": sender.display_name, "id": str(sender.id)} + mail["sender"] = {"avatarUrl": sender.display_avatar.url, "name": sender.display_name, "id": str(sender.id)} return { "op": "initUser", - "user_id": str(user_id), + "userId": str(user_id), "data": data } @@ -117,29 +117,29 @@ async def initPlayer(player: Player, member: Member, data: Dict) -> Dict: return { "op": "initPlayer", - "guild_id": str(player.guild.id), - "user_id": str(data.get("user_id")), + "guildId": str(player.guild.id), + "userId": str(data.get("userId")), "users": [{ - "user_id": str(member.id), - "avatar_url": member.display_avatar.url, + "userId": str(member.id), + "avatarUrl": member.display_avatar.url, "name": member.name } for member in player.channel.members ], - "tracks": [ {"track_id": track.track_id, "requester_id": str(track.requester.id)} for track in player.queue._queue ], - "repeat_mode": player.queue.repeat.lower(), - "channel_name": player.channel.name, - "current_queue_position": player.queue._position + (0 if player.is_playing else 1), - "current_position": 0 or player.position if player.is_playing else 0, - "is_playing": player.is_playing, - "is_paused": player.is_paused, - "is_dj": player.is_privileged(member, check_user_join=False), + "tracks": [ {"trackId": track.track_id, "requesterId": str(track.requester.id)} for track in player.queue._queue ], + "repeatMode": player.queue.repeat.lower(), + "channelName": player.channel.name, + "currentQueuePosition": player.queue._position + (0 if player.is_playing else 1), + "currentPosition": 0 or player.position if player.is_playing else 0, + "isPlaying": player.is_playing, + "isPaused": player.is_paused, + "isDj": player.is_privileged(member, check_user_join=False), "autoplay": player.settings.get("autoplay", False), "volume": player.volume, "filters": [{"tag": filter.tag, "scope": filter.scope, "payload": filter.payload} for filter in player.filters.get_filters()], - "available_filters": available_filters + "availableFilters": available_filters } async def closeConnection(bot: commands.Bot, data: Dict) -> None: - guild_id = int(data.get("guild_id")) + guild_id = int(data.get("guildId")) guild = bot.get_guild(guild_id) player: Player = guild.voice_client if player: @@ -150,13 +150,13 @@ async def getRecommendation(bot: commands.Bot, data: Dict) -> None: if not node: return - track_data = decode(track_id := data.get("track_id")) + track_data = decode(track_id := data.get("trackId")) track = Track(track_id=track_id, info=track_data, requester=bot.user) tracks: List[Track] = await node.get_recommendations(track, limit=60) return { "op": "getRecommendation", - "user_id": str(data.get("user_id")), + "userId": str(data.get("userId")), "callback": data.get("callback"), "tracks": [track.track_id for track in tracks] if tracks else [] } @@ -238,7 +238,7 @@ async def getTracks(bot: commands.Bot, data: Dict) -> Dict: query = data.get("query", None) if query: - payload = {"op": "getTracks", "user_id": data.get("user_id"), "callback": data.get("callback")} + payload = {"op": "getTracks", "userId": data.get("userId"), "callback": data.get("callback")} tracks = await NodePool.get_node().get_tracks(query=query, requester=None) if not tracks: return payload @@ -268,7 +268,7 @@ async def removeTrack(player: Player, member: Member, data: Dict) -> None: @require_permission() async def clearQueue(player: Player, member: Member, data: Dict) -> None: - queue_type = data.get("queue_type", "").lower() + queue_type = data.get("queueType", "").lower() await player.clear_queue(queue_type, member) @require_permission(only_admin=True) @@ -316,8 +316,8 @@ async def toggleAutoplay(player: Player, member: Member, data: Dict) -> Dict: return { "op": "toggleAutoplay", "status": check, - "guild_id": player.guild.id, - "requester_id": str(member.id) + "guildId": player.guild.id, + "requesterId": str(member.id) } @require_permission() @@ -374,30 +374,33 @@ async def _getPlaylist(user_id: int, playlist_id: str) -> Dict: return playlist async def getPlaylist(bot: commands.Bot, data: Dict) -> Dict: - user_id = int(data.get("user_id")) - playlist_id = str(data.get("playlist_id")) + user_id = int(data.get("userId")) + playlist_id = str(data.get("playlistId")) - payload = {"op": "loadPlaylist", "playlist_id": playlist_id, "user_id": str(user_id)} + payload = {"op": "loadPlaylist", "playlistId": playlist_id, "userId": str(user_id)} playlist = await _getPlaylist(user_id, playlist_id) payload["tracks"] = playlist["tracks"] if playlist else [] return payload async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: - user_id = int(data.get("user_id")) - playlist_id = str(data.get("playlist_id")) + user_id = int(data.get("userId")) + playlist_id = str(data.get("playlistId")) _type = data.get("type") + if not playlist_id and not _type == "createPlaylist": + return error_msg("Unable to process this request without a playlist ID.", user_id=user_id, level="error") + rank, max_p, max_t = func.check_roles() if _type == "createPlaylist": - name, playlist_url = data.get("name"), data.get("playlist_url") + name, playlist_url = data.get("playlistName"), data.get("playlistUrl") if not name: return { "op": "updatePlaylist", "status": "error", "msg": f"You must enter name for this field!", - "field": "create-playlist-name", - "user_id": str(user_id) + "field": "playlistName", + "userId": str(user_id) } playlist = await func.get_user(user_id, "playlist") @@ -406,8 +409,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "op": "updatePlaylist", "status": "error", "msg": f"You cannot create more than '{max_p}' playlists!", - "field": "create-playlist-name", - "user_id": str(user_id) + "field": "playlistName", + "userId": str(user_id) } for playlist_data in playlist.values(): @@ -416,8 +419,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "op": "updatePlaylist", "status": "error", "msg": f"Playlist '{name}' already exists.", - "field": "create-playlist-name", - "user_id": str(user_id) + "field": "playlistName", + "userId": str(user_id) } if playlist_url: @@ -427,8 +430,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "op": "updatePlaylist", "status": "error", "msg": f"Please enter a valid link or public spotify or youtube playlist link.", - "field": "create-playlist-url", - "user_id": str(user_id) + "field": "playlistUrl", + "userId": str(user_id) } assgined_playlist_id = _assign_playlist_id(list(playlist.keys())) @@ -437,9 +440,9 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: return { "op": "updatePlaylist", "status": "created", - "playlist_id": assgined_playlist_id, + "playlistId": assgined_playlist_id, "msg": f"You have created '{name}' playlist.", - "user_id": str(user_id), + "userId": str(user_id), "data": data } @@ -454,9 +457,9 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: return { "op": "updatePlaylist", "status": "deleted", - "playlist_id": playlist_id, + "playlistId": playlist_id, "msg": f"You have removed playlist '{playlist['name']}'", - "user_id": str(user_id) + "userId": str(user_id) } elif _type == "renamePlaylist": @@ -466,8 +469,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "op": "updatePlaylist", "status": "error", "msg": f"You must enter name for this field!", - "field": "rename-playlist-name", - "user_id": str(user_id) + "field": "playlistName", + "userId": str(user_id) } playlist = await func.get_user(user_id, "playlist") @@ -477,8 +480,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "op": "updatePlaylist", "status": "error", "msg": f"Playlist '{data['name']}' already exists.", - "field": "rename-playlist-name", - "user_id": str(user_id) + "field": "playlistName", + "userId": str(user_id) } await func.update_user(user_id, {"$set": {f'playlist.{playlist_id}.name': name}}) @@ -486,14 +489,14 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "op": "updatePlaylist", "status": "renamed", "name": name, - "playlist_id": playlist_id, + "playlistId": playlist_id, "msg": f"You have renamed the playlist to '{name}'.", - "field": "rename-playlist-name", - "user_id": str(user_id) + "field": "playlistName", + "userId": str(user_id) } elif _type == "addTrack": - track_id = data.get("track_id") + track_id = data.get("trackId") if not track_id: return error_msg("No track ID could be located.", user_id=user_id, level='error') @@ -513,18 +516,21 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: return { "op": "updatePlaylist", "status": "addTrack", - "playlist_id": playlist_id, - "track_id": track_id, + "playlistId": playlist_id, + "trackId": track_id, "msg": f"Added {decoded_track.title} into '{playlist['name']}' playlist.", - "user_id": str(user_id) + "userId": str(user_id) } elif _type == "removeTrack": - track_id, track_position = data.get("track_id"), data.get("track_position", 0) + track_id, track_position = data.get("trackId"), data.get("trackPosition", 0) if not track_id: return error_msg("No track ID could be located.", user_id=user_id, level='error') playlist = await _getPlaylist(user_id, playlist_id) + if not playlist: + return error_msg("Playlist not found!", user_id=user_id, level='error') + if playlist['type'] in ['share', 'link']: return error_msg("You cannot remove songs from a linked playlist through Vocard.", user_id=user_id, level='error') @@ -540,11 +546,11 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: return { "op": "updatePlaylist", "status": "removeTrack", - "playlist_id": playlist_id, - "track_position": track_position, - "track_id": track_id, + "playlistId": playlist_id, + "trackPosition": track_position, + "trackId": track_id, "msg": f"Removed '{decoded_track['title']}' from '{playlist['name']}' playlist.", - "user_id": str(user_id) + "userId": str(user_id) } elif _type == "updateInbox": @@ -554,11 +560,11 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: if is_accpet and len(list(user.get("playlist").keys())) >= max_p: return error_msg(f"You cannot create more than '{max_p}' playlists!", user_id=user_id, level = "error") - info = data.get("refer_id", "").split("-") + info = data.get("referId", "").split("-") sender_id, refer_id = info[0], info[1] inbox = user.get("inbox") - payload = {"op": "updatePlaylist", "status": "updateInbox", "user_id": str(user_id), "accpet": is_accpet, "sender_id": sender_id, "refer_id": refer_id} + payload = {"op": "updatePlaylist", "status": "updateInbox", "userId": str(user_id), "accpet": is_accpet, "senderId": sender_id, "referId": refer_id} for index, mail in enumerate(inbox.copy()): if not (str(mail.get("sender")) == sender_id and mail.get("referId") == refer_id): continue @@ -587,7 +593,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: }}) payload.update({ - "playlist_id": assgined_playlist_id, + "playlistId": assgined_playlist_id, "msg": f"You have created '{playlist_name}' playlist.", "data": share_playlist, }) @@ -596,14 +602,14 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: return payload async def getMutualGuilds(bot: commands.Bot, data: Dict) -> Dict: - user_id = int(data.get("user_id")) + user_id = int(data.get("userId")) - payload = {"op": "getMutualGuilds", "mutualGuilds": {}, "inviteGuilds": {}, "user_id": str(user_id)} + payload = {"op": "getMutualGuilds", "mutualGuilds": {}, "inviteGuilds": {}, "userId": str(user_id)} for guild_id, guild_info in data.get("guilds", {}).items(): if guild := bot.get_guild(int(guild_id)): payload["mutualGuilds"][guild_id] = { **guild_info, - "member_count": guild.member_count + "memberCount": guild.member_count } else: payload["inviteGuilds"][guild_id] = {**guild_info} @@ -611,8 +617,8 @@ async def getMutualGuilds(bot: commands.Bot, data: Dict) -> Dict: return payload async def getSettings(bot: commands.Bot, data: Dict) -> Dict: - user_id = int(data.get("user_id")) - guild_id = int(data.get("guild_id")) + user_id = int(data.get("userId")) + guild_id = int(data.get("guildId")) guild = bot.get_guild(guild_id) if not guild: @@ -636,7 +642,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict: "settings": settings, "options": { "languages": list(func.LANGS.keys()), - "queue_modes": ["Queue", "FairQueue"], + "queueModes": ["Queue", "FairQueue"], "roles": [role.name for role in guild.roles] }, "guild": { @@ -644,7 +650,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict: "name": guild.name, "id": str(guild_id) }, - "user_id": str(user_id) + "userId": str(user_id) } async def getLyrics(bot: commands.Bot, data: Dict) -> Dict: @@ -655,7 +661,10 @@ async def getLyrics(bot: commands.Bot, data: Dict) -> Dict: song: dict[str, str] = await lyricsPlatform.get(platform)().get_lyrics(title, artist) payload = { "op": "getLyrics", - "user_id": data.get("user_id"), + "userId": data.get("userId"), + "title": title, + "artist": artist, + "platform": platform, "lyrics": {_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()} if song else {}, "callback": data.get("callback") } @@ -663,8 +672,8 @@ async def getLyrics(bot: commands.Bot, data: Dict) -> Dict: return payload async def updateSettings(bot: commands.Bot, data: Dict) -> None: - user_id = int(data.get("user_id")) - guild_id = int(data.get("guild_id")) + user_id = int(data.get("userId")) + guild_id = int(data.get("guildId")) guild = bot.get_guild(guild_id) if not guild: @@ -721,7 +730,7 @@ METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = { async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None: op: str = data.get("op", "") method = METHODS.get(op) - if not method or not (user_id := data.get("user_id")): + if not method or not (user_id := data.get("userId")): return user_id = int(user_id) @@ -730,7 +739,7 @@ async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None: else: if RATELIMIT_COUNTER[user_id]["count"] >= 100: - return await ipc_client.send({"op": "rateLimited", "user_id": str(user_id)}) + return await ipc_client.send({"op": "rateLimited", "userId": str(user_id)}) RATELIMIT_COUNTER[user_id]["count"] += method.credit try: @@ -739,7 +748,7 @@ async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None: params = method.params if not (type(method) == SystemMethod): - if guild_id := data.get("guild_id"): + if guild_id := data.get("guildId"): if (guild := bot.get_guild(int(guild_id))): env["guild"] = guild @@ -778,10 +787,12 @@ async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None: await ipc_client.send(resp) except Exception as e: + import traceback + traceback.print_exc() payload = { "op": "errorMsg", "level": "error", "msg": str(e), - "user_id": str(user_id) + "userId": str(user_id) } await ipc_client.send(payload) \ No newline at end of file diff --git a/voicelink/player.py b/voicelink/player.py index d8a6f52..75b45a4 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -72,7 +72,7 @@ async def connect_channel(ctx: Union[commands.Context, Interaction], channel: Vo )) if ctx.bot.ipc.is_connected: - await player.send_ws({"op": "createPlayer", "member_ids": [str(member.id) for member in channel.members]}) + await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]}) return player @@ -299,9 +299,9 @@ class Player(VoiceProtocol): if self.is_ipc_connected: await self.send_ws({ "op": "playerUpdate", - "last_update": self._last_update, - "is_connected": self._is_connected, - "last_position": self._last_position + "lastUpdate": self._last_update, + "isConnected": self._is_connected, + "lastPosition": self._last_position }) async def _dispatch_voice_update(self, voice_data: Dict[str, Any] = None): @@ -405,9 +405,9 @@ class Player(VoiceProtocol): if self.is_ipc_connected: await self.send_ws({ "op": "trackUpdate", - "current_queue_position": self.queue._position if track else self.queue._position + 1, - "track_id": track.track_id if track else None, - "is_paused": self._paused + "currentQueuePosition": self.queue._position if track else self.queue._position + 1, + "trackId": track.track_id if track else None, + "isPaused": self._paused }) async def invoke_controller(self): @@ -639,7 +639,7 @@ class Player(VoiceProtocol): await self.send_ws({ "op": "removeTrack", "indexes": list(removed_tracks.keys()), - "first_track_id": list(removed_tracks.values())[0].track_id + "firstTrackId": list(removed_tracks.values())[0].track_id }, requester=requester) return removed_tracks @@ -692,8 +692,8 @@ class Player(VoiceProtocol): if self.is_ipc_connected: await self.send_ws({ "op": "shuffleTrack", - "tracks": [{"track_id": track.track_id, "requester_id": str(track.requester.id)} for track in replacement], - "queue_type": queue_type + "tracks": [{"trackId": track.track_id, "requesterId": str(track.requester.id)} for track in replacement], + "queueType": queue_type }, requester) self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) has been shuffled the queue.") @@ -767,7 +767,7 @@ class Player(VoiceProtocol): if self.is_ipc_connected: await self.send_ws({ "op": "clearQueue", - "queue_type": queue_type + "queueType": queue_type }, requester) async def remove_filter(self, filter_tag: str, requester: Member = None, fast_apply: bool = False) -> Filters: @@ -863,7 +863,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) + payload['guildId'] = str(self.guild.id) if requester: - payload['requester_id'] = str(requester.id) + payload['requesterId'] = str(requester.id) await self.bot.ipc.send(payload) \ No newline at end of file From bb5e870f042db5db94d9536e6e34c68856e462a0 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:29:50 +0800 Subject: [PATCH 27/65] Added new api for dashboard --- ipc/methods.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/ipc/methods.py b/ipc/methods.py index ed97f4c..536e789 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -245,7 +245,11 @@ async def getTracks(bot: commands.Bot, data: Dict) -> Dict: payload["tracks"] = [ track.track_id for track in (tracks.tracks if isinstance(tracks, Playlist) else tracks ) ] return payload - + +async def searchAndPlay(player: Player, member: Member, data: Dict) -> None: + payload = await getTracks(player.bot, data) + await addTracks(player, member, payload) + async def shuffleTrack(player: Player, member: Member, data: Dict) -> None: if not player.is_privileged(member): if member in player.shuffle_votes: @@ -699,6 +703,49 @@ async def updateSettings(bot: commands.Bot, data: Dict) -> None: await func.update_settings(guild.id, {"$set": data}) +async def getFeaturedPlaylists(bot: commands.Bot, data: Dict) -> Dict: + locale = data.get("locale", "sv_SE") + limit = data.get("limit", 20) + offset = data.get("offset", 0) + + request_url = f"https://api.spotify.com/v1/browse/featured-playlists?locale={locale}&limit={max(1, min(limit, 50))}&offset={max(0, offset)}" + + node = NodePool.get_node() + result = await node.spotify_client.get_request(request_url) + + return { + "op": "getFeaturedPlaylists", + "userId": data.get("userId"), + "callback": data.get("callback"), + "playlists": [ + { + "id": item.get("id"), + "title": item.get("name"), + "description": item.get("description"), + "imageUrl": item.get("images", [{}])[0].get("url"), + "href": item.get("external_urls", {}).get("spotify") + } + for item in result.get("playlists", {}).get("items", []) + ] + } + +async def getCategoryPlaylists(bot: commands.Bot, data: Dict) -> Dict: + node = NodePool.get_node() + + return { + "op": "getCategoryPlaylists", + "userId": data.get("userId"), + "callback": data.get("callback"), + "playlists": [ + { + "id": category.id, + "title": category.name, + "imageUrl": category.icon, + } + for category in await node.spotify_client.get_categories() + ] + } + METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = { "initBot": SystemMethod(initBot, credit=0), "initUser": SystemMethod(initUser, credit=2), @@ -725,6 +772,9 @@ METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = { "updatePosition": PlayerMethod(updatePosition), "toggleAutoplay": PlayerMethod(toggleAutoplay), "updateFilter": PlayerMethod(updateFilter), + "searchAndPlay": PlayerMethod(searchAndPlay, credit=5, auto_connect=True), + "getFeaturedPlaylists": SystemMethod(getFeaturedPlaylists, credit=5), + "getCategoryPlaylists": SystemMethod(getCategoryPlaylists, credit=2) } async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None: From 237b14c2ce3d55796457ff3bcc9133e4e486772c Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:30:54 +0800 Subject: [PATCH 28/65] Enhance IPC connection handler --- ipc/client.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/ipc/client.py b/ipc/client.py index a1b2513..de0c460 100644 --- a/ipc/client.py +++ b/ipc/client.py @@ -65,9 +65,42 @@ class IPCClient: async def send(self, data: dict): if self.is_connected: - self._logger.debug(f"Send Message: {data}") - await self._websocket.send_json(data) + try: + await self._websocket.send_json(data) + self._logger.debug(f"Send Message: {data}") + except ConnectionResetError as _: + await self.disconnect() + await self.connect() + await self._websocket.send_json(data) + self._logger.debug(f"Send Message: {data}") + async def send(self, data: dict): + # Check if the websocket is still open + if self.is_connected: + try: + await self._websocket.send_json(data) + self._logger.debug(f"Sent Message: {data}") + except ConnectionResetError: + self._logger.warning("Connection lost, attempting to reconnect.") + await self._handle_reconnect(data) + except Exception as e: + self._logger.error(f"Failed to send message: {e}") + else: + self._logger.warning("WebSocket is not connected or already closed.") + + async def _handle_reconnect(self, data: dict): + await self.disconnect() + await self.connect() + await asyncio.sleep(1) # Optional delay before retrying + if self.is_connected: + try: + await self._websocket.send_json(data) + self._logger.debug(f"Sent Message on reconnect: {data}") + except Exception as e: + self._logger.error(f"Failed to send message on reconnect: {e}") + else: + self._logger.error("Reconnection failed, not connected.") + async def connect(self): try: if not self._session: @@ -107,4 +140,4 @@ class IPCClient: @property def is_connected(self) -> bool: - return self._is_connected \ No newline at end of file + return self._is_connected and self._websocket and not self._websocket.closed \ No newline at end of file From f4b3ebe2aea44331580a13286dfa97c6fd7778cd Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:33:43 +0800 Subject: [PATCH 29/65] Updated the player checker --- cogs/task.py | 65 +++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/cogs/task.py b/cogs/task.py index 7ed0db7..fe31273 100644 --- a/cogs/task.py +++ b/cogs/task.py @@ -67,44 +67,41 @@ class Task(commands.Cog): @tasks.loop(minutes=5.0) async def player_check(self): - if not self.bot.voice_clients: - return - - player: voicelink.Player - for player in self.bot.voice_clients: - try: - if not player.channel or not player.context or not player.guild: + for identifier, node in voicelink.NodePool._nodes.items(): + for guild_id, player in node._players.copy().items(): + try: + if not player.channel or not player.context or not player.guild: + await player.teardown() + continue + except: await player.teardown() continue - except: - await player.teardown() - continue - - try: - members = player.channel.members - if (not player.is_playing and player.queue.is_empty) or not any(False if member.bot or member.voice.self_deaf else True for member in members): - if not player.settings.get('24/7', False): - await player.teardown() - continue + + try: + members = player.channel.members + if (not player.is_playing and player.queue.is_empty) or not any(False if member.bot or member.voice.self_deaf else True for member in members): + if not player.settings.get('24/7', False): + await player.teardown() + continue + else: + if not player.is_paused: + await player.set_pause(True) else: - if not player.is_paused: - await player.set_pause(True) - else: - if not player.guild.me: - await player.teardown() - continue - elif not player.guild.me.voice: - await player.connect(timeout=0.0, reconnect=True) + if not player.guild.me: + await player.teardown() + continue + elif not player.guild.me.voice: + await player.connect(timeout=0.0, reconnect=True) + + if player.dj not in members: + for m in members: + if not m.bot: + player.dj = m + break + + except Exception as e: + func.logger.error("Error occurred while checking the player!", exc_info=e) - if player.dj not in members: - for m in members: - if not m.bot: - player.dj = m - break - - except Exception as e: - func.logger.error("Error occurred while checking the player!", exc_info=e) - @tasks.loop(hours=12.0) async def cache_cleaner(self): func.SETTINGS_BUFFER.clear() From e933fd3645c6279535d00f8105879f25df4fc6fb Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 22 Jan 2025 15:00:23 +0800 Subject: [PATCH 30/65] Fixed Node Reconnection Functionality --- voicelink/pool.py | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/voicelink/pool.py b/voicelink/pool.py index 997a079..f5bba50 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -210,29 +210,44 @@ class Node: await player.on_voice_state_update(data["d"]) except KeyError: return - + async def _listen(self) -> None: - backoff = ExponentialBackoff(base=7) + backoff = ExponentialBackoff(base=7) while True: try: msg = await self._websocket.receive() - except: - break - if msg.type == aiohttp.WSMsgType.CLOSED: - self._available = False - retry = backoff.delay() - self._logger.info(f"Trying to reconnect node [{self._identifier}] with {round(retry)}s") - await asyncio.sleep(retry) - if not self.is_connected: - try: - await self.connect() - except: - pass - else: + if msg.type == aiohttp.WSMsgType.CLOSED: + self._available = False + self._logger.warning(f"WebSocket closed for node [{self._identifier}]") + break + + elif msg.type == aiohttp.WSMsgType.ERROR: + self._logger.error(f"WebSocket error for node [{self._identifier}]") + break + self._bot.loop.create_task(self._handle_payload(msg.json())) + except aiohttp.ClientConnectionError as e: + self._logger.error(f"Connection error: {e}") + self._available = False + break + + except Exception as e: + self._logger.exception(f"Unexpected error: {e}") + self._available = False + break + + while not self._available: + retry = backoff.delay() + self._logger.info(f"Trying to reconnect node [{self._identifier}] in {round(retry)}s") + await asyncio.sleep(retry) + try: + await self.connect() + except Exception as e: + self._logger.error(f"Reconnection failed: {e}") + async def _handle_payload(self, data: dict) -> None: op = data.get("op", None) if not op: @@ -323,6 +338,7 @@ class Node: async def reconnect(self) -> None: await asyncio.sleep(10) for player in self.players.copy().values(): + await asyncio.sleep(3) try: if player._voice_state: await player._dispatch_voice_update(player._voice_state) From 87ace628dfc9a8544724e07c6fedff4031341bcd Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 23 Jan 2025 20:50:16 +0800 Subject: [PATCH 31/65] Fixed typo --- ipc/methods.py | 8 ++++---- main.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ipc/methods.py b/ipc/methods.py index 536e789..c782184 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -559,22 +559,22 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: elif _type == "updateInbox": user = await func.get_user(user_id) - is_accpet = data.get("accept", False) + is_accept = data.get("accept", False) - if is_accpet and len(list(user.get("playlist").keys())) >= max_p: + if is_accept and len(list(user.get("playlist").keys())) >= max_p: return error_msg(f"You cannot create more than '{max_p}' playlists!", user_id=user_id, level = "error") info = data.get("referId", "").split("-") sender_id, refer_id = info[0], info[1] inbox = user.get("inbox") - payload = {"op": "updatePlaylist", "status": "updateInbox", "userId": str(user_id), "accpet": is_accpet, "senderId": sender_id, "referId": refer_id} + payload = {"op": "updatePlaylist", "status": "updateInbox", "userId": str(user_id), "accept": is_accept, "senderId": sender_id, "referId": refer_id} for index, mail in enumerate(inbox.copy()): if not (str(mail.get("sender")) == sender_id and mail.get("referId") == refer_id): continue del inbox[index] - if is_accpet: + if is_accept: share_playlists = await func.get_user(mail["sender"], "playlist") if refer_id not in share_playlists: return error_msg("The shared playlist couldn’t be found. It’s possible that the user has already deleted it.", user_id=user_id) diff --git a/main.py b/main.py index d2ff78c..e169576 100644 --- a/main.py +++ b/main.py @@ -198,11 +198,11 @@ if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True): file_handler.namer = lambda name: name.replace(".log", "") + ".log" file_handler.setFormatter(logging.Formatter('{asctime} [{levelname:<8}] {name}: {message}', '%Y-%m-%d %H:%M:%S', style='{')) - for log_name, log_level in LOG_SETTINGS.get("level", {}).items(): - _logger = logging.getLogger(log_name) - _logger.setLevel(log_level) - - logging.getLogger().addHandler(file_handler) +for log_name, log_level in LOG_SETTINGS.get("level", {}).items(): + _logger = logging.getLogger(log_name) + _logger.setLevel(log_level) + +logging.getLogger().addHandler(file_handler) # Setup the bot object intents = discord.Intents.default() From 1910c7e0b628014346b7697eabe08e3abd39b2da Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:26:01 +0800 Subject: [PATCH 32/65] Fixed bugs --- main.py | 5 ++--- voicelink/player.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index e169576..8d5037b 100644 --- a/main.py +++ b/main.py @@ -197,13 +197,12 @@ if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True): file_handler = TimedRotatingFileHandler(filename=f'{log_path}/vocard.log', encoding="utf-8", backupCount=LOG_SETTINGS.get("max-history", 30), when="d") file_handler.namer = lambda name: name.replace(".log", "") + ".log" file_handler.setFormatter(logging.Formatter('{asctime} [{levelname:<8}] {name}: {message}', '%Y-%m-%d %H:%M:%S', style='{')) + logging.getLogger().addHandler(file_handler) for log_name, log_level in LOG_SETTINGS.get("level", {}).items(): _logger = logging.getLogger(log_name) _logger.setLevel(log_level) - -logging.getLogger().addHandler(file_handler) - + # Setup the bot object intents = discord.Intents.default() intents.message_content = False if func.settings.bot_prefix is None else True diff --git a/voicelink/player.py b/voicelink/player.py index 75b45a4..8b6f51d 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -38,7 +38,8 @@ from discord import ( Message, PartialMessage, Interaction, - errors + errors, + ChannelType ) from discord.ext import commands @@ -851,7 +852,8 @@ class Player(VoiceProtocol): rv = {key: func() if callable(func) else func for key, func in self._ph.variables.items()} status = None if remove_status else self._ph.replace(text=template, variables=rv) # if self.channel.status != status: - await self.channel.edit(status=status) + if self.channel.type == ChannelType.voice: + await self.channel.edit(status=status) except Exception as e: self._logger.error( From 537f9f02f2c4dd8e3f843b4ff5f50f92b1ec0184 Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:45:53 +0200 Subject: [PATCH 33/65] Add files via upload --- Dockerfile | 32 ++++++++---- application.yml | 126 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 109 ++++++++++++++++++++++++++------------- 3 files changed, 220 insertions(+), 47 deletions(-) create mode 100644 application.yml diff --git a/Dockerfile b/Dockerfile index 91db733..3cd2bf1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,26 @@ -FROM python:3.12-slim +# Stage 1: Build +FROM python:3.12-slim-bookworm as builder -# Install build dependencies -RUN apt-get update -y && apt-get install -y gcc python3-dev - -# Set the working directory to /app WORKDIR /app - -# Copy the current directory contents into the container at /app -COPY . /app - -# Install any needed packages specified in requirements.txt +COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Run main.py when the container launches +# Stage 2: Runtime +FROM python:3.12-slim-bookworm + +# Install system dependencies (if any are needed) +RUN apt-get update && apt-get install -y --no-install-recommends \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /app + +# Copy installed Python packages from the builder stage +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages + +# Copy the application code +COPY . . + +# Run the application CMD ["python", "-u", "main.py"] \ No newline at end of file diff --git a/application.yml b/application.yml new file mode 100644 index 0000000..63f6a62 --- /dev/null +++ b/application.yml @@ -0,0 +1,126 @@ +server: # REST and WS server + port: 2333 + address: 0.0.0.0 + http2: + enabled: true # Whether to enable HTTP/2 support +plugins: + youtube: + enabled: true # Whether this source can be used. + allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used. + allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded. + allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded. + # The clients to use for track loading. See below for a list of valid clients. + # Clients are queried in the order they are given (so the first client is queried first and so on...) + clients: + - MUSIC + - ANDROID_VR + - WEB + - WEBEMBEDDED + # The below section of the config allows setting specific options for each client, such as the requests they will handle. + # If an option, or client, is unspecified, then the default option value/client values will be used instead. + # If a client is configured, but is not registered above, the options for that client will be ignored. + # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT + # WARNING!: UNDERSTANDING WHAT IT DOES. MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. + + # Write the names of clients as they are specified under the heading "Available Clients". + clientOptions: + WEB: + # Example: Disabling a client's playback capabilities. + playback: false + videoLoading: false # Disables loading of videos for this client. A client may still be used for playback even if this is set to 'false'. + WEBEMBEDDED: + # Example: Configuring a client to exclusively be used for video loading and playback. + playlistLoading: false # Disables loading of playlists and mixes. + searching: false # Disables the ability to search for videos. +lavalink: + plugins: + - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.3" +# - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin +# repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default +# snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository +# pluginsDir: "./plugins" # optional, defaults to "./plugins" +# defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository +# defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository + server: + password: "youshallnotpass" + sources: + # The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead. + youtube: false + bandcamp: true + soundcloud: true + twitch: true + vimeo: true + nico: true + http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address. + local: false + filters: # All filters are enabled by default + volume: true + equalizer: true + karaoke: true + timescale: true + tremolo: true + vibrato: true + distortion: true + rotation: true + channelMix: true + lowPass: true + nonAllocatingFrameBuffer: false # Setting to true reduces the number of allocations made by each player at the expense of frame rebuilding (e.g. non-instantaneous volume changes) + bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses. + frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered + opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. + resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU. + trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data. + useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready. + youtubePlaylistLoadLimit: 6 # Number of pages at 100 each + playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds + youtubeSearchEnabled: true + soundcloudSearchEnabled: true + gc-warnings: true + #ratelimit: + #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks + #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink + #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch + #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing + #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times + #youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without. + #email: "" # Email of Google account + #password: "" # Password of Google account + #httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked + #proxyHost: "localhost" # Hostname of the proxy, (ip or domain) + #proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy + #proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth + #proxyPassword: "" # Password for basic authentication + +metrics: + prometheus: + enabled: false + endpoint: /metrics + +sentry: + dsn: "" + environment: "" +# tags: +# some_key: some_value +# another_key: another_value + +logging: + file: + path: ./logs/ + + level: + root: INFO + lavalink: INFO + + request: + enabled: true + includeClientInfo: true + includeHeaders: false + includeQueryString: true + includePayload: true + maxPayloadLength: 10000 + + + logback: + rollingpolicy: + max-file-size: 1GB + max-history: 30 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 07edf48..624b5a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,61 +1,98 @@ -version: "3.8" +# ------------------------------------------------------------------------------------------------------------ # + +# READ THIS BEFORE INSTALL! + +# This is a docker-compose file for running Vocard with Lavalink and MongoDB(optional). +# You can selfhost MongoDB. Just uncomment lines starting with single "#" below in the compose file. +# In order to run this, you need to have Docker and Docker Compose installed. +# You can install Docker from https://docs.docker.com/get-docker/ +# and Docker Compose from https://docs.docker.com/compose/install/ + +# Step 1: Start the installation by creating the future config directory for Vocard. +# example - `root@docker:~# mkdir -p /opt/vocard/config` + +# Use `cd` to navigate to the config directory. +# example - `root@docker:~# cd /opt/vocard/config` + +# Step 3: Choose installation method: Build the image from the Dockerfile or pull it from GitHub(recommended). +# If you chose to pull from Docker Hub, comment the "build" lines and uncomment the "image" line. +# If you chose to build the image from the Dockerfile, do the following: +# uncomment this +# build: +# dockerfile: ./Dockerfile +# and comment this +# image: ghcr.io/choco/vocard:latest +# example - `root@docker:/opt/vocard/config# wget https://github.com/ChocoMeow/Vocard/archive/refs/heads/main.zip` + +# Step 4: Configure application.yml and settings.json in the config directory. +# In order to avoid silly syntax errors it is recommended to use external code editor such as VS Code or Notepad++. +# Then you can upload files to host using tools such as WinSCP or +# using `nano` to create and edit the files directly using hosts terminal. +# NOTE that some terminals DO NOT let you paste, so you can either use WinSCP or SSH app like Putty. + + +# example - `root@docker:/opt/vocard/config# nano application.yml` +# example - `root@docker:/opt/vocard/config# nano settings.json` +# To exit nano, press `Ctrl + S`, then `Ctrl + X` to save changes. + + +# Step 5: If the values are set correctly, you can start the installation by running the following command +# example - `root@docker:/opt/vocard/config# docker-compose up -d` (could be `docker compose` on some systems) + +# ------------------------------------------ THANK YOU FOR READING! ------------------------------------------ # services: lavalink: - image: ghcr.io/lavalink-devs/lavalink:latest container_name: lavalink + image: ghcr.io/lavalink-devs/lavalink:latest + restart: unless-stopped environment: - _JAVA_OPTIONS=-Xmx1G - SERVER_PORT=2333 - - LAVALINK_SERVER_PASSWORD=youshallnotpass + - LAVALINK_SERVER_PASSWORD=youshallnotpass # Change password if needed (don't forget to change it in settings.json too) volumes: - ## Use "./" if you want to create a mount from your current directory (where docker-compose.yml is located) - ## Having access to files INSIDE the container is complicated. Mount function is used to have access to certain container files or folders. - ## Read more: https://docs.docker.com/storage/bind-mounts/ - - ./application.yml:/opt/Lavalink/application.yml + - ./application.yml:/opt/Lavalink/application.yml # Mount the application.yml file to the container. Use relative path to the file (relative to docker-compose.yml). networks: - - local + - vocard expose: - "2333" - # # You can selfhost MongoDB. Just uncomment lines starting with single "#" below. - # mongo: - # image: mongo:latest - # container_name: mongo - # restart: unless-stopped - # volumes: - # # Use "./" if you want to create a mount from your current directory (where docker-compose.yml is located) - # # Having access to files INSIDE the container is complicated. Mount function is used to have access to certain container files or folders. - # # Read more: https://docs.docker.com/storage/bind-mounts/ - # - ./data/mongo/db:/data/db - # - ./data/mongo/conf:/data/configdb - # environment: - # - MONGO_INITDB_ROOT_USERNAME=admin - # - MONGO_INITDB_ROOT_PASSWORD=admin - # expose: - # - "27017" - # networks: - # - local - # command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"] + #mongo: + # container_name: mongo + # image: mongo:latest + # + # restart: unless-stopped + # volumes: + # # Mount the data folders to the container. Use relative path to the folder (relative to docker-compose.yml). + # - ./data/mongo/db:/data/db + # - ./data/mongo/conf:/data/configdb + # environment: + # - MONGO_INITDB_ROOT_USERNAME=admin + # - MONGO_INITDB_ROOT_PASSWORD=admin + # expose: + # - "27017" + # networks: + # - vocard + # command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"] vocard: container_name: vocard - volumes: - ## Use "./" if you want to create a mount from your current directory (where docker-compose.yml is located) - ## Having access to files INSIDE the container is complicated. Mount function is used to have access to certain container files or folders. - ## Read more: https://docs.docker.com/storage/bind-mounts/ - - ./settings.json:/app/settings.json + restart: unless-stopped + # If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line. + # image: ghcr.io/choco/vocard:latest build: dockerfile: ./Dockerfile + volumes: + - ./settings.json:/app/settings.json # Mount the settings.json file to the container. Use relative path to the file (relative to docker-compose.yml). + networks: + - vocard depends_on: lavalink: condition: service_started # mongo: # condition: service_started - networks: - - local networks: - local: - name: local + vocard: + name: vocard \ No newline at end of file From ab01af019d05fbe2539d25ca68df575f06d1af78 Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Sun, 26 Jan 2025 13:00:42 +0200 Subject: [PATCH 34/65] Update docker-compose.yml --- docker-compose.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 624b5a2..a2bcf17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,7 @@ # example - `root@docker:/opt/vocard/config# docker-compose up -d` (could be `docker compose` on some systems) # ------------------------------------------ THANK YOU FOR READING! ------------------------------------------ # - +name: vocard services: lavalink: container_name: lavalink @@ -50,13 +50,20 @@ services: environment: - _JAVA_OPTIONS=-Xmx1G - SERVER_PORT=2333 - - LAVALINK_SERVER_PASSWORD=youshallnotpass # Change password if needed (don't forget to change it in settings.json too) + # there is no point in changing the password here, since the container is available only in docker network + - LAVALINK_SERVER_PASSWORD=youshallnotpass # Change password if needed (don't forget to change it in healthcheck below and settings.json) volumes: - - ./application.yml:/opt/Lavalink/application.yml # Mount the application.yml file to the container. Use relative path to the file (relative to docker-compose.yml). + - ./application.yml:/opt/Lavalink/application.yml networks: - vocard expose: - "2333" + healthcheck: + test: 'curl -H "Authorization: youshallnotpass" -s http://localhost:2333/version' + interval: 10s + timeout: 10s + retries: 5 + start_period: 10s #mongo: # container_name: mongo @@ -64,7 +71,6 @@ services: # # restart: unless-stopped # volumes: - # # Mount the data folders to the container. Use relative path to the folder (relative to docker-compose.yml). # - ./data/mongo/db:/data/db # - ./data/mongo/conf:/data/configdb # environment: @@ -84,15 +90,15 @@ services: build: dockerfile: ./Dockerfile volumes: - - ./settings.json:/app/settings.json # Mount the settings.json file to the container. Use relative path to the file (relative to docker-compose.yml). + - ./settings.json:/app/settings.json networks: - vocard depends_on: lavalink: - condition: service_started + condition: service_healthy # mongo: # condition: service_started networks: vocard: - name: vocard \ No newline at end of file + name: vocard From 9a9254b18324e8284e1cd126895d1fab7b1660e0 Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Sun, 26 Jan 2025 13:19:24 +0200 Subject: [PATCH 35/65] Add files via upload --- Dockerfile-lavalink | 19 +++++++++++++++++++ docker-compose.yml | 14 +++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 Dockerfile-lavalink diff --git a/Dockerfile-lavalink b/Dockerfile-lavalink new file mode 100644 index 0000000..837e96c --- /dev/null +++ b/Dockerfile-lavalink @@ -0,0 +1,19 @@ +FROM eclipse-temurin:22-jre-alpine + +# Install wget to download Lavalink.jar +RUN apk add --no-cache wget + +# Run as non-root user +RUN addgroup -g 322 lavalink && \ + adduser -D -u 322 -G lavalink lavalink + +WORKDIR /opt/Lavalink + +# Download Lavalink.jar +RUN wget -O Lavalink.jar https://github.com/lavalink-devs/Lavalink/releases/download/4.0.8/Lavalink.jar + +RUN chown -R lavalink:lavalink /opt/Lavalink + +USER lavalink + +ENTRYPOINT ["java", "-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2", "-jar", "Lavalink.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index a2bcf17..752f1b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,8 +44,9 @@ name: vocard services: lavalink: container_name: lavalink - image: ghcr.io/lavalink-devs/lavalink:latest - + #image: ghcr.io/lavalink-devs/lavalink:latest + build: + dockerfile: ./Dockerfile-lavalink restart: unless-stopped environment: - _JAVA_OPTIONS=-Xmx1G @@ -59,11 +60,10 @@ services: expose: - "2333" healthcheck: - test: 'curl -H "Authorization: youshallnotpass" -s http://localhost:2333/version' + test: nc -z -v localhost 2333 interval: 10s - timeout: 10s - retries: 5 - start_period: 10s + timeout: 5s + retries: 3 #mongo: # container_name: mongo @@ -101,4 +101,4 @@ services: networks: vocard: - name: vocard + name: vocard \ No newline at end of file From 3c0515adbf0e1952a74b54de33053ee024f62d2b Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:12:40 +0800 Subject: [PATCH 36/65] Added mit license --- addons/lyrics.py | 23 +++++++++++++++++++++++ addons/placeholders.py | 23 +++++++++++++++++++++++ addons/settings.py | 23 +++++++++++++++++++++++ function.py | 23 +++++++++++++++++++++++ main.py | 23 +++++++++++++++++++++++ update.py | 23 +++++++++++++++++++++++ views/__init__.py | 23 +++++++++++++++++++++++ views/embedBuilder.py | 23 +++++++++++++++++++++++ voicelink/formatter.py | 23 +++++++++++++++++++++++ voicelink/placeholders.py | 23 +++++++++++++++++++++++ 10 files changed, 230 insertions(+) diff --git a/addons/lyrics.py b/addons/lyrics.py index aa71d53..ca1583b 100644 --- a/addons/lyrics.py +++ b/addons/lyrics.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import aiohttp, random, bs4, re import function as func diff --git a/addons/placeholders.py b/addons/placeholders.py index d08d097..a3a2a64 100644 --- a/addons/placeholders.py +++ b/addons/placeholders.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from discord.ext import commands from re import findall from importlib import import_module diff --git a/addons/settings.py b/addons/settings.py index 7638101..c5b8c5c 100644 --- a/addons/settings.py +++ b/addons/settings.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from typing import ( Dict, List, diff --git a/function.py b/function.py index da6a72f..8d8ec79 100644 --- a/function.py +++ b/function.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import discord, json, os, copy, logging from discord.ext import commands diff --git a/main.py b/main.py index 8d5037b..2bbbdc1 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import discord import sys import os diff --git a/update.py b/update.py index ddb060a..efae7f2 100644 --- a/update.py +++ b/update.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import requests, zipfile, os, shutil, argparse from io import BytesIO diff --git a/views/__init__.py b/views/__init__.py index f060dd4..883cfe5 100644 --- a/views/__init__.py +++ b/views/__init__.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from discord.ext import commands class ButtonOnCooldown(commands.CommandError): diff --git a/views/embedBuilder.py b/views/embedBuilder.py index 405dd0e..1e93c91 100644 --- a/views/embedBuilder.py +++ b/views/embedBuilder.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import discord, copy import function as func diff --git a/voicelink/formatter.py b/voicelink/formatter.py index 115d642..71480fd 100644 --- a/voicelink/formatter.py +++ b/voicelink/formatter.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from __future__ import annotations import base64, io, abc, struct, dataclasses diff --git a/voicelink/placeholders.py b/voicelink/placeholders.py index 92c577d..b1f361e 100644 --- a/voicelink/placeholders.py +++ b/voicelink/placeholders.py @@ -1,3 +1,26 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + from __future__ import annotations import re From 0b0fbc4720b753c7fc76e5d92588a1e6a27c9b9c Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 31 Jan 2025 21:49:14 +0800 Subject: [PATCH 37/65] Group lavalink configs in folder --- docker-compose.yml | 12 +- .../Dockerfile-lavalink | 38 +-- application.yml => lavalink/application.yml | 250 +++++++++--------- 3 files changed, 149 insertions(+), 151 deletions(-) rename Dockerfile-lavalink => lavalink/Dockerfile-lavalink (96%) rename application.yml => lavalink/application.yml (98%) diff --git a/docker-compose.yml b/docker-compose.yml index 752f1b6..841cfde 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ # This is a docker-compose file for running Vocard with Lavalink and MongoDB(optional). # You can selfhost MongoDB. Just uncomment lines starting with single "#" below in the compose file. # In order to run this, you need to have Docker and Docker Compose installed. -# You can install Docker from https://docs.docker.com/get-docker/ +# You can install Docker from https://docs.docker.com/get-docker/ # and Docker Compose from https://docs.docker.com/compose/install/ # Step 1: Start the installation by creating the future config directory for Vocard. @@ -30,12 +30,10 @@ # using `nano` to create and edit the files directly using hosts terminal. # NOTE that some terminals DO NOT let you paste, so you can either use WinSCP or SSH app like Putty. - # example - `root@docker:/opt/vocard/config# nano application.yml` # example - `root@docker:/opt/vocard/config# nano settings.json` # To exit nano, press `Ctrl + S`, then `Ctrl + X` to save changes. - # Step 5: If the values are set correctly, you can start the installation by running the following command # example - `root@docker:/opt/vocard/config# docker-compose up -d` (could be `docker compose` on some systems) @@ -46,7 +44,7 @@ services: container_name: lavalink #image: ghcr.io/lavalink-devs/lavalink:latest build: - dockerfile: ./Dockerfile-lavalink + dockerfile: ./lavalink/Dockerfile-lavalink restart: unless-stopped environment: - _JAVA_OPTIONS=-Xmx1G @@ -54,7 +52,7 @@ services: # there is no point in changing the password here, since the container is available only in docker network - LAVALINK_SERVER_PASSWORD=youshallnotpass # Change password if needed (don't forget to change it in healthcheck below and settings.json) volumes: - - ./application.yml:/opt/Lavalink/application.yml + - ./lavalink/application.yml:/opt/Lavalink/application.yml networks: - vocard expose: @@ -68,7 +66,7 @@ services: #mongo: # container_name: mongo # image: mongo:latest - # + # # restart: unless-stopped # volumes: # - ./data/mongo/db:/data/db @@ -101,4 +99,4 @@ services: networks: vocard: - name: vocard \ No newline at end of file + name: vocard diff --git a/Dockerfile-lavalink b/lavalink/Dockerfile-lavalink similarity index 96% rename from Dockerfile-lavalink rename to lavalink/Dockerfile-lavalink index 837e96c..e0fb2b0 100644 --- a/Dockerfile-lavalink +++ b/lavalink/Dockerfile-lavalink @@ -1,19 +1,19 @@ -FROM eclipse-temurin:22-jre-alpine - -# Install wget to download Lavalink.jar -RUN apk add --no-cache wget - -# Run as non-root user -RUN addgroup -g 322 lavalink && \ - adduser -D -u 322 -G lavalink lavalink - -WORKDIR /opt/Lavalink - -# Download Lavalink.jar -RUN wget -O Lavalink.jar https://github.com/lavalink-devs/Lavalink/releases/download/4.0.8/Lavalink.jar - -RUN chown -R lavalink:lavalink /opt/Lavalink - -USER lavalink - -ENTRYPOINT ["java", "-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2", "-jar", "Lavalink.jar"] +FROM eclipse-temurin:22-jre-alpine + +# Install wget to download Lavalink.jar +RUN apk add --no-cache wget + +# Run as non-root user +RUN addgroup -g 322 lavalink && \ + adduser -D -u 322 -G lavalink lavalink + +WORKDIR /opt/Lavalink + +# Download Lavalink.jar +RUN wget -O Lavalink.jar https://github.com/lavalink-devs/Lavalink/releases/download/4.0.8/Lavalink.jar + +RUN chown -R lavalink:lavalink /opt/Lavalink + +USER lavalink + +ENTRYPOINT ["java", "-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2", "-jar", "Lavalink.jar"] diff --git a/application.yml b/lavalink/application.yml similarity index 98% rename from application.yml rename to lavalink/application.yml index 63f6a62..76a01a9 100644 --- a/application.yml +++ b/lavalink/application.yml @@ -1,126 +1,126 @@ -server: # REST and WS server - port: 2333 - address: 0.0.0.0 - http2: - enabled: true # Whether to enable HTTP/2 support -plugins: - youtube: - enabled: true # Whether this source can be used. - allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used. - allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded. - allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded. - # The clients to use for track loading. See below for a list of valid clients. - # Clients are queried in the order they are given (so the first client is queried first and so on...) - clients: - - MUSIC - - ANDROID_VR - - WEB - - WEBEMBEDDED - # The below section of the config allows setting specific options for each client, such as the requests they will handle. - # If an option, or client, is unspecified, then the default option value/client values will be used instead. - # If a client is configured, but is not registered above, the options for that client will be ignored. - # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT - # WARNING!: UNDERSTANDING WHAT IT DOES. MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. - - # Write the names of clients as they are specified under the heading "Available Clients". - clientOptions: - WEB: - # Example: Disabling a client's playback capabilities. - playback: false - videoLoading: false # Disables loading of videos for this client. A client may still be used for playback even if this is set to 'false'. - WEBEMBEDDED: - # Example: Configuring a client to exclusively be used for video loading and playback. - playlistLoading: false # Disables loading of playlists and mixes. - searching: false # Disables the ability to search for videos. -lavalink: - plugins: - - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.3" -# - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin -# repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default -# snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository -# pluginsDir: "./plugins" # optional, defaults to "./plugins" -# defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository -# defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository - server: - password: "youshallnotpass" - sources: - # The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead. - youtube: false - bandcamp: true - soundcloud: true - twitch: true - vimeo: true - nico: true - http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address. - local: false - filters: # All filters are enabled by default - volume: true - equalizer: true - karaoke: true - timescale: true - tremolo: true - vibrato: true - distortion: true - rotation: true - channelMix: true - lowPass: true - nonAllocatingFrameBuffer: false # Setting to true reduces the number of allocations made by each player at the expense of frame rebuilding (e.g. non-instantaneous volume changes) - bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses. - frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered - opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. - resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU. - trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data. - useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready. - youtubePlaylistLoadLimit: 6 # Number of pages at 100 each - playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds - youtubeSearchEnabled: true - soundcloudSearchEnabled: true - gc-warnings: true - #ratelimit: - #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks - #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink - #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch - #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing - #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times - #youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without. - #email: "" # Email of Google account - #password: "" # Password of Google account - #httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked - #proxyHost: "localhost" # Hostname of the proxy, (ip or domain) - #proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy - #proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth - #proxyPassword: "" # Password for basic authentication - -metrics: - prometheus: - enabled: false - endpoint: /metrics - -sentry: - dsn: "" - environment: "" -# tags: -# some_key: some_value -# another_key: another_value - -logging: - file: - path: ./logs/ - - level: - root: INFO - lavalink: INFO - - request: - enabled: true - includeClientInfo: true - includeHeaders: false - includeQueryString: true - includePayload: true - maxPayloadLength: 10000 - - - logback: - rollingpolicy: - max-file-size: 1GB +server: # REST and WS server + port: 2333 + address: 0.0.0.0 + http2: + enabled: true # Whether to enable HTTP/2 support +plugins: + youtube: + enabled: true # Whether this source can be used. + allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used. + allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded. + allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded. + # The clients to use for track loading. See below for a list of valid clients. + # Clients are queried in the order they are given (so the first client is queried first and so on...) + clients: + - MUSIC + - ANDROID_VR + - WEB + - WEBEMBEDDED + # The below section of the config allows setting specific options for each client, such as the requests they will handle. + # If an option, or client, is unspecified, then the default option value/client values will be used instead. + # If a client is configured, but is not registered above, the options for that client will be ignored. + # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT + # WARNING!: UNDERSTANDING WHAT IT DOES. MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. + + # Write the names of clients as they are specified under the heading "Available Clients". + clientOptions: + WEB: + # Example: Disabling a client's playback capabilities. + playback: false + videoLoading: false # Disables loading of videos for this client. A client may still be used for playback even if this is set to 'false'. + WEBEMBEDDED: + # Example: Configuring a client to exclusively be used for video loading and playback. + playlistLoading: false # Disables loading of playlists and mixes. + searching: false # Disables the ability to search for videos. +lavalink: + plugins: + - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.3" +# - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin +# repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default +# snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository +# pluginsDir: "./plugins" # optional, defaults to "./plugins" +# defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository +# defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository + server: + password: "youshallnotpass" + sources: + # The default Youtube source is now deprecated and won't receive further updates. Please use https://github.com/lavalink-devs/youtube-source#plugin instead. + youtube: false + bandcamp: true + soundcloud: true + twitch: true + vimeo: true + nico: true + http: true # warning: keeping HTTP enabled without a proxy configured could expose your server's IP address. + local: false + filters: # All filters are enabled by default + volume: true + equalizer: true + karaoke: true + timescale: true + tremolo: true + vibrato: true + distortion: true + rotation: true + channelMix: true + lowPass: true + nonAllocatingFrameBuffer: false # Setting to true reduces the number of allocations made by each player at the expense of frame rebuilding (e.g. non-instantaneous volume changes) + bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses. + frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered + opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. + resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU. + trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data. + useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready. + youtubePlaylistLoadLimit: 6 # Number of pages at 100 each + playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds + youtubeSearchEnabled: true + soundcloudSearchEnabled: true + gc-warnings: true + #ratelimit: + #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks + #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink + #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch + #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing + #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times + #youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without. + #email: "" # Email of Google account + #password: "" # Password of Google account + #httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked + #proxyHost: "localhost" # Hostname of the proxy, (ip or domain) + #proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy + #proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth + #proxyPassword: "" # Password for basic authentication + +metrics: + prometheus: + enabled: false + endpoint: /metrics + +sentry: + dsn: "" + environment: "" +# tags: +# some_key: some_value +# another_key: another_value + +logging: + file: + path: ./logs/ + + level: + root: INFO + lavalink: INFO + + request: + enabled: true + includeClientInfo: true + includeHeaders: false + includeQueryString: true + includePayload: true + maxPayloadLength: 10000 + + + logback: + rollingpolicy: + max-file-size: 1GB max-history: 30 \ No newline at end of file From 284fd5c2fa80061696e751bb3241a415a5d5d2aa Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Fri, 31 Jan 2025 21:39:15 +0200 Subject: [PATCH 38/65] Update Dockerfile-lavalink Changed image from `alpine` to `noble` to support required libraries and also updated to 23 java runtime --- lavalink/Dockerfile-lavalink | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lavalink/Dockerfile-lavalink b/lavalink/Dockerfile-lavalink index e0fb2b0..265ab4d 100644 --- a/lavalink/Dockerfile-lavalink +++ b/lavalink/Dockerfile-lavalink @@ -1,15 +1,14 @@ -FROM eclipse-temurin:22-jre-alpine +FROM eclipse-temurin:23-jre-noble -# Install wget to download Lavalink.jar -RUN apk add --no-cache wget +RUN apt-get update && \ + apt-get install -y netcat-openbsd && \ + rm -rf /var/lib/apt/lists/* -# Run as non-root user -RUN addgroup -g 322 lavalink && \ - adduser -D -u 322 -G lavalink lavalink +RUN groupadd -g 322 lavalink && \ + useradd -u 322 -g lavalink -m -d /opt/Lavalink lavalink WORKDIR /opt/Lavalink -# Download Lavalink.jar RUN wget -O Lavalink.jar https://github.com/lavalink-devs/Lavalink/releases/download/4.0.8/Lavalink.jar RUN chown -R lavalink:lavalink /opt/Lavalink From 8cac7d8283f17fc4cfa86f99b37c8fc10986dfe4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:12:09 +0800 Subject: [PATCH 39/65] Bump youtubesource plugin version to latest release --- lavalink/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lavalink/application.yml b/lavalink/application.yml index 76a01a9..704256d 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -34,7 +34,7 @@ plugins: searching: false # Disables the ability to search for videos. lavalink: plugins: - - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.3" + - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.4" # - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin # repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default # snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository From 4a62e5b81c3eeceef5282461510ddfe03def73c5 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:37:08 +0800 Subject: [PATCH 40/65] Fixed missing gcc env for macOS in Dockerfile --- Dockerfile | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3cd2bf1..f5ea993 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,25 @@ # Stage 1: Build FROM python:3.12-slim-bookworm as builder +# Install build dependencies (gcc, Python headers, etc.) +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + python3-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory WORKDIR /app + +# Copy only the requirements file to take advantage of Docker's caching COPY requirements.txt . + +# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt # Stage 2: Runtime FROM python:3.12-slim-bookworm -# Install system dependencies (if any are needed) -RUN apt-get update && apt-get install -y --no-install-recommends \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - # Set the working directory WORKDIR /app From 283806576b68235511a92ef78747e59a48348914 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:03:42 +0800 Subject: [PATCH 41/65] Fixed a syntax error for python 3.11 --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 2bbbdc1..f8e6d08 100644 --- a/main.py +++ b/main.py @@ -143,7 +143,7 @@ class Vocard(commands.Bot): 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}'") + func.logger.warning(f'Missing translation for "{", ".join(values)}" in "{locale_key}"') async def on_ready(self): func.logger.info("------------------") From f5af35a6a1a0922b35ab0aa08e32cc186ecba84b Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:26:24 +0800 Subject: [PATCH 42/65] Added yt ratelimiter --- cogs/listeners.py | 2 +- settings Example.json | 10 +++- update.py | 2 +- voicelink/player.py | 8 ++- voicelink/pool.py | 57 +++++++++++--------- voicelink/ratelimit.py | 120 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 voicelink/ratelimit.py diff --git a/cogs/listeners.py b/cogs/listeners.py index 4db2495..a5c4a4d 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -65,7 +65,7 @@ class Listeners(commands.Cog): async def on_voicelink_track_exception(self, player: voicelink.Player, track, error: dict): try: player._track_is_stuck = True - await player.context.send(f"{error['message']}! The next song will begin in the next 5 seconds.", delete_after=10) + await player.context.send(f"{error['message']} The next song will begin in the next 5 seconds.", delete_after=10) except: pass diff --git a/settings Example.json b/settings Example.json index 5a26452..875626c 100644 --- a/settings Example.json +++ b/settings Example.json @@ -12,7 +12,15 @@ "port": 2333, "password": "youshallnotpass", "secure": false, - "identifier": "DEFAULT" + "identifier": "DEFAULT", + "yt_ratelimit": { + "tokens": [], + "config": { + "retry_time": 10800, + "max_requests": 30 + }, + "strategy": "LoadBalance" + } } }, "prefix": "?", diff --git a/update.py b/update.py index efae7f2..5b84a2b 100644 --- a/update.py +++ b/update.py @@ -25,7 +25,7 @@ import requests, zipfile, os, shutil, argparse from io import BytesIO ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.7.0b3" +__version__ = "v2.7.0b4" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" diff --git a/voicelink/player.py b/voicelink/player.py index 8b6f51d..35d27f0 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -45,7 +45,7 @@ from discord import ( from discord.ext import commands from . import events from .enums import SearchType, LoopType, RequestMethod -from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent +from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent, TrackExceptionEvent from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, TrackLoadError, FilterTagAlreadyInUse, DuplicateTrack from .filters import Filter, Filters from .objects import Track, Playlist @@ -350,6 +350,9 @@ class Player(VoiceProtocol): if isinstance(event, TrackEndEvent) and event.reason != "replaced": self._current = None + + if isinstance(event, TrackExceptionEvent) and event.exception["message"] == "This content isn’t available.": + await self._node.yt_ratelimit.flag_active_token() event.dispatch(self._bot) @@ -572,8 +575,9 @@ 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.node.yt_ratelimit.handle_request() self._current = track diff --git a/voicelink/pool.py b/voicelink/pool.py index f5bba50..1d2b5a1 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -52,6 +52,7 @@ from .exceptions import ( from .objects import Playlist, Track from .utils import ExponentialBackoff, NodeStats, NodeInfo, Ping from .enums import RequestMethod +from .ratelimit import YTRatelimit, YTToken, STRATEGY if TYPE_CHECKING: from .player import Player @@ -86,6 +87,7 @@ class Node: port: int, password: str, identifier: str, + yt_ratelimit: dict, secure: bool = False, heartbeat: int = 30, session: Optional[aiohttp.ClientSession] = None, @@ -103,7 +105,7 @@ class Node: self._heartbeat: int = heartbeat self._secure: bool = secure self._logger: Optional[logging.Logger] = logger - + self._websocket_uri: str = f"{'wss' if self._secure else 'ws'}://{self._host}:{self._port}/" + NODE_VERSION + "/websocket" self._rest_uri: str = f"{'https' if self._secure else 'http'}://{self._host}:{self._port}" @@ -129,6 +131,8 @@ class Node: self._spotify_client_secret: Optional[str] = spotify_client_secret self._spotify_client: Optional[spotify.Client] = None + self.yt_ratelimit: YTRatelimit = STRATEGY.get(yt_ratelimit.get("strategy"))(self, yt_ratelimit) + self._bot.add_listener(self._update_handler, "on_socket_response") def __repr__(self): @@ -287,11 +291,15 @@ class Node: return await resp.json(content_type=None) return await resp.json() - + async def connect(self) -> Node: """Initiates a connection with a Lavalink node and adds it to the node pool.""" try: + if self._available: + self._logger.info(f"Node [{self._identifier}] already connected.") + return + self._websocket = await self._session.ws_connect( self._websocket_uri, headers=self._headers, heartbeat=self._heartbeat ) @@ -364,18 +372,8 @@ class Node: Context object on the track it builds. """ - async with self._session.get( - f"{self._rest_uri}/" + NODE_VERSION + "/decodetrack?", - headers={"Authorization": self._password}, - params={"track": identifier} - ) as resp: - if not resp.status == 200: - raise TrackLoadError( - f"Failed to build track. Check if the identifier is correct and try again." - ) - - data: dict = await resp.json() - return Track(track_id=identifier, info=data, requester=requester) + data = await self.send(RequestMethod.GET, f"decodetrack?encodedTrack={identifier}") + return Track(track_id=identifier, info=data, requester=requester) async def get_tracks( self, @@ -436,11 +434,7 @@ class Node: ) elif DISCORD_MP3_URL_REGEX.match(query): - async with self._session.get( - url=f"{self._rest_uri}/" + NODE_VERSION + f"/loadtracks?identifier={quote(query)}", - headers={"Authorization": self._password} - ) as response: - data: dict = await response.json() + data = await self.send(RequestMethod.GET, f"loadtracks?identifier={quote(query)}") try: track: dict = data["data"] @@ -455,11 +449,7 @@ class Node: ) ] else: - async with self._session.get( - url=f"{self._rest_uri}/" + NODE_VERSION + f"/loadtracks?identifier={quote(query)}", - headers={"Authorization": self._password} - ) as response: - data = await response.json() + data = await self.send(RequestMethod.GET, f"loadtracks?identifier={quote(query)}") load_type = data.get("loadType") @@ -556,7 +546,21 @@ class Node: tracks = tracks.tracks return tracks[:limit] if limit else tracks + + async def update_refresh_yt_access_token(self, token: YTToken) -> dict: + if not self._available: + raise NodeNotAvailable(f"The node '{self._identifier}' is unavailable.") + uri: str = f"{self._rest_uri}/youtube" + async with self._session.request( + method="POST", + url=uri, + headers={"Authorization": self._password}, + json={"refreshToken": token.token} + ) as resp: + if resp.status >= 300: + raise NodeException(f"Getting errors from Lavalink REST api") + class NodePool: """The base class for the node pool. This holds all the nodes that are to be used by the bot. @@ -632,6 +636,7 @@ class NodePool: port: str, password: str, identifier: str, + yt_ratelimit: dict, secure: bool = False, heartbeat: int = 30, spotify_client_id: Optional[str] = None, @@ -651,8 +656,8 @@ class NodePool: node = Node( pool=cls, bot=bot, host=host, port=port, password=password, - identifier=identifier, secure=secure, heartbeat=heartbeat, spotify_client_id=spotify_client_id, - session=session, spotify_client_secret=spotify_client_secret, + yt_ratelimit=yt_ratelimit, identifier=identifier, secure=secure, heartbeat=heartbeat, + session=session, spotify_client_id=spotify_client_id, spotify_client_secret=spotify_client_secret, resume_key=resume_key, logger=logger ) diff --git a/voicelink/ratelimit.py b/voicelink/ratelimit.py new file mode 100644 index 0000000..35d7bf2 --- /dev/null +++ b/voicelink/ratelimit.py @@ -0,0 +1,120 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import time +from abc import ABC, abstractmethod +from typing import List, Optional, Dict, TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .pool import Node + +class YTToken: + def __init__(self, token: str): + self.token: str = token + self.allow_retry_time: float = 0.0 + self.requested_times: int = 0 + self.is_flagged: bool = False + self.flagged_time: float = 0.0 + + @property + def allow_retry(self) -> bool: + """Determine if the token can be used again.""" + return time.time() >= self.allow_retry_time + +class YTRatelimit(ABC): + """ + Abstract base class for YouTube rate limit strategies. + """ + def __init__(self, node: "Node", tokens: List[str]) -> None: + self.node: "Node" = node + self.tokens: List[YTToken] = [YTToken(token) for token in tokens] + self.active_token: Optional[YTToken] = self.tokens[0] if self.tokens else None + + @abstractmethod + async def flag_active_token(self) -> None: + """ + Mark the current active token as flagged when a rate-limit is encountered. + """ + pass + + @abstractmethod + async def handle_request(self) -> None: + """ + Update usage count or perform any necessary pre-request operations. + """ + pass + + async def swap_token(self) -> Optional[YTToken]: + """ + Swap the active token with another token that is either not flagged or ready to retry. + If a new token is found, update it via the node and return it. + """ + for token in self.tokens: + if not token.is_flagged or token.allow_retry: + try: + await self.node.update_refresh_yt_access_token(token) + self.active_token = token + return token + except Exception as e: + self.node._logger.error("Something wrong while updating the youtube access token.", exc_info=e) + + self.node._logger.warning("No active token available for processing the request.") + return None + +class LoadBalance(YTRatelimit): + """ + A rate limiting strategy that load balances requests across tokens. + """ + def __init__(self, node: "Node", config: Dict[str, Any]): + super().__init__(node, tokens=config.get("tokens", [])) + self._config: Dict[str, Any] = config.get("config", {}) + self._retry_time: int = self._config.get("retry_time", 10_800) + self._max_requests: int = self._config.get("max_requests", 30) + + async def flag_active_token(self) -> None: + """ + Flag the active token and set a delay (e.g., 3 hours) until it can be retried. + """ + if self.active_token: + self.active_token.is_flagged = True + self.active_token.flagged_time = time.time() + self.active_token.allow_retry_time = self.active_token.flagged_time + self._retry_time + await self.swap_token() + + async def handle_request(self) -> None: + """ + Increment the active token's usage counter and swap tokens if a threshold is reached. + """ + if not self.active_token: + return await self.swap_token() + + self.active_token.requested_times += 1 + if self.active_token.requested_times >= self._max_requests: + self.active_token.requested_times = 0 + swapped_token = await self.swap_token() + if swapped_token is None: + return self.node._logger.warning("No available token found after swapping.") + +STRATEGY = { + "LoadBalance": LoadBalance +} \ No newline at end of file From da36a65bdaaa42cc5c61151b8c76a492b8e46418 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:26:55 +0800 Subject: [PATCH 43/65] Dump discord.py and aiohttp --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 386fa6e..26abb7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.4.0 +discord.py==2.5.0 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 @@ -6,4 +6,4 @@ validators==0.18.2 humanize==4.0.0 beautifulsoup4==4.11.1 psutil==5.9.8 -aiohttp==3.9.5 \ No newline at end of file +aiohttp==3.11.12 \ No newline at end of file From 59034d8f5ae5136878e159fa15e2031979579f0c Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:35:24 +0800 Subject: [PATCH 44/65] Prevent usage of active token --- voicelink/ratelimit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicelink/ratelimit.py b/voicelink/ratelimit.py index 35d7bf2..171a512 100644 --- a/voicelink/ratelimit.py +++ b/voicelink/ratelimit.py @@ -70,7 +70,7 @@ class YTRatelimit(ABC): If a new token is found, update it via the node and return it. """ for token in self.tokens: - if not token.is_flagged or token.allow_retry: + if token != self.active_token and (not token.is_flagged or token.allow_retry): try: await self.node.update_refresh_yt_access_token(token) self.active_token = token From 5b88f89894cee2ba217716e80022dce97443bd94 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:17:55 +0800 Subject: [PATCH 45/65] Changed yt-ratelimit to optional --- voicelink/player.py | 17 +++++++++-------- voicelink/pool.py | 8 ++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/voicelink/player.py b/voicelink/player.py index 35d27f0..b17ff76 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -43,6 +43,7 @@ from discord import ( ) from discord.ext import commands + from . import events from .enums import SearchType, LoopType, RequestMethod from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent, TrackExceptionEvent @@ -352,7 +353,8 @@ class Player(VoiceProtocol): self._current = None if isinstance(event, TrackExceptionEvent) and event.exception["message"] == "This content isn’t available.": - await self._node.yt_ratelimit.flag_active_token() + if self._node.yt_ratelimit: + await self._node.yt_ratelimit.flag_active_token() event.dispatch(self._bot) @@ -438,16 +440,14 @@ class Player(VoiceProtocol): self.controller = await self.context.channel.send(embed=embed, view=view) elif not await self.is_position_fresh(): - try: - await self.controller.delete() - except Exception as e: - self._logger.warning( - f"Failed to delete outdated controller in {self.guild.name}({self.guild.id}): {e}" - ) + await self.controller.delete() self.controller = await self.context.channel.send(embed=embed, view=view) else: await self.controller.edit(embed=embed, view=view) + + except errors.Forbidden: + self._logger.warning(f"Missing permission to update the music controller on {self.guild.name}({self.guild.id})") 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) @@ -577,7 +577,8 @@ class Player(VoiceProtocol): 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.node.yt_ratelimit.handle_request() + if self._node.yt_ratelimit: + await self._node.yt_ratelimit.handle_request() self._current = track diff --git a/voicelink/pool.py b/voicelink/pool.py index 1d2b5a1..593695e 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -87,9 +87,9 @@ class Node: port: int, password: str, identifier: str, - yt_ratelimit: dict, secure: bool = False, heartbeat: int = 30, + yt_ratelimit: dict = None, session: Optional[aiohttp.ClientSession] = None, spotify_client_id: Optional[str] = None, spotify_client_secret: Optional[str] = None, @@ -131,7 +131,7 @@ class Node: self._spotify_client_secret: Optional[str] = spotify_client_secret self._spotify_client: Optional[spotify.Client] = None - self.yt_ratelimit: YTRatelimit = STRATEGY.get(yt_ratelimit.get("strategy"))(self, yt_ratelimit) + self.yt_ratelimit: Optional[YTRatelimit] = STRATEGY.get(yt_ratelimit.get("strategy"))(self, yt_ratelimit) if yt_ratelimit else None self._bot.add_listener(self._update_handler, "on_socket_response") @@ -636,9 +636,9 @@ class NodePool: port: str, password: str, identifier: str, - yt_ratelimit: dict, secure: bool = False, heartbeat: int = 30, + yt_ratelimit: dict = None, spotify_client_id: Optional[str] = None, spotify_client_secret: Optional[str] = None, session: Optional[aiohttp.ClientSession] = None, @@ -656,7 +656,7 @@ class NodePool: node = Node( pool=cls, bot=bot, host=host, port=port, password=password, - yt_ratelimit=yt_ratelimit, identifier=identifier, secure=secure, heartbeat=heartbeat, + identifier=identifier, secure=secure, heartbeat=heartbeat, yt_ratelimit=yt_ratelimit, session=session, spotify_client_id=spotify_client_id, spotify_client_secret=spotify_client_secret, resume_key=resume_key, logger=logger ) From 2c16543e0cfa4d230d2e2fcce7194e947ce362c4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:01:37 +0800 Subject: [PATCH 46/65] Fixed track source --- cogs/basic.py | 11 ++++++----- function.py | 4 ++-- settings Example.json | 6 +++--- voicelink/enums.py | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index fa885dd..49740cd 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 send(ctx, embed, view=LinkView(texts[2].format(track.source), track.emoji, track.uri)) + return await send(ctx, embed, view=LinkView(texts[2].format(track.source.title()), track.emoji, track.uri)) class Basic(commands.Cog): def __init__(self, bot: commands.Bot) -> None: @@ -86,7 +86,7 @@ class Basic(commands.Cog): return [app_commands.Choice(name=c.capitalize(), value=c) for c in self.bot.cogs if c not in ["Nodes", "Task"] and current in c] 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 voicelink.pool.URL_REGEX.match(current): return [] if current: node = voicelink.NodePool.get_node() @@ -216,7 +216,7 @@ class Basic(commands.Cog): platform="Select the platform you want to search." ) @app_commands.choices(platform=[ - app_commands.Choice(name=search_type.name.replace("_", " ").title(), value=search_type.name) + app_commands.Choice(name=search_type.display_name, value=search_type.name) for search_type in SearchType ]) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -232,13 +232,14 @@ class Basic(commands.Cog): if url(query): return await send(ctx, "noLinkSupport", ephemeral=True) - tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=SearchType[platform] if platform in SearchType.__members__ else SearchType.YOUTUBE) + search_type: SearchType = SearchType.match(platform) or SearchType.YOUTUBE + tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=search_type) if not tracks: return await send(ctx, "noTrackFound") texts = await get_lang(ctx.guild.id, "searchTitle", "searchDesc", "live", "trackLoad_pos", "trackLoad", "searchWait", "searchSuccess") query_track = "\n".join(f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1)) - embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(get_source(platform, "emoji"), platform, len(tracks[0:10]), query_track), color=settings.embed_color) + embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(get_source(search_type.display_name, "emoji"), search_type.display_name, len(tracks[0:10]), query_track), color=settings.embed_color) view = SearchView(tracks=tracks[0:10], texts=[texts[5], texts[6]]) view.response = await send(ctx, embed, view=view, ephemeral=True) diff --git a/function.py b/function.py index 8d8ec79..2b9d9cd 100644 --- a/function.py +++ b/function.py @@ -131,8 +131,8 @@ 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(), settings.sources_settings.get("others")) - return source_settings.get(type, ("🔗" if type == "emoji" else settings.embed_color)) + source_settings: dict[str, str] = settings.sources_settings.get(source.lower().replace(" ", ""), settings.sources_settings.get("others")) + return source_settings.get(type) def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]: if ctx.author.id in settings.bot_access_user: diff --git a/settings Example.json b/settings Example.json index 875626c..3c195a5 100644 --- a/settings Example.json +++ b/settings Example.json @@ -55,7 +55,7 @@ "emoji": "<:youtube:826661982760992778>", "color": "0xFF0000" }, - "youtube music": { + "youtubemusic": { "emoji": "<:youtube:826661982760992778>", "color": "0xFF0000" }, @@ -79,7 +79,7 @@ "emoji": "<:vimeo:864694001919721473>", "color": "0x1ABCEA" }, - "apple": { + "applemusic": { "emoji": "<:applemusic:994844332374884413>", "color": "0xE298C4" }, @@ -92,7 +92,7 @@ "color": "0x74ECE9" }, "others": { - "emoji": "🌎", + "emoji": "🔗", "color": "0xb3b3b3" } }, diff --git a/voicelink/enums.py b/voicelink/enums.py index 9fb9578..58a0bc8 100644 --- a/voicelink/enums.py +++ b/voicelink/enums.py @@ -65,6 +65,21 @@ class SearchType(Enum): def __str__(self) -> str: return self.value + + @classmethod + def match(cls, value: str): + """find an enum based on a search string.""" + normalized_value = value.lower().replace("_", "").replace(" ", "") + + for member in cls: + normalized_name = member.name.lower().replace("_", "") + if member.value == value or normalized_name == normalized_value: + return member + return None + + @property + def display_name(self) -> str: + return self.name.replace("_", " ").title() class RequestMethod(Enum): """The enum for the different request methods in Voicelink From ef66400f663bea97ee107d0bfeede564c67f7447 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:13:22 +0800 Subject: [PATCH 47/65] Dump discord.py --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 26abb7c..eeb5897 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.5.0 +discord.py==2.5.1 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 From 76b9a41b40b703fb57964b07509965003c339fa4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:47:13 +0800 Subject: [PATCH 48/65] Dump discordpy --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eeb5897..352250a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.5.1 +discord.py==2.5.2 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 From ecc6a2144737ea47971332ea253f993a3fd8f145 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:30:38 +0800 Subject: [PATCH 49/65] Added lyrics button in controller --- 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 | 3 ++- langs/UA.json | 1 + views/controller.py | 36 +++++++++++++++++++++++++++++++----- 9 files changed, 40 insertions(+), 6 deletions(-) diff --git a/langs/CH.json b/langs/CH.json index ff6d4be..1b7ba66 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -116,6 +116,7 @@ "buttonShuffle": "隨機播放", "buttonForward": "前進", "buttonRewind": "後退", + "buttonLyrics": "歌詞", "nowplayingDesc": "**現在播放:**\n```{0}```", "nowplayingField": "接下來播放:", diff --git a/langs/DE.json b/langs/DE.json index c863567..aa92b94 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -116,6 +116,7 @@ "buttonShuffle": "Zufall", "buttonForward": "Vorspulen", "buttonRewind": "Zurückspulen", + "buttonLyrics": "Liedtexte", "nowplayingDesc": "**Jetzt wird abgespielt:**\n```{0}```", "nowplayingField": "Als nächstes:", diff --git a/langs/EN.json b/langs/EN.json index e11767f..271e270 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -116,6 +116,7 @@ "buttonShuffle": "Shuffle", "buttonForward": "Forward", "buttonRewind": "Rewind", + "buttonLyrics": "Lyrics", "nowplayingDesc": "**Now Playing:**\n```{0}```", "nowplayingField": "Up Next:", diff --git a/langs/ES.json b/langs/ES.json index 990cdea..c0185a9 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -116,6 +116,7 @@ "buttonShuffle": "Aleatorio", "buttonForward": "Adelante", "buttonRewind": "Atrás", + "buttonLyrics": "Letras", "nowplayingDesc": "**Reproduciendo ahora:**\n```{0}```", "nowplayingField": "A continuación:", diff --git a/langs/JA.json b/langs/JA.json index 9544615..10abde1 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -116,6 +116,7 @@ "buttonShuffle": "シャッフル", "buttonForward": "進む", "buttonRewind": "戻る", + "buttonLyrics": "歌詞", "nowplayingDesc": "**現在再生中:**\n```{0}```", "nowplayingField": "次に再生する曲:", diff --git a/langs/KO.json b/langs/KO.json index 0dfd6c6..0429429 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -116,6 +116,7 @@ "buttonShuffle": "셔플", "buttonForward": "앞으로", "buttonRewind": "뒤로", + "buttonLyrics": "가사", "nowplayingDesc": "**현재 재생중인 곡:**\n```{0}```", "nowplayingField": "다음 곡:", diff --git a/langs/RU.json b/langs/RU.json index bbc9cca..7d34277 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -116,7 +116,8 @@ "buttonShuffle": "Перемешать", "buttonForward": "+10сек", "buttonRewind": "Назад", - + "buttonLyrics": "Тексты песен", + "nowplayingDesc": "**Сейчас играет:**\n```{0}```", "nowplayingField": "Следующее:", "nowplayingLink": "Слушать на {0}", diff --git a/langs/UA.json b/langs/UA.json index df3a9e0..9aebf32 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -116,6 +116,7 @@ "buttonShuffle": "Перемішати", "buttonForward": "Вперед", "buttonRewind": "Назад", + "buttonLyrics": "Тексти пісень", "nowplayingDesc": "**Зараз грає:**\n```{0}```", "nowplayingField": "Наступне:", diff --git a/views/controller.py b/views/controller.py index d142b3e..07790d8 100644 --- a/views/controller.py +++ b/views/controller.py @@ -22,14 +22,15 @@ SOFTWARE. """ import discord +import re import voicelink +import addons +import views import function as func from discord.ext import commands from typing import Dict -from . import ButtonOnCooldown - def key(interaction: discord.Interaction): return interaction.user @@ -45,10 +46,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, view: discord.ui.View = None, ephemeral: bool = False) -> None: stay = self.player.settings.get("controller_msg", True) return await func.send( interaction, key, *params, + view=view, delete_after=None if ephemeral or stay else 10, ephemeral=ephemeral ) @@ -362,6 +364,29 @@ class Rewind(ControlButton): await self.player.seek(position) await self.send(interaction, 'rewind', func.time(position)) +class Lyrics(ControlButton): + def __init__(self, **kwargs): + super().__init__( + emoji="📜", + label="buttonLyrics", + disabled=kwargs["player"].current is None, + **kwargs + ) + + async def callback(self, interaction: discord.Interaction): + if not self.player or not self.player.is_playing: + return await self.send(interaction, "noTrackPlaying", ephemeral=True) + + title = self.player.current.title + artist = self.player.current.author + + song: dict[str, str] = await addons.lyricsPlatform.get(func.settings.lyrics_platform)().get_lyrics(title, artist) + if not song: + return await self.send(interaction, "lyricsNotFound", ephemeral=True) + + view = views.LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=interaction.user) + view.response = await self.send(interaction, view.build_embed(), view=view, ephemeral=True) + class Tracks(discord.ui.Select): def __init__(self, player, style, row): @@ -435,6 +460,7 @@ BUTTONTYPE: Dict[str, ControlButton] = { "shuffle": Shuffle, "forward": Forward, "rewind": Rewind, + "lyrics": Lyrics, "tracks": Tracks, "effects": Effects } @@ -476,14 +502,14 @@ class InteractiveController(discord.ui.View): if self.player.channel and self.player.is_user_join(interaction.user): retry_after = self.cooldown.update_rate_limit(interaction) if retry_after: - raise ButtonOnCooldown(retry_after) + raise views.ButtonOnCooldown(retry_after) return True else: await func.send(interaction, "notInChannel", interaction.user.mention, self.player.channel.mention, ephemeral=True) return False async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item): - if isinstance(error, ButtonOnCooldown): + if isinstance(error, views.ButtonOnCooldown): sec = int(error.retry_after) await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True) From 4a9c26e326d32efbba763babdf7107d299764fa3 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:31:32 +0800 Subject: [PATCH 50/65] Added createSongRequestChannel translation --- langs/CH.json | 3 ++- langs/DE.json | 2 +- langs/ES.json | 3 ++- langs/JA.json | 3 ++- langs/KO.json | 3 ++- langs/RU.json | 3 ++- langs/UA.json | 3 ++- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/langs/CH.json b/langs/CH.json index 1b7ba66..72cf9a8 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -189,5 +189,6 @@ "invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。", "invalidTimeOrder": "結束時間不能小於或等於開始時間。", - "setStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。" + "setStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。", + "createSongRequestChannel": "一個歌曲請求頻道 ({0}) 已建立!您可以在該頻道中透過歌曲名稱或 URL 開始要求任何歌曲,而無需使用機器人前綴。" } \ No newline at end of file diff --git a/langs/DE.json b/langs/DE.json index aa92b94..ac6c022 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -191,4 +191,4 @@ "setStageAnnounceTemplate": "Fertig! Ab sofort wird der Sprachstatus wie der, in dem Du Dich gerade befindest, gemäß Deiner Vorlage benannt. Du solltest in wenigen Sekunden eine Aktualisierung sehen.", "createSongRequestChannel": "Der Song Request Channel ({0}) wurde erstellt! Du kannst jeden Song nach Namen oder URL in diesem Kanal anfordern, ohne den Bot-Präfix verwenden zu müssen." -} +} \ No newline at end of file diff --git a/langs/ES.json b/langs/ES.json index c0185a9..63fb603 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -189,5 +189,6 @@ "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.", + "createSongRequestChannel": "¡Se ha creado un canal de solicitudes de canciones ({0})! Puedes empezar a solicitar cualquier canción por nombre o URL en ese canal, sin necesidad de usar el prefijo del bot." } \ No newline at end of file diff --git a/langs/JA.json b/langs/JA.json index 10abde1..24a0e19 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -189,5 +189,6 @@ "invalidEndTime": "無効な終了時間!時間は `00:00` と `{0}` の間に設定する必要があります。", "invalidTimeOrder": "終了時間は開始時間より大きくない必要があります。", - "setStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。" + "setStageAnnounceTemplate": "完了!これからは、今いるボイスステータスがあなたのテンプレートに従って名前が付けられます。数秒以内に更新されるのを見ることができるはずです。", + "createSongRequestChannel": "曲のリクエストチャンネル ({0}) が作成されました!そのチャンネルで、曲名または URL を使用して任意の曲をリクエストできます。ボットのプレフィックスは必要ありません。" } \ No newline at end of file diff --git a/langs/KO.json b/langs/KO.json index 0429429..30192f0 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -189,5 +189,6 @@ "invalidEndTime": "효력 없는 종료 시간! 시간은 `00:00` 과 `{0}` 사이에 설정해야 합니다.", "invalidTimeOrder": "종료 시간은 시작 시간보다 클수 있어야 합니다.", - "setStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다." + "setStageAnnounceTemplate": "완료! 이제부터 지금 있는 음성 상태는 귀하의 템플릿에 따라 이름이 지정됩니다. 몇 초 후에 업데이트되는 것을 볼 수 있을 것입니다.", + "createSongRequestChannel": "노래 요청 채널 ({0})이 생성되었습니다! 해당 채널에서 노래 제목이나 URL로 원하는 노래를 요청할 수 있으며, 봇 접두사를 사용할 필요가 없습니다." } \ No newline at end of file diff --git a/langs/RU.json b/langs/RU.json index 7d34277..ca703d3 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -188,5 +188,6 @@ "invalidEndTime": "Невозможное время конца! Вход времени должен быть внутри `00:00` и `{0}`.", "invalidTimeOrder": "Время конца не может быть меньше или равно времени начала.", - "setStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд." + "setStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд.", + "createSongRequestChannel": "Канал для запроса песен ({0}) создан! Вы можете начать запрашивать любую песню по названию или URL в этом канале без использования префикса бота." } \ No newline at end of file diff --git a/langs/UA.json b/langs/UA.json index 9aebf32..cd96d18 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -188,5 +188,6 @@ "invalidEndTime": "Недійснений час закінчення! Час має бути в межах `00:00` та `{0}`.", "invalidTimeOrder": "Час закінчення не може бути меншим або рівним часу початку.", - "setStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд." + "setStageAnnounceTemplate": "Готово! Відтепер статус голосу, як той, в якому ви зараз перебуваєте, буде називатися відповідно до вашого шаблону. Ви повинні побачити оновлення через кілька секунд.", + "createSongRequestChannel": "Канал для запитів пісень ({0}) створено! Ви можете почати запитувати будь-яку пісню за назвою або URL у цьому каналі без необхідності використовувати префікс бота." } \ No newline at end of file From fbe619622889d3c35023069fbc821bf07d720b62 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:31:37 +0800 Subject: [PATCH 51/65] Update lyrics.py --- addons/lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/lyrics.py b/addons/lyrics.py index ca1583b..06cf0f0 100644 --- a/addons/lyrics.py +++ b/addons/lyrics.py @@ -232,7 +232,7 @@ class Lrclib(LyricsPlatform): return [] async def get_lyrics(self, title, artist): - params = {"q": f"{title} - {artist}"} + params = {"q": title} result = await self.get(LRCLIB_ENDPOINT + "search", params) if result: return {"default": result[0].get("plainLyrics", "")} From 0ab2438abea90b8dcae456230a9fae540ac8daa0 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:24:52 +0800 Subject: [PATCH 52/65] Fixed typo --- cogs/playlist.py | 16 ++++++++-------- langs/CH.json | 2 +- langs/DE.json | 2 +- langs/EN.json | 2 +- langs/ES.json | 2 +- langs/JA.json | 2 +- langs/KO.json | 2 +- langs/RU.json | 2 +- langs/UA.json | 2 +- views/debug.py | 10 +++++----- views/help.py | 14 +++++++------- views/inbox.py | 4 ++-- voicelink/filters.py | 2 +- voicelink/objects.py | 17 +++++++++-------- 14 files changed, 40 insertions(+), 39 deletions(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index bec4135..a7f3f6b 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -136,11 +136,11 @@ class Playlists(commands.Cog, name="playlist"): if not result['playlist']['tracks']: return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True) - playtrack = [] + _tracks = [] for track in result['playlist']['tracks'][:max_t]: - playtrack.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author)) + _tracks.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author)) - tracks = {"name": result['playlist']['name'], "tracks": playtrack} + tracks = {"name": result['playlist']['name'], "tracks": _tracks} if not tracks: return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True) @@ -237,7 +237,7 @@ class Playlists(commands.Cog, name="playlist"): if link: tracks = await voicelink.NodePool.get_node().get_tracks(link, requester=ctx.author) if not isinstance(tracks, voicelink.Playlist): - return await send(ctx, "playlistNotInvaildUrl", ephemeral=True) + return await send(ctx, "playlistNotInvalidUrl", ephemeral=True) data = {'uri': link, 'perms': {'read': []}, 'name': name, 'type': 'link'} if link else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}}) @@ -351,7 +351,7 @@ class Playlists(commands.Cog, name="playlist"): return update_data, dId = {}, {dId for dId in user["playlist"]} - for data in view.newplaylist[:(max_p - len(user['playlist']))]: + for data in view.new_playlist[:(max_p - len(user['playlist']))]: addId = assign_playlist_id(dId) await update_user(data['sender'], {"$push": {f"playlist.{data['referId']}.perms.read": ctx.author.id}}) update_data[f'playlist.{addId}'] = { @@ -449,11 +449,11 @@ class Playlists(commands.Cog, name="playlist"): if not result['playlist']['tracks']: return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True) - playtrack = [] + _tracks = [] for track in result['playlist']['tracks']: - playtrack.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author)) + _tracks.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author)) - tracks = {"name": result['playlist']['name'], "tracks": playtrack} + tracks = {"name": result['playlist']['name'], "tracks": _tracks} if not tracks: return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True) diff --git a/langs/CH.json b/langs/CH.json index 72cf9a8..0d21c97 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0} 沒有建立播放清單帳戶。", "overPlaylistCreation": "你不能建立超過 `{0}` 個播放清單!", "playlistExists": "播放清單 [`{0}`] 已存在。", - "playlistNotInvaildUrl": "請輸入有效的連結或公開的 Spotify 或 YouTube 播放清單連結。", + "playlistNotInvalidUrl": "請輸入有效的連結或公開的 Spotify 或 YouTube 播放清單連結。", "playlistCreated": "你已建立 `{0}` 播放清單。輸入 /playlist view 檢視更多資訊。", "playlistRenamed": "你已將 `{0}` 更名為 `{1}`。", "playlistLimitTrack": "你已達到限制!你只能將 `{0}` 首歌曲添加至你的播放清單中。", diff --git a/langs/DE.json b/langs/DE.json index ac6c022..155b281 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0} hat noch kein Playlist-Konto erstellt.", "overPlaylistCreation": "Du kannst nicht mehr als `{0}` Wiedergabelisten erstellen!", "playlistExists": "Die Playlist [`{0}`] existiert bereits.", - "playlistNotInvaildUrl": "Bitte gebe einen gültigen Link oder öffentlichen Spotify/YouTube-Playlist-Link ein.", + "playlistNotInvalidUrl": "Bitte gebe einen gültigen Link oder öffentlichen Spotify/YouTube-Playlist-Link ein.", "playlistCreated": "Du hast die Wiedergabeliste `{0}` erstellt. Gebe /playlist view ein, um weitere Informationen zu erhalten.", "playlistRenamed": "Du hast `{0}` zu `{1}` umbenannt.", "playlistLimitTrack": "Du hast die Grenze erreicht! Du kannst deiner Playlist nur noch `{0}` Songs hinzufügen.", diff --git a/langs/EN.json b/langs/EN.json index 271e270..336b108 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0} didn't create a playlist account.", "overPlaylistCreation": "You cannot create more than `{0}` playlists!", "playlistExists": "Playlist [`{0}`] already exists.", - "playlistNotInvaildUrl": "Please enter a valid link or public spotify or youtube playlist link.", + "playlistNotInvalidUrl": "Please enter a valid link or public spotify or youtube playlist link.", "playlistCreated": "You have created `{0}` playlist. Type /playlist view for more info.", "playlistRenamed": "You have renamed `{0}` to `{1}`.", "playlistLimitTrack": "You have reached the limit! You can only add `{0}` songs to your playlist.", diff --git a/langs/ES.json b/langs/ES.json index 63fb603..3ee7877 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "`{0}` no ha creado una cuenta de lista de reproducción.", "overPlaylistCreation": "¡No puede crear más de `{0}` listas de reproducción!", "playlistExists": "La lista de reproducción [`{0}`] ya existe.", - "playlistNotInvaildUrl": "Ingrese un enlace válido o un enlace público de lista de reproducción de Spotify o YouTube.", + "playlistNotInvalidUrl": "Ingrese un enlace válido o un enlace público de lista de reproducción de Spotify o YouTube.", "playlistCreated": "Ha creado una lista de reproducción llamada `{0}`. Escriba /playlist view para obtener más información.", "playlistRenamed": "Ha cambiado el nombre de `{0}` a `{1}`.", "playlistLimitTrack": "¡Ha alcanzado el límite! Solo puede agregar `{0}` canciones a su lista de reproducción.", diff --git a/langs/JA.json b/langs/JA.json index 24a0e19..b8d9ebd 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0}さんはプレイリストアカウントを作成していません。", "overPlaylistCreation": " {0}個以上のプレイリストを作成することはできません!", "playlistExists": "プレイリスト[{0}]はすでに存在します。", - "playlistNotInvaildUrl": "有効なリンクまたは公開SpotifyまたはYouTubeプレイリストリンクを入力してください。", + "playlistNotInvalidUrl": "有効なリンクまたは公開SpotifyまたはYouTubeプレイリストリンクを入力してください。", "playlistCreated": "{0}プレイリストを作成しました。詳細については、/playlist viewを入力してください。", "playlistRenamed": "{0}を{1}に名前を変更しました。", "playlistLimitTrack": "この制限に達しました!プレイリストには{0}曲しか追加できません。", diff --git a/langs/KO.json b/langs/KO.json index 30192f0..5582649 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0}님은 재생 목록 계정을 만들지 않았습니다.", "overPlaylistCreation": "더 이상 {0}개 이상의 재생 목록을 만들 수 없습니다!", "playlistExists": "재생 목록 [{0}]이(가) 이미 있습니다.", - "playlistNotInvaildUrl": "유효한 링크 또는 공개 Spotify 또는 YouTube 재생 목록 링크를 입력하세요.", + "playlistNotInvalidUrl": "유효한 링크 또는 공개 Spotify 또는 YouTube 재생 목록 링크를 입력하세요.", "playlistCreated": "{0} 재생 목록을 만들었습니다. 자세한 내용은 /playlist view를 입력하세요.", "playlistRenamed": "{0}을(를) {1}(으)로 이름을 바꿨습니다.", "playlistLimitTrack": "죄송합니다! 재생 목록에 추가할 수 있는 노래는 {0}곡까지입니다.", diff --git a/langs/RU.json b/langs/RU.json index ca703d3..3d352bd 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "Пользователь `{0}` не создал учетную запись плейлиста.", "overPlaylistCreation": "Вы не можете создавать больше `{0}` плейлистов!", "playlistExists": "Плейлист [`{0}`] уже существует.", - "playlistNotInvaildUrl": "Пожалуйста, введите действительную ссылку или публичную плейлист-ссылку на Spotify или YouTube.", + "playlistNotInvalidUrl": "Пожалуйста, введите действительную ссылку или публичную плейлист-ссылку на Spotify или YouTube.", "playlistCreated": "Вы создали плейлист `{0}`. Введите /playlist view для получения дополнительной информации.", "playlistRenamed": "Вы переименовали `{0}` в `{1}`.", "playlistLimitTrack": "Вы достигли лимита! Вы можете добавить только `{0}` треков в свой плейлист.", diff --git a/langs/UA.json b/langs/UA.json index cd96d18..0f3c745 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0} не створив обліковий запис плейлиста.", "overPlaylistCreation": "Ви не можете створювати більше `{0}` плейлистів!", "playlistExists": "Плейлист [`{0}`] вже існує.", - "playlistNotInvaildUrl": "Будь ласка, введіть дійсне посилання або публічне плейлист-посилання на Spotify або YouTube.", + "playlistNotInvalidUrl": "Будь ласка, введіть дійсне посилання або публічне плейлист-посилання на Spotify або YouTube.", "playlistCreated": "Ви створили плейлист `{0}`. Введіть /playlist view для отримання додаткової інформації.", "playlistRenamed": "Ви перейменували `{0}` на `{1}`.", "playlistLimitTrack": "Ви досягли ліміту! Ви можете додати тільки `{0}` пісень до свого плейлиста.", diff --git a/views/debug.py b/views/debug.py index 65ddb9c..5e8125e 100644 --- a/views/debug.py +++ b/views/debug.py @@ -32,7 +32,7 @@ import function as func from typing import Optional from discord.ext import commands -class ExceuteModal(discord.ui.Modal): +class ExecuteModal(discord.ui.Modal): def __init__(self, code: str, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.code: str = code @@ -139,7 +139,7 @@ class CogsDropdown(discord.ui.Select): except Exception as e: return await interaction.response.send_message(f"Unable to reload `{selected}`! Reason: {e}", ephemeral=True) - await interaction.response.send_message(f"Reloaded `{selected}` sucessfully!", ephemeral=True) + await interaction.response.send_message(f"Reloaded `{selected}` successfully!", ephemeral=True) class NodesDropdown(discord.ui.Select): def __init__(self, bot: commands.Bot): @@ -177,7 +177,7 @@ class NodesDropdown(discord.ui.Select): await interaction.response.defer() await self.view.message.edit(embed=self.view.build_embed(), view=self.view) -class ExceutePanel(discord.ui.View): +class ExecutePanel(discord.ui.View): def __init__(self, bot, *, timeout = 180): self.bot: commands.Bot = bot @@ -208,7 +208,7 @@ class ExceutePanel(discord.ui.View): await self.message.edit(view=self) async def execute(self, interaction: discord.Interaction): - modal = ExceuteModal(self.code, title="Enter Your Code") + modal = ExecuteModal(self.code, title="Enter Your Code") await interaction.response.send_modal(modal) await modal.wait() @@ -362,7 +362,7 @@ class CogsView(discord.ui.View): class DebugView(discord.ui.View): def __init__(self, bot, *, timeout: float | None = 180): self.bot: commands.Bot = bot - self.panel: ExceutePanel = ExceutePanel(bot) + self.panel: ExecutePanel = ExecutePanel(bot) super().__init__(timeout=timeout) diff --git a/views/help.py b/views/help.py index 6402e7a..fcb4005 100644 --- a/views/help.py +++ b/views/help.py @@ -27,7 +27,7 @@ from discord.ext import commands import function as func class HelpDropdown(discord.ui.Select): - def __init__(self, categorys:list): + def __init__(self, categories:list): self.view: HelpView super().__init__( @@ -38,7 +38,7 @@ class HelpDropdown(discord.ui.Select): discord.SelectOption(emoji="🕹️", label="Tutorial", description="How to use Vocard."), ] + [ discord.SelectOption(emoji=emoji, label=f"{category} Commands", description=f"This is {category.lower()} Category.") - for category, emoji in zip(categorys, ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"]) + for category, emoji in zip(categories, ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"]) ], custom_id="select" ) @@ -54,13 +54,13 @@ class HelpView(discord.ui.View): self.author: discord.Member = author self.bot: commands.Bot = bot self.response: discord.Message = None - self.categorys: list[str] = [ name.capitalize() for name, cog in bot.cogs.items() if len([c for c in cog.walk_commands()]) ] + self.categories: list[str] = [ name.capitalize() for name, cog in bot.cogs.items() if len([c for c in cog.walk_commands()]) ] self.add_item(discord.ui.Button(label='Website', emoji='🌎', url='https://vocard.xyz')) self.add_item(discord.ui.Button(label='Document', emoji=':support:915152950471581696', url='https://docs.vocard.xyz')) self.add_item(discord.ui.Button(label='Github', emoji=':github:1098265017268322406', url='https://github.com/ChocoMeow/Vocard')) self.add_item(discord.ui.Button(label='Donate', emoji=':patreon:913397909024800878', url='https://www.patreon.com/Vocard')) - self.add_item(HelpDropdown(self.categorys)) + self.add_item(HelpDropdown(self.categories)) async def on_error(self, error, item, interaction) -> None: return @@ -82,8 +82,8 @@ class HelpView(discord.ui.View): if category == "news": embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=func.settings.embed_color) embed.add_field( - name=f"Available Categories: [{2 + len(self.categorys)}]", - value="```py\n👉 News\n2. Tutorial\n{}```".format("".join(f"{i}. {c}\n" for i, c in enumerate(self.categorys, start=3))), + name=f"Available Categories: [{2 + len(self.categories)}]", + value="```py\n👉 News\n2. Tutorial\n{}```".format("".join(f"{i}. {c}\n" for i, c in enumerate(self.categories, start=3))), inline=True ) @@ -94,7 +94,7 @@ class HelpView(discord.ui.View): return embed embed = discord.Embed(title=f"Category: {category.capitalize()}", color=func.settings.embed_color) - embed.add_field(name=f"Categories: [{2 + len(self.categorys)}]", value="```py\n" + "\n".join(("👉 " if c == category.capitalize() else f"{i}. ") + c for i, c in enumerate(['News', 'Tutorial'] + self.categorys, start=1)) + "```", inline=True) + embed.add_field(name=f"Categories: [{2 + len(self.categories)}]", value="```py\n" + "\n".join(("👉 " if c == category.capitalize() else f"{i}. ") + c for i, c in enumerate(['News', 'Tutorial'] + self.categories, start=1)) + "```", inline=True) if category == 'tutorial': embed.description = "How can use Vocard? Some simple commands you should know now after watching this video." diff --git a/views/inbox.py b/views/inbox.py index dc7d00d..7524b7b 100644 --- a/views/inbox.py +++ b/views/inbox.py @@ -44,7 +44,7 @@ class InboxView(discord.ui.View): def __init__(self, author: discord.Member, inbox: list[dict[str, Any]]): super().__init__(timeout=60) self.inbox: list[dict[str, Any]] = inbox - self.newplaylist = [] + self.new_playlist = [] self.author: discord.Member = author self.response: discord.Message = None @@ -88,7 +88,7 @@ class InboxView(discord.ui.View): @discord.ui.button(label='Accept', style=discord.ButtonStyle.green, custom_id="accept", disabled=True) async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button): - self.newplaylist.append(self.current) + self.new_playlist.append(self.current) self.inbox.remove(self.current) self.current = None await self.button_change(interaction) diff --git a/voicelink/filters.py b/voicelink/filters.py index f20d7d1..74b1568 100644 --- a/voicelink/filters.py +++ b/voicelink/filters.py @@ -287,7 +287,7 @@ class Vibrato(Filter): self._init_with_scope({ "frequency": [0, 14], "depth": [0, 1] - }, tag=tag, frequenc=frequency, depth=depth) + }, tag=tag, frequency=frequency, depth=depth) def __repr__(self): return f" str: return f" length={self.length}>" - def toDict(self) -> dict: - return { - "track_id": self.track_id, - "info": self.info, - "thumbnail": self.thumbnail - } - @property def track_id(self) -> str: if not self._track_id: @@ -135,6 +128,14 @@ class Track: def formatted_length(self) -> str: return ctime(self.length) + @property + def data(self) -> dict: + return { + "track_id": self.track_id, + "info": self.info, + "thumbnail": self.thumbnail + } + class Playlist: """The base playlist object. Returns critical playlist information needed for parsing by Lavalink. From 3a3675eb59a045d102fdc7367e711835f95ba0f9 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:20:57 +0800 Subject: [PATCH 53/65] Reconnect the player after restarted --- cogs/listeners.py | 84 +++++++++++++++++++++++++++++++++++++++++++- function.py | 14 ++++++-- ipc/methods.py | 24 +++++-------- main.py | 4 +-- views/debug.py | 32 +++++++++++++++-- voicelink/objects.py | 3 +- voicelink/player.py | 30 +++++++++++++--- 7 files changed, 162 insertions(+), 29 deletions(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index a5c4a4d..33cd117 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -21,9 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import voicelink +import os import asyncio import discord +import voicelink import function as func from discord.ext import commands @@ -36,6 +37,7 @@ class Listeners(commands.Cog): self.voicelink = voicelink.NodePool() bot.loop.create_task(self.start_nodes()) + bot.loop.create_task(self.restore_last_session_players()) async def start_nodes(self) -> None: """Connect and intiate nodes.""" @@ -52,6 +54,86 @@ class Listeners(commands.Cog): except Exception as e: func.logger.error(f'Node {n["identifier"]} is not able to connect! - Reason: {e}') + async def restore_last_session_players(self) -> None: + """Re-establish connections for players from the last session.""" + await self.bot.wait_until_ready() + players = func.open_json(func.LAST_SESSION_FILE_NAME) + if not players: + return + + for data in players: + try: + channel_id = data.get("channel_id") + if not channel_id: + continue + + channel = self.bot.get_channel(channel_id) + if not channel: + continue + + + dj_member = channel.guild.get_member(data.get("dj")) + if not dj_member: + continue + + # Get the guild settings + settings = await func.get_settings(channel.guild.id) + + # Connect to the channel and initialize the player. + player: voicelink.Player = await channel.connect( + cls=voicelink.Player(self.bot, channel, func.TempCtx(dj_member, channel), settings) + ) + + # Restore the queue. + queue_data = data.get("queue", {}) + for track_data in queue_data.get("tracks", []): + track_id = track_data.get("track_id") + if not track_id: + continue + + decoded_track = voicelink.decode(track_id) + requester = channel.guild.get_member(track_data.get("requester_id")) + track = voicelink.Track(track_id=track_id, info=decoded_track, requester=requester) + player.queue._queue.append(track) + + # Restore queue settings. + player.queue._position = queue_data.get("position", 0) - 1 + repeat_mode = queue_data.get("repeat_mode", "OFF") + try: + loop_mode = voicelink.LoopType[repeat_mode] + except KeyError: + loop_mode = voicelink.LoopType.OFF + player.queue._repeat.set_mode(loop_mode) + player.queue._repeat_position = queue_data.get("repeat_position") + + # Restore player settings + player.dj = dj_member + player.settings['autoplay'] = data.get('autoplay', False) + + # Resume playback or invoke the controller based on the player's state. + if not player.is_playing: + await player.do_next() + + if is_paused := data.get("is_paused"): + await player.set_pause(is_paused, self.bot.user) + + if position := data.get("position"): + await player.seek(int(position), self.bot.user) + + await asyncio.sleep(5) + + except Exception as e: + func.logger.error(f"Error encountered while restoring a player for channel ID {channel_id}.", exc_info=e) + + # Delete the last session file if it exists. + try: + file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME) + if os.path.exists(file_path): + os.remove(file_path) + + except Exception as del_error: + func.logger.error("Failed to remove session file: %s", file_path, exc_info=del_error) + @commands.Cog.listener() async def on_voicelink_track_end(self, player: voicelink.Player, track, _): await player.do_next() diff --git a/function.py b/function.py index 2b9d9cd..e8fcae5 100644 --- a/function.py +++ b/function.py @@ -73,6 +73,14 @@ USER_BASE: dict[str, Any] = { } ALLOWED_MENTIONS = discord.AllowedMentions().none() +LAST_SESSION_FILE_NAME = "last-session.json" + +#-------------- Vocard Classes -------------- +class TempCtx(): + def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None: + self.author: discord.Member = author + self.channel: discord.VoiceChannel = channel + self.guild: discord.Guild = channel.guild #-------------- Vocard Functions -------------- def open_json(path: str) -> dict: @@ -85,9 +93,9 @@ def open_json(path: str) -> dict: def update_json(path: str, new_data: dict) -> None: data = open_json(path) if not data: - return - - data.update(new_data) + data = new_data + else: + data.update(new_data) with open(os.path.join(ROOT_DIR, path), "w") as json_file: json.dump(data, json_file, indent=4) diff --git a/ipc/methods.py b/ipc/methods.py index c782184..6d05a4b 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -22,12 +22,6 @@ SCOPES = { "stage_announce_template": str } -class TempCtx(): - def __init__(self, author: Member, channel: VoiceChannel) -> None: - self.author = author - self.channel = channel - self.guild = channel.guild - class SystemMethod: def __init__(self, function: callable, *, credit: int = 1): self.function: callable = function @@ -44,9 +38,9 @@ def require_permission(only_admin: bool = False): def decorator(func) -> callable: async def wrapper(player: Player, member: Member, dict: Dict) -> Optional[Dict]: if only_admin and not member.guild_permissions.manage_guild: - return error_msg("Only the admins may use this funciton!", user_id=member.id) + return error_msg("Only the admins may use this function!", user_id=member.id) if not player.is_privileged(member): - return error_msg("Only the DJ or admins may use this funciton!", user_id=member.id) + return error_msg("Only the DJ or admins may use this function!", user_id=member.id) return await func(player, member, dict) return wrapper return decorator @@ -67,7 +61,7 @@ async def connect_channel(member: Member, bot: commands.Bot) -> Player: channel = member.voice.channel try: settings = await func.get_settings(channel.guild.id) - player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings)) + player: Player = await channel.connect(cls=Player(bot, channel, func.TempCtx(member, channel), settings)) await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]}) return player except: @@ -438,13 +432,13 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: "userId": str(user_id) } - assgined_playlist_id = _assign_playlist_id(list(playlist.keys())) + assigned_playlist_id = _assign_playlist_id(list(playlist.keys())) data = {'uri': playlist_url, 'perms': {'read': []}, 'name': name, 'type': 'link'} if playlist_url else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} - await func.update_user(user_id, {"$set": {f"playlist.{assgined_playlist_id}": data}}) + await func.update_user(user_id, {"$set": {f"playlist.{assigned_playlist_id}": data}}) return { "op": "updatePlaylist", "status": "created", - "playlistId": assgined_playlist_id, + "playlistId": assigned_playlist_id, "msg": f"You have created '{name}' playlist.", "userId": str(user_id), "data": data @@ -579,7 +573,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: if refer_id not in share_playlists: return error_msg("The shared playlist couldn’t be found. It’s possible that the user has already deleted it.", user_id=user_id) - assgined_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys())) + assigned_playlist_id = _assign_playlist_id(list(user.get("playlist", []).keys())) playlist_name = f"Share{time.strftime('%M%S', time.gmtime(int(mail['time'])))}" share_playlist = share_playlists.get(refer_id) share_playlist.update({ @@ -588,7 +582,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: }) await func.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}}) await func.update_user(user_id, {"$set": { - f'playlist.{assgined_playlist_id}': { + f'playlist.{assigned_playlist_id}': { 'user': mail['sender'], 'referId': mail['referId'], 'name': playlist_name, 'type': 'share' @@ -597,7 +591,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict: }}) payload.update({ - "playlistId": assgined_playlist_id, + "playlistId": assigned_playlist_id, "msg": f"You have created '{playlist_name}' playlist.", "data": share_playlist, }) diff --git a/main.py b/main.py index f8e6d08..202db23 100644 --- a/main.py +++ b/main.py @@ -27,13 +27,13 @@ import os import aiohttp import update import logging +import voicelink import function as func from discord.ext import commands from ipc import IPCClient from motor.motor_asyncio import AsyncIOMotorClient from logging.handlers import TimedRotatingFileHandler -from voicelink import VoicelinkException from addons import Settings class Translator(discord.app_commands.Translator): @@ -181,7 +181,7 @@ class Vocard(commands.Bot): embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {func.settings.invite_link}") return await ctx.reply(embed=embed) - elif not issubclass(error.__class__, VoicelinkException): + elif not issubclass(error.__class__, voicelink.VoicelinkException): error = await func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link func.logger.error(f"An unexpected error occurred in the {ctx.command.name} command on the {ctx.guild.name}({ctx.guild.id}).", exc_info=exception) diff --git a/views/debug.py b/views/debug.py index 5e8125e..3fa70ca 100644 --- a/views/debug.py +++ b/views/debug.py @@ -23,6 +23,7 @@ SOFTWARE. import discord import io +import os import contextlib import textwrap import traceback @@ -132,7 +133,7 @@ class CogsDropdown(discord.ui.Select): selected = self.values[0].lower() try: if selected == "all": - for name in self.bot.cogs.keys(): + for name in self.bot.cogs.copy().keys(): await self.bot.reload_extension(f"cogs.{name.lower()}") else: await self.bot.reload_extension(f"cogs.{selected}") @@ -384,4 +385,31 @@ class DebugView(discord.ui.View): async def nodes(self, interaction: discord.Interaction, button: discord.ui.Button): view = NodesPanel(self.bot) await interaction.response.send_message(embed=view.build_embed(), view=view, ephemeral=True) - view.message = await interaction.original_response() \ No newline at end of file + view.message = await interaction.original_response() + + @discord.ui.button(label="Stop-Bot", emoji="🔴") + async def stop(self, interaction: discord.Interaction, button: discord.ui.Button): + for name in self.bot.cogs.copy().keys(): + try: + await self.bot.unload_extension(name) + except: + pass + + player_data = [] + for identifier, node in voicelink.NodePool._nodes.items(): + for guild_id, player in node._players.copy().items(): + if not player.guild.me.voice: + continue + + player_data.append(player.data) + try: + await player.teardown() + except: + pass + + session_file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME) + if os.path.exists(session_file_path): + os.remove(session_file_path) + + func.update_json(func.LAST_SESSION_FILE_NAME, player_data) + await interaction.client.close() \ No newline at end of file diff --git a/voicelink/objects.py b/voicelink/objects.py index d95b2c4..7c8bdcf 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -132,8 +132,7 @@ class Track: def data(self) -> dict: return { "track_id": self.track_id, - "info": self.info, - "thumbnail": self.thumbnail + "requester_id": self.requester.id } class Playlist: diff --git a/voicelink/player.py b/voicelink/player.py index b17ff76..4480b79 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -224,6 +224,27 @@ class Player(VoiceProtocol): """Calculates and returns the player's current ping in seconds.""" return round(self._ping / 1000, 2) + @property + def autoplay(self) -> bool: + return self.settings.get("autoplay", False) + + @property + def data(self) -> dict: + return { + "guild_id": self._guild.id, + "channel_id": self.channel.id, + "queue": { + "tracks": [track.data for track in self.queue._queue], + "position": self.queue._position, + "repeat_mode": self.queue._repeat.current.name, + "repeat_position": self.queue._repeat_position + }, + "dj": self.dj.id, + "is_paused": self.is_paused, + "position": self.position, + "autoplay": self.autoplay + } + @property def is_ipc_connected(self) -> bool: """Indicates whether the Inter-Process Communication (IPC) connection is active.""" @@ -388,7 +409,7 @@ class Player(VoiceProtocol): track = self.queue.get() if not track: - if self.settings.get("autoplay", False) and await self.get_recommendations(): + if self.autoplay and await self.get_recommendations(): return await self.do_next() else: try: @@ -403,9 +424,7 @@ class Player(VoiceProtocol): "$push": {"history": {"$each": [track.track_id], "$slice": -25}} })) - if self.settings.get('controller', True): - await self.invoke_controller() - + await self.invoke_controller() await self.update_voice_status() if self.is_ipc_connected: @@ -418,6 +437,9 @@ class Player(VoiceProtocol): async def invoke_controller(self): """Sends or updates the music controller message in the designated channel.""" + if not self.settings.get('controller', True): + return + if self._updating or not self.channel: return From ba4f1d349f1eae4e0a7c97b20e8fa654e5132f9f Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:59:36 +0800 Subject: [PATCH 54/65] Prevent rejoin channel that without members --- cogs/listeners.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index 33cd117..13ff320 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -70,8 +70,9 @@ class Listeners(commands.Cog): channel = self.bot.get_channel(channel_id) if not channel: continue - - + elif not any(False if member.bot or member.voice.self_deaf else True for member in channel.members): + continue + dj_member = channel.guild.get_member(data.get("dj")) if not dj_member: continue From 8f0a515e9f61d2ff69dfacb65e25f0ebc24690d8 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 11 Mar 2025 09:51:33 +0800 Subject: [PATCH 55/65] Update debug.py --- views/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/debug.py b/views/debug.py index 3fa70ca..8ec1e80 100644 --- a/views/debug.py +++ b/views/debug.py @@ -398,7 +398,7 @@ class DebugView(discord.ui.View): player_data = [] for identifier, node in voicelink.NodePool._nodes.items(): for guild_id, player in node._players.copy().items(): - if not player.guild.me.voice: + if not player.guild.me.voice or not player.current: continue player_data.append(player.data) From 3a1440c875e52c13c0e6d23b1c6fb7059b167f8e Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 16 Mar 2025 11:07:12 +0800 Subject: [PATCH 56/65] Update listeners.py --- cogs/listeners.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index 13ff320..90d0e90 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -41,7 +41,6 @@ class Listeners(commands.Cog): async def start_nodes(self) -> None: """Connect and intiate nodes.""" - await self.bot.wait_until_ready() for n in func.settings.nodes.values(): try: await self.voicelink.create_node( From 5498c604661a20742e0371b80b87a94a78951efa Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 22 Mar 2025 18:42:28 +0800 Subject: [PATCH 57/65] Replaced internal spotify with lavalink --- addons/settings.py | 2 - cogs/basic.py | 4 +- cogs/listeners.py | 4 +- ipc/methods.py | 47 +-------- settings Example.json | 2 - views/debug.py | 2 - voicelink/enums.py | 2 - voicelink/exceptions.py | 20 ---- voicelink/objects.py | 54 ++-------- voicelink/player.py | 15 +-- voicelink/pool.py | 134 +++--------------------- voicelink/spotify/__init__.py | 26 ----- voicelink/spotify/client.py | 175 -------------------------------- voicelink/spotify/exceptions.py | 8 -- voicelink/spotify/objects.py | 135 ------------------------ 15 files changed, 29 insertions(+), 601 deletions(-) delete mode 100644 voicelink/spotify/__init__.py delete mode 100644 voicelink/spotify/client.py delete mode 100644 voicelink/spotify/exceptions.py delete mode 100644 voicelink/spotify/objects.py diff --git a/addons/settings.py b/addons/settings.py index c5b8c5c..5e8e4a9 100644 --- a/addons/settings.py +++ b/addons/settings.py @@ -32,8 +32,6 @@ class Settings: def __init__(self, settings: Dict) -> None: self.token: str = settings.get("token") self.client_id: int = int(settings.get("client_id", 0)) - self.spotify_client_id: str = settings.get("spotify_client_id") - self.spotify_client_secret: str = settings.get("spotify_client_secret") self.genius_token: str = settings.get("genius_token") self.mongodb_url: str = settings.get("mongodb_url") self.mongodb_name: str = settings.get("mongodb_name") diff --git a/cogs/basic.py b/cogs/basic.py index 49740cd..90d1eb9 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -90,9 +90,9 @@ class Basic(commands.Cog): if current: node = voicelink.NodePool.get_node() - if node and node.spotify_client: + if node: try: - tracks: list[voicelink.Track] = await node.spotifySearch(current, requester=interaction.user) + tracks: list[voicelink.Track] = await node.get_tracks(current, requester=interaction.user, search_type=SearchType.SPOTIFY) 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 [] diff --git a/cogs/listeners.py b/cogs/listeners.py index 90d0e90..f8a5583 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -44,9 +44,7 @@ class Listeners(commands.Cog): for n in func.settings.nodes.values(): try: await self.voicelink.create_node( - bot=self.bot, - spotify_client_id=func.settings.spotify_client_id, - spotify_client_secret=func.settings.spotify_client_secret, + bot=self.bot, logger=func.logger, **n ) diff --git a/ipc/methods.py b/ipc/methods.py index 6d05a4b..1771ead 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -697,49 +697,6 @@ async def updateSettings(bot: commands.Bot, data: Dict) -> None: await func.update_settings(guild.id, {"$set": data}) -async def getFeaturedPlaylists(bot: commands.Bot, data: Dict) -> Dict: - locale = data.get("locale", "sv_SE") - limit = data.get("limit", 20) - offset = data.get("offset", 0) - - request_url = f"https://api.spotify.com/v1/browse/featured-playlists?locale={locale}&limit={max(1, min(limit, 50))}&offset={max(0, offset)}" - - node = NodePool.get_node() - result = await node.spotify_client.get_request(request_url) - - return { - "op": "getFeaturedPlaylists", - "userId": data.get("userId"), - "callback": data.get("callback"), - "playlists": [ - { - "id": item.get("id"), - "title": item.get("name"), - "description": item.get("description"), - "imageUrl": item.get("images", [{}])[0].get("url"), - "href": item.get("external_urls", {}).get("spotify") - } - for item in result.get("playlists", {}).get("items", []) - ] - } - -async def getCategoryPlaylists(bot: commands.Bot, data: Dict) -> Dict: - node = NodePool.get_node() - - return { - "op": "getCategoryPlaylists", - "userId": data.get("userId"), - "callback": data.get("callback"), - "playlists": [ - { - "id": category.id, - "title": category.name, - "imageUrl": category.icon, - } - for category in await node.spotify_client.get_categories() - ] - } - METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = { "initBot": SystemMethod(initBot, credit=0), "initUser": SystemMethod(initUser, credit=2), @@ -766,9 +723,7 @@ METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = { "updatePosition": PlayerMethod(updatePosition), "toggleAutoplay": PlayerMethod(toggleAutoplay), "updateFilter": PlayerMethod(updateFilter), - "searchAndPlay": PlayerMethod(searchAndPlay, credit=5, auto_connect=True), - "getFeaturedPlaylists": SystemMethod(getFeaturedPlaylists, credit=5), - "getCategoryPlaylists": SystemMethod(getCategoryPlaylists, credit=2) + "searchAndPlay": PlayerMethod(searchAndPlay, credit=5, auto_connect=True) } async def process_methods(ipc_client, bot: commands.Bot, data: Dict) -> None: diff --git a/settings Example.json b/settings Example.json index 3c195a5..4322aa5 100644 --- a/settings Example.json +++ b/settings Example.json @@ -1,8 +1,6 @@ { "token": "YOUR_BOT_TOKEN", "client_id": "YOUR_BOT_CLIENT_ID", - "spotify_client_id": "YOUR_SPOTIFY_CLIENT_ID", - "spotify_client_secret": "YOUR_SPOTIFY_CLIENT_SECRET", "genius_token": "YOUR_GENIUS_TOKEN", "mongodb_url": "YOUR_MONGODB_URL", "mongodb_name": "YOUR_MONGODB_DB_NAME", diff --git a/views/debug.py b/views/debug.py index 8ec1e80..0b4c2dd 100644 --- a/views/debug.py +++ b/views/debug.py @@ -105,8 +105,6 @@ class AddNodeModal(discord.ui.Modal): try: await voicelink.NodePool.create_node( bot=interaction.client, - spotify_client_id=func.settings.spotify_client_id, - spotify_client_secret=func.settings.spotify_client_secret, logger=func.logger, **config ) diff --git a/voicelink/enums.py b/voicelink/enums.py index 58a0bc8..ef74c58 100644 --- a/voicelink/enums.py +++ b/voicelink/enums.py @@ -38,8 +38,6 @@ class LoopType(Enum): 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.YOUTUBE searches using regular Youtube, which is best for all scenarios. diff --git a/voicelink/exceptions.py b/voicelink/exceptions.py index 0c7fca8..59c0570 100644 --- a/voicelink/exceptions.py +++ b/voicelink/exceptions.py @@ -74,26 +74,6 @@ class FilterTagInvalid(VoicelinkException): """An invalid tag was passed or Voicelink was unable to find a filter tag""" pass -class SpotifyAlbumLoadFailed(VoicelinkException): - """The voicelink Spotify client was unable to load an album.""" - pass - - -class SpotifyTrackLoadFailed(VoicelinkException): - """The voicelink Spotify client was unable to load a track.""" - pass - - -class SpotifyPlaylistLoadFailed(VoicelinkException): - """The voicelink Spotify client was unable to load a playlist.""" - pass - - -class InvalidSpotifyClientAuthorization(VoicelinkException): - """No Spotify client authorization was provided for track searching.""" - pass - - class QueueFull(VoicelinkException): pass diff --git a/voicelink/objects.py b/voicelink/objects.py index 7c8bdcf..3f9ad78 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -33,7 +33,6 @@ from function import ( time as ctime ) -from .spotify import Playlist as spPlaylist from .formatter import encode YOUTUBE_REGEX = re.compile(r'(https?://)?(www\.)?youtube\.(com|nl)/watch\?v=([-\w]+)') @@ -51,11 +50,9 @@ class Track: "author", "uri", "source", - "spotify", "artist_id", "original", "_search_type", - "spotify_track", "thumbnail", "emoji", "length", @@ -73,7 +70,6 @@ class Track: info: dict, requester: Member, search_type: SearchType = SearchType.YOUTUBE, - spotify_track = None, ): self._track_id: Optional[str] = track_id self.info: dict = info @@ -83,13 +79,7 @@ class Track: self.author: str = info.get("author", "Unknown") self.uri: str = info.get("uri", "https://discord.com/application-directory/605618911471468554") self.source: str = info.get("sourceName", extract(self.uri).domain) - self.spotify: bool = self.source == "spotify" - if self.spotify: - self.artist_id: Optional[list] = info.get("artist_id") - - self.original: Optional[Track] = None if self.spotify else self - self._search_type: SearchType = SearchType.YOUTUBE if self.spotify else search_type - self.spotify_track: Track = spotify_track + self._search_type: SearchType = search_type self.thumbnail: str = info.get("artworkUrl") if not self.thumbnail and YOUTUBE_REGEX.match(self.uri): @@ -143,12 +133,9 @@ class Playlist: __slots__ = ( "playlist_info", - "tracks_raw", - "spotify", "name", - "spotify_playlist", - "_thumbnail", - "_uri", + "thumbnail", + "uri", "tracks" ) @@ -158,29 +145,16 @@ class Playlist: playlist_info: dict, tracks: list, requester: Member = None, - spotify: bool = False, - spotify_playlist: Optional[spPlaylist] = None ): self.playlist_info: dict = playlist_info - self.tracks_raw: list[Track] = tracks - self.spotify: bool = spotify self.name: str = playlist_info.get("name") - self.spotify_playlist: Optional[spPlaylist] = spotify_playlist - - self._thumbnail: str = None - self._uri: str = None + self.thumbnail: str = None + self.uri: str = None - if self.spotify: - self.tracks = tracks - self._thumbnail = self.spotify_playlist.image - self._uri = self.spotify_playlist.uri - else: - self.tracks = [ - Track(track_id=track["encoded"], info=track["info"], requester=requester) - for track in self.tracks_raw - ] - self._thumbnail = None - self._uri = None + self.tracks = [ + Track(track_id=track["encoded"], info=track["info"], requester=requester) + for track in tracks + ] def __str__(self) -> str: return self.name @@ -188,16 +162,6 @@ class Playlist: def __repr__(self) -> str: return f"" - @property - def uri(self) -> Optional[str]: - """Spotify album/playlist URI, or None if not a Spotify object.""" - return self._uri - - @property - def thumbnail(self) -> Optional[str]: - """Spotify album/playlist thumbnail, or None if not a Spotify object.""" - return self._thumbnail - @property def track_count(self) -> int: return len(self.tracks) diff --git a/voicelink/player.py b/voicelink/player.py index 4480b79..a24c07b 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -524,10 +524,6 @@ class Player(VoiceProtocol): ) -> Union[List[Track], Playlist]: """Fetches tracks from the node's REST api to parse into Lavalink. - If you passed in Spotify API credentials when you created the node, - you can also pass in a Spotify URL of a playlist, album or track and it will be parsed - accordingly. - You can also pass in a discord.py Context object to get a Context object on any track you search. """ @@ -579,19 +575,12 @@ class Player(VoiceProtocol): end: int = 0, ignore_if_playing: bool = False ) -> Track: - """Plays a track. If a Spotify track is passed in, it will be handled accordingly.""" + """Plays a track.""" if not self._node: return track - if track.spotify: - if not track.original: - 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] - data = { - "encodedTrack": track.original.track_id if track.original else track.track_id, + "encodedTrack": track.track_id, "position": str(start if start else track.position) } diff --git a/voicelink/pool.py b/voicelink/pool.py index 593695e..4965289 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -35,13 +35,11 @@ from typing import Dict, Optional, TYPE_CHECKING, Union, List from urllib.parse import quote from . import ( - __version__, - spotify, + __version__ ) from .enums import SearchType, NodeAlgorithm from .exceptions import ( - InvalidSpotifyClientAuthorization, NodeConnectionFailure, NodeCreationError, NodeException, @@ -57,10 +55,6 @@ from .ratelimit import YTRatelimit, YTToken, STRATEGY if TYPE_CHECKING: from .player import Player -SPOTIFY_URL_REGEX = re.compile( - r"https?://open.spotify.com/(?Palbum|playlist|track|artist)/(?P[a-zA-Z0-9]+)" -) - DISCORD_MP3_URL_REGEX = re.compile( r"https?://cdn.discordapp.com/attachments/(?P[0-9]+)/" r"(?P[0-9]+)/(?P[a-zA-Z0-9_.]+)+" @@ -74,8 +68,7 @@ NODE_VERSION = "v4" class Node: """The base class for a node. - This node object represents a Lavalink node. - To enable Spotify searching, pass in a proper Spotify Client ID and Spotify Client Secret + This node object represents a Lavalink node. """ def __init__( @@ -91,8 +84,6 @@ class Node: heartbeat: int = 30, yt_ratelimit: dict = None, session: Optional[aiohttp.ClientSession] = None, - spotify_client_id: Optional[str] = None, - spotify_client_secret: Optional[str] = None, resume_key: Optional[str] = None, logger: Optional[logging.Logger] = None ): @@ -127,10 +118,6 @@ class Node: self._players: Dict[int, Player] = {} self._info: Optional[NodeInfo] = None - self._spotify_client_id: Optional[str] = spotify_client_id - self._spotify_client_secret: Optional[str] = spotify_client_secret - self._spotify_client: Optional[spotify.Client] = None - self.yt_ratelimit: Optional[YTRatelimit] = STRATEGY.get(yt_ratelimit.get("strategy"))(self, yt_ratelimit) if yt_ratelimit else None self._bot.add_listener(self._update_handler, "on_socket_response") @@ -145,15 +132,6 @@ class Node: """Takes a guild ID as a parameter. Returns a voicelink Player object.""" return self._players.get(guild_id, None) - @property - def spotify_client(self) -> Optional[spotify.Client]: - if not self._spotify_client: - self._spotify_client = spotify.Client( - self._spotify_client_id, self._spotify_client_secret - ) - - return self._spotify_client - @property def is_connected(self) -> bool: """"Property which returns whether this node is connected or not""" @@ -384,56 +362,15 @@ class Node: ) -> Union[List[Track], Playlist]: """Fetches tracks from the node's REST api to parse into Lavalink. - If you passed in Spotify API credentials, you can also pass in a - Spotify URL of a playlist, album or track and it will be parsed accordingly. - You can also pass in a discord.py Context object to get a Context object on any track you search. """ if not URL_REGEX.match(query): - if search_type == SearchType.SPOTIFY: - return await self.spotifySearch(query=query, requester=requester) - - else: + if ':' not in query: query = f"{search_type}:{query}" - if SPOTIFY_URL_REGEX.match(query): - try: - 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?") - - if isinstance(spotify_results, spotify.Track): - return [ - Track( - track_id=None, - info=spotify_results.to_dict(), - requester=requester, - search_type=search_type, - spotify_track=spotify_results, - ) - ] - - tracks = [ - Track( - track_id=None, - info=track.to_dict(), - requester=requester, - search_type=search_type, - spotify_track=track, - ) for track in spotify_results.tracks if track.uri - ] - - return Playlist( - playlist_info={"name": spotify_results.name, "selectedTrack": 0}, - tracks=tracks, - requester=requester, - spotify=True, - spotify_playlist=spotify_results - ) - - elif DISCORD_MP3_URL_REGEX.match(query): + if DISCORD_MP3_URL_REGEX.match(query): data = await self.send(RequestMethod.GET, f"loadtracks?identifier={quote(query)}") try: @@ -463,7 +400,7 @@ class Node: elif load_type == "empty": return None - elif load_type == "playlist": + elif load_type in ("playlist", "recommendations"): data = data.get("data") return Playlist( @@ -491,57 +428,18 @@ class Node: requester=requester ) ] - - async def spotifySearch(self, query: str, *, requester: Member) -> Optional[List[Track]]: - 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/" - ) - - tracks = await self._spotify_client.track_search(query=query) - except Exception as _: - raise TrackLoadError("Not able to find the provided Spotify entity, is it private?") - - return [ - Track( - track_id=None, - requester=requester, - search_type=SearchType.YOUTUBE, - spotify_track=track, - info=track.to_dict() - ) - for track in tracks ] async def get_recommendations(self, track: Track, limit: int = 20) -> List[Optional[Track]]: - if track.spotify: - if not self.spotify_client: - return [] - - spotify_tracks = await self.spotify_client.similar_track(seed_tracks=track.identifier, limit=limit) - - tracks = [ - Track( - track_id=None, - search_type=SearchType.YOUTUBE, - spotify_track=track, - info=track.to_dict(), - requester=self.bot.user - ) - for track in spotify_tracks - ] + if track.source == "youtube": + query = f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}" - else: - if track.source != 'youtube': - return [] - - tracks = await self.get_tracks( - f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}", - requester=self.bot.user - ) + elif track.source == "spotify": + query = f"sprec:seed_tracks={track.identifier}" + if not query: + return [] + + tracks = await self.get_tracks(query=query, requester=self.bot.user) if isinstance(tracks, Playlist): tracks = tracks.tracks @@ -639,14 +537,11 @@ class NodePool: secure: bool = False, heartbeat: int = 30, yt_ratelimit: dict = None, - spotify_client_id: Optional[str] = None, - spotify_client_secret: Optional[str] = None, session: Optional[aiohttp.ClientSession] = None, resume_key: Optional[str] = None, logger: Optional[logging.Logger] = None ) -> Node: """Creates a Node object to be then added into the node pool. - For Spotify searching capabilites, pass in valid Spotify API credentials. """ if identifier in cls._nodes.keys(): raise NodeCreationError(f"A node with identifier '{identifier}' already exists.") @@ -657,8 +552,7 @@ class NodePool: node = Node( pool=cls, bot=bot, host=host, port=port, password=password, identifier=identifier, secure=secure, heartbeat=heartbeat, yt_ratelimit=yt_ratelimit, - session=session, spotify_client_id=spotify_client_id, spotify_client_secret=spotify_client_secret, - resume_key=resume_key, logger=logger + session=session, resume_key=resume_key, logger=logger ) await node.connect() diff --git a/voicelink/spotify/__init__.py b/voicelink/spotify/__init__.py deleted file mode 100644 index a7a4a10..0000000 --- a/voicelink/spotify/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""MIT License - -Copyright (c) 2022 Vocard Development - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from .exceptions import InvalidSpotifyURL, SpotifyRequestException -from .objects import * -from .client import Client diff --git a/voicelink/spotify/client.py b/voicelink/spotify/client.py deleted file mode 100644 index c247b9c..0000000 --- a/voicelink/spotify/client.py +++ /dev/null @@ -1,175 +0,0 @@ -"""MIT License - -Copyright (c) 2022 Vocard Development - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import re -import time -import aiohttp - -from base64 import b64encode -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}" -SPOTIFY_URL_REGEX = re.compile( - r"https?://open.spotify.com/(?Palbum|playlist|track|artist)/(?P[a-zA-Z0-9]+)" -) - -class Client: - """The base client for the Spotify module of Voicelink. - This class will do all the heavy lifting of getting all the metadata - for any Spotify URL you throw at it. - """ - - def __init__(self, client_id: str, client_secret: str) -> None: - 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: 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: - """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: - url, data = ANONYMOUS_GRANT_URL, None - - 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}" - ) - - 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_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() - - async with self.session.get(url, headers=self._bearer_headers) as resp: - if resp.status != 200: - raise SpotifyRequestException( - f"Error while fetching results: {resp.status} {resp.reason}" - ) - - 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) - 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) - - 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 next_data.get("items", []) if track.get("track") is not None - ]) - next_page_url = next_data.get("next") - - 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" - - 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/exceptions.py b/voicelink/spotify/exceptions.py deleted file mode 100644 index e421fbf..0000000 --- a/voicelink/spotify/exceptions.py +++ /dev/null @@ -1,8 +0,0 @@ -class SpotifyRequestException(Exception): - """An error occurred when making a request to the Spotify API""" - pass - - -class InvalidSpotifyURL(Exception): - """An invalid Spotify URL was passed""" - pass diff --git a/voicelink/spotify/objects.py b/voicelink/spotify/objects.py deleted file mode 100644 index 59a645d..0000000 --- a/voicelink/spotify/objects.py +++ /dev/null @@ -1,135 +0,0 @@ -class Track: - """The base class for a Spotify Track""" - - __slots__ = ( - "name", - "artists", - "artist_id", - "length", - "id", - "image", - "uri" - ) - - def __init__(self, data: dict, image = None) -> None: - self.name: str = data.get('name', 'Unknown') - self.artists: str = ", ".join(filter(None, (artist["name"] for artist in data.get('artists')))) - self.artist_id: list[str] = [artist["id"] for artist in data.get('artists')] - self.length: int = data.get('duration_ms') - self.id: str = data.get('id') - self.image: str = images[0]["url"] if (images := data.get("album", {}).get("images")) else image - self.uri: str = None if data["is_local"] else data["external_urls"]["spotify"] - - def to_dict(self) -> dict: - return { - "title": self.name, - "author": self.artists, - "length": self.length, - "identifier": self.id, - "artist_id": self.artist_id, - "uri": self.uri, - "isStream": False, - "isSeekable": True, - "position": 0, - "artworkUrl": self.image - } - - def __repr__(self) -> str: - return ( - f"" - ) - -class Album: - """The base class for a Spotify album""" - - __slots__ = ( - "name", - "artists", - "image", - "tracks", - "total_tracks", - "id", - "uri" - ) - - def __init__(self, data: dict) -> None: - self.name: str = data.get('name', 'Unknown') - self.artists: str = ", ".join(filter(None, (artist["name"] for artist in data.get('artists')))) - self.image: str = data["images"][0]["url"] - self.tracks: list[Track] = [Track(track, image=self.image) for track in data["tracks"]["items"]] - self.total_tracks: int = data["total_tracks"] - self.id: str = data.get('id') - self.uri: str = data["external_urls"]["spotify"] - - def __repr__(self) -> str: - return ( - f"" - ) - -class Artist: - """The base class for a Spotify playlist""" - - __slots__ = ( - "tracks", - "image", - "total_tracks", - "owner", - "id", - "uri", - "name" - ) - def __init__(self, data: dict) -> None: - self.tracks: list[Track] = [Track(track) for track in data['tracks']] - if self.tracks: - self.image: str = self.tracks[0].image - self.total_tracks: int = len(self.tracks) - self.owner: str = self.tracks[0].artists - self.id: str = self.tracks[0].artist_id - self.uri: str = data['tracks'][0]['album']['artists'][0]['external_urls']['spotify'] - self.name: str = f"Top tracks - {self.owner}" - - def __repr__(self) -> str: - return ( - f"" - ) - -class Playlist: - """The base class for a Spotify playlist""" - - __slots__ = ( - "name", - "tracks", - "owner", - "total_tracks", - "id", - "image", - "uri" - ) - - def __init__(self, data: dict, tracks: list[Track]) -> None: - self.name: str = data.get('name', 'Unknown') - self.tracks: list[Track] = tracks - self.owner: str = data["owner"]["display_name"] - self.total_tracks: int = data["tracks"]["total"] - self.id: str = data.get('id') - self.image: str = data["images"][0]["url"] if len(data.get("images", [])) else None - self.uri: str = data["external_urls"]["spotify"] - - def __repr__(self) -> str: - return ( - f"" - ) - -class Category: - def __init__(self, data: dict) -> None: - self.href: str = data.get("href") - self.id: str = data.get("id") - self.name: str = data.get("name") - self.icon: str = data.get("icons", [{}])[0].get("url") - - def __repr__(self) -> str: - return (f" Date: Sat, 22 Mar 2025 19:22:46 +0800 Subject: [PATCH 58/65] Fixed some bugs --- update.py | 2 +- voicelink/objects.py | 2 -- voicelink/player.py | 10 ++++------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/update.py b/update.py index 5b84a2b..b244bb5 100644 --- a/update.py +++ b/update.py @@ -29,7 +29,7 @@ __version__ = "v2.7.0b4" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" -IGNORE_FILES = ["settings.json", "logs"] +IGNORE_FILES = ["settings.json", "logs", "last-session.json"] class bcolors: WARNING = '\033[93m' diff --git a/voicelink/objects.py b/voicelink/objects.py index 3f9ad78..56fbd46 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -50,8 +50,6 @@ class Track: "author", "uri", "source", - "artist_id", - "original", "_search_type", "thumbnail", "emoji", diff --git a/voicelink/player.py b/voicelink/player.py index a24c07b..e3d0b76 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -151,21 +151,19 @@ class Player(VoiceProtocol): @property def position(self) -> float: """Property which returns the player's position in a track in milliseconds""" - current = self._current.original - if not self.is_playing or not self._current: return 0 if self.is_paused: - return min(self._last_position, current.length) + return min(self._last_position, self._current.length) difference = (time.time() * 1000) - self._last_update position = self._last_position + difference - if position > current.length: + if position > self._current.length: return 0 - return min(position, current.length) + return min(position, self._current.length) @property def is_playing(self) -> bool: @@ -663,7 +661,7 @@ class Player(VoiceProtocol): async def seek(self, position: float, requester: Member = None) -> float: """Seeks to a position in the currently playing track milliseconds""" - if position < 0 or position > self._current.original.length: + if position < 0 or position > self._current.length: raise TrackInvalidPosition("Seek position must be between 0 and the track length") await self.send(method=RequestMethod.PATCH, data={"position": position}) From d310f201bcbe91e5b9284cbb999b14ab2477f325 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:09:34 +0800 Subject: [PATCH 59/65] Updated lavalink application.yml --- lavalink/application.yml | 73 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/lavalink/application.yml b/lavalink/application.yml index 704256d..5969f11 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -14,8 +14,10 @@ plugins: clients: - MUSIC - ANDROID_VR + - ANDROID_MUSIC - WEB - - WEBEMBEDDED + - WEBEMBEDDED + - TVHTML5EMBEDDED # The below section of the config allows setting specific options for each client, such as the requests they will handle. # If an option, or client, is unspecified, then the default option value/client values will be used instead. # If a client is configured, but is not registered above, the options for that client will be ignored. @@ -32,9 +34,76 @@ plugins: # Example: Configuring a client to exclusively be used for video loading and playback. playlistLoading: false # Disables loading of playlists and mixes. searching: false # Disables the ability to search for videos. + lavasrc: + providers: # Custom providers for track loading. This is the default + # - "dzisrc:%ISRC%" # Deezer ISRC provider + # - "dzsearch:%QUERY%" # Deezer search provider + - "ytsearch:\"%ISRC%\"" # Will be ignored if track does not have an ISRC. See https://en.wikipedia.org/wiki/International_Standard_Recording_Code + - "ytsearch:%QUERY%" # Will be used if track has no ISRC or no track could be found for the ISRC + # you can add multiple other fallback sources here + sources: + spotify: true # Enable Spotify source + applemusic: false # Enable Apple Music source + deezer: false # Enable Deezer source + yandexmusic: false # Enable Yandex Music source + flowerytts: false # Enable Flowery TTS source + youtube: false # Enable YouTube search source (https://github.com/topi314/LavaSearch) + vkmusic: false # Enable Vk Music source + lyrics-sources: + spotify: false # Enable Spotify lyrics source + deezer: false # Enable Deezer lyrics source + youtube: false # Enable YouTube lyrics source + yandexmusic: false # Enable Yandex Music lyrics source + vkmusic: false # Enable Vk Music lyrics source + spotify: + clientId: "" + clientSecret: "" + # spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api + countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + playlistLoadLimit: 6 # The number of pages at 100 tracks each + albumLoadLimit: 6 # The number of pages at 50 tracks each + resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow) + localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"` + applemusic: + countryCode: "US" # the country code you want to use for filtering the artists top tracks and language. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + mediaAPIToken: "your apple music api token" # apple music api token + # or specify an apple music key + keyID: "your key id" + teamID: "your team id" + musicKitKey: | + -----BEGIN PRIVATE KEY----- + your key + -----END PRIVATE KEY----- + playlistLoadLimit: 6 # The number of pages at 300 tracks each + albumLoadLimit: 6 # The number of pages at 300 tracks each + deezer: + masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else) + # arl: "your deezer arl" # the arl cookie used for accessing the deezer api this is optional but required for formats above MP3_128 + formats: [ "FLAC", "MP3_320", "MP3_256", "MP3_128", "MP3_64", "AAC_64" ] # the formats you want to use for the deezer tracks. "FLAC", "MP3_320", "MP3_256" & "AAC_64" are only available for premium users and require a valid arl + yandexmusic: + accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music + playlistLoadLimit: 1 # The number of pages at 100 tracks each + albumLoadLimit: 1 # The number of pages at 50 tracks each + artistLoadLimit: 1 # The number of pages at 10 tracks each + flowerytts: + voice: "default voice" # (case-sensitive) get default voice from here https://api.flowery.pw/v1/tts/voices + translate: false # whether to translate the text to the native language of voice + silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0. + speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.) + audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3 + youtube: + countryCode: "US" # the country code you want to use for searching lyrics via ISRC. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + vkmusic: + userToken: "your user token" # This token is needed for authorization in the api. Guide: https://github.com/topi314/LavaSrc#vk-music + playlistLoadLimit: 1 # The number of pages at 50 tracks each + artistLoadLimit: 1 # The number of pages at 10 tracks each + recommendationsLoadLimit: 10 # Number of tracks lavalink: plugins: - - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.4" + - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.5" + snapshot: false + - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:06b7cab" + snapshot: true # - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin # repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default # snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository From c3f84305ee41ad1734c43522c7c933f230990b3a Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:10:07 +0800 Subject: [PATCH 60/65] Update .gitignore --- .gitignore | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 51c14c6..ceb9c91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,179 @@ -Vocard.rar +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +**/.DS_Store + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -*.pyc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Custom file settings.json logs \ No newline at end of file From 8007555759d7fce74fa4b890f33c5c0df22162af Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:11:04 +0800 Subject: [PATCH 61/65] Rewrite get_track function --- voicelink/pool.py | 76 +++++++++++------------------------------------ 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/voicelink/pool.py b/voicelink/pool.py index 4965289..91a0f2a 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -31,7 +31,7 @@ import logging from discord import Client, Member from discord.ext.commands import Bot -from typing import Dict, Optional, TYPE_CHECKING, Union, List +from typing import Dict, Optional, Union, List, Any, TYPE_CHECKING from urllib.parse import quote from . import ( @@ -55,11 +55,6 @@ from .ratelimit import YTRatelimit, YTToken, STRATEGY if TYPE_CHECKING: from .player import Player -DISCORD_MP3_URL_REGEX = re.compile( - r"https?://cdn.discordapp.com/attachments/(?P[0-9]+)/" - r"(?P[0-9]+)/(?P[a-zA-Z0-9_.]+)+" -) - URL_REGEX = re.compile( r"https?://(?:www\.)?.+" ) @@ -360,74 +355,37 @@ class Node: requester: Member, search_type: SearchType = SearchType.YOUTUBE ) -> Union[List[Track], Playlist]: - """Fetches tracks from the node's REST api to parse into Lavalink. + """ + Fetches tracks from the node's REST api to parse into Lavalink. - You can also pass in a discord.py Context object to get a - Context object on any track you search. + You can also pass in a discord.py Context object to get a + Context object on any track you search. """ - if not URL_REGEX.match(query): - if ':' not in query: - query = f"{search_type}:{query}" + if not URL_REGEX.match(query) and ':' not in query: + query = f"{search_type}:{query}" - if DISCORD_MP3_URL_REGEX.match(query): - data = await self.send(RequestMethod.GET, f"loadtracks?identifier={quote(query)}") - - try: - track: dict = data["data"] - except: - raise TrackLoadError("Not able to find the provided track.") - - return [ - Track( - track_id=track["encoded"], - info=track["info"], - requester=requester - ) - ] - else: - data = await self.send(RequestMethod.GET, f"loadtracks?identifier={quote(query)}") - - load_type = data.get("loadType") + response: dict[str, Any] = await self.send(RequestMethod.GET, f"loadtracks?identifier={quote(query)}") + data = response.get("data") + load_type = response.get("loadType") if not load_type: raise TrackLoadError("There was an error while trying to load this track.") - - elif load_type == "error": - exception = data["data"] - raise TrackLoadError(f"{exception['message']} [{exception['severity']}]") - + elif load_type == "empty": return None + elif load_type == "error": + raise TrackLoadError(f"{data['message']} [{data['severity']}]") + elif load_type in ("playlist", "recommendations"): - data = data.get("data") - - return Playlist( - playlist_info=data["info"], - tracks=data["tracks"], - requester=requester - ) + return Playlist(playlist_info=data["info"], tracks=data["tracks"], requester=requester) elif load_type == "search": - return [ - Track( - track_id=track["encoded"], - info=track["info"], - requester=requester - ) - for track in data["data"] - ] + return [Track(track_id=track["encoded"], info=track["info"], requester=requester) for track in data] elif load_type == "track": - track = data["data"] - return [ - Track( - track_id=track["encoded"], - info=track["info"], - requester=requester - ) - ] + return [Track(track_id=data["encoded"], info=data["info"], requester=requester)] async def get_recommendations(self, track: Track, limit: int = 20) -> List[Optional[Track]]: if track.source == "youtube": From 7c14bb56a7aea8a8edea8803cde14079f4bcc7c0 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:20:43 +0800 Subject: [PATCH 62/65] Rewrite play_autocomplete function --- cogs/basic.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 90d1eb9..b025974 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -86,24 +86,18 @@ class Basic(commands.Cog): return [app_commands.Choice(name=c.capitalize(), value=c) for c in self.bot.cogs if c not in ["Nodes", "Task"] and current in c] async def play_autocomplete(self, interaction: discord.Interaction, current: str) -> list: - if voicelink.pool.URL_REGEX.match(current): return [] + if voicelink.pool.URL_REGEX.match(current): + return [] if current: node = voicelink.NodePool.get_node() - if node: - try: - tracks: list[voicelink.Track] = await node.get_tracks(current, requester=interaction.user, search_type=SearchType.SPOTIFY) - 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 [] + if not node: + return [] + tracks: list[voicelink.Track] = await node.get_tracks(current, requester=interaction.user, search_type=SearchType.SPOTIFY) + 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] if tracks else [] - 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] - return history_tracks + history = {track["identifier"]: track for track_id in reversed(await get_user(interaction.user.id, "history")) if (track := voicelink.decode(track_id))["uri"]} + return [app_commands.Choice(name=truncate_string(f"🕒 {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25] @commands.hybrid_command(name="connect", aliases=get_aliases("connect")) @app_commands.describe(channel="Provide a channel to connect.") From 3699590c0e07b3a74315534ba148b6f26b1fc3e3 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 5 Apr 2025 22:06:10 +0800 Subject: [PATCH 63/65] Fixed some bugs --- addons/__init__.py | 2 +- addons/lyrics.py | 6 +++--- cogs/basic.py | 16 +++++++++------- ipc/methods.py | 28 ++++++++++++++-------------- settings Example.json | 2 +- views/controller.py | 22 ++++++++++++---------- 6 files changed, 40 insertions(+), 36 deletions(-) diff --git a/addons/__init__.py b/addons/__init__.py index a835660..9bcfb4e 100644 --- a/addons/__init__.py +++ b/addons/__init__.py @@ -1,3 +1,3 @@ -from .lyrics import lyricsPlatform +from .lyrics import LYRICS_PLATFORMS from .placeholders import Placeholders from .settings import Settings \ No newline at end of file diff --git a/addons/lyrics.py b/addons/lyrics.py index 06cf0f0..40b8e1d 100644 --- a/addons/lyrics.py +++ b/addons/lyrics.py @@ -28,7 +28,7 @@ from abc import ABC, abstractmethod from urllib.parse import quote from math import floor from importlib import import_module -from typing import Optional +from typing import Optional, Type userAgents = '''Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36 @@ -236,8 +236,8 @@ class Lrclib(LyricsPlatform): result = await self.get(LRCLIB_ENDPOINT + "search", params) if result: return {"default": result[0].get("plainLyrics", "")} - -lyricsPlatform: dict[str, LyricsPlatform] = { + +LYRICS_PLATFORMS: dict[str, Type[LyricsPlatform]] = { "a_zlyrics": A_ZLyrics, "genius": Genius, "lyrist": Lyrist, diff --git a/cogs/basic.py b/cogs/basic.py index b025974..9eb6d36 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -41,7 +41,7 @@ from function import ( ) from voicelink import SearchType, LoopType -from addons import lyricsPlatform +from addons import LYRICS_PLATFORMS from views import SearchView, ListView, LinkView, LyricsView, HelpView from validators import url @@ -792,12 +792,14 @@ class Basic(commands.Cog): artist = player.current.author await ctx.defer() - song: dict[str, str] = await lyricsPlatform.get(settings.lyrics_platform)().get_lyrics(title, artist) - if not song: - 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 send(ctx, view.build_embed(), view=view) + lyrics_platform = LYRICS_PLATFORMS.get(settings.lyrics_platform) + if lyrics_platform: + lyrics = await lyrics_platform().get_lyrics(title, artist) + if not lyrics: + return await send(ctx, "lyricsNotFound", ephemeral=True) + + view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=ctx.author) + view.response = await send(ctx, view.build_embed(), view=view) @commands.hybrid_command(name="swapdj", aliases=get_aliases("swapdj")) @app_commands.describe(member="Choose a member to transfer the dj role.") diff --git a/ipc/methods.py b/ipc/methods.py index 1771ead..200b4ec 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -6,7 +6,7 @@ from typing import List, Dict, Union, Optional from discord import User, Member, VoiceChannel from discord.ext import commands from voicelink import Player, Track, Playlist, NodePool, decode, LoopType, Filters -from addons import lyricsPlatform +from addons import LYRICS_PLATFORMS RATELIMIT_COUNTER: Dict[int, Dict[str, float]] = {} SCOPES = { @@ -653,21 +653,21 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict: async def getLyrics(bot: commands.Bot, data: Dict) -> Dict: title, artist, platform = data.get("title", ""), data.get("artist", ""), data.get("platform", "") - if not platform or platform not in lyricsPlatform: + if not platform or platform not in LYRICS_PLATFORMS: platform = func.settings.lyrics_platform - song: dict[str, str] = await lyricsPlatform.get(platform)().get_lyrics(title, artist) - payload = { - "op": "getLyrics", - "userId": data.get("userId"), - "title": title, - "artist": artist, - "platform": platform, - "lyrics": {_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()} if song else {}, - "callback": data.get("callback") - } - - return payload + lyrics_platform = LYRICS_PLATFORMS.get(platform) + if lyrics_platform: + lyrics: dict[str, str] = await lyrics_platform().get_lyrics(title, artist) + return { + "op": "getLyrics", + "userId": data.get("userId"), + "title": title, + "artist": artist, + "platform": platform, + "lyrics": {_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()} if lyrics else {}, + "callback": data.get("callback") + } async def updateSettings(bot: commands.Bot, data: Dict) -> None: user_id = int(data.get("userId")) diff --git a/settings Example.json b/settings Example.json index 4322aa5..ed29928 100644 --- a/settings Example.json +++ b/settings Example.json @@ -40,7 +40,7 @@ "bot_access_user": [], "embed_color":"0xb3b3b3", "default_max_queue": 1000, - "lyrics_platform": "lyrist", + "lyrics_platform": "lrclib", "ipc_client": { "host": "127.0.0.1", "port": 8000, diff --git a/views/controller.py b/views/controller.py index 07790d8..d403e83 100644 --- a/views/controller.py +++ b/views/controller.py @@ -29,7 +29,7 @@ import views import function as func from discord.ext import commands -from typing import Dict +from typing import Dict, Type def key(interaction: discord.Interaction): return interaction.user @@ -380,12 +380,14 @@ class Lyrics(ControlButton): title = self.player.current.title artist = self.player.current.author - song: dict[str, str] = await addons.lyricsPlatform.get(func.settings.lyrics_platform)().get_lyrics(title, artist) - if not song: - return await self.send(interaction, "lyricsNotFound", ephemeral=True) + lyrics_platform = addons.LYRICS_PLATFORMS.get(func.settings.lyrics_platform) + if lyrics_platform: + lyrics = await lyrics_platform().get_lyrics(title, artist) + if not lyrics: + return await self.send(interaction, "lyricsNotFound", ephemeral=True) - view = views.LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=interaction.user) - view.response = await self.send(interaction, view.build_embed(), view=view, ephemeral=True) + view = views.LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user) + view.response = await self.send(interaction, view.build_embed(), view=view, ephemeral=True) class Tracks(discord.ui.Select): def __init__(self, player, style, row): @@ -446,7 +448,7 @@ class Effects(discord.ui.Select): await self.player.add_filter(selected_filter, requester=interaction.user) await func.send(interaction, "addEffect", selected_filter.tag) -BUTTONTYPE: Dict[str, ControlButton] = { +BUTTON_TYPE: Dict[str, Type[ControlButton]] = { "back": Back, "resume": Resume, "skip": Skip, @@ -465,7 +467,7 @@ BUTTONTYPE: Dict[str, ControlButton] = { "effects": Effects } -BUTTONCOLOR: Dict[str, discord.ButtonStyle] = { +BUTTON_COLORS: Dict[str, discord.ButtonStyle] = { "blue": discord.ButtonStyle.primary, "grey": discord.ButtonStyle.secondary, "red": discord.ButtonStyle.danger, @@ -483,8 +485,8 @@ class InteractiveController(discord.ui.View): if isinstance(btn, Dict): color = list(btn.values())[0] btn = list(btn.keys())[0] - btnClass = BUTTONTYPE.get(btn.lower()) - style = BUTTONCOLOR.get(color.lower(), BUTTONCOLOR["grey"]) + btnClass = BUTTON_TYPE.get(btn.lower()) + style = BUTTON_COLORS.get(color.lower(), BUTTON_COLORS["grey"]) if not btnClass or (self.player.queue.is_empty and btn == "tracks"): continue self.add_item(btnClass(player=player, style=style, row=row)) From 1b54e83ceaf3627f8a5114ddd3188285108514f5 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:37:14 +0800 Subject: [PATCH 64/65] Fixed some bugs --- ipc/methods.py | 2 +- update.py | 2 +- voicelink/player.py | 11 +++++++---- voicelink/pool.py | 1 + 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ipc/methods.py b/ipc/methods.py index 200b4ec..8365b2b 100644 --- a/ipc/methods.py +++ b/ipc/methods.py @@ -297,7 +297,7 @@ async def updatePause(player: Player, member: Member, data: Dict) -> None: await player.set_pause(pause, member) @require_permission() -async def updatePosition(player: Player, member: Member, data: Dict) -> None: +async def updatePosition(player: Player, member: Member, data: Dict) -> None: position = data.get("position"); await player.seek(position, member); diff --git a/update.py b/update.py index b244bb5..c8a5a5d 100644 --- a/update.py +++ b/update.py @@ -25,7 +25,7 @@ import requests, zipfile, os, shutil, argparse from io import BytesIO ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.7.0b4" +__version__ = "v2.7.0" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" diff --git a/voicelink/player.py b/voicelink/player.py index e3d0b76..5db80cc 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -47,7 +47,7 @@ from discord.ext import commands from . import events from .enums import SearchType, LoopType, RequestMethod from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent, TrackExceptionEvent -from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, TrackLoadError, FilterTagAlreadyInUse, DuplicateTrack +from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, FilterTagAlreadyInUse, DuplicateTrack from .filters import Filter, Filters from .objects import Track, Playlist from .pool import Node, NodePool @@ -73,6 +73,9 @@ async def connect_channel(ctx: Union[commands.Context, Interaction], channel: Vo channel, ctx, settings )) + if player.volume != 100: + await player.set_volume(player.volume) + if ctx.bot.ipc.is_connected: await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]}) @@ -591,9 +594,6 @@ class Player(VoiceProtocol): self._current = track - if self.volume != 100: - await self.set_volume(self.volume) - self._logger.debug(f"Player in {self.guild.name}({self.guild.id}) playing {track.title} from uri {track.uri} with a length of {track.length}") return self._current @@ -661,6 +661,9 @@ class Player(VoiceProtocol): async def seek(self, position: float, requester: Member = None) -> float: """Seeks to a position in the currently playing track milliseconds""" + if not self._current: + raise VoicelinkException("Nothing is playing right now") + if position < 0 or position > self._current.length: raise TrackInvalidPosition("Seek position must be between 0 and the track length") diff --git a/voicelink/pool.py b/voicelink/pool.py index 91a0f2a..ec625d0 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -388,6 +388,7 @@ class Node: return [Track(track_id=data["encoded"], info=data["info"], requester=requester)] async def get_recommendations(self, track: Track, limit: int = 20) -> List[Optional[Track]]: + query = "" if track.source == "youtube": query = f"https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}" From fb32abfdc6e447efd980cf866a9d385d55cb05e8 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:19:52 +0800 Subject: [PATCH 65/65] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5249a08..afe10e3 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Vocard is a highly customizable Discord music bot, designed to deliver a user-fr * Multiple languages available * Easy to update * Supports docker -* Premium dashboard (in beta) +* [Premium dashboard](https://github.com/ChocoMeow/Vocard-Dashboard) ## Screenshot -![features](https://github.com/user-attachments/assets/f34b542d-be37-4170-bb80-c44748d8eb04) +![features](https://github.com/user-attachments/assets/2a1baf75-d1c8-41d1-a66f-7011e96d5feb) ## Requirements * [Python 3.11+](https://www.python.org/downloads/)