From 59d864a9dc31eb999d1f0558ef1d88656eee1612 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 17 Mar 2023 12:34:57 +0800
Subject: [PATCH 01/40] Update requirements.txt
---
requirements.txt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 04ddf2b..30a25d3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-discord.py==2.1.0
+discord.py==2.2.2
pymongo==4.1.1
dnspython==2.2.1
tldextract==3.2.1
@@ -6,3 +6,6 @@ validators==0.18.2
humanize==4.0.0
python-dotenv==0.20.0
beautifulsoup4==4.11.1
+websockets==10.4
+Flask==2.2.3
+Flask-SocketIO==5.3.2
From 1e0a8314cbd63abcb8df46274c2c96cd209b80c3 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 17 Mar 2023 12:37:35 +0800
Subject: [PATCH 02/40] Add IPC
---
main.py | 48 +++++++-----
update.py | 59 --------------
web/__init__.py | 2 +
web/ipc/__init__.py | 2 +
web/ipc/client.py | 41 ++++++++++
web/ipc/methods.py | 183 ++++++++++++++++++++++++++++++++++++++++++++
web/ipc/server.py | 67 ++++++++++++++++
7 files changed, 323 insertions(+), 79 deletions(-)
delete mode 100644 update.py
create mode 100644 web/__init__.py
create mode 100644 web/ipc/__init__.py
create mode 100644 web/ipc/client.py
create mode 100644 web/ipc/methods.py
create mode 100644 web/ipc/server.py
diff --git a/main.py b/main.py
index 9868d78..cc560ae 100644
--- a/main.py
+++ b/main.py
@@ -1,9 +1,12 @@
import discord
-import sys, os, traceback, aiohttp
-import update
+import sys
+import os
+import traceback
+import aiohttp
import function as func
from discord.ext import commands
+from web import IPCServer
from dotenv import load_dotenv
from datetime import datetime
from voicelink import VoicelinkException
@@ -14,26 +17,30 @@ func.settings_setup()
class Translator(discord.app_commands.Translator):
async def load(self):
print("Loaded Translator")
-
+
async def unload(self):
print("Unload Translator")
-
+
async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext):
if str(locale) in func.local_langs:
return func.local_langs[str(locale)].get(string.message, None)
return None
class Vocard(commands.Bot):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.ipc = IPCServer(self, sercet_key="Vocard")
async def on_message(self, message: discord.Message, /) -> None:
if message.author.bot or not message.guild:
return False
-
+
if self.user.mentioned_in(message) and not message.mention_everyone:
await message.channel.send(f"My prefix is `{await self.command_prefix(self, message)}`")
await self.process_commands(message)
-
+
async def setup_hook(self):
func.langs_setup()
for module in os.listdir(func.root_dir + '/cogs'):
@@ -44,6 +51,8 @@ class Vocard(commands.Bot):
except Exception as e:
print(traceback.format_exc())
+ await self.ipc.start()
+
await bot.tree.set_translator(Translator())
await bot.tree.sync()
@@ -60,7 +69,7 @@ class Vocard(commands.Bot):
error = getattr(exception, 'original', exception)
if isinstance(error, (commands.CommandNotFound, aiohttp.client_exceptions.ClientOSError)):
return
-
+
elif isinstance(error, (commands.CommandOnCooldown, commands.MissingPermissions, commands.RangeError, commands.BadArgument)):
pass
@@ -68,25 +77,25 @@ class Vocard(commands.Bot):
command = f" Correct Usage: {ctx.prefix}" + (f"{ctx.command.parent.qualified_name} " if ctx.command.parent else "") + f"{ctx.command.name} {ctx.command.signature}"
position = command.find(f"<{ctx.current_parameter.name}>") + 1
error = f"```css\n[You are missing argument!]\n{command}\n" + " " * position + "^" * len(ctx.current_parameter.name) + "```"
-
+
elif not issubclass(error.__class__, VoicelinkException):
- error = function.get_lang(ctx.guild.id, "unknownException") + function.invite_link
- if (guildId := ctx.guild.id) not in function.error_log:
- function.error_log[guildId] = {}
- function.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
-
+ error = func.get_lang(ctx.guild.id, "unknownException") + func.invite_link
+ if (guildId := ctx.guild.id) not in func.error_log:
+ func.error_log[guildId] = {}
+ func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
+
try:
return await ctx.reply(error, ephemeral=True)
except:
pass
class CommandCheck(discord.app_commands.CommandTree):
-
+
async def interaction_check(self, interaction: discord.Interaction, /) -> bool:
if not interaction.guild:
await interaction.response.send_message("This command can only be used in guilds!")
return False
-
+
return await super().interaction_check(interaction)
async def get_prefix(bot, message: discord.Message):
@@ -118,10 +127,10 @@ async def app_command_error(interaction: discord.Interaction, error):
elif isinstance(error, (discord.app_commands.CommandOnCooldown, discord.app_commands.errors.MissingPermissions)):
pass
elif not issubclass(error.__class__, VoicelinkException):
- error = function.get_lang(interaction.guild_id, "unknownException") + function.invite_link
- if (guildId := interaction.guild_id) not in function.error_log:
- function.error_log[guildId] = {}
- function.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
+ error = func.get_lang(interaction.guild_id, "unknownException") + func.invite_link
+ if (guildId := interaction.guild_id) not in func.error_log:
+ func.error_log[guildId] = {}
+ func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
try:
if interaction.response.is_done():
return await interaction.followup.send(error, ephemeral=True)
@@ -131,5 +140,4 @@ async def app_command_error(interaction: discord.Interaction, error):
pass
if __name__ == "__main__":
- update.checkVersion(withMsg=True)
bot.run(os.getenv("TOKEN"), log_handler=None)
\ No newline at end of file
diff --git a/update.py b/update.py
deleted file mode 100644
index 997aad2..0000000
--- a/update.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import requests, zipfile, os, sys, shutil, traceback
-from io import BytesIO
-
-root_dir = os.path.dirname(os.path.abspath(__file__))
-install_pack_dir = os.path.join(root_dir, "Vocard.zip")
-__version__ = "v2.5.7"
-
-def checkVersion(withMsg = False):
- resp = requests.get("https://api.github.com/repos/ChocoMeow/Vocard/releases/latest")
- version = resp.json().get("name", __version__)
- if withMsg:
- if version == __version__:
- print(f"Your bot is up-to-date! - {version}")
- else:
- print(f"Your bot is not up-to-date! This latest version is {version} and you are currently running version {__version__}\n. Run `python update.py --start` to update your bot!")
- return version
-
-def downloadFile(version:str = None):
- if not version:
- version = checkVersion()
- print("Downloading Vocard version: " + version)
- response = requests.get("https://github.com/ChocoMeow/Vocard/archive/" + version + ".zip")
- print("Download Completed")
- unZip(response, version)
-
-def unZip(response, version: str):
- print("Installing ...")
- zfile = zipfile.ZipFile(BytesIO(response.content))
- zfile.extractall(root_dir)
-
- version = version.replace("v", "")
- source_dir = os.path.join(root_dir, f"Vocard-{version}")
- if os.path.exists(source_dir):
- for filename in os.listdir(root_dir):
- if filename in ["settings.json", ".env", f"Vocard-{version}"]:
- continue
- filename = os.path.join(root_dir, filename)
- if os.path.isdir(filename):
- shutil.rmtree(filename)
- else:
- os.remove(filename)
- for filename in os.listdir(source_dir):
- shutil.move(os.path.join(source_dir, filename), os.path.join(root_dir, filename))
- os.rmdir(source_dir)
-
-def start():
- try:
- downloadFile()
- if os.path.exists(install_pack_dir):
- os.remove(install_pack_dir)
- print("Update Successfully! Run `python main.py` to start your bot")
- except Exception as e:
- print(traceback.format_exc())
-
-if "--start" in sys.argv:
- start()
-
-if "--check" in sys.argv:
- checkVersion(withMsg = True)
\ No newline at end of file
diff --git a/web/__init__.py b/web/__init__.py
new file mode 100644
index 0000000..4bafd1f
--- /dev/null
+++ b/web/__init__.py
@@ -0,0 +1,2 @@
+from .ipc import *
+from .objects import *
\ No newline at end of file
diff --git a/web/ipc/__init__.py b/web/ipc/__init__.py
new file mode 100644
index 0000000..8d6d350
--- /dev/null
+++ b/web/ipc/__init__.py
@@ -0,0 +1,2 @@
+from .server import IPCServer
+from .client import IPCClient
\ No newline at end of file
diff --git a/web/ipc/client.py b/web/ipc/client.py
new file mode 100644
index 0000000..96bcc14
--- /dev/null
+++ b/web/ipc/client.py
@@ -0,0 +1,41 @@
+import websockets, json
+from uuid import uuid4
+
+class IPCClient:
+ def __init__(self,
+ host = "127.0.0.1",
+ port = 8000,
+ secret_key = None,
+ callback = None):
+
+ self.host = host
+ self.port = port
+ self.websocket = None
+ self.secret_key = secret_key
+ self.callback = callback
+ self.id = uuid4()
+
+ async def connect(self):
+ self.websocket = await websockets.connect(f"ws://{self.host}:{self.port}", extra_headers={"Client-Id": str(self.id)})
+ await self.start_receiver()
+
+ async def send(self, message, user):
+ if not self.websocket:
+ await self.connect()
+ data = json.loads(message)
+
+ payload = {
+ "sercet": self.secret_key,
+ "data": data | {"user_id": user.id, "guild_id": user.guild_id}
+ }
+
+ await self.websocket.send(json.dumps(payload))
+
+ async def receive(self):
+ async for message in self.websocket:
+ if self.callback:
+ data = json.loads(message)
+ self.callback(data)
+
+ async def start_receiver(self):
+ await self.receive()
\ No newline at end of file
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
new file mode 100644
index 0000000..c8c7026
--- /dev/null
+++ b/web/ipc/methods.py
@@ -0,0 +1,183 @@
+import json
+import voicelink
+
+from discord import Member
+from discord.ext import commands
+
+def missingPermission(user_id:int):
+ payload = {"op": "missingPermission", "msg": "Only the DJ or admins may use this funciton!"}
+ payload["user_id"] = user_id
+ return payload
+
+async def initPlayer(player, member: Member, data: dict):
+ return {
+ "op": "initPlayer",
+ "guild_id": player.guild.id,
+ "user_id": data.get("user_id"),
+ "users": [{
+ "user_id": member.id,
+ "avatar_url": member.avatar.url,
+ "name": member.name
+ } for member in player.channel.members ],
+ "tracks": [ track.toDict() for track in player.queue._queue ],
+ "current_queue_position": player.queue._position if player.is_playing else player.queue._position + 1,
+ "current_position": 0 or player.position if player.is_playing else 0,
+ "is_playing": player.is_playing,
+ "is_paused": player.is_paused
+ }
+
+async def skipTo(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ index = data.get("index", 1)
+ if index > 1:
+ player.queue.skipto(index)
+
+ if player.queue._repeat == 1:
+ player.queue.set_repeat("off")
+ await player.stop()
+
+async def backTo(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ index = data.get("index", 1)
+ if not player.is_playing:
+ player.queue.backto(index)
+ await player.do_next()
+ else:
+ player.queue.backto(index + 1)
+ await player.stop()
+
+async def moveTrack(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ c = player.queue._position - 1
+ position = data.get("position")
+ new_position = data.get("newPosition")
+
+ moveItem = player.queue._queue[position]
+ player.queue._queue.remove(moveItem)
+ player.queue._queue.insert(new_position, moveItem)
+ return {
+ "op": "moveTrack",
+ "position": {
+ "index": position - c,
+ "track_id": moveItem.track_id
+ },
+ "newPosition": {
+ "index": new_position - c
+ },
+ "guild_id": player.guild.id,
+ "requester_id": member.id,
+ "skip_users": [member.id]
+ }
+
+async def addTracks(player, member: Member, data: dict):
+ raw_tracks = data.get("tracks", [])
+ tracks = [voicelink.Track(
+ track_id=track["track_id"],
+ info=track["info"],
+ requester=member
+ ) for track in raw_tracks]
+
+ await player.add_track(tracks)
+
+ if not player.is_playing:
+ await player.do_next()
+
+async def getTracks(player, member: Member, data: dict):
+ query = data.get("query", None)
+ if query:
+ payload = {"op": "getTracks", "user_id": member.id}
+ tracks = await player.get_tracks(query, requester=member)
+ if not tracks:
+ return payload
+
+ payload["tracks"] = [ track.toDict() for track in tracks ]
+ return payload
+
+async def shuffleTrack(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ await player.shuffle(data.get("type", "queue"), member)
+
+async def updateRepeatMode(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ mode = data.get("mode", "off")
+ if mode.lower() not in ['off', 'track', 'queue']:
+ mode = "off"
+ player.queue.set_repeat(mode)
+
+async def updatePause(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ pause = data.get("pause", True)
+ if pause:
+ player.pause_votes.clear()
+ else:
+ player.resume_votes.clear()
+ await player.set_pause(pause, member)
+
+async def updatePosition(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ position = data.get("position");
+ await player.seek(position, member);
+
+methods = {
+ "initPlayer": initPlayer,
+ "skipTo": skipTo,
+ "backTo": backTo,
+ "moveTrack": moveTrack,
+ "addTracks": addTracks,
+ "getTracks": getTracks,
+ "shuffleTrack": shuffleTrack,
+ "updateRepeat": updateRepeatMode,
+ "updatePause": updatePause,
+ "updatePosition": updatePosition,
+}
+
+async def process_methods(websocket, bot: commands.Bot, data: dict) -> None:
+ method = methods.get(data.get("op", ""), None)
+ if not method:
+ return
+
+ guild = None
+ member = None
+ guild_id = data.get("guild_id", None)
+ user_id = data.get("user_id", None)
+ if guild_id is None:
+ user = bot.get_user(user_id)
+ if not user:
+ return
+
+ for g in user.mutual_guilds:
+ m = g.get_member(user.id)
+ if m.voice and m.voice.channel:
+ guild = g
+ member = m
+
+ else:
+ guild = bot.get_guild(guild_id)
+ member = guild.get_member(user_id)
+
+ if not guild:
+ return
+
+ player = guild.voice_client
+ if not player or not Member:
+ return
+ try:
+ resp: dict = await method(player, member, data)
+ if resp:
+ await websocket.send(json.dumps(resp))
+ except:
+ return
\ No newline at end of file
diff --git a/web/ipc/server.py b/web/ipc/server.py
new file mode 100644
index 0000000..ad1efdf
--- /dev/null
+++ b/web/ipc/server.py
@@ -0,0 +1,67 @@
+import json
+
+from websockets.server import serve, WebSocketServerProtocol
+from websockets.exceptions import ConnectionClosed
+from discord.ext import commands
+
+from typing import (
+ Optional,
+ Dict,
+ Any
+)
+
+class IPCServer:
+ def __init__(
+ self,
+ bot: commands.Bot,
+ host: str = "127.0.0.1",
+ port: int = 8000,
+ sercet_key: Optional[str] = None
+ ):
+ self.bot = bot
+ self.host = host
+ self.port = port
+ self.sercet_key = sercet_key
+
+ self.user = {}
+ self.connections = set()
+
+ def is_secure(self, data: dict) -> bool:
+ if (key := data.get("sercet")):
+ return str(key) == str(self.sercet_key)
+ return bool(self.sercet_key is None)
+
+ async def start(self):
+ try:
+ print("Starting IPC")
+ await serve(self.handle_ipc_connection, self.host, self.port)
+ except Exception as e:
+ print(e)
+
+ async def handle_ipc_connection(self, websocket: WebSocketServerProtocol):
+ from .methods import process_methods
+
+ client_id = websocket.request_headers.get("Client-ID", None)
+ if not client_id:
+ return
+
+ self.connections.add(websocket)
+ try:
+ async for message in websocket:
+ data: Dict[str, Any] = json.loads(message)
+ if self.is_secure(data):
+ action = data.get("data")
+ await process_methods(websocket, self.bot, action)
+
+ except ConnectionClosed:
+ pass
+
+ self.connections.remove(websocket)
+
+ async def send(self, payload):
+ for conn in self.connections:
+ try:
+ await conn.send(json.dumps(payload))
+ except:
+ await conn.close()
+ self.connections.remove(conn)
\ No newline at end of file
From c4197bdd868d21957e181282b44b442bc668efea Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 17 Mar 2023 12:38:57 +0800
Subject: [PATCH 03/40] Add message handler
---
cogs/admin.py | 2 +-
cogs/basic.py | 18 +++++++++++----
cogs/listener.py | 27 ++++++++++++++++++++++
function.py | 1 +
views/controller.py | 10 ++++----
voicelink/objects.py | 7 ++++++
voicelink/player.py | 55 ++++++++++++++++++++++++++++++++++++++------
7 files changed, 103 insertions(+), 17 deletions(-)
diff --git a/cogs/admin.py b/cogs/admin.py
index 0bee764..3067c47 100644
--- a/cogs/admin.py
+++ b/cogs/admin.py
@@ -179,7 +179,7 @@ class Admin(commands.Cog, name="settings"):
player: voicelink.Player = ctx.guild.voice_client
if player:
player.settings['volume'] = value
- await player.set_volume(value)
+ await player.set_volume(value, ctx.author)
update_settings(ctx.guild.id, {'volume': value})
await ctx.send(get_lang(ctx.guild.id, 'setVolume').format(value))
diff --git a/cogs/basic.py b/cogs/basic.py
index 1c67752..80c0cb3 100644
--- a/cogs/basic.py
+++ b/cogs/basic.py
@@ -256,7 +256,7 @@ class Basic(commands.Cog):
else:
return await ctx.send(player.get_msg('pauseVote').format(ctx.author, len(player.pause_votes), required))
- await player.set_pause(True)
+ await player.set_pause(True, ctx.author)
player.pause_votes.clear()
await ctx.send(player.get_msg('paused').format(ctx.author))
@@ -281,7 +281,7 @@ class Basic(commands.Cog):
else:
return await ctx.send(player.get_msg('resumeVote').format(ctx.author, len(player.resume_votes), required))
- await player.set_pause(False)
+ await player.set_pause(False, ctx.author)
player.resume_votes.clear()
await ctx.send(player.get_msg('resumed').format(ctx.author))
@@ -374,7 +374,7 @@ class Basic(commands.Cog):
if num is None:
return await ctx.send(player.get_msg('timeFormatError'), ephemeral=True)
- await player.seek(num)
+ await player.seek(num, ctx.author)
await ctx.send(player.get_msg('seek').format(position))
@commands.hybrid_command(name="queue", aliases=get_aliases("queue"))
@@ -591,7 +591,7 @@ class Basic(commands.Cog):
else:
return await ctx.send(player.get_msg('shuffleVote').format(ctx.author, len(player.skip_votes), required))
- await player.shuffle("queue")
+ await player.shuffle("queue", ctx.author)
await ctx.send(player.get_msg('shuffled'))
@commands.hybrid_command(name="swap", aliases=get_aliases("swap"))
@@ -610,6 +610,11 @@ class Basic(commands.Cog):
return await ctx.send(player.get_msg('missingPerms_pos'), ephemeral=True)
track1, track2 = player.queue.swap(position1, position2)
+ await player.send_ws({
+ "op": "swapTrack",
+ "position1": {"index": position1, "track_id": track1.track_id},
+ "position2": {"index": position2, "track_id": track2.track_id}
+ }, requester=ctx.author)
await ctx.send(player.get_msg('swapped').format(track1.title, track2.title))
@commands.hybrid_command(name="move", aliases=get_aliases("move"))
@@ -628,6 +633,11 @@ class Basic(commands.Cog):
return await ctx.send(player.get_msg('missingPerms_pos'), ephemeral=True)
moved_track = player.queue.move(target, to)
+ await player.send_ws({
+ "op": "moveTrack",
+ "position": {"index": target, "track_id": moved_track.track_id},
+ "newPosition": {"index": to}
+ }, requester=ctx.author)
await ctx.send(player.get_msg('moved').format(moved_track.title, to))
@commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics"))
diff --git a/cogs/listener.py b/cogs/listener.py
index d33a04a..43c33f5 100644
--- a/cogs/listener.py
+++ b/cogs/listener.py
@@ -46,5 +46,32 @@ class Nodes(commands.Cog):
await asyncio.sleep(5)
await player.do_next()
+ @commands.Cog.listener()
+ async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState):
+ if member.bot:
+ return
+
+ is_joined = True
+ if before.channel and after.channel:
+ guild = after.channel.guild.id
+
+ elif not before.channel and after.channel:
+ guild = after.channel.guild.id
+
+ elif before.channel and not after.channel:
+ guild = before.channel.guild.id
+ is_joined = False
+
+ await self.bot.ipc.send({
+ "op": "updateGuild",
+ "user": {
+ "user_id": member.id,
+ "avatar_url": member.avatar.url,
+ "name": member.name,
+ },
+ "guild_id": guild,
+ "is_joined": is_joined
+ })
+
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Nodes(bot))
diff --git a/function.py b/function.py
index 2e88a2c..d2713ea 100644
--- a/function.py
+++ b/function.py
@@ -233,6 +233,7 @@ async def connect_channel(ctx: commands.Context, channel: discord.VoiceChannel =
get_lang(ctx.guild.id, 'noPermission'))
player: voicelink.Player = await channel.connect(cls=voicelink.Player(ctx.bot, channel, ctx))
+ await player.send_ws({"op": "createPlayer", "members_id": [member.id for member in channel.members]})
return player
def time(millis:int) -> str:
diff --git a/views/controller.py b/views/controller.py
index 6c28b19..ff30cb7 100644
--- a/views/controller.py
+++ b/views/controller.py
@@ -85,7 +85,7 @@ class Resume(discord.ui.Button):
self.player.resume_votes.clear()
self.emoji = "⏸️"
self.label = self.player.get_msg("buttonPause")
- await self.player.set_pause(False)
+ await self.player.set_pause(False, interaction.user)
else:
if not self.player.is_privileged(interaction.user):
@@ -101,7 +101,7 @@ class Resume(discord.ui.Button):
self.player.pause_votes.clear()
self.emoji = "▶️"
self.label = self.player.get_msg("buttonResume")
- await self.player.set_pause(True)
+ await self.player.set_pause(True, interaction.user)
await interaction.response.edit_message(view=self.view)
class Skip(discord.ui.Button):
@@ -208,7 +208,7 @@ class VolumeUp(discord.ui.Button):
return interaction.response.send_message(self.player.get_msg("missingPerms_function"))
value = value if (value := self.player.volume + 20) <= 150 else 150
- await self.player.set_volume(value)
+ await self.player.set_volume(value, interaction.user)
await interaction.response.send_message(self.player.get_msg('setVolume').format(value), ephemeral=True)
@@ -222,7 +222,7 @@ class VolumeDown(discord.ui.Button):
return interaction.response.send_message(self.player.get_msg("missingPerms_function"))
value = value if (value := self.player.volume - 20) >= 0 else 150
- await self.player.set_volume(value)
+ await self.player.set_volume(value, interaction.user)
await interaction.response.send_message(self.player.get_msg('setVolume').format(value), ephemeral=True)
@@ -246,7 +246,7 @@ class VolumeMute(discord.ui.Button):
self.emoji = "🔇"
self.label = self.player.get_msg("buttonVolumeMute")
- await self.player.set_volume(value)
+ await self.player.set_volume(value, interaction.user)
await interaction.response.edit_message(view=self.view)
diff --git a/voicelink/objects.py b/voicelink/objects.py
index 12aada4..100a741 100644
--- a/voicelink/objects.py
+++ b/voicelink/objects.py
@@ -83,6 +83,13 @@ class Track:
self.is_seekable = info.get("isSeekable", True)
self.position = info.get("position", 0)
+ def toDict(self):
+ return {
+ "track_id": self.track_id,
+ "info": self.info,
+ "thumbnail": self.thumbnail
+ }
+
def __eq__(self, other):
if not isinstance(other, Track):
return False
diff --git a/voicelink/player.py b/voicelink/player.py
index 5aaebd9..a75e4c2 100644
--- a/voicelink/player.py
+++ b/voicelink/player.py
@@ -233,6 +233,12 @@ class Player(VoiceProtocol):
self._is_connected = state.get("connected")
self._last_position = state.get("position")
self._ping = state.get("ping")
+ if self.bot.ipc.connections:
+ await self.send_ws({ "op": "playerUpdate",
+ "last_update": self._last_update,
+ "is_connected": self._is_connected,
+ "last_position": self._last_position
+ })
async def _dispatch_voice_update(self, voice_data: Dict[str, Any]):
if {"sessionId", "event"} != self._voice_state.keys():
@@ -310,7 +316,15 @@ class Player(VoiceProtocol):
if self.settings.get('controller', True):
await self.invoke_controller()
-
+
+ if self.bot.ipc.connections:
+ await self.send_ws({
+ "op": "trackUpdate",
+ "current_queue_position": self.queue._position if track else self.queue._position + 1,
+ "track_id": track.track_id if track else None,
+ "is_paused": self._paused
+ })
+
async def invoke_controller(self):
if self.updating or not self.channel:
return
@@ -373,7 +387,9 @@ class Player(VoiceProtocol):
async def teardown(self):
timeNow = round(time.time())
func.update_settings(self.guild.id, {"lastActice": timeNow, "playTime": round(self.settings.get("playTime", 0) + ((timeNow - self.joinTime) / 60), 2)})
-
+ if self.bot.ipc.connections:
+ await self.send_ws({"op": "playerClose"})
+
try:
await self.controller.delete()
except:
@@ -543,9 +559,12 @@ class Player(VoiceProtocol):
position = self.queue.put_at_front(raw_tracks) if at_font else self.queue.put(raw_tracks)
tracks.append(raw_tracks)
finally:
- return len(tracks) if isList else position
+ if tracks:
+ if self.bot.ipc.connections:
+ await self.send_ws({"op": "addTrack", "tracks": [track.toDict() for track in tracks]}, tracks[0].requester)
+ return len(tracks) if isList else position
- async def seek(self, position: float) -> float:
+ async def seek(self, position: float, requester: Member = None) -> float:
"""Seeks to a position in the currently playing track milliseconds"""
if position < 0 or position > self._current.original.length:
raise TrackInvalidPosition(
@@ -553,17 +572,23 @@ class Player(VoiceProtocol):
)
await self._node.send(method=0, guild_id=self._guild.id, data={"position": position})
+ if self.bot.ipc.connections:
+ await self.send_ws({"op": "updatePosition", "position": position}, requester)
return self._position
- async def set_pause(self, pause: bool) -> bool:
+ async def set_pause(self, pause: bool, requester: Member = None) -> bool:
"""Sets the pause state of the currently playing track."""
await self._node.send(method=0, guild_id=self._guild.id, data={"paused": pause})
+ if self.bot.ipc.connections:
+ await self.send_ws({"op": "updatePause", "pause": pause}, requester)
self._paused = pause
return self._paused
- async def set_volume(self, volume: int) -> int:
+ async def set_volume(self, volume: int, requester: Member = None) -> int:
"""Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500."""
await self._node.send(method=0, guild_id=self._guild.id, data={"volume": volume})
+ if self.bot.ipc.connections:
+ await self.send_ws({"op": "updateVolume", "volume": volume}, requester)
self._volume = volume
return self._volume
@@ -575,6 +600,16 @@ class Player(VoiceProtocol):
shuffle(replacement)
self.queue.replace(queue_type, replacement)
self.shuffle_votes.clear()
+ if self.bot.ipc.connections:
+ await self.send_ws({
+ "op": "shuffleTrack",
+ "tracks": [track.toDict() for track in self.queue._queue],
+ "verified": {
+ "index": self.queue._position if queue_type == "queue" else 0,
+ "track_id": replacement[0].track_id,
+ }
+ }, requester)
+
async def add_filter(self, filter: Filter, fast_apply=False) -> Filters:
try:
self._filters.add_filter(filter=filter)
@@ -625,4 +660,10 @@ class Player(VoiceProtocol):
await self.set_pause(True)
if self.volume != 100:
- await self.set_volume(self.volume)
\ No newline at end of file
+ await self.set_volume(self.volume)
+
+ async def send_ws(self, payload, requester: Member = None):
+ payload['guild_id'] = self.guild.id
+ if requester:
+ payload['requester_id'] = requester.id
+ await self.bot.ipc.send(payload)
From 490e7da0438daa82f00ff157f16f81e58e03336c Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 17 Mar 2023 12:39:29 +0800
Subject: [PATCH 04/40] Add dashboard
---
web/objects.py | 25 ++
web/static/css/main.css | 499 +++++++++++++++++++++++++++++++++++++++
web/static/js/Socket.js | 47 ++++
web/static/js/action.js | 97 ++++++++
web/static/js/objects.js | 370 +++++++++++++++++++++++++++++
web/templates/index.html | 97 ++++++++
web/webapp.py | 202 ++++++++++++++++
7 files changed, 1337 insertions(+)
create mode 100644 web/objects.py
create mode 100644 web/static/css/main.css
create mode 100644 web/static/js/Socket.js
create mode 100644 web/static/js/action.js
create mode 100644 web/static/js/objects.js
create mode 100644 web/templates/index.html
create mode 100644 web/webapp.py
diff --git a/web/objects.py b/web/objects.py
new file mode 100644
index 0000000..72121f6
--- /dev/null
+++ b/web/objects.py
@@ -0,0 +1,25 @@
+class Asset:
+ def __init__(self, userId: str, key: str):
+ self.key = key
+ self.url = f"https://cdn.discordapp.com/avatars/{userId}/{key}.png"
+
+class User:
+ def __init__(self, data: dict):
+ self.id = int(data.get("id"))
+ self.username = data.get("username")
+ self.display_name = data.get("display_name")
+ self.avatar = Asset(self.id, data.get("avatar"))
+ self.avatar_decoration = data.get("avatar_decoration")
+ self.discriminator = data.get("discriminator")
+ self.public_flag = data.get("public_flag")
+ self.flags = data.get("flags")
+ self.banner = data.get("banner")
+ self.banner_color = data.get("banner_color")
+ self.locale = data.get("locale")
+ self.mfa_enabled = data.get("mfa_enabled")
+ self.premium_type = data.get("premium_type")
+ self.access_token = data.get("access_token")
+ self.refresh_token = data.get("refresh_token")
+
+ self.sid = None
+ self.guild_id = None
\ No newline at end of file
diff --git a/web/static/css/main.css b/web/static/css/main.css
new file mode 100644
index 0000000..51b5e8b
--- /dev/null
+++ b/web/static/css/main.css
@@ -0,0 +1,499 @@
+@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;600;700&display=swap');
+
+* {
+ padding: 0;
+ margin: 0;
+ box-sizing: border-box;
+ color: #ffffff;
+ font-family: 'Poppins', sans-serif;
+}
+
+html,
+body {
+ background-color: #020202;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+ max-height: 100%;
+}
+
+/* width */
+::-webkit-scrollbar {
+ width: 10px;
+}
+
+/* Handle */
+::-webkit-scrollbar-thumb {
+ background: #888;
+}
+
+/* Handle on hover */
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-height: 100%;
+}
+
+.header {
+ display: flex;
+ padding: 1rem;
+ justify-content: space-between;
+ align-items: center;
+ position: sticky;
+ top: 0;
+}
+
+.header .left {
+ display: flex;
+}
+
+.header .left p {
+ font-size: 20;
+ padding: 12px;
+ border-radius: 10px;
+ margin: 0rem 0.4rem;
+ transition: all 0.2s;
+ cursor: pointer;
+}
+
+.header .left p:hover {
+ background-color: rgba(142, 142, 142, 20%);
+}
+
+.header .center .search-contrainer {
+ display: flex;
+ justify-content: center;
+ position: relative;
+}
+
+.header .center .search-contrainer .search-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 8px;
+ padding: .6rem 1rem;
+ width: 500px;
+ overflow: hidden;
+ background-color: rgba(33, 33, 33, 255);
+}
+
+.header .center .search-loader {
+ display: none;
+ border: 3px solid transparent;
+ border-radius: 50%;
+ border-top: 3px solid #c9cbcc;
+ width: 20px;
+ height: 20px;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.header .center input {
+ margin: 0 1rem;
+ background-color: transparent;
+ width: 100%;
+ height: 100%;
+ border: none;
+ outline: none;
+ font-size: 15px;
+}
+
+.header .center .search-result-list {
+ position: absolute;
+ display: none;
+ overflow-x: hidden;
+ overflow-y: auto;
+ top: 50px;
+ width: 100%;
+ height: 400px;
+ border-radius: 8px;
+ padding: 1rem .5rem;
+ background-color: rgba(33, 33, 33, 255);
+}
+
+.header .search-result {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 8px;
+ padding: .5rem 1rem;
+ cursor: pointer;
+ margin-bottom: .5rem;
+}
+
+.header .search-result:hover {
+ background-color: rgba(142, 142, 142, 20%);
+}
+
+.header .search-result-left {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ margin-right: .6rem;
+ max-width: 90%;
+}
+
+.header .search-result-info p.info {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+}
+
+.header .search-result-info p.desc {
+ font-size: 14px;
+ color: gray;
+}
+
+.header .search-result img {
+ width: auto;
+ height: 50px;
+ border-radius: 4px;
+ margin-right: .5rem;
+}
+
+.header .right .account {
+ display: flex;
+ align-items: center;
+ border-radius: 12px;
+ padding: 10px;
+ transition: all .2s;
+ cursor: pointer;
+}
+
+.header .right .account:hover {
+ background-color: rgba(142, 142, 142, 20%);
+}
+
+.header .right .account img {
+ border-radius: 2rem;
+ height: 40;
+ margin-right: .6rem;
+}
+
+.main {
+ display: flex;
+ flex-grow: 1;
+ padding: 0rem 4rem;
+ justify-content: space-around;
+}
+
+.main .toastContrainer {
+ position: absolute;
+ left: 2%;
+}
+
+.toastContrainer .toast {
+ display: flex;
+ position: relative;
+ padding: .8rem;
+ margin-top: 1rem;
+ background-color: rgba(142, 142, 142, 30%);
+ border-radius: 10px;
+ overflow: hidden;
+ transform: translateX(-100%);
+ animation: moveRight .5s linear forwards;
+}
+
+@keyframes moveRight {
+ 100% {
+ transform: translateX(0);
+ }
+}
+
+.toastContrainer .toast::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background-color: azure;
+ animation: anim 6s linear forwards;
+}
+
+@keyframes anim {
+ 100% {
+ width: 0;
+ }
+}
+
+.toastContrainer .toast img {
+ border-radius: 10px;
+ width: 40px;
+ height: 40px;
+ margin-right: .7rem;
+}
+
+.toastContrainer .toast .content p.username {
+ font-size: 15px;
+}
+
+.toastContrainer .toast .content p.message {
+ font-size: 10px;
+ width: 300px;
+ white-space: pre-wrap;
+}
+
+.main .thumbnail {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex: 1;
+}
+
+.main .thumbnail img {
+ padding: 1rem;
+ width: 70%;
+ height: auto;
+}
+
+.player-controller {
+ position: sticky;
+ bottom: 0;
+}
+
+.progress-bar-container {
+ position: relative;
+ width: 100%;
+}
+
+.seek-bar {
+ position: absolute;
+ appearance: none;
+ width: 100%;
+ height: 4px;
+ cursor: pointer;
+ overflow: hidden;
+ background-color: rgb(73, 73, 73);
+}
+
+.seek-bar:hover {
+ height: 8px;
+}
+
+.seek-bar::-webkit-slider-thumb {
+ appearance: none;
+ width: 1px;
+ height: 20px;
+ background: rgb(171, 168, 168);
+ box-shadow: -1000px 0 0 1000px rgb(171, 168, 168);
+}
+
+.control-container {
+ display: flex;
+ padding: 1rem;
+ align-items: center;
+ background-color: #212121;
+ justify-content: space-between;
+}
+
+.player-controller .left {
+ display: flex;
+ flex: 1;
+}
+
+.player-controller .left .control {
+ display: flex;
+ align-items: center;
+}
+
+.player-controller .left .control i {
+ cursor: pointer;
+ padding: 5px;
+ margin-left: .8rem;
+ font-size: 20;
+}
+
+.player-controller .left .control #play-pause-button {
+ font-size: 30;
+}
+
+.player-controller .left .position {
+ display: flex;
+ padding: 10px;
+ margin-left: 1rem;
+}
+
+.player-controller .center {
+ display: flex;
+ padding: 0rem 1rem;
+ justify-content: center;
+ align-items: center;
+ flex-grow: 1;
+ flex: 2;
+ max-width: 40%;
+}
+
+.player-controller .center img {
+ border-radius: .2rem;
+ height: 40;
+}
+
+.player-controller .center .data {
+ margin-left: 1rem;
+ overflow: hidden;
+}
+
+.player-controller .center .desc {
+ color: grey;
+ font-size: 13;
+}
+
+.player-controller .right {
+ flex: 1;
+ display: flex;
+ justify-content: right;
+ align-items: center;
+ padding-right: 5px;
+}
+
+.player-controller .right i {
+ cursor: pointer;
+ padding: 5px;
+ margin-left: .8rem;
+ font-size: 20;
+ color: gray;
+}
+
+.main .queue-list {
+ overflow-y: auto;
+ overflow-x: hidden;
+ flex: 1;
+ display: flex;
+ justify-content: center;
+}
+
+.track {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ border-radius: .5rem;
+ padding: 5px 15px;
+ overflow: hidden;
+ margin-bottom: .5rem;
+ transition: background-color .2s, transform .2s;
+}
+
+.track:hover {
+ background-color: rgba(142, 142, 142, 20%);
+ /* transform: scale(1.02); */
+}
+
+.track.active {
+ background-color: rgba(142, 142, 142, 30%);
+}
+
+.track .left {
+ display: flex;
+ justify-content: left;
+ align-items: center;
+ margin-right: .5rem;
+ flex: 1;
+}
+
+.track .left i {
+ cursor: move;
+ margin-right: 1rem;
+ font-size: 20px;
+}
+
+.track .left img {
+ padding: 5px;
+ height: 50;
+ border-radius: .5rem;
+}
+
+.track .left .info {
+ display: block;
+ max-width: 300px;
+ margin-left: .5rem;
+}
+
+.track .left .info p {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.track .left .info p.desc {
+ color: grey;
+ font-size: 13;
+}
+
+.main .queue-list ul {
+ list-style: none;
+ width: 90%;
+ padding: 1rem;
+ max-height: 300px;
+}
+
+@media (max-width: 935px) {
+ .main {
+ flex-direction: column;
+ justify-content: center;
+ padding: 0;
+ }
+
+ .header .center {
+ display: none;
+ }
+
+ .player-controller .left .position {
+ display: none;
+ }
+
+ .player-controller .center img {
+ display: none;
+ }
+
+ ul {
+ width: 100%;
+ }
+}
+
+@media (max-width: 640px) {
+ .main {
+ padding: 0;
+ }
+
+ .main .thumbnail img {
+ padding: 0;
+ width: 80%;
+ height: auto;
+ }
+
+ .main .queue-list {
+ overflow-y: auto;
+ overflow-x: hidden;
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ min-width: 100%;
+ }
+
+ .track .left i {
+ margin-right: .5rem;
+ font-size: 15px;
+ }
+
+ .track .left img {
+ padding: 2px;
+ height: 30;
+ border-radius: .2rem;
+ }
+
+ .track .left .info {
+ max-width: 200px;
+ }
+}
\ No newline at end of file
diff --git a/web/static/js/Socket.js b/web/static/js/Socket.js
new file mode 100644
index 0000000..74ac9f2
--- /dev/null
+++ b/web/static/js/Socket.js
@@ -0,0 +1,47 @@
+class Socket {
+ constructor(url) {
+ this.url = url;
+ this.socket = null;
+ }
+
+ connect(player) {
+ this.socket = io.connect(this.url);
+
+ this.socket.on('connect', () => {
+ console.log("Connected to server!");
+ });
+
+ this.socket.on('disconnect', () => {
+ player.init();
+ console.log("Disconnected from server!");
+ });
+
+ this.socket.on('error', (e) => {
+ console.log(e);
+ })
+ }
+
+ disconnect() {
+ if (this.socket) {
+ this.socket.disconnect();
+ }
+ }
+
+ send(msg) {
+ if (this.socket) {
+ this.socket.emit('message', msg);
+ }
+ }
+
+ addMessageListener(callback) {
+ if (this.socket) {
+ this.socket.on('message', callback);
+ }
+ }
+
+ removeMessageListener(callback) {
+ if (this.socket) {
+ this.socket.off('message', callback);
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/static/js/action.js b/web/static/js/action.js
new file mode 100644
index 0000000..aa6cac0
--- /dev/null
+++ b/web/static/js/action.js
@@ -0,0 +1,97 @@
+$(document).ready(function () {
+ const player = new Player(userId);
+ var startPos = null;
+
+ var typingTimer;
+ var doneTypingInterval = 2000;
+
+ $('body').click(function(event) {
+ var $target = $(event.target);
+ var $resultList = $target.closest(".search-contrainer");
+
+ if (!$resultList.is('.search-contrainer') && $('#search-result-list').css("display") != 'none') {
+ $("#search-result-list").fadeOut(200);
+ }
+ });
+
+ $(function () {
+ $("#sortable").sortable({
+ handle: ".handle",
+ scroll: true,
+ axis: "y",
+ start: function (event, ui) {
+ startPos = ui.item.index();
+ },
+ stop: function (event, ui) {
+ if (startPos != null) {
+ var newPos = ui.item.index();
+ if (startPos != newPos) {
+ player.moveTrack(startPos, newPos)
+ }
+ }
+ }
+ });
+ });
+
+ $('#sortable').on('click', 'li', function () {
+ var index = $(this).index();
+ var position = player.current_queue_position;
+
+ if (index < position) {
+ player.backTo(position - index);
+ } else if (index > position) {
+ player.skipTo(index - position)
+ } else {
+ player.togglePause();
+ }
+ })
+
+ $('#search-result-list').on('click', 'li', function () {
+ var index = $(this).index();
+ var track = player.searchList[index];
+ if (track != undefined) {
+ player.send({ "op": "addTracks", "tracks": [track] })
+ }
+ $("#search-result-list").fadeOut(200);
+ })
+
+ $('#search-input').on('input', function () {
+ clearTimeout(typingTimer);
+ $("#search-loader").fadeIn(200);
+ typingTimer = setTimeout(function () {
+ var input = $('#search-input').val();
+ if (input.replace(/\s+/g, '') != "") {
+ player.send({ "op": "getTracks", "query": input })
+ } else {
+ $("#search-loader").fadeOut(200);
+ $("#search-result-list").fadeOut(200);
+ }
+ }, doneTypingInterval);
+ });
+
+ $('#search-input').focus(function () {
+ if ($(this).val() != "") {
+ $("#search-result-list").fadeIn(200);
+ }
+ })
+
+ $('#play-pause-button').on('click', function () {
+ player.togglePause();
+ });
+
+ $('#skip-button').on('click', function () {
+ player.skipTo();
+ });
+
+ $('#back-button').on('click', function () {
+ player.backTo();
+ });
+
+ $('#seek-bar').change(function () {
+ player.seekTo($(this).val());
+ })
+
+ $("#shuffle-button").on('click', function() {
+ player.shuffle();
+ })
+});
\ No newline at end of file
diff --git a/web/static/js/objects.js b/web/static/js/objects.js
new file mode 100644
index 0000000..206d156
--- /dev/null
+++ b/web/static/js/objects.js
@@ -0,0 +1,370 @@
+class Timer {
+ constructor(callback, interval) {
+ this.callback = callback;
+ this.interval = interval;
+ this.timerId = null;
+ this.isRunning = false;
+ }
+
+ start() {
+ if (!this.isRunning) {
+ this.isRunning = true;
+ this.timerId = setInterval(() => {
+ this.callback();
+ }, this.interval);
+ }
+ }
+
+ stop() {
+ if (this.isRunning) {
+ clearInterval(this.timerId);
+ this.timerId = null;
+ this.isRunning = false;
+ }
+ }
+
+ getIsRunning() {
+ return this.isRunning;
+ }
+}
+
+const actions = {
+ initPlayer: function (player, data) {
+ player.init();
+ player.addTrack(data["tracks"]);
+ player.updateCurrentQueuePos(data['current_queue_position']);
+ player.isDJ = data['is_dj'];
+ player.is_paused = data['is_paused'];
+ player.current_position = data['current_position'];
+ data["users"].forEach(user => {
+ player.addUser(user);
+ })
+ },
+
+ playerUpdate: function (player, data) {
+ player.last_update = data["last_update"];
+ player.is_connected = data["is_connected"];
+ player.current_position = data["last_position"];
+ },
+
+ trackUpdate: function (player, data) {
+ var track = player.updateCurrentQueuePos(data['current_queue_position']);
+ player.is_paused = data['is_paused'];
+ if (track?.track_id != data["track_id"]) {
+ player.send({ "op": "initPlayer" })
+ }
+ },
+
+ addTrack: function (player, data) {
+ player.addTrack(data["tracks"]);
+ var tracks = data["tracks"];
+ if (tracks.length == 1) {
+ var msg = `Added ${tracks[0]['info']['title']} songs into the queue.`
+ } else {
+ var msg = `Added ${tracks.length} into the queue.`
+ }
+ player.showToast(data["requester_id"], msg)
+ },
+
+ getTracks: function (player, data) {
+ var tracks = data["tracks"];
+ if (tracks != undefined) {
+ const resultList = $("#search-result-list");
+ resultList.empty();
+ player.searchList = tracks;
+ for (var i in tracks) {
+ var track = new Track(tracks[i]);
+ resultList.append(`
${track.title}
${track.author}
${player.msToReadableTime(track.length)}
`)
+ }
+ }
+ $("#search-result-list").fadeIn(200);
+ $("#search-loader").fadeOut(200);
+ },
+
+ playerClose: function (player, data) {
+ player.init();
+ },
+
+ updateGuild: function (player, data) {
+ const user = data["user"];
+ if (user["user_id"] == player.userId) {
+ if (data['is_joined']) {
+ player.send({ "op": "initPlayer" });
+ } else {
+ player.init();
+ }
+ }
+ if (data['is_joined']) {
+ player.addUser(user);
+ player.showToast(user["user_id"], "Joined your channel!");
+ } else {
+ if (player.users.hasOwnProperty(user["user_id"])) {
+ player.showToast(user["user_id"], "Left your channel!");
+ delete player.users[user['user_id']];
+ }
+ }
+ },
+
+ updatePause: function (player, data) {
+ player.is_paused = data['pause'];
+ var msg = "";
+ if (data["pause"]) {
+ msg = "Paused the player."
+ } else {
+ msg = "Resumed the player."
+ }
+ player.showToast(data["requester_id"], msg)
+ },
+
+ updatePosition: function (player, data) {
+ player.current_position = data["position"];
+ },
+
+ swapTrack: function (player, data) {
+ var index1 = player.current_queue_position + data['position2']["index"];
+ var index2 = player.current_queue_position + data['position1']["index"];
+ var track1 = player.queue[index1];
+ var track2 = player.queue[index2];
+
+ if (track1?.track_id != data['position1']["track_id"] || track2?.track_id != data['position2']["track_id"]) {
+ return player.send({ "op": "initPlayer" });
+ }
+
+ player.queue[index1] = player.queue.splice(index2, 1, player.queue[index1])[0];
+ player.initSortable();
+ player.showToast(data["requester_id"], `${track1.title} and ${track2.title} are swapped`)
+ },
+
+ moveTrack: function (player, data) {
+ let position = player.current_queue_position + data["position"]["index"];
+ let newPosition = player.current_queue_position + data["newPosition"]["index"];
+ let element = player.queue.splice(position, 1)[0];
+ if (element?.track_id != data["position"]["track_id"]) {
+ return player.send({ "op": "initPlayer" });
+ }
+ player.queue.splice(newPosition, 0, element);
+
+ const $ul = $('#sortable');
+ const $li = $ul.children().eq(position);
+ $li.detach();
+ $ul.children().eq(newPosition).before($li);
+ player.showToast(data["requester_id"], `Moved ${element.title} to ${newPosition}`)
+ },
+
+ shuffleTrack: function(player, data) {
+ var tracks = data["tracks"];
+ if (tracks != undefined) {
+ player.queue = [];
+ tracks.forEach(rawTrack => {
+
+ var track = new Track(rawTrack);
+ player.queue.push(track);
+ });
+ player.initSortable();
+ player.showToast(data["requester_id"], "The queue is shuffled.")
+ }
+ }
+
+}
+
+class Track {
+ constructor(object) {
+ this.title = object["info"]["title"];
+ this.author = object["info"]["author"];
+ this.imageUrl = object["thumbnail"];
+ this.length = object["info"]["length"];
+ this.track_id = object["track_id"];
+ }
+}
+
+class Player {
+ constructor(userId) {
+ this.socket = new Socket('http://127.0.0.1:5000');
+ this.socket.connect(this);
+ this.socket.addMessageListener((msg) => this.handleMessage(msg));
+ this.timer = new Timer(() => this.updateTime(), 1000);
+ this.userId = parseInt(userId);
+ this.isDJ = false;
+ this.date = new Date();
+ this.queue = [];
+ this.users = {};
+ this.searchList = []
+ this.currentTrack = null;
+ this.current_queue_position = 0;
+ this.current_position = 0;
+ this.is_paused = false;
+ this.volume = 100;
+ this.last_update = 0;
+ this.is_connected = true;
+ }
+
+ handleMessage(data) {
+ const op = data["op"];
+ const validActions = Object.keys(actions);
+
+ if (validActions.includes(op)) {
+ actions[op](this, data);
+ } else {
+ console.log(`Invalid action: ${op}`)
+ }
+
+ return this.updateInfo()
+ }
+
+ init() {
+ this.queue = []
+ this.currentTrack = null;
+ $('#sortable').empty();
+ this.updateInfo();
+ }
+
+ initSortable() {
+ $('#sortable').empty();
+ for (var i in this.queue) {
+ var track = this.queue[i];
+ $("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
+ }
+ }
+
+ updateCurrentQueuePos(pos) {
+ this.current_queue_position = pos - 1;
+ this.currentTrack = this.queue[this.current_queue_position];
+
+ $('#sortable li div').removeClass('active');
+ const li = $(`#sortable li:eq(${this.current_queue_position})`);
+ li.find('div').addClass('active');
+ $('.queue-list').animate({ scrollTop: li.position().top - $('.queue-list').position().top }, 'slow');
+
+ return this.currentTrack;
+ }
+
+ addUser(user) {
+ this.users[user['user_id']] = { "avatar_url": user["avatar_url"], "name": user["name"] };
+ }
+
+ addTrack(tracks) {
+ for (var i in tracks) {
+ var track = new Track(tracks[i]);
+ this.queue.push(track);
+ $("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
+ }
+ }
+
+ moveTrack(target, to) {
+ this.send({ "op": "moveTrack", "position": target, "newPosition": to })
+ }
+
+ togglePause() {
+ this.send({ "op": "updatePause", "pause": !this.is_paused });
+ }
+
+ skipTo(index = 1) {
+ this.send({ "op": "skipTo", "index": index });
+ }
+
+ backTo(index = 1) {
+ this.send({ "op": "backTo", "index": index });
+ }
+
+ seekTo(tempPosition) {
+ if (this.currentTrack == undefined) {
+ return;
+ }
+ var position = tempPosition / 500 * this.currentTrack.length;
+ this.send({ "op": "updatePosition", "position": position });
+ }
+
+ shuffle() {
+ if ((this.queue.length - this.current_queue_position) > 3) {
+ this.send({ "op": "shuffleTrack" });
+ } else {
+ this.showToast(this.userId, "Add more songs to the queue before shuffling.");
+ }
+ }
+
+ send(payload) {
+ var json = JSON.stringify(payload)
+ this.socket.send(json);
+ }
+
+ isPlaying() {
+ return (this.currentTrack != undefined && this.is_connected);
+ }
+
+ msToReadableTime(ms) {
+ let totalSeconds = Math.floor(ms / 1000);
+
+ let hours = Math.floor(totalSeconds / 3600);
+ let minutes = Math.floor((totalSeconds % 3600) / 60);
+ let seconds = totalSeconds % 60;
+
+ minutes = (minutes < 10) ? "0" + minutes : minutes;
+ seconds = (seconds < 10) ? "0" + seconds : seconds;
+
+ let timeString = "";
+ if (hours > 0) {
+ timeString += hours + ":" + minutes + ":" + seconds;
+ } else {
+ timeString += minutes + ":" + seconds;
+ }
+
+ return timeString;
+ }
+
+ showToast(userId, msg) {
+ var user = this.users[userId];
+ if (user != null) {
+ var $element = $(``)
+ $(".toastContrainer").append($element)
+
+ setTimeout(function () {
+ $element.fadeOut(500, function () {
+ $(this).remove();
+ });
+ }, 6000);
+ }
+ }
+
+ updateTime() {
+ if (this.currentTrack == undefined) {
+ return this.timer.stop();
+ }
+
+ if (this.current_position >= this.currentTrack?.length) {
+ return this.timer.stop();
+ }
+ this.current_position += 1000;
+ $("#position").text(this.msToReadableTime(this.current_position));
+
+ var time = (this.current_position / this.currentTrack?.length) * 500;
+ $("#seek-bar").val(time);
+ }
+
+ updateInfo() {
+ var currentTrack = this.currentTrack;
+ if (currentTrack == undefined) {
+ $("#title").text("");
+ $("#author").text("");
+ $("#position").text("00:00");
+ $("#length").text("00:00");
+ $("#image").removeAttr('src');
+ $("#largeImage").removeAttr('src');
+ } else {
+ $("#title").text(currentTrack.title);
+ $("#author").text(currentTrack.author);
+ $("#length").text(this.msToReadableTime(currentTrack.length));
+ $("#image").attr("src", currentTrack.imageUrl);
+ $("#largeImage").attr("src", currentTrack.imageUrl);
+ }
+ var play_pause_btn = $("#play-pause-button")
+ if (this.is_paused || currentTrack == undefined) {
+ play_pause_btn.removeClass('fa-pause').addClass('fa-play');
+ if (this.timer.getIsRunning()) {
+ this.timer.stop();
+ }
+ } else {
+ play_pause_btn.removeClass('fa-play').addClass('fa-pause');
+ this.timer.start();
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/templates/index.html b/web/templates/index.html
new file mode 100644
index 0000000..14de6fe
--- /dev/null
+++ b/web/templates/index.html
@@ -0,0 +1,97 @@
+
+
+
+
+ Vocard Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/webapp.py b/web/webapp.py
new file mode 100644
index 0000000..5ad4c80
--- /dev/null
+++ b/web/webapp.py
@@ -0,0 +1,202 @@
+from flask import Flask, redirect, url_for, session, request, render_template, abort
+from flask_socketio import SocketIO, emit, join_room, leave_room, rooms, disconnect
+from ipc import IPCClient
+from objects import User
+from dotenv import load_dotenv
+
+import requests
+import json
+import os
+import asyncio
+import functools
+import threading
+
+load_dotenv()
+
+app = Flask(__name__)
+app.secret_key = "aNOwneoiniowefn"
+socketio = SocketIO(app)
+
+# Discord OAuth2 credentials
+CLIENT_ID = os.getenv("CLIENT_ID")
+CLIENT_SECRET = os.getenv("CLIENT_SECRET_ID")
+REDIRECT_URI = 'http://127.0.0.1:5000/callback'
+DISCORD_API_BASE_URL = 'https://discord.com/api'
+
+USERS = {}
+
+def start_ipc_client(loop):
+ client = IPCClient(secret_key="Vocard")
+ asyncio.set_event_loop(loop)
+ loop.run_until_complete(client.connect())
+
+def create_ipc_client():
+ loop = asyncio.new_event_loop()
+ threading.Thread(target=start_ipc_client,
+ args=(loop,), daemon=True).start()
+ return IPCClient(secret_key="Vocard", callback=message_handler)
+
+def get_user(user_id: int):
+ for user in USERS.values():
+ if user.id == user_id:
+ return user
+ return None
+
+def user_join_room(guild_id: int, user: User) -> None:
+ if not user.sid:
+ return
+ join_room(guild_id, sid=user.sid)
+ user.guild_id = guild_id
+
+def user_leave_room(guild_id: int, user: User) -> None:
+ if not user.sid:
+ return
+ leave_room(guild_id, sid=user.sid)
+ user.guild_id = None
+
+def message_handler(data: dict):
+ op = data.get("op")
+
+ user_id = data.get("user_id", None)
+ if user_id:
+ user: User = get_user(user_id)
+ else:
+ user = None
+
+ guild_id = data.get("guild_id", None)
+
+ if op == "updateGuild":
+ user_id = data.get("user", {}).get("user_id", None)
+ is_joined = data.get("is_joined")
+
+ user: User = get_user(user_id)
+ if user and guild_id:
+ user_join_room(guild_id, user) if is_joined else user_leave_room(guild_id, user)
+
+ elif op == "createPlayer":
+ members_id = data.get("members_id", [])
+ for member_id in members_id:
+ user: User = get_user(member_id)
+ if user:
+ user_join_room(guild_id, user)
+ emit('message', {
+ "op": "updateGuild",
+ "user": {
+ "user_id": user.id,
+ "avatar_url": user.avatar.url,
+ "name": user.display_name
+ },
+ "is_joined": True
+ }, to=user.sid)
+ return
+
+ if user:
+ if guild_id and user.sid:
+ join_room(guild_id, sid=user.sid)
+ elif guild_id in rooms(user.sid):
+ pass
+ else:
+ emit('message', data, to=user.sid)
+
+ if op == "initPlayer":
+ return emit('message', data, to=user.sid)
+
+ if guild_id:
+ skip_sids = []
+ if skip_users := data.get("skip_users"):
+ for user_id in skip_users:
+ if user := get_user(user_id):
+ skip_sids.append(user.sid)
+
+ emit('message', data, room=guild_id, skip_sid=skip_sids)
+
+ipc_client = create_ipc_client()
+
+def login_required(func):
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ token = session.get("discord_token", None)
+ if not token:
+ return redirect(url_for('login'))
+
+ if token not in USERS:
+ resp = requests_api(f'{DISCORD_API_BASE_URL}/users/@me',
+ headers={'Authorization': f'Bearer {token}'})
+ if resp:
+ user = USERS[token] = User(resp)
+ else:
+ abort(401, description="Unauthorized")
+ else:
+ user = USERS[token]
+
+ return func(user, *args, **kwargs)
+ return wrapper
+
+
+def requests_api(url: str, headers=None):
+ resp = requests.get(url=url, headers=headers)
+ if not resp:
+ return False
+
+ return resp.json()
+
+# home page
+@app.route('/')
+@login_required
+def home(user: User):
+ return render_template("index.html", user=user)
+
+# login page
+@app.route('/login')
+def login():
+ # redirect to Discord OAuth2 login page
+ params = {
+ 'client_id': CLIENT_ID,
+ 'response_type': 'code',
+ 'redirect_uri': REDIRECT_URI,
+ 'scope': 'identify'
+ }
+ return redirect(f'{DISCORD_API_BASE_URL}/oauth2/authorize?{"&".join([f"{k}={v}" for k, v in params.items()])}')
+
+# callback page
+@app.route('/callback')
+def callback():
+ # fetch user token from Discord OAuth2
+ code = request.args.get('code')
+ data = {
+ 'client_id': CLIENT_ID,
+ 'client_secret': CLIENT_SECRET,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+ 'redirect_uri': REDIRECT_URI,
+ 'scope': 'identify'
+ }
+ response = requests.post(f'{DISCORD_API_BASE_URL}/oauth2/token', data=data)
+ token_data = json.loads(response.content.decode('utf-8'))
+ session['discord_token'] = token_data.get("access_token")
+
+ return redirect(url_for("home"))
+
+
+@socketio.on("connect")
+@login_required
+def handle_connect(user: User):
+ if user.sid:
+ disconnect(sid=user.sid)
+ user.sid = request.sid
+ asyncio.run(ipc_client.send('{"op": "initPlayer"}', user))
+
+@socketio.on("disconnect")
+@login_required
+def handle_disconnect(user: User):
+ user.sid = None
+
+@socketio.on("message")
+@login_required
+def handle_message(user: User, msg):
+ asyncio.run(ipc_client.send(msg, user))
+
+
+if __name__ == '__main__':
+ socketio.run(app, host="127.0.0.1", port=5000)
From d8263fb8f35935b0c32d0a6d870844084f1e1833 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sat, 18 Mar 2023 10:15:08 +0800
Subject: [PATCH 05/40] Add configuration for ipc server
---
.env Example | 2 ++
function.py | 4 +++-
main.py | 3 ++-
settings Example.json | 1 +
web/ipc/client.py | 12 +++++++-----
web/ipc/server.py | 12 ++++++------
6 files changed, 21 insertions(+), 13 deletions(-)
diff --git a/.env Example b/.env Example
index 5bd0292..18b1cda 100644
--- a/.env Example
+++ b/.env Example
@@ -1,5 +1,7 @@
TOKEN = YOUR_BOT_TOKEN
CLIENT_ID = YOUR_BOT_CLIENT_ID
+CLIENT_SECRET_ID = YOUR_BOT_CLIENT_SECRET_ID
+
BUG_REPORT_CHANNEL_ID = YOUR_DISCORD_CHANNEL_ID
SPOTIFY_CLIENT_ID = YOUR_SPOTIFY_CLIENT_ID
diff --git a/function.py b/function.py
index d2713ea..351a140 100644
--- a/function.py
+++ b/function.py
@@ -56,6 +56,7 @@ max_queue = 1000 #The default maximum number of tracks in the queue
cooldowns_settings = {} #Stores command cooldown settings
aliases_settings = {} #Stores command aliases settings
controller_settings = {} #Stores the layout of music controller
+enable_ipc = False #Enable ipc server or not
#----------------- Nodes -----------------
nodes = {}
@@ -105,11 +106,12 @@ def settings_setup():
with open(os.path.join(root_dir, "settings.json"), encoding="utf8") as json_file:
rawSettings = json.load(json_file)
- global nodes, max_queue, bot_prefix, embed_color, bot_access_user, emoji_source_raw, cooldowns_settings, aliases_settings, controller_settings
+ global nodes, max_queue, bot_prefix, embed_color, bot_access_user, emoji_source_raw, cooldowns_settings, aliases_settings, controller_settings, enable_ipc
nodes = rawSettings.get("nodes", {})
if (new_max_queue := rawSettings.get("default_max_queue", max_queue)):
max_queue = new_max_queue
bot_prefix = rawSettings.get("prefix", "")
+ enable_ipc = rawSettings.get("enable_ipc", False)
embed_color = int(rawSettings.get("embed_color", "0xb3b3b3"), 16)
bot_access_user = rawSettings.get("bot_access_user", [])
emoji_source_raw = rawSettings.get("emoji_source_raw", {})
diff --git a/main.py b/main.py
index cc560ae..bf11c87 100644
--- a/main.py
+++ b/main.py
@@ -51,7 +51,8 @@ class Vocard(commands.Bot):
except Exception as e:
print(traceback.format_exc())
- await self.ipc.start()
+ if func.enable_ipc:
+ await self.ipc.start()
await bot.tree.set_translator(Translator())
await bot.tree.sync()
diff --git a/settings Example.json b/settings Example.json
index 0847813..286f234 100644
--- a/settings Example.json
+++ b/settings Example.json
@@ -10,6 +10,7 @@
},
"prefix": "?",
"bot_access_user": [],
+ "enable_ipc": false,
"color_code":"0xb3b3b3",
"default_max_queue": 1000,
"emoji_source_raw": {
diff --git a/web/ipc/client.py b/web/ipc/client.py
index 96bcc14..e782e25 100644
--- a/web/ipc/client.py
+++ b/web/ipc/client.py
@@ -2,11 +2,13 @@ import websockets, json
from uuid import uuid4
class IPCClient:
- def __init__(self,
- host = "127.0.0.1",
- port = 8000,
- secret_key = None,
- callback = None):
+ def __init__(
+ self,
+ host = "127.0.0.1",
+ port = 8000,
+ secret_key = None,
+ callback = None
+ ):
self.host = host
self.port = port
diff --git a/web/ipc/server.py b/web/ipc/server.py
index ad1efdf..df0894b 100644
--- a/web/ipc/server.py
+++ b/web/ipc/server.py
@@ -12,12 +12,12 @@ from typing import (
class IPCServer:
def __init__(
- self,
- bot: commands.Bot,
- host: str = "127.0.0.1",
- port: int = 8000,
- sercet_key: Optional[str] = None
- ):
+ self,
+ bot: commands.Bot,
+ host: str = "127.0.0.1",
+ port: int = 8000,
+ sercet_key: Optional[str] = None
+ ):
self.bot = bot
self.host = host
self.port = port
From 0c941e804e96fe9e1be933fa6fa2096a93a03ddd Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sat, 18 Mar 2023 11:12:39 +0800
Subject: [PATCH 06/40] Add logout function
---
web/static/css/main.css | 55 +++++++++++++++++++++++++++++++++++-----
web/templates/index.html | 13 +++++++---
web/webapp.py | 8 ++++++
3 files changed, 67 insertions(+), 9 deletions(-)
diff --git a/web/static/css/main.css b/web/static/css/main.css
index 51b5e8b..517f35b 100644
--- a/web/static/css/main.css
+++ b/web/static/css/main.css
@@ -18,6 +18,10 @@ body {
max-height: 100%;
}
+a {
+ text-decoration: none;
+}
+
/* width */
::-webkit-scrollbar {
width: 10px;
@@ -40,6 +44,51 @@ body {
min-height: 100%;
}
+.dropdown-container {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ cursor: pointer;
+ border-radius: 10px;
+ transition: all .1s linear;
+}
+
+.dropdown-container:hover {
+ background-color: rgba(142, 142, 142, 20%);
+}
+
+.dropdown-container:hover ul {
+ opacity: 1;
+}
+
+.dropdown-container ul {
+ list-style: none;
+ position: absolute;
+ padding: .5rem;
+ border-radius: 10px;
+ top: 68px;
+ width: 100%;
+ opacity: 0;
+ background-color: rgba(142, 142, 142, 20%);
+ transition: all .1s linear;
+}
+
+.dropdown-container li {
+ display: flex;
+ padding: .5rem .8rem;
+ justify-content: center;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ border-radius: 4px;
+ transition: all .2s linear;
+ color: #FF0000;
+}
+
+.dropdown-container li:hover {
+ background-color: rgba(142, 142, 142, 30%);
+}
+
.header {
display: flex;
padding: 1rem;
@@ -167,12 +216,6 @@ body {
align-items: center;
border-radius: 12px;
padding: 10px;
- transition: all .2s;
- cursor: pointer;
-}
-
-.header .right .account:hover {
- background-color: rgba(142, 142, 142, 20%);
}
.header .right .account img {
diff --git a/web/templates/index.html b/web/templates/index.html
index 14de6fe..a6d29a4 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -42,9 +42,16 @@
-
-
-
{{ user.username }}
+
+
+
+
{{ user.username }}
+
+
diff --git a/web/webapp.py b/web/webapp.py
index 5ad4c80..e639ffb 100644
--- a/web/webapp.py
+++ b/web/webapp.py
@@ -159,6 +159,14 @@ def login():
}
return redirect(f'{DISCORD_API_BASE_URL}/oauth2/authorize?{"&".join([f"{k}={v}" for k, v in params.items()])}')
+# Logout page
+@app.route('/logout')
+@login_required
+def logout(user: User):
+ session.pop("discord_token", None)
+
+ return redirect(url_for("home"))
+
# callback page
@app.route('/callback')
def callback():
From 74846c583f12ca1ea2e682f8847e2e78ff3c1767 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sun, 19 Mar 2023 16:46:55 +0800
Subject: [PATCH 07/40] Cleaned up some code
---
addons/__init__.py | 3 +-
addons/settings.py | 18 +++++++
cogs/admin.py | 10 ++--
cogs/basic.py | 8 +--
cogs/{listener.py => listeners.py} | 2 +-
cogs/playlist.py | 4 +-
function.py | 81 ++++++++++++------------------
main.py | 17 ++++---
settings Example.json | 6 ++-
views/controller.py | 4 +-
views/help.py | 6 +--
views/inbox.py | 2 +-
views/list.py | 2 +-
views/lyrics.py | 2 +-
views/playlist.py | 4 +-
voicelink/player.py | 10 ++--
16 files changed, 94 insertions(+), 85 deletions(-)
create mode 100644 addons/settings.py
rename cogs/{listener.py => listeners.py} (98%)
diff --git a/addons/__init__.py b/addons/__init__.py
index 088e3ad..aa4e903 100644
--- a/addons/__init__.py
+++ b/addons/__init__.py
@@ -1 +1,2 @@
-from .lyrics import getLyrics
\ No newline at end of file
+from .lyrics import getLyrics
+from .settings import Settings
\ No newline at end of file
diff --git a/addons/settings.py b/addons/settings.py
new file mode 100644
index 0000000..91ad991
--- /dev/null
+++ b/addons/settings.py
@@ -0,0 +1,18 @@
+class Settings:
+ def __init__(self, settings: dict) -> None:
+ self.invite_link = "https://discord.gg/wRCgB7vBQv"
+ self.nodes = settings.get("nodes", {})
+ self.max_queue = settings.get("default_max_queue", 1000)
+ self.bot_prefix = settings.get("prefix", "")
+ self.embed_color = int(settings.get("embed_color", "0xb3b3b3"), 16)
+ self.bot_access_user = settings.get("bot_access_user", [])
+ self.emoji_source_raw = settings.get("emoji_source_raw", {})
+ self.cooldowns_settings = settings.get("cooldowns", {})
+ self.aliases_settings = settings.get("aliases", {})
+ self.controller_settings = settings.get("controller", [["back", "resume", "skip", {"stop": "red"}, "add"], ["tracks"]])
+ self.ipc_server = settings.get("ipc_server", {
+ "host": "127.0.0.1",
+ "port": 8000,
+ "enable": False
+ }
+ )
\ No newline at end of file
diff --git a/cogs/admin.py b/cogs/admin.py
index 3067c47..a040e34 100644
--- a/cogs/admin.py
+++ b/cogs/admin.py
@@ -13,7 +13,7 @@ from function import (
update_settings,
get_settings,
get_lang,
- embed_color,
+ settings as sett,
time as ctime,
get_aliases,
cooldown_check
@@ -138,13 +138,13 @@ class Admin(commands.Cog, name="settings"):
async def view(self, ctx: commands.Context):
"Show all the bot settings in your server."
player, settings = self.get_settings(ctx)
- embed = discord.Embed(color=embed_color)
+ embed = discord.Embed(color=sett.embed_color)
embed.set_author(name=get_lang(ctx.guild.id, 'settingsMenu').format(ctx.guild.name), icon_url=self.bot.user.display_avatar.url)
if ctx.guild.icon:
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.add_field(name=get_lang(ctx.guild.id, 'settingsTitle'), value=get_lang(ctx.guild.id, 'settingsValue').format(
- settings.get('prefix', func.bot_prefix),
+ settings.get('prefix', func.settings.bot_prefix),
settings.get('lang', 'EN'),
settings.get('controller', True),
f"<@&{settings['dj']}>" if 'dj' in settings else '`None`',
@@ -156,7 +156,7 @@ class Admin(commands.Cog, name="settings"):
)
embed.add_field(name=get_lang(ctx.guild.id, 'settingsTitle2'), value=get_lang(ctx.guild.id, 'settingsValue2').format(
settings.get("queueType", "Queue"),
- func.max_queue,
+ func.settings.max_queue,
settings.get("duplicateTrack", True)
)
)
@@ -219,7 +219,7 @@ class Admin(commands.Cog, name="settings"):
@app_commands.command(name="debug")
async def debug(self, interaction: discord.Interaction):
- if interaction.user.id not in func.bot_access_user:
+ if interaction.user.id not in func.settings.bot_access_user:
return await interaction.response.send_message("You are not able to use this command!")
def clear_code(content):
diff --git a/cogs/basic.py b/cogs/basic.py
index 80c0cb3..0dd87b5 100644
--- a/cogs/basic.py
+++ b/cogs/basic.py
@@ -11,7 +11,7 @@ from function import (
youtube_api_key,
requests_api,
get_lang,
- embed_color,
+ settings,
cooldown_check,
get_aliases,
connect_channel
@@ -36,7 +36,7 @@ async def nowplay(ctx: commands.Context, player: voicelink.Player):
upnext = "\n".join(f"`{index}.` `[{track.formatLength}]` [{track.title[:30]}]({track.uri})" for index, track in enumerate(
player.queue.tracks()[:2], start=2))
embed = discord.Embed(description=player.get_msg(
- 'nowplayingDesc').format(track.title), color=embed_color)
+ 'nowplayingDesc').format(track.title), color=settings.embed_color)
embed.set_author(name=track.requester if track.requester else ctx.bot,
icon_url=track.requester.display_avatar.url if track.requester else ctx.me.display_avatar.url)
@@ -187,7 +187,7 @@ class Basic(commands.Cog):
query_track = "\n".join(
f"`{index}.` `[{track.formatLength}]` **{track.title[:35]}**" for index, track in enumerate(tracks[0:10], start=1))
embed = discord.Embed(title=player.get_msg('searchTitle').format(query), description=player.get_msg(
- 'searchDesc').format(emoji_source(platform), platform, len(tracks[0:10]), query_track), color=embed_color)
+ 'searchDesc').format(emoji_source(platform), platform, len(tracks[0:10]), query_track), color=settings.embed_color)
view = SearchView(tracks=tracks[0:10], lang=player.lang)
message = await ctx.send(embed=embed, view=view, ephemeral=True)
view.response = message
@@ -759,7 +759,7 @@ class Basic(commands.Cog):
async def ping(self, ctx: commands.Context):
"Test if the bot is alive, and see the delay between your commands and my response."
player: voicelink.Player = ctx.guild.voice_client
- embed = discord.Embed(color=embed_color)
+ embed = discord.Embed(color=settings.embed_color)
embed.add_field(name=get_lang(ctx.guild.id, 'pingTitle1'), value=get_lang(ctx.guild.id, 'pingfield1').format(
"0", "0", self.bot.latency, '😭' if self.bot.latency > 5 else ('😨' if self.bot.latency > 1 else '👌'), "St Louis, MO, United States"))
if player:
diff --git a/cogs/listener.py b/cogs/listeners.py
similarity index 98%
rename from cogs/listener.py
rename to cogs/listeners.py
index 43c33f5..71cd510 100644
--- a/cogs/listener.py
+++ b/cogs/listeners.py
@@ -19,7 +19,7 @@ class Nodes(commands.Cog):
async def start_nodes(self) -> None:
"""Connect and intiate nodes."""
await self.bot.wait_until_ready()
- for n in func.nodes.values():
+ for n in func.settings.nodes.values():
try:
await self.voicelink.create_node(bot=self.bot,
spotify_client_id=getenv('SPOTIFY_CLIENT_ID'),
diff --git a/cogs/playlist.py b/cogs/playlist.py
index d027c72..e10571b 100644
--- a/cogs/playlist.py
+++ b/cogs/playlist.py
@@ -13,7 +13,7 @@ from function import (
update_inbox,
get_lang,
playlist_name,
- embed_color,
+ settings,
get_aliases,
cooldown_check,
connect_channel
@@ -179,7 +179,7 @@ class Playlists(commands.Cog, name="playlist"):
embed = discord.Embed(title=get_lang(ctx.guild.id, 'playlistViewTitle').format(ctx.author.name),
description='```%0s %4s %10s %10s %10s\n' % tuple(get_lang(ctx.guild.id, 'playlistViewHeaders')) + '\n'.join(
'%0s %3s. %10s %10s %10s' % (info['emoji'], info['id'], f"[{info['time']}]", info['name'], len(info['tracks'])) for info in results) + '```',
- color=embed_color)
+ color=settings.embed_color)
embed.add_field(name=get_lang(ctx.guild.id, 'playlistMaxP'),
value=f"➥ {len(user)}/{max_p}", inline=True)
embed.add_field(name=get_lang(ctx.guild.id,
diff --git a/function.py b/function.py
index 351a140..556b329 100644
--- a/function.py
+++ b/function.py
@@ -11,6 +11,7 @@ from time import strptime
from io import BytesIO
from pymongo import MongoClient
from typing import Optional
+from addons import Settings
root_dir = os.path.dirname(os.path.abspath(__file__))
@@ -36,30 +37,18 @@ Playlist = mongodb[MONGODB_NAME]['Playlist']
youtube_api_key = os.getenv('YOUTUBE_API_KEY')
#--------------- Cache Var ---------------
-invite_link = "https://discord.gg/wRCgB7vBQv" #Template of invite link
-embed_color = None
-
try:
report_channel_id = int(os.getenv("BUG_REPORT_CHANNEL_ID"))
except:
report_channel_id = 0
-emoji_source_raw = {} #Stores all source emoji for track
+settings: Settings
error_log = {} #Stores error that not a Voicelink Exception
-bot_access_user = [] #Stores bot access user id
langs = {} #Stores all the languages in ./langs
guild_settings = {} #Cache guild language
local_langs = {} #Stores all the localization languages in ./local_langs
playlist_name = {} #Cache the user's playlist name
-bot_prefix = "?" #The default bot prefix
-max_queue = 1000 #The default maximum number of tracks in the queue
-cooldowns_settings = {} #Stores command cooldown settings
-aliases_settings = {} #Stores command aliases settings
-controller_settings = {} #Stores the layout of music controller
-enable_ipc = False #Enable ipc server or not
-#----------------- Nodes -----------------
-nodes = {}
#-------------- Vocard Functions --------------
def get_settings(guild_id:int):
@@ -86,38 +75,12 @@ def update_settings(guild_id:int, data: dict, mode="Set"):
collection.update_one({"_id":guild_id}, {"$unset":data})
return
-def langs_setup():
- for language in os.listdir(os.path.join(root_dir, "langs")):
- if language.endswith('.json'):
- with open(os.path.join(root_dir, "langs", language), encoding="utf8") as json_file:
- lang = json.load(json_file)
-
- langs[language[:-5]] = lang
-
- for language in os.listdir(os.path.join(root_dir, "local_langs")):
- if language.endswith('.json'):
- with open(os.path.join(root_dir, "local_langs", language), encoding="utf8") as json_file:
- lang = json.load(json_file)
-
- local_langs[language[:-5]] = lang
- return
-
-def settings_setup():
- with open(os.path.join(root_dir, "settings.json"), encoding="utf8") as json_file:
- rawSettings = json.load(json_file)
-
- global nodes, max_queue, bot_prefix, embed_color, bot_access_user, emoji_source_raw, cooldowns_settings, aliases_settings, controller_settings, enable_ipc
- nodes = rawSettings.get("nodes", {})
- if (new_max_queue := rawSettings.get("default_max_queue", max_queue)):
- max_queue = new_max_queue
- bot_prefix = rawSettings.get("prefix", "")
- enable_ipc = rawSettings.get("enable_ipc", False)
- embed_color = int(rawSettings.get("embed_color", "0xb3b3b3"), 16)
- bot_access_user = rawSettings.get("bot_access_user", [])
- emoji_source_raw = rawSettings.get("emoji_source_raw", {})
- cooldowns_settings = rawSettings.get("cooldowns", {})
- aliases_settings = rawSettings.get("aliases", {})
- controller_settings = rawSettings.get("controller", [["back", "resume", "skip", {"stop": "red"}, "add"], ["tracks"]])
+def open_json(path: str):
+ try:
+ with open(path, encoding="utf8") as json_file:
+ return json.load(json_file)
+ except:
+ return {}
def get_lang(guild_id:int, key:str):
lang = get_settings(guild_id).get("lang", "EN")
@@ -131,12 +94,30 @@ async def requests_api(url: str):
return await resp.json(encoding="utf-8")
+def init():
+ global settings
+
+ json = open_json(os.path.join(root_dir, "settings.json"))
+ if json:
+ settings = Settings(json)
+
+def langs_setup():
+ for language in os.listdir(os.path.join(root_dir, "langs")):
+ if language.endswith('.json'):
+ langs[language[:-5]] = open_json(os.path.join(root_dir, "langs", language))
+
+ 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(root_dir, "local_langs", language))
+
+ return
+
async def create_account(ctx: commands.Context):
if not ctx.author:
return
from views import CreateView
view = CreateView()
- embed=discord.Embed(title="Do you want to create an account on Vocard?", color=embed_color)
+ embed=discord.Embed(title="Do you want to create an account on Vocard?", color=settings.embed_color)
embed.description = f"> Plan: `Default` | `5` Playlist | `500` tracks in each playlist."
embed.add_field(name="Terms of Service:", value=" ➥ We assure you that all your data on Vocard will not be disclosed to any third party\n"
" ➥ We will not perform any data analysis on your data\n"
@@ -262,7 +243,7 @@ def formatTime(number:str) -> Optional[int]:
return (int(num.tm_hour) * 3600 + int(num.tm_min) * 60 + int(num.tm_sec)) * 1000
def emoji_source(emoji:str):
- return emoji_source_raw.get(emoji.lower(), "🔗")
+ return settings.emoji_source_raw.get(emoji.lower(), "🔗")
def gen_report() -> Optional[discord.File]:
if error_log:
@@ -278,12 +259,12 @@ def gen_report() -> Optional[discord.File]:
return None
def cooldown_check(ctx: commands.Context) -> Optional[commands.Cooldown]:
- if ctx.author.id in bot_access_user:
+ if ctx.author.id in settings.bot_access_user:
return None
- cooldown = cooldowns_settings.get(f"{ctx.command.parent.qualified_name} {ctx.command.name}" if ctx.command.parent else ctx.command.name)
+ cooldown = settings.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 aliases_settings.get(name, [])
+ return settings.aliases_settings.get(name, [])
diff --git a/main.py b/main.py
index bf11c87..b927b40 100644
--- a/main.py
+++ b/main.py
@@ -12,7 +12,7 @@ from datetime import datetime
from voicelink import VoicelinkException
load_dotenv()
-func.settings_setup()
+func.init()
class Translator(discord.app_commands.Translator):
async def load(self):
@@ -30,7 +30,12 @@ class Vocard(commands.Bot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.ipc = IPCServer(self, sercet_key="Vocard")
+ self.ipc = IPCServer(
+ self,
+ func.settings.ipc_server["host"],
+ func.settings.ipc_server["port"],
+ sercet_key="Vocard"
+ )
async def on_message(self, message: discord.Message, /) -> None:
if message.author.bot or not message.guild:
@@ -51,7 +56,7 @@ class Vocard(commands.Bot):
except Exception as e:
print(traceback.format_exc())
- if func.enable_ipc:
+ if func.settings.ipc_server["enable"]:
await self.ipc.start()
await bot.tree.set_translator(Translator())
@@ -101,11 +106,11 @@ class CommandCheck(discord.app_commands.CommandTree):
async def get_prefix(bot, message: discord.Message):
settings = func.get_settings(message.guild.id)
- return settings.get("prefix", func.bot_prefix)
+ return settings.get("prefix", func.settings.bot_prefix)
intents = discord.Intents.default()
intents.members = True
-intents.message_content = True if func.bot_prefix else False
+intents.message_content = True if func.settings.bot_prefix else False
member_cache = discord.MemberCacheFlags(
voice=True,
joined=False
@@ -128,7 +133,7 @@ async def app_command_error(interaction: discord.Interaction, error):
elif isinstance(error, (discord.app_commands.CommandOnCooldown, discord.app_commands.errors.MissingPermissions)):
pass
elif not issubclass(error.__class__, VoicelinkException):
- error = func.get_lang(interaction.guild_id, "unknownException") + func.invite_link
+ error = func.get_lang(interaction.guild_id, "unknownException") + func.settings.invite_link
if (guildId := interaction.guild_id) not in func.error_log:
func.error_log[guildId] = {}
func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
diff --git a/settings Example.json b/settings Example.json
index 286f234..bbc1608 100644
--- a/settings Example.json
+++ b/settings Example.json
@@ -10,9 +10,13 @@
},
"prefix": "?",
"bot_access_user": [],
- "enable_ipc": false,
"color_code":"0xb3b3b3",
"default_max_queue": 1000,
+ "ipc_server": {
+ "host": "127.0.0.1",
+ "port": 8000,
+ "enable": false
+ },
"emoji_source_raw": {
"youtube": "<:youtube:826661982760992778>",
"youtube music": "<:youtube:826661982760992778>",
diff --git a/views/controller.py b/views/controller.py
index ff30cb7..82709d7 100644
--- a/views/controller.py
+++ b/views/controller.py
@@ -301,7 +301,7 @@ class InteractiveController(discord.ui.View):
super().__init__(timeout=None)
self.player = player
- for row, btnRow in enumerate(func.controller_settings):
+ for row, btnRow in enumerate(func.settings.controller_settings):
for btn in btnRow:
color = ""
if isinstance(btn, Dict):
@@ -320,7 +320,7 @@ class InteractiveController(discord.ui.View):
await interaction.response.send_message(self.player.get_msg("nodeReconnect"), ephemeral=True)
return False
- if interaction.user.id in func.bot_access_user:
+ if interaction.user.id in func.settings.bot_access_user:
return True
if self.player.channel and self.player.is_user_join(interaction.user):
diff --git a/views/help.py b/views/help.py
index 0a94774..506fde1 100644
--- a/views/help.py
+++ b/views/help.py
@@ -58,7 +58,7 @@ class HelpView(discord.ui.View):
self.response = None
self.categorys = [ name.capitalize() for name, cog in bot.cogs.items() if len([c for c in cog.walk_commands()]) ]
- self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.invite_link))
+ self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.settings.invite_link))
self.add_item(discord.ui.Button(label='Invite', emoji=':invite:915152589056790589', url='https://discord.com/oauth2/authorize?client_id={}&permissions=2184260928&scope=bot%20applications.commands'.format(getenv('CLIENT_ID'))))
self.add_item(discord.ui.Button(label='Github', url='https://github.com/ChocoMeow/Vocard'))
self.add_item(discord.ui.Button(label='Donate', emoji=':patreon:913397909024800878', url='https://www.patreon.com/Vocard'))
@@ -84,7 +84,7 @@ class HelpView(discord.ui.View):
def build_embed(self, category: str):
category = category.lower()
if category == "news":
- embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=func.embed_color)
+ embed = discord.Embed(title="Vocard Help Menu", url="https://discord.com/channels/811542332678996008/811909963718459392/1069971173116481636", color=func.settings.embed_color)
embed.add_field(name=f"Available Categories: [{2 + len(self.categorys)}]",
value="```py\n👉 News\n2. Tutorial\n{}```".format("".join(f"{i}. {c}\n" for i, c in enumerate(self.categorys, start=3))),
@@ -101,7 +101,7 @@ class HelpView(discord.ui.View):
return embed
- embed = discord.Embed(title=f"Category: {category.capitalize()}", color=func.embed_color)
+ embed = discord.Embed(title=f"Category: {category.capitalize()}", color=func.settings.embed_color)
embed.add_field(name=f"Categories: [{2 + len(self.categorys)}]", value="```py\n" + "\n".join(("👉 " if c == category.capitalize() else f"{i}. ") + c for i, c in enumerate(['News', 'Tutorial'] + self.categorys, start=1)) + "```", inline=True)
if category == 'tutorial':
diff --git a/views/inbox.py b/views/inbox.py
index e169e9f..68597ff 100644
--- a/views/inbox.py
+++ b/views/inbox.py
@@ -51,7 +51,7 @@ class InboxView(discord.ui.View):
def build_embed(self):
embed=discord.Embed(title=f"📭 All {self.author}'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.embed_color)
+ color=func.settings.embed_color)
if self.current:
embed.add_field(name="Message Info:", value=f"```{self.current['description']}\nSender ID: {self.current['sender']}\nPlaylist ID: {self.current['referId']}\nInvite Time: {self.current['time'].strftime('%d-%m %H:%M:%S')}```")
return embed
diff --git a/views/list.py b/views/list.py
index 5a4d623..cbc582a 100644
--- a/views/list.py
+++ b/views/list.py
@@ -64,7 +64,7 @@ class ListView(discord.ui.View):
offset = self.current_page * 7
tracks = self.tracks[(offset-7):offset]
- embed = discord.Embed(title=self.player.get_msg('viewTitle'), color=func.embed_color)
+ embed = discord.Embed(title=self.player.get_msg('viewTitle'), color=func.settings.embed_color)
embed.description=self.player.get_msg('viewDesc').format(self.player.current.uri, f"```{self.player.current.title}```") if self.player.current else self.player.get_msg('nowplayingDesc').format("None")
queueText = ""
for index, track in enumerate(tracks, start=offset - 6):
diff --git a/views/lyrics.py b/views/lyrics.py
index c374be9..26fbe3c 100644
--- a/views/lyrics.py
+++ b/views/lyrics.py
@@ -72,7 +72,7 @@ class LyricsView(discord.ui.View):
def build_embed(self):
chunk = self.source.get(self.lang)[self.current_page - 1]
- embed=discord.Embed(description=chunk, color=func.embed_color)
+ 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
diff --git a/views/playlist.py b/views/playlist.py
index f749891..e4ab7df 100644
--- a/views/playlist.py
+++ b/views/playlist.py
@@ -88,7 +88,7 @@ class PlaylistView(discord.ui.View):
offset = self.current_page * 7
tracks = self.current['tracks'][(offset-7):offset]
- embed = discord.Embed(title=func.get_lang(self.guildID, 'playlistView'), color=func.embed_color)
+ embed = discord.Embed(title=func.get_lang(self.guildID, 'playlistView'), color=func.settings.embed_color)
embed.description= func.get_lang(self.guildID, 'playlistViewDesc').format(self.current['name'], self.current['id'], len(self.current['tracks']), self.current.get('owner', "None"), self.current['type'])
@@ -163,7 +163,7 @@ class CreateView(discord.ui.View):
self.value = None
self.response = None
self.add_item(agree())
- self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.invite_link))
+ self.add_item(discord.ui.Button(label='Support', emoji=':support:915152950471581696', url=func.settings.invite_link))
async def on_timeout(self):
for child in self.children:
diff --git a/voicelink/player.py b/voicelink/player.py
index a75e4c2..048684b 100644
--- a/voicelink/player.py
+++ b/voicelink/player.py
@@ -88,7 +88,7 @@ class Player(VoiceProtocol):
self.joinTime = round(time.time())
self._volume = self.settings.get('volume', 100)
self.lang = self.settings.get('lang', 'EN') if self.settings.get('lang', 'EN') in func.langs else "EN"
- self.queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.max_queue), self.settings.get("duplicateTrack", True), self.get_msg)
+ self.queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg)
self._node = NodePool.get_node()
self._current: Track = None
@@ -216,7 +216,7 @@ class Player(VoiceProtocol):
return True
def is_privileged(self, user: Member, check_user_join: bool = True):
- if user.id in func.bot_access_user:
+ if user.id in func.settings.bot_access_user:
return True
manage_perm = user.guild_permissions.manage_guild
@@ -360,18 +360,18 @@ class Player(VoiceProtocol):
track = self.current
if not track:
- embed=Embed(title=self.get_msg("noTrackPlaying"), description=f"[Vote](https://top.gg/bot/605618911471468554/vote/) | [Support]({func.invite_link}) | [Invite](https://discord.com/oauth2/authorize?client_id=605618911471468554&permissions=2184260928&scope=bot%20applications.commands) | [Questionnaire](https://forms.gle/UqeeEv4GEdCq9hi3A)", color=func.embed_color)
+ embed=Embed(title=self.get_msg("noTrackPlaying"), description=f"[Vote](https://top.gg/bot/605618911471468554/vote/) | [Support]({func.settings.invite_link}) | [Invite](https://discord.com/oauth2/authorize?client_id=605618911471468554&permissions=2184260928&scope=bot%20applications.commands) | [Questionnaire](https://forms.gle/UqeeEv4GEdCq9hi3A)", color=func.settings.embed_color)
embed.set_image(url='https://i.imgur.com/dIFBwU7.png')
else:
try:
- embed = Embed(color=func.embed_color)
+ embed = Embed(color=func.settings.embed_color)
embed.set_author(name=self.get_msg("playerAuthor").format(self.channel.name), icon_url=self.client.user.avatar.url)
embed.description = self.get_msg("playerDesc").format(track.title, track.uri, (track.requester.mention if track.requester else "<@605618911471468554>"), (f"<@&{self.settings['dj']}>" if self.settings.get('dj') else f"{self.dj.mention}"))
embed.set_image(url=track.thumbnail if track.thumbnail else "https://cdn.discordapp.com/attachments/674788144931012638/823086668445384704/eq-dribbble.gif")
embed.set_footer(text=self.get_msg("playerFooter").format(self.queue.count, (self.get_msg("live") if track.is_stream else func.time(track.length)), self.volume, self.get_msg("playerFooter2").format(self.queue.repeat.capitalize()) if self.queue._repeat else ""))
except:
- embed = Embed(description=self.get_msg("missingTrackInfo"), color=func.embed_color)
+ embed = Embed(description=self.get_msg("missingTrackInfo"), color=func.settings.embed_color)
return embed
async def is_position_fresh(self):
From cff5ae484758863879908d607382aaef1cf43d3c Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sun, 19 Mar 2023 16:47:43 +0800
Subject: [PATCH 08/40] Update main.py
---
main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main.py b/main.py
index b927b40..f09181d 100644
--- a/main.py
+++ b/main.py
@@ -85,7 +85,7 @@ class Vocard(commands.Bot):
error = f"```css\n[You are missing argument!]\n{command}\n" + " " * position + "^" * len(ctx.current_parameter.name) + "```"
elif not issubclass(error.__class__, VoicelinkException):
- error = func.get_lang(ctx.guild.id, "unknownException") + func.invite_link
+ error = func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
if (guildId := ctx.guild.id) not in func.error_log:
func.error_log[guildId] = {}
func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
From 4ec64668682059f725c2ed40fd7dfe134fca4c5c Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sun, 19 Mar 2023 18:16:09 +0800
Subject: [PATCH 09/40] Fixed chapter view
---
views/chapter.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/views/chapter.py b/views/chapter.py
index 93b2caa..325baa6 100644
--- a/views/chapter.py
+++ b/views/chapter.py
@@ -41,7 +41,7 @@ class Dropdown(discord.ui.Select):
super().__init__(
placeholder=self.player.get_msg('chaptersDropdown'),
min_values=1, max_values=1,
- options=options,
+ options=options[:25],
)
async def callback(self, interaction: discord.Interaction):
@@ -52,6 +52,7 @@ class Dropdown(discord.ui.Select):
return await self.view.stop_view()
await self.player.seek(position)
+ await interaction.response.send_message(self.player.get_msg('seek').format(formatTime(position)))
class ChapterView(discord.ui.View):
def __init__(self, player, chapters, author):
@@ -61,7 +62,7 @@ class ChapterView(discord.ui.View):
self.chapters = chapters
self.author = author
self.response = None
- self.add_item(self.Dropdown(player, chapters))
+ self.add_item(Dropdown(player, chapters))
async def on_error(self, error: Exception, item, interaction) -> None:
return
From c94c65e404ee7383aead1dad08b3ff4082859397 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Mon, 20 Mar 2023 10:39:35 +0800
Subject: [PATCH 10/40] Fix error_log
---
main.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/main.py b/main.py
index f09181d..20b10e7 100644
--- a/main.py
+++ b/main.py
@@ -88,7 +88,7 @@ class Vocard(commands.Bot):
error = func.get_lang(ctx.guild.id, "unknownException") + func.settings.invite_link
if (guildId := ctx.guild.id) not in func.error_log:
func.error_log[guildId] = {}
- func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
+ func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
try:
return await ctx.reply(error, ephemeral=True)
@@ -126,8 +126,8 @@ bot = Vocard(command_prefix=get_prefix,
intents=intents)
@bot.tree.error
-async def app_command_error(interaction: discord.Interaction, error):
- error = getattr(error, 'original', error)
+async def app_command_error(interaction: discord.Interaction, exception):
+ error = getattr(exception, 'original', exception)
if isinstance(error, (discord.errors.NotFound, aiohttp.client_exceptions.ClientOSError)):
return
elif isinstance(error, (discord.app_commands.CommandOnCooldown, discord.app_commands.errors.MissingPermissions)):
@@ -136,7 +136,7 @@ async def app_command_error(interaction: discord.Interaction, error):
error = func.get_lang(interaction.guild_id, "unknownException") + func.settings.invite_link
if (guildId := interaction.guild_id) not in func.error_log:
func.error_log[guildId] = {}
- func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc())
+ func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
try:
if interaction.response.is_done():
return await interaction.followup.send(error, ephemeral=True)
From 08985db659e4abc61076b01d9032caaf83fcdbcb Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Mon, 20 Mar 2023 11:01:21 +0800
Subject: [PATCH 11/40] Fix join listeners
---
cogs/listeners.py | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/cogs/listeners.py b/cogs/listeners.py
index 71cd510..6d8879e 100644
--- a/cogs/listeners.py
+++ b/cogs/listeners.py
@@ -51,16 +51,26 @@ class Nodes(commands.Cog):
if member.bot:
return
- is_joined = True
- if before.channel and after.channel:
- guild = after.channel.guild.id
+ if before.channel == after.channel:
+ return
- elif not before.channel and after.channel:
- guild = after.channel.guild.id
+ player: voicelink.Player = member.guild.voice_client
+ if not player:
+ return
+
+ guild = member.guild.id
+ is_joined = True
+
+ if not before.channel and after.channel:
+ if after.channel.id != player.channel.id:
+ return
elif before.channel and not after.channel:
- guild = before.channel.guild.id
is_joined = False
+
+ elif before.channel and after.channel:
+ if after.channel.id != player.channel.id:
+ is_joined = False
await self.bot.ipc.send({
"op": "updateGuild",
From 8790438e41bf2d264f76dd0239c1281a3ad74d67 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Tue, 21 Mar 2023 23:49:04 +0800
Subject: [PATCH 12/40] Rewrite set_repeat function and clean code
---
cogs/basic.py | 6 ++---
views/controller.py | 13 +++-------
voicelink/player.py | 54 ++++++++++++++++++++++++++++-------------
voicelink/queue.py | 34 ++++++++++++--------------
web/ipc/methods.py | 11 +++------
web/static/js/action.js | 6 ++---
6 files changed, 66 insertions(+), 58 deletions(-)
diff --git a/cogs/basic.py b/cogs/basic.py
index 0dd87b5..3f4b9f8 100644
--- a/cogs/basic.py
+++ b/cogs/basic.py
@@ -318,7 +318,7 @@ class Basic(commands.Cog):
await ctx.send(player.get_msg('skipped').format(ctx.author))
if player.queue._repeat == 1:
- player.queue.set_repeat("off")
+ await player.queue.set_repeat("off")
await player.stop()
@commands.hybrid_command(name="back", aliases=get_aliases("back"))
@@ -353,7 +353,7 @@ class Basic(commands.Cog):
await ctx.send(player.get_msg('backed').format(ctx.author))
if player.queue._repeat == 1:
- player.queue.set_repeat("off")
+ await player.queue.set_repeat("off")
@commands.hybrid_command(name="seek", aliases=get_aliases("seek"))
@app_commands.describe(position="Input position. Exmaple: 1:20.")
@@ -465,7 +465,7 @@ class Basic(commands.Cog):
if mode.lower() not in ['off', 'track', 'queue']:
mode = "off"
- player.queue.set_repeat(mode)
+ await player.set_repeat(mode)
await ctx.send(player.get_msg('repeat').format(mode.capitalize()))
@commands.hybrid_command(name="clear", aliases=get_aliases("clear"))
diff --git a/views/controller.py b/views/controller.py
index 82709d7..6a3d080 100644
--- a/views/controller.py
+++ b/views/controller.py
@@ -63,7 +63,7 @@ class Back(discord.ui.Button):
await interaction.response.send_message(self.player.get_msg("backed").format(interaction.user))
if self.player.queue._repeat == 1:
- self.player.queue.set_repeat("off")
+ await self.player.set_repeat("off")
class Resume(discord.ui.Button):
def __init__(self, player, style, row):
@@ -127,7 +127,7 @@ class Skip(discord.ui.Button):
await interaction.response.send_message(self.player.get_msg("skipped").format(interaction.user))
if self.player.queue._repeat == 1:
- self.player.queue.set_repeat("off")
+ await self.player.set_repeat("off")
await self.player.stop()
class Stop(discord.ui.Button):
@@ -182,20 +182,13 @@ class Add(discord.ui.Button):
class Loop(discord.ui.Button):
def __init__(self, player, style, row):
self.player = player
- self.loopType = {
- 0: "off",
- 1: "track",
- 2: "queue",
- }
super().__init__(emoji="🔁", label=player.get_msg('buttonLoop'), style=style, row=row)
async def callback(self, interaction: discord.Interaction):
if not self.player.is_privileged(interaction.user):
return await interaction.response.send_message(self.player.get_msg('missingPerms_mode'), ephemeral=True)
- current_repeat = self.player.queue._repeat
- mode = self.loopType.get((current_repeat + 1)%len(self.loopType), 'off')
- self.player.queue.set_repeat(mode)
+ mode = await self.player.set_repeat()
await interaction.response.send_message(self.player.get_msg('repeat').format(mode.capitalize()))
class VolumeUp(discord.ui.Button):
diff --git a/voicelink/player.py b/voicelink/player.py
index 048684b..38a1640 100644
--- a/voicelink/player.py
+++ b/voicelink/player.py
@@ -43,7 +43,8 @@ from discord import (
StageChannel,
Member,
Embed,
- ui
+ ui,
+ Message
)
from discord.ext import commands
@@ -85,27 +86,27 @@ class Player(VoiceProtocol):
self._guild = channel.guild if channel else None
self.settings: dict = func.get_settings(ctx.guild.id)
- self.joinTime = round(time.time())
- self._volume = self.settings.get('volume', 100)
- self.lang = self.settings.get('lang', 'EN') if self.settings.get('lang', 'EN') in func.langs else "EN"
- self.queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg)
+ self.joinTime: float = round(time.time())
+ self._volume: int = self.settings.get('volume', 100)
+ self.lang: dict = self.settings.get('lang', 'EN') if self.settings.get('lang', 'EN') in func.langs else "EN"
+ self.queue: Queue = eval(self.settings.get("queueType", "Queue"))(self.settings.get("maxQueue", func.settings.max_queue), self.settings.get("duplicateTrack", True), self.get_msg)
self._node = NodePool.get_node()
self._current: Track = None
self._filters: Filters = Filters()
- self._paused = False
- self._is_connected = False
- self._ping = 0.0
+ self._paused: bool = False
+ self._is_connected: bool = False
+ self._ping: float = 0.0
- self._position = 0
- self._last_position = 0
- self._last_update = 0
+ self._position: int = 0
+ self._last_position: int = 0
+ self._last_update: int = 0
self._ending_track: Optional[Track] = None
- self._voice_state = {}
+ self._voice_state: dict = {}
- self.controller = None
- self.updating = False
+ self.controller: Message = None
+ self.updating: bool = False
self.pause_votes = set()
self.resume_votes = set()
@@ -567,9 +568,7 @@ class Player(VoiceProtocol):
async def seek(self, position: float, requester: Member = None) -> float:
"""Seeks to a position in the currently playing track milliseconds"""
if position < 0 or position > self._current.original.length:
- raise TrackInvalidPosition(
- "Seek position must be between 0 and the track length"
- )
+ raise TrackInvalidPosition("Seek position must be between 0 and the track length")
await self._node.send(method=0, guild_id=self._guild.id, data={"position": position})
if self.bot.ipc.connections:
@@ -610,6 +609,27 @@ class Player(VoiceProtocol):
}
}, requester)
+ async def set_repeat(self, mode:str = None):
+ if not mode:
+ mode = self.queue._repeat_mode.get((self.queue._repeat + 1)%len(self.queue._repeat_mode), 'off')
+
+ is_found = False
+ for i, m in self.queue._repeat_mode.items():
+ if m == mode.lower():
+ is_found = True
+ self.queue._repeat = i
+ if i == 2:
+ self._repeat_position = self._position - 1
+ break
+
+ if not is_found:
+ raise VoicelinkException("Invalid repeat mode.")
+
+ if self.bot.ipc.connections:
+ await self.send_ws({"op": "repeatTrack", "repeatMode": mode})
+
+ return mode
+
async def add_filter(self, filter: Filter, fast_apply=False) -> Filters:
try:
self._filters.add_filter(filter=filter)
diff --git a/voicelink/queue.py b/voicelink/queue.py
index 20ff437..7b8800d 100644
--- a/voicelink/queue.py
+++ b/voicelink/queue.py
@@ -26,13 +26,20 @@ from .objects import Track
from discord import Member
class Queue:
- def __init__(self, size:int, duplicateTrack: bool, get_msg):
+ def __init__(self, size:int, duplicate_track: bool, get_msg):
self._queue = []
self._position = 0
self._size = size
self._repeat = 0
self._repeat_position = 0
- self._duplicateTrack = duplicateTrack
+ self._duplicate_track = duplicate_track
+
+ self._repeat_mode = {
+ 0: "off",
+ 1: "track",
+ 2: "queue",
+ }
+
self.get_msg = get_msg
def get(self):
@@ -55,7 +62,7 @@ class Queue:
if self.count >= self._size:
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
- if not self._duplicateTrack:
+ if not self._duplicate_track:
if item.uri in [track.uri for track in self._queue]:
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
@@ -66,7 +73,7 @@ class Queue:
if self.count >= self._size:
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
- if not self._duplicateTrack:
+ if not self._duplicate_track:
if item.uri in [track.uri for track in self._queue]:
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
@@ -77,7 +84,7 @@ class Queue:
if self.count >= self._size:
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
- if not self._duplicateTrack:
+ if not self._duplicate_track:
if item.uri in [track.uri for track in self._queue]:
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
@@ -94,15 +101,6 @@ class Queue:
raise OutofList(self.get_msg("voicelinkOutofList"))
else:
self._position -= index
-
- def set_repeat(self, mode:str):
- if mode == 'track':
- self._repeat = 1
- elif mode == 'queue':
- self._repeat = 2
- self._repeat_position = self._position - 1
- else:
- self._repeat = 0
def history_clear(self, is_playing: bool):
self._queue[:self._position - 1 if is_playing else self._position] = []
@@ -173,7 +171,7 @@ class Queue:
@property
def repeat(self):
- return "Off" if self._repeat == 0 else ("Track" if self._repeat == 1 else "Queue")
+ return self._repeat_mode.get(self._repeat, "Off").capitalize()
@property
def is_empty(self):
@@ -184,15 +182,15 @@ class Queue:
return False
class FairQueue(Queue):
- def __init__(self, size:int, duplicateTrack: bool, get_msg):
- super().__init__(size, duplicateTrack, get_msg)
+ def __init__(self, size:int, duplicate_track: bool, get_msg):
+ super().__init__(size, duplicate_track, get_msg)
self._set = set()
async def put(self, item: Track) -> int:
if len(self._queue) >= self._size:
raise QueueFull(self.get_msg("voicelinkQueueFull").format(self._size))
- if not self._duplicateTrack:
+ if not self._duplicate_track:
if item.uri in [track.uri for track in self._queue]:
raise DuplicateTrack(self.get_msg("voicelinkDuplicateTrack"))
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index c8c7026..ad69f9e 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -35,7 +35,7 @@ async def skipTo(player, member: Member, data: dict):
player.queue.skipto(index)
if player.queue._repeat == 1:
- player.queue.set_repeat("off")
+ await player.set_repeat("off")
await player.stop()
async def backTo(player, member: Member, data: dict):
@@ -105,14 +105,11 @@ async def shuffleTrack(player, member: Member, data: dict):
await player.shuffle(data.get("type", "queue"), member)
-async def updateRepeatMode(player, member: Member, data: dict):
+async def repeatTrack(player, member: Member, data: dict):
if not player.is_privileged(member):
return missingPermission(member.id)
- mode = data.get("mode", "off")
- if mode.lower() not in ['off', 'track', 'queue']:
- mode = "off"
- player.queue.set_repeat(mode)
+ await player.set_repeat()
async def updatePause(player, member: Member, data: dict):
if not player.is_privileged(member):
@@ -140,7 +137,7 @@ methods = {
"addTracks": addTracks,
"getTracks": getTracks,
"shuffleTrack": shuffleTrack,
- "updateRepeat": updateRepeatMode,
+ "repeatTrack": repeatTrack,
"updatePause": updatePause,
"updatePosition": updatePosition,
}
diff --git a/web/static/js/action.js b/web/static/js/action.js
index aa6cac0..b17c516 100644
--- a/web/static/js/action.js
+++ b/web/static/js/action.js
@@ -5,7 +5,7 @@ $(document).ready(function () {
var typingTimer;
var doneTypingInterval = 2000;
- $('body').click(function(event) {
+ $('body').click(function (event) {
var $target = $(event.target);
var $resultList = $target.closest(".search-contrainer");
@@ -91,7 +91,7 @@ $(document).ready(function () {
player.seekTo($(this).val());
})
- $("#shuffle-button").on('click', function() {
+ $("#shuffle-button").on('click', function () {
player.shuffle();
})
-});
\ No newline at end of file
+});
From 06b3a0e12c9a26723ee485afb28f4ea086e80f3c Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Wed, 22 Mar 2023 19:21:48 +0800
Subject: [PATCH 13/40] Fix exception raised while using slash command
---
main.py | 22 ++--------------------
1 file changed, 2 insertions(+), 20 deletions(-)
diff --git a/main.py b/main.py
index 20b10e7..6c92ddd 100644
--- a/main.py
+++ b/main.py
@@ -73,6 +73,8 @@ class Vocard(commands.Bot):
async def on_command_error(self, ctx: commands.Context, exception, /) -> None:
error = getattr(exception, 'original', exception)
+ if ctx.interaction:
+ error = getattr(error, 'original', error)
if isinstance(error, (commands.CommandNotFound, aiohttp.client_exceptions.ClientOSError)):
return
@@ -125,25 +127,5 @@ bot = Vocard(command_prefix=get_prefix,
case_insensitive=True,
intents=intents)
-@bot.tree.error
-async def app_command_error(interaction: discord.Interaction, exception):
- error = getattr(exception, 'original', exception)
- if isinstance(error, (discord.errors.NotFound, aiohttp.client_exceptions.ClientOSError)):
- return
- elif isinstance(error, (discord.app_commands.CommandOnCooldown, discord.app_commands.errors.MissingPermissions)):
- pass
- elif not issubclass(error.__class__, VoicelinkException):
- error = func.get_lang(interaction.guild_id, "unknownException") + func.settings.invite_link
- if (guildId := interaction.guild_id) not in func.error_log:
- func.error_log[guildId] = {}
- func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
- try:
- if interaction.response.is_done():
- return await interaction.followup.send(error, ephemeral=True)
-
- return await interaction.response.send_message(error, ephemeral=True)
- except:
- pass
-
if __name__ == "__main__":
bot.run(os.getenv("TOKEN"), log_handler=None)
\ No newline at end of file
From 402e7040b8e1ef100b645dd30c415e2d65305fab Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Wed, 22 Mar 2023 19:23:06 +0800
Subject: [PATCH 14/40] Fix display_name is null in dashboard
---
web/webapp.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/webapp.py b/web/webapp.py
index e639ffb..f5e6baf 100644
--- a/web/webapp.py
+++ b/web/webapp.py
@@ -84,7 +84,7 @@ def message_handler(data: dict):
"user": {
"user_id": user.id,
"avatar_url": user.avatar.url,
- "name": user.display_name
+ "name": user.username
},
"is_joined": True
}, to=user.sid)
From 0f44e6afe9dd5e55372f2777f9acaa0823a0c91c Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Wed, 22 Mar 2023 19:32:42 +0800
Subject: [PATCH 15/40] Add repeat track in dashboard
---
cogs/basic.py | 2 --
web/ipc/methods.py | 6 +++---
web/static/js/action.js | 4 ++++
web/static/js/objects.js | 30 ++++++++++++++++++++++++++----
web/templates/index.html | 4 +++-
5 files changed, 36 insertions(+), 10 deletions(-)
diff --git a/cogs/basic.py b/cogs/basic.py
index 3f4b9f8..b946366 100644
--- a/cogs/basic.py
+++ b/cogs/basic.py
@@ -463,8 +463,6 @@ class Basic(commands.Cog):
if not player.is_privileged(ctx.author):
return await ctx.send(player.get_msg('missingPerms_mode'), ephemeral=True)
- if mode.lower() not in ['off', 'track', 'queue']:
- mode = "off"
await player.set_repeat(mode)
await ctx.send(player.get_msg('repeat').format(mode.capitalize()))
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index ad69f9e..2d762ea 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -20,6 +20,7 @@ async def initPlayer(player, member: Member, data: dict):
"name": member.name
} for member in player.channel.members ],
"tracks": [ track.toDict() for track in player.queue._queue ],
+ "repeat_mode": player.queue.repeat.lower(),
"current_queue_position": player.queue._position if player.is_playing else player.queue._position + 1,
"current_position": 0 or player.position if player.is_playing else 0,
"is_playing": player.is_playing,
@@ -147,13 +148,12 @@ async def process_methods(websocket, bot: commands.Bot, data: dict) -> None:
if not method:
return
- guild = None
- member = None
+ guild, member = None, None
guild_id = data.get("guild_id", None)
user_id = data.get("user_id", None)
if guild_id is None:
user = bot.get_user(user_id)
- if not user:
+ if not user:
return
for g in user.mutual_guilds:
diff --git a/web/static/js/action.js b/web/static/js/action.js
index b17c516..c6a1a43 100644
--- a/web/static/js/action.js
+++ b/web/static/js/action.js
@@ -91,6 +91,10 @@ $(document).ready(function () {
player.seekTo($(this).val());
})
+ $("#repeat-button").on('click', function () {
+ player.repeatMode();
+ })
+
$("#shuffle-button").on('click', function () {
player.shuffle();
})
diff --git a/web/static/js/objects.js b/web/static/js/objects.js
index 206d156..24f6bda 100644
--- a/web/static/js/objects.js
+++ b/web/static/js/objects.js
@@ -36,6 +36,7 @@ const actions = {
player.isDJ = data['is_dj'];
player.is_paused = data['is_paused'];
player.current_position = data['current_position'];
+ player.repeat = data['repeat_mode'];
data["users"].forEach(user => {
player.addUser(user);
})
@@ -151,7 +152,7 @@ const actions = {
player.showToast(data["requester_id"], `Moved ${element.title} to ${newPosition}`)
},
- shuffleTrack: function(player, data) {
+ shuffleTrack: function (player, data) {
var tracks = data["tracks"];
if (tracks != undefined) {
player.queue = [];
@@ -163,6 +164,11 @@ const actions = {
player.initSortable();
player.showToast(data["requester_id"], "The queue is shuffled.")
}
+ },
+
+ repeatTrack: function (player, data) {
+ player.repeat = data["repeatMode"];
+ },
}
}
@@ -188,7 +194,8 @@ class Player {
this.date = new Date();
this.queue = [];
this.users = {};
- this.searchList = []
+ this.searchList = [];
+ this.repeat = "off";
this.currentTrack = null;
this.current_queue_position = 0;
this.current_position = 0;
@@ -239,7 +246,7 @@ class Player {
}
addUser(user) {
- this.users[user['user_id']] = { "avatar_url": user["avatar_url"], "name": user["name"] };
+ this.users[user['user_id']] = { avatar_url: user["avatar_url"], name: user["name"] };
}
addTrack(tracks) {
@@ -282,6 +289,10 @@ class Player {
}
}
+ repeatMode() {
+ this.send({ "op": "repeatTrack" })
+ }
+
send(payload) {
var json = JSON.stringify(payload)
this.socket.send(json);
@@ -356,7 +367,8 @@ class Player {
$("#image").attr("src", currentTrack.imageUrl);
$("#largeImage").attr("src", currentTrack.imageUrl);
}
- var play_pause_btn = $("#play-pause-button")
+ var play_pause_btn = $("#play-pause-button");
+ var repeat_btn = $("#repeat-button");
if (this.is_paused || currentTrack == undefined) {
play_pause_btn.removeClass('fa-pause').addClass('fa-play');
if (this.timer.getIsRunning()) {
@@ -366,5 +378,15 @@ class Player {
play_pause_btn.removeClass('fa-play').addClass('fa-pause');
this.timer.start();
}
+
+ repeat_btn.removeClass("fa-repeat-1").addClass("fa-repeat")
+ if (this.repeat == "off") {
+ console.log("running");
+ repeat_btn.css('color', '');
+ } else if (this.repeat == "track") {
+ repeat_btn.css('color', '#fff');
+ } else {
+ repeat_btn.removeClass("fa-repeat").addClass("fa-repeat-1");
+ }
}
}
\ No newline at end of file
diff --git a/web/templates/index.html b/web/templates/index.html
index a6d29a4..c1d0230 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -34,7 +34,8 @@
+
From d82de6605fff81cdfbb61dac52d5dd8f18d00c89 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Wed, 22 Mar 2023 19:33:48 +0800
Subject: [PATCH 16/40] Add error message in dashboard
---
web/ipc/methods.py | 12 +++++++++---
web/static/img/error.png | Bin 0 -> 1445 bytes
web/static/img/info.png | Bin 0 -> 1670 bytes
web/static/js/objects.js | 12 +++++++++++-
4 files changed, 20 insertions(+), 4 deletions(-)
create mode 100644 web/static/img/error.png
create mode 100644 web/static/img/info.png
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index 2d762ea..2f61f2a 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -5,7 +5,7 @@ from discord import Member
from discord.ext import commands
def missingPermission(user_id:int):
- payload = {"op": "missingPermission", "msg": "Only the DJ or admins may use this funciton!"}
+ payload = {"op": "errorMsg", "level": "info", "msg": "Only the DJ or admins may use this funciton!"}
payload["user_id"] = user_id
return payload
@@ -176,5 +176,11 @@ async def process_methods(websocket, bot: commands.Bot, data: dict) -> None:
resp: dict = await method(player, member, data)
if resp:
await websocket.send(json.dumps(resp))
- except:
- return
\ No newline at end of file
+ except Exception as e:
+ payload = {
+ "op": "errorMsg",
+ "level": "info",
+ "msg": e,
+ "user_id": member.id
+ }
+ await websocket.send(json.dumps(payload))
\ No newline at end of file
diff --git a/web/static/img/error.png b/web/static/img/error.png
new file mode 100644
index 0000000000000000000000000000000000000000..1cb4e9e9f881a41897c6982d34d06ed54e15340f
GIT binary patch
literal 1445
zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7Bet#3xhBt!>l*8o|0J?9FtA#Ax;TbZFuuL(*d>!Has1(|6$b@Q8to{5
z=(MLvX;NyKn^WHcg-5)5nxxkYG@X=Wd9Sc;GjAlPqQb0eW}j6j`+eWPde{DSzp`Fc
znmMmuCVfvn*f{v>-t_+b`%$F_!osZ*)EgZX1UOij1}`P~;?~^Vcek#8S62M_p!xl?
zpKATz-tD`0tuSVP@%yW99>g%H8nm2HswvTprWSf()6XA&1K{WLiD837=p+
zek`!ZHU~&{JjrFU4A<^UT9de1)Ln6sIp>nq^6rJN5?9LtdDq3{Ppo{T;b)q&ybEZK
zcQ@aYD}^Fk+us67rN|$5_q@ORXM^Wql^;%dm=6vE%43s}EctS9$A4m(Im>AXTB2&t-r0JTHpt~*8$|X}hjs#A3(3)i>
zRCq;w!l`9lE;7p%CkR(;th(5n+x31Y3)9M+H|=w~qMqk{RBH?|2?d7mD`5}S;NyWE
zOY|B;E_i_i1c3$vW64vN$xFzhVnIq+Sn0?PC$PhdWy3XHLK;S&zUpomOlF**?Q
zSi{9=jziU~YM_!fK@a}vASHh}Q~FPf0i&gkMaZY{)UP>ZYI=>A@0QOfVm&Cb{FHtD
zmj#b=>zZ;5zt;8b%)P(&49lj4Q=ghDWi~tLW#n$2?6FLJLh;&lw!$a3uw2@@UpPSJ
za%7D{apVlmNtrrU6L#iK7CrexO!ma9mzVQ&ww_xnWjzTLpC=-|EU*7|`fuPr$FSc4
z+czo|O3c0Q=qGn_W!sx86FVNO)*pT4Cw_8e`r3Tw}aS9$cpUBUKE*OIS_w}EE;687+)J@HKH2{T}98t~;xB*>op
zA9i<5TaN6>UkV-d^K1l85BsUiaC3!7*5D27n
z5`6*#^sgjs`!?WN?Ol@(baLTnpGXi$L0!_MK)Lr}fKw_G<6r}->E1mB3?M<)&ek9h
zouaTFAOiwz(>{5^+ViT^%y19oEDqK-KRNQG059jQJ2c7%dxWc&G3
z>|#1~+mfAC>F`@JYJ23M4g6vcEBmw@G%Nx(gFgHL{0~l08+o?}K1yNaC^F!D!9OWWYOa
zMb?sRGdaq4=Fix&JSF2vyb1W>Slwg`9MFK~wu}{*x=soz(g9JZc0tu1r{D=KT?Hlg
zZ^oBGz;YywZ;y$}&^(l3=)faoi+xoFTTui4Qp19bOzpcULqzk-h`gpuN%$!v*8CB2
zx*EC&&)C&p=8HGLs!52-FU}8$uagIS@c!V34WV(h{H@IY92BuRj8aG)|1;0%XWkU?
z*a)v$Vgsjh!0V`p{i=W<;fLn8{IZ4)676=@#XUqV5{#Fx3rpohN*)?3eQQ1
zoF%*=g@MAJ54&h`qz1Qn`$tXjc`5)^ABxG>)h7CACK*)dbEpo1xXg
zK5q6K4L@D?Bv6O+s8EX!zTV)F_8Wu%IT~q-_i%OA@V?Ap%LN}dSGZB3Ii&C164LeMN?KMivPlP9$t53O6)k=uESDpk
zZOQd)Ux
z%Yqj=R6nabvma!8S5^4=wYoI==$o^qgfHHTa|9tq1zX6yzzNG98s@r~fkT3)+BI?4
z76+T5;%Lrs4L}lUu@7$b?m%Gp_tET|Q!K*t^{CE4npBD~@abiXCEn+;PnNuCZ?Wkb
z&chMTj2`y}ZqY~G$V15A-5ucxnX^bQHMX5N=bZ41qxBw=#$anh?r?R5`RO=Tsh1u}
zV;ADqc=5(83<1FuZl)Y(Oa?d9Me*r_fk8wQBKUwwv$!ZYG-^)0eO->$HtQi^ZIk82
zXy-UWF#g8St_}_bvap{DCTAc^8r!=*!goS20mjfy?EIKM)iG
zHj{Yx@uBB`Gl_CE)N_T}#wp&E{{}F%Hp2-HC5Xh5fxlop>G5~!pa>^O>9N5ADK_eR
zxQ%O`3t!pGOFUMa5(;kj8@C3r*1J@YU6zPS7V5`FKdaEKG09=}`V?lVqKh7+g|UR4
zlkVpQ?_*6j`UXqxp^`(1D`#H&t>AH%EL{%X*BRb_L1C#nCbfIfsjn-w@ygf%*B))4
z1nYGLOVhG(UD8|;=ZPOcy&ph`RMQ1#FW+lQJ&Oh{a{--DW}R97Jv+XBWp9
zi```39eyxn;iJ)ham#rEuRc2;x!M!CBVbyFgnFU%D?g+!xtq33N27zS-+ `)
$(".toastContrainer").append($element)
From 725e703072fb8c048ed6cf5dd242709c7de2b162 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Wed, 22 Mar 2023 19:35:25 +0800
Subject: [PATCH 17/40] Optimize CSS code
---
web/static/css/main.css | 49 ++++++++++++++++++-----------------------
1 file changed, 21 insertions(+), 28 deletions(-)
diff --git a/web/static/css/main.css b/web/static/css/main.css
index 517f35b..f6260b6 100644
--- a/web/static/css/main.css
+++ b/web/static/css/main.css
@@ -143,8 +143,13 @@ a {
}
@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
}
.header .center input {
@@ -297,7 +302,7 @@ a {
.main .thumbnail img {
padding: 1rem;
- width: 70%;
+ width: 80%;
height: auto;
}
@@ -407,6 +412,11 @@ a {
margin-left: .8rem;
font-size: 20;
color: gray;
+ transition: color .2s linear;
+}
+
+.player-controller .right i:hover {
+ color: #fff;
}
.main .queue-list {
@@ -419,6 +429,7 @@ a {
.track {
display: flex;
+ justify-content: space-between;
align-items: center;
cursor: pointer;
border-radius: .5rem;
@@ -430,7 +441,6 @@ a {
.track:hover {
background-color: rgba(142, 142, 142, 20%);
- /* transform: scale(1.02); */
}
.track.active {
@@ -442,7 +452,8 @@ a {
justify-content: left;
align-items: center;
margin-right: .5rem;
- flex: 1;
+ max-width: 90%;
+ overflow: hidden;
}
.track .left i {
@@ -458,14 +469,12 @@ a {
}
.track .left .info {
- display: block;
- max-width: 300px;
margin-left: .5rem;
+ overflow: hidden;
}
.track .left .info p {
text-overflow: ellipsis;
- white-space: nowrap;
overflow: hidden;
}
@@ -488,6 +497,10 @@ a {
padding: 0;
}
+ .main .toastContrainer {
+ display: none;
+ }
+
.header .center {
display: none;
}
@@ -510,12 +523,6 @@ a {
padding: 0;
}
- .main .thumbnail img {
- padding: 0;
- width: 80%;
- height: auto;
- }
-
.main .queue-list {
overflow-y: auto;
overflow-x: hidden;
@@ -525,18 +532,4 @@ a {
min-width: 100%;
}
- .track .left i {
- margin-right: .5rem;
- font-size: 15px;
- }
-
- .track .left img {
- padding: 2px;
- height: 30;
- border-radius: .2rem;
- }
-
- .track .left .info {
- max-width: 200px;
- }
}
\ No newline at end of file
From 927cfc4cd897325da11f06b0164b53930b6e233f Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 24 Mar 2023 22:26:29 +0800
Subject: [PATCH 18/40] Add response message
---
web/ipc/methods.py | 67 +++++++++++++++++++++++++++++++++++++---
web/static/js/objects.js | 14 +++++++--
2 files changed, 73 insertions(+), 8 deletions(-)
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index 2f61f2a..2c78a39 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -9,6 +9,15 @@ def missingPermission(user_id:int):
payload["user_id"] = user_id
return payload
+def error_msg(msg: str, *, user_id: int = None, guild_id: int = None, level: str = "info"):
+ payload = {"op": "errorMsg", "level": level}
+ if user_id:
+ payload["user_id"]: user_id
+ if guild_id:
+ payload["guild_id"]: guild_id
+
+ return payload
+
async def initPlayer(player, member: Member, data: dict):
return {
"op": "initPlayer",
@@ -29,8 +38,17 @@ async def initPlayer(player, member: Member, data: dict):
async def skipTo(player, member: Member, data: dict):
if not player.is_privileged(member):
- return missingPermission(member.id)
-
+ if member == player.current.requester:
+ pass
+ elif member in player.skip_votes:
+ return error_msg(player.get_msg('voted', 'EN'), user_id=member.id)
+ else:
+ player.skip_votes.add(member)
+ if len(player.skip_votes) >= (required := player.required()):
+ pass
+ else:
+ return error_msg(player.get_msg('skipVote', 'EN').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
+
index = data.get("index", 1)
if index > 1:
player.queue.skipto(index)
@@ -41,7 +59,17 @@ async def skipTo(player, member: Member, data: dict):
async def backTo(player, member: Member, data: dict):
if not player.is_privileged(member):
- return missingPermission(member.id)
+ if member == player.current.requester:
+ pass
+
+ elif member in player.skip_votes:
+ return error_msg(player.get_msg('voted', 'EN'), user_id=member.id)
+ else:
+ player.skip_votes.add(member)
+ if len(player.skip_votes) >= (required := player.required()):
+ pass
+ else:
+ return error_msg(player.get_msg('backVote', 'EN').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
index = data.get("index", 1)
if not player.is_playing:
@@ -102,7 +130,15 @@ async def getTracks(player, member: Member, data: dict):
async def shuffleTrack(player, member: Member, data: dict):
if not player.is_privileged(member):
- return missingPermission(member.id)
+
+ if member in player.shuffle_votes:
+ return error_msg(player.get_msg('voted'), user_id=member.id)
+ else:
+ player.shuffle_votes.add(member)
+ if len(player.shuffle_votes) >= (required := player.required()):
+ pass
+ else:
+ return error_msg(player.get_msg('shuffleVote').format(member, len(player.skip_votes), required), guild_id=player.guild.id)
await player.shuffle(data.get("type", "queue"), member)
@@ -117,10 +153,31 @@ async def updatePause(player, member: Member, data: dict):
return missingPermission(member.id)
pause = data.get("pause", True)
+ if not player.is_privileged(member):
+ if pause:
+ if member in player.pause_votes:
+ return error_msg(player.get_msg('voted'), user_id=member.id)
+ else:
+ player.pause_votes.add(member)
+ if len(player.pause_votes) >= (required := player.required()):
+ pass
+ else:
+ return error_msg(player.get_msg('pauseVote').format(member, len(player.pause_votes), required), guild_id=player.guild.id)
+ else:
+ if member in player.resume_votes:
+ return error_msg(player.get_msg('voted'), user_id=member.id)
+ else:
+ player.resume_votes.add(member)
+ if len(player.resume_votes) >= (required := player.required()):
+ pass
+ else:
+ return error_msg(player.get_msg('resumeVote').format(member, len(player.resume_votes), required), guild_id=player.guild.id)
+
if pause:
player.pause_votes.clear()
else:
player.resume_votes.clear()
+
await player.set_pause(pause, member)
async def updatePosition(player, member: Member, data: dict):
@@ -179,7 +236,7 @@ async def process_methods(websocket, bot: commands.Bot, data: dict) -> None:
except Exception as e:
payload = {
"op": "errorMsg",
- "level": "info",
+ "level": "error",
"msg": e,
"user_id": member.id
}
diff --git a/web/static/js/objects.js b/web/static/js/objects.js
index 28b687c..26e4258 100644
--- a/web/static/js/objects.js
+++ b/web/static/js/objects.js
@@ -184,6 +184,7 @@ class Track {
this.imageUrl = object["thumbnail"];
this.length = object["info"]["length"];
this.track_id = object["track_id"];
+ this.uri = object["info"]["uri"];
}
}
@@ -207,6 +208,8 @@ class Player {
this.volume = 100;
this.last_update = 0;
this.is_connected = true;
+
+ this.channelName = "";
}
handleMessage(data) {
@@ -238,14 +241,19 @@ class Player {
}
updateCurrentQueuePos(pos) {
- this.current_queue_position = pos - 1;
+ if (pos != undefined) {
+ this.current_queue_position = pos - 1;
+ }
this.currentTrack = this.queue[this.current_queue_position];
$('#sortable li div').removeClass('active');
const li = $(`#sortable li:eq(${this.current_queue_position})`);
li.find('div').addClass('active');
- $('.queue-list').animate({ scrollTop: li.position().top - $('.queue-list').position().top }, 'slow');
-
+
+ const queue = $('.queue-list')
+ if (queue.prop('scrollHeight') > queue.prop('clientHeight')) {
+ queue.animate({ scrollTop: li.position().top - queue.position().top }, 'slow');
+ }
return this.currentTrack;
}
From 5a529fa7445d4132b6fb928e8b12b1544801738c Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 24 Mar 2023 22:27:02 +0800
Subject: [PATCH 19/40] Add attr in get_msg function
---
voicelink/player.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/voicelink/player.py b/voicelink/player.py
index 38a1640..8b63ad7 100644
--- a/voicelink/player.py
+++ b/voicelink/player.py
@@ -196,8 +196,8 @@ class Player(VoiceProtocol):
def ping(self) -> float:
return round(self._ping / 1000, 2)
- def get_msg(self, mKey: str) -> str:
- return func.langs.get(self.lang, func.langs["EN"])[mKey]
+ def get_msg(self, mKey: str, lang: str = None) -> str:
+ return func.langs.get(lang if lang else self.lang, func.langs["EN"])[mKey]
def required(self, leave=False):
if self.settings.get('votedisable'):
From e3ea9725ca6add8744a13cb2385fa00582f06f43 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Fri, 24 Mar 2023 22:30:19 +0800
Subject: [PATCH 20/40] Add users bar
---
web/ipc/methods.py | 1 +
web/static/css/main.css | 67 +++++++++++++++++++++++++++++++++++-----
web/static/js/action.js | 23 ++++++++++++--
web/static/js/objects.js | 16 ++++++++--
web/templates/index.html | 10 ++++++
5 files changed, 106 insertions(+), 11 deletions(-)
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index 2c78a39..760ba27 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -30,6 +30,7 @@ async def initPlayer(player, member: Member, data: dict):
} for member in player.channel.members ],
"tracks": [ track.toDict() for track in player.queue._queue ],
"repeat_mode": player.queue.repeat.lower(),
+ "channel_name": player.channel.name,
"current_queue_position": player.queue._position if player.is_playing else player.queue._position + 1,
"current_position": 0 or player.position if player.is_playing else 0,
"is_playing": player.is_playing,
diff --git a/web/static/css/main.css b/web/static/css/main.css
index f6260b6..20ea6c4 100644
--- a/web/static/css/main.css
+++ b/web/static/css/main.css
@@ -16,6 +16,7 @@ body {
user-select: none;
white-space: nowrap;
max-height: 100%;
+ overflow-x: hidden;
}
a {
@@ -50,7 +51,7 @@ a {
position: relative;
cursor: pointer;
border-radius: 10px;
- transition: all .1s linear;
+ transition: all .1s ease-in-out;
}
.dropdown-container:hover {
@@ -70,7 +71,7 @@ a {
width: 100%;
opacity: 0;
background-color: rgba(142, 142, 142, 20%);
- transition: all .1s linear;
+ transition: all .1s ease-in-out;
}
.dropdown-container li {
@@ -81,7 +82,7 @@ a {
justify-content: space-between;
font-size: 14px;
border-radius: 4px;
- transition: all .2s linear;
+ transition: all .2s ease-in-out;
color: #FF0000;
}
@@ -139,7 +140,7 @@ a {
border-top: 3px solid #c9cbcc;
width: 20px;
height: 20px;
- animation: spin 1s linear infinite;
+ animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
@@ -250,7 +251,7 @@ a {
border-radius: 10px;
overflow: hidden;
transform: translateX(-100%);
- animation: moveRight .5s linear forwards;
+ animation: moveRight .5s ease-in-out forwards;
}
@keyframes moveRight {
@@ -267,7 +268,7 @@ a {
width: 100%;
height: 3px;
background-color: azure;
- animation: anim 6s linear forwards;
+ animation: anim 6s ease-in-out forwards;
}
@keyframes anim {
@@ -412,7 +413,7 @@ a {
margin-left: .8rem;
font-size: 20;
color: gray;
- transition: color .2s linear;
+ transition: color .2s ease-in-out;
}
.player-controller .right i:hover {
@@ -490,6 +491,58 @@ a {
max-height: 300px;
}
+.users-bar {
+ position: absolute;
+ right: 0;
+ background-color: rgb(37, 37, 37);
+ width: 350px;
+ max-width: 400px;
+ height: 100%;
+ overflow: hidden;
+ padding: 1.5rem 1rem;
+ transform: translateX(100%);
+ transition: all .5s ease-in-out;
+}
+
+.users-bar.active {
+ transform: translateX(0);
+}
+
+.users-bar .info {
+ padding: 0 .5rem;
+ margin-bottom: 1rem;
+}
+
+.users-bar .info h5 {
+ color: rgba(142, 142, 142);
+ ;
+}
+
+.users-container {
+ width: 100%;
+ overflow-y: auto;
+}
+
+.user {
+ padding: .5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: .5rem;
+}
+
+.user .left {
+ display: flex;
+ align-items: center;
+}
+
+.user img {
+ width: 35px;
+ height: auto;
+ border-radius: 8px;
+ margin-right: .5rem;
+}
+
@media (max-width: 935px) {
.main {
flex-direction: column;
diff --git a/web/static/js/action.js b/web/static/js/action.js
index c6a1a43..0edca72 100644
--- a/web/static/js/action.js
+++ b/web/static/js/action.js
@@ -7,11 +7,18 @@ $(document).ready(function () {
$('body').click(function (event) {
var $target = $(event.target);
- var $resultList = $target.closest(".search-contrainer");
- if (!$resultList.is('.search-contrainer') && $('#search-result-list').css("display") != 'none') {
+ if (!$target.closest(".search-contrainer").is('.search-contrainer') && $('#search-result-list').css("display") != 'none') {
$("#search-result-list").fadeOut(200);
}
+
+ if (!$target.closest(".users-bar").is('.users-bar') &&
+ !$target.closest("#users-button").is('#users-button') &&
+ $(".users-bar").hasClass("active")) {
+ $(".users-bar").removeClass("active");
+ $("#users-button").css({ "color": "" });
+ }
+
});
$(function () {
@@ -98,4 +105,16 @@ $(document).ready(function () {
$("#shuffle-button").on('click', function () {
player.shuffle();
})
+
+ $("#users-button").on('click', function () {
+ const userBar = $(".users-bar")
+ userBar.toggleClass("active");
+ if (userBar.hasClass("active")) {
+ $(this).css({ "color": "#fff" });
+ } else {
+ $(this).css({ "color": "" });
+ }
+
+ })
+
});
diff --git a/web/static/js/objects.js b/web/static/js/objects.js
index 26e4258..3bdbf23 100644
--- a/web/static/js/objects.js
+++ b/web/static/js/objects.js
@@ -37,6 +37,7 @@ const actions = {
player.is_paused = data['is_paused'];
player.current_position = data['current_position'];
player.repeat = data['repeat_mode'];
+ player.channelName = data["channel_name"]
data["users"].forEach(user => {
player.addUser(user);
})
@@ -102,6 +103,7 @@ const actions = {
if (player.users.hasOwnProperty(user["user_id"])) {
player.showToast(user["user_id"], "Left your channel!");
delete player.users[user['user_id']];
+ player.updateUser();
}
}
},
@@ -228,6 +230,7 @@ class Player {
init() {
this.queue = []
this.currentTrack = null;
+ this.users = [];
$('#sortable').empty();
this.updateInfo();
}
@@ -238,6 +241,7 @@ class Player {
var track = this.queue[i];
$("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
}
+ this.updateCurrentQueuePos();
}
updateCurrentQueuePos(pos) {
@@ -259,8 +263,15 @@ class Player {
addUser(user) {
this.users[user['user_id']] = { avatar_url: user["avatar_url"], name: user["name"] };
+ this.updateUser();
}
+ updateUser() {
+ $("#users-container").empty();
+ for (const [id, user] of Object.entries(this.users)) {
+ $("#users-container").append(`${user.name}
`)
+ };
+ }
addTrack(tracks) {
for (var i in tracks) {
var track = new Track(tracks[i]);
@@ -297,7 +308,7 @@ class Player {
if ((this.queue.length - this.current_queue_position) > 3) {
this.send({ "op": "shuffleTrack" });
} else {
- this.showToast(this.userId, "Add more songs to the queue before shuffling.");
+ this.showToast("info", "Add more songs to the queue before shuffling.");
}
}
@@ -385,6 +396,7 @@ class Player {
$("#image").attr("src", currentTrack.imageUrl);
$("#largeImage").attr("src", currentTrack.imageUrl);
}
+ $("#channel-name").text((this.channelName == "") ? "Not Found" : this.channelName);
var play_pause_btn = $("#play-pause-button");
var repeat_btn = $("#repeat-button");
if (this.is_paused || currentTrack == undefined) {
@@ -399,7 +411,6 @@ class Player {
repeat_btn.removeClass("fa-repeat-1").addClass("fa-repeat")
if (this.repeat == "off") {
- console.log("running");
repeat_btn.css('color', '');
} else if (this.repeat == "track") {
repeat_btn.css('color', '#fff');
@@ -407,4 +418,5 @@ class Player {
repeat_btn.removeClass("fa-repeat").addClass("fa-repeat-1");
}
}
+
}
\ No newline at end of file
diff --git a/web/templates/index.html b/web/templates/index.html
index c1d0230..7e13ac3 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -68,6 +68,15 @@
+
+
+
+ You are listen in
+
+
+
+
From c6849b7515a44d9f24143323772ddb9184a6fe63 Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sat, 25 Mar 2023 15:45:16 +0800
Subject: [PATCH 21/40] Fix swap track in dashboard
---
web/ipc/methods.py | 12 +++++++++++-
web/static/js/objects.js | 8 ++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index 760ba27..58eb74f 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -91,6 +91,16 @@ async def moveTrack(player, member: Member, data: dict):
moveItem = player.queue._queue[position]
player.queue._queue.remove(moveItem)
player.queue._queue.insert(new_position, moveItem)
+
+ if position > c and new_position <= c:
+ player.queue._position += 1
+
+ elif position < c and new_position >= c:
+ player.queue._position -= 1
+
+ elif position == c:
+ player.queue._position = new_position + 1
+
return {
"op": "moveTrack",
"position": {
@@ -238,7 +248,7 @@ async def process_methods(websocket, bot: commands.Bot, data: dict) -> None:
payload = {
"op": "errorMsg",
"level": "error",
- "msg": e,
+ "msg": str(e),
"user_id": member.id
}
await websocket.send(json.dumps(payload))
\ No newline at end of file
diff --git a/web/static/js/objects.js b/web/static/js/objects.js
index 3bdbf23..21541a7 100644
--- a/web/static/js/objects.js
+++ b/web/static/js/objects.js
@@ -281,6 +281,14 @@ class Player {
}
moveTrack(target, to) {
+ let element = this.queue.splice(target, 1)[0];
+ this.queue.splice(to, 0, element);
+
+ const $ul = $('#sortable');
+ const $li = $ul.children().eq(position);
+ $li.detach();
+ $ul.children().eq(to).before($li);
+
this.send({ "op": "moveTrack", "position": target, "newPosition": to })
}
From 70c744ddb39b513fb81b37a623071068162ea3dd Mon Sep 17 00:00:00 2001
From: Choco <94597336+ChocoMeow@users.noreply.github.com>
Date: Sat, 25 Mar 2023 16:45:49 +0800
Subject: [PATCH 22/40] Add action to track in dashboard
---
cogs/basic.py | 13 ++++++++--
voicelink/queue.py | 26 ++++++++++++--------
web/ipc/methods.py | 24 +++++++++++++++++++
web/static/css/main.css | 51 +++++++++++++++++++++++++++++++++++++++-
web/static/js/action.js | 23 +++++++++++++++++-
web/static/js/objects.js | 47 ++++++++++++++++++++++++++++++++++--
web/templates/index.html | 7 ++++++
7 files changed, 175 insertions(+), 16 deletions(-)
diff --git a/cogs/basic.py b/cogs/basic.py
index b946366..558ac98 100644
--- a/cogs/basic.py
+++ b/cogs/basic.py
@@ -507,8 +507,17 @@ class Basic(commands.Cog):
if not player.is_privileged(ctx.author):
return await ctx.send(player.get_msg('missingPerms_queue'), ephemeral=True)
- removedTrack = player.queue.remove(position1, position2, member)
- await ctx.send(player.get_msg('removed').format(removedTrack))
+ removedTrack = player.queue.remove(position1, position2, member=member)
+
+ if self.bot.ipc.connections and removedTrack:
+ await player.send_ws({
+ "op": "removeTrack",
+ "positions": [track["position"] for track in removedTrack],
+ "track_ids": [track["track"].track_id for track in removedTrack],
+ "current_queue_position": player.queue._position
+ }, requester=ctx.author)
+
+ await ctx.send(player.get_msg('removed').format(len(removedTrack)))
@commands.hybrid_command(name="forward", aliases=get_aliases("forward"))
@app_commands.describe(position="Input a amount that you to forward to. Exmaple: 1:20")
diff --git a/voicelink/queue.py b/voicelink/queue.py
index 7b8800d..ba13881 100644
--- a/voicelink/queue.py
+++ b/voicelink/queue.py
@@ -138,19 +138,25 @@ class Queue:
def remove(self, index:int, index2:int, member: Member = None):
pos = self._position - 1
- if not index2:
+
+ if index2 is None:
index2 = index
- count = 0
+
+ if index2 < index:
+ temp = index
+ index = index2
+ index2 = temp
+
try:
- for track in self._queue[pos + index: pos + index2 + 1]:
+ count = []
+ for i, track in enumerate(self._queue[pos + index: pos + index2 + 1], start=0):
if member:
- if track.requester == member:
- self._queue.remove(track)
- count += 1
- continue
- else:
- self._queue.remove(track)
- count += 1
+ if track.requester != member:
+ continue
+
+ self._queue.remove(track)
+ count.append({"position": pos + index + i, "track": track})
+
return count
except:
raise OutofList(self.get_msg("voicelinkOutofList"))
diff --git a/web/ipc/methods.py b/web/ipc/methods.py
index 58eb74f..89db079 100644
--- a/web/ipc/methods.py
+++ b/web/ipc/methods.py
@@ -159,6 +159,29 @@ async def repeatTrack(player, member: Member, data: dict):
await player.set_repeat()
+async def removeTrack(player, member: Member, data: dict):
+ if not player.is_privileged(member):
+ return missingPermission(member.id)
+
+ position = data.get("position")
+ verify_id = data.get("track_id")
+
+ track = player.queue._queue[position]
+ if track.track_id == verify_id:
+ player.queue._queue.remove(track)
+
+ if position < player.queue._position:
+ player.queue._position -= 1
+
+ return {
+ "op": "removeTrack",
+ "positions": [position],
+ "track_ids": [track.track_id],
+ "current_queue_position": player.queue._position,
+ "requester_id": member.id,
+ "guild_id": player.guild.id
+ }
+
async def updatePause(player, member: Member, data: dict):
if not player.is_privileged(member):
return missingPermission(member.id)
@@ -207,6 +230,7 @@ methods = {
"getTracks": getTracks,
"shuffleTrack": shuffleTrack,
"repeatTrack": repeatTrack,
+ "removeTrack": removeTrack,
"updatePause": updatePause,
"updatePosition": updatePosition,
}
diff --git a/web/static/css/main.css b/web/static/css/main.css
index 20ea6c4..b1bf3d1 100644
--- a/web/static/css/main.css
+++ b/web/static/css/main.css
@@ -444,6 +444,14 @@ a {
background-color: rgba(142, 142, 142, 20%);
}
+.track:hover .time {
+ display: none;
+}
+
+.track:hover i.action {
+ display: block;
+}
+
.track.active {
background-color: rgba(142, 142, 142, 30%);
}
@@ -457,12 +465,25 @@ a {
overflow: hidden;
}
-.track .left i {
+.track i {
cursor: move;
margin-right: 1rem;
font-size: 20px;
}
+.track i.action {
+ display: none;
+ padding: .5rem 1rem;
+ border-radius: 50%;
+ margin-right: 0;
+ cursor: pointer;
+ transition: background-color .2s;
+}
+
+.track i.action:hover {
+ background-color: rgba(157, 157, 157, 50%);
+}
+
.track .left img {
padding: 5px;
height: 50;
@@ -543,6 +564,34 @@ a {
margin-right: .5rem;
}
+.context-menu {
+ display: none;
+ position: absolute;
+}
+
+.context-menu ul {
+ list-style: none;
+ padding: .5rem;
+ border-radius: .5rem;
+ background-color: rgb(37, 37, 37);
+}
+
+.context-menu li {
+ padding: .5rem;
+ border-radius: .3rem;
+ cursor: pointer;
+ transition: background-color .2s;
+}
+
+.context-menu li:hover {
+ padding: .5rem;
+ background-color: rgb(98, 98, 98);
+}
+
+.context-menu i {
+ margin-right: .8rem;
+}
+
@media (max-width: 935px) {
.main {
flex-direction: column;
diff --git a/web/static/js/action.js b/web/static/js/action.js
index 0edca72..70c6372 100644
--- a/web/static/js/action.js
+++ b/web/static/js/action.js
@@ -1,6 +1,7 @@
$(document).ready(function () {
const player = new Player(userId);
var startPos = null;
+ var selectedTrack = null;
var typingTimer;
var doneTypingInterval = 2000;
@@ -19,6 +20,11 @@ $(document).ready(function () {
$("#users-button").css({ "color": "" });
}
+ if (!$target.closest(".action").is(".action") &&
+ !$target.closest("#context-menu li").is("#context-menu li") &&
+ $("#context-menu").css("display") != "none") {
+ $("#context-menu").fadeOut(200);
+ }
});
$(function () {
@@ -40,10 +46,16 @@ $(document).ready(function () {
});
});
- $('#sortable').on('click', 'li', function () {
+ $('#sortable').on('click', 'li', function (event) {
var index = $(this).index();
var position = player.current_queue_position;
+ if ($(event.target).hasClass('action')) {
+ selectedTrack = {position: index, track: player.queue[index]};
+ $("#context-menu").css({ "left": `${event.pageX - 150}px`, "top": `${event.pageY + 30}px` }).fadeIn(200);
+ return
+ }
+
if (index < position) {
player.backTo(position - index);
} else if (index > position) {
@@ -117,4 +129,13 @@ $(document).ready(function () {
})
+ $("#remove-track-button").on('click', function() {
+ player.removeTrack(selectedTrack?.position, selectedTrack?.track)
+ $("#context-menu").fadeOut(200);
+ })
+
+ $("#copy-track-button").on('click', function() {
+ navigator.clipboard.writeText(selectedTrack?.track.uri);
+ $("#context-menu").fadeOut(200);
+ })
});
diff --git a/web/static/js/objects.js b/web/static/js/objects.js
index 21541a7..8028d1d 100644
--- a/web/static/js/objects.js
+++ b/web/static/js/objects.js
@@ -172,6 +172,37 @@ const actions = {
player.repeat = data["repeatMode"];
},
+ removeTrack: function (player, data) {
+ const positions = data["positions"];
+ const trackIds = data["track_ids"];
+ var removeTracks = []
+ var newQueue = [];
+
+ player.queue.forEach((track, index) => {
+ if (positions.includes(index)) {
+ if (trackIds.includes(track.track_id)) {
+ console.log("removed" + index);
+ removeTracks.push(track);
+ } else {
+ return player.send({ "op": "initPlayer" })
+ }
+ } else {
+ newQueue.push(track);
+ }
+ })
+
+ player.queue = newQueue;
+ player.initSortable();
+
+ if (removeTracks.length == 1) {
+ var msg = `Removed ${removeTracks[0].title} from the queue.`;
+ } else {
+ var msg = `Removed ${removeTracks.length} tracks from the queue.`;
+ }
+
+ player.showToast(data["requester_id"], msg);
+ },
+
errorMsg: function (player, data) {
var level = data["level"];
player.showToast(level, data["msg"]);
@@ -239,7 +270,7 @@ class Player {
$('#sortable').empty();
for (var i in this.queue) {
var track = this.queue[i];
- $("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
+ $("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
}
this.updateCurrentQueuePos();
}
@@ -276,7 +307,7 @@ class Player {
for (var i in tracks) {
var track = new Track(tracks[i]);
this.queue.push(track);
- $("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
+ $("#sortable").append(`${track.title}
${track.author}
${this.msToReadableTime(track.length)}
`)
}
}
@@ -292,6 +323,18 @@ class Player {
this.send({ "op": "moveTrack", "position": target, "newPosition": to })
}
+ removeTrack(position, track) {
+ var rawTrack = this.queue[position];
+ if (position == this.current_queue_position) {
+ return this.showToast("error", "You are not allow to remove playing track!");
+ }
+ if (rawTrack.track_id == track.track_id) {
+ this.send({"op": "removeTrack", "position": position, "track_id": track.track_id});
+ } else {
+ this.showToast("error", "Track not found!");
+ }
+ }
+
togglePause() {
this.send({ "op": "updatePause", "pause": !this.is_paused });
}
diff --git a/web/templates/index.html b/web/templates/index.html
index 7e13ac3..51a4026 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -111,6 +111,13 @@
+
+