From e6a1d30026740cb38319877fb8205166aa9d1b76 Mon Sep 17 00:00:00 2001 From: Choco Date: Tue, 8 Aug 2023 13:14:07 +0800 Subject: [PATCH 01/27] Optimize code for improved cleanliness and enhanced performance. --- addons/settings.py | 37 +---------- cogs/basic.py | 18 +++--- function.py | 4 +- views/controller.py | 2 +- views/search.py | 2 +- voicelink/objects.py | 116 ++++++++++++++++++++++------------- voicelink/player.py | 2 +- voicelink/pool.py | 2 +- voicelink/queue.py | 12 ++-- voicelink/spotify/client.py | 12 ++-- voicelink/spotify/objects.py | 108 ++++++++++++++++++++------------ 11 files changed, 172 insertions(+), 143 deletions(-) diff --git a/addons/settings.py b/addons/settings.py index 38f5c50..bd18fec 100644 --- a/addons/settings.py +++ b/addons/settings.py @@ -13,42 +13,9 @@ class Settings: self.emoji_source_raw = settings.get("emoji_source_raw", {}) self.cooldowns_settings = settings.get("cooldowns", {}) self.aliases_settings = settings.get("aliases", {}) - self.controller = settings.get("default_controller", - { - "embeds": { - "active": { - "description": "**Now Playing: ```[@@track_name@@]```\nLink: [Click Me](@@track_url@@) | Requester: @@requester@@ | DJ: @@dj@@**", - "footer": { - "text": "Queue Length: @@queue_length@@ | Duration: @@duration@@ | Volume: @@volume@@% {{loop_mode!=Off ?? | Repeat: @@loop_mode@@}}", - }, - "image": "@@track_thumbnail@@", - "author": { - "name": "Music Controller | @@channel_name@@", - "icon_url": "@@bot_icon@@" - }, - "color": "@@default_embed_color@@" - }, - "inactive": { - "title": { - "name": "There are no songs playing right now" - }, - "description": "[Support](@@server_invite_link@@) | [Invite](@@invite_link@@) | [Questionnaire](https://forms.gle/Qm8vjBfg2kp13YGD7)", - "image": "https://i.imgur.com/dIFBwU7.png", - "color": "@@default_embed_color@@" - } - }, - "default_buttons": [ - ["back", "resume", "skip", {"stop": "red"}, "add"], - ["tracks"] - ] - }) + self.controller = settings.get("default_controller", {}) self.lyrics_platform = settings.get("lyrics_platform", "A_ZLyrics").lower() - self.ipc_server = settings.get("ipc_server", { - "host": "127.0.0.1", - "port": 8000, - "enable": False - } - ) + self.ipc_server = settings.get("ipc_server", {}) self.version = settings.get("version", "") class TOKENS: diff --git a/cogs/basic.py b/cogs/basic.py index 5c79b30..c16d447 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -33,7 +33,7 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player): if not track: return await ctx.send(player.get_msg('noTrackPlaying'), ephemeral=True) - upnext = "\n".join(f"`{index}.` `[{track.formatLength}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate( + upnext = "\n".join(f"`{index}.` `[{track.formatted_length}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate( player.queue.tracks()[:2], start=2)) embed = discord.Embed(description=player.get_msg( 'nowplayingDesc').format(track.title), color=settings.embed_color) @@ -48,7 +48,7 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player): ":pause_button:" if player.is_paused else ":arrow_forward:") embed.add_field( - name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatLength}]**", inline=False) + name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatted_length}]**", inline=False) return await ctx.send(embed=embed, view=LinkView(player.get_msg('nowplayingLink').format(track.source), track.emoji, track.uri)) @@ -103,7 +103,7 @@ class Basic(commands.Cog): await ctx.send(player.get_msg('playlistLoad').format(tracks.name, index)) else: position = await player.add_track(tracks[0]) - await ctx.send((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + (player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength)), allowed_mentions=False) + await ctx.send((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + (player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').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) finally: @@ -143,7 +143,7 @@ class Basic(commands.Cog): await interaction.response.send_message(player.get_msg('playlistLoad').format(tracks.name, index)) else: position = await player.add_track(tracks[0]) - await interaction.response.send_message((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + (player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength)), allowed_mentions=False) + await interaction.response.send_message((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + (player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length)), allowed_mentions=False) except voicelink.QueueFull as e: await interaction.response.send_message(e) @@ -190,7 +190,7 @@ class Basic(commands.Cog): return await ctx.send(player.get_msg('noTrackFound')) query_track = "\n".join( - f"`{index}.` `[{track.formatLength}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1)) + f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1)) embed = discord.Embed(title=player.get_msg('searchTitle').format(query), description=player.get_msg( 'searchDesc').format(emoji_source(platform), platform, len(tracks[0:10]), query_track), color=settings.embed_color) view = SearchView(tracks=tracks[0:10], lang=player.lang) @@ -202,8 +202,8 @@ class Basic(commands.Cog): for value in view.values: track = tracks[int(value.split(". ")[0]) - 1] position = await player.add_track(track) - msg += ((f"`{player.get_msg('live')}`" if track.is_stream else "") + (player.get_msg('trackLoad_pos').format(track.title, track.uri, track.author, track.formatLength, - position) if position >= 1 else player.get_msg('trackLoad').format(track.title, track.uri, track.author, track.formatLength))) + msg += ((f"`{player.get_msg('live')}`" if track.is_stream else "") + (player.get_msg('trackLoad_pos').format(track.title, track.uri, track.author, track.formatted_length, + position) if position >= 1 else player.get_msg('trackLoad').format(track.title, track.uri, track.author, track.formatted_length))) await ctx.send(msg, allowed_mentions=False) if not player.is_playing: @@ -231,7 +231,7 @@ class Basic(commands.Cog): await ctx.send(player.get_msg('playlistLoad').format(tracks.name, index)) else: position = await player.add_track(tracks[0], at_font=True) - await ctx.send((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + (player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength)), allowed_mentions=False) + await ctx.send((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + (player.get_msg('trackLoad_pos').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length, position) if position >= 1 and player.is_playing else player.get_msg('trackLoad').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) @@ -262,7 +262,7 @@ class Basic(commands.Cog): await ctx.send(player.get_msg('playlistLoad').format(tracks.name, index)) else: await player.add_track(tracks[0], at_font=True) - await ctx.send((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + player.get_msg('trackLoad').format(tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatLength), allowed_mentions=False) + await ctx.send((f"`{player.get_msg('live')}`" if tracks[0].is_stream else "") + player.get_msg('trackLoad').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) diff --git a/function.py b/function.py index b684331..be945c5 100644 --- a/function.py +++ b/function.py @@ -17,7 +17,7 @@ if not os.path.exists(os.path.join(root_dir, "settings.json")): raise Exception("Settings file not set!") #-------------- API Clients -------------- -tokens: TOKENS = TOKENS(); +tokens: TOKENS = TOKENS() if not (tokens.mongodb_name and tokens.mongodb_url): raise Exception("MONGODB_NAME and MONGODB_URL can't not be empty in .env") @@ -133,7 +133,7 @@ def formatTime(number:str) -> Optional[int]: return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000 -def emoji_source(emoji:str): +def emoji_source(emoji:str) -> str: return settings.emoji_source_raw.get(emoji.lower(), "🔗") def gen_report() -> Optional[discord.File]: diff --git a/views/controller.py b/views/controller.py index 06c087e..d3bb926 100644 --- a/views/controller.py +++ b/views/controller.py @@ -376,7 +376,7 @@ class Tracks(discord.ui.Select): for index, track in enumerate(self.player.queue.tracks(), start=1): if index > 10: break - options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatLength), emoji=track.emoji)) + options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatted_length), emoji=track.emoji)) super().__init__( placeholder=player.get_msg("playerDropdown"), diff --git a/views/search.py b/views/search.py index a8c9141..84cee9d 100644 --- a/views/search.py +++ b/views/search.py @@ -31,7 +31,7 @@ class SearchDropdown(discord.ui.Select): self.lang = lang options = [] for index, track in enumerate(self.tracks, start=1): - options.append(discord.SelectOption(label=f"{index}. {track.title[:50]}", description=f"{track.author[:50]} · {track.formatLength}")) + options.append(discord.SelectOption(label=f"{index}. {track.title[:50]}", description=f"{track.author[:50]} · {track.formatted_length}")) super().__init__(placeholder=langs[lang]['searchWait'], min_values=1, max_values=len(tracks), diff --git a/voicelink/objects.py b/voicelink/objects.py index 5138363..9a3bcd4 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -41,6 +41,28 @@ class Track: You can also pass in commands.Context to get a discord.py Context object in your track. """ + __slots__ = ( + "track_id", + "info", + "identifier", + "title", + "author", + "uri", + "source", + "spotify", + "artist_id", + "original", + "_search_type", + "spotify_track", + "thumbnail", + "emoji", + "length", + "requester", + "is_stream", + "is_seekable", + "position" + ) + def __init__( self, *, @@ -50,73 +72,83 @@ class Track: search_type: SearchType = SearchType.ytsearch, spotify_track = None, ): - self.track_id = track_id - self.info = info + self.track_id: str = track_id + self.info: dict = info - self.identifier = info.get("identifier") - self.title = info.get("title", "Unknown") - self.author = info.get("author", "Unknown") - self.uri = info.get("uri", "https://discord.com/application-directory/605618911471468554") - self.source = info.get("sourceName", extract(self.uri).domain) - self.spotify = True if self.source == "spotify" else False + self.identifier: str = info.get("identifier") + self.title: str = info.get("title", "Unknown") + 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 = True if self.source == "spotify" else False if self.spotify: - self.artistId: Optional[list] = info.get("artistId") + self.artist_id: Optional[list] = info.get("artist_id") self.original: Optional[Track] = None if self.spotify else self - self._search_type = SearchType.ytmsearch if self.spotify else search_type - self.spotify_track = spotify_track + self._search_type: SearchType = SearchType.ytmsearch if self.spotify else search_type + self.spotify_track: Track = spotify_track - self.thumbnail = None - - self.emoji = emoji_source(self.source) + self.thumbnail: str = None + self.emoji: str = emoji_source(self.source) if info.get("thumbnail"): self.thumbnail = info.get("thumbnail") elif YOUTUBE_REGEX.match(self.uri): self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/hqdefault.jpg" - if self.source == "soundcloud" and "/preview/" in self.identifier: - self.length = 30000 - else: - self.length = info.get("length") + self.length: float = 3000 if self.source == "soundcloud" and "/preview/" in self.identifier else info.get("length") - self.formatLength = ctime(self.length) - self.requester = requester - self.is_stream = info.get("isStream", False) - self.is_seekable = info.get("isSeekable", True) - self.position = info.get("position", 0) + self.requester: Member = requester + self.is_stream: bool = info.get("isStream", False) + self.is_seekable: bool = info.get("isSeekable", True) + self.position: int = info.get("position", 0) if not track_id: self.track_id = encode(self) - def toDict(self): + def __eq__(self, other) -> bool: + if not isinstance(other, Track): + return False + + return other.track_id == self.track_id + + def __str__(self) -> str: + return self.title + + def __repr__(self) -> str: + return f" length={self.length}>" + + def toDict(self) -> dict: return { "track_id": self.track_id, "info": self.info, "thumbnail": self.thumbnail } - def encode(self): + def encode(self) -> bytes: return encode(self) - def __eq__(self, other): - if not isinstance(other, Track): - return False - - return other.track_id == self.track_id - - def __str__(self): - return self.title - - def __repr__(self): - return f" length={self.length}>" - + @property + def formatted_length(self) -> str: + return ctime(self.length) + class Playlist: """The base playlist object. Returns critical playlist information needed for parsing by Lavalink. You can also pass in commands.Context to get a discord.py Context object in your tracks. """ + __slots__ = ( + "playlist_info", + "tracks_raw", + "spotify", + "name", + "spotify_playlist", + "_thumbnail", + "_uri", + "tracks" + ) + def __init__( self, *, @@ -147,12 +179,10 @@ class Playlist: self._thumbnail = None self._uri = None - self.track_count = len(self.tracks) - - def __str__(self): + def __str__(self) -> str: return self.name - def __repr__(self): + def __repr__(self) -> str: return f"" @property @@ -164,3 +194,7 @@ class Playlist: 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 6b3b5ea..e093380 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -441,7 +441,7 @@ class Player(VoiceProtocol): try: tracks = await self._node._spotify_client.trackSearch(query=query) - except: + except Exception as _: raise TrackLoadError("Not able to find the provided Spotify entity, is it private?") return [ Track( diff --git a/voicelink/pool.py b/voicelink/pool.py index 7a2e543..030499d 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -394,7 +394,7 @@ class Node: try: spotify_results = await self._spotify_client.search(query=query) - except: + except Exception as _: raise TrackLoadError("Not able to find the provided Spotify entity, is it private?") if isinstance(spotify_results, spotify.Track): diff --git a/voicelink/queue.py b/voicelink/queue.py index 4672401..3133411 100644 --- a/voicelink/queue.py +++ b/voicelink/queue.py @@ -53,12 +53,12 @@ class LoopTypeCycle: class Queue: def __init__(self, size: int, allow_duplicate: bool, get_msg: Callable[[str], str]) -> None: - self._queue = [] - self._position = 0 - self._size = size - self._repeat = LoopTypeCycle() - self._repeat_position = 0 - self._allow_duplicate = allow_duplicate + self._queue: List[Track] = [] + self._position: int = 0 + self._size: int = size + self._repeat: LoopTypeCycle = LoopTypeCycle() + self._repeat_position: int = 0 + self._allow_duplicate: bool = allow_duplicate self.get_msg = get_msg diff --git a/voicelink/spotify/client.py b/voicelink/spotify/client.py index 016b7e0..66e76c2 100644 --- a/voicelink/spotify/client.py +++ b/voicelink/spotify/client.py @@ -23,12 +23,12 @@ SOFTWARE. import re import time -from base64 import b64encode - import aiohttp +from base64 import b64encode +from typing import List, Union from .objects import Track, Album, Artist, Playlist -from .exceptions import InvalidSpotifyURL, SpotifyRequestException +from .exceptions import InvalidSpotifyURL, SpotifyRequestException GRANT_URL = "https://accounts.spotify.com/api/token" REQUEST_URL = "https://api.spotify.com/v1/{type}s/{id}" @@ -71,7 +71,7 @@ class Client: self._expiry = time.time() + (int(data["expires_in"]) - 10) self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"} - async def trackSearch(self, query: str, track: str = "track", limit: int = 10) -> list: + async def trackSearch(self, query: str, track: str = "track", limit: int = 10) -> List[Track]: if not self._bearer_token or time.time() >= self._expiry: await self._fetch_bearer_token() @@ -87,7 +87,7 @@ class Client: return [ Track(track) for track in data['tracks']['items'] ] - async def similar_track(self, seed_tracks: str, *, limit: int = 5) -> list: + async def similar_track(self, seed_tracks: str, *, limit: int = 5) -> List[Track]: if not self._bearer_token or time.time() >= self._expiry: await self._fetch_bearer_token() @@ -103,7 +103,7 @@ class Client: return [ Track(track) for track in data['tracks'] ] - async def search(self, *, query: str): + async def search(self, *, query: str) -> Union(Track, Album, Playlist): if not self._bearer_token or time.time() >= self._expiry: await self._fetch_bearer_token() diff --git a/voicelink/spotify/objects.py b/voicelink/spotify/objects.py index 94b65f9..ad7a1b8 100644 --- a/voicelink/spotify/objects.py +++ b/voicelink/spotify/objects.py @@ -3,22 +3,24 @@ from typing import List 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 = data.get('name', 'Unknown') - self.artists = ", ".join(artist["name"] for artist in data.get('artists')) - self.artistId = [artist['id'] for artist in data.get('artists')] - self.length = data.get('duration_ms') - self.id = data.get('id') - - if data.get("album") and data["album"].get("images"): - self.image = data["album"]["images"][0]["url"] - else: - self.image = image - - if data["is_local"]: - self.uri = None - else: - self.uri = data["external_urls"]["spotify"] + self.name: str = data.get('name', 'Unknown') + self.artists: str = ", ".join(artist["name"] for artist in data.get('artists')) + self.artist_id: list = [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 { @@ -26,7 +28,7 @@ class Track: "author": self.artists, "length": self.length, "identifier": self.id, - "artistId": self.artistId, + "artist_id": self.artist_id, "uri": self.uri, "isStream": False, "isSeekable": True, @@ -43,14 +45,24 @@ class Track: 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 = data.get('name', 'Unknown') - self.artists = ", ".join(artist["name"] for artist in data.get('artists')) - self.image = data["images"][0]["url"] - self.tracks = [Track(track, image=self.image) for track in data["tracks"]["items"]] - self.total_tracks = data["total_tracks"] - self.id = data.get('id') - self.uri = data["external_urls"]["spotify"] + self.name: str = data.get('name', 'Unknown') + self.artists: str = ", ".join(artist["name"] for artist in data.get('artists')) + self.image: str = data["images"][0]["url"] + self.tracks: list = [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 ( @@ -61,15 +73,24 @@ class Album: 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 = [Track(track) for track in data['tracks']] + self.tracks: List[Track] = [Track(track) for track in data['tracks']] if self.tracks: - self.image = self.tracks[0].image - self.total_tracks = len(self.tracks) - self.owner = self.tracks[0].artists - self.id = self.tracks[0].artistId - self.uri = data['tracks'][0]['album']['artists'][0]['external_urls']['spotify'] - self.name = f"Top tracks - {self.owner}" + 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 ( @@ -80,17 +101,24 @@ class Artist: 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 = data.get('name', 'Unknown') - self.tracks = tracks - self.owner = data["owner"]["display_name"] - self.total_tracks = data["tracks"]["total"] - self.id = data.get('id') - if data.get("images") and len(data["images"]): - self.image = data["images"][0]["url"] - else: - self.image = None - self.uri = data["external_urls"]["spotify"] + 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 ( From b89ec4226e0a769d7132cde84f933fe5e0864751 Mon Sep 17 00:00:00 2001 From: Choco Date: Tue, 8 Aug 2023 13:14:13 +0800 Subject: [PATCH 02/27] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index aeee6d4..d8cf54d 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,6 @@ SPOTIFY_CLIENT_SECRET = 0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX GENIUS_TOKEN = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -YOUTUBE_API_KEY = AXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - MONGODB_URL = mongodb+srv://user:password@clusterURL MONGODB_NAME = Vocard ``` @@ -74,7 +72,6 @@ MONGODB_NAME = Vocard | BUG_REPORT_CHANNEL_ID | All the error messages will send to this text channel ***(optional)*** | | SPOTIFY_CLIENT_ID | Your Spoity client id [(Spotify Portal)](https://developer.spotify.com/dashboard/applications) ***(optional)*** | | SPOTIFY_CLIENT_SECRET | Your Spoity client sercret id [(Spotify Portal)](https://developer.spotify.com/dashboard/applications) ***(optional)*** | -| YOUTUBE_API_KEY | Your youtube api key [(Google API)](https://cloud.google.com/apis) ***(optional)*** | | GENIUS_TOKEN | Your genius api key [(Genius Lyrics API)](https://genius.com/api-clients) ***(optional)*** | | MONGODB_URL | Your Mongo datebase url [(Mongodb)](https://www.mongodb.com/) | | MONGODB_NAME | The datebase name that you created on [Mongodb](https://www.mongodb.com/) | From 06913ce5babc3041d67515a7b1dfee9dba83ef71 Mon Sep 17 00:00:00 2001 From: Choco Date: Tue, 8 Aug 2023 14:56:28 +0800 Subject: [PATCH 03/27] Added thumbnail in nowplaying embed --- cogs/basic.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index c16d447..d013a37 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -33,22 +33,20 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player): if not track: return await ctx.send(player.get_msg('noTrackPlaying'), ephemeral=True) - upnext = "\n".join(f"`{index}.` `[{track.formatted_length}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate( - player.queue.tracks()[:2], start=2)) - embed = discord.Embed(description=player.get_msg( - 'nowplayingDesc').format(track.title), color=settings.embed_color) - embed.set_author(name=track.requester if track.requester else ctx.bot, - icon_url=track.requester.display_avatar.url if track.requester else ctx.me.display_avatar.url) + upnext = "\n".join(f"`{index}.` `[{track.formatted_length}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate(player.queue.tracks()[:2], start=2)) + embed = discord.Embed(description=player.get_msg('nowplayingDesc').format(track.title), color=settings.embed_color) + embed.set_author( + name=track.requester if track.requester else ctx.bot, + icon_url=track.requester.display_avatar.url if track.requester else ctx.me.display_avatar.url + ) + embed.set_thumbnail(url=track.thumbnail) if upnext: embed.add_field(name=player.get_msg('nowplayingField'), value=upnext) - pbar = "".join(":radio_button:" if i == round( - player.position // round(track.length // 15)) else "▬" for i in range(15)) - icon = ":red_circle:" if track.is_stream else ( - ":pause_button:" if player.is_paused else ":arrow_forward:") - embed.add_field( - name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatted_length}]**", inline=False) + pbar = "".join(":radio_button:" if i == round(player.position // round(track.length // 15)) else "▬" for i in range(15)) + icon = ":red_circle:" if track.is_stream else (":pause_button:" if player.is_paused else ":arrow_forward:") + embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{ctime(player.position)}/{track.formatted_length}]**", inline=False) return await ctx.send(embed=embed, view=LinkView(player.get_msg('nowplayingLink').format(track.source), track.emoji, track.uri)) @@ -63,8 +61,7 @@ class Basic(commands.Cog): self.bot.tree.add_command(self.ctx_menu) async def cog_unload(self) -> None: - self.bot.tree.remove_command( - self.ctx_menu.name, type=self.ctx_menu.type) + self.bot.tree.remove_command(self.ctx_menu.name, type=self.ctx_menu.type) async def help_autocomplete(self, interaction: discord.Interaction, current: str) -> list: 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] From e97e52d127b60dc6aa2e063c4503a76427ff2cd5 Mon Sep 17 00:00:00 2001 From: Choco Date: Tue, 8 Aug 2023 14:57:02 +0800 Subject: [PATCH 04/27] Fixed bug --- voicelink/spotify/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicelink/spotify/client.py b/voicelink/spotify/client.py index 66e76c2..58c5205 100644 --- a/voicelink/spotify/client.py +++ b/voicelink/spotify/client.py @@ -103,7 +103,7 @@ class Client: return [ Track(track) for track in data['tracks'] ] - async def search(self, *, query: str) -> Union(Track, Album, Playlist): + async def search(self, *, query: str) -> Union[Track, Album, Playlist]: if not self._bearer_token or time.time() >= self._expiry: await self._fetch_bearer_token() From 148f19e5fe9c73bbdf449a13f8cc1f622d492df6 Mon Sep 17 00:00:00 2001 From: Choco Date: Wed, 9 Aug 2023 12:05:05 +0800 Subject: [PATCH 05/27] General clean code --- cogs/admin.py | 4 ---- cogs/basic.py | 2 +- views/search.py | 15 ++++++++------- voicelink/player.py | 1 - 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/cogs/admin.py b/cogs/admin.py index 6b8017d..fd289f7 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -63,10 +63,6 @@ class Admin(commands.Cog, name="settings"): if language not in langs: return await ctx.send(get_lang(ctx.guild.id, "languageNotFound")) - player, settings = self.get_settings(ctx) - if player: - player.lang = language - update_settings(ctx.guild.id, {'lang': language}) await ctx.send(get_lang(ctx.guild.id, 'changedLanguage').format(language)) diff --git a/cogs/basic.py b/cogs/basic.py index d013a37..c655619 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -190,7 +190,7 @@ class Basic(commands.Cog): f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1)) embed = discord.Embed(title=player.get_msg('searchTitle').format(query), description=player.get_msg( 'searchDesc').format(emoji_source(platform), platform, len(tracks[0:10]), query_track), color=settings.embed_color) - view = SearchView(tracks=tracks[0:10], lang=player.lang) + view = SearchView(tracks=tracks[0:10], lang=player.get_msg) message = await ctx.send(embed=embed, view=view, ephemeral=True) view.response = message await view.wait() diff --git a/views/search.py b/views/search.py index 84cee9d..a496147 100644 --- a/views/search.py +++ b/views/search.py @@ -26,21 +26,22 @@ import discord from function import langs class SearchDropdown(discord.ui.Select): - def __init__(self, tracks, lang): + def __init__(self, tracks, get_msg): self.tracks = tracks - self.lang = lang + self.get_msg = get_msg options = [] for index, track in enumerate(self.tracks, start=1): options.append(discord.SelectOption(label=f"{index}. {track.title[:50]}", description=f"{track.author[:50]} · {track.formatted_length}")) - super().__init__(placeholder=langs[lang]['searchWait'], - min_values=1, max_values=len(tracks), - options=options - ) + super().__init__( + placeholder=get_msg('searchWait'), + min_values=1, max_values=len(tracks), + options=options + ) async def callback(self, interaction: discord.Interaction): self.disabled = True - self.placeholder = langs[self.lang]['searchSuccess'] + self.placeholder = self.get_msg('searchSuccess') await interaction.response.edit_message(view=self.view) self.view.values = self.values self.view.stop() diff --git a/voicelink/player.py b/voicelink/player.py index e093380..38875ca 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -107,7 +107,6 @@ class Player(VoiceProtocol): self.settings: dict = func.get_settings(ctx.guild.id) self.joinTime: float = round(time.time()) self._volume: int = self.settings.get('volume', 100) - self.lang: dict = self.settings.get('lang', 'EN') if self.settings.get('lang', 'EN') in func.langs else "EN" 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() From 52f3b36bf22645fda6176e97018f8f2bd6cb2568 Mon Sep 17 00:00:00 2001 From: Choco Date: Tue, 22 Aug 2023 16:17:02 +0800 Subject: [PATCH 06/27] Rewrite debug command --- cogs/listeners.py | 4 +- cogs/{admin.py => settings.py} | 94 +++++++++---------- requirements.txt | 1 + views/__init__.py | 2 +- views/debug.py | 159 +++++++++++++++++++++++++++++++-- 5 files changed, 201 insertions(+), 59 deletions(-) rename cogs/{admin.py => settings.py} (82%) diff --git a/cogs/listeners.py b/cogs/listeners.py index 336a32e..a9e2a7d 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -5,7 +5,7 @@ import function as func from discord.ext import commands -class Nodes(commands.Cog): +class Listeners(commands.Cog): """Music Cog.""" def __init__(self, bot: commands.Bot): @@ -82,4 +82,4 @@ class Nodes(commands.Cog): }) async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Nodes(bot)) + await bot.add_cog(Listeners(bot)) diff --git a/cogs/admin.py b/cogs/settings.py similarity index 82% rename from cogs/admin.py rename to cogs/settings.py index fd289f7..c67cfc9 100644 --- a/cogs/admin.py +++ b/cogs/settings.py @@ -1,9 +1,6 @@ import discord import voicelink -import io -import contextlib -import textwrap -import traceback +import psutil import function as func from typing import Tuple @@ -19,11 +16,18 @@ from function import ( get_aliases, cooldown_check ) -from views import DebugModal, HelpView, EmbedBuilderView +from views import DebugView, HelpView, EmbedBuilderView -class Admin(commands.Cog, name="settings"): +def formatBytes(bytes: int, unit: bool = False): + if bytes <= 1_000_000_000: + return f"{bytes / (1024 ** 2):.1f}" + ("MB" if unit else "") + + else: + return f"{bytes / (1024 ** 3):.1f}" + ("GB" if unit else "") + +class Settings(commands.Cog, name="settings"): def __init__(self, bot) -> None: - self.bot = bot + self.bot: commands.Bot = bot self.description = "This category is only available to admin permissions on the server." def get_settings(self, ctx: commands.Context) -> Tuple[voicelink.Player, dict]: @@ -246,53 +250,41 @@ class Admin(commands.Cog, name="settings"): if interaction.user.id not in func.settings.bot_access_user: return await interaction.response.send_message("You are not able to use this command!") - def clear_code(content: str): - if content.startswith("```") and content.endswith("```"): - return "\n".join(content.split("\n")[1:])[:-3] - else: - return content + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') - modal = DebugModal(title="Debug Panel") - await interaction.response.send_modal(modal) - await modal.wait() + available_memory, total_memory = memory.available, memory.total + used_disk_space, total_disk_space = disk.used, disk.total + embed = discord.Embed(title="📄 Debug Panel", color=func.settings.embed_color) + embed.description = "```== System Info ==\n" \ + f"• CPU: {psutil.cpu_freq().current}Mhz ({psutil.cpu_percent()}%)\n" \ + f"• RAM: {formatBytes(total_memory - available_memory)}/{formatBytes(total_memory, True)} ({memory.percent}%)\n" \ + f"• DISK: {formatBytes(total_disk_space - used_disk_space)}/{formatBytes(total_disk_space, True)} ({disk.percent}%)```" - if modal.values is None: - return + embed.add_field( + name="🤖 Bot Information", + value=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"• PLAYERS: {len(self.bot.voice_clients)}```", + inline=False + ) - e = None - - local_variables = { - "discord": discord, - "commands": commands, - "voicelink": voicelink, - "bot": self.bot, - "interaction": interaction, - "channel": interaction.channel, - "author": interaction.user, - "guild": interaction.guild, - "message": interaction.message, - "input": None - } - - code = clear_code(modal.values) - str_obj = io.StringIO() # Retrieves a stream of data - try: - with contextlib.redirect_stdout(str_obj): - exec( - f"async def func():\n{textwrap.indent(code, ' ')}", local_variables) - obj = await local_variables["func"]() - result = f"{str_obj.getvalue()}\n-- {obj}\n" - except Exception as e: - errormsg = ''.join( - traceback.format_exception(e, e, e.__traceback__)) - return await interaction.followup.send(f"```py\n{errormsg}```") - - string = result.split("\n") - text = "" - for index, i in enumerate(string, start=1): - text += f"{'%03d' % index} | {i}\n" - return await interaction.followup.send(f"```{text}```") + node: voicelink.Node + for name, node in voicelink.NodePool._nodes.items(): + total_memory = node.stats.used + node.stats.free + embed.add_field( + name=f"{name} Node - " + ("🟢 Connected" if node._available else "🔴 Disconnected"), + value=f"```• ADDRESS: {node._host}:{node._port}\n" \ + f"• PLAYERS: {len(node._players)}\n" \ + f"• CPU: {node.stats.cpu_process_load:.1f}%\n" \ + f"• RAM: {formatBytes(node.stats.free)}/{formatBytes(total_memory, True)} ({(node.stats.free/total_memory) * 100:.1f}%)\n" + f"• LATENCY: {node.latency:.2f}ms\n" \ + f"• UPTIME: {func.time(node.stats.uptime)}```", + inline=True + ) + await interaction.response.send_message(embed=embed, view=DebugView(self.bot), ephemeral=True) async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Admin(bot)) + await bot.add_cog(Settings(bot)) diff --git a/requirements.txt b/requirements.txt index 9c997df..c539330 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ beautifulsoup4==4.11.1 websockets==10.4 Flask==2.2.3 Flask-SocketIO==5.3.2 +psutil==5.9.5 diff --git a/views/__init__.py b/views/__init__.py index 092389e..e1453ca 100644 --- a/views/__init__.py +++ b/views/__init__.py @@ -13,5 +13,5 @@ from .chapter import ChapterView from .playlist import PlaylistView, CreateView from .inbox import InboxView from .link import LinkView -from .debug import DebugModal +from .debug import DebugView from .embedBuilder import EmbedBuilderView diff --git a/views/debug.py b/views/debug.py index a1348c3..6624cdb 100644 --- a/views/debug.py +++ b/views/debug.py @@ -22,20 +22,169 @@ SOFTWARE. """ import discord +import function +import io +import contextlib +import textwrap +import traceback -class DebugModal(discord.ui.Modal): - def __init__(self, *args, **kwargs) -> None: +from discord.ext import commands + +class ExceuteModal(discord.ui.Modal): + def __init__(self, code: str, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.values = None + self.code = code self.add_item( discord.ui.TextInput( label="Code Runner", placeholder="Input Your Code", style=discord.TextStyle.long, + default=self.code ) ) async def on_submit(self, interaction: discord.Interaction): - self.values = self.children[0].value - self.stop() \ No newline at end of file + await interaction.response.defer() + self.code = self.children[0].value + self.stop() + +class CogsDropdown(discord.ui.Select): + def __init__(self, bot: commands.Bot): + self.bot = bot + + options = [discord.SelectOption(label="All", description="All the cogs")] + + for name, cog in bot.cogs.items(): + options.append(discord.SelectOption(label=name.capitalize(), description=cog.description[:50])) + + super().__init__( + placeholder="Select a cog to reload...", + min_values=1, max_values=1, + options=options, + ) + + async def callback(self, interaction: discord.Interaction) -> None: + selected = self.values[0].lower() + try: + if selected == "all": + 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}") + 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) + +class ExceutePanel(discord.ui.View): + def __init__(self, bot, *, timeout = 180): + self.bot: commands.Bot = bot + + self.message: discord.WebhookMessage = None + self.code: str = None + self._error: Exception = None + + super().__init__(timeout=timeout) + + def toggle_button(self, name: str, status: bool): + child: discord.ui.Button + for child in self.children: + if child.label == name: + child.disabled = status + break + + def clear_code(self, content: str): + """Automatically removes code blocks from the code.""" + if content.startswith('```') and content.endswith('```'): + return '\n'.join(content.split('\n')[1:-1]) + + return content.strip('` \n') + + async def on_timeout(self) -> None: + for child in self.children: + child.disabled = True + if self.message: + await self.message.edit(view=self) + + async def execute(self, interaction: discord.Interaction): + modal = ExceuteModal(self.code, title="Enter Your Code") + await interaction.response.send_modal(modal) + await modal.wait() + + if not (code := modal.code): + return + + self._error = None + text = "" + + local_variables = { + "discord": discord, + "bot": self.bot, + "interaction": interaction, + "input": None + } + + self.code = self.clear_code(code) + str_obj = io.StringIO() #Retrieves a stream of data + try: + with contextlib.redirect_stdout(str_obj): + exec(f"async def func():\n{textwrap.indent(self.code, ' ')}", local_variables) + obj = await local_variables["func"]() + result = f"{str_obj.getvalue()}\n-- {obj}\n" + except Exception as e: + text = f"{e.__class__.__name__}: {e}" + self._error = e + + if not self._error: + text = "\n".join([f"{'%03d' % index} | {i}" for index, i in enumerate(result.split("\n"), start=1)]) + + self.toggle_button("Error", True if self._error is None else False) + + if not self.message: + self.message = await interaction.followup.send(f"```{text}```", view=self, ephemeral=True) + else: + await self.message.edit(content=f"```{text}```", view=self) + + @discord.ui.button(label="End", emoji="🗑️", custom_id="end") + async def end(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.message: + await self.message.delete() + self.stop() + + @discord.ui.button(label="Rerun", emoji="🔄", custom_id="rerun") + async def rerun(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.execute(interaction) + + @discord.ui.button(label="Error", emoji="👾", custom_id="Error") + async def error(self, interaction: discord.Interaction, button: discord.ui.Button): + result = ''.join(traceback.format_exception(self._error, self._error, self._error.__traceback__)) + await self.message.edit(content=f"```py\n{result}```") + +class CogsView(discord.ui.View): + def __init__(self, bot, *, timeout: float | None = 180): + super().__init__(timeout=timeout) + + self.add_item(CogsDropdown(bot)) + +class DebugView(discord.ui.View): + def __init__(self, bot, *, timeout: float | None = 180): + self.bot: commands.Bot = bot + self.panel: ExceutePanel = ExceutePanel(bot) + + super().__init__(timeout=timeout) + + @discord.ui.button(label='Command', emoji="▶️", style=discord.ButtonStyle.green) + async def run_command(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.panel.execute(interaction) + + @discord.ui.button(label='Cogs', emoji="🔃") + async def reload_cog(self, interaction: discord.Interaction, button: discord.ui.Button): + return await interaction.response.send_message("Reload Cogs", view=CogsView(self.bot), ephemeral=True) + + @discord.ui.button(label='Send Logs', emoji="📥", style=discord.ButtonStyle.red) + async def send_error_logs(self, interaction: discord.Interaction, button: discord.ui.Button): + if not function.error_log: + return await interaction.response.send_message("Sorry there are not error logs!", ephemeral=True) + + await interaction.response.send_message(file=function.gen_report(), ephemeral=True) \ No newline at end of file From ad6fe7ee8be1be74d93be0215fad5c224465a52e Mon Sep 17 00:00:00 2001 From: Choco Date: Tue, 22 Aug 2023 16:18:05 +0800 Subject: [PATCH 07/27] Dump discord.py from 2.3.1 to 2.3.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c539330..86938fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.3.1 +discord.py==2.3.2 pymongo==4.1.1 dnspython==2.2.1 tldextract==3.2.1 From ab84966ea02fb12ee08cc483b234ab61d5c85599 Mon Sep 17 00:00:00 2001 From: Choco Date: Thu, 24 Aug 2023 10:38:32 +0800 Subject: [PATCH 08/27] Fixed Rewind button in controller --- views/controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/views/controller.py b/views/controller.py index d3bb926..025666c 100644 --- a/views/controller.py +++ b/views/controller.py @@ -94,7 +94,7 @@ class Resume(ControlButton): super().__init__( emoji="⏸️", label="buttonPause", - disabled=not bool(kwargs["player"].current), + disabled=kwargs["player"].current is None, **kwargs ) @@ -187,7 +187,7 @@ class Add(ControlButton): def __init__(self, **kwargs): super().__init__( emoji="❤️", - disabled=not bool(kwargs["player"].current), + disabled=kwargs["player"].current is None, **kwargs ) @@ -332,7 +332,7 @@ class Forward(ControlButton): super().__init__( emoji="⏩", label="buttonForward", - disabled=not bool(kwargs["player"].current), + disabled=kwargs["player"].current is None, **kwargs ) @@ -351,7 +351,7 @@ class Rewind(ControlButton): super().__init__( emoji="⏪", label="buttonRewind", - disabled=not bool(kwargs["player"].current) + disabled=kwargs["player"].current is None, **kwargs ) From e287a3cf8ce67e816dbaa7ebbfb950126b4e220e Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 26 Aug 2023 09:38:54 +0800 Subject: [PATCH 09/27] Fixed queue slash command --- cogs/basic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/basic.py b/cogs/basic.py index c655619..2fd4312 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -416,6 +416,7 @@ class Basic(commands.Cog): @commands.hybrid_group( name="queue", aliases=get_aliases("queue"), + fallback="list", invoke_without_command=True ) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) From b7456f53124f8c37ddc6c76d6d5b149da9b1e1ce Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 26 Aug 2023 09:48:36 +0800 Subject: [PATCH 10/27] General clean code --- cogs/basic.py | 16 +++---- cogs/playlist.py | 47 ++++++++---------- cogs/settings.py | 16 +++---- cogs/task.py | 6 +-- function.py | 114 ++++++++++++++++++++------------------------ main.py | 14 +++--- update.py | 14 +++--- views/controller.py | 4 +- views/debug.py | 2 +- views/list.py | 9 ++-- views/search.py | 2 +- web/ipc/methods.py | 12 ++--- 12 files changed, 119 insertions(+), 137 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 2fd4312..4e40999 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -191,8 +191,8 @@ class Basic(commands.Cog): embed = discord.Embed(title=player.get_msg('searchTitle').format(query), description=player.get_msg( 'searchDesc').format(emoji_source(platform), platform, len(tracks[0:10]), query_track), color=settings.embed_color) view = SearchView(tracks=tracks[0:10], lang=player.get_msg) - message = await ctx.send(embed=embed, view=view, ephemeral=True) - view.response = message + view.response = await ctx.send(embed=embed, view=view, ephemeral=True) + await view.wait() if view.values is not None: msg = "" @@ -432,8 +432,7 @@ class Basic(commands.Cog): if player.queue.is_empty: return await nowplay(ctx, player) view = ListView(player=player, author=ctx.author) - message = await ctx.send(embed=view.build_embed(), view=view) - view.response = message + view.response = await ctx.send(embed=view.build_embed(), view=view) @queue.command(name="export", aliases=get_aliases("export")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -520,8 +519,7 @@ class Basic(commands.Cog): return await nowplay(ctx, player) view = ListView(player=player, author=ctx.author, isQueue=False) - message = await ctx.send(embed=view.build_embed(), view=view) - view.response = message + view.response = await ctx.send(embed=view.build_embed(), view=view) @commands.hybrid_command(name="leave", aliases=get_aliases("leave")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -776,8 +774,7 @@ class Basic(commands.Cog): return await ctx.send(get_lang(ctx.guild.id, 'lyricsNotFound'), ephemeral=True) view = LyricsView(name=name, source={_: re.findall(r'.*\n(?:.*\n){,22}', v) for _, v in song.items()}, author=ctx.author) - message = await ctx.send(embed=view.build_embed(), view=view) - view.response = message + view.response = await ctx.send(embed=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.") @@ -839,8 +836,7 @@ class Basic(commands.Cog): return await ctx.send(player.get_msg('noChaptersFound'), ephemeral=True) view = ChapterView(player, chapters, author=ctx.author) - message = await ctx.send(view=view) - view.response = message + view.response = await ctx.send(view=view) @commands.hybrid_command(name="autoplay", aliases=get_aliases("autoplay")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) diff --git a/cogs/playlist.py b/cogs/playlist.py index 5147851..38ec9c7 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -12,7 +12,7 @@ from function import ( update_playlist, update_inbox, get_lang, - playlist_name, + PLAYLIST_NAME, settings, get_aliases, cooldown_check @@ -70,10 +70,10 @@ class Playlists(commands.Cog, name="playlist"): self.description = "This is the Vocard playlist system. You can save your favorites and use Vocard to play on any server." async def playlist_autocomplete(self, interaction: discord.Interaction, current: str) -> list: - playlists = playlist_name.get(str(interaction.user.id), None) + playlists = PLAYLIST_NAME.get(str(interaction.user.id), None) if not playlists: playlists_raw = await get_playlist(interaction.user.id, 'playlist') - playlists = playlist_name[str(interaction.user.id)] = [ + playlists = PLAYLIST_NAME[str(interaction.user.id)] = [ value['name'] for value in playlists_raw.values()] if playlists_raw else [] if current: return [app_commands.Choice(name=p, value=p) for p in playlists if current in p] @@ -87,8 +87,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) - message = await ctx.send(embed=embed, view=view) - view.response = message + view.response = await ctx.send(embed=embed, view=view) @playlist.command(name="play", aliases=get_aliases("play")) @app_commands.describe( @@ -104,7 +103,7 @@ class Playlists(commands.Cog, name="playlist"): return await create_account(ctx) if not result['playlist']: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotFound').format(name), ephemeral=True) - rank, max_p, max_t = await checkroles(ctx.author.id) + rank, max_p, max_t = await checkroles() if result['position'] > max_p: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotAccess'), ephemeral=True) @@ -142,7 +141,7 @@ class Playlists(commands.Cog, name="playlist"): user = await check_playlist(ctx, full=True) if not user: return await create_account(ctx) - rank, max_p, max_t = await checkroles(ctx.author.id) + rank, max_p, max_t = await checkroles() results = [] for index, data in enumerate(user, start=1): @@ -156,7 +155,7 @@ class Playlists(commands.Cog, name="playlist"): if share := playlist['type'] == 'share': playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) if not playlist: - await update_playlist(ctx.author.id, {f"playlist.{data}": 1}, mode=False) + await update_playlist(ctx.author.id, {f"playlist.{data}": 1}, mode="unset") continue if playlist['type'] == 'link': tracks = await search_playlist(playlist['uri'], requester=ctx.author) @@ -196,7 +195,7 @@ class Playlists(commands.Cog, name="playlist"): if len(name) > 10: return await ctx.send(get_lang(ctx.guild.id, 'playlistOverText'), ephemeral=True) - rank, max_p, max_t = await checkroles(ctx.author.id) + rank, max_p, max_t = await checkroles() user = await check_playlist(ctx, full=True) if not user: return await create_account(ctx) @@ -212,9 +211,8 @@ class Playlists(commands.Cog, name="playlist"): if not isinstance(tracks, voicelink.Playlist): return await ctx.send(get_lang(ctx.guild.id, 'playlistNotInvaildUrl'), ephemeral=True) - playlist_name.pop(str(ctx.author.id), None) data = {'uri': link, 'perms': {'read': []}, 'name': name, 'type': 'link'} if link else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} - await update_playlist(ctx.author.id, {f"playlist.{assign_playlistId([data for data in user])}": data}) + await update_playlist(ctx.author.id, {f"playlist.{assign_playlistId([data for data in user])}": data}, update_cache=True) await ctx.send(get_lang(ctx.guild.id, 'playlistCreated').format(name)) @playlist.command(name="delete", aliases=get_aliases("delete")) @@ -232,10 +230,9 @@ class Playlists(commands.Cog, name="playlist"): return await ctx.send(get_lang(ctx.guild.id, 'playlistDeleteError'), ephemeral=True) if result['playlist']['type'] == 'share': - await update_playlist(result['playlist']['user'], {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}, pull=True, mode=False) + await update_playlist(result['playlist']['user'], {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}, mode="pull") - playlist_name.pop(str(ctx.author.id), None) - await update_playlist(ctx.author.id, {f"playlist.{result['id']}": 1}, mode=False) + await update_playlist(ctx.author.id, {f"playlist.{result['id']}": 1}, mode="unset", update_cache=True) return await ctx.send(get_lang(ctx.guild.id, 'playlistRemove').format(result['playlist']['name'])) @playlist.command(name="share", aliases=get_aliases("share")) @@ -300,8 +297,7 @@ class Playlists(commands.Cog, name="playlist"): if not found: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotFound').format(name), ephemeral=True) - playlist_name.pop(str(ctx.author.id), None) - await update_playlist(ctx.author.id, {f'playlist.{id}.name': newname}) + await update_playlist(ctx.author.id, {f'playlist.{id}.name': newname}, update_cache=True) await ctx.send(get_lang(ctx.guild.id, 'playlistRenamed').format(name, newname)) @playlist.command(name="inbox", aliases=get_aliases("inbox")) @@ -316,8 +312,7 @@ class Playlists(commands.Cog, name="playlist"): inbox = user['inbox'].copy() view = InboxView(ctx.author, user['inbox']) - message = await ctx.send(embed=view.build_embed(), view=view, ephemeral=True) - view.response = message + view.response = await ctx.send(embed=view.build_embed(), view=view, ephemeral=True) await view.wait() if inbox == user['inbox']: @@ -325,13 +320,12 @@ class Playlists(commands.Cog, name="playlist"): updateData, dId = {}, {dId for dId in user["playlist"]} for data in view.newplaylist[:(5 - len(user['playlist']))]: addId = assign_playlistId(dId) - await update_playlist(data['sender'], {f"playlist.{data['referId']}.perms.read": ctx.author.id}, push=True) + await update_playlist(data['sender'], {f"playlist.{data['referId']}.perms.read": ctx.author.id}, mode="push") updateData[f'playlist.{addId}'] = {'user': data['sender'], 'referId': data['referId'], 'name': f"Share{data['time'].strftime('%M%S')}", 'type': 'share'} dId.add(addId) - playlist_name.pop(str(ctx.author.id), None) - await update_playlist(ctx.author.id, updateData | {'inbox': view.inbox}) + await update_playlist(ctx.author.id, updateData | {'inbox': view.inbox}, update_cache=True) @playlist.command(name="add", aliases=get_aliases("add")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -350,7 +344,7 @@ class Playlists(commands.Cog, name="playlist"): if result['playlist']['type'] in ['share', 'link']: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotAllow'), ephemeral=True) - rank, max_p, max_t = await checkroles(ctx.author.id) + rank, max_p, max_t = await checkroles() if len(result['playlist']['tracks']) >= max_t: return await ctx.send(get_lang(ctx.guild.id, 'playlistLimitTrack').format(max_t), ephemeral=True) @@ -364,7 +358,7 @@ class Playlists(commands.Cog, name="playlist"): if results[0].is_stream: return await ctx.send(get_lang(ctx.guild.id, 'playlistStream'), ephemeral=True) - await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': results[0].track_id}, push=True) + await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': results[0].track_id}, mode="push") await ctx.send(get_lang(ctx.guild.id, 'playlistAdded').format(results[0].title, ctx.author, result['playlist']['name'])) @playlist.command(name="remove", aliases=get_aliases("remove")) @@ -386,7 +380,7 @@ class Playlists(commands.Cog, name="playlist"): if not 0 < position <= len(result['playlist']['tracks']): return await ctx.send(get_lang(ctx.guild.id, 'playlistPositionNotFound').format(position, name)) - await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}, pull=True, mode=False) + await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}, mode="pull") track = voicelink.decode(result['playlist']['tracks'][position - 1]) await ctx.send(get_lang(ctx.guild.id, 'playlistRemoved').format(track.get("title"), ctx.author, name)) @@ -462,7 +456,7 @@ class Playlists(commands.Cog, name="playlist"): if len(name) > 10: return await ctx.send(get_lang(ctx.guild.id, 'playlistOverText'), ephemeral=True) - rank, max_p, max_t = await checkroles(ctx.author.id) + rank, max_p, max_t = await checkroles() user = await check_playlist(ctx, full=True) if not user: return await create_account(ctx) @@ -479,9 +473,8 @@ class Playlists(commands.Cog, name="playlist"): track_ids = bytes.split(b"\n")[-1] track_ids = track_ids.decode().split(",") - playlist_name.pop(str(ctx.author.id), None) data = {'tracks': track_ids, 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} - await update_playlist(ctx.author.id, {f"playlist.{assign_playlistId([data for data in user])}": data}) + await update_playlist(ctx.author.id, {f"playlist.{assign_playlistId([data for data in user])}": data}, update_cache=True) await ctx.send(get_lang(ctx.guild.id, 'playlistCreated').format(name)) except: diff --git a/cogs/settings.py b/cogs/settings.py index c67cfc9..b2d0db2 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -7,7 +7,7 @@ from typing import Tuple from discord import app_commands from discord.ext import commands from function import ( - langs, + LANGS, update_settings, get_settings, get_lang, @@ -47,8 +47,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) - message = await ctx.send(embed=embed, view=view) - view.response = message + view.response = await ctx.send(embed=embed, view=view) @settings.command(name="prefix", aliases=get_aliases("prefix")) @commands.has_permissions(manage_guild=True) @@ -64,7 +63,7 @@ class Settings(commands.Cog, name="settings"): async def language(self, ctx: commands.Context, language: str): "You can choose your preferred language, the bot message will change to the language you set." language = language.upper() - if language not in langs: + if language not in LANGS: return await ctx.send(get_lang(ctx.guild.id, "languageNotFound")) update_settings(ctx.guild.id, {'lang': language}) @@ -73,8 +72,8 @@ class Settings(commands.Cog, name="settings"): @language.autocomplete('language') async def autocomplete_callback(self, interaction: discord.Interaction, current: str) -> list: if current: - return [app_commands.Choice(name=lang, value=lang) for lang in langs.keys() if current.upper() in lang] - return [app_commands.Choice(name=lang, value=lang) for lang in langs.keys()] + return [app_commands.Choice(name=lang, value=lang) for lang in LANGS.keys() if current.upper() in lang] + return [app_commands.Choice(name=lang, value=lang) for lang in LANGS.keys()] @settings.command(name="dj", aliases=get_aliases("dj")) @commands.has_permissions(manage_guild=True) @@ -86,7 +85,7 @@ class Settings(commands.Cog, name="settings"): if not role: if player: player.settings.pop('dj', None) - update_settings(ctx.guild.id, {'dj': ''}, mode="Unset") + update_settings(ctx.guild.id, {'dj': ''}, mode="unset") else: if player: player.settings['dj'] = role.id @@ -229,8 +228,7 @@ class Settings(commands.Cog, name="settings"): controller_settings = settings.get("default_controller", func.settings.controller) view = EmbedBuilderView(ctx.author, controller_settings.get("embeds").copy()) - message = await ctx.send(embed=view.build_embed(), view=view) - view.response = message + view.response = await ctx.send(embed=view.build_embed(), view=view) @settings.command(name="controllermsg", aliases=get_aliases("controllermsg")) @commands.has_permissions(manage_guild=True) diff --git a/cogs/task.py b/cogs/task.py index 988258f..048cc0b 100644 --- a/cogs/task.py +++ b/cogs/task.py @@ -87,8 +87,8 @@ class Task(commands.Cog): @tasks.loop(hours=12.0) async def cache_cleaner(self): - func.guild_settings.clear() - func.playlist_name.clear() + func.GUILD_SETTINGS.clear() + func.PLAYLIST_NAME.clear() errorFile = func.gen_report() if errorFile: @@ -98,7 +98,7 @@ class Task(commands.Cog): await report_channel.send(content=f"Report Before: ", file=errorFile) except Exception as e: print(f"Report could not be sent (Reason: {e})") - func.error_log.clear() + func.ERROR_LOGS.clear() async def setup(bot: commands.Bot): await bot.add_cog(Task(bot)) diff --git a/function.py b/function.py index be945c5..1e0fb30 100644 --- a/function.py +++ b/function.py @@ -8,12 +8,12 @@ from datetime import datetime from time import strptime from io import BytesIO from pymongo import MongoClient -from typing import Optional, Union +from typing import Optional, Union, Any from addons import Settings, TOKENS -root_dir = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -if not os.path.exists(os.path.join(root_dir, "settings.json")): +if not os.path.exists(os.path.join(ROOT_DIR, "settings.json")): raise Exception("Settings file not set!") #-------------- API Clients -------------- @@ -32,45 +32,46 @@ try: except Exception as e: raise Exception("Not able to connect MongoDB! Reason:", e) -collection = mongodb[tokens.mongodb_name]['Settings'] -Playlist = mongodb[tokens.mongodb_name]['Playlist'] +SETTINGS_DB = mongodb[tokens.mongodb_name]['Settings'] +PLAYLISTS_DB = mongodb[tokens.mongodb_name]['Playlist'] #--------------- Cache Var --------------- settings: Settings -error_log = {} #Stores error that not a Voicelink Exception -langs = {} #Stores all the languages in ./langs -guild_settings = {} #Cache guild language -local_langs = {} #Stores all the localization languages in ./local_langs -playlist_name = {} #Cache the user's playlist name +ERROR_LOGS: dict[int, dict[int, str]] = {} #Stores error that not a Voicelink Exception +LANGS: dict[str, dict[str, str]] = {} #Stores all the languages in ./langs +GUILD_SETTINGS: dict[int, dict[str, Any]] = {} #Cache guild language +LOCAL_LANGS: dict[str, dict[str, str]] = {} #Stores all the localization languages in ./local_langs +PLAYLIST_NAME: dict[str, list[str]] = {} #Cache the user's playlist name #-------------- Vocard Functions -------------- def get_settings(guild_id:int) -> dict: - settings = guild_settings.get(guild_id, None) + settings = GUILD_SETTINGS.get(guild_id, None) if not settings: - settings = collection.find_one({"_id":guild_id}) + settings = SETTINGS_DB.find_one({"_id":guild_id}) if not settings: - collection.insert_one({"_id":guild_id}) - settings = {} - guild_settings[guild_id] = settings + SETTINGS_DB.insert_one({"_id":guild_id}) + + GUILD_SETTINGS[guild_id] = settings or {} return settings -def update_settings(guild_id:int, data: dict, mode="Set") -> None: +def update_settings(guild_id:int, data: dict, mode="set") -> None: settings = get_settings(guild_id) - if mode == "Set": - for key, value in data.items(): - if settings.get(key) != value: - guild_settings[guild_id][key] = value - collection.update_one({"_id":guild_id}, {"$set":data}) - elif mode == "Delete": - for key, value in data.items(): - if settings.get(key) != value: - del guild_settings[guild_id][key] - collection.update_one({"_id":guild_id}, {"$unset":data}) - return + + for key, value in data.items(): + if settings.get(key) != value: + match mode: + case "set": + GUILD_SETTINGS[guild_id][key] = value + case "unset": + GUILD_SETTINGS[guild_id].pop(key) + case _: + return + + SETTINGS_DB.update_one({"_id":guild_id}, {f"${mode}":data}) def open_json(path: str) -> dict: try: - with open(os.path.join(root_dir, path), encoding="utf8") as json_file: + with open(os.path.join(ROOT_DIR, path), encoding="utf8") as json_file: return json.load(json_file) except: return {} @@ -82,15 +83,15 @@ def update_json(path: str, new_data: dict) -> None: data.update(new_data) - with open(os.path.join(root_dir, path), "w") as json_file: + with open(os.path.join(ROOT_DIR, path), "w") as json_file: json.dump(data, json_file, indent=4) def get_lang(guild_id:int, key:str) -> str: lang = get_settings(guild_id).get("lang", "EN") - if lang in langs and not langs[lang]: - langs[lang] = open_json(os.path.join("langs", f"{lang}.json")) + if lang in LANGS and not LANGS[lang]: + LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) - return langs.get(lang, {}).get(key, "Language pack not found!") + return LANGS.get(lang, {}).get(key, "Language pack not found!") def init() -> None: global settings @@ -100,13 +101,13 @@ def init() -> None: settings = Settings(json) def langs_setup() -> None: - for language in os.listdir(os.path.join(root_dir, "langs")): + for language in os.listdir(os.path.join(ROOT_DIR, "langs")): if language.endswith('.json'): - langs[language[:-5]] = {} + LANGS[language[:-5]] = {} - for language in os.listdir(os.path.join(root_dir, "local_langs")): + for language in os.listdir(os.path.join(ROOT_DIR, "local_langs")): if language.endswith('.json'): - local_langs[language[:-5]] = open_json(os.path.join("local_langs", language)) + LOCAL_LANGS[language[:-5]] = open_json(os.path.join("local_langs", language)) return @@ -137,9 +138,9 @@ def emoji_source(emoji:str) -> str: return settings.emoji_source_raw.get(emoji.lower(), "🔗") def gen_report() -> Optional[discord.File]: - if error_log: + if ERROR_LOGS: errorText = "" - for guild_id, error in error_log.items(): + for guild_id, error in ERROR_LOGS.items(): errorText += f"Guild ID: {guild_id}\n" + "-" * 30 + "\n" for index, (key, value) in enumerate(error.items() , start=1): errorText += f"Error No: {index}, Time: {datetime.fromtimestamp(key)}\n" + value + "-" * 30 + "\n\n" @@ -183,20 +184,19 @@ async def create_account(ctx: Union[commands.Context, discord.Interaction]) -> N "‌ ➥ You have the right to immediately stop the services we offer to you\n" "‌ ➥ Please do not abuse our services, such as affecting other users\n", inline=False) if isinstance(ctx, commands.Context): - message = await ctx.reply(embed=embed, view=view, ephemeral=True) + view.response = await ctx.reply(embed=embed, view=view, ephemeral=True) else: - message = await ctx.response.send_message(embed=embed, view=view, ephemeral=True) - - view.response = message + view.response = await ctx.response.send_message(embed=embed, view=view, ephemeral=True) + await view.wait() if view.value: try: - Playlist.insert_one({'_id':author.id, 'playlist': {'200':{'tracks':[],'perms':{ 'read': [], 'write':[], 'remove': []},'name':'Favourite', 'type':'playlist' }},'inbox':[] }) + PLAYLISTS_DB.insert_one({'_id':author.id, 'playlist': {'200':{'tracks':[],'perms':{ 'read': [], 'write':[], 'remove': []},'name':'Favourite', 'type':'playlist' }},'inbox':[] }) except: pass -async def get_playlist(userid:int, dType:str=None, dId:str=None) -> dict: - user = Playlist.find_one({"_id":userid}, {"_id": 0}) +async def get_playlist(user_id:int, dType:str=None, dId:str=None) -> dict: + user = PLAYLISTS_DB.find_one({"_id":user_id}, {"_id": 0}) if not user: return None if dType: @@ -205,21 +205,13 @@ async def get_playlist(userid:int, dType:str=None, dId:str=None) -> dict: return user[dType] return user -async def update_playlist(userid:int, data:dict=None, push=False, pull=False, mode=True) -> None: - if mode is True: - if push: - return Playlist.update_one({"_id":userid}, {"$push": data}) - Playlist.update_one({"_id":userid}, {"$set": data}) - else: - if pull: - return Playlist.update_one({"_id":userid}, {"$pull": data}) - Playlist.update_one({"_id":userid}, {"$unset": data}) - return +async def update_playlist(user_id:int, data:dict, *, mode:str="set", update_cache: bool=False) -> None: + if update_cache: + PLAYLIST_NAME.pop(str(user_id), None) + PLAYLISTS_DB.update_one({"_id":user_id}, {f"${mode}": data}) -async def update_inbox(userid:int, data:dict) -> None: - return Playlist.update_one({"_id":userid}, {"$push":{'inbox':data}}) +async def update_inbox(user_id:int, data:dict) -> None: + return PLAYLISTS_DB.update_one({"_id":user_id}, {"$push":{'inbox':data}}) -async def checkroles(userid:int): - rank, max_p, max_t = 'Normal', 5, 500 - - return rank, max_p, max_t \ No newline at end of file +async def checkroles(): + return 'Normal', 5, 500 \ No newline at end of file diff --git a/main.py b/main.py index beb3263..b5ea72f 100644 --- a/main.py +++ b/main.py @@ -21,8 +21,8 @@ class Translator(discord.app_commands.Translator): print("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) + if str(locale) in func.LOCAL_LANGS: + return func.LOCAL_LANGS[str(locale)].get(string.message, None) return None class Vocard(commands.Bot): @@ -50,7 +50,7 @@ class Vocard(commands.Bot): async def setup_hook(self): func.langs_setup() - for module in os.listdir(func.root_dir + '/cogs'): + for module in os.listdir(func.ROOT_DIR + '/cogs'): if module.endswith('.py'): try: await self.load_extension(f"cogs.{module[:-3]}") @@ -77,7 +77,7 @@ class Vocard(commands.Bot): print("------------------") func.tokens.client_id = self.user.id - func.local_langs.clear() + func.LOCAL_LANGS.clear() async def on_command_error(self, ctx: commands.Context, exception, /) -> None: error = getattr(exception, 'original', exception) @@ -96,9 +96,9 @@ class Vocard(commands.Bot): elif not issubclass(error.__class__, VoicelinkException): error = func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link - if (guildId := ctx.guild.id) not in func.error_log: - func.error_log[guildId] = {} - func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__)) + if (guildId := ctx.guild.id) not in func.ERROR_LOGS: + func.ERROR_LOGS[guildId] = {} + func.ERROR_LOGS[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__)) try: return await ctx.reply(error, ephemeral=True) diff --git a/update.py b/update.py index 538a2ae..55c85b4 100644 --- a/update.py +++ b/update.py @@ -1,8 +1,8 @@ import requests, zipfile, os, shutil, argparse from io import BytesIO -root_dir = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.6.6" +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +__version__ = "v2.6.6b3" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" @@ -65,22 +65,22 @@ def install(response, version): if user_input.lower() in ["y", "yes"]: print("Installing ...") zfile = zipfile.ZipFile(BytesIO(response.content)) - zfile.extractall(root_dir) + zfile.extractall(ROOT_DIR) version = version.replace("v", "") - source_dir = os.path.join(root_dir, f"Vocard-{version}") + source_dir = os.path.join(ROOT_DIR, f"Vocard-{version}") if os.path.exists(source_dir): - for filename in os.listdir(root_dir): + for filename in os.listdir(ROOT_DIR): if filename in IGNORE_FILES + [f"Vocard-{version}"]: continue - filename = os.path.join(root_dir, filename) + filename = os.path.join(ROOT_DIR, filename) if os.path.isdir(filename): shutil.rmtree(filename) else: os.remove(filename) for filename in os.listdir(source_dir): - shutil.move(os.path.join(source_dir, filename), os.path.join(root_dir, filename)) + shutil.move(os.path.join(source_dir, filename), os.path.join(ROOT_DIR, filename)) os.rmdir(source_dir) print(f"{bcolors.OKGREEN}Version {version} installed Successfully! Run `python main.py` to start your bot{bcolors.ENDC}") else: diff --git a/views/controller.py b/views/controller.py index 025666c..a779020 100644 --- a/views/controller.py +++ b/views/controller.py @@ -200,13 +200,13 @@ class Add(ControlButton): user = await get_playlist(interaction.user.id, 'playlist') if not user: return await create_account(interaction) - rank, max_p, max_t = await checkroles(interaction.user.id) + rank, max_p, max_t = await checkroles() if len(user['200']['tracks']) >= max_t: return await self.send(interaction, self.player.get_msg("playlistlimited").format(max_t), ephemeral=True) if track.track_id in user['200']['tracks']: return await self.send(interaction, self.player.get_msg("playlistrepeated"), ephemeral=True) - respond = await update_playlist(interaction.user.id, {'playlist.200.tracks': track.track_id}, push=True) + respond = await update_playlist(interaction.user.id, {'playlist.200.tracks': track.track_id}, mode="push") if respond: await self.send(interaction, self.player.get_msg("playlistAdded").format(track.title, interaction.user.mention, user['200']['name']), ephemeral=True) else: diff --git a/views/debug.py b/views/debug.py index 6624cdb..6aaac52 100644 --- a/views/debug.py +++ b/views/debug.py @@ -184,7 +184,7 @@ class DebugView(discord.ui.View): @discord.ui.button(label='Send Logs', emoji="📥", style=discord.ButtonStyle.red) async def send_error_logs(self, interaction: discord.Interaction, button: discord.ui.Button): - if not function.error_log: + if not function.ERROR_LOGS: return await interaction.response.send_message("Sorry there are not error logs!", ephemeral=True) await interaction.response.send_message(file=function.gen_report(), ephemeral=True) \ No newline at end of file diff --git a/views/list.py b/views/list.py index cbc582a..ec47306 100644 --- a/views/list.py +++ b/views/list.py @@ -31,14 +31,17 @@ class ListView(discord.ui.View): super().__init__(timeout=60) self.player = player - self.name = player.get_msg('queueTitle') if isQueue else player.get_msg('historyTitle') - self.tracks = player.queue.tracks() if isQueue else player.queue.history() + self.name: str = player.get_msg('queueTitle') if isQueue else player.get_msg('historyTitle') + self.tracks: list = player.queue.tracks() if isQueue else player.queue.history() + self.response: discord.Message = None + if not isQueue: self.tracks.reverse() - self.author = author + self.author: discord.Member = author self.page = ceil(len(self.tracks) / 7) self.current_page = 1 + try: self.time = func.time(sum([track.length for track in self.tracks])) except: diff --git a/views/search.py b/views/search.py index a496147..f6ec9ff 100644 --- a/views/search.py +++ b/views/search.py @@ -23,7 +23,7 @@ SOFTWARE. import discord -from function import langs +from function import LANGS class SearchDropdown(discord.ui.Select): def __init__(self, tracks, get_msg): diff --git a/web/ipc/methods.py b/web/ipc/methods.py index 3f598da..4705fcb 100644 --- a/web/ipc/methods.py +++ b/web/ipc/methods.py @@ -279,7 +279,7 @@ async def getPlaylists(member: Member, data: dict): playlist = await func.get_playlist(pList["user"], "playlist", pList["referId"]) if playlist: if member.id not in playlist["perms"]["read"]: - await func.update_playlist(member.id, {f"playlist.{pId}": 1}, mode=False) + await func.update_playlist(member.id, {f"playlist.{pId}": 1}, mode="unset") del playlists[pId] continue @@ -304,9 +304,9 @@ async def removePlaylist(member: Member, data:dict): if isShare: refer_user = data.get("refer_user") - await func.update_playlist(refer_user, {f"playlist.{pId}.perms.read": member.id}, pull=True, mode=False) + await func.update_playlist(refer_user, {f"playlist.{pId}.perms.read": member.id}, mode="pull") - await func.update_playlist(member.id, {f'playlist.{pId}': 1}, mode=False) + await func.update_playlist(member.id, {f'playlist.{pId}': 1}, mode="unset") async def addPlaylistTrack(member: Member, data: dict): track_id = data.get("track_id") @@ -321,14 +321,14 @@ async def addPlaylistTrack(member: Member, data: dict): if playlist["type"] != "playlist": return error_msg(func.get_lang(member.guild.id, 'playlistNotAllow'), user_id=member.id) - rank, max_p, max_t = await func.checkroles(member.id) + rank, max_p, max_t = await func.checkroles() if len(playlist["tracks"]) >= max_t: return error_msg(func.get_lang(member.guild.id, "playlistlimited").format(max_t), user_id=member.id) if track_id in playlist['tracks']: return error_msg(func.get_lang(member.guild.id, "playlistrepeated"), user_id=member.id) - await func.update_playlist(member.id, {f'playlist.{pId}.tracks': track_id}, push=True) + await func.update_playlist(member.id, {f'playlist.{pId}.tracks': track_id}, mode="push") async def removePlaylistTrack(member: Member, data: dict): track_id = data.get("track_id") @@ -336,7 +336,7 @@ async def removePlaylistTrack(member: Member, data: dict): if not track_id or not pId: return - await func.update_playlist(member.id, {f'playlist.{pId}.tracks': track_id }, pull=True, mode=False) + await func.update_playlist(member.id, {f'playlist.{pId}.tracks': track_id }, mode="pull") methods = { "initPlayer": [initPlayer, False], From 4f13023a04631e07e2f0253f0cf2714a15b4db1b Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 6 Sep 2023 09:35:43 +0800 Subject: [PATCH 11/27] remove async on check_roles method --- cogs/playlist.py | 12 ++++++------ function.py | 8 ++++---- views/controller.py | 4 ++-- web/ipc/methods.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index 38ec9c7..bdba67e 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -8,7 +8,7 @@ from function import ( time as ctime, get_playlist, create_account, - checkroles, + check_roles, update_playlist, update_inbox, get_lang, @@ -103,7 +103,7 @@ class Playlists(commands.Cog, name="playlist"): return await create_account(ctx) if not result['playlist']: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotFound').format(name), ephemeral=True) - rank, max_p, max_t = await checkroles() + rank, max_p, max_t = check_roles() if result['position'] > max_p: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotAccess'), ephemeral=True) @@ -141,7 +141,7 @@ class Playlists(commands.Cog, name="playlist"): user = await check_playlist(ctx, full=True) if not user: return await create_account(ctx) - rank, max_p, max_t = await checkroles() + rank, max_p, max_t = check_roles() results = [] for index, data in enumerate(user, start=1): @@ -195,7 +195,7 @@ class Playlists(commands.Cog, name="playlist"): if len(name) > 10: return await ctx.send(get_lang(ctx.guild.id, 'playlistOverText'), ephemeral=True) - rank, max_p, max_t = await checkroles() + rank, max_p, max_t = check_roles() user = await check_playlist(ctx, full=True) if not user: return await create_account(ctx) @@ -344,7 +344,7 @@ class Playlists(commands.Cog, name="playlist"): if result['playlist']['type'] in ['share', 'link']: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotAllow'), ephemeral=True) - rank, max_p, max_t = await checkroles() + rank, max_p, max_t = check_roles() if len(result['playlist']['tracks']) >= max_t: return await ctx.send(get_lang(ctx.guild.id, 'playlistLimitTrack').format(max_t), ephemeral=True) @@ -456,7 +456,7 @@ class Playlists(commands.Cog, name="playlist"): if len(name) > 10: return await ctx.send(get_lang(ctx.guild.id, 'playlistOverText'), ephemeral=True) - rank, max_p, max_t = await checkroles() + rank, max_p, max_t = check_roles() user = await check_playlist(ctx, full=True) if not user: return await create_account(ctx) diff --git a/function.py b/function.py index 1e0fb30..6e09162 100644 --- a/function.py +++ b/function.py @@ -163,6 +163,9 @@ def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]: def get_aliases(name: str) -> list: return settings.aliases_settings.get(name, []) +def check_roles() -> tuple[str, int, int]: + return 'Normal', 5, 500 + async def requests_api(url: str) -> dict: async with aiohttp.ClientSession() as session: resp = await session.get(url) @@ -211,7 +214,4 @@ async def update_playlist(user_id:int, data:dict, *, mode:str="set", update_cach PLAYLISTS_DB.update_one({"_id":user_id}, {f"${mode}": data}) async def update_inbox(user_id:int, data:dict) -> None: - return PLAYLISTS_DB.update_one({"_id":user_id}, {"$push":{'inbox':data}}) - -async def checkroles(): - return 'Normal', 5, 500 \ No newline at end of file + return PLAYLISTS_DB.update_one({"_id":user_id}, {"$push":{'inbox':data}}) \ No newline at end of file diff --git a/views/controller.py b/views/controller.py index a779020..ea8f8b1 100644 --- a/views/controller.py +++ b/views/controller.py @@ -31,7 +31,7 @@ from function import ( get_playlist, update_playlist, create_account, - checkroles + check_roles ) from typing import Dict @@ -200,7 +200,7 @@ class Add(ControlButton): user = await get_playlist(interaction.user.id, 'playlist') if not user: return await create_account(interaction) - rank, max_p, max_t = await checkroles() + rank, max_p, max_t = check_roles() if len(user['200']['tracks']) >= max_t: return await self.send(interaction, self.player.get_msg("playlistlimited").format(max_t), ephemeral=True) diff --git a/web/ipc/methods.py b/web/ipc/methods.py index 4705fcb..74fcc6d 100644 --- a/web/ipc/methods.py +++ b/web/ipc/methods.py @@ -321,7 +321,7 @@ async def addPlaylistTrack(member: Member, data: dict): if playlist["type"] != "playlist": return error_msg(func.get_lang(member.guild.id, 'playlistNotAllow'), user_id=member.id) - rank, max_p, max_t = await func.checkroles() + rank, max_p, max_t = func.check_roles() if len(playlist["tracks"]) >= max_t: return error_msg(func.get_lang(member.guild.id, "playlistlimited").format(max_t), user_id=member.id) From 40e1d343ea0031a076d947868745bd308d697eb4 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:47:38 +0800 Subject: [PATCH 12/27] Updated Mit License --- cogs/basic.py | 23 +++++++++++++++++++++++ cogs/effect.py | 23 +++++++++++++++++++++++ cogs/listeners.py | 23 +++++++++++++++++++++++ cogs/playlist.py | 23 +++++++++++++++++++++++ cogs/settings.py | 23 +++++++++++++++++++++++ cogs/task.py | 23 +++++++++++++++++++++++ function.py | 2 +- views/chapter.py | 2 +- views/controller.py | 2 +- views/debug.py | 2 +- views/help.py | 2 +- views/inbox.py | 2 +- views/link.py | 2 +- views/list.py | 2 +- views/lyrics.py | 2 +- views/playlist.py | 2 +- views/search.py | 2 +- voicelink/enums.py | 2 +- voicelink/exceptions.py | 2 +- voicelink/filters.py | 2 +- voicelink/objects.py | 2 +- voicelink/player.py | 2 +- voicelink/pool.py | 2 +- voicelink/queue.py | 2 +- voicelink/utils.py | 2 +- 25 files changed, 157 insertions(+), 19 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index 4e40999..d6db904 100644 --- a/cogs/basic.py +++ b/cogs/basic.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 voicelink import re diff --git a/cogs/effect.py b/cogs/effect.py index bfc8d21..607d56a 100644 --- a/cogs/effect.py +++ b/cogs/effect.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 voicelink diff --git a/cogs/listeners.py b/cogs/listeners.py index a9e2a7d..dda3daf 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.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 voicelink import asyncio import discord diff --git a/cogs/playlist.py b/cogs/playlist.py index bdba67e..449950f 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.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 voicelink diff --git a/cogs/settings.py b/cogs/settings.py index b2d0db2..4ec9c88 100644 --- a/cogs/settings.py +++ b/cogs/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. +""" + import discord import voicelink import psutil diff --git a/cogs/task.py b/cogs/task.py index 048cc0b..9620189 100644 --- a/cogs/task.py +++ b/cogs/task.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 voicelink import discord import function as func diff --git a/function.py b/function.py index 6e09162..a698f2c 100644 --- a/function.py +++ b/function.py @@ -50,7 +50,7 @@ def get_settings(guild_id:int) -> dict: settings = SETTINGS_DB.find_one({"_id":guild_id}) if not settings: SETTINGS_DB.insert_one({"_id":guild_id}) - + GUILD_SETTINGS[guild_id] = settings or {} return settings diff --git a/views/chapter.py b/views/chapter.py index 325baa6..b64c536 100644 --- a/views/chapter.py +++ b/views/chapter.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/controller.py b/views/controller.py index ea8f8b1..ad50808 100644 --- a/views/controller.py +++ b/views/controller.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/debug.py b/views/debug.py index 6aaac52..0f63aeb 100644 --- a/views/debug.py +++ b/views/debug.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/help.py b/views/help.py index 7fa0dd0..693ba53 100644 --- a/views/help.py +++ b/views/help.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/inbox.py b/views/inbox.py index 49b42dd..70a008e 100644 --- a/views/inbox.py +++ b/views/inbox.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/link.py b/views/link.py index 9f6531d..0ceb3d5 100644 --- a/views/link.py +++ b/views/link.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/list.py b/views/list.py index ec47306..8def253 100644 --- a/views/list.py +++ b/views/list.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/lyrics.py b/views/lyrics.py index 26fbe3c..b93169a 100644 --- a/views/lyrics.py +++ b/views/lyrics.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/playlist.py b/views/playlist.py index 57cd06d..1cc077d 100644 --- a/views/playlist.py +++ b/views/playlist.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/views/search.py b/views/search.py index f6ec9ff..6d7b344 100644 --- a/views/search.py +++ b/views/search.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/enums.py b/voicelink/enums.py index d9e4a00..e459243 100644 --- a/voicelink/enums.py +++ b/voicelink/enums.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/exceptions.py b/voicelink/exceptions.py index b38a0ac..0c7fca8 100644 --- a/voicelink/exceptions.py +++ b/voicelink/exceptions.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/filters.py b/voicelink/filters.py index 75829d0..3ac9bc5 100644 --- a/voicelink/filters.py +++ b/voicelink/filters.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/objects.py b/voicelink/objects.py index 9a3bcd4..b42ae81 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/player.py b/voicelink/player.py index 38875ca..8c7eb7f 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/pool.py b/voicelink/pool.py index 030499d..761f821 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/queue.py b/voicelink/queue.py index 3133411..1f1a017 100644 --- a/voicelink/queue.py +++ b/voicelink/queue.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 diff --git a/voicelink/utils.py b/voicelink/utils.py index 0a692a0..1b0025e 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 From 0ba96704f00efb13b238fa30273e532190be5323 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:59:57 +0800 Subject: [PATCH 13/27] Supported Lavalink v4 beta3 --- cogs/listeners.py | 4 ++-- voicelink/__init__.py | 4 ++-- voicelink/events.py | 13 ++++++------- voicelink/formatter.py | 3 ++- voicelink/objects.py | 24 +++++++++++++----------- voicelink/player.py | 2 +- voicelink/pool.py | 25 ++++++++++++++++++------- 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/cogs/listeners.py b/cogs/listeners.py index dda3daf..0bcee7a 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -59,10 +59,10 @@ class Listeners(commands.Cog): await player.do_next() @commands.Cog.listener() - async def on_voicelink_track_exception(self, player: voicelink.Player, track, _): + async def on_voicelink_track_exception(self, player: voicelink.Player, track, error: dict): try: player._track_is_stuck = True - await player.context.send(f"{_} Please wait for 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/voicelink/__init__.py b/voicelink/__init__.py index f82f387..d48084f 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 @@ -21,7 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -__version__ = "1.3" +__version__ = "1.4" __author__ = 'Vocard Development, Choco' __license__ = "MIT" __copyright__ = "Copyright 2023 (c) Vocard Development, Choco" diff --git a/voicelink/events.py b/voicelink/events.py index cb9b83c..66f0caf 100644 --- a/voicelink/events.py +++ b/voicelink/events.py @@ -1,6 +1,6 @@ """MIT License -Copyright (c) 2023 Vocard Development +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 @@ -108,12 +108,11 @@ class TrackExceptionEvent(VoicelinkEvent): def __init__(self, data: dict, player): self.player = player self.track = self.player._ending_track - if data.get('error'): - # User is running Lavalink <= 3.3 - self.exception: str = data["error"] - else: - # User is running Lavalink >=3.4 - self.exception: str = data["exception"] + self.exception: dict = data.get("exception", { + "severity": "", + "message": "", + "cause": "" + }) # on_voicelink_track_exception(player, track, error) self.handler_args = self.player, self.track, self.exception diff --git a/voicelink/formatter.py b/voicelink/formatter.py index ee7f676..8b4fde5 100644 --- a/voicelink/formatter.py +++ b/voicelink/formatter.py @@ -180,7 +180,8 @@ class TrackDecoder: "identifier": body_reader.read_utf(), "is_stream": body_reader.read_bool(), "uri": body_reader.read_optional_utf(), - "thumbnail": None if version != 0 else body_reader.read_optional_utf(), + "thumbnail": None if version not in [0, 3] else body_reader.read_optional_utf(), + "isrc": None if version != 3 else body_reader.read_optional_utf(), "sourceName": body_reader.read_utf(), "position": body_reader.read_long() } diff --git a/voicelink/objects.py b/voicelink/objects.py index b42ae81..4cba071 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -42,7 +42,7 @@ class Track: """ __slots__ = ( - "track_id", + "_track_id", "info", "identifier", "title", @@ -72,7 +72,7 @@ class Track: search_type: SearchType = SearchType.ytsearch, spotify_track = None, ): - self.track_id: str = track_id + self._track_id: Optional[str] = track_id self.info: dict = info self.identifier: str = info.get("identifier") @@ -80,7 +80,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 = True if self.source == "spotify" else False + self.spotify: bool = self.source == "spotify" if self.spotify: self.artist_id: Optional[list] = info.get("artist_id") @@ -91,8 +91,9 @@ class Track: self.thumbnail: str = None self.emoji: str = emoji_source(self.source) - if info.get("thumbnail"): - self.thumbnail = info.get("thumbnail") + if artworkUrl := info.get("artworkUrl"): + self.thumbnail = artworkUrl + elif YOUTUBE_REGEX.match(self.uri): self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/hqdefault.jpg" @@ -103,9 +104,6 @@ class Track: self.is_seekable: bool = info.get("isSeekable", True) self.position: int = info.get("position", 0) - if not track_id: - self.track_id = encode(self) - def __eq__(self, other) -> bool: if not isinstance(other, Track): return False @@ -124,9 +122,13 @@ class Track: "info": self.info, "thumbnail": self.thumbnail } - - def encode(self) -> bytes: - return encode(self) + + @property + def track_id(self) -> str: + if not self._track_id: + self._track_id = encode(self) + + return self._track_id @property def formatted_length(self) -> str: diff --git a/voicelink/player.py b/voicelink/player.py index 8c7eb7f..0f6d0d4 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -303,7 +303,7 @@ class Player(VoiceProtocol): event_type = data.get("type") event: VoicelinkEvent = getattr(events, event_type)(data, self) - if isinstance(event, TrackEndEvent) and event.reason != "REPLACED": + if isinstance(event, TrackEndEvent) and event.reason != "replaced": self._current = None event.dispatch(self._bot) diff --git a/voicelink/pool.py b/voicelink/pool.py index 761f821..15cefa9 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -66,7 +66,7 @@ URL_REGEX = re.compile( r"https?://(?:www\.)?.+" ) -NODE_VERSION = "v3" +NODE_VERSION = "v4" CALL_METHOD = ["PATCH", "DELETE"] def exception_catch_callback(task): @@ -461,33 +461,44 @@ class Node: ) as response: data = await response.json() + print(data) load_type = data.get("loadType") if not load_type: raise TrackLoadError("There was an error while trying to load this track.") - elif load_type == "LOAD_FAILED": + elif load_type == "error": exception = data["exception"] raise TrackLoadError(f"{exception['message']} [{exception['severity']}]") - elif load_type == "NO_MATCHES": + elif load_type == "empty": return None - elif load_type == "PLAYLIST_LOADED": + elif load_type == "playlist": return Playlist( playlist_info=data["playlistInfo"], tracks=data["tracks"], requester=requester ) - elif load_type == "SEARCH_RESULT" or load_type == "TRACK_LOADED": + elif load_type == "search": return [ Track( - track_id=track["track"], + track_id=track["encoded"], + info=track["info"], + requester=requester + ) + for track in data["data"] + ] + + elif load_type == "track": + track = data["data"] + return [ + Track( + track_id=track["encoded"], info=track["info"], requester=requester ) - for track in data["tracks"] ] class NodePool: From 58b33d41c7c6641550f580375e42b13b0d7085ff Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:00:56 +0800 Subject: [PATCH 14/27] Update update.py --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index 55c85b4..6902e7f 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.6b3" +__version__ = "v2.6.6b4" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" From c8fd1cca1966f2df977fec6d940a87968ffbd863 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:19:32 +0800 Subject: [PATCH 15/27] Fixed issue with being able to play playlists --- voicelink/objects.py | 2 +- voicelink/pool.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/voicelink/objects.py b/voicelink/objects.py index 4cba071..60b0981 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -175,7 +175,7 @@ class Playlist: self._uri = self.spotify_playlist.uri else: self.tracks = [ - Track(track_id=track["track"], info=track["info"], requester=requester) + Track(track_id=track["encoded"], info=track["info"], requester=requester) for track in self.tracks_raw ] self._thumbnail = None diff --git a/voicelink/pool.py b/voicelink/pool.py index 15cefa9..703304d 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -461,7 +461,6 @@ class Node: ) as response: data = await response.json() - print(data) load_type = data.get("loadType") if not load_type: @@ -475,8 +474,10 @@ class Node: return None elif load_type == "playlist": + data = data.get("data") + return Playlist( - playlist_info=data["playlistInfo"], + playlist_info=data["info"], tracks=data["tracks"], requester=requester ) From e7f4118470bb4e04da7a5995d6242edb55f0886d Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:20:07 +0800 Subject: [PATCH 16/27] General clean code --- cogs/playlist.py | 2 +- views/list.py | 18 +++++--- views/playlist.py | 112 ++++++++++++++++++++++++++-------------------- views/search.py | 27 +++++------ 4 files changed, 89 insertions(+), 70 deletions(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index 449950f..0d3cb7c 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -191,7 +191,7 @@ class Playlists(commands.Cog, name="playlist"): init.append(dt) playlist['tracks'] = init results.append({'emoji': ('🔒' if max_p < index else ('🤝' if share else '❤️')), 'id': data, 'time': ctime(time), 'name': user[data]['name'], 'tracks': playlist['tracks'], 'perms': playlist['perms'], 'owner': user[data].get('user', None), 'type': user[data]['type']}) - + except Exception as e: results.append({'emoji': '⛔', 'id': data, 'time': '00:00', 'name': 'Error', 'tracks': [], 'type': 'error'}) diff --git a/views/list.py b/views/list.py index 8def253..3fcf184 100644 --- a/views/list.py +++ b/views/list.py @@ -82,26 +82,30 @@ class ListView(discord.ui.View): async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button): if self.current_page != 1: self.current_page = 1 - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(label='Back', style=discord.ButtonStyle.blurple) async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button): if self.current_page > 1: self.current_page -= 1 - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(label='Next', style=discord.ButtonStyle.blurple) async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): if self.current_page < self.page: self.current_page += 1 - await interaction.response.edit_message(embed=self.build_embed()) + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() @discord.ui.button(label='>>', style=discord.ButtonStyle.grey) async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button): if self.current_page != self.page: self.current_page = self.page - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red) async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button): await self.response.delete() diff --git a/views/playlist.py b/views/playlist.py index 1cc077d..c90b95e 100644 --- a/views/playlist.py +++ b/views/playlist.py @@ -24,35 +24,40 @@ SOFTWARE. import discord import function as func +from voicelink import Track from math import ceil from tldextract import extract +from typing import Any class Select_playlist(discord.ui.Select): def __init__(self, results): - options = [discord.SelectOption(emoji='🌎', label='All Playlist')] - for index, playlist in enumerate(results, start=1): - if playlist['type'] != 'error': - options.append(discord.SelectOption(emoji=playlist['emoji'], label=f'{index}. {playlist["name"]}', description=f"{playlist['time']} · {playlist['type']}")) + self.view: PlaylistView super().__init__( placeholder="Select a playlist to view ..", - options=options + options=[discord.SelectOption(emoji='🌎', label='All Playlist')] + + [ + discord.SelectOption(emoji=playlist['emoji'], label=f'{index}. {playlist["name"]}', description=f"{playlist['time']} · {playlist['type']}") + for index, playlist in enumerate(results, start=1) if playlist['type'] != 'error' + ] ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> None: if self.values[0] == 'All Playlist': self.view.current = None return await interaction.response.edit_message(embed=self.view.viewEmbed) + self.view.current = self.view.results[int(self.values[0].split(". ")[0]) - 1] self.view.page = ceil(len(self.view.current['tracks']) / 7) self.view.current_page = 1 await interaction.response.edit_message(embed=self.view.build_embed()) class agree(discord.ui.Button): - def __init__(self): + def __init__(self) -> None: + self.view: CreateView super().__init__(label="Agree", style=discord.ButtonStyle.green) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> None: self.label = "Created" self.disabled = True self.style=discord.ButtonStyle.primary @@ -62,57 +67,61 @@ class agree(discord.ui.Button): self.view.stop() class PlaylistView(discord.ui.View): - def __init__(self, viewEmbed, results, author): + def __init__( + self, + viewEmbed: discord.Embed, + results: list[dict[str, Any]], + author: discord.Message + ) -> None: super().__init__(timeout=60) - self.viewEmbed = viewEmbed - self.results = results - self.author = author - self.guildID = author.guild.id - self.response = None - self.current = None - self.page = 0 - self.current_page = 1 + self.viewEmbed: discord.Embed = viewEmbed + self.results: list[dict[str, Any]] = results + self.author: discord.Member = author + self.response: discord.Message = None + + self.current: dict[str, Any] = None + self.page: int = 0 + self.current_page: int = 1 self.add_item(Select_playlist(results)) - async def interaction_check(self, interaction): - if interaction.user == self.author: - return True - return False + async def interaction_check(self, interaction: discord.Interaction) -> bool: + return interaction.user == self.author - async def on_error(self, error, item, interaction): + async def on_error(self, error, item, interaction) -> None: return - def build_embed(self): - offset = self.current_page * 7 - tracks = self.current['tracks'][(offset-7):offset] + def build_embed(self) -> discord.Embed: + offset: int = self.current_page * 7 + tracks: list[Track] = self.current['tracks'][(offset-7):offset] + guild_id = self.author.id - embed = discord.Embed(title=func.get_lang(self.guildID, 'playlistView'), color=func.settings.embed_color) + embed = discord.Embed(title=func.get_lang(guild_id, 'playlistView'), color=func.settings.embed_color) - embed.description= func.get_lang(self.guildID, 'playlistViewDesc').format(self.current['name'], self.current['id'], len(self.current['tracks']), owner if (owner := self.current.get('owner')) else f"{self.author.id} (You)", self.current['type']) + embed.description= func.get_lang(guild_id, 'playlistViewDesc').format(self.current['name'], self.current['id'], len(self.current['tracks']), owner if (owner := self.current.get('owner')) else f"{self.author.id} (You)", self.current['type']) perms = self.current['perms'] - permsStr = func.get_lang(self.guildID, 'settingsPermTitle') + permsStr = func.get_lang(guild_id, 'settingsPermTitle') if self.current['type'] == 'share': - embed.add_field(name=permsStr, value=func.get_lang(self.guildID, 'playlistViewPermsValue').format('✓' if 'write' in perms and self.author.id in perms['write'] else '✘', '✓' if 'remove' in perms and self.author.id in perms['remove'] else '✘')) + embed.add_field(name=permsStr, value=func.get_lang(guild_id, 'playlistViewPermsValue').format('✓' if 'write' in perms and self.author.id in perms['write'] else '✘', '✓' if 'remove' in perms and self.author.id in perms['remove'] else '✘')) else: - embed.add_field(name=permsStr, value=func.get_lang(self.guildID, 'playlistViewPermsValue2').format(', '.join(f'<@{user}>' for user in perms['read']))) + embed.add_field(name=permsStr, value=func.get_lang(guild_id, 'playlistViewPermsValue2').format(', '.join(f'<@{user}>' for user in perms['read']))) - trackStr = func.get_lang(self.guildID, 'playlistViewTrack') + trackStr = func.get_lang(guild_id, 'playlistViewTrack') if tracks: if self.current.get("type") == "playlist": embed.add_field(name=trackStr, value="\n".join(f"{func.emoji_source(track['sourceName'])} `{index}.` `[{func.time(track['length'])}]` **{track['title'][:30]}**" for index, track in enumerate(tracks, start=offset - 6)), inline=False) else: embed.add_field(name=trackStr, value='\n'.join(f"{func.emoji_source(extract(track.info['uri']).domain)} `{index}.` `[{func.time(track.length)}]` **{track.title[:30]}** " for index, track in enumerate(tracks, start=offset - 6)), inline=False) else: - embed.add_field(name=trackStr, value=func.get_lang(self.guildID, 'playlistNoTrack').format(self.current['name']), inline=False) + embed.add_field(name=trackStr, value=func.get_lang(guild_id, 'playlistNoTrack').format(self.current['name']), inline=False) - embed.set_footer(text=func.get_lang(self.guildID, 'playlistViewPage').format(self.current_page, self.page, self.current['time'])) + embed.set_footer(text=func.get_lang(guild_id, 'playlistViewPage').format(self.current_page, self.page, self.current['time'])) return embed - async def on_timeout(self): + async def on_timeout(self) -> None: for child in self.children: child.disabled = True try: @@ -121,51 +130,56 @@ class PlaylistView(discord.ui.View): pass @discord.ui.button(label='<<', style=discord.ButtonStyle.grey) - async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if not self.current: return if self.current_page != 1: self.current_page = 1 - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(label='Back', style=discord.ButtonStyle.blurple) - async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if not self.current: return if self.current_page > 1: self.current_page -= 1 - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(label='Next', style=discord.ButtonStyle.blurple) - async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if not self.current: return if self.current_page < self.page: self.current_page += 1 - await interaction.response.edit_message(embed=self.build_embed()) + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() @discord.ui.button(label='>>', style=discord.ButtonStyle.grey) - async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if not self.current: return if self.current_page != self.page: self.current_page = self.page - await interaction.response.edit_message(embed=self.build_embed()) + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() @discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red) - async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: await self.response.delete() self.stop() class CreateView(discord.ui.View): - def __init__(self): + def __init__(self) -> None: super().__init__(timeout=20) - self.value = None - self.response = None + self.value: bool = None + self.response: discord.Message = None + self.add_item(agree()) self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.settings.invite_link)) - async def on_timeout(self): + async def on_timeout(self) -> None: for child in self.children: child.disabled = True try: diff --git a/views/search.py b/views/search.py index 6d7b344..720021b 100644 --- a/views/search.py +++ b/views/search.py @@ -23,23 +23,23 @@ SOFTWARE. import discord -from function import LANGS +from voicelink import Track class SearchDropdown(discord.ui.Select): - def __init__(self, tracks, get_msg): - self.tracks = tracks - self.get_msg = get_msg - options = [] - for index, track in enumerate(self.tracks, start=1): - options.append(discord.SelectOption(label=f"{index}. {track.title[:50]}", description=f"{track.author[:50]} · {track.formatted_length}")) - + def __init__(self, tracks: list[Track], get_msg: callable) -> None: + self.view: SearchView + self.get_msg: callable = get_msg + super().__init__( placeholder=get_msg('searchWait'), min_values=1, max_values=len(tracks), - options=options + options=[ + discord.SelectOption(label=f"{i}. {track.title[:50]}", description=f"{track.author[:50]} · {track.formatted_length}") + for i, track in enumerate(tracks, start=1) + ] ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> None: self.disabled = True self.placeholder = self.get_msg('searchSuccess') await interaction.response.edit_message(view=self.view) @@ -47,10 +47,11 @@ class SearchDropdown(discord.ui.Select): self.view.stop() class SearchView(discord.ui.View): - def __init__(self, tracks, lang): + def __init__(self, tracks: list[Track], lang: callable) -> None: super().__init__(timeout=60) - self.response = None - self.values = None + + self.response: discord.Message = None + self.values: list[str] = None self.add_item(SearchDropdown(tracks, lang)) async def on_error(self, error, item, interaction): From e2e6c0567eefa030fbb9bb3368e3e588a22d5729 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Mon, 11 Sep 2023 20:02:08 +0800 Subject: [PATCH 17/27] General clean code --- addons/lyrics.py | 12 ++++++--- cogs/basic.py | 6 ++--- requirements.txt | 2 +- update.py | 2 +- views/chapter.py | 45 +++++++++++++++++-------------- views/debug.py | 17 ++++++------ views/embedBuilder.py | 6 ++--- views/help.py | 61 +++++++++++++++++++++-------------------- views/inbox.py | 27 +++++++++++-------- views/list.py | 56 +++++++++++++++++++++----------------- views/lyrics.py | 63 ++++++++++++++++++++++--------------------- views/playlist.py | 6 +++-- voicelink/player.py | 4 +-- 13 files changed, 166 insertions(+), 141 deletions(-) diff --git a/addons/lyrics.py b/addons/lyrics.py index c24d702..10e4cac 100644 --- a/addons/lyrics.py +++ b/addons/lyrics.py @@ -1,6 +1,7 @@ import aiohttp, random, bs4, re import function as func +from abc import ABC, abstractmethod from urllib.parse import quote from math import floor from importlib import import_module @@ -46,7 +47,12 @@ Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.9 (KHTM Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/ Safari/530.6 Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5''' -class A_ZLyrics(): +class LyricsPlatform(ABC): + @abstractmethod + async def getLyrics(): + ... + +class A_ZLyrics(LyricsPlatform): async def get(self, url): try: async with aiohttp.ClientSession() as session: @@ -166,7 +172,7 @@ class A_ZLyrics(): return text -class Genius(): +class Genius(LyricsPlatform): def __init__(self) -> None: self.module = import_module("lyricsgenius") self.genius = self.module.Genius(func.tokens.genius_token) @@ -178,7 +184,7 @@ class Genius(): return {"default": song.lyrics} -lyricsPlatform = { +lyricsPlatform: dict[str, LyricsPlatform] = { "a_zlyrics": A_ZLyrics, "genius": Genius } \ No newline at end of file diff --git a/cogs/basic.py b/cogs/basic.py index d6db904..dfc0b63 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -59,8 +59,8 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player): upnext = "\n".join(f"`{index}.` `[{track.formatted_length}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate(player.queue.tracks()[:2], start=2)) embed = discord.Embed(description=player.get_msg('nowplayingDesc').format(track.title), color=settings.embed_color) embed.set_author( - name=track.requester if track.requester else ctx.bot, - icon_url=track.requester.display_avatar.url if track.requester else ctx.me.display_avatar.url + name=track.requester, + icon_url=track.requester.display_avatar.url ) embed.set_thumbnail(url=track.thumbnail) @@ -792,7 +792,7 @@ class Basic(commands.Cog): name = player.current.title + " " + player.current.author await ctx.defer() - song = await lyricsPlatform.get(settings.lyrics_platform)().getLyrics(name) + song: dict[str, str] = await lyricsPlatform.get(settings.lyrics_platform)().getLyrics(name) if not song: return await ctx.send(get_lang(ctx.guild.id, 'lyricsNotFound'), ephemeral=True) diff --git a/requirements.txt b/requirements.txt index 86938fa..ce159b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ discord.py==2.3.2 -pymongo==4.1.1 +pymongo==4.5.0 dnspython==2.2.1 tldextract==3.2.1 validators==0.18.2 diff --git a/update.py b/update.py index 6902e7f..b8432b3 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.6b4" +__version__ = "v2.6.6b5" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" diff --git a/views/chapter.py b/views/chapter.py index b64c536..ad67d16 100644 --- a/views/chapter.py +++ b/views/chapter.py @@ -20,28 +20,30 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations import discord from function import formatTime +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from voicelink import Player class Dropdown(discord.ui.Select): - def __init__(self, player, chapters): + def __init__(self, player: Player, chapters): + self.view: ChapterView - self.player = player - self.chapters = chapters - self.current = player.current.uri - - options = [ - discord.SelectOption(label=f"{index}. {title[:30]}", - description=time) - for index, (time, title) in enumerate(self.chapters, start=1) - ] + self.player: Player = player + self.chapters: list[str] = chapters + self.current: str = player.current.uri super().__init__( placeholder=self.player.get_msg('chaptersDropdown'), min_values=1, max_values=1, - options=options[:25], + options=[ + discord.SelectOption(label=f"{index}. {title[:30]}", description=time) + for index, (time, title) in enumerate(self.chapters, start=1) + ][:25] ) async def callback(self, interaction: discord.Interaction): @@ -55,13 +57,18 @@ class Dropdown(discord.ui.Select): await interaction.response.send_message(self.player.get_msg('seek').format(formatTime(position))) class ChapterView(discord.ui.View): - def __init__(self, player, chapters, author): + def __init__( + self, + player: Player, + chapters: list[str], + author: discord.Member + ) -> None: super().__init__(timeout=180) - self.player = player - self.chapters = chapters - self.author = author - self.response = None + self.player: Player = player + self.chapters: list[str] = chapters + self.author: discord.Member = author + self.response: discord.Message = None self.add_item(Dropdown(player, chapters)) async def on_error(self, error: Exception, item, interaction) -> None: @@ -78,7 +85,5 @@ class ChapterView(discord.ui.View): async def on_timeout(self) -> None: await self.stop_view() - async def interaction_check(self, interaction): - if interaction.user == self.author: - return True - return False \ No newline at end of file + async def interaction_check(self, interaction: discord.Interaction): + return interaction.user == self.author \ No newline at end of file diff --git a/views/debug.py b/views/debug.py index 0f63aeb..0f9b14a 100644 --- a/views/debug.py +++ b/views/debug.py @@ -33,7 +33,7 @@ from discord.ext import commands class ExceuteModal(discord.ui.Modal): def __init__(self, code: str, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.code = code + self.code: str = code self.add_item( discord.ui.TextInput( @@ -51,24 +51,23 @@ class ExceuteModal(discord.ui.Modal): class CogsDropdown(discord.ui.Select): def __init__(self, bot: commands.Bot): - self.bot = bot - - options = [discord.SelectOption(label="All", description="All the cogs")] - - for name, cog in bot.cogs.items(): - options.append(discord.SelectOption(label=name.capitalize(), description=cog.description[:50])) + self.bot: commands.Bot = bot super().__init__( placeholder="Select a cog to reload...", min_values=1, max_values=1, - options=options, + options=[discord.SelectOption(label="All", description="All the cogs")] + + [ + discord.SelectOption(label=name.capitalize(), description=cog.description[:50]) + for name, cog in bot.cogs.items() + ], ) async def callback(self, interaction: discord.Interaction) -> None: selected = self.values[0].lower() try: if selected == "all": - for name in self.bot.cogs.copy().keys(): + for name in self.bot.cogs.keys(): await self.bot.reload_extension(f"cogs.{name.lower()}") else: await self.bot.reload_extension(f"cogs.{selected}") diff --git a/views/embedBuilder.py b/views/embedBuilder.py index 449f7b9..aa9e07c 100644 --- a/views/embedBuilder.py +++ b/views/embedBuilder.py @@ -59,10 +59,8 @@ class EmbedBuilderView(discord.ui.View): except: pass - async def interaction_check(self, interaction): - if interaction.user == self.author: - return True - return False + async def interaction_check(self, interaction: discord.Interaction): + return interaction.user == self.author @discord.ui.button(label="Edit Content", style=discord.ButtonStyle.blurple) async def edit_content(self, interaction: discord.Interaction, button: discord.ui.Button): diff --git a/views/help.py b/views/help.py index 693ba53..fd485cb 100644 --- a/views/help.py +++ b/views/help.py @@ -28,34 +28,33 @@ import function as func class HelpDropdown(discord.ui.Select): def __init__(self, categorys:list): - options = [ - discord.SelectOption(emoji="🆕", label="News", description="View new updates of Vocard."), - discord.SelectOption(emoji="🕹️", label="Tutorial", description="How to use Vocard."), - ] - cog_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] - for category, emoji in zip(categorys, cog_emojis): - options.append(discord.SelectOption(emoji=emoji, - label=f"{category} Commands", - description=f"This is {category.lower()} Category.")) - + self.view: HelpView + super().__init__( placeholder="Select Category!", min_values=1, max_values=1, - options=options, custom_id="select" + options=[ + discord.SelectOption(emoji="🆕", label="News", description="View new updates of Vocard."), + 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️⃣"]) + ], + custom_id="select" ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> None: embed = self.view.build_embed(self.values[0].split(" ")[0]) await interaction.response.edit_message(embed=embed) class HelpView(discord.ui.View): - def __init__(self, bot: commands.Bot, author: discord.Member): + def __init__(self, bot: commands.Bot, author: discord.Member) -> None: super().__init__(timeout=60) - self.author = author - self.bot = bot - self.response = None - self.categorys = [ name.capitalize() for name, cog in bot.cogs.items() if len([c for c in cog.walk_commands()]) ] + 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.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.settings.invite_link)) self.add_item(discord.ui.Button(label='Invite', emoji=':invite:915152589056790589', url='https://discord.com/oauth2/authorize?client_id={}&permissions=2184260928&scope=bot%20applications.commands'.format(func.tokens.client_id))) @@ -63,10 +62,10 @@ class HelpView(discord.ui.View): self.add_item(discord.ui.Button(label='Donate', emoji=':patreon:913397909024800878', url='https://www.patreon.com/Vocard')) self.add_item(HelpDropdown(self.categorys)) - async def on_error(self, error, item, interaction): + async def on_error(self, error, item, interaction) -> None: return - async def on_timeout(self): + async def on_timeout(self) -> None: for child in self.children: if child.custom_id == "select": child.disabled = True @@ -75,22 +74,20 @@ class HelpView(discord.ui.View): except: pass - async def interaction_check(self, interaction): - if interaction.user == self.author: - return True - return False + async def interaction_check(self, interaction: discord.Interaction) -> None: + return interaction.user == self.author - def build_embed(self, category: str): + def build_embed(self, category: str) -> discord.Embed: category = category.lower() if category == "news": embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=func.settings.embed_color) - - embed.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))), - inline=True) + 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))), + inline=True + ) update = "Vocard is a simple music bot. It leads to a comfortable experience which is user-friendly, It supports YouTube, Soundcloud, Spotify, Twitch and more!" - embed.add_field(name="📰 Information:", value=update, inline=True) embed.add_field(name="Get Started", value="```Join a voice channel and /play {Song/URL} a song. (Names, Youtube Video Links or Playlist links or Spotify links are supported on Vocard)```", inline=False) @@ -107,7 +104,9 @@ class HelpView(discord.ui.View): commands = [command for command in cog.walk_commands()] embed.description = cog.description - embed.add_field(name=f"{category} Commands: [{len(commands)}]", - value="```{}```".format("".join(f"/{command.qualified_name}\n" for command in commands if not command.qualified_name == cog.qualified_name))) + embed.add_field( + name=f"{category} Commands: [{len(commands)}]", + value="```{}```".format("".join(f"/{command.qualified_name}\n" for command in commands if not command.qualified_name == cog.qualified_name)) + ) return embed \ No newline at end of file diff --git a/views/inbox.py b/views/inbox.py index 70a008e..d9c8fd9 100644 --- a/views/inbox.py +++ b/views/inbox.py @@ -24,8 +24,11 @@ SOFTWARE. import discord import function as func +from typing import Any + class Select_message(discord.ui.Select): def __init__(self, inbox): + self.view: InboxView options = [discord.SelectOption(label=f"{index}. {mail['title'][:50]}", description=mail['type'], emoji='✉️' if mail['type'] == 'invite' else '📢') for index, mail in enumerate(inbox, start=1) ] super().__init__( @@ -38,25 +41,27 @@ class Select_message(discord.ui.Select): await self.view.button_change(interaction) class InboxView(discord.ui.View): - def __init__(self, author, inbox): + def __init__(self, author: discord.Member, inbox: list[dict[str, Any]]): super().__init__(timeout=60) - self.author: discord.Member = author - self.inbox = inbox - self.response = None + self.inbox: list[dict[str, Any]] = inbox self.newplaylist = [] + self.author: discord.Member = author + self.response: discord.Message = None self.current = None + self.add_item(Select_message(inbox)) async def interaction_check(self, interaction: discord.Interaction): - if interaction.user == self.author: - return True - return False + return interaction.user == self.author + + def build_embed(self) -> discord.Embed: + embed=discord.Embed( + title=f"📭 All {self.author.name}'s Inbox", + description=f'Max Messages: {len(self.inbox)}/10' + '```%0s %2s %20s\n' % (" ", "ID:", "Title:") + '\n'.join('%0s %2s. %35s'% ('✉️' if mail['type'] == 'invite' else '📢', index, mail['title'][:35] + "...") for index, mail in enumerate(self.inbox, start=1)) + '```', + color=func.settings.embed_color + ) - def build_embed(self): - embed=discord.Embed(title=f"📭 All {self.author.name}'s Inbox", - description=f'Max Messages: {len(self.inbox)}/10' + '```%0s %2s %20s\n' % (" ", "ID:", "Title:") + '\n'.join('%0s %2s. %35s'% ('✉️' if mail['type'] == 'invite' else '📢', index, mail['title'][:35] + "...") for index, mail in enumerate(self.inbox, start=1)) + '```', - color=func.settings.embed_color) if self.current: embed.add_field(name="Message Info:", value=f"```{self.current['description']}\nSender ID: {self.current['sender']}\nPlaylist ID: {self.current['referId']}\nInvite Time: {self.current['time'].strftime('%d-%m %H:%M:%S')}```") return embed diff --git a/views/list.py b/views/list.py index 3fcf184..9b60eb0 100644 --- a/views/list.py +++ b/views/list.py @@ -25,29 +25,38 @@ import discord import function as func from math import ceil +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from voicelink import Player, Track class ListView(discord.ui.View): - def __init__(self, player, author, isQueue = True): + def __init__( + self, + player: "Player", + author: discord.Member, + isQueue = True + ) -> None: super().__init__(timeout=60) - self.player = player + self.player: Player = player self.name: str = player.get_msg('queueTitle') if isQueue else player.get_msg('historyTitle') - self.tracks: list = player.queue.tracks() if isQueue else player.queue.history() + self.tracks: list[Track] = player.queue.tracks() if isQueue else player.queue.history() self.response: discord.Message = None if not isQueue: self.tracks.reverse() self.author: discord.Member = author - self.page = ceil(len(self.tracks) / 7) - self.current_page = 1 + self.page: int = ceil(len(self.tracks) / 7) + self.current_page: int = 1 try: - self.time = func.time(sum([track.length for track in self.tracks])) - except: + self.time: str = func.time(sum([track.length for track in self.tracks])) + except Exception as _: self.time = "∞" - async def on_timeout(self): + async def on_timeout(self) -> None: for child in self.children: child.disabled = True try: @@ -55,58 +64,57 @@ class ListView(discord.ui.View): except: pass - async def on_error(self, error, item, interaction): + async def on_error(self, error, item, interaction) -> None: return - async def interaction_check(self, interaction): - if interaction.user == self.author: - return True - return False + async def interaction_check(self, interaction: discord.Interaction) -> bool: + return interaction.user == self.author - def build_embed(self): - offset = self.current_page * 7 - tracks = self.tracks[(offset-7):offset] + def build_embed(self) -> discord.Embed: + offset: int = self.current_page * 7 + tracks: list[Track] = self.tracks[(offset-7):offset] embed = discord.Embed(title=self.player.get_msg('viewTitle'), color=func.settings.embed_color) embed.description=self.player.get_msg('viewDesc').format(self.player.current.uri, f"```{self.player.current.title}```") if self.player.current else self.player.get_msg('nowplayingDesc').format("None") - queueText = "" - for index, track in enumerate(tracks, start=offset - 6): - queueText += f"{track.emoji} `{index}.` `[" + (self.player.get_msg("live") if track.is_stream else func.time(track.length)) + f'`] **{track.title[:30]}** ' + (track.requester.mention if track.requester else self.player.client.id) + "\n" + queueText = "\n".join([ + f"{track.emoji} `{i}.` `[" + (self.player.get_msg("live") if track.is_stream else func.time(track.length)) + f'`] **{track.title[:30]}** ' + (track.requester.mention) + for i, track in enumerate(tracks, start=offset-6) + ]) embed.add_field(name=self.name, value=queueText) embed.set_footer(text=self.player.get_msg('viewFooter').format(self.current_page, self.page, self.time)) return embed @discord.ui.button(label='<<', style=discord.ButtonStyle.grey) - async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page != 1: self.current_page = 1 return await interaction.response.edit_message(embed=self.build_embed()) await interaction.response.defer() @discord.ui.button(label='Back', style=discord.ButtonStyle.blurple) - async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page > 1: self.current_page -= 1 return await interaction.response.edit_message(embed=self.build_embed()) await interaction.response.defer() @discord.ui.button(label='Next', style=discord.ButtonStyle.blurple) - async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page < self.page: self.current_page += 1 return await interaction.response.edit_message(embed=self.build_embed()) await interaction.response.defer() @discord.ui.button(label='>>', style=discord.ButtonStyle.grey) - async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page != self.page: self.current_page = self.page return await interaction.response.edit_message(embed=self.build_embed()) await interaction.response.defer() @discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red) - async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: await self.response.delete() self.stop() \ No newline at end of file diff --git a/views/lyrics.py b/views/lyrics.py index b93169a..b520313 100644 --- a/views/lyrics.py +++ b/views/lyrics.py @@ -25,41 +25,40 @@ import discord import function as func class LyricsDropdown(discord.ui.Select): - def __init__(self, langs: list): - options = [discord.SelectOption(label=lang) for lang in langs] + def __init__(self, langs: list[str]) -> None: + self.view: LyricsView super().__init__( placeholder="Select A Lyrics Translation", min_values=1, max_values=1, - options=options, custom_id="selectLyricsLangs" + options=[discord.SelectOption(label=lang) for lang in langs], + custom_id="selectLyricsLangs" ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> None: self.view.lang = self.values[0] self.view.current_page = 1 self.view.pages = len(self.view.source.get(self.values[0])) await interaction.response.edit_message(embed=self.view.build_embed()) class LyricsView(discord.ui.View): - def __init__(self, name: str, source: dict, author: discord.Member): + def __init__(self, name: str, source: dict, author: discord.Member) -> None: super().__init__(timeout=60) - self.name = name - self.source = source - self.lang = list(source.keys())[0] - self.author = author + self.name: str = name + self.source: dict[str, list[str]] = source + self.lang: list[str] = list(source.keys())[0] + self.author: discord.Member = author - self.response = None - self.pages = len(self.source.get(self.lang)) - self.current_page = 1 + self.response: discord.Message = None + self.pages: int = len(self.source.get(self.lang)) + self.current_page: int = 1 self.add_item(LyricsDropdown(list(source.keys()))) - async def interaction_check(self, interaction): - if interaction.user == self.author: - return True - return False + async def interaction_check(self, interaction: discord.Interaction) -> bool: + return interaction.user == self.author - async def on_timeout(self): + async def on_timeout(self) -> None: for child in self.children: child.disabled = True try: @@ -67,10 +66,10 @@ class LyricsView(discord.ui.View): except: pass - async def on_error(self, error, item, interaction): + async def on_error(self, error, item, interaction) -> None: return - def build_embed(self): + def build_embed(self) -> discord.Embed: chunk = self.source.get(self.lang)[self.current_page - 1] embed=discord.Embed(description=chunk, color=func.settings.embed_color) embed.set_author(name=f"Searching Query: {self.name}", icon_url=self.author.display_avatar.url) @@ -78,30 +77,34 @@ class LyricsView(discord.ui.View): return embed @discord.ui.button(label='<<', style=discord.ButtonStyle.grey) - async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page != 1: self.current_page = 1 - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(label='Back', style=discord.ButtonStyle.blurple) - async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page > 1: self.current_page -= 1 - await interaction.response.edit_message(embed=self.build_embed()) - + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() + @discord.ui.button(label='Next', style=discord.ButtonStyle.blurple) - async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page < self.pages: self.current_page += 1 - await interaction.response.edit_message(embed=self.build_embed()) + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() @discord.ui.button(label='>>', style=discord.ButtonStyle.grey) - async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: if self.current_page != self.pages: self.current_page = self.pages - await interaction.response.edit_message(embed=self.build_embed()) + return await interaction.response.edit_message(embed=self.build_embed()) + await interaction.response.defer() @discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red) - async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button): + async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: await self.response.delete() self.stop() \ No newline at end of file diff --git a/views/playlist.py b/views/playlist.py index c90b95e..545375a 100644 --- a/views/playlist.py +++ b/views/playlist.py @@ -24,10 +24,12 @@ SOFTWARE. import discord import function as func -from voicelink import Track from math import ceil from tldextract import extract -from typing import Any +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from voicelink import Track class Select_playlist(discord.ui.Select): def __init__(self, results): diff --git a/voicelink/player.py b/voicelink/player.py index 0f6d0d4..14cee67 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -96,8 +96,8 @@ class Player(VoiceProtocol): channel: Optional[VoiceChannel] = None, ctx: Union[commands.Context, Interaction] = None, ): - self.client = client - self._bot = client + self.client: Client = client + self._bot: Client = client self.context = ctx self.dj: Member = ctx.user if isinstance(ctx, Interaction) else ctx.author self.channel: VoiceChannel = channel From ad6743e9c6e848fd0c3fd92ba3bbe3601ad6a37f Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 17 Sep 2023 14:13:56 +0800 Subject: [PATCH 18/27] Fixed some bugs --- cogs/basic.py | 3 +-- cogs/listeners.py | 2 +- cogs/playlist.py | 3 +-- update.py | 2 +- views/search.py | 5 ++++- voicelink/formatter.py | 10 +++++++--- voicelink/objects.py | 12 ++++-------- voicelink/pool.py | 6 +++--- voicelink/spotify/objects.py | 14 ++++++-------- web/static/js/objects.js | 13 +++++++++++++ 10 files changed, 41 insertions(+), 29 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index dfc0b63..bca8b22 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -888,8 +888,7 @@ class Basic(commands.Cog): category = "News" view = HelpView(self.bot, ctx.author) embed = view.build_embed(category) - message = await ctx.send(embed=embed, view=view) - view.response = message + view.response = await ctx.send(embed=embed, view=view) @commands.hybrid_command(name="ping", aliases=get_aliases("ping")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) diff --git a/cogs/listeners.py b/cogs/listeners.py index 0bcee7a..583a1b3 100644 --- a/cogs/listeners.py +++ b/cogs/listeners.py @@ -96,7 +96,7 @@ class Listeners(commands.Cog): "op": "updateGuild", "user": { "user_id": member.id, - "avatar_url": member.avatar.url, + "avatar_url": member.display_avatar.url, "name": member.name, }, "channel_name": member.voice.channel.name if is_joined else "", diff --git a/cogs/playlist.py b/cogs/playlist.py index 0d3cb7c..3e173dc 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -204,8 +204,7 @@ class Playlists(commands.Cog, name="playlist"): embed.set_footer(text=get_lang(ctx.guild.id, 'playlistFooter')) view = PlaylistView(embed, results, ctx.author) - messsage = await ctx.send(embed=embed, view=view, ephemeral=True) - view.response = messsage + view.response = await ctx.send(embed=embed, view=view, ephemeral=True) @playlist.command(name="create", aliases=get_aliases("create")) @app_commands.describe( diff --git a/update.py b/update.py index b8432b3..569f2ef 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.6b5" +__version__ = "v2.6.7b6" GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" diff --git a/views/search.py b/views/search.py index 720021b..dbab80e 100644 --- a/views/search.py +++ b/views/search.py @@ -20,10 +20,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations import discord -from voicelink import Track +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from voicelink import Track class SearchDropdown(discord.ui.Select): def __init__(self, tracks: list[Track], get_msg: callable) -> None: diff --git a/voicelink/formatter.py b/voicelink/formatter.py index 8b4fde5..115d642 100644 --- a/voicelink/formatter.py +++ b/voicelink/formatter.py @@ -1,6 +1,10 @@ +from __future__ import annotations + import base64, io, abc, struct, dataclasses -from typing import Union, BinaryIO, Optional +from typing import Union, BinaryIO, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .objects import Track @dataclasses.dataclass(frozen=True) class Codec: @@ -180,14 +184,14 @@ class TrackDecoder: "identifier": body_reader.read_utf(), "is_stream": body_reader.read_bool(), "uri": body_reader.read_optional_utf(), - "thumbnail": None if version not in [0, 3] else body_reader.read_optional_utf(), + "artworkUrl": None if version not in [0, 3] else body_reader.read_optional_utf(), "isrc": None if version != 3 else body_reader.read_optional_utf(), "sourceName": body_reader.read_utf(), "position": body_reader.read_long() } class TrackEncoder: - def encode(self, stream: MessageOutput, track) -> None: + def encode(self, stream: MessageOutput, track: Track) -> None: body_writer = stream.start() body_writer.write_byte(0) diff --git a/voicelink/objects.py b/voicelink/objects.py index 60b0981..3a36a36 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -88,15 +88,11 @@ class Track: self._search_type: SearchType = SearchType.ytmsearch if self.spotify else search_type self.spotify_track: Track = spotify_track - self.thumbnail: str = None - self.emoji: str = emoji_source(self.source) + self.thumbnail: str = info.get("artworkUrl") + if not self.thumbnail and YOUTUBE_REGEX.match(self.uri): + self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/maxresdefault.jpg" - if artworkUrl := info.get("artworkUrl"): - self.thumbnail = artworkUrl - - elif YOUTUBE_REGEX.match(self.uri): - self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/hqdefault.jpg" - + self.emoji: str = emoji_source(self.source) self.length: float = 3000 if self.source == "soundcloud" and "/preview/" in self.identifier else info.get("length") self.requester: Member = requester diff --git a/voicelink/pool.py b/voicelink/pool.py index 703304d..6b33e2a 100644 --- a/voicelink/pool.py +++ b/voicelink/pool.py @@ -401,20 +401,20 @@ class Node: return [ Track( track_id=None, + info=spotify_results.to_dict(), requester=requester, search_type=search_type, spotify_track=spotify_results, - info=spotify_results.to_dict() ) ] tracks = [ Track( track_id=None, + info=track.to_dict(), requester=requester, search_type=search_type, spotify_track=track, - info=track.to_dict() ) for track in spotify_results.tracks if track.uri ] @@ -467,7 +467,7 @@ class Node: raise TrackLoadError("There was an error while trying to load this track.") elif load_type == "error": - exception = data["exception"] + exception = data["data"] raise TrackLoadError(f"{exception['message']} [{exception['severity']}]") elif load_type == "empty": diff --git a/voicelink/spotify/objects.py b/voicelink/spotify/objects.py index ad7a1b8..2cdd5f9 100644 --- a/voicelink/spotify/objects.py +++ b/voicelink/spotify/objects.py @@ -1,5 +1,3 @@ -from typing import List - class Track: """The base class for a Spotify Track""" @@ -16,7 +14,7 @@ class Track: def __init__(self, data: dict, image=None) -> None: self.name: str = data.get('name', 'Unknown') self.artists: str = ", ".join(artist["name"] for artist in data.get('artists')) - self.artist_id: list = [artist['id'] 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 @@ -33,7 +31,7 @@ class Track: "isStream": False, "isSeekable": True, "position": 0, - "thumbnail": self.image + "artworkUrl": self.image } def __repr__(self) -> str: @@ -59,7 +57,7 @@ class Album: self.name: str = data.get('name', 'Unknown') self.artists: str = ", ".join(artist["name"] for artist in data.get('artists')) self.image: str = data["images"][0]["url"] - self.tracks: list = [Track(track, image=self.image) for track in data["tracks"]["items"]] + 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"] @@ -83,7 +81,7 @@ class Artist: "name" ) def __init__(self, data: dict) -> None: - self.tracks: List[Track] = [Track(track) for track in data['tracks']] + 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) @@ -111,9 +109,9 @@ class Playlist: "uri" ) - def __init__(self, data: dict, tracks: List[Track]) -> None: + def __init__(self, data: dict, tracks: list[Track]) -> None: self.name: str = data.get('name', 'Unknown') - self.tracks: List[Track] = tracks + 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') diff --git a/web/static/js/objects.js b/web/static/js/objects.js index b8ff078..7ccd622 100644 --- a/web/static/js/objects.js +++ b/web/static/js/objects.js @@ -130,6 +130,19 @@ const decoders = [ const source = input.readUTF(); return { track_id, title, author, length, identifier, isStream, uri, thumbnail: null, source, position: 0n }; + }, + (input, track_id) => { + const title = input.readUTF(); + const author = input.readUTF(); + const length = input.readLong(); + const identifier = input.readUTF(); + const isStream = input.readBoolean(); + const uri = input.readBoolean() ? input.readUTF() : null; + const thumbnail = input.readBoolean() ? input.readUTF() : null; + const isrc = input.readBoolean() ? input.readUTF() : null; + const source = input.readUTF(); + + return { track_id, title, author, length, identifier, isStream, uri, thumbnail, source, position: 0n }; } ] function decode(track_id) { From 8eeb2d665a589c6e980b1ef4e881ea9b545ecdaf Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 17 Sep 2023 18:17:10 +0800 Subject: [PATCH 19/27] Fixed unable to seek in dashboard --- web/static/js/objects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/static/js/objects.js b/web/static/js/objects.js index 7ccd622..2ff9229 100644 --- a/web/static/js/objects.js +++ b/web/static/js/objects.js @@ -494,7 +494,7 @@ class Player { return; } let position = tempPosition / 500 * this.currentTrack.length; - this.send({ "op": "updatePosition", "position": position }); + this.send({ "op": "updatePosition", "position": Math.trunc(position) }); } shuffle() { From bfc57f70e076c31124525c0f103b1d6fd936e38d Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 20 Sep 2023 19:54:24 +0800 Subject: [PATCH 20/27] Return the state of update db method --- function.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/function.py b/function.py index a698f2c..bf51583 100644 --- a/function.py +++ b/function.py @@ -54,7 +54,7 @@ def get_settings(guild_id:int) -> dict: GUILD_SETTINGS[guild_id] = settings or {} return settings -def update_settings(guild_id:int, data: dict, mode="set") -> None: +def update_settings(guild_id:int, data: dict, mode="set") -> bool: settings = get_settings(guild_id) for key, value in data.items(): @@ -65,9 +65,10 @@ def update_settings(guild_id:int, data: dict, mode="set") -> None: case "unset": GUILD_SETTINGS[guild_id].pop(key) case _: - return + return False - SETTINGS_DB.update_one({"_id":guild_id}, {f"${mode}":data}) + result = SETTINGS_DB.update_one({"_id":guild_id}, {f"${mode}":data}) + return result.modified_count > 0 def open_json(path: str) -> dict: try: @@ -198,7 +199,7 @@ async def create_account(ctx: Union[commands.Context, discord.Interaction]) -> N except: pass -async def get_playlist(user_id:int, dType:str=None, dId:str=None) -> dict: +async def get_playlist(user_id:int, dType:str=None, dId:str=None) -> bool: user = PLAYLISTS_DB.find_one({"_id":user_id}, {"_id": 0}) if not user: return None @@ -211,7 +212,9 @@ async def get_playlist(user_id:int, dType:str=None, dId:str=None) -> dict: async def update_playlist(user_id:int, data:dict, *, mode:str="set", update_cache: bool=False) -> None: if update_cache: PLAYLIST_NAME.pop(str(user_id), None) - PLAYLISTS_DB.update_one({"_id":user_id}, {f"${mode}": data}) + result = PLAYLISTS_DB.update_one({"_id":user_id}, {f"${mode}": data}) + return result.modified_count > 0 -async def update_inbox(user_id:int, data:dict) -> None: - return PLAYLISTS_DB.update_one({"_id":user_id}, {"$push":{'inbox':data}}) \ No newline at end of file +async def update_inbox(user_id:int, data:dict) -> bool: + result = PLAYLISTS_DB.update_one({"_id":user_id}, {"$push":{'inbox':data}}) + return result.modified_count > 0 \ No newline at end of file From d88b5253dc0a96a28a2d7b85b3c953de127d8506 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:13:18 +0800 Subject: [PATCH 21/27] Implemented an embed to guide users about missing arguments --- main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index b5ea72f..e1c214b 100644 --- a/main.py +++ b/main.py @@ -90,9 +90,16 @@ class Vocard(commands.Bot): pass elif isinstance(error, (commands.MissingRequiredArgument, commands.MissingRequiredAttachment)): - command = f" Correct Usage: {ctx.prefix}" + (f"{ctx.command.parent.qualified_name} " if ctx.command.parent else "") + f"{ctx.command.name} {ctx.command.signature}" + command = f"{ctx.prefix}" + (f"{ctx.command.parent.qualified_name} " if ctx.command.parent else "") + f"{ctx.command.name} {ctx.command.signature}" position = command.find(f"<{ctx.current_parameter.name}>") + 1 - error = f"```css\n[You are missing argument!]\n{command}\n" + " " * position + "^" * len(ctx.current_parameter.name) + "```" + description = f"**Correct Usage:**\n```{command}\n" + " " * position + "^" * len(ctx.current_parameter.name) + "```\n" + if ctx.command.aliases: + description += f"**Aliases:**\n`{', '.join([f'{ctx.prefix}{alias}' for alias in ctx.command.aliases])}`\n\n" + description += f"**Description:**\n{ctx.command.help}\n\u200b" + + embed = discord.Embed(description=description, color=func.settings.embed_color) + embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {func.settings.invite_link}") + return await ctx.reply(embed=embed) elif not issubclass(error.__class__, VoicelinkException): error = func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link From 9d0223e0b494c44b43b496ce11a0714e7e8c634e Mon Sep 17 00:00:00 2001 From: lqwxx <73760464+lqwxx@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:34:08 +0200 Subject: [PATCH 22/27] UA Language packet --- langs/UA.json | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 langs/UA.json diff --git a/langs/UA.json b/langs/UA.json new file mode 100644 index 0000000..197a809 --- /dev/null +++ b/langs/UA.json @@ -0,0 +1,192 @@ +{ + "unknownException": "⚠️ Щось пішло не так під час виконання команди! Будь ласка, спробуйте пізніше або приєднайтеся до нашого сервера Discord для отримання додаткової підтримки.", + "enabled": "вкл", + "disabled": "викл", + + "nodeReconnect": "Будь ласка, спробуйте знову! Після перепідключення вузла.", + "noChannel": "емає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.", + "alreadyConnected": "Уже підключений до голосового каналу.", + "noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.", + "noPlaySource": "Неможливо знайти робочі джерела!", + "noPlayer": "На цьому сервері не знайдено жодного активного плеєра.", + "notVote": "Ця команда вимагає вашого голосу! Введіть `/vote` для отримання додаткової інформації.", + "languageNotFound": "Мовний пакет не знайдено! Будь ласка, виберіть наявний мовний пакет.", + "changedLanguage": "Успішно змінено на мовний пакет `{0}`.", + "setPrefix": "Готово! Мій префікс на вашому сервері тепер `{0}`. Спробуйте запустити `{1}ping`, щоб перевірити його.", + "setDJ": "Встановити роль DJ {0}.", + "setqueue": "Встановити режим черги на `{0}`.", + "247": "Тепер у вас `{0}` режим 24/7.", + "bypassVote": "Тепер у вас `{0}` система голосування.", + "setVolume": "Встановити гучність на `{0}`%", + "togglecontroller": "Тепер у вас `{0}` контролер музики.", + "toggleDuplicateTrack": "Тепер у вас `{0}` запобігання дублюванню треку в черзі.", + "toggleControllerMsg": "Тепер у вас `{0}` повідомлення від контролера музики.", + "settingsMenu": "Налаштування сервера | {0}", + "settingsTitle": "❤️ Основна інформація:", + "settingsValue": "Префікс: `{0}`\nМова: `{1}`\nУвімкнути контролер музики: `{2}`\nDJ роль: {3}\nОбхід голосування: `{4}`\n24/7: `{5}`\nГромкість за замовчуванням: `{6}%`\nЧас програвання: `{7}`", + "settingsTitle2": "🔗 Інформація про чергу:", + "settingsValue2": "Режим черги: `{0}`\nМаксимальна кількість пісень: `{1}`\nДозволити дублювання треків: `{2}`", + "settingsPermTitle": "✨ Права:", + "settingsPermValue": "{0} Адміністратор\n{1} Керування_Сервером\n{2} Керування_Каналом\n{3} Керування_Сообщениями", + "pingTitle1": "Інформація про бота:", + "pingTitle2": "Інформація про плеєр:", + "pingfield1": "````ID Шарда: {0}/{1}\nЗатримка Шарда: {2:.3f}s {3}\nРегіон: {4}````", + "pingfield2": "```Вузол: {0} - {1:.3f}s\nГравці: {2}\nРегіон Голосу: {3}```", + "karaoke": "Ви оновили рівень на **{0}**, моно-рівень на **{1}**, фільтрувальну смугу на **{2}** і ширину фільтра на **{3}**", + "tremolo&vibrato": "Ви оновили значення ефекту tremolo на **{0}** і vibrato на **{1}**", + "rotation": "Ви оновили значення ефекту rotation на **{0}***", + "distortion": "Ви ввімкнули ефект rotation.", + "lowpass": "Ви оновили значення ефекту lowpass на **{0}**", + "channelmix": "Ви оновили ChannelMix. Лівий-на-лівий: **{0}**, Правий-на-правий: **{1}**, Лівий-на-правий: **{2}**, Правий-на-лівий: **{3}**", + "nightcore": "Ви ввімкнули ефект Nightcore.", + "8d": "Ви ввімкнули ефект 8D.", + "vaporwave": "Ви ввімкнули ефект Vaporwave.", + "cleareffect": "Звукові ефекти були очищені!", + "FilterTagAlreadyInUse": "Цей звуковий ефект уже використовується! Будь ласка, використовуйте /cleareffect <Тег>, щоб видалити його.", + + "playlistViewTitle": "📜 Усі плейлисти користувача {0}", + "playlistViewHeaders": [" ", "ID:", "Час:", "Назва:", "Треки:"], + "playlistMaxP": "Максимальна кількість плейлистів:", + "playlistMaxT": "Максимальное количество треков:", + "playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.", + "playlistNotFound": "Плейлист [`{0}`] не знайдено. Введіть /playlist view, щоб подивитися всі ваші плейлисти.", + "playlistNotAccess": "Вибачте! У вас немає доступу до цього плейлиста!", + "playlistNoTrack": "Вибачте! У плейлисті [`{0}`] немає треків.", + "playlistNotAllow": "Ця команда не дозволена для пов'язаних і загальних плейлистів.", + "playlistPlay": "Додано плейлист [`{0}`] з `{1}` піснями в чергу.", + "playlistOverText": "Вибачте! Ім'я плейлиста не може перевищувати 10 символів.", + "playlistSameName": "Вибачте! Це ім'я не може збігатися з вашим новим ім'ям.", + "playlistDeleteError": "Ви не можете видалити плейлист за замовчуванням.", + "playlistRemove": "Ви видалили плейлист [`{0}`].", + "playlistSendErrorPlayer": "Вибачте! Ви не можете надіслати запрошення самому собі.", + "playlistSendErrorBot": "Вибачте! Ви не можете надіслати запрошення боту.", + "playlistBelongs": "Вибачте! Цей плейлист належить <@{0}>.", + "playlistShare": "Вибачте! Цим плейлистом уже поділилися з {0}.", + "playlistSent": "Вибачте! Ви вже надіслали запрошення раніше.", + "noPlaylistAcc": "{0} не створив обліковий запис плейлиста.", + "overPlaylistCreation": "Ви не можете створювати більше `{0}` плейлистів!", + "playlistExists": "Плейлист [`{0}`] вже існує.", + "playlistNotInvaildUrl": "Будь ласка, введіть дійсне посилання або публічне плейлист-посилання на Spotify або YouTube.", + "playlistCreated": "Ви створили плейлист `{0}`. Введіть /playlist view для отримання додаткової інформації.", + "playlistRenamed": "Ви перейменували `{0}` на `{1}`.", + "playlistLimitTrack": "Ви досягли ліміту! Ви можете додати тільки `{0}` пісень до свого плейлиста.", + "playlistPlaylistLink": "Вам не дозволено використовувати посилання на плейлист.", + "playlistStream": "Вам не дозволено додавати потокові відео у свій плейлист.", + "playlistPositionNotFound": "Не вдається знайти позицію `{0}` у вашому плейлисті [`{1}`]!", + "playlistRemoved": "👋 Видалено **{0}** з плейлиста {1} [`{2}`].", + "playlistClear": "Ви успішно очистили свій плейлист [`{0}`].", + "playlistView": "Перегляд плейлистів", + "playlistViewDesc": "```Ім'я | ID: {0} | {1}\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```", + "playlistViewPermsValue": "📖 Читання: ✓ ✍🏽 Запис: {0} 🗑️ Видалення: {1}", + "playlistViewPermsValue2": "📖 Читання: {0}", + "playlistViewTrack": "Треки", + "playlistViewPage": "Сторінка: {0}/{1} | Загальна тривалість: {2}", + "inboxFull": "Вибачте! Поштова скринька {0} переповнена.", + "inboxNoMsg": "У вашій поштовій скриньці немає повідомлень.", + "invitationSent": "Запрошення надіслано {0}.", + + "notInChannel": "{0}, для використання голосових команд ви маєте перебувати в {1}. Будь ласка, перезайдіть, якщо ви вже в голосі!", + "noTrackPlaying": "Зараз немає пісень, які грають.", + "noTrackFound": "Пісні з таким запитом не знайдено! Будь ласка, вкажіть дійсне посилання.", + "noLinkSupport": "Команда пошуку не підтримує посилання!", + "voted": "Ви проголосували!", + "missingPerms_pos": "Тільки DJ або адміністратори можуть змінювати позицію.", + "missingPerms_mode": "Тільки DJ або адміністратори можуть перемкнути режим циклу.", + "missingPerms_queue": "Тільки DJ або адміністратори можуть видаляти треки з черги.", + "missingPerms_autoplay": "Тільки DJ або адміністратори можуть вмикати або вимикати режим autoplay!", + "missingPerms_function": "Тільки DJ або адміністратори можуть використовувати цю функцію.", + "timeFormatError": "Неправильний формат часу. Приклад: 2:42PM або 12:39:31", + "lyricsNotFound": "Текст пісні не знайдено. Введіть /lyrics <Назва пісні> <Автор> для пошуку тексту.", + "missingTrackInfo": "Деяка інформація про трек відсутня.", + "noVoiceChannel": "Голосовий канал не знайдено!", + + "playlistAddError": "Вам не дозволено додавати потокові відео в плейлист!", + "playlistAddError2": "Сталася помилка під час додавання треків у плейлист!", + "playlistlimited": "Ви досягли ліміту! Ви можете додати тільки {0} пісні до свого плейлиста.", + "playlistrepeated": "Такий самий трек уже є у вашому плейлисті!", + "playlistAdded": "❤️ Додано **{0}** до плейлиста користувача {1} [`{2}`]!", + + "playerDropdown": "Виберіть пісню для переходу ...", + + "buttonBack": "Назад", + "buttonPause": "Призупинити", + "buttonResume": "Продовжити", + "buttonSkip": "Вперед", + "buttonLeave": "СТОП", + "buttonLoop": "Зациклити", + "buttonVolumeUp": "Гучність +", + "buttonVolumeDown": "Гучність -", + "buttonVolumeMute": "Вимкнути звук", + "buttonVolumeUnmute": "Увімкнути звук", + "buttonAutoPlay": "Autoplay", + "buttonShuffle": "Перемішати", + "buttonForward": "+10сек", + "buttonRewind": "Назад", + + "nowplayingDesc": "**Зараз грає:**\n```{0}```", + "nowplayingField": "Наступне:", + "nowplayingLink": "Слухати на {0}", + + "connect": "Підключено до {0}", + + "live": "В ЕФІРІ", + "playlistLoad": "🎶 Додано плейлист **{0}** з `{1}` піснями в чергу.", + "trackLoad": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) для початку програвання.\n", + "trackLoad_pos": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) у чергу на позицію **{4}**\n", + "searchTitle": "Пошук: {0}", + "searchDesc": "➥ Платформа: {0} **{1}**\n➥ Результати: **{2}**\n\n{3}", + "searchWait": "Виберіть пісню, яку хочете додати в чергу.", + "searchTimeout": "Пошук перервано: перевищено час очікування. Будь ласка, спробуйте пізніше.", + "searchSuccess": "Пісню додано в чергу.", + + "queueTitle": "Наступний у черзі:", + "historyTitle": "Історія черги:", + "viewTitle": "Поточна черга", + "viewDesc": "**Заразом грає: [*посилання*]({0}) ⮯**\n{1}", + "viewFooter": "Сторінка: {0}/{1} | Загальна тривалість: {2}", + + "pauseError": "Плеєр уже на паузі.", + "pauseVote": "{0} проголосував за паузу пісні. [{1}/{2}]", + "paused": "`{0}` Поставив плеєр на паузу.", + "resumeError": "Плеєр не на паузі.", + "resumeVote": "{0} проголосував за продовження пісні. [{1}/{2}]", + "resumed": "`{0}` Прибрав плеєр із паузи.", + "shuffleError": "Додати більше пісень у чергу перед перемішуванням.", + "shuffleVote": "{0} проголосував за перемішування черги. [{1}/{2}]", + "shuffled": "Черга перемішана.", + "skipError": "Немає пісень для пропуску.", + "skipVote": "{0} проголосував за пропуск пісні. [{1}/{2}]", + "skipped": "`{0}` пропустив пісню.", + + "backVote": "{0} проголосував за повернення до попередньої пісні. [{1}/{2}]", + "backed": "`{0}` повернувся до попередньої пісні.", + + "leaveVote": "{0} проголосував за зупинку плеєра. [{1}/{2}]", + "left": "`{0}` зупинив плеєр.", + + "seek": "Встановити плеєр на **{0}**", + "repeat": "Режим циклу встановлено на `{0}`", + "cleared": "Очищено всі треки в `{0}`", + "removed": "Видалено `{0}` треків із черги.", + "forward": "Перемотати плеєр вперед на **{0}**", + "rewind": "Перемотати плеєр назад на **{0}**", + "replay": "Повторення поточної пісні.", + "swapped": "Треки `{0}` і `{1}` обміняні місцями", + "moved": "Трек `{0}` зміщений на `{1}`", + "autoplay": "Режим autoplay встановлено на **{0}**", + + "notdj": "Ви не DJ. Поточний DJ: {0}.", + "djToMe": "Ви не можете передати роль DJ собі або боту.", + "djnotinchannel": "`{0}` не знаходиться в голосовому каналі.", + "djswap": "Ви передали роль DJ `{0}`.", + + "chaptersDropdown": "Виберіть епізод для переходу ...", + "noChaptersFound": "Епізод не знайдено!", + "chatpersNotSupport": "Ця команда підтримує тільки відео на YouTube!", + + "voicelinkQueueFull": "Вибачте, ви досягли максимальної кількості `{0}` треків у черзі!", + "voicelinkOutofList": "Будь ласка, надайте дійсний індекс треку!", + "voicelinkDuplicateTrack": "Вибачте, цей трек уже є в черзі.", + + "deocdeError": "Щось пішло не так під час декодування файлу!" +} \ No newline at end of file From 0ee81528492283c44e5ccff7f5dde9733940d824 Mon Sep 17 00:00:00 2001 From: lqwxx <73760464+lqwxx@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:37:47 +0200 Subject: [PATCH 23/27] Update UA.json --- langs/UA.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/langs/UA.json b/langs/UA.json index 197a809..92bf755 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -4,7 +4,7 @@ "disabled": "викл", "nodeReconnect": "Будь ласка, спробуйте знову! Після перепідключення вузла.", - "noChannel": "емає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.", + "noChannel": "Немає голосового каналу для підключення. Будь ласка, вкажіть або приєднайтеся до одного.", "alreadyConnected": "Уже підключений до голосового каналу.", "noPermission": "Вибачте! У мене немає дозволу на підключення або розмову у вашому голосовому каналі.", "noPlaySource": "Неможливо знайти робочі джерела!", @@ -189,4 +189,4 @@ "voicelinkDuplicateTrack": "Вибачте, цей трек уже є в черзі.", "deocdeError": "Щось пішло не так під час декодування файлу!" -} \ No newline at end of file +} From 3a0cc0c857b9419b3ad739136fcba2e3fd0cec82 Mon Sep 17 00:00:00 2001 From: lqwxx <73760464+lqwxx@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:27:36 +0200 Subject: [PATCH 24/27] Update UA.json --- langs/UA.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/langs/UA.json b/langs/UA.json index 92bf755..0fa726c 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -120,7 +120,7 @@ "buttonVolumeUnmute": "Увімкнути звук", "buttonAutoPlay": "Autoplay", "buttonShuffle": "Перемішати", - "buttonForward": "+10сек", + "buttonForward": "Вперед", "buttonRewind": "Назад", "nowplayingDesc": "**Зараз грає:**\n```{0}```", @@ -129,7 +129,7 @@ "connect": "Підключено до {0}", - "live": "В ЕФІРІ", + "live": "Прямий єфір", "playlistLoad": "🎶 Додано плейлист **{0}** з `{1}` піснями в чергу.", "trackLoad": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) для початку програвання.\n", "trackLoad_pos": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) у чергу на позицію **{4}**\n", @@ -182,7 +182,7 @@ "chaptersDropdown": "Виберіть епізод для переходу ...", "noChaptersFound": "Епізод не знайдено!", - "chatpersNotSupport": "Ця команда підтримує тільки відео на YouTube!", + "chatpersNotSupport": "Ця команда підтримує тільки відео з YouTube!", "voicelinkQueueFull": "Вибачте, ви досягли максимальної кількості `{0}` треків у черзі!", "voicelinkOutofList": "Будь ласка, надайте дійсний індекс треку!", From eeb4e7a68e4e7413bb4f732d4ed24e6bd006e56b Mon Sep 17 00:00:00 2001 From: lqwxx <73760464+lqwxx@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:30:22 +0200 Subject: [PATCH 25/27] Update UA.json --- langs/UA.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/UA.json b/langs/UA.json index 0fa726c..34b846e 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -129,7 +129,7 @@ "connect": "Підключено до {0}", - "live": "Прямий єфір", + "live": "Прямий ефір", "playlistLoad": "🎶 Додано плейлист **{0}** з `{1}` піснями в чергу.", "trackLoad": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) для початку програвання.\n", "trackLoad_pos": "Додано **[{0}](<{1}>)** від **{2}** (`{3}`) у чергу на позицію **{4}**\n", From e4bcd6e48358809c3e5170e33587c5e4d90d99c4 Mon Sep 17 00:00:00 2001 From: lqwxx <73760464+lqwxx@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:16:27 +0200 Subject: [PATCH 26/27] Update UA.json --- langs/UA.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/UA.json b/langs/UA.json index 34b846e..e5b920d 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -95,7 +95,7 @@ "missingPerms_queue": "Тільки DJ або адміністратори можуть видаляти треки з черги.", "missingPerms_autoplay": "Тільки DJ або адміністратори можуть вмикати або вимикати режим autoplay!", "missingPerms_function": "Тільки DJ або адміністратори можуть використовувати цю функцію.", - "timeFormatError": "Неправильний формат часу. Приклад: 2:42PM або 12:39:31", + "timeFormatError": "Неправильний формат часу. Приклад: 2:42", "lyricsNotFound": "Текст пісні не знайдено. Введіть /lyrics <Назва пісні> <Автор> для пошуку тексту.", "missingTrackInfo": "Деяка інформація про трек відсутня.", "noVoiceChannel": "Голосовий канал не знайдено!", From 8333f92a15b164bdd0553c2ea0b352d5c5933a77 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:32:47 +0800 Subject: [PATCH 27/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8cf54d..cf6e1a6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Click on the image below to watch the tutorial on Youtube. ## Requirements * [Python 3.10+](https://www.python.org/downloads/) * [Modules in requirements](https://github.com/ChocoMeow/Vocard/blob/main/requirements.txt) -* [Lavalink Server (Requires 3.7.0+)](https://github.com/freyacodes/Lavalink) +* [Lavalink Server (Requires 4.0.0+)](https://github.com/freyacodes/Lavalink) ## Quick Start ```sh