Merge pull request #1 from ChocoMeow/beta
Update project to v2.6.0 with new features and fixes
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
TOKEN = YOUR_BOT_TOKEN
|
||||
CLIENT_ID = YOUR_BOT_CLIENT_ID
|
||||
CLIENT_SECRET_ID = YOUR_BOT_CLIENT_SECRET_ID
|
||||
SERCET_KEY = DASHBOARD_SERCET_KEY
|
||||
|
||||
BUG_REPORT_CHANNEL_ID = YOUR_DISCORD_CHANNEL_ID
|
||||
|
||||
SPOTIFY_CLIENT_ID = YOUR_SPOTIFY_CLIENT_ID
|
||||
|
||||
31
README.md
31
README.md
@@ -5,18 +5,26 @@
|
||||
# Vocard (Discord Music Bot)
|
||||
> Vocard is a simple custom Disocrd Music Bot built with Python & [discord.py](https://discordpy.readthedocs.io/en/stable/)
|
||||
|
||||
# Demo (Bot & Dashboard)
|
||||
* [Discord Bot](https://discord.com/api/oauth2/authorize?client_id=890399639008866355&permissions=36708608&scope=bot%20applications.commands)
|
||||
* [Dashboard](http://vocard.xyz)
|
||||
|
||||
## Tutorial
|
||||
Click on the image below to watch the tutorial on Youtube.
|
||||
|
||||
[](https://www.youtube.com/watch?v=f_Z0RLRZzWw)
|
||||
|
||||
## Screenshot
|
||||
<div style="display: flex">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/218930155-630d8a0c-aa2d-4fcf-b6b4-957b8b2ba891.png" height=300>
|
||||
<img src="https://user-images.githubusercontent.com/94597336/218930292-a2221d22-40bb-4040-9823-6527f2f5f833.png" height=300>
|
||||
<img src="https://user-images.githubusercontent.com/94597336/218930558-f0b3bc78-cf0f-4b75-853e-eb15b13287c2.png" height=300>
|
||||
<img src="https://user-images.githubusercontent.com/94597336/218930368-46fff9ef-628b-4daa-aee1-7c5e5692700c.png" height=300>
|
||||
</div>
|
||||
### Discord Bot
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766331-dfa7d360-18d7-4014-ac6a-4fca55907d99.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766379-c824512e-a6f7-4ca5-9342-cc8e78d52491.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766408-37d733f7-c849-4cbd-9e17-0cd5800affb3.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766416-22ae3d91-40d9-44c0-bde1-9d40bd54c3af.png">
|
||||
|
||||
### Dashboard
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766460-2c8e7718-c1c2-4c8a-8c0e-185d81db20cc.png">
|
||||
<img src="https://user-images.githubusercontent.com/94597336/227766493-e7010813-077b-486a-b45c-4fbdc86f40bc.png">
|
||||
|
||||
|
||||
## Run the Projects
|
||||
[](https://replit.com/new/github/ChocoMeow/Vocard)
|
||||
@@ -40,6 +48,9 @@ Start your bot with `python main.py`
|
||||
```sh
|
||||
TOKEN = XXXXXXXXXXXXXXXXXXXXXXXX.XXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
CLIENT_ID = 123456789012345678
|
||||
CLIENT_SECRET_ID = YOUR_BOT_CLIENT_SECRET_ID
|
||||
SERCET_KEY = DASHBOARD_SERCET_KEY
|
||||
|
||||
BUG_REPORT_CHANNEL_ID = 123456789012345678
|
||||
|
||||
SPOTIFY_CLIENT_ID = 0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
@@ -54,6 +65,8 @@ MONGODB_NAME = Vocard
|
||||
| --- | --- |
|
||||
| TOKEN | Your Discord bot token [(Discord Portal)](https://discord.com/developers/applications) |
|
||||
| CLIENT_ID | Your Discord bot client id [(Discord Portal)](https://discord.com/developers/applications) |
|
||||
| CLIENT_SECRET_ID | Your Discord bot client secret id [(Discord Portal)](https://discord.com/developers/applications) ***(optional)*** |
|
||||
| SERCET_KEY | Secret key for dashboard ***(optional)*** |
|
||||
| BUG_REPORT_CHANNEL_ID | All the error messages will send to this text channel ***(optional)*** |
|
||||
| SPOTIFY_CLIENT_ID | Your Spoity client id [(Spotify Portal)](https://developer.spotify.com/dashboard/applications) ***(optional)*** |
|
||||
| SPOTIFY_CLIENT_SECRET | Your Spoity client sercret id [(Spotify Portal)](https://developer.spotify.com/dashboard/applications) ***(optional)*** |
|
||||
@@ -78,6 +91,11 @@ MONGODB_NAME = Vocard
|
||||
"bot_access_user": [],
|
||||
"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>",
|
||||
@@ -111,6 +129,7 @@ MONGODB_NAME = Vocard
|
||||
* For `bot_access_user` you can pass the [discord user id](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-). Example: `[123456789012345678]`
|
||||
* For `color_code` you must pass a [Hexadecimal color code](https://htmlcolorcodes.com/) and add `0x` before the color code. Example: `"0xb3b3b3"`
|
||||
* For `default_max_queue` you can set a default maximum number of tracks that can be added to the queue.
|
||||
* For `ipc_server` you can set the host, password and enable of the ipc server.
|
||||
* For `emoji_source_raw` you can change the source emoji of the track with discord emoji like `<:EMOJI_NAME:EMOJI_ID>`
|
||||
* For `cooldowns` you can set a custom cooldown in the command. Example: `"command_name": [The total number of tokens available, The length of the cooldown period in seconds]`
|
||||
* For `aliases` you can set custom aliases in the command. Example: `"command_name": [alias1, alias2, ...]`
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .lyrics import getLyrics
|
||||
from .lyrics import getLyrics
|
||||
from .settings import Settings
|
||||
18
addons/settings.py
Normal file
18
addons/settings.py
Normal file
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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.")
|
||||
@@ -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"))
|
||||
@@ -463,9 +463,7 @@ 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"
|
||||
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"))
|
||||
@@ -509,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 player.is_ipc_connected 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")
|
||||
@@ -591,7 +598,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 +617,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 +640,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"))
|
||||
@@ -749,7 +766,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:
|
||||
|
||||
@@ -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'),
|
||||
@@ -46,5 +46,43 @@ 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
|
||||
|
||||
if before.channel == after.channel:
|
||||
return
|
||||
|
||||
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:
|
||||
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",
|
||||
"user": {
|
||||
"user_id": member.id,
|
||||
"avatar_url": member.avatar.url,
|
||||
"name": member.name,
|
||||
},
|
||||
"channel_name": member.voice.channel.name if is_joined else "",
|
||||
"guild_id": guild,
|
||||
"is_joined": is_joined
|
||||
})
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Nodes(bot))
|
||||
@@ -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,
|
||||
|
||||
83
function.py
83
function.py
@@ -3,6 +3,7 @@ import json
|
||||
import aiohttp
|
||||
import os
|
||||
|
||||
from importlib import import_module
|
||||
from discord.ext import commands
|
||||
from dotenv import load_dotenv
|
||||
from random import choice
|
||||
@@ -11,6 +12,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,29 +38,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
|
||||
|
||||
#----------------- Nodes -----------------
|
||||
nodes = {}
|
||||
|
||||
#-------------- Vocard Functions --------------
|
||||
def get_settings(guild_id:int):
|
||||
@@ -85,37 +76,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
|
||||
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", "")
|
||||
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")
|
||||
@@ -129,12 +95,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"
|
||||
@@ -221,7 +205,7 @@ async def similar_track(player) -> bool:
|
||||
return False
|
||||
|
||||
async def connect_channel(ctx: commands.Context, channel: discord.VoiceChannel = None):
|
||||
import voicelink
|
||||
voicelink = import_module("voicelink");
|
||||
try:
|
||||
channel = channel or ctx.author.voice.channel
|
||||
except:
|
||||
@@ -233,6 +217,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:
|
||||
@@ -259,7 +244,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:
|
||||
@@ -275,12 +260,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, [])
|
||||
|
||||
72
main.py
72
main.py
@@ -1,39 +1,52 @@
|
||||
import discord
|
||||
import sys, os, traceback, aiohttp
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import aiohttp
|
||||
import update
|
||||
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
|
||||
|
||||
load_dotenv()
|
||||
func.settings_setup()
|
||||
func.init()
|
||||
|
||||
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,
|
||||
host=func.settings.ipc_server["host"],
|
||||
port=func.settings.ipc_server["port"],
|
||||
sercet_key=os.getenv("SERCET_KEY")
|
||||
)
|
||||
|
||||
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 +57,9 @@ class Vocard(commands.Bot):
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
|
||||
if func.settings.ipc_server["enable"]:
|
||||
await self.ipc.start()
|
||||
|
||||
await bot.tree.set_translator(Translator())
|
||||
await bot.tree.sync()
|
||||
|
||||
@@ -58,9 +74,11 @@ 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
|
||||
|
||||
|
||||
elif isinstance(error, (commands.CommandOnCooldown, commands.MissingPermissions, commands.RangeError, commands.BadArgument)):
|
||||
pass
|
||||
|
||||
@@ -68,34 +86,34 @@ 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.settings.invite_link
|
||||
if (guildId := ctx.guild.id) not in func.error_log:
|
||||
func.error_log[guildId] = {}
|
||||
func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
|
||||
|
||||
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):
|
||||
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
|
||||
@@ -110,26 +128,6 @@ bot = Vocard(command_prefix=get_prefix,
|
||||
case_insensitive=True,
|
||||
intents=intents)
|
||||
|
||||
@bot.tree.error
|
||||
async def app_command_error(interaction: discord.Interaction, error):
|
||||
error = getattr(error, 'original', error)
|
||||
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 = 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())
|
||||
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__":
|
||||
update.checkVersion(withMsg=True)
|
||||
bot.run(os.getenv("TOKEN"), log_handler=None)
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"bot_access_user": [],
|
||||
"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>",
|
||||
|
||||
@@ -3,7 +3,7 @@ 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"
|
||||
__version__ = "v2.6.0"
|
||||
|
||||
def checkVersion(withMsg = False):
|
||||
resp = requests.get("https://api.github.com/repos/ChocoMeow/Vocard/releases/latest")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -208,7 +201,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 +215,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 +239,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)
|
||||
|
||||
@@ -301,7 +294,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 +313,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):
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,8 @@ from discord import (
|
||||
StageChannel,
|
||||
Member,
|
||||
Embed,
|
||||
ui
|
||||
ui,
|
||||
Message
|
||||
)
|
||||
|
||||
from discord.ext import commands
|
||||
@@ -83,29 +84,30 @@ class Player(VoiceProtocol):
|
||||
self.dj: Member = ctx.author
|
||||
self.channel: VoiceChannel = channel
|
||||
self._guild = channel.guild if channel else None
|
||||
self._ipc_connection: bool = False
|
||||
|
||||
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.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()
|
||||
@@ -195,8 +197,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'):
|
||||
@@ -209,14 +211,18 @@ class Player(VoiceProtocol):
|
||||
|
||||
return required
|
||||
|
||||
@property
|
||||
def is_ipc_connected(self) -> bool:
|
||||
return bool(self._ipc_connection and len(self.bot.ipc.connections))
|
||||
|
||||
def is_user_join(self, user: Member):
|
||||
if user not in self.channel.members:
|
||||
if not user.guild_permissions.manage_guild:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_privileged(self, user: Member, check_user_join: bool = True):
|
||||
if user.id in func.bot_access_user:
|
||||
def is_privileged(self, user: Member, check_user_join: bool = True) -> bool:
|
||||
if user.id in func.settings.bot_access_user:
|
||||
return True
|
||||
|
||||
manage_perm = user.guild_permissions.manage_guild
|
||||
@@ -227,12 +233,19 @@ class Player(VoiceProtocol):
|
||||
return manage_perm or (self.settings['dj'] in [role.id for role in user.roles])
|
||||
return self.dj.id == user.id or manage_perm
|
||||
|
||||
async def _update_state(self, data: dict):
|
||||
async def _update_state(self, data: dict) -> None:
|
||||
state: dict = data.get("state")
|
||||
self._last_update = time.time() * 1000
|
||||
self._is_connected = state.get("connected")
|
||||
self._last_position = state.get("position")
|
||||
self._ping = state.get("ping")
|
||||
if self.is_ipc_connected:
|
||||
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 +323,15 @@ class Player(VoiceProtocol):
|
||||
|
||||
if self.settings.get('controller', True):
|
||||
await self.invoke_controller()
|
||||
|
||||
|
||||
if self.is_ipc_connected:
|
||||
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
|
||||
@@ -346,18 +367,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):
|
||||
@@ -373,7 +394,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.is_ipc_connected:
|
||||
await self.send_ws({"op": "playerClose"})
|
||||
|
||||
try:
|
||||
await self.controller.delete()
|
||||
except:
|
||||
@@ -543,27 +566,34 @@ 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.is_ipc_connected:
|
||||
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(
|
||||
"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.is_ipc_connected:
|
||||
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.is_ipc_connected:
|
||||
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.is_ipc_connected:
|
||||
await self.send_ws({"op": "updateVolume", "volume": volume}, requester)
|
||||
self._volume = volume
|
||||
return self._volume
|
||||
|
||||
@@ -575,6 +605,37 @@ class Player(VoiceProtocol):
|
||||
shuffle(replacement)
|
||||
self.queue.replace(queue_type, replacement)
|
||||
self.shuffle_votes.clear()
|
||||
if self.is_ipc_connected:
|
||||
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 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.is_ipc_connected:
|
||||
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)
|
||||
@@ -625,4 +686,10 @@ class Player(VoiceProtocol):
|
||||
await self.set_pause(True)
|
||||
|
||||
if self.volume != 100:
|
||||
await self.set_volume(self.volume)
|
||||
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)
|
||||
|
||||
@@ -26,19 +26,27 @@ 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):
|
||||
track = None
|
||||
try:
|
||||
track = self._queue[self._position - 1 if self._repeat == 1 else self._position]
|
||||
track = self._queue[self._position -
|
||||
1 if self._repeat == 1 else self._position]
|
||||
if self._repeat != 1:
|
||||
self._position += 1
|
||||
except:
|
||||
@@ -54,30 +62,30 @@ class Queue:
|
||||
def put(self, item: Track) -> int:
|
||||
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"))
|
||||
|
||||
self._queue.append(item)
|
||||
return self.count
|
||||
|
||||
|
||||
def put_at_front(self, item: Track):
|
||||
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"))
|
||||
|
||||
self._queue.insert(self._position, item)
|
||||
return 1
|
||||
|
||||
|
||||
def put_at_index(self, index: int, item: Track):
|
||||
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"))
|
||||
|
||||
@@ -88,21 +96,12 @@ class Queue:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
else:
|
||||
self._position += index - 1
|
||||
|
||||
|
||||
def backto(self, index: int):
|
||||
if not self._position - index >= 0:
|
||||
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] = []
|
||||
@@ -111,14 +110,14 @@ class Queue:
|
||||
def clear(self):
|
||||
del self._queue[self._position:]
|
||||
|
||||
def replace(self, queue_type:str, replacement:list):
|
||||
def replace(self, queue_type: str, replacement: list):
|
||||
if queue_type == "queue":
|
||||
self.clear()
|
||||
self._queue += replacement
|
||||
elif queue_type == "history":
|
||||
self._queue[:self._position] = replacement
|
||||
|
||||
def swap(self, num1:int, num2:int):
|
||||
|
||||
def swap(self, num1: int, num2: int):
|
||||
try:
|
||||
pos = self._position - 1
|
||||
self._queue[pos + num1], self._queue[pos + num2] = self._queue[pos + num2], self._queue[pos + num1]
|
||||
@@ -126,10 +125,10 @@ class Queue:
|
||||
except IndexError:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
def move(self, target:int, to:int):
|
||||
def move(self, target: int, to: int):
|
||||
if not 0 < target <= self.count or not 0 < to:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
|
||||
try:
|
||||
moveItem = self._queue[self._position + target - 1]
|
||||
self._queue.remove(moveItem)
|
||||
@@ -138,25 +137,31 @@ class Queue:
|
||||
except:
|
||||
raise OutofList(self.get_msg("voicelinkOutofList"))
|
||||
|
||||
def remove(self, index:int, index2:int, member: Member = None):
|
||||
def remove(self, index: int, index2: int = None, 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"))
|
||||
|
||||
|
||||
def history(self, incTrack: bool = False) -> list:
|
||||
if incTrack:
|
||||
return self._queue[:self._position]
|
||||
@@ -170,10 +175,10 @@ class Queue:
|
||||
@property
|
||||
def count(self):
|
||||
return len(self._queue[self._position:])
|
||||
|
||||
|
||||
@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):
|
||||
@@ -183,16 +188,17 @@ class Queue:
|
||||
return True
|
||||
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"))
|
||||
|
||||
@@ -205,8 +211,8 @@ class FairQueue(Queue):
|
||||
self._set.clear()
|
||||
for track in tracks[lastIndex:]:
|
||||
if track.requester in self._set:
|
||||
break;
|
||||
break
|
||||
lastIndex += 1
|
||||
self._set.add(track.requester)
|
||||
await self.put_at_index(lastIndex, item)
|
||||
return lastIndex
|
||||
return lastIndex
|
||||
|
||||
2
web/__init__.py
Normal file
2
web/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .ipc import *
|
||||
from .objects import *
|
||||
2
web/ipc/__init__.py
Normal file
2
web/ipc/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .server import IPCServer
|
||||
from .client import IPCClient
|
||||
57
web/ipc/client.py
Normal file
57
web/ipc/client.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import websockets, json, asyncio
|
||||
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()
|
||||
self.is_connected = False
|
||||
|
||||
async def connect_and_reconnect(self):
|
||||
while True:
|
||||
await self.connect()
|
||||
print("Reconnect in next 10s!")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.websocket = await websockets.connect(f"ws://{self.host}:{self.port}", extra_headers={"Client-Id": str(self.id)})
|
||||
await self.start_receiver()
|
||||
except:
|
||||
self.is_connected = False
|
||||
pass
|
||||
|
||||
async def send(self, message, user):
|
||||
if not self.is_connected:
|
||||
await self.connect_and_reconnect()
|
||||
|
||||
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):
|
||||
print("Connected ipc server!")
|
||||
self.is_connected = True
|
||||
async for message in self.websocket:
|
||||
if self.callback:
|
||||
data = json.loads(message)
|
||||
self.callback(data)
|
||||
|
||||
async def start_receiver(self):
|
||||
await self.receive()
|
||||
281
web/ipc/methods.py
Normal file
281
web/ipc/methods.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import json
|
||||
|
||||
from discord import Member
|
||||
from discord.ext import commands
|
||||
from voicelink import Player, Track, Playlist
|
||||
|
||||
def missingPermission(user_id:int):
|
||||
payload = {"op": "errorMsg", "level": "info", "msg": "Only the DJ or admins may use this funciton!"}
|
||||
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: Player, member: Member, data: dict):
|
||||
player._ipc_connection = True
|
||||
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 ],
|
||||
"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,
|
||||
"is_paused": player.is_paused,
|
||||
"is_dj": player.is_privileged(member, check_user_join=False)
|
||||
}
|
||||
|
||||
async def skipTo(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
if player.current and 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)
|
||||
|
||||
if player.queue._repeat == 1:
|
||||
await player.set_repeat("off")
|
||||
await player.stop()
|
||||
|
||||
async def backTo(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
if player.current and 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:
|
||||
player.queue.backto(index)
|
||||
await player.do_next()
|
||||
else:
|
||||
player.queue.backto(index + 1)
|
||||
await player.stop()
|
||||
|
||||
async def moveTrack(player: 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)
|
||||
|
||||
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": {
|
||||
"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: Player, member: Member, data: dict):
|
||||
raw_tracks = data.get("tracks", [])
|
||||
tracks = [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: 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
|
||||
|
||||
if isinstance(tracks, Playlist):
|
||||
tracks = [ track for track in tracks.tracks[:20] ]
|
||||
|
||||
payload["tracks"] = [ track.toDict() for track in tracks ]
|
||||
return payload
|
||||
|
||||
async def shuffleTrack(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
|
||||
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)
|
||||
|
||||
async def repeatTrack(player: Player, member: Member, data: dict):
|
||||
if not player.is_privileged(member):
|
||||
return missingPermission(member.id)
|
||||
|
||||
await player.set_repeat()
|
||||
|
||||
async def removeTrack(player: 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: Player, member: Member, data: dict):
|
||||
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: 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,
|
||||
"repeatTrack": repeatTrack,
|
||||
"removeTrack": removeTrack,
|
||||
"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, 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:
|
||||
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: 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 Exception as e:
|
||||
payload = {
|
||||
"op": "errorMsg",
|
||||
"level": "error",
|
||||
"msg": str(e),
|
||||
"user_id": member.id
|
||||
}
|
||||
await websocket.send(json.dumps(payload))
|
||||
67
web/ipc/server.py
Normal file
67
web/ipc/server.py
Normal file
@@ -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)
|
||||
25
web/objects.py
Normal file
25
web/objects.py
Normal file
@@ -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
|
||||
659
web/static/css/main.css
Normal file
659
web/static/css/main.css
Normal file
@@ -0,0 +1,659 @@
|
||||
@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%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--grey: rgba(142, 142, 142, 20%);
|
||||
--grey-hover: rgba(142, 142, 142, 30%);
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: absolute;
|
||||
opacity: 20%;
|
||||
border-radius: 40%;
|
||||
filter: blur(60px);
|
||||
transition: all 2s ease-in-out;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-container:hover {
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.dropdown-container:hover .dropdown-item {
|
||||
display: block;
|
||||
background-color: var(--grey);
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-content img {
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: .6rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dropdown-item a {
|
||||
display: block;
|
||||
padding: .5rem 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-item i {
|
||||
margin-right: .6rem;
|
||||
}
|
||||
|
||||
.dropdown-item a:hover {
|
||||
background-color: var(--grey-hover);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.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: var(--grey);
|
||||
}
|
||||
|
||||
.header .center .search-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header .center .search-container .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 ease-in-out 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: var(--grey);
|
||||
}
|
||||
|
||||
.header .search-result-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
margin-right: .6rem;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: 0rem 4rem;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.main .toastContrainer {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.toastContrainer .toast {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: .8rem;
|
||||
margin-top: 1rem;
|
||||
background-color: var(--grey-hover);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transform: translateX(-100%);
|
||||
animation: moveRight .5s ease-in-out 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 ease-in-out 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;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main .thumbnail img {
|
||||
padding: 1rem;
|
||||
width: 75%;
|
||||
height: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.main .thumbnail-background {
|
||||
position: absolute;
|
||||
padding: 1rem;
|
||||
filter: blur(80px);
|
||||
transform: translate3d(0px, 0px, 0) scale(1);
|
||||
opacity: var(0.6);
|
||||
border-radius: inherit;
|
||||
transition: all 1s ease-in-out;
|
||||
}
|
||||
|
||||
.player-controller {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.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;
|
||||
transition: color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.player-controller .right i:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main .queue-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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: var(--grey);
|
||||
}
|
||||
|
||||
.track:hover .time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.track:hover i.action {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.track.active {
|
||||
background-color: var(--grey-hover);
|
||||
}
|
||||
|
||||
.track .left {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
margin-right: .5rem;
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.track .left .info {
|
||||
margin-left: .5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track .left .info p {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track .left .info p.desc {
|
||||
color: grey;
|
||||
font-size: 13;
|
||||
}
|
||||
|
||||
.main .queue-list ul {
|
||||
list-style: none;
|
||||
width: 90%;
|
||||
padding: 1rem;
|
||||
padding-bottom: 5rem;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.users-bar {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 1.5rem 0;
|
||||
background-color: rgb(37, 37, 37);
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all .5s ease-in-out;
|
||||
}
|
||||
|
||||
.users-bar.active {
|
||||
width: 350px;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main .toastContrainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.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 .queue-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
BIN
web/static/img/error.png
Normal file
BIN
web/static/img/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
web/static/img/info.png
Normal file
BIN
web/static/img/info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
47
web/static/js/Socket.js
Normal file
47
web/static/js/Socket.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
200
web/static/js/action.js
Normal file
200
web/static/js/action.js
Normal file
@@ -0,0 +1,200 @@
|
||||
$(document).ready(function () {
|
||||
const player = new Player(userId);
|
||||
const img = document.getElementById("largeImage");
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
var startPos = null;
|
||||
var selectedTrack = null;
|
||||
|
||||
var typingTimer;
|
||||
var doneTypingInterval = 2000;
|
||||
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.onload = function () {
|
||||
try {
|
||||
const context = canvas.getContext('2d', {willReadFrequently: true});
|
||||
const width = canvas.width = img.width;
|
||||
const height = canvas.height = Math.round(img.height * 0.7); // set canvas height to 80% of image height
|
||||
const startY = Math.round((img.height - height) / 2); // calculate starting Y coordinate to center the canvas
|
||||
context.drawImage(img, 0, startY, width, height, 0, 0, width, height);
|
||||
getMainColorsFromImage(4)
|
||||
.then(colors => {
|
||||
$(".thumbnail-background").css({
|
||||
"background": `linear-gradient(-132deg, ${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[3]})`,
|
||||
"width": "70%",
|
||||
"padding-bottom": "70%"
|
||||
})
|
||||
})
|
||||
.catch(error => { return });
|
||||
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getMainColorsFromImage(numColors) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const imageData = canvas.getContext('2d', { willReadFrequently: true }).getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const colorCount = {};
|
||||
const threshold = 20; // adjust this value to change the threshold for black/white similarity
|
||||
const colorGap = 30; // adjust this value to change the minimum color difference between each main color
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
const r = imageData[i];
|
||||
const g = imageData[i + 1];
|
||||
const b = imageData[i + 2];
|
||||
const color = `rgb(${r}, ${g}, ${b})`;
|
||||
if (!colorCount[color]) {
|
||||
// Check if the color is too close to black or white
|
||||
const isBlackOrWhite = r < threshold || r > 255 - threshold || g < threshold || g > 255 - threshold || b < threshold || b > 255 - threshold;
|
||||
if (isBlackOrWhite) continue;
|
||||
// Check if the color is too similar to previous colors
|
||||
const prevColors = Object.keys(colorCount);
|
||||
const isTooSimilar = prevColors.some(prevColor => {
|
||||
const [pr, pg, pb] = prevColor.match(/\d+/g).map(Number);
|
||||
const diff = Math.sqrt(Math.pow(r - pr, 2) + Math.pow(g - pg, 2) + Math.pow(b - pb, 2));
|
||||
return diff < colorGap;
|
||||
});
|
||||
if (isTooSimilar) continue;
|
||||
colorCount[color] = 0;
|
||||
}
|
||||
colorCount[color]++;
|
||||
}
|
||||
const colors = Object.keys(colorCount).sort((color1, color2) => colorCount[color2] - colorCount[color1]).slice(0, numColors);
|
||||
resolve(colors);
|
||||
});
|
||||
}
|
||||
|
||||
$('body').click(function (event) {
|
||||
var $target = $(event.target);
|
||||
|
||||
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": "" });
|
||||
}
|
||||
|
||||
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 () {
|
||||
$("#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 (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) {
|
||||
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());
|
||||
})
|
||||
|
||||
$("#repeat-button").on('click', function () {
|
||||
player.repeatMode();
|
||||
})
|
||||
|
||||
$("#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": "" });
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
$("#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);
|
||||
})
|
||||
});
|
||||
490
web/static/js/objects.js
Normal file
490
web/static/js/objects.js
Normal file
@@ -0,0 +1,490 @@
|
||||
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.isDJ = data['is_dj'];
|
||||
player.addTrack(data["tracks"]);
|
||||
player.updateCurrentQueuePos(data['current_queue_position']);
|
||||
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);
|
||||
})
|
||||
|
||||
$("#sortable").sortable("option", "disabled", !player.isDJ);
|
||||
},
|
||||
|
||||
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(`<li class="search-result"><div class="search-result-left"><img src=${track.imageUrl} /><div class="search-result-info"><p class="info">${track.title}</p><p class="desc">${track.author}</p></div></div><p>${player.msToReadableTime(track.length)}</p></li>`)
|
||||
}
|
||||
}
|
||||
$("#search-result-list").fadeIn(200);
|
||||
$("#search-loader").fadeOut(200);
|
||||
},
|
||||
|
||||
playerClose: function (player, data) {
|
||||
player.init();
|
||||
},
|
||||
|
||||
updateGuild: function (player, data) {
|
||||
const user = data["user"];
|
||||
player.channelName = data["channel_name"];
|
||||
|
||||
if (user["user_id"] == player.userId) {
|
||||
if (data['is_joined']) {
|
||||
player.send({ "op": "initPlayer" });
|
||||
} else {
|
||||
player.init();
|
||||
}
|
||||
}
|
||||
if (data['is_joined']) {
|
||||
player.addUser(user);
|
||||
if (user["user_id"] != player.userId) {
|
||||
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']];
|
||||
player.updateUser();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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.")
|
||||
}
|
||||
},
|
||||
|
||||
repeatTrack: function (player, data) {
|
||||
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)) {
|
||||
if (index < player.current_queue_position) {
|
||||
player.current_queue_position -= 1;
|
||||
}
|
||||
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"]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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"];
|
||||
this.uri = object["info"]["uri"];
|
||||
}
|
||||
}
|
||||
|
||||
class Player {
|
||||
constructor(userId) {
|
||||
this.socket = new Socket(`${window.location.protocol}//${window.location.hostname}:${window.location.port}`);
|
||||
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.repeat = "off";
|
||||
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;
|
||||
|
||||
this.channelName = "";
|
||||
}
|
||||
|
||||
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;
|
||||
this.users = [];
|
||||
$('#sortable').empty();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
$('#sortable').empty();
|
||||
for (var i in this.queue) {
|
||||
var track = this.queue[i];
|
||||
$("#sortable").append(`<li><div class="track"><div class="left">${(this.isDJ) ? '<i class="fa-solid fa-bars handle"></i>' : ''}<img src=${track.imageUrl} /><div class="info"><p>${track.title}</p><p class="desc">${track.author}</p></div></div><p class="time">${this.msToReadableTime(track.length)}</p><i class="fa-solid fa-ellipsis-vertical action"></i></div></li>`)
|
||||
}
|
||||
this.updateCurrentQueuePos();
|
||||
}
|
||||
|
||||
updateCurrentQueuePos(pos) {
|
||||
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');
|
||||
|
||||
const queue = $('.queue-list')
|
||||
if (queue.prop('scrollHeight') > queue.prop('clientHeight')) {
|
||||
queue.animate({ scrollTop: li.position().top - queue.position().top }, 'slow');
|
||||
}
|
||||
return this.currentTrack;
|
||||
}
|
||||
|
||||
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(`<li class="user"><div class="left"><img src=${user.avatar_url} /><p>${user.name}</p></div><i class="fa-solid fa-microphone"></i></li>`)
|
||||
};
|
||||
}
|
||||
addTrack(tracks) {
|
||||
for (var i in tracks) {
|
||||
var track = new Track(tracks[i]);
|
||||
this.queue.push(track);
|
||||
$("#sortable").append(`<li><div class="track"><div class="left">${(this.isDJ) ? '<i class="fa-solid fa-bars handle"></i>' : ''}<img src=${track.imageUrl} /><div class="info"><p>${track.title}</p><p class="desc">${track.author}</p></div></div><p class="time">${this.msToReadableTime(track.length)}</p><i class="fa-solid fa-ellipsis-vertical action"></i></div></li>`)
|
||||
}
|
||||
}
|
||||
|
||||
moveTrack(target, to) {
|
||||
const c = this.current_queue_position;
|
||||
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);
|
||||
|
||||
if (target > c && to <= c) {
|
||||
this.current_queue_position += 1;
|
||||
} else if (target < c && to >= c) {
|
||||
this.current_queue_position -= 1;
|
||||
} else if (target == c) {
|
||||
this.current_queue_position = to;
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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("info", "Add more songs to the queue before shuffling.");
|
||||
}
|
||||
}
|
||||
|
||||
repeatMode() {
|
||||
this.send({ "op": "repeatTrack" })
|
||||
}
|
||||
|
||||
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) {
|
||||
if (userId == "info") {
|
||||
var user = { avatar_url: "/static/img/info.png", name: "Info" }
|
||||
} else if (userId == "error") {
|
||||
var user = { avatar_url: "/static/img/error.png", name: "Error" }
|
||||
} else {
|
||||
var user = this.users[userId];
|
||||
}
|
||||
if (user != null) {
|
||||
var $element = $(`<div class="toast"><img src=${user['avatar_url']} alt="user-icon"/><div class="content"><p class="username">${user['name']}</p><p class="message">${msg}</p></div></div>`)
|
||||
$(".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", `${window.location.protocol}//${window.location.hostname}:${window.location.port}/` + 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
repeat_btn.removeClass("fa-repeat-1").addClass("fa-repeat")
|
||||
if (this.repeat == "off") {
|
||||
repeat_btn.css('color', '');
|
||||
} else if (this.repeat == "track") {
|
||||
repeat_btn.css('color', '#fff');
|
||||
} else {
|
||||
repeat_btn.removeClass("fa-repeat").addClass("fa-repeat-1");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
122
web/templates/index.html
Normal file
122
web/templates/index.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Vocard Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
|
||||
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ+jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"
|
||||
integrity="sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/action.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/objects.js') }}"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
||||
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script> const userId = "{{ user.id }}"</script>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<p>Home</p>
|
||||
<p>Playlist</p>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="search-container">
|
||||
<div class="search-bar">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<input id="search-input" class="search-input"
|
||||
placeholder="Search your input and add it to the queue." />
|
||||
<div id="search-loader" class="search-loader"></div>
|
||||
</div>
|
||||
<ul id="search-result-list" class="search-result-list">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="dropdown-container">
|
||||
<div class="dropdown-content">
|
||||
<img src="{{ user.avatar.url }}" />
|
||||
<p>{{ user.username }}</p>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="logout"><i class="fa-solid fa-right-from-bracket"></i>Log Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="toastContrainer"></div>
|
||||
<div class="thumbnail">
|
||||
<img id="largeImage" />
|
||||
<div class="thumbnail-background"></div>
|
||||
</div>
|
||||
<div class="queue-list">
|
||||
<ul id="sortable">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="users-bar">
|
||||
<div class="info">
|
||||
<h3 id="channel-name"> </h3>
|
||||
<h5>You are listen in</h5>
|
||||
</div>
|
||||
<ul id="users-container" class="users-container">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="player-controller">
|
||||
<div class="progress-bar-container">
|
||||
<input type="range" value="0" min="0" max="500" class="seek-bar" id="seek-bar">
|
||||
</div>
|
||||
<div class="control-container">
|
||||
<div class="left">
|
||||
<div class="control">
|
||||
<i id="back-button" class="fa-solid fa-backward-step"></i>
|
||||
<i id="play-pause-button" class="fa-solid fa-play"></i>
|
||||
<i id="skip-button" class="fa-solid fa-forward-step"></i>
|
||||
</div>
|
||||
|
||||
<div class="position">
|
||||
<p id="position">00:00 </p>/ <p id="length"> 00:00</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<img class="image" id="image">
|
||||
<div class="data">
|
||||
<p id="title"></p>
|
||||
<p class="desc" id="author"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<i id="repeat-button" class="fa-solid fa-repeat"></i>
|
||||
<i id="shuffle-button" class="fa-solid fa-shuffle"></i>
|
||||
<i id="users-button" class="fa-solid fa-user-group"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="context-menu" class="context-menu">
|
||||
<ul>
|
||||
<li id="remove-track-button"><i class="fa-solid fa-trash"></i>Remove</li>
|
||||
<li id="copy-track-button"><i class="fa-solid fa-share"></i>Copy Song Link</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
208
web/webapp.py
Normal file
208
web/webapp.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from flask import Flask, redirect, url_for, session, request, render_template, abort, Response, stream_with_context
|
||||
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 = os.getenv("SERCET_KEY")
|
||||
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 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.username
|
||||
},
|
||||
"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 = IPCClient(secret_key=app.secret_key, callback=message_handler)
|
||||
|
||||
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()
|
||||
|
||||
@app.route('/<path:url>', methods=["GET"])
|
||||
def proxy(url):
|
||||
request = requests.get(url, stream=True)
|
||||
response = Response(
|
||||
stream_with_context(request.iter_content()),
|
||||
content_type=request.headers['content-type'],
|
||||
status=request.status_code
|
||||
)
|
||||
response.headers['Access-Control-Allow-Origin'] = "*"
|
||||
return response
|
||||
|
||||
# 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()])}')
|
||||
|
||||
# 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():
|
||||
# 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)
|
||||
Reference in New Issue
Block a user