Migrate scripts to voicelink, rewrite handlers and views
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
from .lyrics import LYRICS_PLATFORMS
|
||||
from .placeholders import Placeholders
|
||||
from .settings import Settings
|
||||
@@ -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
|
||||
@@ -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", "")
|
||||
315
cogs/basic.py
315
cogs/basic.py
@@ -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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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, _):
|
||||
|
||||
185
cogs/playlist.py
185
cogs/playlist.py
@@ -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)
|
||||
|
||||
133
cogs/settings.py
133
cogs/settings.py
@@ -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(
|
||||
|
||||
10
cogs/task.py
10
cogs/task.py
@@ -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))
|
||||
283
function.py
283
function.py
@@ -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, [])
|
||||
@@ -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:
|
||||
|
||||
@@ -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 couldn’t be found. It’s 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),
|
||||
|
||||
171
langs/CH.json
171
langs/CH.json
@@ -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 開始要求任何歌曲,而無需使用機器人前綴。"
|
||||
}
|
||||
@@ -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}` 已暂停播放器。",
|
||||
|
||||
@@ -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}` 已暫停播放器。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.": "为您的播放列表命名。",
|
||||
|
||||
@@ -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
100
main.py
@@ -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)
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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)
|
||||
|
||||
110
views/lyrics.py
110
views/lyrics.py
@@ -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()
|
||||
199
views/queue.py
199
views/queue.py
@@ -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)
|
||||
@@ -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
134
voicelink/config.py
Normal 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
133
voicelink/language.py
Normal 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()
|
||||
@@ -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
514
voicelink/mongodb.py
Normal 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)}")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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}},
|
||||
)
|
||||
@@ -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':
|
||||
@@ -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:
|
||||
@@ -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
81
voicelink/views/lyrics.py
Normal 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)
|
||||
107
voicelink/views/pagination.py
Normal file
107
voicelink/views/pagination.py
Normal 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)
|
||||
@@ -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
132
voicelink/views/queue.py
Normal 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)
|
||||
@@ -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:
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user