Merge pull request #14 from ChocoMeow/beta

Vocard v2.6.7 Update: new features, bug fixes, and code clean-up
This commit is contained in:
Choco
2023-12-08 10:33:07 +08:00
committed by GitHub
41 changed files with 1184 additions and 627 deletions

View File

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

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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
@@ -33,22 +56,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.formatLength}]` [{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,
icon_url=track.requester.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.formatLength}]**", 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 +84,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]
@@ -103,7 +123,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 +163,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,20 +210,20 @@ 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)
message = await ctx.send(embed=embed, view=view, ephemeral=True)
view.response = message
view = SearchView(tracks=tracks[0:10], lang=player.get_msg)
view.response = await ctx.send(embed=embed, view=view, ephemeral=True)
await view.wait()
if view.values is not None:
msg = ""
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 +251,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 +282,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)
@@ -419,6 +439,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)
@@ -434,8 +455,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)
@@ -522,8 +542,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)
@@ -773,13 +792,12 @@ 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)
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.")
@@ -841,8 +859,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)
@@ -871,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)

View File

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

View File

@@ -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
@@ -5,7 +28,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):
@@ -36,10 +59,10 @@ class Nodes(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
@@ -73,7 +96,7 @@ class Nodes(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 "",
@@ -82,4 +105,4 @@ class Nodes(commands.Cog):
})
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Nodes(bot))
await bot.add_cog(Listeners(bot))

View File

@@ -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
@@ -8,11 +31,11 @@ from function import (
time as ctime,
get_playlist,
create_account,
checkroles,
check_roles,
update_playlist,
update_inbox,
get_lang,
playlist_name,
PLAYLIST_NAME,
settings,
get_aliases,
cooldown_check
@@ -70,10 +93,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 +110,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 +126,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 = check_roles()
if result['position'] > max_p:
return await ctx.send(get_lang(ctx.guild.id, 'playlistNotAccess'), ephemeral=True)
@@ -142,7 +164,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 = check_roles()
results = []
for index, data in enumerate(user, start=1):
@@ -156,7 +178,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)
@@ -169,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'})
@@ -182,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(
@@ -196,7 +217,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 = check_roles()
user = await check_playlist(ctx, full=True)
if not user:
return await create_account(ctx)
@@ -212,9 +233,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 +252,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 +319,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 +334,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 +342,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 +366,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 = check_roles()
if len(result['playlist']['tracks']) >= max_t:
return await ctx.send(get_lang(ctx.guild.id, 'playlistLimitTrack').format(max_t), ephemeral=True)
@@ -364,7 +380,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 +402,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 +478,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 = check_roles()
user = await check_playlist(ctx, full=True)
if not user:
return await create_account(ctx)
@@ -479,9 +495,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:

View File

@@ -1,16 +1,36 @@
"""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 io
import contextlib
import textwrap
import traceback
import psutil
import function as func
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,
@@ -19,11 +39,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]:
@@ -43,8 +70,7 @@ class Admin(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)
@@ -60,21 +86,17 @@ class Admin(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"))
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))
@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 +108,7 @@ class Admin(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 +251,7 @@ class Admin(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)
@@ -250,53 +271,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))

View File

@@ -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
@@ -87,8 +110,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 +121,7 @@ class Task(commands.Cog):
await report_channel.send(content=f"Report Before: <t:{round(datetime.timestamp(datetime.now()))}:F>", 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))

View File

@@ -8,16 +8,16 @@ 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 --------------
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")
@@ -32,45 +32,47 @@ 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") -> bool:
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 False
result = SETTINGS_DB.update_one({"_id":guild_id}, {f"${mode}":data})
return result.modified_count > 0
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 +84,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 +102,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
@@ -133,13 +135,13 @@ 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]:
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"
@@ -162,6 +164,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)
@@ -183,20 +188,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) -> bool:
user = PLAYLISTS_DB.find_one({"_id":user_id}, {"_id": 0})
if not user:
return None
if dType:
@@ -205,21 +209,12 @@ 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)
result = PLAYLISTS_DB.update_one({"_id":user_id}, {f"${mode}": data})
return result.modified_count > 0
async def update_inbox(userid:int, data:dict) -> None:
return Playlist.update_one({"_id":userid}, {"$push":{'inbox':data}})
async def checkroles(userid:int):
rank, max_p, max_t = 'Normal', 5, 500
return rank, max_p, max_t
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

192
langs/UA.json Normal file
View File

@@ -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:42",
"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": "Вперед",
"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": "Щось пішло не так під час декодування файлу!"
}

25
main.py
View File

@@ -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)
@@ -90,15 +90,22 @@ 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
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)

View File

@@ -1,5 +1,5 @@
discord.py==2.3.1
pymongo==4.1.1
discord.py==2.3.2
pymongo==4.5.0
dnspython==2.2.1
tldextract==3.2.1
validators==0.18.2
@@ -9,3 +9,4 @@ beautifulsoup4==4.11.1
websockets==10.4
Flask==2.2.3
Flask-SocketIO==5.3.2
psutil==5.9.5

View File

@@ -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.7b6"
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:

View File

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

View File

@@ -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
@@ -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
async def interaction_check(self, interaction: discord.Interaction):
return interaction.user == self.author

View File

@@ -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
@@ -31,7 +31,7 @@ from function import (
get_playlist,
update_playlist,
create_account,
checkroles
check_roles
)
from typing import Dict
@@ -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
)
@@ -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 = check_roles()
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:
@@ -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
)
@@ -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"),

View File

@@ -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
@@ -22,20 +22,168 @@ 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: str = 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()
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: commands.Bot = bot
super().__init__(
placeholder="Select a cog to reload...",
min_values=1, max_values=1,
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.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_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)

View File

@@ -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):

View File

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

View File

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

View File

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

View File

@@ -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
@@ -25,26 +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[Track] = player.queue.tracks() if isQueue else player.queue.history()
self.response: discord.Message = None
self.name = player.get_msg('queueTitle') if isQueue else player.get_msg('historyTitle')
self.tracks = player.queue.tracks() if isQueue else player.queue.history()
if not isQueue:
self.tracks.reverse()
self.author = author
self.author: discord.Member = author
self.page: int = ceil(len(self.tracks) / 7)
self.current_page: int = 1
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:
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:
@@ -52,54 +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
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.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 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()

View File

@@ -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
@@ -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()

View File

@@ -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
@@ -26,33 +26,40 @@ import function as func
from math import ceil
from tldextract import extract
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from voicelink import Track
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 +69,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 +132,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:

View File

@@ -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
@@ -20,36 +20,41 @@ 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 langs
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from voicelink import Track
class SearchDropdown(discord.ui.Select):
def __init__(self, tracks, lang):
self.tracks = tracks
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}"))
super().__init__(placeholder=langs[lang]['searchWait'],
min_values=1, max_values=len(tracks),
options=options
)
def __init__(self, tracks: list[Track], get_msg: callable) -> None:
self.view: SearchView
self.get_msg: callable = get_msg
async def callback(self, interaction: discord.Interaction):
super().__init__(
placeholder=get_msg('searchWait'),
min_values=1, max_values=len(tracks),
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) -> None:
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()
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):

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,13 +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 != 0 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)

View File

@@ -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
@@ -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,81 @@ class Track:
search_type: SearchType = SearchType.ytsearch,
spotify_track = None,
):
self.track_id = track_id
self.info = info
self._track_id: Optional[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 = self.source == "spotify"
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.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"
self.emoji = emoji_source(self.source)
self.emoji: str = emoji_source(self.source)
self.length: float = 3000 if self.source == "soundcloud" and "/preview/" in self.identifier else info.get("length")
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"
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 self.source == "soundcloud" and "/preview/" in self.identifier:
self.length = 30000
else:
self.length = 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)
if not track_id:
self.track_id = encode(self)
def toDict(self):
return {
"track_id": self.track_id,
"info": self.info,
"thumbnail": self.thumbnail
}
def encode(self):
return encode(self)
def __eq__(self, other):
def __eq__(self, other) -> bool:
if not isinstance(other, Track):
return False
return other.track_id == self.track_id
def __str__(self):
def __str__(self) -> str:
return self.title
def __repr__(self):
def __repr__(self) -> str:
return f"<Voicelink.track title={self.title!r} uri=<{self.uri!r}> length={self.length}>"
def toDict(self) -> dict:
return {
"track_id": self.track_id,
"info": self.info,
"thumbnail": self.thumbnail
}
@property
def track_id(self) -> str:
if not self._track_id:
self._track_id = encode(self)
return self._track_id
@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,
*,
@@ -141,18 +171,16 @@ 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
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"<Voicelink.playlist name={self.name!r} track_count={len(self.tracks)}>"
@property
@@ -164,3 +192,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)

View File

@@ -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
@@ -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
@@ -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()
@@ -304,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)
@@ -441,7 +440,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(

View File

@@ -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
@@ -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):
@@ -394,27 +394,27 @@ 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):
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
]
@@ -466,28 +466,40 @@ class Node:
if not load_type:
raise TrackLoadError("There was an error while trying to load this track.")
elif load_type == "LOAD_FAILED":
exception = data["exception"]
elif load_type == "error":
exception = data["data"]
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":
data = data.get("data")
return Playlist(
playlist_info=data["playlistInfo"],
playlist_info=data["info"],
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:

View File

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

View File

@@ -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()

View File

@@ -1,24 +1,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[str] = [artist['id'] for artist in data.get('artists')]
self.length: int = data.get('duration_ms')
self.id: str = data.get('id')
self.image: str = images[0]["url"] if (images := data.get("album", {}).get("images")) else image
self.uri: str = None if data["is_local"] else data["external_urls"]["spotify"]
def to_dict(self) -> dict:
return {
@@ -26,12 +26,12 @@ 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,
"position": 0,
"thumbnail": self.image
"artworkUrl": self.image
}
def __repr__(self) -> str:
@@ -43,14 +43,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(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 +71,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 +99,24 @@ class Artist:
class Playlist:
"""The base class for a Spotify playlist"""
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"]
__slots__ = (
"name",
"tracks",
"owner",
"total_tracks",
"id",
"image",
"uri"
)
def __init__(self, data: dict, tracks: list[Track]) -> None:
self.name: str = data.get('name', 'Unknown')
self.tracks: list[Track] = tracks
self.owner: str = data["owner"]["display_name"]
self.total_tracks: int = data["tracks"]["total"]
self.id: str = data.get('id')
self.image: str = data["images"][0]["url"] if len(data.get("images", [])) else None
self.uri: str = data["external_urls"]["spotify"]
def __repr__(self) -> str:
return (

View File

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

View File

@@ -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 = 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)
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],

View File

@@ -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) {
@@ -481,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() {