Add dashboard
This commit is contained in:
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
|
||||
499
web/static/css/main.css
Normal file
499
web/static/css/main.css
Normal file
@@ -0,0 +1,499 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
color: #ffffff;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #020202;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header .left {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header .left p {
|
||||
font-size: 20;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
margin: 0rem 0.4rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header .left p:hover {
|
||||
background-color: rgba(142, 142, 142, 20%);
|
||||
}
|
||||
|
||||
.header .center .search-contrainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header .center .search-contrainer .search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding: .6rem 1rem;
|
||||
width: 500px;
|
||||
overflow: hidden;
|
||||
background-color: rgba(33, 33, 33, 255);
|
||||
}
|
||||
|
||||
.header .center .search-loader {
|
||||
display: none;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
border-top: 3px solid #c9cbcc;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.header .center input {
|
||||
margin: 0 1rem;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.header .center .search-result-list {
|
||||
position: absolute;
|
||||
display: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border-radius: 8px;
|
||||
padding: 1rem .5rem;
|
||||
background-color: rgba(33, 33, 33, 255);
|
||||
}
|
||||
|
||||
.header .search-result {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding: .5rem 1rem;
|
||||
cursor: pointer;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.header .search-result:hover {
|
||||
background-color: rgba(142, 142, 142, 20%);
|
||||
}
|
||||
|
||||
.header .search-result-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
margin-right: .6rem;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.header .search-result-info p.info {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.header .search-result-info p.desc {
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.header .search-result img {
|
||||
width: auto;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.header .right .account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
transition: all .2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header .right .account:hover {
|
||||
background-color: rgba(142, 142, 142, 20%);
|
||||
}
|
||||
|
||||
.header .right .account img {
|
||||
border-radius: 2rem;
|
||||
height: 40;
|
||||
margin-right: .6rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: 0rem 4rem;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.main .toastContrainer {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
}
|
||||
|
||||
.toastContrainer .toast {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: .8rem;
|
||||
margin-top: 1rem;
|
||||
background-color: rgba(142, 142, 142, 30%);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transform: translateX(-100%);
|
||||
animation: moveRight .5s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes moveRight {
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.toastContrainer .toast::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: azure;
|
||||
animation: anim 6s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes anim {
|
||||
100% {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toastContrainer .toast img {
|
||||
border-radius: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: .7rem;
|
||||
}
|
||||
|
||||
.toastContrainer .toast .content p.username {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.toastContrainer .toast .content p.message {
|
||||
font-size: 10px;
|
||||
width: 300px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.main .thumbnail {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main .thumbnail img {
|
||||
padding: 1rem;
|
||||
width: 70%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.player-controller {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.seek-bar {
|
||||
position: absolute;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
background-color: rgb(73, 73, 73);
|
||||
}
|
||||
|
||||
.seek-bar:hover {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.seek-bar::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgb(171, 168, 168);
|
||||
box-shadow: -1000px 0 0 1000px rgb(171, 168, 168);
|
||||
}
|
||||
|
||||
.control-container {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
align-items: center;
|
||||
background-color: #212121;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.player-controller .left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.player-controller .left .control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-controller .left .control i {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin-left: .8rem;
|
||||
font-size: 20;
|
||||
}
|
||||
|
||||
.player-controller .left .control #play-pause-button {
|
||||
font-size: 30;
|
||||
}
|
||||
|
||||
.player-controller .left .position {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.player-controller .center {
|
||||
display: flex;
|
||||
padding: 0rem 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
flex: 2;
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.player-controller .center img {
|
||||
border-radius: .2rem;
|
||||
height: 40;
|
||||
}
|
||||
|
||||
.player-controller .center .data {
|
||||
margin-left: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player-controller .center .desc {
|
||||
color: grey;
|
||||
font-size: 13;
|
||||
}
|
||||
|
||||
.player-controller .right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.player-controller .right i {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin-left: .8rem;
|
||||
font-size: 20;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.main .queue-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: .5rem;
|
||||
padding: 5px 15px;
|
||||
overflow: hidden;
|
||||
margin-bottom: .5rem;
|
||||
transition: background-color .2s, transform .2s;
|
||||
}
|
||||
|
||||
.track:hover {
|
||||
background-color: rgba(142, 142, 142, 20%);
|
||||
/* transform: scale(1.02); */
|
||||
}
|
||||
|
||||
.track.active {
|
||||
background-color: rgba(142, 142, 142, 30%);
|
||||
}
|
||||
|
||||
.track .left {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
margin-right: .5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.track .left i {
|
||||
cursor: move;
|
||||
margin-right: 1rem;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.track .left img {
|
||||
padding: 5px;
|
||||
height: 50;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.track .left .info {
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.track .left .info p {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track .left .info p.desc {
|
||||
color: grey;
|
||||
font-size: 13;
|
||||
}
|
||||
|
||||
.main .queue-list ul {
|
||||
list-style: none;
|
||||
width: 90%;
|
||||
padding: 1rem;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
@media (max-width: 935px) {
|
||||
.main {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header .center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-controller .left .position {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-controller .center img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main .thumbnail img {
|
||||
padding: 0;
|
||||
width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main .queue-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.track .left i {
|
||||
margin-right: .5rem;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.track .left img {
|
||||
padding: 2px;
|
||||
height: 30;
|
||||
border-radius: .2rem;
|
||||
}
|
||||
|
||||
.track .left .info {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
web/static/js/action.js
Normal file
97
web/static/js/action.js
Normal file
@@ -0,0 +1,97 @@
|
||||
$(document).ready(function () {
|
||||
const player = new Player(userId);
|
||||
var startPos = null;
|
||||
|
||||
var typingTimer;
|
||||
var doneTypingInterval = 2000;
|
||||
|
||||
$('body').click(function(event) {
|
||||
var $target = $(event.target);
|
||||
var $resultList = $target.closest(".search-contrainer");
|
||||
|
||||
if (!$resultList.is('.search-contrainer') && $('#search-result-list').css("display") != 'none') {
|
||||
$("#search-result-list").fadeOut(200);
|
||||
}
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$("#sortable").sortable({
|
||||
handle: ".handle",
|
||||
scroll: true,
|
||||
axis: "y",
|
||||
start: function (event, ui) {
|
||||
startPos = ui.item.index();
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
if (startPos != null) {
|
||||
var newPos = ui.item.index();
|
||||
if (startPos != newPos) {
|
||||
player.moveTrack(startPos, newPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#sortable').on('click', 'li', function () {
|
||||
var index = $(this).index();
|
||||
var position = player.current_queue_position;
|
||||
|
||||
if (index < position) {
|
||||
player.backTo(position - index);
|
||||
} else if (index > position) {
|
||||
player.skipTo(index - position)
|
||||
} else {
|
||||
player.togglePause();
|
||||
}
|
||||
})
|
||||
|
||||
$('#search-result-list').on('click', 'li', function () {
|
||||
var index = $(this).index();
|
||||
var track = player.searchList[index];
|
||||
if (track != undefined) {
|
||||
player.send({ "op": "addTracks", "tracks": [track] })
|
||||
}
|
||||
$("#search-result-list").fadeOut(200);
|
||||
})
|
||||
|
||||
$('#search-input').on('input', function () {
|
||||
clearTimeout(typingTimer);
|
||||
$("#search-loader").fadeIn(200);
|
||||
typingTimer = setTimeout(function () {
|
||||
var input = $('#search-input').val();
|
||||
if (input.replace(/\s+/g, '') != "") {
|
||||
player.send({ "op": "getTracks", "query": input })
|
||||
} else {
|
||||
$("#search-loader").fadeOut(200);
|
||||
$("#search-result-list").fadeOut(200);
|
||||
}
|
||||
}, doneTypingInterval);
|
||||
});
|
||||
|
||||
$('#search-input').focus(function () {
|
||||
if ($(this).val() != "") {
|
||||
$("#search-result-list").fadeIn(200);
|
||||
}
|
||||
})
|
||||
|
||||
$('#play-pause-button').on('click', function () {
|
||||
player.togglePause();
|
||||
});
|
||||
|
||||
$('#skip-button').on('click', function () {
|
||||
player.skipTo();
|
||||
});
|
||||
|
||||
$('#back-button').on('click', function () {
|
||||
player.backTo();
|
||||
});
|
||||
|
||||
$('#seek-bar').change(function () {
|
||||
player.seekTo($(this).val());
|
||||
})
|
||||
|
||||
$("#shuffle-button").on('click', function() {
|
||||
player.shuffle();
|
||||
})
|
||||
});
|
||||
370
web/static/js/objects.js
Normal file
370
web/static/js/objects.js
Normal file
@@ -0,0 +1,370 @@
|
||||
class Timer {
|
||||
constructor(callback, interval) {
|
||||
this.callback = callback;
|
||||
this.interval = interval;
|
||||
this.timerId = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.isRunning) {
|
||||
this.isRunning = true;
|
||||
this.timerId = setInterval(() => {
|
||||
this.callback();
|
||||
}, this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isRunning) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
getIsRunning() {
|
||||
return this.isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
initPlayer: function (player, data) {
|
||||
player.init();
|
||||
player.addTrack(data["tracks"]);
|
||||
player.updateCurrentQueuePos(data['current_queue_position']);
|
||||
player.isDJ = data['is_dj'];
|
||||
player.is_paused = data['is_paused'];
|
||||
player.current_position = data['current_position'];
|
||||
data["users"].forEach(user => {
|
||||
player.addUser(user);
|
||||
})
|
||||
},
|
||||
|
||||
playerUpdate: function (player, data) {
|
||||
player.last_update = data["last_update"];
|
||||
player.is_connected = data["is_connected"];
|
||||
player.current_position = data["last_position"];
|
||||
},
|
||||
|
||||
trackUpdate: function (player, data) {
|
||||
var track = player.updateCurrentQueuePos(data['current_queue_position']);
|
||||
player.is_paused = data['is_paused'];
|
||||
if (track?.track_id != data["track_id"]) {
|
||||
player.send({ "op": "initPlayer" })
|
||||
}
|
||||
},
|
||||
|
||||
addTrack: function (player, data) {
|
||||
player.addTrack(data["tracks"]);
|
||||
var tracks = data["tracks"];
|
||||
if (tracks.length == 1) {
|
||||
var msg = `Added ${tracks[0]['info']['title']} songs into the queue.`
|
||||
} else {
|
||||
var msg = `Added ${tracks.length} into the queue.`
|
||||
}
|
||||
player.showToast(data["requester_id"], msg)
|
||||
},
|
||||
|
||||
getTracks: function (player, data) {
|
||||
var tracks = data["tracks"];
|
||||
if (tracks != undefined) {
|
||||
const resultList = $("#search-result-list");
|
||||
resultList.empty();
|
||||
player.searchList = tracks;
|
||||
for (var i in tracks) {
|
||||
var track = new Track(tracks[i]);
|
||||
resultList.append(`<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"];
|
||||
if (user["user_id"] == player.userId) {
|
||||
if (data['is_joined']) {
|
||||
player.send({ "op": "initPlayer" });
|
||||
} else {
|
||||
player.init();
|
||||
}
|
||||
}
|
||||
if (data['is_joined']) {
|
||||
player.addUser(user);
|
||||
player.showToast(user["user_id"], "Joined your channel!");
|
||||
} else {
|
||||
if (player.users.hasOwnProperty(user["user_id"])) {
|
||||
player.showToast(user["user_id"], "Left your channel!");
|
||||
delete player.users[user['user_id']];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updatePause: function (player, data) {
|
||||
player.is_paused = data['pause'];
|
||||
var msg = "";
|
||||
if (data["pause"]) {
|
||||
msg = "Paused the player."
|
||||
} else {
|
||||
msg = "Resumed the player."
|
||||
}
|
||||
player.showToast(data["requester_id"], msg)
|
||||
},
|
||||
|
||||
updatePosition: function (player, data) {
|
||||
player.current_position = data["position"];
|
||||
},
|
||||
|
||||
swapTrack: function (player, data) {
|
||||
var index1 = player.current_queue_position + data['position2']["index"];
|
||||
var index2 = player.current_queue_position + data['position1']["index"];
|
||||
var track1 = player.queue[index1];
|
||||
var track2 = player.queue[index2];
|
||||
|
||||
if (track1?.track_id != data['position1']["track_id"] || track2?.track_id != data['position2']["track_id"]) {
|
||||
return player.send({ "op": "initPlayer" });
|
||||
}
|
||||
|
||||
player.queue[index1] = player.queue.splice(index2, 1, player.queue[index1])[0];
|
||||
player.initSortable();
|
||||
player.showToast(data["requester_id"], `${track1.title} and ${track2.title} are swapped`)
|
||||
},
|
||||
|
||||
moveTrack: function (player, data) {
|
||||
let position = player.current_queue_position + data["position"]["index"];
|
||||
let newPosition = player.current_queue_position + data["newPosition"]["index"];
|
||||
let element = player.queue.splice(position, 1)[0];
|
||||
if (element?.track_id != data["position"]["track_id"]) {
|
||||
return player.send({ "op": "initPlayer" });
|
||||
}
|
||||
player.queue.splice(newPosition, 0, element);
|
||||
|
||||
const $ul = $('#sortable');
|
||||
const $li = $ul.children().eq(position);
|
||||
$li.detach();
|
||||
$ul.children().eq(newPosition).before($li);
|
||||
player.showToast(data["requester_id"], `Moved ${element.title} to ${newPosition}`)
|
||||
},
|
||||
|
||||
shuffleTrack: function(player, data) {
|
||||
var tracks = data["tracks"];
|
||||
if (tracks != undefined) {
|
||||
player.queue = [];
|
||||
tracks.forEach(rawTrack => {
|
||||
|
||||
var track = new Track(rawTrack);
|
||||
player.queue.push(track);
|
||||
});
|
||||
player.initSortable();
|
||||
player.showToast(data["requester_id"], "The queue is shuffled.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Track {
|
||||
constructor(object) {
|
||||
this.title = object["info"]["title"];
|
||||
this.author = object["info"]["author"];
|
||||
this.imageUrl = object["thumbnail"];
|
||||
this.length = object["info"]["length"];
|
||||
this.track_id = object["track_id"];
|
||||
}
|
||||
}
|
||||
|
||||
class Player {
|
||||
constructor(userId) {
|
||||
this.socket = new Socket('http://127.0.0.1:5000');
|
||||
this.socket.connect(this);
|
||||
this.socket.addMessageListener((msg) => this.handleMessage(msg));
|
||||
this.timer = new Timer(() => this.updateTime(), 1000);
|
||||
this.userId = parseInt(userId);
|
||||
this.isDJ = false;
|
||||
this.date = new Date();
|
||||
this.queue = [];
|
||||
this.users = {};
|
||||
this.searchList = []
|
||||
this.currentTrack = null;
|
||||
this.current_queue_position = 0;
|
||||
this.current_position = 0;
|
||||
this.is_paused = false;
|
||||
this.volume = 100;
|
||||
this.last_update = 0;
|
||||
this.is_connected = true;
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
const op = data["op"];
|
||||
const validActions = Object.keys(actions);
|
||||
|
||||
if (validActions.includes(op)) {
|
||||
actions[op](this, data);
|
||||
} else {
|
||||
console.log(`Invalid action: ${op}`)
|
||||
}
|
||||
|
||||
return this.updateInfo()
|
||||
}
|
||||
|
||||
init() {
|
||||
this.queue = []
|
||||
this.currentTrack = null;
|
||||
$('#sortable').empty();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
$('#sortable').empty();
|
||||
for (var i in this.queue) {
|
||||
var track = this.queue[i];
|
||||
$("#sortable").append(`<li><div class="track"><div class="left"><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>${this.msToReadableTime(track.length)}</p></div></li>`)
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentQueuePos(pos) {
|
||||
this.current_queue_position = pos - 1;
|
||||
this.currentTrack = this.queue[this.current_queue_position];
|
||||
|
||||
$('#sortable li div').removeClass('active');
|
||||
const li = $(`#sortable li:eq(${this.current_queue_position})`);
|
||||
li.find('div').addClass('active');
|
||||
$('.queue-list').animate({ scrollTop: li.position().top - $('.queue-list').position().top }, 'slow');
|
||||
|
||||
return this.currentTrack;
|
||||
}
|
||||
|
||||
addUser(user) {
|
||||
this.users[user['user_id']] = { "avatar_url": user["avatar_url"], "name": user["name"] };
|
||||
}
|
||||
|
||||
addTrack(tracks) {
|
||||
for (var i in tracks) {
|
||||
var track = new Track(tracks[i]);
|
||||
this.queue.push(track);
|
||||
$("#sortable").append(`<li><div class="track"><div class="left"><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>${this.msToReadableTime(track.length)}</p></div></li>`)
|
||||
}
|
||||
}
|
||||
|
||||
moveTrack(target, to) {
|
||||
this.send({ "op": "moveTrack", "position": target, "newPosition": to })
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
this.send({ "op": "updatePause", "pause": !this.is_paused });
|
||||
}
|
||||
|
||||
skipTo(index = 1) {
|
||||
this.send({ "op": "skipTo", "index": index });
|
||||
}
|
||||
|
||||
backTo(index = 1) {
|
||||
this.send({ "op": "backTo", "index": index });
|
||||
}
|
||||
|
||||
seekTo(tempPosition) {
|
||||
if (this.currentTrack == undefined) {
|
||||
return;
|
||||
}
|
||||
var position = tempPosition / 500 * this.currentTrack.length;
|
||||
this.send({ "op": "updatePosition", "position": position });
|
||||
}
|
||||
|
||||
shuffle() {
|
||||
if ((this.queue.length - this.current_queue_position) > 3) {
|
||||
this.send({ "op": "shuffleTrack" });
|
||||
} else {
|
||||
this.showToast(this.userId, "Add more songs to the queue before shuffling.");
|
||||
}
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
var json = JSON.stringify(payload)
|
||||
this.socket.send(json);
|
||||
}
|
||||
|
||||
isPlaying() {
|
||||
return (this.currentTrack != undefined && this.is_connected);
|
||||
}
|
||||
|
||||
msToReadableTime(ms) {
|
||||
let totalSeconds = Math.floor(ms / 1000);
|
||||
|
||||
let hours = Math.floor(totalSeconds / 3600);
|
||||
let minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
let seconds = totalSeconds % 60;
|
||||
|
||||
minutes = (minutes < 10) ? "0" + minutes : minutes;
|
||||
seconds = (seconds < 10) ? "0" + seconds : seconds;
|
||||
|
||||
let timeString = "";
|
||||
if (hours > 0) {
|
||||
timeString += hours + ":" + minutes + ":" + seconds;
|
||||
} else {
|
||||
timeString += minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
showToast(userId, msg) {
|
||||
var user = this.users[userId];
|
||||
if (user != null) {
|
||||
var $element = $(`<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", currentTrack.imageUrl);
|
||||
}
|
||||
var play_pause_btn = $("#play-pause-button")
|
||||
if (this.is_paused || currentTrack == undefined) {
|
||||
play_pause_btn.removeClass('fa-pause').addClass('fa-play');
|
||||
if (this.timer.getIsRunning()) {
|
||||
this.timer.stop();
|
||||
}
|
||||
} else {
|
||||
play_pause_btn.removeClass('fa-play').addClass('fa-pause');
|
||||
this.timer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
97
web/templates/index.html
Normal file
97
web/templates/index.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<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-contrainer">
|
||||
<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="account">
|
||||
<img src="{{ user.avatar.url }}" />
|
||||
<p>{{ user.username }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="toastContrainer"></div>
|
||||
<div class="thumbnail">
|
||||
<img id="largeImage" />
|
||||
</div>
|
||||
<div class="queue-list">
|
||||
<ul id="sortable">
|
||||
</ul>
|
||||
</div>
|
||||
</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="shuffle-button" class="fa-solid fa-shuffle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
202
web/webapp.py
Normal file
202
web/webapp.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from flask import Flask, redirect, url_for, session, request, render_template, abort
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room, rooms, disconnect
|
||||
from ipc import IPCClient
|
||||
from objects import User
|
||||
from dotenv import load_dotenv
|
||||
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import functools
|
||||
import threading
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "aNOwneoiniowefn"
|
||||
socketio = SocketIO(app)
|
||||
|
||||
# Discord OAuth2 credentials
|
||||
CLIENT_ID = os.getenv("CLIENT_ID")
|
||||
CLIENT_SECRET = os.getenv("CLIENT_SECRET_ID")
|
||||
REDIRECT_URI = 'http://127.0.0.1:5000/callback'
|
||||
DISCORD_API_BASE_URL = 'https://discord.com/api'
|
||||
|
||||
USERS = {}
|
||||
|
||||
def start_ipc_client(loop):
|
||||
client = IPCClient(secret_key="Vocard")
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(client.connect())
|
||||
|
||||
def create_ipc_client():
|
||||
loop = asyncio.new_event_loop()
|
||||
threading.Thread(target=start_ipc_client,
|
||||
args=(loop,), daemon=True).start()
|
||||
return IPCClient(secret_key="Vocard", callback=message_handler)
|
||||
|
||||
def get_user(user_id: int):
|
||||
for user in USERS.values():
|
||||
if user.id == user_id:
|
||||
return user
|
||||
return None
|
||||
|
||||
def user_join_room(guild_id: int, user: User) -> None:
|
||||
if not user.sid:
|
||||
return
|
||||
join_room(guild_id, sid=user.sid)
|
||||
user.guild_id = guild_id
|
||||
|
||||
def user_leave_room(guild_id: int, user: User) -> None:
|
||||
if not user.sid:
|
||||
return
|
||||
leave_room(guild_id, sid=user.sid)
|
||||
user.guild_id = None
|
||||
|
||||
def message_handler(data: dict):
|
||||
op = data.get("op")
|
||||
|
||||
user_id = data.get("user_id", None)
|
||||
if user_id:
|
||||
user: User = get_user(user_id)
|
||||
else:
|
||||
user = None
|
||||
|
||||
guild_id = data.get("guild_id", None)
|
||||
|
||||
if op == "updateGuild":
|
||||
user_id = data.get("user", {}).get("user_id", None)
|
||||
is_joined = data.get("is_joined")
|
||||
|
||||
user: User = get_user(user_id)
|
||||
if user and guild_id:
|
||||
user_join_room(guild_id, user) if is_joined else user_leave_room(guild_id, user)
|
||||
|
||||
elif op == "createPlayer":
|
||||
members_id = data.get("members_id", [])
|
||||
for member_id in members_id:
|
||||
user: User = get_user(member_id)
|
||||
if user:
|
||||
user_join_room(guild_id, user)
|
||||
emit('message', {
|
||||
"op": "updateGuild",
|
||||
"user": {
|
||||
"user_id": user.id,
|
||||
"avatar_url": user.avatar.url,
|
||||
"name": user.display_name
|
||||
},
|
||||
"is_joined": True
|
||||
}, to=user.sid)
|
||||
return
|
||||
|
||||
if user:
|
||||
if guild_id and user.sid:
|
||||
join_room(guild_id, sid=user.sid)
|
||||
elif guild_id in rooms(user.sid):
|
||||
pass
|
||||
else:
|
||||
emit('message', data, to=user.sid)
|
||||
|
||||
if op == "initPlayer":
|
||||
return emit('message', data, to=user.sid)
|
||||
|
||||
if guild_id:
|
||||
skip_sids = []
|
||||
if skip_users := data.get("skip_users"):
|
||||
for user_id in skip_users:
|
||||
if user := get_user(user_id):
|
||||
skip_sids.append(user.sid)
|
||||
|
||||
emit('message', data, room=guild_id, skip_sid=skip_sids)
|
||||
|
||||
ipc_client = create_ipc_client()
|
||||
|
||||
def login_required(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
token = session.get("discord_token", None)
|
||||
if not token:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if token not in USERS:
|
||||
resp = requests_api(f'{DISCORD_API_BASE_URL}/users/@me',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
if resp:
|
||||
user = USERS[token] = User(resp)
|
||||
else:
|
||||
abort(401, description="Unauthorized")
|
||||
else:
|
||||
user = USERS[token]
|
||||
|
||||
return func(user, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def requests_api(url: str, headers=None):
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
if not resp:
|
||||
return False
|
||||
|
||||
return resp.json()
|
||||
|
||||
# home page
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def home(user: User):
|
||||
return render_template("index.html", user=user)
|
||||
|
||||
# login page
|
||||
@app.route('/login')
|
||||
def login():
|
||||
# redirect to Discord OAuth2 login page
|
||||
params = {
|
||||
'client_id': CLIENT_ID,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'scope': 'identify'
|
||||
}
|
||||
return redirect(f'{DISCORD_API_BASE_URL}/oauth2/authorize?{"&".join([f"{k}={v}" for k, v in params.items()])}')
|
||||
|
||||
# callback page
|
||||
@app.route('/callback')
|
||||
def callback():
|
||||
# fetch user token from Discord OAuth2
|
||||
code = request.args.get('code')
|
||||
data = {
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'scope': 'identify'
|
||||
}
|
||||
response = requests.post(f'{DISCORD_API_BASE_URL}/oauth2/token', data=data)
|
||||
token_data = json.loads(response.content.decode('utf-8'))
|
||||
session['discord_token'] = token_data.get("access_token")
|
||||
|
||||
return redirect(url_for("home"))
|
||||
|
||||
|
||||
@socketio.on("connect")
|
||||
@login_required
|
||||
def handle_connect(user: User):
|
||||
if user.sid:
|
||||
disconnect(sid=user.sid)
|
||||
user.sid = request.sid
|
||||
asyncio.run(ipc_client.send('{"op": "initPlayer"}', user))
|
||||
|
||||
@socketio.on("disconnect")
|
||||
@login_required
|
||||
def handle_disconnect(user: User):
|
||||
user.sid = None
|
||||
|
||||
@socketio.on("message")
|
||||
@login_required
|
||||
def handle_message(user: User, msg):
|
||||
asyncio.run(ipc_client.send(msg, user))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, host="127.0.0.1", port=5000)
|
||||
Reference in New Issue
Block a user