Migrate scripts to voicelink, rewrite handlers and views

This commit is contained in:
Choco
2025-09-29 19:39:19 +08:00
parent 7a4f85a74d
commit ef54f96f51
52 changed files with 2048 additions and 1642 deletions

View File

@@ -1,3 +0,0 @@
from .lyrics import LYRICS_PLATFORMS
from .placeholders import Placeholders
from .settings import Settings

View File

@@ -1,63 +0,0 @@
"""MIT License
Copyright (c) 2023 - present Vocard Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from discord.ext import commands
from re import findall
from importlib import import_module
class Placeholders:
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.voicelink = import_module("voicelink")
self.variables = {
"guilds": self.guilds_count,
"users": self.users_count,
"players": self.players_count,
"nodes": self.nodes_count
}
def guilds_count(self) -> int:
return len(self.bot.guilds)
def users_count(self) -> int:
return len(self.bot.users)
def players_count(self) -> int:
count = 0
for node in self.voicelink.NodePool._nodes.values():
count += len(node._players)
return count
def nodes_count(self):
return len(self.voicelink.NodePool._nodes)
def replace(self, msg: str) -> str:
keys = findall(r'@@(.*?)@@', msg)
for key in keys:
value = self.variables.get(key.lower(), None)
if value:
msg = msg.replace(f"@@{key}@@", str(value()))
return msg

View File

@@ -1,59 +0,0 @@
"""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 os
from dotenv import load_dotenv
from typing import (
Dict,
List,
Any,
Union
)
load_dotenv()
class Settings:
def __init__(self, settings: Dict) -> None:
self.token: str = settings.get("token") or os.getenv("TOKEN")
self.client_id: int = int(settings.get("client_id", 0)) or int(os.getenv("CLIENT_ID"))
self.genius_token: str = settings.get("genius_token") or os.getenv("GENIUS_TOKEN")
self.mongodb_url: str = settings.get("mongodb_url") or os.getenv("MONGODB_URL")
self.mongodb_name: str = settings.get("mongodb_name") or os.getenv("MONGODB_NAME")
self.invite_link: str = "https://discord.gg/wRCgB7vBQv"
self.nodes: Dict[str, Dict[str, Union[str, int, bool]]] = settings.get("nodes", {})
self.max_queue: int = settings.get("default_max_queue", 1000)
self.bot_prefix: str = settings.get("prefix", "")
self.activity: List[Dict[str, str]] = settings.get("activity", [{"listen": "/help"}])
self.logging: Dict[Union[str, Dict[str, Union[str, bool]]]] = settings.get("logging", {})
self.embed_color: str = int(settings.get("embed_color", "0xb3b3b3"), 16)
self.bot_access_user: List[int] = settings.get("bot_access_user", [])
self.sources_settings: Dict[Dict[str, str]] = settings.get("sources_settings", {})
self.cooldowns_settings: Dict[str, List[int]] = settings.get("cooldowns", {})
self.aliases_settings: Dict[str, List[str]] = settings.get("aliases", {})
self.controller: Dict[str, Dict[str, Any]] = settings.get("default_controller", {})
self.voice_status_template: str = settings.get("default_voice_status_template", "")
self.lyrics_platform: str = settings.get("lyrics_platform", "A_ZLyrics").lower()
self.ipc_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {})
self.version: str = settings.get("version", "")

View File

@@ -21,39 +21,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import discord, voicelink, re
import re
import discord
import voicelink
from io import StringIO
from validators import url
from discord import app_commands
from discord.ext import commands
from function import (
settings,
send,
time as ctime,
format_time,
get_source,
get_user,
get_lang,
truncate_string,
cooldown_check,
get_aliases,
logger
)
from voicelink import SearchType, LoopType
from addons import LYRICS_PLATFORMS
from views import SearchView, QueueView, LinkView, LyricsView, HelpView
from validators import url
from voicelink import MongoDBHandler, LangHandler, Config
from voicelink.views import SearchView, QueueView, LinkView, LyricsView, HelpView
from voicelink.utils import format_ms, format_to_ms, truncate_string, dispatch_message, send_localized_message
async def nowplay(ctx: commands.Context, player: voicelink.Player):
track = player.current
if not track:
return await send(ctx, 'noTrackPlaying', ephemeral=True)
return await send_localized_message(ctx, 'noTrackPlaying', ephemeral=True)
texts = await get_lang(ctx.guild.id, "nowplayingDesc", "nowplayingField", "nowplayingLink")
texts = await LangHandler.get_lang(ctx.guild.id, "nowplayingDesc", "nowplayingField", "nowplayingLink")
upnext = "\n".join(f"`{index}.` `[{track.formatted_length}]` [{truncate_string(track.title)}]({track.uri})" for index, track in enumerate(player.queue.tracks()[:2], start=2))
embed = discord.Embed(description=texts[0].format(track.title), color=settings.embed_color)
embed = discord.Embed(description=texts[0].format(track.title), color=Config().embed_color)
embed.set_author(
name=track.requester.display_name,
icon_url=track.requester.display_avatar.url
@@ -65,9 +59,9 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player):
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)
embed.add_field(name="\u2800", value=f"{icon} {pbar} **[{format_ms(player.position)}/{track.formatted_length}]**", inline=False)
return await send(ctx, embed, view=LinkView(texts[2].format(track.source.title()), track.emoji, track.uri))
return await dispatch_message(ctx, embed, view=LinkView(texts[2].format(track.source.title()), track.emoji, track.uri))
class Basic(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
@@ -103,7 +97,7 @@ class Basic(commands.Cog):
return [app_commands.Choice(name=truncate_string(f"🎵 {track.author} - {track.title}", 100), value=truncate_string(f"{track.author} - {track.title}", 100)) for track in tracks]
history = {track["identifier"]: track for track_id in reversed(await get_user(interaction.user.id, "history")) if (track := voicelink.decode(track_id))["uri"]}
history = {track["identifier"]: track for track_id in reversed(await MongoDBHandler.get_user(interaction.user.id, d_type="history")) if (track := voicelink.Track.decode(track_id))["uri"]}
return [app_commands.Choice(name=truncate_string(f"🕒 {track['author']} - {track['title']}", 100), value=track['uri']) for track in history.values() if len(track['uri']) <= 100][:25]
@commands.hybrid_command(name="connect", aliases=get_aliases("connect"))
@@ -114,9 +108,9 @@ class Basic(commands.Cog):
try:
player = await voicelink.connect_channel(ctx, channel)
except discord.errors.ClientException:
return await send(ctx, "alreadyConnected")
return await send_localized_message(ctx, "alreadyConnected")
await send(ctx, 'connect', player.channel)
await send_localized_message(ctx, 'connect', player.channel)
@commands.hybrid_command(name="play", aliases=get_aliases("play"))
@app_commands.describe(
@@ -133,27 +127,26 @@ class Basic(commands.Cog):
player = await voicelink.connect_channel(ctx)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if ctx.interaction:
await ctx.interaction.response.defer()
tracks = await player.get_tracks(query, requester=ctx.author)
if not tracks:
return await send(ctx, "noTrackFound")
return await send_localized_message(ctx, "noTrackFound")
try:
if isinstance(tracks, voicelink.Playlist):
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end))
await send(ctx, "playlistLoad", tracks.name, index)
index = await player.add_track(tracks.tracks, start_time=format_to_ms(start), end_time=format_to_ms(end))
await send_localized_message(ctx, "playlistLoad", tracks.name, index)
else:
position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end))
texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
position = await player.add_track(tracks[0], start_time=format_to_ms(start), end_time=format_to_ms(end))
texts = await LangHandler.get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
await send(
await dispatch_message(
ctx,
stream_content + additional_content,
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
@@ -176,32 +169,32 @@ class Basic(commands.Cog):
query = message.attachments[0].url
if not query:
return await send(interaction, "noPlaySource", ephemeral=True)
return await send_localized_message(interaction, "noPlaySource", ephemeral=True)
player: voicelink.Player = interaction.guild.voice_client
if not player:
player = await voicelink.connect_channel(interaction)
if not player.is_user_join(interaction.user):
return await send(interaction, "notInChannel", interaction.user.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(interaction, "notInChannel", interaction.user.mention, player.channel.mention, ephemeral=True)
await interaction.response.defer()
tracks = await player.get_tracks(query, requester=interaction.user)
if not tracks:
return await send(interaction, "noTrackFound")
return await send_localized_message(interaction, "noTrackFound")
try:
if isinstance(tracks, voicelink.Playlist):
index = await player.add_track(tracks.tracks)
await send(interaction, "playlistLoad", tracks.name, index)
await send_localized_message(interaction, "playlistLoad", tracks.name, index)
else:
position = await player.add_track(tracks[0])
texts = await get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad")
texts = await LangHandler.get_lang(interaction.guild.id, "live", "trackLoad_pos", "trackLoad")
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
await send(
await dispatch_message(
interaction,
stream_content + additional_content,
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
@@ -218,31 +211,31 @@ class Basic(commands.Cog):
)
@app_commands.choices(platform=[
app_commands.Choice(name=search_type.display_name, value=search_type.name)
for search_type in SearchType
for search_type in voicelink.SearchType
])
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def search(self, ctx: commands.Context, *, query: str, platform: str = SearchType.YOUTUBE.name):
async def search(self, ctx: commands.Context, *, query: str, platform: str = voicelink.SearchType.YOUTUBE.name):
"Searches your query and displays the results."
player: voicelink.Player = ctx.guild.voice_client
if not player:
player = await voicelink.connect_channel(ctx)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if url(query):
return await send(ctx, "noLinkSupport", ephemeral=True)
return await send_localized_message(ctx, "noLinkSupport", ephemeral=True)
search_type: SearchType = SearchType.match(platform) or SearchType.YOUTUBE
search_type: voicelink.SearchType = voicelink.SearchType.match(platform) or voicelink.SearchType.YOUTUBE
tracks = await player.get_tracks(query=query, requester=ctx.author, search_type=search_type)
if not tracks:
return await send(ctx, "noTrackFound")
return await send_localized_message(ctx, "noTrackFound")
texts = await get_lang(ctx.guild.id, "searchTitle", "searchDesc", "live", "trackLoad_pos", "trackLoad", "searchWait", "searchSuccess")
texts = await LangHandler.get_lang(ctx.guild.id, "searchTitle", "searchDesc", "live", "trackLoad_pos", "trackLoad", "searchWait", "searchSuccess")
query_track = "\n".join(f"`{index}.` `[{track.formatted_length}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1))
embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(get_source(search_type.display_name, "emoji"), search_type.display_name, len(tracks[0:10]), query_track), color=settings.embed_color)
embed = discord.Embed(title=texts[0].format(query), description=texts[1].format(Config().get_source_config(search_type.display_name, "emoji"), search_type.display_name, len(tracks[0:10]), query_track), color=Config().embed_color)
view = SearchView(tracks=tracks[0:10], texts=[texts[5], texts[6]])
view.response = await send(ctx, embed, view=view, ephemeral=True)
view.response = await dispatch_message(ctx, embed, view=view, ephemeral=True)
await view.wait()
if view.values is not None:
@@ -251,7 +244,7 @@ class Basic(commands.Cog):
track = tracks[int(value.split(". ")[0]) - 1]
position = await player.add_track(track)
msg += (f"`{texts[2]}`" if track.is_stream else "") + (texts[3].format(track.title, track.uri, track.author, track.formatted_length, position) if position >= 1 else texts[4].format(track.title, track.uri, track.author, track.formatted_length))
await send(ctx, msg)
await dispatch_message(ctx, msg)
if not player.is_playing:
await player.do_next()
@@ -271,27 +264,27 @@ class Basic(commands.Cog):
player = await voicelink.connect_channel(ctx)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if ctx.interaction:
await ctx.interaction.response.defer()
tracks = await player.get_tracks(query, requester=ctx.author)
if not tracks:
return await send(ctx, "noTrackFound")
return await send_localized_message(ctx, "noTrackFound")
try:
if isinstance(tracks, voicelink.Playlist):
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end), at_front=True)
await send(ctx, "playlistLoad", tracks.name, index)
index = await player.add_track(tracks.tracks, start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
await send_localized_message(ctx, "playlistLoad", tracks.name, index)
else:
position = await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
texts = await get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
position = await player.add_track(tracks[0], start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
texts = await LangHandler.get_lang(ctx.guild.id, "live", "trackLoad_pos", "trackLoad")
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
additional_content = texts[1] if position >= 1 and player.is_playing else texts[2]
await send(
await dispatch_message(
ctx,
stream_content + additional_content,
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
@@ -315,26 +308,26 @@ class Basic(commands.Cog):
player = await voicelink.connect_channel(ctx)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingFunctionPerm", ephemeral=True)
return await send_localized_message(ctx, "missingFunctionPerm", ephemeral=True)
if ctx.interaction:
await ctx.interaction.response.defer()
tracks = await player.get_tracks(query, requester=ctx.author)
if not tracks:
return await send(ctx, "noTrackFound")
return await send_localized_message(ctx, "noTrackFound")
try:
if isinstance(tracks, voicelink.Playlist):
index = await player.add_track(tracks.tracks, start_time=format_time(start), end_time=format_time(end), at_front=True)
await send(ctx, "playlistLoad", tracks.name, index)
index = await player.add_track(tracks.tracks, start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
await send_localized_message(ctx, "playlistLoad", tracks.name, index)
else:
texts = await get_lang(ctx.guild.id, "live", "trackLoad")
await player.add_track(tracks[0], start_time=format_time(start), end_time=format_time(end), at_front=True)
texts = await LangHandler.get_lang(ctx.guild.id, "live", "trackLoad")
await player.add_track(tracks[0], start_time=format_to_ms(start), end_time=format_to_ms(end), at_front=True)
stream_content = f"`{texts[0]}`" if tracks[0].is_stream else ""
await send(
await dispatch_message(
ctx,
stream_content + texts[1],
tracks[0].title, tracks[0].uri, tracks[0].author, tracks[0].formatted_length,
@@ -351,21 +344,21 @@ class Basic(commands.Cog):
"Pause the music."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if player.is_paused:
return await send(ctx, "pauseError", ephemeral=True)
return await send_localized_message(ctx, "pauseError", ephemeral=True)
if not player.is_privileged(ctx.author):
if ctx.author in player.pause_votes:
return await send(ctx, "voted", ephemeral=True)
return await send_localized_message(ctx, "voted", ephemeral=True)
player.pause_votes.add(ctx.author)
if len(player.pause_votes) < (required := player.required()):
return await send(ctx, "pauseVote", ctx.author, len(player.pause_votes), required)
return await send_localized_message(ctx, "pauseVote", ctx.author, len(player.pause_votes), required)
await player.set_pause(True, ctx.author)
await send(ctx, "paused", ctx.author)
await send_localized_message(ctx, "paused", ctx.author)
@commands.hybrid_command(name="resume", aliases=get_aliases("resume"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -373,21 +366,21 @@ class Basic(commands.Cog):
"Resume the music."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_paused:
return await send(ctx, "resumeError")
return await send_localized_message(ctx, "resumeError")
if not player.is_privileged(ctx.author):
if ctx.author in player.resume_votes:
return await send(ctx, "voted", ephemeral=True)
return await send_localized_message(ctx, "voted", ephemeral=True)
player.resume_votes.add(ctx.author)
if len(player.resume_votes) < (required := player.required()):
return await send(ctx, "resumeVote", ctx.author, len(player.resume_votes), required)
return await send_localized_message(ctx, "resumeVote", ctx.author, len(player.resume_votes), required)
await player.set_pause(False, ctx.author)
await send(ctx, "resumed", ctx.author)
await send_localized_message(ctx, "resumed", ctx.author)
@commands.hybrid_command(name="skip", aliases=get_aliases("skip"))
@app_commands.describe(index="Enter a index that you want to skip to.")
@@ -396,28 +389,28 @@ class Basic(commands.Cog):
"Skips to the next song or skips to the specified song."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.node._available:
return await send(ctx, "nodeReconnect")
return await send_localized_message(ctx, "nodeReconnect")
if not player.is_playing:
return await send(ctx, "skipError", ephemeral=True)
return await send_localized_message(ctx, "skipError", ephemeral=True)
if not player.is_privileged(ctx.author):
if ctx.author == player.current.requester:
pass
elif ctx.author in player.skip_votes:
return await send(ctx, "voted", ephemeral=True)
return await send_localized_message(ctx, "voted", ephemeral=True)
else:
player.skip_votes.add(ctx.author)
if len(player.skip_votes) < (required := player.required()):
return await send(ctx, "skipVote", ctx.author, len(player.skip_votes), required)
return await send_localized_message(ctx, "skipVote", ctx.author, len(player.skip_votes), required)
if index:
player.queue.skipto(index)
await send(ctx, "skipped", ctx.author)
await send_localized_message(ctx, "skipped", ctx.author)
if player.queue._repeat.mode == voicelink.LoopType.TRACK:
await player.set_repeat(voicelink.LoopType.OFF)
@@ -430,18 +423,18 @@ class Basic(commands.Cog):
"Skips back to the previous song or skips to the specified previous song."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.node._available:
return await send(ctx, "nodeReconnectode")
return await send_localized_message(ctx, "nodeReconnectode")
if not player.is_privileged(ctx.author):
if ctx.author in player.previous_votes:
return await send(ctx, "voted", ephemeral=True)
return await send_localized_message(ctx, "voted", ephemeral=True)
player.previous_votes.add(ctx.author)
if len(player.previous_votes) < (required := player.required()):
return await send(ctx, "backVote", ctx.author, len(player.previous_votes), required)
return await send_localized_message(ctx, "backVote", ctx.author, len(player.previous_votes), required)
if not player.is_playing:
player.queue.backto(index)
@@ -450,7 +443,7 @@ class Basic(commands.Cog):
player.queue.backto(index + 1)
await player.stop()
await send(ctx, "backed", ctx.author)
await send_localized_message(ctx, "backed", ctx.author)
if player.queue._repeat.mode == voicelink.LoopType.TRACK:
await player.set_repeat(voicelink.LoopType.OFF)
@@ -461,19 +454,19 @@ class Basic(commands.Cog):
"Change the player position."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingPosPerm", ephemeral=True)
return await send_localized_message(ctx, "missingPosPerm", ephemeral=True)
if not player.current or player.position == 0:
return await send(ctx, "noTrackPlaying", ephemeral=True)
return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True)
if not (num := format_time(position)):
return await send(ctx, "timeFormatError", ephemeral=True)
if not (num := format_to_ms(position)):
return await send_localized_message(ctx, "timeFormatError", ephemeral=True)
await player.seek(num, ctx.author)
await send(ctx, "seek", position)
await send_localized_message(ctx, "seek", position)
@commands.hybrid_group(
name="queue",
@@ -486,15 +479,15 @@ class Basic(commands.Cog):
"Display the players queue songs in your queue."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if player.queue.is_empty:
return await nowplay(ctx, player)
view = QueueView(player=player, author=ctx.author)
view.response = await send(ctx, await view.build_embed(), view=view)
view.response = await dispatch_message(ctx, await view.build_embed(), view=view)
@queue.command(name="export", aliases=get_aliases("export"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -502,13 +495,13 @@ class Basic(commands.Cog):
"Exports the entire queue to a text file"
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if player.queue.is_empty and not player.current:
return await send(ctx, "noTrackPlaying", ephemeral=True)
return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True)
await ctx.defer()
@@ -518,7 +511,7 @@ class Basic(commands.Cog):
total_length = 0
for index, track in enumerate(tracks, start=1):
temp += f"{index}. {track.title} [{ctime(track.length)}]\n"
temp += f"{index}. {track.title} [{format_ms(track.length)}]\n"
raw += track.track_id
if index != len(tracks):
raw += ","
@@ -527,7 +520,7 @@ class Basic(commands.Cog):
temp = "!Remember do not change this file!\n------------->Info<-------------\nGuild: {} ({})\nRequester: {} ({})\nTracks: {} - {}\n------------>Tracks<------------\n".format(
ctx.guild.name, ctx.guild.id,
ctx.author.display_name, ctx.author.id,
len(tracks), ctime(total_length)
len(tracks), format_ms(total_length)
) + temp
temp += raw
@@ -542,19 +535,19 @@ class Basic(commands.Cog):
player = await voicelink.connect_channel(ctx)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
try:
bytes = await attachment.read()
track_ids = bytes.split(b"\n")[-1]
track_ids = track_ids.decode().split(",")
tracks = [voicelink.Track(track_id=track_id, info=voicelink.decode(track_id), requester=ctx.author) for track_id in track_ids]
tracks = [voicelink.Track(track_id=track_id, info=voicelink.Track.decode(track_id), requester=ctx.author) for track_id in track_ids]
if not tracks:
return await send(ctx, "noTrackFound")
return await send_localized_message(ctx, "noTrackFound")
index = await player.add_track(tracks)
await send(ctx, "playlistLoad", attachment.filename, index)
await send_localized_message(ctx, "playlistLoad", attachment.filename, index)
except Exception as e:
logger.error("error", exc_info=e)
raise e
@@ -569,16 +562,16 @@ class Basic(commands.Cog):
"Display the players queue songs in your history queue."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if not player.queue.history():
return await nowplay(ctx, player)
view = QueueView(player=player, author=ctx.author, is_queue=False)
view.response = await send(ctx, await view.build_embed(), view=view)
view.response = await dispatch_message(ctx, await view.build_embed(), view=view)
@commands.hybrid_command(name="leave", aliases=get_aliases("leave"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -586,19 +579,19 @@ class Basic(commands.Cog):
"Disconnects the bot from your voice channel and chears the queue."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
if ctx.author in player.stop_votes:
return await send(ctx, "voted", ephemeral=True)
return await send_localized_message(ctx, "voted", ephemeral=True)
else:
player.stop_votes.add(ctx.author)
if len(player.stop_votes) >= (required := player.required(leave=True)):
pass
else:
return await send(ctx, "leaveVote", ctx.author, len(player.stop_votes), required)
return await send_localized_message(ctx, "leaveVote", ctx.author, len(player.stop_votes), required)
await send(ctx, "left", ctx.author)
await send_localized_message(ctx, "left", ctx.author)
await player.teardown()
@commands.hybrid_command(name="nowplaying", aliases=get_aliases("nowplaying"))
@@ -607,10 +600,10 @@ class Basic(commands.Cog):
"Shows details of the current track."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
await nowplay(ctx, player)
@@ -618,20 +611,20 @@ class Basic(commands.Cog):
@app_commands.describe(mode="Choose a looping mode.")
@app_commands.choices(mode=[
app_commands.Choice(name=loop_type.name.title(), value=loop_type.name)
for loop_type in LoopType
for loop_type in voicelink.LoopType
])
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def loop(self, ctx: commands.Context, mode: str):
"Changes Loop mode."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingModePerm", ephemeral=True)
return await send_localized_message(ctx, "missingModePerm", ephemeral=True)
await player.set_repeat(LoopType[mode] if mode in LoopType.__members__ else LoopType.OFF, ctx.author)
await send(ctx, "repeat", mode.capitalize())
await player.set_repeat(voicelink.LoopType[mode] if mode in voicelink.LoopType.__members__ else voicelink.LoopType.OFF, ctx.author)
await send_localized_message(ctx, "repeat", mode.capitalize())
@commands.hybrid_command(name="clear", aliases=get_aliases("clear"))
@app_commands.describe(queue="Choose a queue that you want to clear.")
@@ -644,13 +637,13 @@ class Basic(commands.Cog):
"Remove all the tracks in your queue or history queue."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingQueuePerm", ephemeral=True)
return await send_localized_message(ctx, "missingQueuePerm", ephemeral=True)
await player.clear_queue(queue, ctx.author)
await send(ctx, "cleared", queue.capitalize())
await send_localized_message(ctx, "cleared", queue.capitalize())
@commands.hybrid_command(name="remove", aliases=get_aliases("remove"))
@app_commands.describe(
@@ -663,13 +656,13 @@ class Basic(commands.Cog):
"Removes specified track or a range of tracks from the queue."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingQueuePerm", ephemeral=True)
return await send_localized_message(ctx, "missingQueuePerm", ephemeral=True)
removed_tracks = await player.remove_track(position1, position2, remove_target=member, requester=ctx.author)
await send(ctx, "removed", len(removed_tracks.keys()))
await send_localized_message(ctx, "removed", len(removed_tracks.keys()))
@commands.hybrid_command(name="forward", aliases=get_aliases("forward"))
@app_commands.describe(position="Input an amount that you to forward to. Exmaple: 1:20")
@@ -678,19 +671,19 @@ class Basic(commands.Cog):
"Forwards by a certain amount of time in the current track. The default is 10 seconds."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingPosPerm", ephemeral=True)
return await send_localized_message(ctx, "missingPosPerm", ephemeral=True)
if not player.current:
return await send(ctx, "noTrackPlaying", ephemeral=True)
return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True)
if not (num := format_time(position)):
return await send(ctx, "timeFormatError", ephemeral=True)
if not (num := format_to_ms(position)):
return await send_localized_message(ctx, "timeFormatError", ephemeral=True)
await player.seek(int(player.position + num))
await send(ctx, "forward", ctime(player.position + num))
await send_localized_message(ctx, "forward", format_ms(player.position + num))
@commands.hybrid_command(name="rewind", aliases=get_aliases("rewind"))
@app_commands.describe(position="Input an amount that you to rewind to. Exmaple: 1:20")
@@ -699,19 +692,19 @@ class Basic(commands.Cog):
"Rewind by a certain amount of time in the current track. The default is 10 seconds."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingPosPerm", ephemeral=True)
return await send_localized_message(ctx, "missingPosPerm", ephemeral=True)
if not player.current:
return await send(ctx, "noTrackPlaying", ephemeral=True)
return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True)
if not (num := format_time(position)):
return await send(ctx, "timeFormatError", ephemeral=True)
if not (num := format_to_ms(position)):
return await send_localized_message(ctx, "timeFormatError", ephemeral=True)
await player.seek(int(player.position - num))
await send(ctx, "rewind", ctime(player.position - num))
await send_localized_message(ctx, "rewind", format_ms(player.position - num))
@commands.hybrid_command(name="replay", aliases=get_aliases("replay"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -719,16 +712,16 @@ class Basic(commands.Cog):
"Reset the progress of the current song."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingPosPerm", ephemeral=True)
return await send_localized_message(ctx, "missingPosPerm", ephemeral=True)
if not player.current:
return await send(ctx, "noTrackPlaying", ephemeral=True)
return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True)
await player.seek(0)
await send(ctx, "replay")
await send_localized_message(ctx, "replay")
@commands.hybrid_command(name="shuffle", aliases=get_aliases("shuffle"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -736,18 +729,18 @@ class Basic(commands.Cog):
"Randomizes the tracks in the queue."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
if ctx.author in player.shuffle_votes:
return await send(ctx, "voted", ephemeral=True)
return await send_localized_message(ctx, "voted", ephemeral=True)
player.shuffle_votes.add(ctx.author)
if len(player.shuffle_votes) < (required := player.required()):
return await send(ctx, "shuffleVote", ctx.author, len(player.shuffle_votes), required)
return await send_localized_message(ctx, "shuffleVote", ctx.author, len(player.shuffle_votes), required)
await player.shuffle("queue", ctx.author)
await send(ctx, "shuffled")
await send_localized_message(ctx, "shuffled")
@commands.hybrid_command(name="swap", aliases=get_aliases("swap"))
@app_commands.describe(
@@ -759,13 +752,13 @@ class Basic(commands.Cog):
"Swaps the specified song to the specified song."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingPosPerm", ephemeral=True)
return await send_localized_message(ctx, "missingPosPerm", ephemeral=True)
track1, track2 = await player.swap_track(position1, position2, ctx.author)
await send(ctx, "swapped", track1.title, track2.title)
await send_localized_message(ctx, "swapped", track1.title, track2.title)
@commands.hybrid_command(name="move", aliases=get_aliases("move"))
@app_commands.describe(
@@ -777,13 +770,13 @@ class Basic(commands.Cog):
"Moves the specified song to the specified position."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingPosPerm", ephemeral=True)
return await send_localized_message(ctx, "missingPosPerm", ephemeral=True)
moved_track = await player.move_track(target, to, ctx.author)
await send(ctx, "moved", moved_track, to)
await send_localized_message(ctx, "moved", moved_track, to)
@commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics"))
@app_commands.describe(title="Searches for your query and displays the reutned lyrics.")
@@ -793,20 +786,20 @@ class Basic(commands.Cog):
if not title:
player: voicelink.Player = ctx.guild.voice_client
if not player or not player.is_playing:
return await send(ctx, "noTrackPlaying", ephemeral=True)
return await send_localized_message(ctx, "noTrackPlaying", ephemeral=True)
title = player.current.title
artist = player.current.author
await ctx.defer()
lyrics_platform = LYRICS_PLATFORMS.get(settings.lyrics_platform)
lyrics_platform = voicelink.LYRICS_PLATFORMS.get(Config().lyrics_platform)
if lyrics_platform:
lyrics = await lyrics_platform().get_lyrics(title, artist)
if not lyrics:
return await send(ctx, "lyricsNotFound", ephemeral=True)
return await send_localized_message(ctx, "lyricsNotFound", ephemeral=True)
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=ctx.author)
view.response = await send(ctx, view.build_embed(), view=view)
view.response = await dispatch_message(ctx, await 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.")
@@ -815,22 +808,22 @@ class Basic(commands.Cog):
"Transfer dj to another."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_user_join(ctx.author):
return await send(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
return await send_localized_message(ctx, "notInChannel", ctx.author.mention, player.channel.mention, ephemeral=True)
if player.dj.id != ctx.author.id or player.settings.get('dj', False):
return await send(ctx, "notdj", f"<@&{player.settings['dj']}>" if player.settings.get('dj') else player.dj.mention, ephemeral=True)
return await send_localized_message(ctx, "notdj", f"<@&{player.settings['dj']}>" if player.settings.get('dj') else player.dj.mention, ephemeral=True)
if player.dj.id == member.id or member.bot:
return await send(ctx, "djToMe", ephemeral=True)
return await send_localized_message(ctx, "djToMe", ephemeral=True)
if member not in player.channel.members:
return await send(ctx, "djNotInChannel", member, ephemeral=True)
return await send_localized_message(ctx, "djNotInChannel", member, ephemeral=True)
player.dj = member
await send(ctx, "djswap", member)
await send_localized_message(ctx, "djswap", member)
@commands.hybrid_command(name="autoplay", aliases=get_aliases("autoplay"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -838,14 +831,14 @@ class Basic(commands.Cog):
"Toggles autoplay mode, it will automatically queue the best songs to play."
player: voicelink.Player = ctx.guild.voice_client
if not player:
return await send(ctx, "noPlayer", ephemeral=True)
return await send_localized_message(ctx, "noPlayer", ephemeral=True)
if not player.is_privileged(ctx.author):
return await send(ctx, "missingAutoPlayPerm", ephemeral=True)
return await send_localized_message(ctx, "missingAutoPlayPerm", ephemeral=True)
check = not player.settings.get("autoplay", False)
player.settings['autoplay'] = check
await send(ctx, "autoplay", await get_lang(ctx.guild.id, "enabled" if check else "disabled"))
await send_localized_message(ctx, "autoplay", await LangHandler.get_lang(ctx.guild.id, "enabled" if check else "disabled"))
if not player.is_playing:
await player.do_next()
@@ -862,7 +855,7 @@ class Basic(commands.Cog):
category = "News"
view = HelpView(self.bot, ctx.author)
embed = view.build_embed(category)
view.response = await send(ctx, embed, view=view)
view.response = await dispatch_message(ctx, embed, view=view)
@commands.hybrid_command(name="ping", aliases=get_aliases("ping"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -870,9 +863,9 @@ class Basic(commands.Cog):
"Test if the bot is alive, and see the delay between your commands and my response."
player: voicelink.Player = ctx.guild.voice_client
value = await get_lang(ctx.guild.id, "pingTitle1", "pingField1", "pingTitle2", "pingField2")
value = await LangHandler.get_lang(ctx.guild.id, "pingTitle1", "pingField1", "pingTitle2", "pingField2")
embed = discord.Embed(color=settings.embed_color)
embed = discord.Embed(color=Config().embed_color)
embed.add_field(
name=value[0],
value=value[1].format(
@@ -887,7 +880,7 @@ class Basic(commands.Cog):
inline=False
)
await send(ctx, embed)
await dispatch_message(ctx, embed)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Basic(bot))

View File

@@ -25,24 +25,24 @@ import discord
import voicelink
from function import (
send,
get_lang,
get_aliases,
cooldown_check
)
from discord import app_commands
from discord.ext import commands
from voicelink import LangHandler
from voicelink.utils import send_localized_message
async def check_access(ctx: commands.Context):
player: voicelink.Player = ctx.guild.voice_client
if not player:
text = await get_lang(ctx.guild.id, "noPlayer")
text = await LangHandler.get_lang(ctx.guild.id, "noPlayer")
raise voicelink.exceptions.VoicelinkException(text)
if ctx.author not in player.channel.members:
if not ctx.author.guild_permissions.manage_guild:
text = await get_lang(ctx.guild.id, "notInChannel")
text = await LangHandler.get_lang(ctx.guild.id, "notInChannel")
raise voicelink.exceptions.VoicelinkException(text.format(ctx.author.mention, player.channel.mention))
return player
@@ -72,7 +72,7 @@ class Effect(commands.Cog):
effect = voicelink.Timescale(tag="speed", speed=value)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="karaoke", aliases=get_aliases("karaoke"))
@app_commands.describe(
@@ -91,7 +91,7 @@ class Effect(commands.Cog):
effect = voicelink.Karaoke(tag="karaoke", level=level, mono_level=monolevel, filter_band=filterband, filter_width=filterwidth)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="tremolo", aliases=get_aliases("tremolo"))
@app_commands.describe(
@@ -108,7 +108,7 @@ class Effect(commands.Cog):
effect = voicelink.Tremolo(tag="tremolo", frequency=frequency, depth=depth)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="vibrato", aliases=get_aliases("vibrato"))
@app_commands.describe(
@@ -125,7 +125,7 @@ class Effect(commands.Cog):
effect = voicelink.Vibrato(tag="vibrato", frequency=frequency, depth=depth)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="rotation", aliases=get_aliases("rotation"))
@app_commands.describe(hertz="The hertz of the rotation. Default is `0.2`")
@@ -139,7 +139,7 @@ class Effect(commands.Cog):
effect = voicelink.Rotation(tag="rotation", rotation_hertz=hertz)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="distortion", aliases=get_aliases("distortion"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -152,7 +152,7 @@ class Effect(commands.Cog):
effect = voicelink.Distortion(tag="distortion", sin_offset=0.0, sin_scale=1.0, cos_offset=0.0, cos_scale=1.0, tan_offset=0.0, tan_scale=1.0, offset=0.0, scale=1.0)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="lowpass", aliases=get_aliases("lowpass"))
@app_commands.describe(smoothing="The level of the lowPass. Default is `20.0`")
@@ -166,7 +166,7 @@ class Effect(commands.Cog):
effect = voicelink.LowPass(tag="lowpass", smoothing=smoothing)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="channelmix", aliases=get_aliases("channelmix"))
@app_commands.describe(
@@ -185,7 +185,7 @@ class Effect(commands.Cog):
effect = voicelink.ChannelMix(tag="channelmix", left_to_left=left_to_left, right_to_right=right_to_right, left_to_right=left_to_right, right_to_left=right_to_left)
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="nightcore", aliases=get_aliases("nightcore"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -195,7 +195,7 @@ class Effect(commands.Cog):
effect = voicelink.Timescale.nightcore()
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="8d", aliases=get_aliases("8d"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -205,7 +205,7 @@ class Effect(commands.Cog):
effect = voicelink.Rotation.nightD()
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="vaporwave", aliases=get_aliases("vaporwave"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -215,7 +215,7 @@ class Effect(commands.Cog):
effect = voicelink.Timescale.vaporwave()
await player.add_filter(effect, ctx.author)
await send(ctx, "addEffect", effect.tag)
await send_localized_message(ctx, "addEffect", effect.tag)
@commands.hybrid_command(name="cleareffect", aliases=get_aliases("cleareffect"))
@app_commands.describe(effect="Remove a specific sound effects.")
@@ -230,7 +230,7 @@ class Effect(commands.Cog):
else:
await player.reset_filter()
await send(ctx, "clearEffect")
await send_localized_message(ctx, "clearEffect")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Effect(bot))

View File

@@ -29,6 +29,9 @@ import function as func
from discord.ext import commands
from voicelink import MongoDBHandler, Config
from voicelink.utils import TempCtx
class Listeners(commands.Cog):
"""Music Cog."""
@@ -41,20 +44,16 @@ class Listeners(commands.Cog):
async def start_nodes(self) -> None:
"""Connect and intiate nodes."""
for n in func.settings.nodes.values():
for n in Config().nodes.values():
try:
await self.voicelink.create_node(
bot=self.bot,
logger=func.logger,
**n
)
await self.voicelink.create_node(bot=self.bot, **n)
except Exception as e:
func.logger.error(f'Node {n["identifier"]} is not able to connect! - Reason: {e}')
async def restore_last_session_players(self) -> None:
"""Re-establish connections for players from the last session."""
await self.bot.wait_until_ready()
players = func.open_json(func.LAST_SESSION_FILE_NAME)
players = func.open_json(Config.LAST_SESSION_FILE_DIR)
if not players:
return
@@ -75,11 +74,11 @@ class Listeners(commands.Cog):
continue
# Get the guild settings
settings = await func.get_settings(channel.guild.id)
settings = await MongoDBHandler.get_settings(channel.guild.id)
# Connect to the channel and initialize the player.
player: voicelink.Player = await channel.connect(
cls=voicelink.Player(self.bot, channel, func.TempCtx(dj_member, channel), settings)
cls=voicelink.Player(self.bot, channel, TempCtx(dj_member, channel), settings)
)
# Restore the queue.
@@ -89,7 +88,7 @@ class Listeners(commands.Cog):
if not track_id:
continue
decoded_track = voicelink.decode(track_id)
decoded_track = voicelink.Track.decode(track_id)
requester = channel.guild.get_member(track_data.get("requester_id"))
track = voicelink.Track(track_id=track_id, info=decoded_track, requester=requester)
player.queue._queue.append(track)
@@ -125,12 +124,11 @@ class Listeners(commands.Cog):
# Delete the last session file if it exists.
try:
file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
if os.path.exists(file_path):
os.remove(file_path)
if os.path.exists(Config.LAST_SESSION_FILE_DIR):
os.remove(Config.LAST_SESSION_FILE_DIR)
except Exception as del_error:
func.logger.error("Failed to remove session file: %s", file_path, exc_info=del_error)
func.logger.error("Failed to remove session file: %s", Config.LAST_SESSION_FILE_DIR, exc_info=del_error)
@commands.Cog.listener()
async def on_voicelink_track_end(self, player: voicelink.Player, track, _):

View File

@@ -21,23 +21,22 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import discord, voicelink, time
import time
import discord
import voicelink
from io import StringIO
from discord import app_commands
from discord.ext import commands
from function import (
send,
time as ctime,
get_user,
update_user,
check_roles,
get_aliases,
cooldown_check,
logger
)
from views import PlaylistViewManager, InboxView, HelpView
from voicelink import MongoDBHandler, Config
from voicelink.views import PlaylistViewManager, InboxView, HelpView
from voicelink.utils import format_ms, dispatch_message, send_localized_message
def assign_playlist_id(existed: list) -> str:
for i in range(200, 210):
@@ -46,7 +45,7 @@ def assign_playlist_id(existed: list) -> str:
async def check_playlist_perms(user_id: int, author_id: int, playlist_id: str) -> dict:
"""Check if user has read permissions for a specific playlist."""
user_data = await get_user(author_id, 'playlist')
user_data = await MongoDBHandler.get_user(author_id, d_type='playlist')
playlist = user_data.get(playlist_id)
if not playlist or user_id not in playlist['perms']['read']:
@@ -56,7 +55,7 @@ async def check_playlist_perms(user_id: int, author_id: int, playlist_id: str) -
async def check_playlist(ctx: commands.Context, name: str = None, full: bool = False, share: bool = True) -> dict:
"""Get user's playlist data with various filtering options."""
user_playlists = await get_user(ctx.author.id, 'playlist')
user_playlists = await MongoDBHandler.get_user(ctx.author.id, d_type='playlist')
if not ctx.interaction.response.is_done():
await ctx.defer()
@@ -90,7 +89,7 @@ async def search_playlist(url: str, requester: discord.Member, time_needed: bool
result = {"name": tracks.name, "tracks": tracks.tracks}
if time_needed:
result["time"] = ctime(sum(track.length for track in tracks.tracks))
result["time"] = format_ms(sum(track.length for track in tracks.tracks))
return result
except Exception:
@@ -135,7 +134,7 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist
)
if not shared_playlist:
await update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}})
await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}})
return None
if shared_playlist['type'] == 'link':
@@ -157,14 +156,14 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist
decoded_tracks = []
total_time = 0
for track in shared_playlist['tracks']:
decoded_track = voicelink.decode(track)
decoded_track = voicelink.Track.decode(track)
total_time += decoded_track.get("length", 0)
decoded_tracks.append(decoded_track)
return {
'emoji': emoji,
'id': playlist_id,
'time': ctime(total_time),
'time': format_ms(total_time),
'name': playlist_data['name'],
'tracks': decoded_tracks,
'perms': shared_playlist['perms'],
@@ -175,14 +174,14 @@ async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist
decoded_tracks = []
total_time = 0
for track in playlist_data['tracks']:
decoded_track = voicelink.decode(track)
decoded_track = voicelink.Track.decode(track)
total_time += decoded_track.get("length", 0)
decoded_tracks.append(decoded_track)
return {
'emoji': emoji,
'id': playlist_id,
'time': ctime(total_time),
'time': format_ms(total_time),
'name': playlist_data['name'],
'tracks': decoded_tracks,
'perms': playlist_data['perms'],
@@ -196,7 +195,7 @@ 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_raw: dict[str, dict] = await get_user(interaction.user.id, 'playlist')
playlists_raw: dict[str, dict] = await MongoDBHandler.get_user(interaction.user.id, d_type='playlist')
playlists = [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]
@@ -210,7 +209,7 @@ class Playlists(commands.Cog, name="playlist"):
async def playlist(self, ctx: commands.Context):
view = HelpView(self.bot, ctx.author)
embed = view.build_embed(self.qualified_name)
view.response = send(ctx, embed, view=view)
view.response = dispatch_message(ctx, embed, view=view)
@playlist.command(name="play", aliases=get_aliases("play"))
@app_commands.describe(
@@ -224,10 +223,10 @@ class Playlists(commands.Cog, name="playlist"):
result = await check_playlist(ctx, name.lower() if name else None)
if not result['playlist']:
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
rank, max_p, max_t = check_roles()
return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True)
max_p, max_t, _ = Config().get_playlist_config()
if result['position'] > max_p:
return await send(ctx, 'playlistNotAccess', ephemeral=True)
return await send_localized_message(ctx, 'playlistNotAccess', ephemeral=True)
player: voicelink.Player = ctx.guild.voice_client
if not player:
@@ -237,21 +236,21 @@ class Playlists(commands.Cog, name="playlist"):
tracks = await search_playlist(result['playlist']['uri'], ctx.author, time_needed=False)
else:
if not result['playlist']['tracks']:
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
return await send_localized_message(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
_tracks = []
for track in result['playlist']['tracks'][:max_t]:
_tracks.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author))
_tracks.append(voicelink.Track(track_id=track, info=voicelink.Track.decode(track), requester=ctx.author))
tracks = {"name": result['playlist']['name'], "tracks": _tracks}
if not tracks:
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
return await send_localized_message(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
if value and 0 < value <= (len(tracks['tracks'])):
tracks['tracks'] = [tracks['tracks'][value - 1]]
await player.add_track(tracks['tracks'])
await send(ctx, 'playlistPlay', result['playlist']['name'], len(tracks['tracks'][:max_t]))
await send_localized_message(ctx, 'playlistPlay', result['playlist']['name'], len(tracks['tracks'][:max_t]))
if not player.is_playing:
await player.do_next()
@@ -261,13 +260,13 @@ class Playlists(commands.Cog, name="playlist"):
async def view(self, ctx: commands.Context) -> None:
"""List all your playlists and all songs in your favourite playlist."""
user_playlists = await check_playlist(ctx, full=True)
_, max_playlists, _ = check_roles()
max_p, _, _ = Config().get_playlist_config()
playlist_results = []
for index, playlist_id in enumerate(user_playlists, start=1):
playlist_data = user_playlists[playlist_id]
is_locked = max_playlists < index
is_locked = max_p < index
try:
result = await _process_playlist(ctx, playlist_data, playlist_id, is_locked)
@@ -284,7 +283,7 @@ class Playlists(commands.Cog, name="playlist"):
})
view = PlaylistViewManager(ctx, playlist_results)
view.response = await send(ctx, content=await view.build_embed(), view=view, ephemeral=True)
view.response = await dispatch_message(ctx, content=await view.build_embed(), view=view, ephemeral=True)
@playlist.command(name="create", aliases=get_aliases("create"))
@app_commands.describe(
@@ -295,25 +294,25 @@ class Playlists(commands.Cog, name="playlist"):
async def create(self, ctx: commands.Context, name: str, link: str = None):
"Create your custom playlist."
if len(name) > 10:
return await send(ctx, 'playlistOverText', ephemeral=True)
return await send_localized_message(ctx, 'playlistOverText', ephemeral=True)
rank, max_p, max_t = check_roles()
max_p, _, _ = Config().get_playlist_config()
user = await check_playlist(ctx, full=True)
if len(user) >= max_p:
return await send(ctx, 'overPlaylistCreation', max_p, ephemeral=True)
return await send_localized_message(ctx, 'overPlaylistCreation', max_p, ephemeral=True)
for data in user:
if user[data]['name'].lower() == name.lower():
return await send(ctx, 'playlistExists', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistExists', name, ephemeral=True)
if link:
tracks = await voicelink.NodePool.get_node().get_tracks(link, requester=ctx.author)
if not isinstance(tracks, voicelink.Playlist):
return await send(ctx, "playlistNotInvalidUrl", ephemeral=True)
return await send_localized_message(ctx, "playlistNotInvalidUrl", ephemeral=True)
data = {'uri': link, 'perms': {'read': []}, 'name': name, 'type': 'link'} if link else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
await send(ctx, "playlistCreated", name)
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
await send_localized_message(ctx, "playlistCreated", name)
@playlist.command(name="delete", aliases=get_aliases("delete"))
@app_commands.describe(name="The name of the playlist.")
@@ -323,15 +322,15 @@ class Playlists(commands.Cog, name="playlist"):
"Delete your custom playlist."
result = await check_playlist(ctx, name.lower(), share=False)
if not result['playlist']:
return await ctx(ctx, "playlistNotFound", name, ephemeral=True)
return await send_localized_message(ctx, "playlistNotFound", name, ephemeral=True)
if result['id'] == "200":
return await send(ctx, "playlistDeleteError", ephemeral=True)
return await send_localized_message(ctx, "playlistDeleteError", ephemeral=True)
if result['playlist']['type'] == 'share':
await update_user(result['playlist']['user'], {"$pull": {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}})
await MongoDBHandler.update_user(result['playlist']['user'], {"$pull": {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}})
await update_user(ctx.author.id, {"$unset": {f"playlist.{result['id']}": 1}})
return await send(ctx, "playlistRemove", result["playlist"]["name"])
await MongoDBHandler.update_user(ctx.author.id, {"$unset": {f"playlist.{result['id']}": 1}})
return await send_localized_message(ctx, "playlistRemove", result["playlist"]["name"])
@playlist.command(name="share", aliases=get_aliases("share"))
@app_commands.describe(
@@ -343,28 +342,28 @@ class Playlists(commands.Cog, name="playlist"):
async def share(self, ctx: commands.Context, member: discord.Member, name: str):
"Share your custom playlist with your friends."
if member.id == ctx.author.id:
return await send(ctx, 'playlistSendErrorPlayer', ephemeral=True)
return await send_localized_message(ctx, 'playlistSendErrorPlayer', ephemeral=True)
if member.bot:
return await send(ctx, 'playlistSendErrorBot', ephemeral=True)
return await send_localized_message(ctx, 'playlistSendErrorBot', ephemeral=True)
result = await check_playlist(ctx, name.lower(), share=False)
if not result['playlist']:
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True)
if result['playlist']['type'] == 'share':
return await send(ctx, 'playlistBelongs', result['playlist']['user'], ephemeral=True)
return await send_localized_message(ctx, 'playlistBelongs', result['playlist']['user'], ephemeral=True)
if member.id in result['playlist']['perms']['read']:
return await send(ctx, 'playlistShare', member, ephemeral=True)
return await send_localized_message(ctx, 'playlistShare', member, ephemeral=True)
receiver = await get_user(member.id)
receiver = await MongoDBHandler.get_user(member.id)
if not receiver:
return await send(ctx, 'noPlaylistAcc', member)
return await send_localized_message(ctx, 'noPlaylistAcc', member)
for mail in receiver['inbox']:
if mail['sender'] == ctx.author.id and mail['referId'] == result['id']:
return await send(ctx, 'playlistSent', ephemeral=True)
return await send_localized_message(ctx, 'playlistSent', ephemeral=True)
if len(receiver['inbox']) >= 10:
return await send(ctx.guild.id, 'inboxFull', member, ephemeral=True)
return await send_localized_message(ctx.guild.id, 'inboxFull', member, ephemeral=True)
await update_user(
await MongoDBHandler.update_user(
member.id,
{"$push": {"inbox": {
'sender': ctx.author.id,
@@ -375,7 +374,7 @@ class Playlists(commands.Cog, name="playlist"):
'type': 'invite'
}}}
)
return await send(ctx, "invitationSent", member)
return await send_localized_message(ctx, "invitationSent", member)
@playlist.command(name="rename", aliases=get_aliases("rename"))
@app_commands.describe(
@@ -387,36 +386,36 @@ class Playlists(commands.Cog, name="playlist"):
async def rename(self, ctx: commands.Context, name: str, newname: str) -> None:
"Rename your custom playlist."
if len(newname) > 10:
return await send(ctx, 'playlistOverText', ephemeral=True)
return await send_localized_message(ctx, 'playlistOverText', ephemeral=True)
if name.lower() == newname.lower():
return await send(ctx, 'playlistSameName', ephemeral=True)
return await send_localized_message(ctx, 'playlistSameName', ephemeral=True)
user = await check_playlist(ctx, full=True)
found, id = False, 0
for data in user:
if user[data]['name'].lower() == name.lower():
found, id = True, data
if user[data]['name'].lower() == newname.lower():
return await send(ctx, 'playlistExists', ephemeral=True)
return await send_localized_message(ctx, 'playlistExists', ephemeral=True)
if not found:
return await send(ctx.guild.id, 'playlistNotFound', name, ephemeral=True)
return await send_localized_message(ctx.guild.id, 'playlistNotFound', name, ephemeral=True)
await update_user(ctx.author.id, {"$set": {f'playlist.{id}.name': newname}})
await send(ctx, 'playlistRenamed', name, newname)
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{id}.name': newname}})
await send_localized_message(ctx, 'playlistRenamed', name, newname)
@playlist.command(name="inbox", aliases=get_aliases("inbox"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def inbox(self, ctx: commands.Context) -> None:
"Show your playlist invitation."
user = await get_user(ctx.author.id)
rank, max_p, max_t = check_roles()
user = await MongoDBHandler.get_user(ctx.author.id)
max_p, _, _ = Config().get_playlist_config()
if not user['inbox']:
return await send(ctx, 'inboxNoMsg', ephemeral=True)
return await send_localized_message(ctx, "inboxNoMsg", ephemeral=True)
inbox = user['inbox'].copy()
view = InboxView(ctx.author, user['inbox'])
view.response = await send(ctx, view.build_embed(), view=view, ephemeral=True)
view.response = await dispatch_message(ctx, view.build_embed(), view=view, ephemeral=True)
await view.wait()
if inbox == user['inbox']:
@@ -425,7 +424,7 @@ class Playlists(commands.Cog, name="playlist"):
update_data, dId = {}, {dId for dId in user["playlist"]}
for data in view.new_playlist[:(max_p - len(user['playlist']))]:
addId = assign_playlist_id(dId)
await update_user(data['sender'], {"$push": {f"playlist.{data['referId']}.perms.read": ctx.author.id}})
await MongoDBHandler.update_user(data['sender'], {"$push": {f"playlist.{data['referId']}.perms.read": ctx.author.id}})
update_data[f'playlist.{addId}'] = {
'user': data['sender'], 'referId': data['referId'],
'name': f"Share{time.strftime('%M%S', time.gmtime(int(data['time'])))}",
@@ -435,7 +434,7 @@ class Playlists(commands.Cog, name="playlist"):
dId.add(addId)
if update_data:
await update_user(ctx.author.id, {"$set": update_data})
await MongoDBHandler.update_user(ctx.author.id, {"$set": update_data})
@playlist.command(name="add", aliases=get_aliases("add"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -448,26 +447,26 @@ class Playlists(commands.Cog, name="playlist"):
"Add tracks in to your custom playlist."
result = await check_playlist(ctx, name.lower(), share=False)
if not result['playlist']:
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True)
if result['playlist']['type'] in ['share', 'link']:
return await send(ctx, 'playlistNotAllow', ephemeral=True)
return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True)
rank, max_p, max_t = check_roles()
_, max_t, _ = Config().get_playlist_config()
if len(result['playlist']['tracks']) >= max_t:
return await send(ctx, 'playlistLimitTrack', max_t, ephemeral=True)
return await send_localized_message(ctx, 'playlistLimitTrack', max_t, ephemeral=True)
results = await voicelink.NodePool.get_node().get_tracks(query, requester=ctx.author)
if not results:
return await send(ctx, 'noTrackFound')
return await send_localized_message(ctx, 'noTrackFound')
if isinstance(results, voicelink.Playlist):
return await send(ctx, 'playlistPlaylistLink', ephemeral=True)
return await send_localized_message(ctx, 'playlistPlaylistLink', ephemeral=True)
if results[0].is_stream:
return await send(ctx, 'playlistStream', ephemeral=True)
return await send_localized_message(ctx, 'playlistStream', ephemeral=True)
await update_user(ctx.author.id, {"$push": {f'playlist.{result["id"]}.tracks': results[0].track_id}})
await send(ctx, 'playlistAdded', results[0].title, ctx.author, result['playlist']['name'])
await MongoDBHandler.update_user(ctx.author.id, {"$push": {f'playlist.{result["id"]}.tracks': results[0].track_id}})
await send_localized_message(ctx, 'playlistAdded', results[0].title, ctx.author, result['playlist']['name'])
@playlist.command(name="remove", aliases=get_aliases("remove"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -480,16 +479,16 @@ class Playlists(commands.Cog, name="playlist"):
"Remove song from your favorite playlist."
result = await check_playlist(ctx, name.lower(), share=False)
if not result['playlist']:
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True)
if result['playlist']['type'] in ['link', 'share']:
return await send(ctx, 'playlistNotAllow', ephemeral=True)
return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True)
if not 0 < position <= len(result['playlist']['tracks']):
return await send(ctx, 'playlistPositionNotFound', position, name)
return await send_localized_message(ctx, 'playlistPositionNotFound', position, name)
await update_user(ctx.author.id, {"$pull": {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}})
await MongoDBHandler.update_user(ctx.author.id, {"$pull": {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}})
track = voicelink.decode(result['playlist']['tracks'][position - 1])
await send(ctx, 'playlistRemoved', track.get("title"), ctx.author, name)
track = voicelink.Track.decode(result['playlist']['tracks'][position - 1])
await send_localized_message(ctx, 'playlistRemoved', track.get("title"), ctx.author, name)
@playlist.command(name="clear", aliases=get_aliases("clear"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -498,13 +497,13 @@ class Playlists(commands.Cog, name="playlist"):
"Remove all songs from your favorite playlist."
result = await check_playlist(ctx, name.lower(), share=False)
if not result['playlist']:
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True)
if result['playlist']['type'] in ['link', 'share']:
return await send(ctx, 'playlistNotAllow', ephemeral=True)
return await send_localized_message(ctx, 'playlistNotAllow', ephemeral=True)
await update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}})
await send(ctx, 'playlistClear', name)
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}})
await send_localized_message(ctx, 'playlistClear', name)
@playlist.command(name="export", aliases=get_aliases("export"))
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
@@ -513,29 +512,29 @@ class Playlists(commands.Cog, name="playlist"):
"Exports the entire playlist to a text file"
result = await check_playlist(ctx, name.lower())
if not result['playlist']:
return await send(ctx, 'playlistNotFound', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistNotFound', name, ephemeral=True)
if result['playlist']['type'] == 'link':
tracks = await search_playlist(result['playlist']['uri'], ctx.author, time_needed=False)
else:
if not result['playlist']['tracks']:
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
return await send_localized_message(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
_tracks = []
for track in result['playlist']['tracks']:
_tracks.append(voicelink.Track(track_id=track, info=voicelink.decode(track), requester=ctx.author))
_tracks.append(voicelink.Track(track_id=track, info=voicelink.Track.decode(track), requester=ctx.author))
tracks = {"name": result['playlist']['name'], "tracks": _tracks}
if not tracks:
return await send(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
return await send_localized_message(ctx, 'playlistNoTrack', result['playlist']['name'], ephemeral=True)
temp = ""
raw = "----------->Raw Info<-----------\n"
total_length = 0
for index, track in enumerate(tracks['tracks'], start=1):
temp += f"{index}. {track.title} [{ctime(track.length)}]\n"
temp += f"{index}. {track.title} [{format_ms(track.length)}]\n"
raw += track.track_id
if index != len(tracks['tracks']):
raw += ","
@@ -544,7 +543,7 @@ class Playlists(commands.Cog, name="playlist"):
temp = "!Remember do not change this file!\n------------->Info<-------------\nPlaylist: {} ({})\nRequester: {} ({})\nTracks: {} - {}\n------------>Tracks<------------\n".format(
tracks['name'], result['playlist']['type'],
ctx.author.display_name, ctx.author.id,
len(tracks['tracks']), ctime(total_length)
len(tracks['tracks']), format_ms(total_length)
) + temp
temp += raw
@@ -556,17 +555,17 @@ class Playlists(commands.Cog, name="playlist"):
async def _import(self, ctx: commands.Context, name: str, attachment: discord.Attachment):
"Create your custom playlist."
if len(name) > 10:
return await send(ctx, 'playlistOverText', ephemeral=True)
return await send_localized_message(ctx, 'playlistOverText', ephemeral=True)
rank, max_p, max_t = check_roles()
max_p, _, _ = Config().get_playlist_config()
user = await check_playlist(ctx, full=True)
if len(user) >= max_p:
return await send(ctx, 'overPlaylistCreation', max_p, ephemeral=True)
return await send_localized_message(ctx, 'overPlaylistCreation', max_p, ephemeral=True)
for data in user:
if user[data]['name'].lower() == name.lower():
return await send(ctx, 'playlistExists', name, ephemeral=True)
return await send_localized_message(ctx, 'playlistExists', name, ephemeral=True)
try:
bytes = await attachment.read()
@@ -574,8 +573,8 @@ class Playlists(commands.Cog, name="playlist"):
track_ids = track_ids.decode().split(",")
data = {'tracks': track_ids, 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
await update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
await send(ctx, 'playlistCreated', name)
await MongoDBHandler.update_user(ctx.author.id, {"$set": {f"playlist.{assign_playlist_id([data for data in user])}": data}})
await send_localized_message(ctx, 'playlistCreated', name)
except Exception as e:
logger.error("Decode Error", exc_info=e)

View File

@@ -29,18 +29,14 @@ import function as func
from discord import app_commands
from discord.ext import commands
from function import (
LANGS,
send,
update_settings,
get_settings,
get_lang,
time as ctime,
get_aliases,
cooldown_check,
format_bytes
cooldown_check
)
from views import DebugView, HelpView, EmbedBuilderView
from voicelink import MongoDBHandler, LangHandler
from voicelink.views import DebugView, HelpView, EmbedBuilderView
from voicelink.placeholders import PlayerPlaceholder
from voicelink.utils import format_ms, format_bytes, dispatch_message, send_localized_message
def status_icon(status: bool) -> str:
return "" if status else ""
@@ -58,7 +54,7 @@ class Settings(commands.Cog, name="settings"):
async def settings(self, ctx: commands.Context):
view = HelpView(self.bot, ctx.author)
embed = view.build_embed(self.qualified_name)
view.response = await send(ctx, embed, view=view)
view.response = await dispatch_message(ctx, embed, view=view)
@settings.command(name="prefix", aliases=get_aliases("prefix"))
@commands.has_permissions(manage_guild=True)
@@ -66,10 +62,10 @@ class Settings(commands.Cog, name="settings"):
async def prefix(self, ctx: commands.Context, prefix: str):
"Change the default prefix for message commands."
if not self.bot.intents.message_content:
return await send(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
return await send_localized_message(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
await update_settings(ctx.guild.id, {"$set": {"prefix": prefix}})
await send(ctx, "setPrefix", prefix, prefix)
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {"prefix": prefix}})
await send_localized_message(ctx, "setPrefix", prefix, prefix)
@settings.command(name="language", aliases=get_aliases("language"))
@commands.has_permissions(manage_guild=True)
@@ -77,25 +73,25 @@ class Settings(commands.Cog, name="settings"):
async def language(self, ctx: commands.Context, language: str):
"You can choose your preferred language, the bot message will change to the language you set."
language = language.upper()
if language not in LANGS:
return await send(ctx, "languageNotFound")
if language not in voicelink.LangHandler.get_all_languages():
return await send_localized_message(ctx, "languageNotFound")
await update_settings(ctx.guild.id, {"$set": {'lang': language}})
await send(ctx, 'changedLanguage', language)
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'lang': language}})
await send_localized_message(ctx, 'changedLanguage', 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 voicelink.LangHandler.get_all_languages() if current.upper() in lang]
return [app_commands.Choice(name=lang, value=lang) for lang in voicelink.LangHandler.get_all_languages()]
@settings.command(name="dj", aliases=get_aliases("dj"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def dj(self, ctx: commands.Context, role: discord.Role = None):
"Set a DJ role or remove DJ role."
await update_settings(ctx.guild.id, {"$set": {'dj': role.id}} if role else {"$unset": {'dj': None}})
await send(ctx, 'setDJ', f"<@&{role.id}>" if role else "None")
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'dj': role.id}} if role else {"$unset": {'dj': None}})
await send_localized_message(ctx, 'setDJ', f"<@&{role.id}>" if role else "None")
@settings.command(name="queue", aliases=get_aliases("queue"))
@app_commands.choices(mode=[
@@ -107,57 +103,57 @@ class Settings(commands.Cog, name="settings"):
async def queue(self, ctx: commands.Context, mode: str):
"Change to another type of queue mode."
mode = mode if mode.lower() in voicelink.queue.QUEUE_TYPES else next(iter(voicelink.queue.QUEUE_TYPES))
await update_settings(ctx.guild.id, {"$set": {"queue_type": mode}})
await send(ctx, "setQueue", mode.capitalize())
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {"queue_type": mode}})
await send_localized_message(ctx, "setQueue", mode.capitalize())
@settings.command(name="247", aliases=get_aliases("247"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def playforever(self, ctx: commands.Context):
"Toggles 24/7 mode, which disables automatic inactivity-based disconnects."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
toggle = settings.get('24/7', False)
await update_settings(ctx.guild.id, {"$set": {'24/7': not toggle}})
await send(ctx, '247', await get_lang(ctx.guild.id, "enabled" if not toggle else "disabled"))
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'24/7': not toggle}})
await send_localized_message(ctx, '247', await LangHandler.get_lang(ctx.guild.id, "enabled" if not toggle else "disabled"))
@settings.command(name="bypassvote", aliases=get_aliases("bypassvote"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def bypassvote(self, ctx: commands.Context):
"Toggles voting system."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
toggle = settings.get('disabled_vote', True)
await update_settings(ctx.guild.id, {"$set": {'disabled_vote': not toggle}})
await send(ctx, 'bypassVote', await get_lang(ctx.guild.id, "enabled" if not toggle else "disabled"))
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'disabled_vote': not toggle}})
await send_localized_message(ctx, 'bypassVote', await LangHandler.get_lang(ctx.guild.id, "enabled" if not toggle else "disabled"))
@settings.command(name="view", aliases=get_aliases("view"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def view(self, ctx: commands.Context):
"Show all the bot settings in your server."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
texts = await get_lang(ctx.guild.id, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsTitle3", "settingsPermTitle", "settingsPermValue")
embed = discord.Embed(color=func.settings.embed_color)
texts = await LangHandler.get_lang(ctx.guild.id, "settingsMenu", "settingsTitle", "settingsValue", "settingsTitle2", "settingsValue2", "settingsTitle3", "settingsPermTitle", "settingsPermValue")
embed = discord.Embed(color=voicelink.Config().embed_color)
embed.set_author(name=texts[0].format(ctx.guild.name), icon_url=self.bot.user.display_avatar.url)
if ctx.guild.icon:
embed.set_thumbnail(url=ctx.guild.icon.url)
dj_role = ctx.guild.get_role(settings.get('dj', 0))
embed.add_field(name=texts[1], value=texts[2].format(
settings.get('prefix', func.settings.bot_prefix) or 'None',
settings.get('prefix', voicelink.Config().bot_prefix) or 'None',
settings.get('lang', 'EN'),
settings.get('controller', True),
dj_role.name if dj_role else 'None',
settings.get('disabled_vote', False),
settings.get('24/7', False),
settings.get('volume', 100),
ctime(settings.get('played_time', 0) * 60 * 1000),
format_ms(settings.get('played_time', 0) * 60 * 1000),
inline=True)
)
embed.add_field(name=texts[3], value=texts[4].format(
settings.get("queue_type", "Queue"),
func.settings.max_queue,
voicelink.Config().max_queue,
settings.get("duplicate_track", True)
))
@@ -173,7 +169,7 @@ class Settings(commands.Cog, name="settings"):
),
inline=False
)
await send(ctx, embed)
await dispatch_message(ctx, embed)
@settings.command(name="volume", aliases=get_aliases("volume"))
@app_commands.describe(value="Input a integer.")
@@ -185,15 +181,15 @@ class Settings(commands.Cog, name="settings"):
if player:
await player.set_volume(value, ctx.author)
await update_settings(ctx.guild.id, {"$set": {'volume': value}})
await send(ctx, 'setVolume', value)
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'volume': value}})
await send_localized_message(ctx, 'setVolume', value)
@settings.command(name="togglecontroller", aliases=get_aliases("togglecontroller"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def togglecontroller(self, ctx: commands.Context):
"Toggles the music controller."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
toggle = not settings.get('controller', True)
player: voicelink.Player = ctx.guild.voice_client
@@ -203,63 +199,64 @@ class Settings(commands.Cog, name="settings"):
except:
discord.ui.View.from_message(player.controller).stop()
await update_settings(ctx.guild.id, {"$set": {'controller': toggle}})
await send(ctx, 'toggleController', await get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'controller': toggle}})
await send_localized_message(ctx, 'toggleController', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
@settings.command(name="duplicatetrack", aliases=get_aliases("duplicatetrack"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def duplicatetrack(self, ctx: commands.Context):
"Toggle Vocard to prevent duplicate songs from queuing."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
toggle = not settings.get('duplicate_track', False)
player: voicelink.Player = ctx.guild.voice_client
if player:
player.queue._allow_duplicate = toggle
await update_settings(ctx.guild.id, {"$set": {'duplicate_track': toggle}})
return await send(ctx, "toggleDuplicateTrack", await get_lang(ctx.guild.id, "disabled" if toggle else "enabled"))
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'duplicate_track': toggle}})
return await send_localized_message(ctx, "toggleDuplicateTrack", await LangHandler.get_lang(ctx.guild.id, "disabled" if toggle else "enabled"))
@settings.command(name="customcontroller", aliases=get_aliases("customcontroller"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def customcontroller(self, ctx: commands.Context):
"Customizes music controller embeds."
settings = await get_settings(ctx.guild.id)
controller_settings = settings.get("default_controller", func.settings.controller)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
controller_settings = settings.get("default_controller", voicelink.Config().controller)
view = EmbedBuilderView(ctx, controller_settings.get("embeds").copy())
view.response = await send(ctx, view.build_embed(), view=view)
placeholder = PlayerPlaceholder(ctx.bot)
view = EmbedBuilderView(ctx, placeholder, controller_settings.get("embeds").copy())
view.response = await dispatch_message(ctx, view.build_embed(), view=view)
@settings.command(name="controllermsg", aliases=get_aliases("controllermsg"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def controllermsg(self, ctx: commands.Context):
"Toggles to send a message when clicking the button in the music controller."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
toggle = not settings.get('controller_msg', True)
await update_settings(ctx.guild.id, {"$set": {'controller_msg': toggle}})
await send(ctx, 'toggleControllerMsg', await get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'controller_msg': toggle}})
await send_localized_message(ctx, 'toggleControllerMsg', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
@settings.command(name="silentmsg", aliases=get_aliases("silentmsg"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def silentmsg(self, ctx: commands.Context):
"Toggle silent messaging to send discreet messages without alerting recipients."
settings = await get_settings(ctx.guild.id)
settings = await MongoDBHandler.get_settings(ctx.guild.id)
toggle = not settings.get('silent_msg', False)
await update_settings(ctx.guild.id, {"$set": {'silent_msg': toggle}})
await send(ctx, 'toggleSilentMsg', await get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'silent_msg': toggle}})
await send_localized_message(ctx, 'toggleSilentMsg', await LangHandler.get_lang(ctx.guild.id, "enabled" if toggle else "disabled"))
@settings.command(name="stageannounce", aliases=get_aliases("stageannounce"))
@commands.has_permissions(manage_guild=True)
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
async def stageannounce(self, ctx: commands.Context, template: str = None):
"Customize the channel topic template"
await update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}})
await send(ctx, "setStageAnnounceTemplate")
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'stage_announce_template': template}})
await send_localized_message(ctx, "setStageAnnounceTemplate")
@settings.command(name="setupchannel", aliases=get_aliases("setupchannel"))
@app_commands.describe(
@@ -270,7 +267,7 @@ class Settings(commands.Cog, name="settings"):
async def setupchannel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None:
"Sets up a dedicated channel for song requests in your server."
if not self.bot.intents.message_content:
return await send(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
return await send_localized_message(ctx, "missingIntents", "MESSAGE_CONTENT", ephemeral=True)
if not channel:
try:
@@ -282,25 +279,25 @@ class Settings(commands.Cog, name="settings"):
}
channel = await ctx.guild.create_text_channel("vocard-song-requests", overwrites=overwrites)
except:
return await send(ctx, "noCreatePermission")
return await send_localized_message(ctx, "noCreatePermission")
channel_perms = channel.permissions_for(ctx.me)
if not channel_perms.text() and not channel_perms.manage_messages:
return await send(ctx, "noCreatePermission")
return await send_localized_message(ctx, "noCreatePermission")
settings = await func.get_settings(ctx.guild.id)
controller = settings.get("default_controller", func.settings.controller).get("embeds", {}).get("inactive", {})
message = await channel.send(embed=voicelink.build_embed(controller, voicelink.Placeholders(self.bot)))
settings = await MongoDBHandler.get_settings(ctx.guild.id)
controller = settings.get("default_controller", voicelink.Config().controller).get("embeds", {}).get("inactive", {})
message = await channel.send(embed=PlayerPlaceholder.build_embed(controller, PlayerPlaceholder(self.bot)))
await update_settings(ctx.guild.id, {"$set": {'music_request_channel': {
await MongoDBHandler.update_settings(ctx.guild.id, {"$set": {'music_request_channel': {
"text_channel_id": channel.id,
"controller_msg_id": message.id,
}}})
await send(ctx, "createSongRequestChannel", channel.mention)
await send_localized_message(ctx, "createSongRequestChannel", channel.mention)
@app_commands.command(name="debug")
async def debug(self, interaction: discord.Interaction):
if interaction.user.id not in func.settings.bot_access_user:
if interaction.user.id not in voicelink.Config().bot_access_user:
return await interaction.response.send_message("You are not able to use this command!")
memory = psutil.virtual_memory()
@@ -308,7 +305,7 @@ class Settings(commands.Cog, name="settings"):
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 = discord.Embed(title="📄 Debug Panel", color=voicelink.Config().embed_color)
embed.description = "```== System Info ==\n" \
f"• CPU: {psutil.cpu_freq().current}Mhz ({psutil.cpu_percent()}%)\n" \
f"• RAM: {format_bytes(total_memory - available_memory)}/{format_bytes(total_memory, True)} ({memory.percent}%)\n" \
@@ -316,7 +313,7 @@ class Settings(commands.Cog, name="settings"):
embed.add_field(
name="🤖 Bot Information",
value=f"```• VERSION: {func.settings.version}\n" \
value=f"```• VERSION: {voicelink.Config().version}\n" \
f"• LATENCY: {self.bot.latency:.2f}ms\n" \
f"• GUILDS: {len(self.bot.guilds)}\n" \
f"• USERS: {sum([guild.member_count or 0 for guild in self.bot.guilds])}\n" \
@@ -335,7 +332,7 @@ class Settings(commands.Cog, name="settings"):
f"• CPU: {node.stats.cpu_process_load:.1f}%\n" \
f"• RAM: {format_bytes(node.stats.free)}/{format_bytes(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)}```"
f"• UPTIME: {format_ms(node.stats.uptime)}```"
)
else:
embed.add_field(

View File

@@ -26,7 +26,6 @@ import discord
import function as func
from discord.ext import commands, tasks
from addons import Placeholders
class Task(commands.Cog):
def __init__(self, bot: commands.Bot):
@@ -36,7 +35,7 @@ class Task(commands.Cog):
self.cache_cleaner.start()
self.current_act = 0
self.placeholder = Placeholders(bot)
self.placeholder = voicelink.BotPlaceholder(bot)
def cog_unload(self):
self.activity_update.cancel()
@@ -48,7 +47,7 @@ class Task(commands.Cog):
await self.bot.wait_until_ready()
try:
act_data = func.settings.activity[(self.current_act + 1) % len(func.settings.activity) - 1]
act_data = voicelink.Config().activity[(self.current_act + 1) % len(voicelink.Config().activity) - 1]
act_original = self.bot.activity
act_type = getattr(discord.ActivityType, act_data.get("type", "").lower(), discord.ActivityType.playing)
act_name = self.placeholder.replace(act_data.get("name", ""))
@@ -58,7 +57,7 @@ class Task(commands.Cog):
if act_original.type != act_type or act_original.name != act_name:
self.bot.activity = discord.Activity(type=act_type, name=act_name)
await self.bot.change_presence(activity=self.bot.activity, status=status_type)
self.current_act = (self.current_act + 1) % len(func.settings.activity)
self.current_act = (self.current_act + 1) % len(voicelink.Config().activity)
func.logger.info(f"Changed the bot status to {act_name}")
@@ -104,8 +103,7 @@ class Task(commands.Cog):
@tasks.loop(hours=12.0)
async def cache_cleaner(self):
func.SETTINGS_BUFFER.clear()
func.USERS_BUFFER.clear()
await voicelink.MongoDBHandler.cleanup_cache()
async def setup(bot: commands.Bot):
await bot.add_cog(Task(bot))

View File

@@ -21,68 +21,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import discord, json, os, copy, logging
import json
import os
import logging
import voicelink
from discord.ext import commands
from time import strptime
from addons import Settings
from typing import (
Optional,
Union,
Dict,
Any
)
from motor.motor_asyncio import (
AsyncIOMotorClient,
AsyncIOMotorCollection,
)
from typing import Optional
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
logger: logging.Logger = logging.getLogger("vocard")
if not os.path.exists(os.path.join(ROOT_DIR, "settings.json")):
raise Exception("Settings file not set!")
#--------------- Cache Var ---------------
settings: Settings
logger: logging.Logger = logging.getLogger("vocard")
MONGO_DB: AsyncIOMotorClient
SETTINGS_DB: AsyncIOMotorCollection
USERS_DB: AsyncIOMotorCollection
LANGS: dict[str, dict[str, str]] = {} #Stores all the languages in ./langs
LOCAL_LANGS: dict[str, dict[str, str]] = {} #Stores all the localization languages in ./local_langs
SETTINGS_BUFFER: dict[int, dict[str, Any]] = {} #Cache guild language
USERS_BUFFER: dict[str, dict] = {}
MISSING_TRANSLATOR: dict[str, list[str]] = {}
USER_BASE: dict[str, Any] = {
'playlist': {
'200': {
'tracks':[],
'perms': {'read': [], 'write':[], 'remove': []},
'name':'Favourite',
'type':'playlist'
}
},
'history': [],
'inbox':[]
}
ALLOWED_MENTIONS = discord.AllowedMentions().none()
LAST_SESSION_FILE_NAME = "last-session.json"
#-------------- Vocard Classes --------------
class TempCtx():
def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None:
self.author: discord.Member = author
self.channel: discord.VoiceChannel = channel
self.guild: discord.Guild = channel.guild
#-------------- Vocard Functions --------------
def open_json(path: str) -> dict:
try:
with open(os.path.join(ROOT_DIR, path), encoding="utf8") as json_file:
@@ -100,229 +53,13 @@ def update_json(path: str, new_data: dict) -> None:
with open(os.path.join(ROOT_DIR, path), "w") as json_file:
json.dump(data, json_file, indent=4)
def langs_setup() -> None:
for language in os.listdir(os.path.join(ROOT_DIR, "langs")):
if language.endswith('.json'):
LANGS[language[:-5]] = {}
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))
return
def time(millis: int) -> str:
seconds = (millis // 1000) % 60
minutes = (millis // (1000 * 60)) % 60
hours = (millis // (1000 * 60 * 60)) % 24
days = millis // (1000 * 60 * 60 * 24)
if days > 0:
return "%d days, %02d:%02d:%02d" % (days, hours, minutes, seconds)
elif hours > 0:
return "%d:%02d:%02d" % (hours, minutes, seconds)
else:
return "%02d:%02d" % (minutes, seconds)
def format_time(number:str) -> int:
try:
try:
num = strptime(number, '%M:%S')
except ValueError:
try:
num = strptime(number, '%S')
except ValueError:
num = strptime(number, '%H:%M:%S')
except:
return 0
return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000
def get_source(source: str, type: str) -> str:
source_settings: dict[str, str] = settings.sources_settings.get(source.lower().replace(" ", ""), settings.sources_settings.get("others"))
return source_settings.get(type)
def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]:
if ctx.author.id in settings.bot_access_user:
if ctx.author.id in voicelink.Config().bot_access_user:
return None
cooldown = settings.cooldowns_settings.get(f"{ctx.command.parent.qualified_name} {ctx.command.name}" if ctx.command.parent else ctx.command.name)
cooldown = voicelink.Config().cooldowns_settings.get(f"{ctx.command.parent.qualified_name} {ctx.command.name}" if ctx.command.parent else ctx.command.name)
if not cooldown:
return None
return commands.Cooldown(cooldown[0], cooldown[1])
def get_aliases(name: str) -> list:
return settings.aliases_settings.get(name, [])
def check_roles() -> tuple[str, int, int]:
return 'Normal', 5, 500
def truncate_string(text: str, length: int = 40) -> str:
return text[:length - 3] + "..." if len(text) > length else text
def format_bytes(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 "")
def _get_lang(lang: str, *keys) -> Optional[Union[list[str], str]]:
if lang not in LANGS:
lang = "EN"
if not LANGS[lang]:
LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json"))
lang_dict = LANGS[lang]
if len(keys) == 1:
return lang_dict.get(keys[0], "Not found!")
return [lang_dict.get(key, "Not found!") for key in keys]
async def get_lang(guild_id:int, *keys) -> Optional[Union[list[str], str]]:
settings = await get_settings(guild_id)
return _get_lang(settings.get("lang"), *keys)
async def send(
ctx: Union[commands.Context, discord.Interaction],
content: Union[str, discord.Embed] = None,
*params,
view: discord.ui.View = None,
file: discord.File = None,
delete_after: float = None,
ephemeral: bool = False,
requires_fetch: bool = False
) -> Optional[discord.Message]:
if content is None:
content = "No content provided."
# Determine the text to send
if isinstance(content, discord.Embed):
embed = content
text = None
else:
text = await get_lang(ctx.guild.id, content)
if text:
text = text.format(*params)
else:
text = content.format(*params)
embed = None
# Determine the sending function
send_func = (
ctx.send if isinstance(ctx, commands.Context) else
ctx.channel.send if isinstance(ctx, TempCtx) else
ctx.followup.send if ctx.response.is_done() else
ctx.response.send_message
)
# Check settings for delete_after duration
settings = await get_settings(ctx.guild.id)
send_kwargs = {
"content": text,
"embed": embed,
"file": file,
"allowed_mentions": ALLOWED_MENTIONS,
"silent": settings.get("silent_msg", False),
}
if "delete_after" in send_func.__code__.co_varnames:
if settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"):
delete_after = 10
send_kwargs["delete_after"] = delete_after
if "ephemeral" in send_func.__code__.co_varnames:
send_kwargs["ephemeral"] = ephemeral
if view:
send_kwargs["view"] = view
# Send the message or embed
message = await send_func(**send_kwargs)
if isinstance(message, discord.InteractionCallbackResponse):
message = message.resource
if requires_fetch and isinstance(message, (discord.WebhookMessage, discord.InteractionMessage)):
message = await message.fetch()
return message
async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool:
for mode, action in data.items():
for key, value in action.items():
cursors = key.split(".")
nested_data = tempStore
for c in cursors[:-1]:
nested_data = nested_data.setdefault(c, {})
if mode == "$set":
try:
nested_data[cursors[-1]] = value
except TypeError:
nested_data[int(cursors[-1])] = value
elif mode == "$unset":
nested_data.pop(cursors[-1], None)
elif mode == "$inc":
nested_data[cursors[-1]] = nested_data.get(cursors[-1], 0) + value
elif mode == "$push":
if isinstance(value, dict) and "$each" in value:
nested_data.setdefault(cursors[-1], []).extend(value["$each"][value.get("$slice", len(value["$each"])):])
else:
nested_data.setdefault(cursors[-1], []).extend([value])
elif mode == "$push":
if isinstance(value, dict) and "$each" in value:
nested_data.setdefault(cursors[-1], []).extend(value["$each"])
nested_data[cursors[-1]] = nested_data[cursors[-1]][value.get("$slice", len(value["$each"])):]
else:
nested_data.setdefault(cursors[-1], []).extend([value])
elif mode == "$pull":
if cursors[-1] in nested_data:
value = value.get("$in", []) if isinstance(value, dict) else [value]
nested_data[cursors[-1]] = [item for item in nested_data[cursors[-1]] if item not in value]
else:
return False
result = await db.update_one(filter, data)
return result.modified_count > 0
async def get_settings(guild_id:int) -> dict[str, Any]:
settings = SETTINGS_BUFFER.get(guild_id, None)
if not settings:
settings = await SETTINGS_DB.find_one({"_id": guild_id})
if not settings:
await SETTINGS_DB.insert_one({"_id": guild_id})
settings = SETTINGS_BUFFER[guild_id] = settings or {}
return settings
async def update_settings(guild_id: int, data: dict[str, dict[str, Any]]) -> bool:
settings = await get_settings(guild_id)
return await update_db(SETTINGS_DB, settings, {"_id": guild_id}, data)
async def get_user(user_id: int, d_type: Optional[str] = None, need_copy: bool = True) -> Dict[str, Any]:
user = USERS_BUFFER.get(user_id)
if not user:
user = await USERS_DB.find_one({"_id": user_id})
if not user:
user = {"_id": user_id, **USER_BASE}
await USERS_DB.insert_one(user)
USERS_BUFFER[user_id] = user
if d_type:
user = user.setdefault(d_type, copy.deepcopy(USER_BASE.get(d_type)))
return copy.deepcopy(user) if need_copy else user
async def update_user(user_id:int, data:dict) -> bool:
playlist = await get_user(user_id, need_copy=False)
return await update_db(USERS_DB, playlist, {"_id": user_id}, data)
return voicelink.Config().aliases_settings.get(name, [])

View File

@@ -1,7 +1,7 @@
import aiohttp
import asyncio
import logging
import function as func
import voicelink
from discord.ext import commands
from typing import Optional
@@ -39,7 +39,7 @@ class IPCClient:
self._heanders = {
"Authorization": self._password,
"User-Id": str(bot.user.id),
"Client-Version": func.settings.version
"Client-Version": voicelink.Config().version
}
async def _listen(self) -> None:

View File

@@ -1,12 +1,11 @@
import time, re
import function as func
from typing import List, Dict, Union, Optional
from discord import User, Member, VoiceChannel
from discord import User, Member
from discord.ext import commands
from voicelink import Player, Track, Playlist, NodePool, decode, LoopType, Filters
from addons import LYRICS_PLATFORMS
from voicelink import Player, Track, Playlist, NodePool, LoopType, Filters, Config, MongoDBHandler, LangHandler, LYRICS_PLATFORMS
from voicelink.utils import TempCtx
RATELIMIT_COUNTER: Dict[int, Dict[str, float]] = {}
SCOPES = {
@@ -62,8 +61,8 @@ async def connect_channel(member: Member, bot: commands.Bot) -> Player:
channel = member.voice.channel
try:
settings = await func.get_settings(channel.guild.id)
player: Player = await channel.connect(cls=Player(bot, channel, func.TempCtx(member, channel), settings))
settings = await MongoDBHandler.get_settings(channel.guild.id)
player: Player = await channel.connect(cls=Player(bot, channel, TempCtx(member, channel), settings))
await player.send_ws({"op": "createPlayer", "memberIds": [str(member.id) for member in channel.members]})
return player
except:
@@ -86,7 +85,7 @@ async def initBot(bot: commands.Bot, data: Dict) -> Dict:
async def initUser(bot: commands.Bot, data: Dict) -> Dict:
user_id = int(data.get("userId"))
data = await func.get_user(user_id)
data = await MongoDBHandler.get_user(user_id)
for mail in data.get("inbox"):
sender = bot.get_user(mail.get("sender"))
@@ -146,7 +145,7 @@ async def getRecommendation(bot: commands.Bot, data: Dict) -> None:
if not node:
return
track_data = decode(track_id := data.get("trackId"))
track_data = Track.decode(track_id := data.get("trackId"))
track = Track(track_id=track_id, info=track_data, requester=bot.user)
tracks: List[Track] = await node.get_recommendations(track, limit=60)
@@ -212,7 +211,7 @@ async def addTracks(player: Player, member: Member, data: Dict) -> None:
_type = data.get("type", "addToQueue")
tracks = [Track(
track_id=track_id,
info=decode(track_id),
info=Track.decode(track_id),
requester=member
) for track_id in data.get("tracks", [])]
@@ -274,7 +273,7 @@ async def clearQueue(player: Player, member: Member, data: Dict) -> None:
@require_permission(only_admin=True)
async def updateVolume(player: Player, member: Member, data: Dict) -> None:
volume = data.get("volume", 100)
await func.update_settings(player.guild.id, {"$set": {"volume": volume}})
await MongoDBHandler.update_settings(player.guild.id, {"$set": {"volume": volume}})
await player.set_volume(volume=volume, requester=member)
async def updatePause(player: Player, member: Member, data: Dict) -> None:
@@ -358,13 +357,13 @@ def _assign_playlist_id(existed: list) -> str:
return str(i)
async def _getPlaylist(user_id: int, playlist_id: str) -> Dict:
playlists = await func.get_user(user_id, "playlist")
playlists = await MongoDBHandler.get_user(user_id, d_type="playlist")
playlist = playlists.get(playlist_id)
if not playlist:
return
if playlist["type"] == "share":
target_user = await func.get_user(playlist["user"], "playlist")
target_user = await MongoDBHandler.get_user(playlist["user"], d_type="playlist")
target_playlist = target_user.get(playlist["referId"])
if target_playlist and user_id in target_playlist.get("perms", {}).get("read", []):
playlist["tracks"] = await _loadPlaylist(target_playlist)
@@ -391,7 +390,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
if not playlist_id and not _type == "createPlaylist":
return error_msg("Unable to process this request without a playlist ID.", user_id=user_id, level="error")
rank, max_p, max_t = func.check_roles()
max_p, max_t, _ = Config().get_playlist_config()
if _type == "createPlaylist":
name, playlist_url = data.get("playlistName"), data.get("playlistUrl")
if not name:
@@ -403,7 +402,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
"userId": str(user_id)
}
playlist = await func.get_user(user_id, "playlist")
playlist = await MongoDBHandler.get_user(user_id, d_type="playlist")
if len(list(playlist.keys())) >= max_p:
return {
"op": "updatePlaylist",
@@ -436,7 +435,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
assigned_playlist_id = _assign_playlist_id(list(playlist.keys()))
data = {'uri': playlist_url, 'perms': {'read': []}, 'name': name, 'type': 'link'} if playlist_url else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'}
await func.update_user(user_id, {"$set": {f"playlist.{assigned_playlist_id}": data}})
await MongoDBHandler.update_user(user_id, {"$set": {f"playlist.{assigned_playlist_id}": data}})
return {
"op": "updatePlaylist",
"status": "created",
@@ -450,9 +449,9 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
playlist = await _getPlaylist(user_id, playlist_id)
if playlist:
if playlist['type'] == 'share':
await func.update_user(playlist['user'], {"$pull": {f"playlist.{playlist['referId']}.perms.read": user_id}})
await MongoDBHandler.update_user(playlist['user'], {"$pull": {f"playlist.{playlist['referId']}.perms.read": user_id}})
await func.update_user(user_id, {"$unset": {f"playlist.{playlist_id}": 1}})
await MongoDBHandler.update_user(user_id, {"$unset": {f"playlist.{playlist_id}": 1}})
return {
"op": "updatePlaylist",
@@ -473,7 +472,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
"userId": str(user_id)
}
playlist = await func.get_user(user_id, "playlist")
playlist = await MongoDBHandler.get_user(user_id, d_type="playlist")
for data in playlist.values():
if data['name'].lower() == name.lower():
return {
@@ -484,7 +483,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
"userId": str(user_id)
}
await func.update_user(user_id, {"$set": {f'playlist.{playlist_id}.name': name}})
await MongoDBHandler.update_user(user_id, {"$set": {f'playlist.{playlist_id}.name': name}})
return {
"op": "updatePlaylist",
"status": "renamed",
@@ -504,15 +503,15 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
if playlist['type'] in ['share', 'link']:
return error_msg("You cannot add songs to a linked playlist through Vocard.", user_id=user_id, level='error')
rank, max_p, max_t = func.check_roles()
max_p, max_t, _ = Config().get_playlist_config()
if len(playlist['tracks']) >= max_t:
return error_msg(f"You have reached the limit! You can only add {max_t} songs to your playlist.", user_id=user_id)
decoded_track = Track(track_id=track_id, info=decode(track_id), requester=None)
decoded_track = Track(track_id=track_id, info=Track.decode(track_id), requester=None)
if decoded_track.is_stream:
return error_msg("You are not allowed to add streaming videos to your playlist.", user_id=user_id)
await func.update_user(user_id, {"$push": {f'playlist.{playlist_id}.tracks': track_id}})
await MongoDBHandler.update_user(user_id, {"$push": {f'playlist.{playlist_id}.tracks': track_id}})
return {
"op": "updatePlaylist",
"status": "addTrack",
@@ -540,9 +539,9 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
if playlist['tracks'][track_position] != track_id:
return error_msg("Something wrong while removing the track from your playlist.", user_id=user_id, level='error')
await func.update_user(user_id, {"$pull": {f'playlist.{playlist_id}.tracks': playlist['tracks'][track_position]}})
await MongoDBHandler.update_user(user_id, {"$pull": {f'playlist.{playlist_id}.tracks': playlist['tracks'][track_position]}})
decoded_track = decode(playlist['tracks'][track_position])
decoded_track = Track.decode(playlist['tracks'][track_position])
return {
"op": "updatePlaylist",
"status": "removeTrack",
@@ -554,7 +553,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
}
elif _type == "updateInbox":
user = await func.get_user(user_id)
user = await MongoDBHandler.get_user(user_id)
is_accept = data.get("accept", False)
if is_accept and len(list(user.get("playlist").keys())) >= max_p:
@@ -571,7 +570,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
del inbox[index]
if is_accept:
share_playlists = await func.get_user(mail["sender"], "playlist")
share_playlists = await MongoDBHandler.get_user(mail["sender"], d_type="playlist")
if refer_id not in share_playlists:
return error_msg("The shared playlist couldnt be found. Its possible that the user has already deleted it.", user_id=user_id)
@@ -582,8 +581,8 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
"name": playlist_name,
"type": "share"
})
await func.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}})
await func.update_user(user_id, {"$set": {
await MongoDBHandler.update_user(mail['sender'], {"$push": {f"playlist.{mail['referId']}.perms.read": user_id}})
await MongoDBHandler.update_user(user_id, {"$set": {
f'playlist.{assigned_playlist_id}': {
'user': mail['sender'], 'referId': mail['referId'],
'name': playlist_name,
@@ -598,7 +597,7 @@ async def updatePlaylist(bot: commands.Bot, data: Dict) -> Dict:
"data": share_playlist,
})
await func.update_user(user_id, {"$set": {"inbox": inbox}})
await MongoDBHandler.update_user(user_id, {"$set": {"inbox": inbox}})
return payload
async def getMutualGuilds(bot: commands.Bot, data: Dict) -> Dict:
@@ -631,7 +630,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
if not member.guild_permissions.manage_guild:
return error_msg("You don't have permission to access the settings.", user_id=user_id, level='error')
settings = await func.get_settings(guild_id)
settings = await MongoDBHandler.get_settings(guild_id)
if "dj" in settings:
role = guild.get_role(settings["dj"])
if role:
@@ -641,7 +640,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
"op": "getSettings",
"settings": settings,
"options": {
"languages": list(func.LANGS.keys()),
"languages": list(LangHandler.get_all_languages()),
"queueModes": ["Queue", "FairQueue"],
"roles": [role.name for role in guild.roles]
},
@@ -656,7 +655,7 @@ async def getSettings(bot: commands.Bot, data: Dict) -> Dict:
async def getLyrics(bot: commands.Bot, data: Dict) -> Dict:
title, artist, platform = data.get("title", ""), data.get("artist", ""), data.get("platform", "")
if not platform or platform not in LYRICS_PLATFORMS:
platform = func.settings.lyrics_platform
platform = Config().lyrics_platform
lyrics_platform = LYRICS_PLATFORMS.get(platform)
if lyrics_platform:
@@ -697,7 +696,7 @@ async def updateSettings(bot: commands.Bot, data: Dict) -> None:
if key not in SCOPES or not isinstance(value, SCOPES[key]):
del data[key]
await func.update_settings(guild.id, {"$set": data})
await MongoDBHandler.update_settings(guild.id, {"$set": data})
METHODS: Dict[str, Union[SystemMethod, PlayerMethod]] = {
"initBot": SystemMethod(initBot, credit=0),

View File

@@ -1,171 +0,0 @@
{
"unknownException": "⚠️ 執行命令時出現問題!請稍後再試,或加入我們的 Discord 伺服器獲得進一步支援。",
"enabled": "已啟用",
"disabled": "已停用",
"nodeReconnect": "請稍後再試!在節點重新連接後再嘗試。",
"noChannel": "沒有語音頻道可供連接。請提供一個語音頻道或加入一個語音頻道。",
"alreadyConnected": "已經連接到語音頻道。",
"noPermission": "抱歉!我沒有權限加入或在您的語音頻道中發言。",
"noCreatePermission": "抱歉!我沒有權限建立歌曲請求頻道。",
"noPlaySource": "找不到任何可播放的來源!",
"noPlayer": "在此伺服器上找不到播放器。",
"notVote": "此命令需要您的投票!輸入 `/vote` 以獲取更多資訊。",
"missingIntents": "抱歉,此命令無法執行,因為機器人缺少所需的請求意圖:`({0})`.",
"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}` 消息。",
"toggleSilentMsg": "你{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}```",
"settingsTitle3": "🎤 語音狀態資訊:",
"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}```",
"addEffect": "套用音效`{0}`濾鏡。",
"clearEffect": "聲音效果已清除!",
"filterTagAlreadyInUse": "此聲音效果已在使用中!請使用 /cleareffect <Tag> 移除它。",
"playlistViewTitle": "所有 {0} 的播放清單",
"playlistViewHeaders": "ID:,時間:,名稱:,曲目數:",
"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}`] 已存在。",
"playlistNotInvalidUrl": "請輸入有效的連結或公開的 Spotify 或 YouTube 播放清單連結。",
"playlistCreated": "你已建立 `{0}` 播放清單。輸入 /playlist view 檢視更多資訊。",
"playlistRenamed": "你已將 `{0}` 更名為 `{1}`。",
"playlistLimitTrack": "你已達到限制!你只能將 `{0}` 首歌曲添加至你的播放清單中。",
"playlistPlaylistLink": "你無法使用播放清單連結。",
"playlistStream": "你無法將串流影片添加至你的播放清單中。",
"playlistPositionNotFound": "找不到播放清單 [`{1}`] 中位置為 `{0}` 的曲目!",
"playlistRemoved": "👋 已從 {1} 的播放清單 [`{2}`] 中刪除 **{0}**。",
"playlistClear": "你已成功清除播放清單 [`{0}`]。",
"playlistView": "播放清單檢視器",
"playlistViewDesc": "```名稱: {0} [{1}]\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```",
"playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}",
"playlistViewPermsValue2": "📖 讀取: {0}",
"playlistViewTrack": "音軌",
"playlistViewFooter": "總長度: {0}",
"inboxFull": "抱歉!{0} 的收件匣已滿。",
"inboxNoMsg": "您的收件匣中沒有任何訊息。",
"invitationSent": "已發送邀請給 {0}。",
"notInChannel": "{0},您必須在 {1} 中使用語音指令。如果您已在語音頻道中,請重新加入!",
"noTrackPlaying": "現在沒有歌曲正在播放",
"noTrackFound": "找不到符合該查詢的歌曲!請提供有效的網址。",
"noLinkSupport": "搜索命令不支援網址!",
"voted": "您已投票!",
"missingPosPerm": "只有 DJ 或管理員才能更改位置。",
"missingModePerm": "只有 DJ 或管理員才能切換循環模式。",
"missingQueuePerm": "只有 DJ 或管理員才能從隊列中移除音軌。",
"missingAutoPlayPerm": "只有 DJ 或管理員才能啟用或停用自動播放模式!",
"missingFunctionPerm": "只有 DJ 或管理員才能使用此功能。",
"timeFormatError": "時間格式不正確。例如2:42 或 12:39:31",
"lyricsNotFound": "找不到歌詞。輸入 /lyrics <歌曲名稱> <作者> 查找歌詞。",
"missingTrackInfo": "有些音軌資訊缺失。",
"noVoiceChannel": "找不到語音頻道!",
"playlistAddError": "您無權將串流視訊添加到播放清單中!",
"playlistAddError2": "添加音軌到播放清單時發生問題!",
"playlistLimited": "您已達到上限!您只能將 {0} 首歌曲添加到播放清單中。",
"playlistRepeated": "您的播放清單中已經存在相同的音軌!",
"playlistAdded": "❤️ 已將 **{0}** 添加到 {1} 的播放清單中 [`{2}`]!",
"playerDropdown": "選擇要跳轉到的音軌...",
"playerFilter": "選擇要套用的篩選器...",
"buttonBack": "返回",
"buttonPause": "暫停",
"buttonResume": "繼續",
"buttonSkip": "跳過",
"buttonLeave": "離開",
"buttonLoop": "循環",
"buttonVolumeUp": "增加音量",
"buttonVolumeDown": "降低音量",
"buttonVolumeMute": "靜音",
"buttonVolumeUnmute": "取消靜音",
"buttonAutoPlay": "自動播放",
"buttonShuffle": "隨機播放",
"buttonForward": "前進",
"buttonRewind": "後退",
"buttonLyrics": "歌詞",
"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}",
"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": "自動播放模式現在為 **{0}**。",
"notdj": "您不是DJ當前DJ為 {0}。",
"djToMe": "您無法將DJ權限轉移給自己或機器人。",
"djNotInChannel": "`{0}` 不在語音頻道中。",
"djswap": "您已將DJ權限轉移給 `{0}`。",
"voicelinkQueueFull": "抱歉,您已達到隊列中 `{0}` 首歌曲的最大數量!",
"voicelinkOutofList": "請提供有效的歌曲索引!",
"voicelinkDuplicateTrack": "抱歉,此歌曲已在隊列中。",
"decodeError": "解碼文件時出現問題!",
"invalidStartTime": "無效的開始時間! 時間必須在 `00:00` 和 `{0}` 之間。",
"invalidEndTime": "無效的結束時間! 時間必須在 `00:00` 和 `{0}` 之間。",
"invalidTimeOrder": "結束時間不能小於或等於開始時間。",
"setStageAnnounceTemplate": "完成!從現在開始,像您現在的語音狀態將根據您的模板命名。您應該在幾秒鐘內看到它更新。",
"createSongRequestChannel": "一個歌曲請求頻道 ({0}) 已建立!您可以在該頻道中透過歌曲名稱或 URL 開始要求任何歌曲,而無需使用機器人前綴。"
}

View File

@@ -38,7 +38,7 @@
"addEffect": "已应用 `{0}` 音效。",
"clearEffect": "音效已清除!",
"filterTagAlreadyInUse": "该音效已在使用中!请使用 /cleareffect <标签> 移除。",
"playlistViewTitle": "📜 {0} 的所有播放列表",
"playlistViewTitle": "{0} 的所有播放列表",
"playlistViewHeaders": "ID:,时长:,名称:,曲目数:",
"playlistFooter": "输入 /playlist play [播放列表] 将其添加到队列中。",
"playlistNotFound": "未找到播放列表 [`{0}`]。输入 /playlist view 查看所有播放列表。",
@@ -72,6 +72,7 @@
"playlistViewPermsValue": "📖 读取: ✓ ✍🏽 写入: {0} 🗑️ 删除: {1}",
"playlistViewPermsValue2": "📖 读取: {0}",
"playlistViewTrack": "曲目列表",
"playlistViewFooter": "总持续时间:{0}",
"playlistViewPage": "第 {0}/{1} 页 | 总时长: {2}",
"inboxFull": "抱歉!{0} 的收件箱已满。",
"inboxNoMsg": "您的收件箱中没有消息。",
@@ -129,7 +130,6 @@
"historyTitle": "播放历史:",
"viewTitle": "音乐队列",
"viewDesc": "**正在播放: [点我]({0}) ⮯**\n{1}",
"viewFooter": "第 {0}/{1} 页 | 总时长: {2}",
"pauseError": "播放器已暂停。",
"pauseVote": "{0} 投票暂停了歌曲。[{1}/{2}]",
"paused": "`{0}` 已暂停播放器。",

View File

@@ -38,7 +38,7 @@
"addEffect": "已套用 `{0}` 效果。",
"clearEffect": "音效已清除!",
"filterTagAlreadyInUse": "該音效已在使用中!請使用 /cleareffect <標籤> 移除。",
"playlistViewTitle": "📜 {0} 的所有播放清單",
"playlistViewTitle": "{0} 的所有播放清單",
"playlistViewHeaders": "ID:,時長:,名稱:,曲目數:",
"playlistFooter": "輸入 /playlist play [播放清單] 將其加入隊列。",
"playlistNotFound": "未找到播放清單 [`{0}`]。輸入 /playlist view 查看所有播放清單。",
@@ -72,6 +72,7 @@
"playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 寫入: {0} 🗑️ 移除: {1}",
"playlistViewPermsValue2": "📖 讀取: {0}",
"playlistViewTrack": "曲目列表",
"playlistViewFooter": "總持續時間:{0}",
"playlistViewPage": "第 {0}/{1} 頁 | 總時長: {2}",
"inboxFull": "抱歉!{0} 的收件匣已滿。",
"inboxNoMsg": "您的收件匣中沒有訊息。",
@@ -129,7 +130,6 @@
"historyTitle": "播放歷史:",
"viewTitle": "音樂隊列",
"viewDesc": "**正在播放: [點我]({0}) ⮯**\n{1}",
"viewFooter": "第 {0}/{1} 頁 | 總時長: {2}",
"pauseError": "播放器已暫停。",
"pauseVote": "{0} 投票暫停了歌曲。[{1}/{2}]",
"paused": "`{0}` 已暫停播放器。",

View File

@@ -109,7 +109,7 @@
"value": "valor",
"Play the specific track from your custom playlist.": "Reproduce la pista específica de tu lista de reproducción personalizada.",
"view": "ver",
"List all your playlist and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
"List all your playlists and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
"create": "crear",
"Create your custom playlist.": "Crea tu lista de reproducción personalizada.",
"Give a name to your playlist.": "Dale un nombre a tu lista de reproducción.",

View File

@@ -109,7 +109,7 @@
"value": "valor",
"Play the specific track from your custom playlist.": "Reproduce la pista específica de tu lista de reproducción personalizada.",
"view": "ver",
"List all your playlist and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
"List all your playlists and all songs in your favourite playlist.": "Lista todas tus listas de reproducción y canciones en tu lista favorita.",
"create": "crear",
"Create your custom playlist.": "Crea tu lista de reproducción personalizada.",
"Give a name to your playlist.": "Dale un nombre a tu lista de reproducción.",

View File

@@ -109,7 +109,7 @@
"value": "值",
"Play the specific track from your custom playlist.": "播放您自定义播放列表中的指定歌曲。",
"view": "查看",
"List all your playlist and all songs in your favourite playlist.": "列出您所有的播放列表和最喜欢的播放列表中的所有歌曲。",
"List all your playlists and all songs in your favourite playlist.": "列出您所有的播放列表和最喜欢的播放列表中的所有歌曲。",
"create": "创建",
"Create your custom playlist.": "创建自定义播放列表。",
"Give a name to your playlist.": "为您的播放列表命名。",

View File

@@ -109,7 +109,7 @@
"value": "值",
"Play the specific track from your custom playlist.": "播放您自定義播放列表中的指定歌曲。",
"view": "查看",
"List all your playlist and all songs in your favourite playlist.": "列出您所有的播放列表和您最愛的播放列表中的所有歌曲。",
"List all your playlists and all songs in your favourite playlist.": "列出您所有的播放列表和您最愛的播放列表中的所有歌曲。",
"create": "創建",
"Create your custom playlist.": "創建您自定義的播放列表。",
"Give a name to your playlist.": "給您的播放列表命名。",

100
main.py
View File

@@ -27,35 +27,42 @@ import os
import aiohttp
import update
import logging
import voicelink
import function as func
from discord.ext import commands
from ipc import IPCClient
from motor.motor_asyncio import AsyncIOMotorClient
from logging.handlers import TimedRotatingFileHandler
from addons import Settings
from voicelink import Config, LangHandler, MongoDBHandler, VoicelinkException
from voicelink.utils import dispatch_message
class Translator(discord.app_commands.Translator):
MISSING_TRANSLATOR: dict[str, list[str]] = {}
async def load(self):
func.logger.info("Loaded Translator")
async def unload(self):
func.logger.info("Unload Translator")
async def translate(
self,
string: discord.app_commands.locale_str,
locale: discord.Locale,
context: discord.app_commands.TranslationContext
) -> str | None:
locale_key = str(locale).upper()
local_translations = LangHandler._local_langs.get(locale_key)
if not local_translations:
return None
async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext):
locale_key = str(locale)
if locale_key in func.LOCAL_LANGS:
translated_text = func.LOCAL_LANGS[locale_key].get(string.message)
if translated_text is None:
missing_translations = func.MISSING_TRANSLATOR.setdefault(locale_key, [])
if string.message not in missing_translations:
missing_translations.append(string.message)
translated_text = local_translations.get(string.message)
if translated_text is not None:
return translated_text
missing = self.MISSING_TRANSLATOR.setdefault(locale_key, [])
if string.message not in missing:
missing.append(string.message)
return None
class Vocard(commands.Bot):
@@ -77,7 +84,7 @@ class Vocard(commands.Bot):
return await message.channel.send(f"My prefix is `{prefix}`")
# Fetch guild settings and check if the mesage is in the music request channel
settings = await func.get_settings(message.guild.id)
settings = await MongoDBHandler.get_settings(message.guild.id)
if settings and (request_channel := settings.get("music_request_channel")):
if message.channel.id == request_channel.get("text_channel_id"):
ctx = await self.get_context(message)
@@ -91,34 +98,16 @@ class Vocard(commands.Bot):
await cmd(ctx, query=attachment.url)
except Exception as e:
await func.send(ctx, str(e), ephemeral=True)
await dispatch_message(ctx, str(e), ephemeral=True)
finally:
return await message.delete()
await self.process_commands(message)
async def connect_db(self) -> None:
if not ((db_name := func.settings.mongodb_name) and (db_url := func.settings.mongodb_url)):
raise Exception("MONGODB_NAME and MONGODB_URL can't not be empty in settings.json")
try:
func.MONGO_DB = AsyncIOMotorClient(host=db_url)
await func.MONGO_DB.server_info()
func.logger.info(f"Successfully connected to [{db_name}] MongoDB!")
except Exception as e:
func.logger.error("Not able to connect MongoDB! Reason:", exc_info=e)
exit()
func.SETTINGS_DB = func.MONGO_DB[db_name]["Settings"]
func.USERS_DB = func.MONGO_DB[db_name]["Users"]
async def setup_hook(self) -> None:
func.langs_setup()
# Connecting to MongoDB
await self.connect_db()
await MongoDBHandler.init(bot_config.mongodb_url, bot_config.mongodb_name)
# Set translator
await self.tree.set_translator(Translator())
@@ -132,32 +121,34 @@ class Vocard(commands.Bot):
except Exception as e:
func.logger.error(f"Something went wrong while loading {module[:-3]} cog.", exc_info=e)
self.ipc = IPCClient(self, **func.settings.ipc_client)
if func.settings.ipc_client.get("enable", False):
self.ipc = IPCClient(self, **bot_config.ipc_client)
if bot_config.ipc_client.get("enable", False):
try:
await self.ipc.connect()
except Exception as e:
func.logger.error(f"Cannot connected to dashboard! - Reason: {e}")
# Update version tracking
if not func.settings.version or func.settings.version != update.__version__:
if not bot_config.version or bot_config.version != update.__version__:
await self.tree.sync()
func.update_json("settings.json", new_data={"version": update.__version__})
for locale_key, values in func.MISSING_TRANSLATOR.items():
for locale_key, values in self.tree.translator.MISSING_TRANSLATOR.items():
func.logger.warning(f'Missing translation for "{", ".join(values)}" in "{locale_key}"')
self.tree.translator.MISSING_TRANSLATOR.clear()
async def on_ready(self):
func.logger.info("------------------")
func.logger.info(f"Logging As {self.user}")
func.logger.info(f"Bot ID: {self.user.id}")
func.logger.info("------------------")
func.logger.info(f"Vocard Version: {update.__version__}")
func.logger.info(f"Discord Version: {discord.__version__}")
func.logger.info(f"Python Version: {sys.version}")
func.logger.info("------------------")
func.settings.client_id = self.user.id
func.LOCAL_LANGS.clear()
func.MISSING_TRANSLATOR.clear()
bot_config.client_id = self.user.id
LangHandler._local_langs.clear()
async def on_command_error(self, ctx: commands.Context, exception, /) -> None:
error = getattr(exception, 'original', exception)
@@ -178,12 +169,12 @@ class Vocard(commands.Bot):
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}")
embed = discord.Embed(description=description, color=bot_config.embed_color)
embed.set_footer(icon_url=ctx.me.display_avatar.url, text=f"More Help: {bot_config.invite_link}")
return await ctx.reply(embed=embed)
elif not issubclass(error.__class__, voicelink.VoicelinkException):
error = await func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
elif not issubclass(error.__class__, VoicelinkException):
error = await Lang_handler.get_lang(ctx.guild.id, "unknownException") + bot_config.invite_link
func.logger.error(f"An unexpected error occurred in the {ctx.command.name} command on the {ctx.guild.name}({ctx.guild.id}).", exc_info=exception)
try:
@@ -206,13 +197,14 @@ class CommandCheck(discord.app_commands.CommandTree):
return True
async def get_prefix(bot: commands.Bot, message: discord.Message) -> str:
settings = await func.get_settings(message.guild.id)
return settings.get("prefix", func.settings.bot_prefix)
settings = await MongoDBHandler.get_settings(message.guild.id)
return settings.get("prefix", bot_config.bot_prefix)
# Loading settings and logger
func.settings = Settings(func.open_json("settings.json"))
bot_config = Config(func.open_json("settings.json"))
Lang_handler = LangHandler.init()
LOG_SETTINGS = func.settings.logging
LOG_SETTINGS = bot_config.logging
if (LOG_FILE := LOG_SETTINGS.get("file", {})).get("enable", True):
log_path = os.path.abspath(LOG_FILE.get("path", "./logs"))
if not os.path.exists(log_path):
@@ -229,8 +221,8 @@ for log_name, log_level in LOG_SETTINGS.get("level", {}).items():
# Setup the bot object
intents = discord.Intents.default()
intents.message_content = False if func.settings.bot_prefix is None else True
intents.members = func.settings.ipc_client.get("enable", False)
intents.message_content = False if bot_config.bot_prefix is None else True
intents.members = bot_config.ipc_client.get("enable", False)
intents.voice_states = True
bot = Vocard(
@@ -245,4 +237,4 @@ bot = Vocard(
if __name__ == "__main__":
update.check_version(with_msg=True)
bot.run(func.settings.token, root_logger=True)
bot.run(bot_config.token, root_logger=True)

View File

@@ -48,6 +48,11 @@
"secure": false,
"enable": false
},
"playlist_settings": {
"max_playlist": 5,
"max_tracks_per_playlist": 500,
"default_playlist_name": "Favourite"
},
"sources_settings": {
"youtube": {
"emoji": "<:youtube:826661982760992778>",

View File

@@ -31,7 +31,7 @@ import subprocess
from io import BytesIO
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
__version__ = "v2.7.2"
__version__ = "v2.7.3b1"
# URLs for update and migration
PYTHON_CMD_NAME = os.path.basename(sys.executable)

View File

@@ -1,110 +0,0 @@
"""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 function as func
class LyricsDropdown(discord.ui.Select):
def __init__(self, langs: list[str]) -> None:
self.view: LyricsView
super().__init__(
placeholder="Select A Lyrics Translation",
min_values=1, max_values=1,
options=[discord.SelectOption(label=lang) for lang in langs],
custom_id="selectLyricsLangs"
)
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) -> None:
super().__init__(timeout=60)
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: 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: discord.Interaction) -> bool:
return interaction.user == self.author
async def on_timeout(self) -> None:
for child in self.children:
child.disabled = True
try:
await self.response.edit(view=self)
except:
pass
async def on_error(self, error, item, interaction) -> None:
return
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)
embed.set_footer(text=f"Page: {self.current_page}/{self.pages}")
return embed
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
if self.current_page != 1:
self.current_page = 1
return await interaction.response.edit_message(embed=self.build_embed())
await interaction.response.defer()
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
if self.current_page > 1:
self.current_page -= 1
return await interaction.response.edit_message(embed=self.build_embed())
await interaction.response.defer()
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
if self.current_page < self.pages:
self.current_page += 1
return await interaction.response.edit_message(embed=self.build_embed())
await interaction.response.defer()
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
if self.current_page != self.pages:
self.current_page = self.pages
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) -> None:
await self.response.delete()
self.stop()

View File

@@ -1,199 +0,0 @@
"""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 function as func
from typing import TYPE_CHECKING
from .utils import Pagination, BaseModal
if TYPE_CHECKING:
from voicelink import Player, Track
class QueueView(discord.ui.View):
"""
A Discord UI view for displaying and interacting with a paginated list of tracks.
Attributes:
player (Player): The player containing the track queue or history.
author (discord.Member): The member who initiated the view.
is_queue (bool): Indicates if the list is a queue or history.
pagination (Pagination[Track]): Manages pagination of track lists.
response (discord.Message): The message containing the view.
total_duration (str): The total duration of the tracks.
"""
def __init__(self, player: "Player", author: discord.Member, is_queue: bool = True) -> None:
super().__init__(timeout=60)
self.player: Player = player
self.author: discord.Member = author
self.is_queue: bool = is_queue
self.response: discord.Message = None
self.pagination = Pagination["Track"](
items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())),
page_size=7,
)
self.total_duration = self.calculate_total_duration()
self.update_view()
def calculate_total_duration(self) -> str:
"""Calculate the total duration of the tracks."""
try:
return func.time(sum(track.length for track in self.pagination._items))
except Exception:
return ""
def format_description(self, tracks: list["Track"], texts: list[str]) -> str:
"""Format the description for the embed based on current tracks."""
now_playing = (
texts[1].format(self.player.current.uri,
f"```{self.player.current.title}```")
if self.player.current else texts[2].format("None")
)
track_list = "\n".join([
f"{track.emoji} `{i:>2}.` `[{texts[3] if track.is_stream else func.time(track.length)}]` "
f"[{func.truncate_string(track.title)}]({track.uri}) {track.requester.mention}"
for i, track in enumerate(tracks, start=self.pagination.start_index + 1)
])
return f"{now_playing}\n**{texts[4] if self.is_queue else texts[5]}**\n{track_list}"
def update_view(self) -> None:
"""Update button states and page number display based on current pagination state."""
button_states = {
"fast_back": self.pagination.current_page <= 2,
"back": not self.pagination.has_previous_page,
"fast_next": self.pagination.current_page >= self.pagination.total_pages - 1,
"next": not self.pagination.has_next_page,
}
for child in self.children:
if child.custom_id in button_states:
child.disabled = button_states[child.custom_id]
if child.custom_id == "page_number":
child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}"
async def on_timeout(self) -> None:
"""Disable all buttons when the view times out."""
for child in self.children:
child.disabled = True
try:
await self.response.edit(view=self)
except discord.HTTPException:
pass
async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Ensure only the author of the view can interact with it."""
return interaction.user == self.author
async def build_embed(self) -> discord.Embed:
"""Build the embed for the current page of tracks."""
tracks = self.pagination.get_current_page_items()
texts = await func.get_lang(
self.author.guild.id,
"viewTitle",
"viewDesc",
"nowplayingDesc",
"live",
"queueTitle",
"historyTitle",
"playlistViewFooter",
)
embed = discord.Embed(title=texts[0], color=func.settings.embed_color)
embed.description = self.format_description(tracks, texts)
embed.set_footer(
text=texts[6].format(
self.pagination.current_page,
self.pagination.total_pages,
self.total_duration,
)
)
return embed
async def update_and_edit_message(self, interaction: discord.Interaction) -> None:
"""Update the view and edit the message with the new embed."""
self.update_view()
if interaction.response.is_done():
await interaction.followup.edit_message(self.response.id, embed=await self.build_embed(), view=self)
else:
await interaction.response.edit_message(embed=await self.build_embed(), view=self)
@discord.ui.button(label='<<', custom_id="fast_back")
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Jump to the first page."""
self.pagination.go_page(0)
await self.update_and_edit_message(interaction)
@discord.ui.button(label='Back', custom_id="back", style=discord.ButtonStyle.blurple)
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Go to the previous page if it exists."""
self.pagination.go_back()
await self.update_and_edit_message(interaction)
@discord.ui.button(label="--/--", custom_id="page_number", style=discord.ButtonStyle.blurple)
async def page_number(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Display current page number."""
modal = BaseModal(
title="Page Number",
custom_id="page_number_modal",
items=[
discord.ui.TextInput(
label="Page Number",
custom_id="page_number",
placeholder="Enter the page number to navigate.",
default=str(self.pagination.current_page),
max_length=5,
required=True
)
]
)
await interaction.response.send_modal(modal)
await modal.wait()
page_number = modal.values.get("page_number")
if not page_number or not page_number.isdigit():
return
self.pagination.go_page(int(page_number) - 1)
await self.update_and_edit_message(interaction)
@discord.ui.button(label='Next', custom_id="next", style=discord.ButtonStyle.blurple)
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Go to the next page if it exists."""
self.pagination.go_next()
await self.update_and_edit_message(interaction)
@discord.ui.button(label='>>', custom_id="fast_next")
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Jump to the last page."""
self.pagination.go_page(self.pagination.total_pages - 1)
await self.update_and_edit_message(interaction)

View File

@@ -21,18 +21,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
__version__ = "1.4"
__version__ = "1.5"
__author__ = 'Vocard Development, ChocoMeow'
__license__ = "MIT"
__copyright__ = "Copyright 2023 - present (c) Vocard Development, ChocoMeow"
from .config import Config
from .enums import SearchType, LoopType
from .events import *
from .exceptions import *
from .filters import *
from .objects import *
from .player import Player, connect_channel
from .pool import *
from .queue import *
from .placeholders import Placeholders, build_embed
from .transformer import encode, decode
from .player import Player, connect_channel
from .placeholders import PlayerPlaceholder, BotPlaceholder
from .mongodb import MongoDBHandler
from .language import LangHandler
from .lyrics import LYRICS_PLATFORMS

134
voicelink/config.py Normal file
View File

@@ -0,0 +1,134 @@
"""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 os
from pathlib import Path
from dotenv import load_dotenv
from typing import (
Dict,
List,
Any,
Union,
Optional
)
load_dotenv()
class Config:
_instance: Optional['Config'] = None
WORKING_DIR: Path = Path(__file__).resolve().parent.parent
LAST_SESSION_FILE_DIR: str = WORKING_DIR / "last-session.json"
def __new__(cls, settings: Dict[str, Any] = None) -> 'Config':
"""
Singleton pattern to ensure only one instance of Config exists.
If settings are provided, creates a new instance that replaces the old one.
Args:
settings (Dict[str, Any], optional): A dictionary containing configuration settings. Defaults to None.
If provided, creates a new instance that replaces the old one.
"""
if settings is not None:
instance = super(Config, cls).__new__(cls)
instance.__init__(settings)
cls._instance = instance
return instance
if cls._instance is None:
cls._instance = super(Config, cls).__new__(cls)
return cls._instance
def __init__(self, settings: Dict[str, Any] = None) -> None:
"""
Initialize configuration settings.
Args:
settings (Dict[str, Any], optional): A dictionary containing configuration settings.
If None, uses empty dict with default values.
"""
if hasattr(self, 'initialized'):
return
settings = settings or {}
self.token: str = settings.get("token") or os.getenv("TOKEN")
self.client_id: int = int(settings.get("client_id", 0)) or int(os.getenv("CLIENT_ID"))
self.genius_token: str = settings.get("genius_token") or os.getenv("GENIUS_TOKEN")
self.mongodb_url: str = settings.get("mongodb_url") or os.getenv("MONGODB_URL")
self.mongodb_name: str = settings.get("mongodb_name") or os.getenv("MONGODB_NAME")
self.invite_link: str = "https://discord.gg/wRCgB7vBQv"
self.nodes: Dict[str, Dict[str, Union[str, int, bool]]] = settings.get("nodes", {})
self.max_queue: int = settings.get("default_max_queue", 1000)
self.bot_prefix: str = settings.get("prefix", "")
self.activity: List[Dict[str, str]] = settings.get("activity", [{"listen": "/help"}])
self.logging: Dict[Union[str, Dict[str, Union[str, bool]]]] = settings.get("logging", {})
self.embed_color: str = int(settings.get("embed_color", "0xb3b3b3"), 16)
self.bot_access_user: List[int] = settings.get("bot_access_user", [])
self.sources_settings: Dict[Dict[str, str]] = settings.get("sources_settings", {})
self.cooldowns_settings: Dict[str, List[int]] = settings.get("cooldowns", {})
self.aliases_settings: Dict[str, List[str]] = settings.get("aliases", {})
self.controller: Dict[str, Dict[str, Any]] = settings.get("default_controller", {})
self.voice_status_template: str = settings.get("default_voice_status_template", "")
self.lyrics_platform: str = settings.get("lyrics_platform", "A_ZLyrics").lower()
self.ipc_client: Dict[str, Union[str, bool, int]] = settings.get("ipc_client", {})
self.playlist_settings: Dict[str, Union[str, int]] = settings.get("playlist_settings", {})
self.version: str = settings.get("version", "")
self.initialized = True
@classmethod
def get_source_config(cls, source: str, type: str) -> Union[str, None]:
"""
Get source configuration for a specific source and type.
Args:
source (str): The source identifier (e.g., 'youtube', 'spotify').
Case-insensitive and spaces are removed.
type (str): The type of configuration to retrieve (e.g., 'emoji', 'color').
Returns:
Union[str, None]: The configuration value for the specified source and type.
Returns None if either the source or type doesn't exist.
Example:
>>> Config().get_source("youtube", "emoji")
"🎵"
"""
if not isinstance(source, str) or not isinstance(type, str):
return None
normalized_source: str = source.lower().strip().replace(" ", "")
source_settings: dict[str, str] = cls._instance.sources_settings.get(
normalized_source,
cls._instance.sources_settings.get("others", {})
)
return source_settings.get(type)
@classmethod
def get_playlist_config(cls) -> tuple[int, int, str]:
config = cls._instance.playlist_settings
return config.get("max_playlists", 5), config.get("max_tracks_per_playlist", 500), config.get("default_playlist_name", "Favourite")

133
voicelink/language.py Normal file
View File

@@ -0,0 +1,133 @@
"""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 os
import json
import logging
from typing import Optional, Union, List
from .config import Config
from .mongodb import MongoDBHandler
logger = logging.getLogger("vocard.language")
class LangHandler:
"""
Static class for handling language and localization strings.
Preloads all language files at initialization for fast access.
"""
_langs: dict[str, dict[str, str]] = {}
_local_langs: dict[str, dict[str, str]] = {}
@classmethod
def init(
cls,
langs_dir: str = Config.WORKING_DIR / "langs",
local_langs_dir: str = Config.WORKING_DIR / "local_langs"
) -> "LangHandler":
"""
Initialize the language handler by preloading all language files
from the given directories into memory.
Args:
langs_dir (str): Directory containing main language JSON files.
local_langs_dir (str): Directory containing localization JSON files.
Returns:
LangHandler: The class itself with loaded languages.
"""
targets = [
(langs_dir, cls._langs, "language"),
(local_langs_dir, cls._local_langs, "local language"),
]
for directory, storage, label in targets:
if not os.path.exists(directory):
logger.warning(f"{label.capitalize()} directory '{directory}' does not exist.")
continue
for file in os.listdir(directory):
if file.endswith(".json"):
lang_code = file.split(".")[0].upper()
filepath = os.path.join(directory, file)
try:
with open(filepath, encoding="utf8") as f:
storage[lang_code] = json.load(f)
logger.info(f"Loaded {label}: {lang_code}")
except Exception as e:
logger.error(f"Failed to load {label} file '{filepath}': {e}")
return cls
@classmethod
def _get_lang(cls, lang: str, *keys) -> Optional[Union[list[str], str]]:
"""
Internal helper to retrieve strings from the preloaded cls._lang cache.
Args:
lang (str): Language code (e.g., "EN", "FR").
*keys: One or more keys to fetch from the language dictionary.
Returns:
str | list[str] | None: The requested string(s), or "Not found!" if missing.
"""
lang = lang.upper()
if lang not in cls._langs:
lang = "EN"
lang_dict = cls._langs.get(lang, {})
if len(keys) == 1:
value = lang_dict.get(keys[0], "Not found!")
return value
values = [lang_dict.get(key, "Not found!") for key in keys]
return values
@classmethod
async def get_lang(cls, guild_id: int, *keys) -> Optional[Union[list[str], str]]:
"""
Fetch the language setting for a guild from the database and return
the requested string(s).
Args:
guild_id (int): Guild ID to fetch settings for.
*keys: One or more keys to fetch from the language dictionary.
Returns:
str | list[str] | None: The requested string(s).
"""
settings = await MongoDBHandler.get_settings(guild_id)
lang = settings.get("lang", "EN")
return cls._get_lang(lang, *keys)
@classmethod
def get_all_languages(cls) -> List[str]:
"""
Get a combined list of all loaded language codes (main + local).
Returns:
list[str]: List of all available language codes.
"""
return cls._langs.keys()

View File

@@ -29,15 +29,16 @@ import hashlib
import base64
import json
import urllib.parse
import function as func
from datetime import datetime
from abc import ABC, abstractmethod
from urllib.parse import quote
from math import floor
from urllib.parse import quote
from abc import ABC, abstractmethod
from importlib import import_module
from datetime import datetime
from typing import Optional, Type
from .config import Config
userAgents = '''Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.11 Safari/535.19
@@ -202,7 +203,7 @@ class A_ZLyrics(LyricsPlatform):
class Genius(LyricsPlatform):
def __init__(self) -> None:
self.module = import_module("lyricsgenius")
self.genius = self.module.Genius(func.settings.genius_token)
self.genius = self.module.Genius(Config().genius_token)
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
song = self.genius.search_song(title=title, artist=artist)

514
voicelink/mongodb.py Normal file
View File

@@ -0,0 +1,514 @@
"""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 copy
import time
import asyncio
import logging
from typing import Any, Dict, Optional, Literal, TypedDict, List
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
logger: logging.Logger = logging.getLogger("vocard.db")
# Type definitions for better code clarity
class PlaylistPerms(TypedDict):
read: List[int]
write: List[int]
remove: List[int]
class Playlist(TypedDict):
tracks: List[Dict[str, Any]]
perms: PlaylistPerms
name: str
type: Literal["playlist"]
class UserData(TypedDict):
_id: int
playlist: Dict[str, Playlist]
history: List[Dict[str, Any]]
inbox: List[Dict[str, Any]]
UpdateOperationType = Literal["$set", "$unset", "$inc", "$push", "$pull"]
class MongoDBHandler:
"""
Handles MongoDB operations with connection pooling and caching.
Implements a thread-safe singleton pattern for database connections.
"""
# Static instance variables
_client: Optional[AsyncIOMotorClient] = None
_db: Optional[Any] = None
_settings_db: Optional[AsyncIOMotorCollection] = None
_users_db: Optional[AsyncIOMotorCollection] = None
_lock: asyncio.Lock = asyncio.Lock()
# Cache with TTL (Time To Live in seconds)
_CACHE_TTL: int = 300 # 5 minutes
_settings_buffer: Dict[int, Dict[str, Any]] = {}
_users_buffer: Dict[int, Dict[str, Any]] = {}
_last_access: Dict[int, float] = {} # Tracks last access time for cache entries
# Maximum cache size to prevent memory issues
_MAX_CACHE_SIZE: int = 10000
# Default user template
_user_base: UserData = {
"_id": 0, # Will be replaced with actual user ID
"playlist": {
"200": {
"tracks": [],
"perms": {"read": [], "write": [], "remove": []},
"name": "Favourite",
"type": "playlist",
}
},
"history": [],
"inbox": [],
}
@classmethod
async def init(cls, uri: str, db_name: str) -> None:
"""
Initialize the MongoDB connection with connection pooling and error handling.
Args:
uri (str): MongoDB connection URI
db_name (str): Name of the database to use
Raises:
ConnectionError: If unable to connect to MongoDB
Exception: For other initialization errors
"""
if not uri or not db_name:
logger.error("MongoDB initialization failed: URI or database name is missing.")
raise ValueError("Both URI and database name must be provided.")
async with cls._lock:
if cls._client is not None:
logger.warning("MongoDB client is already initialized. Skipping reinitialization.")
return
logger.debug("Initializing MongoDB client with URI: %s and DB name: %s", uri, db_name)
try:
cls._client = AsyncIOMotorClient(
uri,
maxPoolSize=50,
minPoolSize=5,
maxIdleTimeMS=60000,
retryWrites=True
)
logger.debug("MongoDB client created successfully. Testing connection...")
await cls._client.server_info()
logger.debug("MongoDB connection test passed.")
cls._db = cls._client[db_name]
cls._settings_db = cls._db["Settings"]
cls._users_db = cls._db["Users"]
logger.info("MongoDB databases initialized: %s", db_name)
except Exception as e:
logger.error("MongoDB initialization failed: %s", str(e), exc_info=True)
cls._client = None
cls._db = None
cls._settings_db = None
cls._users_db = None
raise ConnectionError(f"Failed to initialize MongoDB: {str(e)}")
@classmethod
async def cleanup_cache(cls) -> None:
"""
Cleanup expired cache entries to prevent memory leaks.
Should be called periodically or when cache size exceeds _MAX_CACHE_SIZE.
"""
current_time = time.time()
logger.info("Starting cache cleanup at timestamp: %.2f", current_time)
async with cls._lock:
try:
# Remove expired entries from settings cache
expired_settings = [
guild_id for guild_id, last_access in cls._last_access.items()
if current_time - last_access > cls._CACHE_TTL and guild_id in cls._settings_buffer
]
logger.debug("Found %d expired cache entries.", len(expired_settings))
for guild_id in expired_settings:
del cls._settings_buffer[guild_id]
del cls._last_access[guild_id]
logger.debug("Removed expired cache for guild_id: %s", guild_id)
# If still too large, remove oldest entries
while len(cls._settings_buffer) > cls._MAX_CACHE_SIZE:
oldest_id = min(cls._last_access.items(), key=lambda x: x[1])[0]
del cls._settings_buffer[oldest_id]
del cls._last_access[oldest_id]
logger.warning("Cache size exceeded. Removed oldest entry: %s", oldest_id)
logger.info("Cache cleanup completed. Current cache size: %d", len(cls._settings_buffer))
except Exception as e:
logger.error("Cache cleanup failed: %s", str(e), exc_info=True)
@classmethod
async def _update_db(
cls,
db: AsyncIOMotorCollection,
cache: Dict[str, Any],
filter_: Dict[str, Any],
data: Dict[UpdateOperationType, Dict[str, Any]],
) -> bool:
"""
Update database and cache atomically with error handling and validation.
Args:
db: MongoDB collection to update
cache: Cache dictionary to update
filter_: MongoDB filter for the update
data: Update operations to perform
Returns:
bool: True if update was successful, False otherwise
Raises:
ValueError: If invalid update operation is provided
"""
async with cls._lock:
try:
# Validate update operations
valid_operations = {"$set", "$unset", "$inc", "$push", "$pull"}
if not all(op in valid_operations for op in data.keys()):
raise ValueError(f"Invalid update operation. Must be one of {valid_operations}")
# Update cache first
for mode, action in data.items():
for key, value in action.items():
cursors = key.split(".")
nested = cache
# Ensure path exists
for c in cursors[:-1]:
if not isinstance(nested, dict):
raise ValueError(f"Invalid path: {key}")
nested = nested.setdefault(c, {})
field = cursors[-1]
try:
if mode == "$set":
nested[field] = value
elif mode == "$unset":
nested.pop(field, None)
elif mode == "$inc":
if not isinstance(nested.get(field, 0), (int, float)):
raise ValueError(f"Cannot increment non-numeric field: {field}")
nested[field] = nested.get(field, 0) + value
elif mode == "$push":
arr = nested.setdefault(field, [])
if not isinstance(arr, list):
raise ValueError(f"Cannot push to non-array field: {field}")
if isinstance(value, dict) and "$each" in value:
arr.extend(value["$each"])
if "$slice" in value:
arr[:] = arr[value["$slice"]:]
else:
arr.append(value)
elif mode == "$pull":
if field in nested:
if not isinstance(nested[field], list):
raise ValueError(f"Cannot pull from non-array field: {field}")
values = value.get("$in", []) if isinstance(value, dict) else [value]
nested[field] = [item for item in nested[field] if item not in values]
except Exception as e:
raise ValueError(f"Error updating {key}: {str(e)}")
# Then update database
result = await db.update_one(filter_, data)
# Update last access time
if '_id' in filter_:
cls._last_access[filter_['_id']] = time.time()
return result.modified_count > 0
except Exception as e:
# Rollback cache if database update fails
if '_id' in filter_:
cls._settings_buffer.pop(filter_['_id'], None)
cls._users_buffer.pop(filter_['_id'], None)
raise Exception(f"Update failed: {str(e)}")
@classmethod
async def get_settings(
cls,
guild_id: int,
*,
force_refresh: bool = False
) -> Dict[str, Any]:
"""
Retrieve settings for a guild with caching.
Args:
guild_id: The Discord guild ID
force_refresh: If True, bypass cache and fetch fresh data
Returns:
Dict containing guild settings
Raises:
ConnectionError: If database operation fails
"""
try:
async with cls._lock:
# Check if we need fresh data
if force_refresh or guild_id not in cls._settings_buffer:
settings = await cls._settings_db.find_one({"_id": guild_id})
if not settings:
settings = {"_id": guild_id}
try:
await cls._settings_db.insert_one(settings)
except Exception as e:
raise ConnectionError(f"Failed to create settings: {str(e)}")
cls._settings_buffer[guild_id] = settings
cls._last_access[guild_id] = time.time()
return copy.deepcopy(cls._settings_buffer[guild_id])
except Exception as e:
raise ConnectionError(f"Failed to retrieve settings: {str(e)}")
@classmethod
async def update_settings(
cls,
guild_id: int,
data: Dict[UpdateOperationType, Dict[str, Any]],
*,
upsert: bool = False
) -> bool:
"""
Update settings for a guild.
Args:
guild_id: The Discord guild ID
data: Update operations to perform
upsert: If True, create document if it doesn't exist
Returns:
bool: True if update was successful
Raises:
ValueError: If invalid update data is provided
ConnectionError: If database operation fails
"""
try:
settings = await cls.get_settings(guild_id)
result = await cls._update_db(
cls._settings_db,
settings,
{"_id": guild_id},
data
)
if not result and upsert:
# Try to insert if update failed and upsert is True
settings = {"_id": guild_id, **data.get("$set", {})}
await cls._settings_db.insert_one(settings)
cls._settings_buffer[guild_id] = settings
return True
return result
except Exception as e:
raise ConnectionError(f"Failed to update settings: {str(e)}")
@classmethod
async def get_user(
cls,
user_id: int,
*,
d_type: Optional[str] = None,
need_copy: bool = True,
force_refresh: bool = False
) -> Dict[str, Any]:
"""
Retrieve user data with caching and type-specific data.
Args:
user_id: The Discord user ID
d_type: Specific data type to retrieve
need_copy: If True, return a deep copy of the data
force_refresh: If True, bypass cache and fetch fresh data
Returns:
Dict containing user data
Raises:
ConnectionError: If database operation fails
ValueError: If invalid d_type is provided
"""
try:
async with cls._lock:
# Check if we need fresh data
if force_refresh or user_id not in cls._users_buffer:
user = await cls._users_db.find_one({"_id": user_id})
if not user:
user = {"_id": user_id, **copy.deepcopy(cls._user_base)}
try:
await cls._users_db.insert_one(user)
except Exception as e:
raise ConnectionError(f"Failed to create user: {str(e)}")
cls._users_buffer[user_id] = user
cls._last_access[user_id] = time.time()
user = cls._users_buffer[user_id]
if d_type:
if d_type not in cls._user_base:
raise ValueError(f"Invalid data type: {d_type}")
user = user.setdefault(d_type, copy.deepcopy(cls._user_base.get(d_type)))
return copy.deepcopy(user) if need_copy else user
except Exception as e:
raise ConnectionError(f"Failed to retrieve user data: {str(e)}")
@classmethod
async def update_user(
cls,
user_id: int,
data: Dict[UpdateOperationType, Dict[str, Any]],
*,
upsert: bool = False
) -> bool:
"""
Update user data.
Args:
user_id: The Discord user ID
data: Update operations to perform
upsert: If True, create user if doesn't exist
Returns:
bool: True if update was successful
Raises:
ValueError: If invalid update data is provided
ConnectionError: If database operation fails
"""
try:
user = await cls.get_user(user_id, need_copy=False)
result = await cls._update_db(
cls._users_db,
user,
{"_id": user_id},
data
)
if not result and upsert:
# Try to insert if update failed and upsert is True
user_data = {"_id": user_id, **data.get("$set", {})}
await cls._users_db.insert_one(user_data)
cls._users_buffer[user_id] = user_data
return True
return result
except Exception as e:
raise ConnectionError(f"Failed to update user: {str(e)}")
@classmethod
async def delete_user(cls, user_id: int) -> bool:
"""
Delete a user's data completely.
Args:
user_id: The Discord user ID
Returns:
bool: True if deletion was successful
Raises:
ConnectionError: If database operation fails
"""
try:
async with cls._lock:
result = await cls._users_db.delete_one({"_id": user_id})
if result.deleted_count > 0:
cls._users_buffer.pop(user_id, None)
cls._last_access.pop(user_id, None)
return True
return False
except Exception as e:
raise ConnectionError(f"Failed to delete user: {str(e)}")
@classmethod
async def get_users_by_criteria(
cls,
criteria: Dict[str, Any],
*,
limit: Optional[int] = None,
skip: int = 0
) -> List[Dict[str, Any]]:
"""
Retrieve multiple users matching specific criteria.
Args:
criteria: MongoDB query criteria
limit: Maximum number of users to return
skip: Number of matching users to skip
Returns:
List of matching user data
Raises:
ConnectionError: If database operation fails
"""
try:
cursor = cls._users_db.find(criteria).skip(skip)
if limit:
cursor = cursor.limit(limit)
users = await cursor.to_list(length=None)
# Update cache with fetched users
async with cls._lock:
current_time = time.time()
for user in users:
user_id = user["_id"]
cls._users_buffer[user_id] = user
cls._last_access[user_id] = current_time
return users
except Exception as e:
raise ConnectionError(f"Failed to retrieve users: {str(e)}")

View File

@@ -22,18 +22,16 @@ SOFTWARE.
"""
import re
from typing import Optional
import discord
from discord import Member
from typing import Optional
from tldextract import extract
from discord import Member
from .enums import SearchType
from function import (
get_source,
time as ctime
)
from .transformer import encode
from .config import Config
from .utils import format_ms
from .transformer import encode, decode
YOUTUBE_REGEX = re.compile(r'(https?://)?(www\.)?youtube\.(com|nl)/watch\?v=([-\w]+)')
@@ -83,7 +81,7 @@ class Track:
if not self.thumbnail and YOUTUBE_REGEX.match(self.uri):
self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/maxresdefault.jpg"
self.emoji: str = get_source(self.source, "emoji")
self.emoji: str = Config().get_source_config(self.source, "emoji")
self.length: float = info.get("length")
self.requester: Member = requester
@@ -114,7 +112,7 @@ class Track:
@property
def formatted_length(self) -> str:
return ctime(self.length)
return format_ms(self.length)
@property
def data(self) -> dict:
@@ -123,6 +121,14 @@ class Track:
"requester_id": self.requester.id
}
@classmethod
def decode(cls, track_id: str) -> dict:
return decode(track_id)
@classmethod
def encode(cls, track_info: dict) -> 'Track':
return encode(track_info)
class Playlist:
"""The base playlist object.
Returns critical playlist information needed for parsing by Lavalink.
@@ -162,4 +168,4 @@ class Playlist:
@property
def track_count(self) -> int:
return len(self.tracks)
return len(self.tracks)

View File

@@ -24,27 +24,30 @@ SOFTWARE.
from __future__ import annotations
import re
import function as func
import discord
from discord import Embed, Client
from discord.ext import commands
from typing import TYPE_CHECKING, List, Callable
from typing import TYPE_CHECKING
from .config import Config
from .utils import format_ms
if TYPE_CHECKING:
from .player import Player
from .objects import Track
def ensure_track(func) -> callable:
def wrapper(self: Placeholders, *args, **kwargs):
from .pool import NodePool
def ensure_track(func) -> Callable:
def wrapper(self: PlayerPlaceholder, *args, **kwargs):
current = self.get_current()
if not current:
return "None"
return func(self, current, *args, **kwargs)
return wrapper
class Placeholders:
def __init__(self, bot: Client, player: Player = None) -> None:
self.bot: Client = bot
class PlayerPlaceholder:
def __init__(self, bot: commands.Bot, player: Player = None) -> None:
self.bot: commands.Bot = bot
self.player: Player = player
self.variables = {
@@ -67,7 +70,7 @@ class Placeholders:
"loop_mode": self.loop_mode,
"default_embed_color": self.default_embed_color,
"bot_icon": self.bot_icon,
"server_invite_link": func.settings.invite_link,
"server_invite_link": Config().invite_link,
"invite_link": f"https://discord.com/oauth2/authorize?client_id={self.bot.user.id}&permissions=2184260928&scope=bot%20applications.commands"
}
@@ -97,7 +100,7 @@ class Placeholders:
@ensure_track
def track_duration(self, track: Track) -> str:
return self.player.get_msg("live") if track.is_stream else func.time(track.length)
return self.player.get_msg("live") if track.is_stream else format_ms(track.length)
@ensure_track
def track_requester_id(self, track: Track) -> str:
@@ -117,7 +120,7 @@ class Placeholders:
@ensure_track
def track_color(self, track: Track) -> int:
return int(func.get_source(track.source, "color"), 16)
return int(Config().get_source_config(track.source, "color"), 16)
@ensure_track
def track_source_name(self, track: Track) -> str:
@@ -152,7 +155,7 @@ class Placeholders:
return self.player.queue.repeat if self.player else "Off"
def default_embed_color(self) -> int:
return func.settings.embed_color
return Config().embed_color
def bot_icon(self) -> str:
return self.bot.user.display_avatar.url if self.player else "https://i.imgur.com/dIFBwU7.png"
@@ -197,41 +200,78 @@ class Placeholders:
text = re.sub(r'@@(.*?)@@', lambda x: str(variables.get(x.group(1), '')), text)
return text
def build_embed(embed_form: dict[str, dict], placeholder: Placeholders) -> Embed:
embed = Embed()
try:
rv = {key: func() if callable(func) else func for key, func in placeholder.variables.items()}
if author := embed_form.get("author"):
embed.set_author(
name = placeholder.replace(author.get("name"), rv),
url = placeholder.replace(author.get("url"), rv),
icon_url = placeholder.replace(author.get("icon_url"), rv)
)
if title := embed_form.get("title"):
embed.title = placeholder.replace(title.get("name"), rv)
embed.url = placeholder.replace(title.get("url"), rv)
@classmethod
def build_embed(cls, embed_form: dict[str, dict], placeholder: PlayerPlaceholder) -> discord.Embed:
embed = discord.Embed()
try:
rv = {key: func() if callable(func) else func for key, func in placeholder.variables.items()}
if author := embed_form.get("author"):
embed.set_author(
name = placeholder.replace(author.get("name"), rv),
url = placeholder.replace(author.get("url"), rv),
icon_url = placeholder.replace(author.get("icon_url"), rv)
)
if title := embed_form.get("title"):
embed.title = placeholder.replace(title.get("name"), rv)
embed.url = placeholder.replace(title.get("url"), rv)
if fields := embed_form.get("fields", []):
for f in fields:
embed.add_field(name=placeholder.replace(f.get("name"), rv), value=placeholder.replace(f.get("value", ""), rv), inline=f.get("inline", False))
if fields := embed_form.get("fields", []):
for f in fields:
embed.add_field(name=placeholder.replace(f.get("name"), rv), value=placeholder.replace(f.get("value", ""), rv), inline=f.get("inline", False))
if footer := embed_form.get("footer"):
embed.set_footer(
text = placeholder.replace(footer.get("text"), rv),
icon_url = placeholder.replace(footer.get("icon_url"), rv)
)
if footer := embed_form.get("footer"):
embed.set_footer(
text = placeholder.replace(footer.get("text"), rv),
icon_url = placeholder.replace(footer.get("icon_url"), rv)
)
if thumbnail := embed_form.get("thumbnail"):
embed.set_thumbnail(url = placeholder.replace(thumbnail, rv))
if image := embed_form.get("image"):
embed.set_image(url = placeholder.replace(image, rv))
if thumbnail := embed_form.get("thumbnail"):
embed.set_thumbnail(url = placeholder.replace(thumbnail, rv))
if image := embed_form.get("image"):
embed.set_image(url = placeholder.replace(image, rv))
embed.description = placeholder.replace(embed_form.get("description"), rv)
embed.color = int(placeholder.replace(embed_form.get("color"), rv))
embed.description = placeholder.replace(embed_form.get("description"), rv)
embed.color = int(placeholder.replace(embed_form.get("color"), rv))
except:
pass
except:
pass
return embed
return embed
class BotPlaceholder:
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.variables = {
"guilds": self.guilds_count,
"users": self.users_count,
"players": self.players_count,
"nodes": self.nodes_count
}
def guilds_count(self) -> int:
return len(self.bot.guilds)
def users_count(self) -> int:
return len(self.bot.users)
def players_count(self) -> int:
count = 0
for node in NodePool._nodes.values():
count += len(node._players)
return count
def nodes_count(self):
return len(NodePool._nodes)
def replace(self, msg: str) -> str:
keys: List[str] = re.findall(r'@@(.*?)@@', msg)
for key in keys:
value = self.variables.get(key.lower(), None)
if value:
msg = msg.replace(f"@@{key}@@", str(value()))
return msg

View File

@@ -22,11 +22,10 @@ SOFTWARE.
"""
import time, logging
import function as func
from math import ceil
from asyncio import sleep
from views import InteractiveController
from random import shuffle, choice
from typing import Any, Dict, List, Optional, Union, Tuple
from discord import (
@@ -45,18 +44,22 @@ from discord import (
from discord.ext import commands
from . import events
from .config import Config
from .pool import Node, NodePool
from .objects import Track, Playlist
from .filters import Filter, Filters
from .enums import SearchType, LoopType, RequestMethod
from .events import VoicelinkEvent, TrackEndEvent, TrackStartEvent, TrackExceptionEvent
from .exceptions import VoicelinkException, FilterInvalidArgument, TrackInvalidPosition, FilterTagAlreadyInUse, DuplicateTrack
from .filters import Filter, Filters
from .objects import Track, Playlist
from .pool import Node, NodePool
from .placeholders import Placeholders, build_embed
from .placeholders import PlayerPlaceholder
from .queue import Queue, QUEUE_TYPES
from random import shuffle, choice
from .mongodb import MongoDBHandler
from .language import LangHandler
from .views import InteractiveController
from .utils import format_ms, dispatch_message
async def connect_channel(ctx: Union[commands.Context, Interaction], channel: VoiceChannel = None):
texts = await func.get_lang(ctx.guild.id, "noChannel", "noPermission")
texts = await LangHandler.get_lang(ctx.guild.id, "noChannel", "noPermission")
try:
channel = channel or ctx.author.voice.channel if isinstance(ctx, commands.Context) else ctx.user.voice.channel
except:
@@ -66,7 +69,7 @@ async def connect_channel(ctx: Union[commands.Context, Interaction], channel: Vo
if check.connect == False or check.speak == False:
raise VoicelinkException(texts[1])
settings = await func.get_settings(channel.guild.id)
settings = await MongoDBHandler.get_settings(channel.guild.id)
player: Player = await channel.connect(
cls=Player(
ctx.bot if isinstance(ctx, commands.Context) else ctx.client,
@@ -116,7 +119,7 @@ class Player(VoiceProtocol):
self.joinTime: float = round(time.time())
self._volume: int = self.settings.get('volume', 100)
self.queue: Queue = QUEUE_TYPES.get(self.settings.get("queue_type", "queue").lower())(
self.settings.get("max_queue", func.settings.max_queue),
self.settings.get("max_queue", Config().max_queue),
self.settings.get("duplicate_track", True), self.get_msg
)
@@ -145,7 +148,7 @@ class Player(VoiceProtocol):
self.shuffle_votes = set()
self.stop_votes = set()
self._ph = Placeholders(client, self)
self._ph = PlayerPlaceholder(client, self)
self._logger: Optional[logging.Logger] = self._node._logger
def __repr__(self):
@@ -258,7 +261,7 @@ class Player(VoiceProtocol):
"""Retrieves a localized message or list of messages based on the given keys
for the guild associated with this player.
"""
return func._get_lang(self.settings.get("lang"), *keys)
return LangHandler._get_lang(self.settings.get("lang"), *keys)
def required(self, leave=False):
"""
@@ -291,7 +294,7 @@ class Player(VoiceProtocol):
has 'Manage Server' permission, or meets the DJ role criteria in the settings.
Raises an exception if `check_user_join` is True and the user is not in the channel.
"""
if user.id in func.settings.bot_access_user:
if user.id in Config().bot_access_user:
return True
manage_perm = user.guild_permissions.manage_guild
@@ -304,10 +307,10 @@ class Player(VoiceProtocol):
def build_embed(self, current_track: Track = None):
"""Builds an embed based on the current track state."""
controller = self.settings.get("default_controller", func.settings.controller).get("embeds", {})
controller = self.settings.get("default_controller", Config().controller).get("embeds", {})
embed_form = controller.get("active" if current_track else "inactive", {})
return build_embed(embed_form, self._ph)
return PlayerPlaceholder.build_embed(embed_form, self._ph)
async def send(self, method: RequestMethod, query: str = None, data: Union[Dict, str] = {}) -> Dict:
"""Sends an HTTP request to the node with the given method, query, and data."""
@@ -424,7 +427,7 @@ class Player(VoiceProtocol):
return await self.do_next()
if not track.requester.bot:
self._bot.loop.create_task(func.update_user(track.requester.id, {
self._bot.loop.create_task(MongoDBHandler.update_user(track.requester.id, {
"$push": {"history": {"$each": [track.track_id], "$slice": -25}}
}))
@@ -463,11 +466,11 @@ class Player(VoiceProtocol):
# Send a new controller message if none exists
if not self.controller:
self.controller = await func.send(self.context, content=embed, view=view, requires_fetch=True)
self.controller = await dispatch_message(self.context, content=embed, view=view, requires_fetch=True)
elif not await self.is_position_fresh():
await self.controller.delete()
self.controller = await func.send(self.context, content=embed, view=view, requires_fetch=True)
self.controller = await dispatch_message(self.context, content=embed, view=view, requires_fetch=True)
else:
await self.controller.edit(embed=embed, view=view)
@@ -495,7 +498,7 @@ class Player(VoiceProtocol):
async def teardown(self):
"""Cleans up the player and associated resources."""
try:
await func.update_settings(self.guild.id, {"$set": {
await MongoDBHandler.update_settings(self.guild.id, {"$set": {
"last_active": (timeNow := round(time.time())),
"played_time": round(self.settings.get("played_time", 0) + ((timeNow - self.joinTime) / 60), 2)
}})
@@ -611,9 +614,9 @@ class Player(VoiceProtocol):
track_length = track.length
if not 0 <= start_time <= track_length:
raise VoicelinkException(self.get_msg("invalidStartTime", func.time(track_length)))
raise VoicelinkException(self.get_msg("invalidStartTime", format_ms(track_length)))
if not 0 <= end_time <= track_length:
raise VoicelinkException(self.get_msg("invalidEndTime", func.time(track_length)))
raise VoicelinkException(self.get_msg("invalidEndTime", format_ms(track_length)))
track.position = start_time
track.end_time = end_time
@@ -864,7 +867,7 @@ class Player(VoiceProtocol):
async def update_voice_status(self, remove_status: bool = False) -> None:
"""Updates the voice status of the channel based on the specified template."""
template = self.settings.get("stage_announce_template", func.settings.voice_status_template)
template = self.settings.get("stage_announce_template", Config().voice_status_template)
if not template or not self.channel:
return

View File

@@ -509,7 +509,7 @@ class NodePool:
raise NodeCreationError(f"A node with identifier '{identifier}' already exists.")
if not logger:
logger = logging.getLogger("voicelink")
logger = logging.getLogger("vocard")
node = Node(
pool=cls, bot=bot, host=host, port=port, password=password,

View File

@@ -21,14 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import discord
from itertools import cycle
from typing import Optional, Tuple, Callable, Dict, List
from .exceptions import QueueFull, OutofList
from .objects import Track
from .enums import LoopType
from typing import Optional, Tuple, Callable, Dict, List
from itertools import cycle
from discord import Member
class LoopTypeCycle:
def __init__(self) -> None:
self._cycle = cycle(LoopType)
@@ -151,7 +152,7 @@ class Queue:
except:
raise OutofList(self.get_msg("voicelinkOutofList"))
def remove(self, index: int, index2: int = None, member: Member = None) -> Dict[int, Track]:
def remove(self, index: int, index2: int = None, member: discord.Member = None) -> Dict[int, Track]:
pos = self._position - 1
if index2 is None:

View File

@@ -22,8 +22,9 @@ SOFTWARE.
"""
import time
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, TYPE_CHECKING, Any
from typing import List, Optional, Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from .pool import Node

View File

@@ -24,19 +24,25 @@ SOFTWARE.
import random
import time
import socket
from timeit import default_timer as timer
import discord
from itertools import zip_longest
from typing import Dict, Optional
from timeit import default_timer as timer
from discord.ext import commands
from typing import Union
__all__ = [
"ExponentialBackoff",
"NodeStats",
"NodeInfoVersion",
"NodeInfo",
"Plugin",
"Ping"
]
from .mongodb import MongoDBHandler
from .language import LangHandler
# __all__ = [
# "ExponentialBackoff",
# "NodeStats",
# "NodeInfoVersion",
# "NodeInfo",
# "Plugin",
# "Ping"
# ]
class ExponentialBackoff:
"""
@@ -203,4 +209,200 @@ class Ping:
((self._host, self._port), None))
s_runtime = 1000 * (cost_time)
return s_runtime
return s_runtime
class TempCtx():
def __init__(self, author: discord.Member, channel: discord.VoiceChannel) -> None:
self.author: discord.Member = author
self.channel: discord.VoiceChannel = channel
self.guild: discord.Guild = channel.guild
def format_to_ms(time_str: str) -> int:
"""
Converts a time string in one of the formats ('HH:MM:SS', 'MM:SS', 'SS') to milliseconds.
Args:
time_str (str): The input time string.
Returns:
int: Time in milliseconds, or 0 if parsing fails.
"""
formats = ['%H:%M:%S', '%M:%S', '%S']
for fmt in formats:
try:
parsed = time.strptime(time_str, fmt)
total_seconds = parsed.tm_hour * 3600 + parsed.tm_min * 60 + parsed.tm_sec
return total_seconds * 1000
except ValueError:
continue
return 0
def format_ms(milliseconds: Union[float, int]) -> str:
"""
Converts milliseconds to a formatted time string.
Args:
milliseconds (int): The time in milliseconds.
Returns:
str: The formatted time string.
Examples:
65000 -> "01:05"
3723000 -> "1:02:03"
90061000 -> "1 days, 01:01:01"
"""
if isinstance(milliseconds, float):
milliseconds = int(milliseconds)
seconds = (milliseconds // 1000) % 60
minutes = (milliseconds // 60_000) % 60
hours = (milliseconds // 3_600_000) % 24
days = milliseconds // 86_400_000
if days:
return f"{days} days, {hours:02}:{minutes:02}:{seconds:02}"
if hours:
return f"{hours}:{minutes:02}:{seconds:02}"
return f"{minutes:02}:{seconds:02}"
def format_bytes(bytes: int, unit: bool = False):
"""
Converts bytes to a human-readable string in MB or GB.
Args:
bytes (int): The number of bytes.
unit (bool): If True, appends the unit (MB or GB) to the result.
Returns:
str: The formatted string.
"""
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 "")
def truncate_string(text: str, length: int = 40) -> str:
"""
Truncates a string to a specified maximum length, appending '...' if the string exceeds that length.
Args:
text (str): The input string to truncate.
length (int, optional): The maximum allowed length of the output string, including the ellipsis. Defaults to 40.
Returns:
str: The truncated string with '...' appended if it was longer than the specified length.
"""
return text[:length - 3] + "..." if len(text) > length else text
async def dispatch_message(
ctx: Union[commands.Context, discord.Interaction, TempCtx],
content: Union[str, discord.Embed] = None,
*params,
view: discord.ui.View = None,
file: discord.File = None,
delete_after: float = None,
ephemeral: bool = False,
requires_fetch: bool = False
) -> Optional[discord.Message]:
"""
Dispatches a message or embed to the appropriate Discord context.
Args:
ctx: The command or interaction context.
content: The message content or embed to send.
*params: Parameters to format the content string.
view: Optional UI view.
file: Optional file to attach.
delete_after: Optional auto-delete duration.
ephemeral: Whether the message should be ephemeral.
requires_fetch: Whether to fetch the message after sending.
Returns:
The sent message object, or None.
"""
if content is None:
content = "No content provided."
# Determine the text to send
embed = content if isinstance(content, discord.Embed) else None
text = None if embed else content.format(*params)
# Determine the sending function
send_func = (
ctx.send if isinstance(ctx, commands.Context) else
ctx.channel.send if isinstance(ctx, TempCtx) else
ctx.followup.send if ctx.response.is_done() else
ctx.response.send_message
)
# Check settings for delete_after duration
settings = await MongoDBHandler.get_settings(ctx.guild.id)
send_kwargs = {
"content": text,
"embed": embed,
"allowed_mentions": discord.AllowedMentions().none(),
"silent": settings.get("silent_msg", False),
}
if file:
send_kwargs["file"] = file
if view:
send_kwargs["view"] = view
if "delete_after" in send_func.__code__.co_varnames:
if settings and ctx.channel.id == settings.get("music_request_channel", {}).get("text_channel_id"):
delete_after = 10
send_kwargs["delete_after"] = delete_after
if "ephemeral" in send_func.__code__.co_varnames:
send_kwargs["ephemeral"] = ephemeral
# Send the message or embed
message = await send_func(**send_kwargs)
if isinstance(message, discord.InteractionCallbackResponse):
message = message.resource
if requires_fetch and isinstance(message, (discord.WebhookMessage, discord.InteractionMessage)):
message = await message.fetch()
return message
async def send_localized_message(
ctx: Union[commands.Context, discord.Interaction, TempCtx],
content_key: str,
*params,
language: str = None,
**kwargs
) -> Optional[discord.Message]:
"""
Sends a localized message using a language key and optional formatting parameters.
Args:
ctx (Union[commands.Context, discord.Interaction]): The Discord context or interaction.
content_key (str): The key used to retrieve the localized message.
language (str, optional): Language code to override guild default. Must exist in LangHandler.get_all_languages().
*params: Optional parameters to format the localized message.
**kwargs: Additional keyword arguments passed to dispatch_message (e.g., view, file, delete_after, ephemeral, requires_fetch).
Returns:
Optional[discord.Message]: The sent message object, or None if sending failed.
"""
if language and language in LangHandler.get_all_languages():
localized_text = LangHandler._get_lang(language, content_key)
else:
localized_text = await LangHandler.get_lang(ctx.guild.id, content_key)
if localized_text:
try:
formatted_text = localized_text.format(*params)
except (IndexError, KeyError):
formatted_text = localized_text
else:
formatted_text = "Translation not found."
return await dispatch_message(ctx, content=formatted_text, **kwargs)

View File

@@ -21,19 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from .embed_builder import EmbedBuilderView
from .controller import InteractiveController
from .debug import DebugView
from .link import LinkView
from .inbox import InboxView
from .playlist import PlaylistViewManager
from .lyrics import LyricsView
from .queue import QueueView
from .help import HelpView
from .search import SearchView
from discord.ext import commands
class ButtonOnCooldown(commands.CommandError):
def __init__(self, retry_after: float) -> None:
self.retry_after = retry_after
from .controller import InteractiveController
from .search import SearchView
from .help import HelpView
from .queue import QueueView
from .lyrics import LyricsView
from .playlist import PlaylistViewManager
from .inbox import InboxView
from .link import LinkView
from .debug import DebugView
from .embedBuilder import EmbedBuilderView

View File

@@ -24,16 +24,23 @@ SOFTWARE.
import discord
import re
import voicelink
import addons
import views
import function as func
import traceback
from discord.ext import commands
from typing import Optional, Dict, Type, Union, Any
from ..config import Config
from ..utils import format_ms, send_localized_message
from ..language import LangHandler
from ..mongodb import MongoDBHandler
def key(interaction: discord.Interaction):
return interaction.user
class ButtonOnCooldown(commands.CommandError):
def __init__(self, retry_after: float) -> None:
self.retry_after = retry_after
class ControlButton(discord.ui.Button):
def __init__(
self,
@@ -74,7 +81,7 @@ class ControlButton(discord.ui.Button):
async def send(self, interaction: discord.Interaction, key: str, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None:
stay = self.player.settings.get("controller_msg", True)
return await func.send(
return await send_localized_message(
interaction, key, *params,
view=view,
delete_after=None if ephemeral or stay else 10,
@@ -191,14 +198,14 @@ class AddFav(ControlButton):
return await self.send(interaction, "noTrackPlaying")
if track.is_stream:
return await self.send(interaction, "playlistAddError")
user = await func.get_user(interaction.user.id, 'playlist')
rank, max_p, max_t = func.check_roles()
user = await MongoDBHandler.get_user(interaction.user.id, 'playlist')
_, max_t, _ = Config().get_playlist_config()
if len(user['200']['tracks']) >= max_t:
return await self.send(interaction, "playlistLimited", max_t, ephemeral=True)
if track.track_id in user['200']['tracks']:
return await self.send(interaction, "playlistRepeated", ephemeral=True)
respond = await func.update_user(interaction.user.id, {"$push": {'playlist.200.tracks': track.track_id}})
respond = await MongoDBHandler.update_user(interaction.user.id, {"$push": {'playlist.200.tracks': track.track_id}})
if respond:
await self.send(interaction, "playlistAdded", track.title, interaction.user.mention, user['200']['name'], ephemeral=True)
else:
@@ -273,7 +280,7 @@ class AutoPlay(ControlButton):
check = not self.player.settings.get("autoplay", False)
self.player.settings['autoplay'] = check
await self.send(interaction, 'autoplay', await func.get_lang(interaction.guild_id, 'enabled' if check else "disabled"))
await self.send(interaction, 'autoplay', await LangHandler.get_lang(interaction.guild_id, 'enabled' if check else "disabled"))
if not self.player.is_playing:
await self.player.do_next()
@@ -313,7 +320,7 @@ class Forward(ControlButton):
position = int(self.player.position + 10000)
await self.player.seek(position)
await self.send(interaction, 'forward', func.time(position))
await self.send(interaction, 'forward', format_ms(position))
class Rewind(ControlButton):
def __init__(self, **kwargs):
@@ -332,7 +339,7 @@ class Rewind(ControlButton):
position = 0 if (value := int(self.player.position - 30000)) <= 0 else value
await self.player.seek(position)
await self.send(interaction, 'rewind', func.time(position))
await self.send(interaction, 'rewind', format_ms(position))
class Lyrics(ControlButton):
def __init__(self, **kwargs):
@@ -342,19 +349,20 @@ class Lyrics(ControlButton):
)
async def callback(self, interaction: discord.Interaction):
from . import LyricsView
if not self.player or not self.player.is_playing:
return await self.send(interaction, "noTrackPlaying", ephemeral=True)
title = self.player.current.title
artist = self.player.current.author
lyrics_platform = addons.LYRICS_PLATFORMS.get(func.settings.lyrics_platform)
lyrics_platform = voicelink.LYRICS_PLATFORMS.get(Config().lyrics_platform)
if lyrics_platform:
lyrics = await lyrics_platform().get_lyrics(title, artist)
if not lyrics:
return await self.send(interaction, "lyricsNotFound", ephemeral=True)
view = views.LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user)
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user)
view.response = await self.send(interaction, view.build_embed(), view=view, ephemeral=True)
class Tracks(discord.ui.Select):
@@ -379,13 +387,13 @@ class Tracks(discord.ui.Select):
async def callback(self, interaction: discord.Interaction):
if not self.player.is_privileged(interaction.user):
return await func.send(interaction, "missingFunctionPerm", ephemeral=True)
return await send_localized_message(interaction, "missingFunctionPerm", ephemeral=True)
self.player.queue.skipto(int(self.values[0].split(". ")[0]))
await self.player.stop()
if self.player.settings.get("controller_msg", True):
await func.send(interaction, "skipped", interaction.user)
await send_localized_message(interaction, "skipped", interaction.user)
class Effects(discord.ui.Select):
def __init__(self, player: "voicelink.Player", btn_data, row):
@@ -404,20 +412,20 @@ class Effects(discord.ui.Select):
async def callback(self, interaction: discord.Interaction):
if not self.player.is_privileged(interaction.user):
return await func.send(interaction, "missingFunctionPerm", ephemeral=True)
return await send_localized_message(interaction, "missingFunctionPerm", ephemeral=True)
avalibable_filters = voicelink.Filters.get_available_filters()
if self.values[0] == "None":
await self.player.reset_filter(requester=interaction.user)
return await func.send(interaction, "clearEffect")
return await send_localized_message(interaction, "clearEffect")
selected_filter = avalibable_filters.get(self.values[0].lower())()
if self.player.filters.has_filter(filter_tag=selected_filter.tag):
await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user)
await func.send(interaction, "clearEffect")
await send_localized_message(interaction, "clearEffect")
else:
await self.player.add_filter(selected_filter, requester=interaction.user)
await func.send(interaction, "addEffect", selected_filter.tag)
await send_localized_message(interaction, "addEffect", selected_filter.tag)
BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = {
"back": Back,
@@ -443,7 +451,7 @@ class InteractiveController(discord.ui.View):
super().__init__(timeout=None)
self.player: voicelink.Player = player
for row_num, btn_row in enumerate(func.settings.controller.get("buttons")):
for row_num, btn_row in enumerate(Config().controller.get("buttons")):
for btn_name, btn_data in btn_row.items():
btn_class = BUTTON_TYPE.get(btn_name.lower())
if not btn_class:
@@ -458,27 +466,28 @@ class InteractiveController(discord.ui.View):
async def interaction_check(self, interaction: discord.Interaction):
if not self.player.node._available:
await func.send(interaction, "nodeReconnect", ephemeral=True)
await send_localized_message(interaction, "nodeReconnect", ephemeral=True)
return False
if interaction.user.id in func.settings.bot_access_user:
if interaction.user.id in Config().bot_access_user:
return True
if self.player.channel and self.player.is_user_join(interaction.user):
retry_after = self.cooldown.update_rate_limit(interaction)
if retry_after:
raise views.ButtonOnCooldown(retry_after)
raise ButtonOnCooldown(retry_after)
return True
else:
await func.send(interaction, "notInChannel", interaction.user.mention, self.player.channel.mention, ephemeral=True)
await send_localized_message(interaction, "notInChannel", interaction.user.mention, self.player.channel.mention, ephemeral=True)
return False
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
if isinstance(error, views.ButtonOnCooldown):
if isinstance(error, ButtonOnCooldown):
sec = int(error.retry_after)
await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True)
elif isinstance(error, Exception):
traceback.print_exception(error)
await interaction.response.send_message(error)
return

View File

@@ -24,15 +24,18 @@ SOFTWARE.
import discord
import io
import os
import json
import contextlib
import textwrap
import traceback
import voicelink
import function as func
from typing import Optional
from discord.ext import commands
from ..config import Config
from ..utils import format_ms, format_bytes
class ExecuteModal(discord.ui.Modal):
def __init__(self, code: str, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@@ -103,11 +106,7 @@ class AddNodeModal(discord.ui.Modal):
await interaction.response.defer()
try:
await voicelink.NodePool.create_node(
bot=interaction.client,
logger=func.logger,
**config
)
await voicelink.NodePool.create_node(bot=interaction.client, **config)
await interaction.followup.send(f"Node {self.children[4].value} is connected!", ephemeral=True)
await self.view.message.edit(embed=self.view.build_embed(), view=self.view)
@@ -278,7 +277,7 @@ class NodesPanel(discord.ui.View):
def build_embed(self) -> discord.Embed:
self.update_btn_status()
embed = discord.Embed(title="📡 Nodes Panel", color=func.settings.embed_color)
embed = discord.Embed(title="📡 Nodes Panel", color=Config().embed_color)
if not voicelink.NodePool._nodes:
embed.description = "```There are no nodes are connected!```"
@@ -295,9 +294,9 @@ class NodesPanel(discord.ui.View):
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: {func.format_bytes(node.stats.free)}/{func.format_bytes(total_memory, True)} ({(node.stats.free/total_memory) * 100:.1f}%)\n"
f"• RAM: {format_bytes(node.stats.free)}/{format_bytes(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)}```"
f"• UPTIME: {format_ms(node.stats.uptime)}```"
)
else:
embed.add_field(
@@ -405,9 +404,9 @@ class DebugView(discord.ui.View):
except:
pass
session_file_path = os.path.join(func.ROOT_DIR, func.LAST_SESSION_FILE_NAME)
if os.path.exists(session_file_path):
os.remove(session_file_path)
if os.path.exists(Config.LAST_SESSION_FILE_DIR):
os.remove(Config.LAST_SESSION_FILE_DIR)
func.update_json(func.LAST_SESSION_FILE_NAME, player_data)
with open(Config.LAST_SESSION_FILE_DIR, "w", encoding="utf8") as f:
json.dump(player_data, f, ensure_ascii=False, indent=4)
await interaction.client.close()

View File

@@ -21,12 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import discord, copy
import function as func
import discord
import copy
from typing import List
from discord.ext import commands
from ..mongodb import MongoDBHandler
from ..placeholders import PlayerPlaceholder
class Modal(discord.ui.Modal):
def __init__(self, items: List[discord.ui.Item], *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@@ -51,28 +54,27 @@ class Dropdown(discord.ui.Select):
super().__init__(placeholder='Select a embed to edit...', min_values=1, max_values=1, options=options)
async def callback(self, interaction: discord.Interaction):
self.view.embedType = self.values[0].lower()
if self.view.embedType not in self.view.data:
self.view.data[self.view.embedType] = {}
self.view.embed_type = self.values[0].lower()
if self.view.embed_type not in self.view.data:
self.view.data[self.view.embed_type] = {}
await interaction.response.edit_message(embed=self.view.build_embed())
class EmbedBuilderView(discord.ui.View):
def __init__(self, context: commands.Context, data: dict) -> None:
from voicelink import Placeholders, build_embed
def __init__(self, context: commands.Context, placeholder: PlayerPlaceholder, data: dict) -> None:
super().__init__(timeout=300)
self.add_item(Dropdown())
self.author: discord.Member = context.author
self.ph: PlayerPlaceholder = placeholder
self.response: discord.Message = None
self.original_data: dict = copy.deepcopy(data)
self.data: dict = copy.deepcopy(data)
self.embedType: str = "active"
self.ph: Placeholders = Placeholders(context.bot)
self.build_embed = lambda: build_embed(self.data.get(self.embedType, {}), self.ph)
self.embed_type: str = "active"
def build_embed(self) -> discord.Embed:
return PlayerPlaceholder.build_embed(self.data.get(self.embed_type, {}), self.ph)
async def on_timeout(self):
for child in self.children:
@@ -87,7 +89,7 @@ class EmbedBuilderView(discord.ui.View):
@discord.ui.button(label="Edit Content", style=discord.ButtonStyle.blurple)
async def edit_content(self, interaction: discord.Interaction, button: discord.ui.Button):
data = self.data.get(self.embedType, {})
data = self.data.get(self.embed_type, {})
items = [
discord.ui.TextInput(
label="Title",
@@ -144,7 +146,7 @@ class EmbedBuilderView(discord.ui.View):
@discord.ui.button(label="Edit Author",)
async def edit_author(self, interaction: discord.Interaction, button: discord.ui.Button):
data = self.data.get(self.embedType, {})
data = self.data.get(self.embed_type, {})
items = [
discord.ui.TextInput(
label="Name",
@@ -192,7 +194,7 @@ class EmbedBuilderView(discord.ui.View):
@discord.ui.button(label="Edit Image")
async def edit_image(self, interaction: discord.Interaction, button: discord.ui.Button):
data = self.data.get(self.embedType, {})
data = self.data.get(self.embed_type, {})
items = [
discord.ui.TextInput(
label="Thumbnail",
@@ -225,7 +227,7 @@ class EmbedBuilderView(discord.ui.View):
@discord.ui.button(label="Edit Footer")
async def edit_footer(self, interaction: discord.Interaction, button: discord.ui.Button):
data = self.data.get(self.embedType, {})
data = self.data.get(self.embed_type, {})
items = [
discord.ui.TextInput(
label="Text",
@@ -260,7 +262,7 @@ class EmbedBuilderView(discord.ui.View):
@discord.ui.button(label="Add Field", style=discord.ButtonStyle.green, row=1)
async def add_field(self, interaction: discord.Interaction, button: discord.ui.Button):
data = self.data.get(self.embedType)
data = self.data.get(self.embed_type)
items = [
discord.ui.TextInput(
label="Name",
@@ -310,7 +312,7 @@ class EmbedBuilderView(discord.ui.View):
)
]
data = self.data.get(self.embedType)
data = self.data.get(self.embed_type)
if "fields" not in data:
data["fields"] = []
@@ -330,7 +332,7 @@ class EmbedBuilderView(discord.ui.View):
@discord.ui.button(label="Apply", style=discord.ButtonStyle.green, row=1)
async def apply(self, interaction: discord.Interaction, button: discord.ui.Button):
await func.update_settings(
await MongoDBHandler.update_settings(
interaction.guild_id,
{"$set": {"default_controller.embeds": self.data}},
)

View File

@@ -24,7 +24,7 @@ SOFTWARE.
import discord
from discord.ext import commands
import function as func
from ..config import Config
class HelpDropdown(discord.ui.Select):
def __init__(self, categories: list[str]) -> None:
@@ -80,7 +80,7 @@ class HelpView(discord.ui.View):
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 = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=Config().embed_color)
embed.add_field(
name=f"Available Categories: [{2 + len(self.categories)}]",
value="```py\n👉 News\n2. Tutorial\n{}```".format("".join(f"{i}. {c}\n" for i, c in enumerate(self.categories, start=3))),
@@ -93,7 +93,7 @@ class HelpView(discord.ui.View):
return embed
embed = discord.Embed(title=f"Category: {category.capitalize()}", color=func.settings.embed_color)
embed = discord.Embed(title=f"Category: {category.capitalize()}", color=Config().embed_color)
embed.add_field(name=f"Categories: [{2 + len(self.categories)}]", value="```py\n" + "\n".join(("👉 " if c == category.capitalize() else f"{i}. ") + c for i, c in enumerate(['News', 'Tutorial'] + self.categories, start=1)) + "```", inline=True)
if category == 'tutorial':

View File

@@ -21,11 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import discord, time
import function as func
import discord
import time
from typing import Any
from ..config import Config
class Select_message(discord.ui.Select):
def __init__(self, inbox):
self.view: InboxView
@@ -59,7 +61,7 @@ class InboxView(discord.ui.View):
embed=discord.Embed(
title=f"📭 All {self.author.display_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
color=Config().embed_color
)
if self.current:

View File

@@ -26,4 +26,4 @@ import discord
class LinkView(discord.ui.View):
def __init__(self, label=None, emoji=None, url=None):
super().__init__(timeout=60)
self.add_item(discord.ui.Button(label=label, emoji=emoji, url=url))
self.add_item(discord.ui.Button(label=label, emoji=emoji, url=url))

81
voicelink/views/lyrics.py Normal file
View File

@@ -0,0 +1,81 @@
"""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
from .utils import Pagination
from .pagination import PaginationView
from ..config import Config
from ..language import LangHandler
# class LyricsDropdown(discord.ui.Select):
# def __init__(self, langs: list[str]) -> None:
# self.view: LyricsView
# super().__init__(
# placeholder="Select A Lyrics Translation",
# min_values=1, max_values=1,
# options=[discord.SelectOption(label=lang) for lang in langs],
# custom_id="selectLyricsLangs"
# )
# 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(PaginationView):
def __init__(self, name: str, source: dict, author: discord.Member) -> None:
self.name: str = name
self.author: discord.Member = author
self.response: discord.Message = None
super().__init__(
Pagination[str](source.get("default"), page_size=1),
author
)
async def on_timeout(self) -> None:
for child in self.children:
child.disabled = True
try:
await self.response.edit(view=self)
except:
pass
async def build_embed(self) -> discord.Embed:
page = self.pagination.get_current_page_items()
text = await LangHandler.get_lang(self.author.guild.id, "searchTitle")
embed=discord.Embed(description=page[0], color=Config().embed_color)
embed.set_author(name=text.format(self.name), icon_url=self.author.display_avatar.url)
return embed
async def update_message(self, interaction: discord.Interaction) -> None:
"""Update the view and edit the message."""
self.update_view()
if interaction.response.is_done():
await interaction.followup.edit_message(self.response.id, embed=await self.build_embed(), view=self)
else:
await interaction.response.edit_message(embed=await self.build_embed(), view=self)

View File

@@ -0,0 +1,107 @@
"""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 traceback
from .utils import Pagination, BaseModal
class PaginationView(discord.ui.View):
def __init__(self, pagination: Pagination, author: discord.Member, timeout = 300):
super().__init__(timeout=timeout)
self.pagination: Pagination = pagination
self.author: discord.Member = author
self.update_view()
def update_view(self) -> None:
"""Update button states and page number display based on current pagination state."""
button_states = {
"fast_back": self.pagination.current_page <= 2,
"back": not self.pagination.has_previous_page,
"fast_next": self.pagination.current_page >= self.pagination.total_pages - 1,
"next": not self.pagination.has_next_page,
}
for child in self.children:
if child.custom_id in button_states:
child.disabled = button_states[child.custom_id]
if child.custom_id == "page_number":
child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}"
async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None:
traceback.print_exception(error)
async def update_message(self, interaction: discord.Interaction) -> None:
"""Update the view and edit the message."""
@discord.ui.button(label='<<', custom_id="fast_back")
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Jump to the first page."""
self.pagination.go_page(0)
await self.update_message(interaction)
@discord.ui.button(label='Back', custom_id="back", style=discord.ButtonStyle.blurple)
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Go to the previous page if it exists."""
self.pagination.go_back()
await self.update_message(interaction)
@discord.ui.button(label="--/--", custom_id="page_number", style=discord.ButtonStyle.blurple)
async def page_number(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Display current page number."""
modal = BaseModal(
title="Page Number",
custom_id="page_number_modal",
items=[
discord.ui.TextInput(
label="Page Number",
custom_id="page_number",
placeholder="Enter the page number to navigate.",
default=str(self.pagination.current_page),
max_length=5,
required=True
)
]
)
await interaction.response.send_modal(modal)
await modal.wait()
page_number = modal.values.get("page_number")
if not page_number or not page_number.isdigit():
return
self.pagination.go_page(int(page_number) - 1)
await self.update_message(interaction)
@discord.ui.button(label='Next', custom_id="next", style=discord.ButtonStyle.blurple)
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Go to the next page if it exists."""
self.pagination.go_next()
await self.update_message(interaction)
@discord.ui.button(label='>>', custom_id="fast_next")
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Jump to the last page."""
self.pagination.go_page(self.pagination.total_pages - 1)
await self.update_message(interaction)

View File

@@ -22,20 +22,19 @@ SOFTWARE.
"""
import discord
import function as func
import unicodedata
from math import ceil
from tldextract import extract
from discord.ext import commands
from typing import Any, TYPE_CHECKING, reveal_type
from typing import Any
from .utils import DynamicViewManager, Pagination, BaseModal
from .utils import DynamicViewManager, Pagination
from .pagination import PaginationView
from ..config import Config
from ..utils import format_ms, truncate_string
from ..language import LangHandler
if TYPE_CHECKING:
from voicelink import Track
class Select_playlist(discord.ui.Select):
class PlaylistDropdown(discord.ui.Select):
def __init__(self, results: list[dict[str, Any]]) -> None:
self.view: PlaylistViewManager
@@ -57,63 +56,39 @@ class Select_playlist(discord.ui.Select):
await interaction.response.edit_message(embed=await view.build_embed(), view=view)
class PlaylistView(discord.ui.View):
class PlaylistView(PaginationView):
def __init__(
self,
primary_view: "PlaylistViewManager",
playlist_data: dict[str, Any]
) -> None:
super().__init__(timeout=180)
self.primary_view: PlaylistViewManager = primary_view
self.author: discord.Member = primary_view.ctx.author
self.id: str = playlist_data.get("id")
self.playlist_id: str = playlist_data.get("id")
self.emoji: str = playlist_data.get("emoji")
self.name: str = playlist_data.get("name")
self.time: str = playlist_data.get("time")
self.type: str = playlist_data.get("type")
self.owner_id: int = playlist_data.get("owner")
self.perms: dict[str, list[int]] = playlist_data.get("perms")
self.pagination: Pagination = Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7)
self.update_view()
def update_view(self) -> None:
"""Update button states and page number display based on current pagination state."""
button_states = {
"fast_back": self.pagination.current_page <= 2,
"back": not self.pagination.has_previous_page,
"fast_next": self.pagination.current_page >= self.pagination.total_pages - 1,
"next": not self.pagination.has_next_page,
}
for child in self.children:
if child.custom_id in button_states:
child.disabled = button_states[child.custom_id]
if child.custom_id == "page_number":
child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}"
async def interaction_check(self, interaction: discord.Interaction) -> bool:
return interaction.user == self.author
async def on_error(self, error, item, interaction) -> None:
return
super().__init__(Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7), primary_view.ctx.author)
async def build_embed(self) -> discord.Embed:
"""Build the embed for the current page of tracks."""
tracks = self.pagination.get_current_page_items()
texts = await func.get_lang(
texts = await LangHandler.get_lang(
self.author.guild.id,
"playlistView", "playlistViewDesc", "settingsPermTitle",
"playlistViewPermsValue", "playlistViewPermsValue2",
"playlistViewTrack", "playlistNoTrack", "playlistViewFooter"
)
embed = discord.Embed(title=texts[0], color=func.settings.embed_color)
embed = discord.Embed(title=texts[0], color=Config().embed_color)
embed.description = texts[1].format(
self.name,
self.id,
self.playlist_id,
self.pagination.total_items,
self.primary_view.ctx.bot.get_user(self.owner_id),
self.type.upper()
@@ -133,11 +108,11 @@ class PlaylistView(discord.ui.View):
if tracks:
for index, track in enumerate(tracks, start=self.pagination.start_index + 1):
if self.type == "playlist":
source_emoji = func.get_source(track['sourceName'], 'emoji')
track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track['length'])}]` [{func.truncate_string(track['title'])}]({track['uri']})"
source_emoji = Config().get_source_config(track['sourceName'], 'emoji')
track_info = f"{source_emoji} `{index:>2}.` `[{format_ms(track['length'])}]` [{truncate_string(track['title'])}]({track['uri']})"
else:
source_emoji = func.get_source(extract(track.info['uri']).domain, 'emoji')
track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track.length)}]` [{func.truncate_string(track.title)}]({track.uri})"
source_emoji = Config().get_source_config(extract(track.info['uri']).domain, 'emoji')
track_info = f"{source_emoji} `{index:>2}.` `[{format_ms(track.length)}]` [{truncate_string(track.title)}]({track.uri})"
embed.description += track_info + "\n"
else:
embed.description += texts[6].format(self.name)
@@ -146,15 +121,7 @@ class PlaylistView(discord.ui.View):
embed.set_footer(text=texts[7].format(self.time))
return embed
async def on_timeout(self) -> None:
for child in self.children:
child.disabled = True
try:
await self.primary_view.response.edit(view=self)
except:
pass
async def update_and_edit_message(self, interaction: discord.Interaction) -> None:
async def update_message(self, interaction: discord.Interaction) -> None:
"""Update the view and edit the message with the new embed."""
self.update_view()
@@ -162,62 +129,7 @@ class PlaylistView(discord.ui.View):
await interaction.followup.edit_message(self.primary_view.response.id, embed=await self.build_embed(), view=self)
else:
await interaction.response.edit_message(embed=await self.build_embed(), view=self)
async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None:
"""Handle errors that occur during interaction."""
func.logger.error(f"Error in PlaylistView: {error}", exc_info=error)
@discord.ui.button(label='<<', custom_id="fast_back")
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Jump to the first page."""
self.pagination.go_page(0)
await self.update_and_edit_message(interaction)
@discord.ui.button(label='Back', custom_id="back", style=discord.ButtonStyle.blurple)
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Go to the previous page if it exists."""
self.pagination.go_back()
await self.update_and_edit_message(interaction)
@discord.ui.button(label="--/--", custom_id="page_number", style=discord.ButtonStyle.blurple)
async def page_number(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Display current page number."""
modal = BaseModal(
title="Page Number",
custom_id="page_number_modal",
items=[
discord.ui.TextInput(
label="Page Number",
custom_id="page_number",
placeholder="Enter the page number to navigate.",
default=str(self.pagination.current_page),
max_length=5,
required=True
)
]
)
await interaction.response.send_modal(modal)
await modal.wait()
page_number = modal.values.get("page_number")
if not page_number or not page_number.isdigit():
return
self.pagination.go_page(int(page_number) - 1)
await self.update_and_edit_message(interaction)
@discord.ui.button(label='Next', custom_id="next", style=discord.ButtonStyle.blurple)
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Go to the next page if it exists."""
self.pagination.go_next()
await self.update_and_edit_message(interaction)
@discord.ui.button(label='>>', custom_id="fast_next")
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Jump to the last page."""
self.pagination.go_page(self.pagination.total_pages - 1)
await self.update_and_edit_message(interaction)
@discord.ui.button(label="<")
async def back_to_home(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
"""Return to the main playlist view."""
@@ -229,7 +141,6 @@ class PlaylistView(discord.ui.View):
await interaction.response.defer()
cmd = interaction.client.get_command("playlist play")
await cmd(self.primary_view.ctx, self.name)
# Need to handle error
@discord.ui.button(label="Share", style=discord.ButtonStyle.blurple)
async def share(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None:
@@ -246,11 +157,18 @@ class PlaylistView(discord.ui.View):
await interaction.response.defer()
cmd = interaction.client.get_command("playlist delete")
await cmd(self.primary_view.ctx, name=self.name)
if self.playlist_id != "200":
view: PlaylistViewManager = self.primary_view.change_view("home")
view.results = [item for item in view.results if item.get("id") != self.playlist_id]
view.clear_items()
view.add_item(PlaylistDropdown(view.results))
self.primary_view.remove_view(self.playlist_id)
await view.response.edit(embed=await view.build_embed(), view=view)
class PlaylistViewManager(DynamicViewManager):
def __init__(self, ctx: commands.Context, results: list[dict[str, Any]]):
self.ctx: commands.Context = ctx
self.ctx: commands.Context[commands.Bot] = ctx
self.results: list[dict[str, Any]] = results
views = {"home": self}
@@ -259,10 +177,9 @@ class PlaylistViewManager(DynamicViewManager):
super().__init__(views=views, timeout=None)
self.response: discord.Message = None
self.add_item(Select_playlist(results))
self.add_item(PlaylistDropdown(results))
def get_width(self, s):
import unicodedata
width = 0
for char in str(s):
if unicodedata.east_asian_width(char) in ('F', 'W'):
@@ -276,6 +193,18 @@ class PlaylistViewManager(DynamicViewManager):
current_width = self.get_width(s)
padding = width - current_width
return s + " " * padding
async def on_timeout(self) -> None:
for view in self._views.values():
view.stop()
for child in self.current_view.children:
child.disabled = True
try:
await self.response.edit(view=self)
except:
pass
async def build_embed(self) -> discord.Embed:
"""
@@ -284,8 +213,8 @@ class PlaylistViewManager(DynamicViewManager):
Returns:
discord.Embed: The constructed embed with playlist details.
"""
_, max_p, _ = func.check_roles()
text = await func.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter")
max_p, _, _ = Config().get_playlist_config()
text = await LangHandler.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter")
headers = text[1].split(",")
headers.insert(0, "")
@@ -299,7 +228,7 @@ class PlaylistViewManager(DynamicViewManager):
info.get('emoji', ' '),
info.get('id', "-" * 3),
f"[{info.get('time', '--:--')}]",
info.get('name', "-" * 6),
truncate_string(info.get('name', "-" * 6), 12),
len(info.get('tracks', []))
])
@@ -311,7 +240,7 @@ class PlaylistViewManager(DynamicViewManager):
embed = discord.Embed(
description=f'```{description}```',
color=func.settings.embed_color
color=Config().embed_color
)
embed.set_author(

132
voicelink/views/queue.py Normal file
View File

@@ -0,0 +1,132 @@
"""MIT License
Copyright (c) 2023 - present Vocard Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from __future__ import annotations
import discord
from typing import TYPE_CHECKING
from .utils import Pagination, BaseModal
from .pagination import PaginationView
from ..language import LangHandler
from ..config import Config
from ..utils import format_ms, truncate_string
if TYPE_CHECKING:
from ..player import Player
from ..objects import Track
class QueueView(PaginationView):
"""
A Discord UI view for displaying and interacting with a paginated list of tracks.
Attributes:
player (Player): The player containing the track queue or history.
author (discord.Member): The member who initiated the view.
is_queue (bool): Indicates if the list is a queue or history.
pagination (Pagination[Track]): Manages pagination of track lists.
response (discord.Message): The message containing the view.
total_duration (str): The total duration of the tracks.
"""
def __init__(self, player: "Player", author: discord.Member, is_queue: bool = True) -> None:
self.player: Player = player
self.is_queue: bool = is_queue
self.total_duration = self.calculate_total_duration()
self.response: discord.Message = None
super().__init__(
Pagination[Track](
items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())),
page_size=7,
),
author
)
def calculate_total_duration(self) -> str:
"""Calculate the total duration of the tracks."""
try:
return format_ms(sum(track.length for track in self.pagination._items))
except Exception:
return ""
def format_description(self, tracks: list["Track"], texts: list[str]) -> str:
"""Format the description for the embed based on current tracks."""
now_playing = (
texts[1].format(self.player.current.uri,
f"```{self.player.current.title}```")
if self.player.current else texts[2].format("None")
)
track_list = "\n".join([
f"{track.emoji} `{i:>2}.` `[{texts[3] if track.is_stream else format_ms(track.length)}]` "
f"[{truncate_string(track.title)}]({track.uri}) {track.requester.mention}"
for i, track in enumerate(tracks, start=self.pagination.start_index + 1)
])
return f"{now_playing}\n**{texts[4] if self.is_queue else texts[5]}**\n{track_list}"
async def on_timeout(self) -> None:
"""Disable all buttons when the view times out."""
for child in self.children:
child.disabled = True
try:
await self.response.edit(view=self)
except discord.HTTPException:
pass
async def build_embed(self) -> discord.Embed:
"""Build the embed for the current page of tracks."""
tracks = self.pagination.get_current_page_items()
texts = await LangHandler.get_lang(
self.author.guild.id,
"viewTitle",
"viewDesc",
"nowplayingDesc",
"live",
"queueTitle",
"historyTitle",
"playlistViewFooter",
)
embed = discord.Embed(title=texts[0], color=Config().embed_color)
embed.description = self.format_description(tracks, texts)
if self.player.current:
embed.set_thumbnail(url=self.player.current.thumbnail)
embed.set_footer(text=texts[6].format(self.total_duration))
return embed
async def update_message(self, interaction: discord.Interaction) -> None:
"""Update the view and edit the message with the new embed."""
super().update_view()
if interaction.response.is_done():
await interaction.followup.edit_message(self.response.id, embed=await self.build_embed(), view=self)
else:
await interaction.response.edit_message(embed=await self.build_embed(), view=self)

View File

@@ -20,13 +20,10 @@ 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 typing import TYPE_CHECKING
if TYPE_CHECKING:
from voicelink import Track
from ..objects import Track
class SearchDropdown(discord.ui.Select):
def __init__(self, tracks: list[Track], texts: list[str]) -> None:

View File

@@ -32,10 +32,6 @@ class DynamicViewManager(discord.ui.View):
self._views: dict[str, discord.ui.View] = views
self._current_view: discord.ui.View | None = None
def current_view(self) -> discord.ui.View | None:
"""Get the current active view."""
return self._current_view
def add_view(self, name: str, view: discord.ui.View) -> None:
"""
Add a view to the manager.
@@ -87,3 +83,8 @@ class DynamicViewManager(discord.ui.View):
self._current_view = view
return view
@property
def current_view(self) -> discord.ui.View | None:
"""Get the current active view."""
return self._current_view