commit
f21cea872c
@ -0,0 +1,112 @@
|
||||
const sqlite3 = require('sqlite3');
|
||||
|
||||
/**
|
||||
* Promise function wrappers for sqlite3
|
||||
* @type {Database}
|
||||
*/
|
||||
exports.Database = class {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
this.database = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise wrapper for sqlite3/Database constructor
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.database = new sqlite3.Database(this.path, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise wrapper for sqlite3/Database run
|
||||
* @param SQL
|
||||
* @param values
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
run(SQL, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (values !== null && values instanceof Array)
|
||||
this.database.run(SQL, values, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
else
|
||||
this.database.run(SQL, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise wrapper for sqlite3/Database get
|
||||
* @param SQL
|
||||
* @param values
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
get(SQL, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (values !== null && values instanceof Array)
|
||||
this.database.get(SQL, values, (err, row) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(row);
|
||||
});
|
||||
else
|
||||
this.database.get(SQL, (err, row) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(row);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise wrapper for sqlite3/Database all
|
||||
* @param SQL
|
||||
* @param values
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
all(SQL, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (values !== null && values instanceof Array)
|
||||
this.database.all(SQL, values, (err, rows) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(rows);
|
||||
});
|
||||
else
|
||||
this.database.all(SQL, (err, rows) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(rows);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for sqlite3/Database close
|
||||
*/
|
||||
close() {
|
||||
this.database.close();
|
||||
}
|
||||
};
|
@ -0,0 +1,477 @@
|
||||
const express = require('express'),
|
||||
graphqlHTTP = require('express-graphql'),
|
||||
{buildSchema} = require('graphql'),
|
||||
compression = require('compression'),
|
||||
md5 = require('js-md5'),
|
||||
sha512 = require('js-sha512'),
|
||||
fs = require('fs'),
|
||||
session = require('express-session'),
|
||||
SQLiteStore = require('connect-sqlite3')(session),
|
||||
bodyParser = require('body-parser'),
|
||||
compileSass = require('express-compile-sass'),
|
||||
config = require('../config.json'),
|
||||
utils = require('../lib/utils');
|
||||
|
||||
let logger = require('winston');
|
||||
|
||||
exports.setLogger = function (newLogger) {
|
||||
logger = newLogger;
|
||||
};
|
||||
|
||||
exports.WebServer = class {
|
||||
constructor(port) {
|
||||
this.app = express();
|
||||
this.server = null;
|
||||
this.port = port;
|
||||
this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8'));
|
||||
this.root = {};
|
||||
}
|
||||
|
||||
configureExpress() {
|
||||
this.app.set('view engine', 'pug');
|
||||
this.app.set('trust proxy', 1);
|
||||
this.app.set('views', './web/http/');
|
||||
|
||||
if (this.app.get('env') === 'devlopment')
|
||||
this.app.use(require('cors')());
|
||||
|
||||
this.app.use(require('cors')());
|
||||
this.app.use(session({
|
||||
store: new SQLiteStore({dir: './data', db: 'sessions.db'}),
|
||||
secret: config.webservice.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {secure: 'auto'},
|
||||
genid: () => generateUUID('Session')
|
||||
}));
|
||||
this.app.use(bodyParser.json());
|
||||
this.app.use(bodyParser.urlencoded({extended: true}));
|
||||
|
||||
this.app.use(compression({
|
||||
filter: (req, res) => {
|
||||
if (req.headers['x-no-compression'])
|
||||
return false;
|
||||
else
|
||||
return compression.filter(req, res);
|
||||
|
||||
}
|
||||
}));
|
||||
this.app.use(compileSass({
|
||||
root: './web/http/'
|
||||
}));
|
||||
this.app.post('/', async (req, res) => {
|
||||
if (!req.body.username || !req.body.password) {
|
||||
res.render('login', {msg: 'Please enter username and password.'});
|
||||
} else {
|
||||
let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]);
|
||||
if (!user) {
|
||||
logger.debug(`User ${req.body.username} failed to authenticate`);
|
||||
res.render('login', {msg: 'Login failed!'});
|
||||
} else {
|
||||
req.session.user = user;
|
||||
res.render('index');
|
||||
}
|
||||
}
|
||||
});
|
||||
this.app.use('/scripts', express.static('./web/http/scripts'));
|
||||
this.app.use((req, res, next) => {
|
||||
if (req.session.user)
|
||||
next();
|
||||
else
|
||||
res.render('login');
|
||||
});
|
||||
this.app.get('/', (req, res) => {
|
||||
res.render('index');
|
||||
});
|
||||
this.app.use('/graphql', graphqlHTTP({
|
||||
schema: this.schema,
|
||||
rootValue: this.root,
|
||||
graphiql: config.webservice.graphiql || false
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting the api webserver
|
||||
*/
|
||||
start() {
|
||||
this.configureExpress();
|
||||
if (config.webservice.https && config.webservice.https.enabled) {
|
||||
let sslKey = null;
|
||||
let sslCert = null;
|
||||
|
||||
if (config.webservice.https.keyFile)
|
||||
sslKey = fs.readFileSync(config.webservice.https.keyFile, 'utf-8');
|
||||
if (config.webservice.https.certFile)
|
||||
sslCert = fs.readFileSync(config.webservice.https.certFile, 'utf-8');
|
||||
if (sslKey && sslCert) {
|
||||
logger.verbose('Creating https server.');
|
||||
this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app);
|
||||
} else {
|
||||
logger.warn('Key or certificate file not found. Fallback to http server.');
|
||||
this.server = require('http').createServer(this.app);
|
||||
}
|
||||
} else {
|
||||
this.server = require('http').createServer(this.app);
|
||||
}
|
||||
this.server.listen(this.port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stopping the webserver
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
stop() {
|
||||
return new Promise((resolve) => {
|
||||
if (this.server)
|
||||
this.server.close(resolve);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a token for a given username
|
||||
* @param username
|
||||
* @param scope
|
||||
* @param password
|
||||
* @param pwIsHash Is the password already a hash string?
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async createUser(username, password, scope, pwIsHash) {
|
||||
if (!pwIsHash) password = sha512(password);
|
||||
let token = generateUUID(['TOKEN', username]);
|
||||
await this.maindb.run('INSERT INTO users (username, password, token, scope) VALUES (?, ?, ?, ?)',
|
||||
[username, password, token, scope]);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting all objects that web can query
|
||||
* @param objects
|
||||
*/
|
||||
async setReferenceObjects(objects) {
|
||||
this.maindb = objects.maindb;
|
||||
await this.maindb.run(`${utils.sql.tableExistCreate} users (
|
||||
${utils.sql.pkIdSerial},
|
||||
username VARCHAR(32) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
scope INTEGER NOT NULL DEFAULT 0
|
||||
)`);
|
||||
this.root = {
|
||||
client: {
|
||||
guilds: async (args) => {
|
||||
let dcGuilds = objects.client.guilds.values();
|
||||
if (args.id)
|
||||
return [(await Promise.all(Array.from(dcGuilds)
|
||||
.map(async (x) => new Guild(x, await objects.getGuildHandler(x)))))
|
||||
.find(x => (x.id === args.id))];
|
||||
else
|
||||
try {
|
||||
return await Promise.all(Array.from(dcGuilds)
|
||||
.slice(args.offset, args.offset + args.first)
|
||||
.map(async (x) => new Guild(x, await objects.getGuildHandler(x))));
|
||||
} catch (err) {
|
||||
logger.error(err.stack);
|
||||
return null;
|
||||
}
|
||||
|
||||
},
|
||||
guildCount: () => {
|
||||
return Array.from(objects.client.guilds.values()).length;
|
||||
},
|
||||
user: () => {
|
||||
return new User(objects.client.user);
|
||||
},
|
||||
ping: () => {
|
||||
return objects.client.ping;
|
||||
},
|
||||
status: () => {
|
||||
return objects.client.status;
|
||||
},
|
||||
uptime: () => {
|
||||
return objects.client.uptime;
|
||||
},
|
||||
voiceConnectionCount: () => {
|
||||
let dcGuilds = Array.from(objects.client.guilds.values());
|
||||
return dcGuilds.filter((x) => {
|
||||
let gh = objects.guildHandlers[x.id];
|
||||
if (gh)
|
||||
if (gh.dj)
|
||||
return gh.dj.playing;
|
||||
else
|
||||
return false;
|
||||
else
|
||||
return false;
|
||||
|
||||
}).length;
|
||||
}
|
||||
},
|
||||
prefix: objects.prefix,
|
||||
presences: objects.presences,
|
||||
config: () => {
|
||||
let newConfig = JSON.parse(JSON.stringify(config));
|
||||
delete newConfig.api;
|
||||
return JSON.stringify(newConfig, null, ' ');
|
||||
},
|
||||
logs: (args) => {
|
||||
return new Promise((resolve) => {
|
||||
let logEntries = [];
|
||||
let lineReader = require('readline').createInterface({
|
||||
input: require('fs').createReadStream('./.log/latest.log')
|
||||
});
|
||||
lineReader.on('line', (line) => {
|
||||
logEntries.push(new LogEntry(JSON.parse(line)));
|
||||
});
|
||||
lineReader.on('close', () => {
|
||||
if (args.level)
|
||||
logEntries = logEntries
|
||||
.filter(x => (utils.logLevels[x.level] >= utils.logLevels[args.level]));
|
||||
|
||||
if (args.id)
|
||||
logEntries = [logEntries.find(x => (x.id === args.id))];
|
||||
|
||||
if (args.first)
|
||||
logEntries = logEntries.slice(args.offset, args.offset + args.first);
|
||||
else
|
||||
logEntries = logEntries.slice(logEntries.length - args.last);
|
||||
|
||||
resolve(logEntries);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* generating an id
|
||||
* @param valArr
|
||||
* @returns {*}
|
||||
*/
|
||||
function generateID(valArr) {
|
||||
let b64 = Buffer.from(valArr.map(x => {
|
||||
if (x)
|
||||
return x.toString();
|
||||
else
|
||||
return 'null';
|
||||
}).join('_')).toString('base64');
|
||||
return md5(b64);
|
||||
}
|
||||
|
||||
/**
|
||||
* generating an unique id
|
||||
* @param input
|
||||
* @returns {*}
|
||||
*/
|
||||
function generateUUID(input) {
|
||||
return generateID([input, (new Date()).getMilliseconds()]) + Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for graphql attribute access to the lib/music/DJ
|
||||
*/
|
||||
class DJ {
|
||||
constructor(musicDj) {
|
||||
this.dj = musicDj;
|
||||
this.quality = musicDj.quality;
|
||||
}
|
||||
|
||||
queue(args) {
|
||||
let queue = this.dj.queue.map((x) => {
|
||||
return {
|
||||
id: generateID(['Media', x.url]),
|
||||
name: x.title,
|
||||
url: x.url,
|
||||
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(x.url)
|
||||
};
|
||||
});
|
||||
if (args.id)
|
||||
return [queue.find(x => (x.id === args.id))];
|
||||
else
|
||||
return queue.slice(args.offset, args.offset + args.first);
|
||||
|
||||
}
|
||||
|
||||
get playing() {
|
||||
return this.dj.playing;
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return this.dj.connected;
|
||||
}
|
||||
|
||||
get paused() {
|
||||
return this.dj.disp? this.dj.disp.paused : false;
|
||||
}
|
||||
|
||||
get queueCount() {
|
||||
return this.dj.queue.length;
|
||||
}
|
||||
|
||||
get songStartTime() {
|
||||
return this.dj.disp.player.streamingData.startTime;
|
||||
}
|
||||
|
||||
get volume() {
|
||||
return this.dj.volume;
|
||||
}
|
||||
|
||||
get repeat() {
|
||||
return this.dj.repeat;
|
||||
}
|
||||
|
||||
get currentSong() {
|
||||
let x = this.dj.current;
|
||||
return {
|
||||
id: generateID(['Media', x.url]),
|
||||
name: x.title,
|
||||
url: x.url,
|
||||
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(x.url)
|
||||
};
|
||||
}
|
||||
|
||||
get voiceChannel() {
|
||||
return this.dj.voiceChannel.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for graphql access to the discord.js Guild and lib/guilding/GuildHandler
|
||||
*/
|
||||
class Guild {
|
||||
constructor(discordGuild, guildHandler) {
|
||||
this.id = generateID(['Guild', discordGuild.id]);
|
||||
this.discordId = discordGuild.id;
|
||||
this.name = discordGuild.name;
|
||||
this.owner = new GuildMember(discordGuild.owner);
|
||||
this.memberCount = discordGuild.memberCount;
|
||||
this.icon = discordGuild.iconURL;
|
||||
this.prMembers = Array.from(discordGuild.members.values())
|
||||
.map((x) => new GuildMember(x));
|
||||
this.prRoles = Array.from(discordGuild.roles.values())
|
||||
.map((x) => new Role(x));
|
||||
guildHandler = guildHandler || {};
|
||||
this.ready = guildHandler.ready;
|
||||
this.prSaved = null;
|
||||
this.guildHandler = guildHandler;
|
||||
this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null;
|
||||
}
|
||||
|
||||
async querySaved() {
|
||||
if (this.guildHandler.db) {
|
||||
let saved = [];
|
||||
let rows = await this.guildHandler.db.all('SELECT * FROM playlists');
|
||||
for (let row of rows)
|
||||
saved.push({
|
||||
id: generateID(['Media', row.url]),
|
||||
name: row.name,
|
||||
url: row.url,
|
||||
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(row.url)
|
||||
});
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
|
||||
async saved(args) {
|
||||
let result = await this.querySaved();
|
||||
if (args.id)
|
||||
return [result.find(x => (x.id === args.id))];
|
||||
else if (args.name)
|
||||
return [result.find(x => (x.name === args.name))];
|
||||
else
|
||||
return result.slice(args.offset, args.offset + args.first);
|
||||
}
|
||||
|
||||
roles(args) {
|
||||
if (args.id)
|
||||
return [this.prRoles.find(x => (x.id === args.id))];
|
||||
else
|
||||
return this.prRoles.slice(args.offset, args.offset + args.first);
|
||||
|
||||
}
|
||||
|
||||
members(args) {
|
||||
if (args.id)
|
||||
return [this.prMembers.find(x => (x.id === args.id))];
|
||||
else
|
||||
return this.prMembers.slice(args.offset, args.offset + args.first);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for graphql access to the discord.js Role
|
||||
*/
|
||||
class Role {
|
||||
constructor(discordRole) {
|
||||
this.id = generateID(['Role', discordRole.id]);
|
||||
this.discordId = discordRole.id;
|
||||
this.name = discordRole.name;
|
||||
this.color = discordRole.hexColor;
|
||||
this.prMembers = Array.from(discordRole.members.values)
|
||||
.map((x) => new GuildMember(x));
|
||||
}
|
||||
|
||||
members(args) {
|
||||
if (args.id)
|
||||
return [this.prMembers.find(x => (x.id === args.id))];
|
||||
else
|
||||
return this.prMembers.slice(args.offset, args.offset + args.first);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for graphql access to the discord.js GuildMember
|
||||
*/
|
||||
class GuildMember {
|
||||
constructor(discordGuildMember) {
|
||||
this.id = generateID(['GuildMember', discordGuildMember.id]);
|
||||
this.discordId = discordGuildMember.id;
|
||||
this.user = new User(discordGuildMember.user);
|
||||
this.nickname = discordGuildMember.nickname;
|
||||
this.prRoles = Array.from(discordGuildMember.roles.values())
|
||||
.map((x) => new Role(x));
|
||||
this.highestRole = new Role(discordGuildMember.highestRole);
|
||||
}
|
||||
|
||||
roles(args) {
|
||||
if (args.id)
|
||||
return [this.prRoles.find(x => (x.id === args.id))];
|
||||
else
|
||||
return this.prRoles.slice(args.offset, args.offset + args.first);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for graphql access to the discord.js User
|
||||
*/
|
||||
class User {
|
||||
constructor(discordUser) {
|
||||
this.id = generateID(['User', discordUser.id]);
|
||||
this.discordId = discordUser.id;
|
||||
this.name = discordUser.username;
|
||||
this.avatar = discordUser.avatarURL;
|
||||
this.bot = discordUser.bot;
|
||||
this.tag = discordUser.tag;
|
||||
this.tag = discordUser.tag;
|
||||
this.presence = {
|
||||
game: discordUser.presence.game? discordUser.presence.game.name : null,
|
||||
status: discordUser.presence.status
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for graphql access to log entries
|
||||
*/
|
||||
class LogEntry {
|
||||
constructor(entry) {
|
||||
this.id = generateID(['LogEntry', entry.level, entry.timestamp]);
|
||||
this.message = entry.message;
|
||||
this.timestamp = entry.timestamp;
|
||||
this.level = entry.level;
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
type Presence {
|
||||
game: String
|
||||
status: String
|
||||
}
|
||||
type User {
|
||||
id: ID!
|
||||
discordId: String
|
||||
name: String!
|
||||
avatar: String
|
||||
bot: Boolean
|
||||
tag: String!
|
||||
presence: Presence
|
||||
}
|
||||
type Role {
|
||||
id: ID!
|
||||
discordId: String
|
||||
name: String
|
||||
color: String
|
||||
members(first: Int = 10, offset: Int = 0, id: String): [GuildMember]
|
||||
}
|
||||
type GuildMember {
|
||||
id: ID!
|
||||
discordId: String
|
||||
user: User
|
||||
nickname: String
|
||||
roles(first: Int = 10, offset: Int = 0, id: String): [Role]
|
||||
highestRole: Role
|
||||
}
|
||||
type DJ {
|
||||
queue(first: Int = 10, offset: Int = 0, id: String): [MediaEntry]
|
||||
queueCount: Int!
|
||||
songStartTime: String
|
||||
playing: Boolean!
|
||||
volume: Float
|
||||
repeat: Boolean
|
||||
currentSong: MediaEntry
|
||||
quality: String
|
||||
voiceChannel: String
|
||||
connected: Boolean!
|
||||
paused: Boolean!
|
||||
}
|
||||
type Guild {
|
||||
id: ID!
|
||||
discordId: String
|
||||
name: String
|
||||
owner: GuildMember
|
||||
dj: DJ
|
||||
members(first: Int = 10, offset: Int = 0, id: String): [GuildMember]
|
||||
memberCount: Int!
|
||||
roles(first: Int = 10, offset: Int = 0, id: String): [Role]
|
||||
icon: String
|
||||
ready: Boolean
|
||||
saved(first: Int = 10, offset: Int = 0, id: String, name: String): [MediaEntry!]
|
||||
savedCount: Int!
|
||||
}
|
||||
type Client {
|
||||
guilds(first: Int = 10, offset: Int = 0, id: String): [Guild]
|
||||
guildCount: Int
|
||||
voiceConnectionCount: Int
|
||||
user: User
|
||||
ping: Float
|
||||
status: Int
|
||||
uptime: Int
|
||||
}
|
||||
type MediaEntry {
|
||||
id: ID!
|
||||
url: String!
|
||||
name: String!
|
||||
thumbnail: String
|
||||
}
|
||||
type LogEntry {
|
||||
id: ID!
|
||||
message: String
|
||||
level: String
|
||||
timestamp: String
|
||||
}
|
||||
type Query {
|
||||
client: Client
|
||||
presences: [String]!
|
||||
config: String
|
||||
prefix: String
|
||||
logs(first: Int, offset: Int = 0, id: String, last: Int = 10, level: String): [LogEntry]
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
doctype html
|
||||
head
|
||||
meta(charset='UTF-8')
|
||||
title Dashboard
|
||||
script(src='https://code.jquery.com/jquery-3.3.1.min.js')
|
||||
script(type='text/javascript' src='https://momentjs.com/downloads/moment.min.js')
|
||||
link(type='text/css' rel='stylesheet' href='sass/style.sass')
|
||||
script(type='text/javascript' src='scripts/query.js')
|
||||
#content
|
||||
#column-left.column
|
||||
h2.cell Logs
|
||||
#log-container.listContainer
|
||||
#column-middle.column
|
||||
#avatar-container
|
||||
#status-indicator
|
||||
img#user-avatar.cell(src='' alt='Avatar')
|
||||
h3#user-tag.cell
|
||||
h4#user-game.cell
|
||||
.space
|
||||
h2.cell Status
|
||||
.cell
|
||||
span.label.text-right Ping:
|
||||
span#client-ping.text-left
|
||||
.cell
|
||||
span.label.text-right Uptime:
|
||||
span#client-uptime.text-left
|
||||
.cell
|
||||
span.label.text-right Socket Status:
|
||||
span#client-status.text-left
|
||||
.cell
|
||||
span.label.text-right Guild Count:
|
||||
span#client-guildCount.text-left
|
||||
.cell
|
||||
span.label.text-right Active Voice Connections:
|
||||
span#client-vcCount.text-left
|
||||
#column-right.column
|
||||
select#guild-select.cell
|
||||
option(value='select-default') -Select a guild-
|
||||
#guildinfo(style='display: none')
|
||||
.listContainer
|
||||
#guild-icon-container.cell
|
||||
img#guild-icon(src='' alt='Icon')
|
||||
#guild-nameAndIcon.listContainer
|
||||
h2#guild-name.cell
|
||||
p.cell by
|
||||
h3#guild-owner.cell
|
||||
.space
|
||||
h3.cell Stats
|
||||
.cell
|
||||
span.label.text-right Member Count:
|
||||
span#guild-memberCount.text-left
|
||||
.space
|
||||
h3.cell DJ
|
||||
.cell
|
||||
span.label.text-right State:
|
||||
span#guild-djStatus.text-left
|
||||
.cell
|
||||
span.label.text-right Repeat:
|
||||
span#dj-repeat.text-left
|
||||
.cell
|
||||
span.label.text-right Voice Channel:
|
||||
span#dj-voiceChannel.text-left
|
||||
#dj-songinfo.listContainer(style='display: none')
|
||||
a#songinfo-container
|
||||
span#dj-songname
|
||||
img#dj-songImg(src='' alt='')
|
||||
#dj-songProgress(style='display:none')
|
||||
span#dj-songCurrentTS
|
||||
#dj-queue-container
|
||||
span.cell.label(id='Queue Song count')
|
||||
span#dj-queueCount
|
||||
| Songs in Queue
|
||||
span.cell
|
||||
| Next
|
||||
span#dj-queueDisplayCount 0
|
||||
| Songs:
|
||||
#dj-songQueue
|
||||
script.
|
||||
startUpdating();
|
@ -0,0 +1,14 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
link(type='text/css' rel='stylesheet' href='sass/style.sass')
|
||||
script(type='text/javascript' src='scripts/lib/sha512.min.js')
|
||||
script(src='https://code.jquery.com/jquery-3.3.1.min.js')
|
||||
script(type='text/javascript' src='scripts/login.js')
|
||||
body
|
||||
.listContainer
|
||||
h1(class='cell') Login
|
||||
h3(class='cell') #{message}
|
||||
input(class='cell' id='username' name='username' type='text' required placeholder='Username' onkeypress=handleSubmit)
|
||||
input(class='cell' id='password' name='password' type='password' required placeholder='Password' onkeypress=handleSubmit)
|
||||
button(class='cell' type='submit' onclick='login()') Log in
|
@ -0,0 +1,280 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Ubuntu')
|
||||
@import vars
|
||||
|
||||
body
|
||||
font-family: $fNormal
|
||||
color: $cPrimary
|
||||
background-color: $cBackground
|
||||
overflow: hidden
|
||||
max-height: 100%
|
||||
max-width: 100%
|
||||
::-webkit-scrollbar
|
||||
width: 12px
|
||||
height: 12px
|
||||
::-webkit-scrollbar-thumb
|
||||
background: darken($cBackground, 5)
|
||||
border-radius: 10px
|
||||
::-webkit-scrollbar-track
|
||||
background: lighten($cBackground, 5)
|
||||
border-radius: 10px
|
||||
|
||||
input
|
||||
color: $cPrimary
|
||||
background: $cBackground
|
||||
border: 2px solid $cPrimary
|
||||
border-radius: 12px
|
||||
padding: 5px
|
||||
margin: auto
|
||||
|
||||
input:focus
|
||||
background: $cBackgroundVariant
|
||||
|
||||
input::placeholder
|
||||
color: darken($cPrimary, 20)
|
||||
|
||||
input.cell
|
||||
margin: 10px auto
|
||||
|
||||
button
|
||||
background: $cBackgroundVariant
|
||||
border: none
|
||||
border-radius: 12px
|
||||
color: $cPrimary
|
||||
padding: 10px
|
||||
|
||||
.column
|
||||
display: table-column
|
||||
padding: 20px
|
||||
align-content: center
|
||||
margin: 0 auto
|
||||
text-align: center
|
||||
max-height: 100vh
|
||||
height: 100vh
|
||||
|
||||
.cell
|
||||
display: list-item
|
||||
list-style: none
|
||||
align-content: center
|
||||
text-align: center
|
||||
margin: auto
|
||||
user-select: none
|
||||
|
||||
.space
|
||||
height: 20px
|
||||
|
||||
h2.cell
|
||||
padding: 5px
|
||||
|
||||
div.cell
|
||||
display: flex
|
||||
align-items: center
|
||||
width: 100%
|
||||
position: relative
|
||||
|
||||
div.cell > *
|
||||
display: table-cell
|
||||
align-items: center
|
||||
width: 100%
|
||||
padding: 2px 5px
|
||||
|
||||
.text-left
|
||||
text-align: left
|
||||
|
||||
.text-right
|
||||
text-align: right
|
||||
|
||||
.label
|
||||
font-weight: bold
|
||||
|
||||
.listContainer
|
||||
display: grid
|
||||
width: 100%
|
||||
text-align: left
|
||||
overflow: hidden
|
||||
position: relative
|
||||
max-height: 90vh
|
||||
|
||||
.listContainer:hover
|
||||
overflow: auto
|
||||
|
||||
.logEntry
|
||||
display: list-item
|
||||
list-style: none
|
||||
padding: 5px
|
||||
border-radius: 10px
|
||||
margin: 5px
|
||||
color: $cOnSurfaceVariant
|
||||
user-select: none
|
||||
position: relative
|
||||
font-size: 110%
|
||||
|
||||
.logEntry[level=debug]
|
||||
background: $cDebug
|
||||
|
||||
.logEntry[level=verbose]
|
||||
background: $cVerbose
|
||||
|
||||
.logEntry[level=info]
|
||||
background: $cInfo
|
||||
|
||||
.logEntry[level=warn]
|
||||
background: $cWarn
|
||||
|
||||
.logEntry[level=warning]
|
||||
background: $cWarn
|
||||
user-select: all
|
||||
|
||||
.logEntry[level=error]
|
||||
background: $cError
|
||||
user-select: all
|
||||
|
||||
.logEntry .infodiv
|
||||
display: flex
|
||||
list-style: none
|
||||
font-size: 75%
|
||||
width: 100%
|
||||
|
||||
.logEntry .infodiv span
|
||||
padding: 0 2px
|
||||
margin: auto
|
||||
width: 50%
|
||||
display: table-cell
|
||||
|
||||
.songEntry
|
||||
display: flex
|
||||
background: lighten($cBackgroundVariant, 5)
|
||||
padding: 2px
|
||||
margin: 5px
|
||||
border-radius: 5px
|
||||
text-decoration: none
|
||||
color: $cPrimary
|
||||
> *
|
||||
display: table-column
|
||||
margin: auto
|
||||
img
|
||||
max-height: 30px
|
||||
max-width: 20%
|
||||
height: auto
|
||||
width: auto
|
||||
border-radius: 2px
|
||||
a
|
||||
width: 80%
|
||||
text-decoration: none
|
||||
color: $cPrimary
|
||||
|
||||
#content
|
||||
display: flex
|
||||
height: 100%
|
||||
width: 100%
|
||||
background-color: $cBackground
|
||||
|
||||
#column-left, #column-middle, #column-right
|
||||
width: 33%
|
||||
|
||||
#column-middle
|
||||
background: $cBackgroundVariant
|
||||
border-radius: 20px
|
||||
height: 100%
|
||||
|
||||
#column-right
|
||||
padding: 0 20px 20px
|
||||
display: grid
|
||||
align-content: start
|
||||
|
||||
#user-avatar
|
||||
max-width: 300px
|
||||
width: 100%
|
||||
height: auto
|
||||
border-radius: 25%
|
||||
|
||||
#avatar-container
|
||||
max-width: 300px
|
||||
width: 100%
|
||||
margin: auto
|
||||
position: relative
|
||||
|
||||
#status-indicator
|
||||
height: 20%
|
||||
width: 20%
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
border-radius: 25%
|
||||
display: block
|
||||
z-index: 200
|
||||
|
||||
#status-indicator[status=online]
|
||||
background-color: $cOnline
|
||||
|
||||
#status-indicator[status=idle]
|
||||
background-color: $cIdle
|
||||
|
||||
#status-indicator[status=dnd]
|
||||
background-color: $cDnd
|
||||
|
||||
#status-indicator[status=offline]
|
||||
background-color: $cOffline
|
||||
|
||||
#guild-select
|
||||
background: $cBackgroundVariant
|
||||
color: $cPrimary
|
||||
font-size: 150%
|
||||
font-family: $fNormal
|
||||
padding: 10px
|
||||
width: 100%
|
||||
margin: auto
|
||||
border: none
|
||||
height: 52px
|
||||
border-radius: 12px
|
||||
-webkit-appearance: none
|
||||
|
||||
#guild-icon-container
|
||||
padding: 10px 0 0 0
|
||||
display: flex
|
||||
|
||||
#guild-icon
|
||||
max-width: 100px
|
||||
width: 50%
|
||||
height: auto
|
||||
border-radius: 25%
|
||||
|
||||
#guild-nameAndIcon
|
||||
width: 50%
|
||||
|
||||
#dj-songinfo
|
||||
background-color: $cBackgroundVariant
|
||||
border-radius: 20px
|
||||
overflow-x: hidden
|
||||
|
||||
#songinfo-container
|
||||
display: list-item
|
||||
text-decoration: none
|
||||
color: $cPrimary
|
||||
padding: 10px
|
||||
width: calc(100% - 20px)
|
||||
|
||||
#dj-queue-container
|
||||
display: grid
|
||||
padding: 0 5px 5px
|
||||
|
||||
#dj-songname
|
||||
font-weight: bold
|
||||
font-size: 120%
|
||||
|
||||
#dj-songImg
|
||||
align-self: center
|
||||
width: 80%
|
||||
height: auto
|
||||
margin: 0 10%
|
||||
border-radius: 5%
|
||||
|
||||
#guildinfo
|
||||
max-height: 90vh
|
||||
overflow-y: hidden
|
||||
|
||||
#guildinfo:hover
|
||||
overflow-y: auto
|
||||
|
||||
#dj-songQueue
|
||||
display: grid
|
||||
max-height: 100%
|
@ -0,0 +1,31 @@
|
||||
$cPrimary: #fff
|
||||
$cPrimaryVariant: #4c10a5
|
||||
$cSecondary: #c889f5
|
||||
$cSecondaryVariant: #740bce
|
||||
$cBackground: #77f
|
||||
$cBackgroundVariant: #55b
|
||||
$cSurface: #fff
|
||||
$cSurfaceVariant: #000
|
||||
$cError: #f59289
|
||||
$cErrorVariant: #b20a00
|
||||
$cOnPrimary: #fff
|
||||
$cOnSecondary: #000
|
||||
$cOnSurface: #000
|
||||
$cOnSurfaceShadow: lighten($cOnSurface, 30%)
|
||||
$cOnSurfaceVariant: #fff
|
||||
$cOnBackground: #000
|
||||
$cOnBackgroundShadow: lighten($cOnBackground, 30%)
|
||||
$cOnBackgroundVariant: #fff
|
||||
$cOnError: #000
|
||||
$cOnErrorVariant: #fff
|
||||
$cOnline: #0f0
|
||||
$cIdle: #ff0
|
||||
$cDnd: #f00
|
||||
$cOffline: #888
|
||||
$cDebug: #00f
|
||||
$cVerbose: #088
|
||||
$cInfo: #890
|
||||
$cWarn: #a60
|
||||
$cError: #a00
|
||||
|
||||
$fNormal: Ubuntu, sans-serif
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,26 @@
|
||||
/* eslint-disable */
|
||||
|
||||
function login() {
|
||||
let username = document.querySelector('#username').value;
|
||||
let password = sha512(document.querySelector('#password').value);
|
||||
$.post({
|
||||
url: "/",
|
||||
data: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
contentType: "application/json"
|
||||
}).done((res) => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
if (!e)
|
||||
e = window.event;
|
||||
if (e.which === 13) {
|
||||
login();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleSubmit, false);
|
@ -0,0 +1,308 @@
|
||||
/* eslint-disable */
|
||||
|
||||
let latestLogs = [];
|
||||
|
||||
let status = {
|
||||
0: 'ready',
|
||||
1: 'connecting',
|
||||
2: 'reconnecting',
|
||||
3: 'idle',
|
||||
4: 'nearly',
|
||||
5: 'disconnected'
|
||||
};
|
||||
|
||||
function getSplitDuration (duration) {
|
||||
let dur = duration;
|
||||
let retObj = {};
|
||||
retObj.milliseconds = dur % 1000;
|
||||
dur = Math.floor(dur / 1000);
|
||||
retObj.seconds = dur % 60;
|
||||
dur = Math.floor(dur / 60);
|
||||
retObj.minutes = dur % 60;
|
||||
dur = Math.floor(dur / 60);
|
||||
retObj.hours = dur % 24;
|
||||
dur = Math.floor(dur / 24);
|
||||
retObj.days = dur;
|
||||
return retObj;
|
||||
}
|
||||
|
||||
function postQuery(query) {
|
||||
return new Promise((resolve) => {
|
||||
$.post({
|
||||
url: "/graphql",
|
||||
data: JSON.stringify({
|
||||
query: query
|
||||
}),
|
||||
contentType: "application/json"
|
||||
}).done((res) => resolve(res));
|
||||
});
|
||||
}
|
||||
|
||||
function queryStatic() {
|
||||
let query = `{
|
||||
client {
|
||||
user {
|
||||
tag
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}`;
|
||||
postQuery(query).then((res) => {
|
||||
let d = res.data;
|
||||
document.querySelector('#user-avatar').setAttribute('src', d.client.user.avatar);
|
||||
document.querySelector('#user-tag').innerText = d.client.user.tag;
|
||||
});
|
||||
}
|
||||
|
||||
function queryGuilds() {
|
||||
let query = `{
|
||||
client {
|
||||
guilds {
|
||||
id
|
||||
name
|
||||
dj {
|
||||
playing
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
postQuery(query).then((res) => {
|
||||
for (let guild of res.data.client.guilds)
|
||||
if ($(`option[value=${guild.id}]`).length === 0) {
|
||||
let option = document.createElement('option');
|
||||
option.setAttribute('value', guild.id);
|
||||
if (guild.dj)
|
||||
option.innerText = guild.dj.playing? guild.name + ' 🎶' : guild.name;
|
||||
let guildSelect = document.querySelector('#guild-select');
|
||||
guildSelect.appendChild(option);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function queryGuild(guildId) {
|
||||
let query = `{
|
||||
client {
|
||||
guilds(id: "${guildId}") {
|
||||
name
|
||||
icon
|
||||
memberCount
|
||||
owner {
|
||||
id
|
||||
user {
|
||||
tag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config
|
||||
}`;
|
||||
postQuery(query).then((res) => {
|
||||
let guild = res.data.client.guilds[0];
|
||||
document.querySelector('#guild-icon').setAttribute('src', guild.icon);
|
||||
document.querySelector('#guild-name').innerText = guild.name;
|
||||
document.querySelector('#guild-owner').innerText = guild.owner.user.tag;
|
||||
document.querySelector('#guild-owner').setAttribute('owner-id', guild.owner.id);
|
||||
document.querySelector('#guild-memberCount').innerText = guild.memberCount;
|
||||
queryGuildStatus(guildId);
|
||||
let serverinfo = $('#guildinfo');
|
||||
if (serverinfo.is(':hidden'))
|
||||
serverinfo.show();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param guildId
|
||||
*/
|
||||
function queryGuildStatus(guildId) {
|
||||
let query = `{
|
||||
client {
|
||||
guilds(id: "${guildId}") {
|
||||
dj {
|
||||
playing
|
||||
connected
|
||||
repeat
|
||||
voiceChannel
|
||||
songStartTime
|
||||
paused
|
||||
currentSong {
|
||||
name
|
||||
url
|
||||
thumbnail
|
||||
}
|
||||
queueCount
|
||||
queue(first: 5) {
|
||||
id
|
||||
name
|
||||
url
|
||||
thumbnail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config
|
||||
}`;
|
||||
postQuery(query).then((res) => {
|
||||
let guild = res.data.client.guilds[0];
|
||||
document.querySelector('#dj-repeat').innerText = guild.dj.repeat? 'on': 'off';
|
||||
document.querySelector('#guild-djStatus').innerText = guild.dj.connected? 'connected' : 'disconnected';
|
||||
if (guild.dj.connected) {
|
||||
let songinfoContainer = $('#dj-songinfo');
|
||||
songinfoContainer.show();
|
||||
document.querySelector('#guild-djStatus').innerText = guild.dj.playing? 'playing' : 'connected';
|
||||
document.querySelector('#dj-voiceChannel').innerText = guild.dj.voiceChannel;
|
||||
|
||||
|
||||
if (guild.dj.playing) {
|
||||
if (songinfoContainer.is(':hidden'))
|
||||
songinfoContainer.show();
|
||||
document.querySelector('#guild-djStatus').innerText = guild.dj.paused? 'paused' : 'playing';
|
||||
document.querySelector('#songinfo-container').setAttribute('href', guild.dj.currentSong.url);
|
||||
document.querySelector('#dj-songname').innerText = guild.dj.currentSong.name;
|
||||
document.querySelector('#dj-songImg').setAttribute('src', guild.dj.currentSong.thumbnail.replace('maxresdefault', 'mqdefault'));
|
||||
let songSd = getSplitDuration(Date.now() - guild.dj.songStartTime);
|
||||
document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
|
||||
document.querySelector('#dj-songCurrentTS').setAttribute('start-ts', guild.dj.songStartTime);
|
||||
document.querySelector('#dj-queueCount').innerText = guild.dj.queueCount;
|
||||
let songContainer = document.querySelector('#dj-songQueue');
|
||||
$('.songEntry').remove();
|
||||
for (let song of guild.dj.queue) {
|
||||
let songEntry = document.createElement('a');
|
||||
songEntry.setAttribute('href', song.url);
|
||||
songEntry.setAttribute('class', 'songEntry');
|
||||
songEntry.setAttribute('song-id', song.id);
|
||||
let imageEntry = document.createElement('img');
|
||||
imageEntry.setAttribute('src', song.thumbnail.replace('maxresdefault', 'mqdefault'));
|
||||
songEntry.appendChild(imageEntry);
|
||||
let nameEntry = document.createElement('a');
|
||||
nameEntry.innerText = song.name;
|
||||
songEntry.appendChild(nameEntry);
|
||||
songContainer.appendChild(songEntry);
|
||||
}
|
||||
document.querySelector('#dj-queueDisplayCount').innerText = document.querySelectorAll('.songEntry').length;
|
||||
} else {
|
||||
if (songinfoContainer.is(':not(:hidden)'))
|
||||
songinfoContainer.hide();
|
||||
}
|
||||
} else {
|
||||
$('#dj-songinfo').hide();
|
||||
document.querySelector('#dj-voiceChannel').innerText = 'None';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function queryStatus() {
|
||||
let query = `{
|
||||
client {
|
||||
ping
|
||||
status
|
||||
uptime
|
||||
guildCount
|
||||
voiceConnectionCount
|
||||
user {
|
||||
presence {
|
||||
game
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
postQuery(query).then((res) => {
|
||||
let d = res.data;
|
||||
document.querySelector('#client-ping').innerText = Math.round(d.client.ping * 10)/10 + ' ms';
|
||||
document.querySelector('#client-status').innerText = status[d.client.status];
|
||||
|
||||
let sd = getSplitDuration(d.client.uptime);
|
||||
document.querySelector('#client-uptime')
|
||||
.innerText = `${sd.days}d ${sd.hours}h ${sd.minutes}min ${sd.seconds}s`;
|
||||
|
||||
document.querySelector('#client-guildCount').innerText = d.client.guildCount;
|
||||
document.querySelector('#client-vcCount').innerText = d.client.voiceConnectionCount;
|
||||
if (d.client.status !== 0)
|
||||
document.querySelector('#status-indicator').setAttribute('status', 'offline');
|
||||
else
|
||||
document.querySelector('#status-indicator').setAttribute('status', d.client.user.presence.status);
|
||||
|
||||
document.querySelector('#user-game').innerText = d.client.user.presence.game;
|
||||
|
||||
setTimeout(() => {
|
||||
let sd = getSplitDuration(d.client.uptime + 1000);
|
||||
document.querySelector('#client-uptime')
|
||||
.innerText = `${sd.days}d ${sd.hours}h ${sd.minutes}min ${sd.seconds}s`;
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function queryLogs(count) {
|
||||
count = count || 5;
|
||||
let query = `{
|
||||
logs(last: ${count}, level: "verbose"){
|
||||
id
|
||||
level
|
||||
message
|
||||
timestamp
|
||||
}
|
||||
}`;
|
||||
postQuery(query).then((res) => {
|
||||
let d = res.data;
|
||||
for (let logEntry of d.logs)
|
||||
if (!latestLogs.find((x) => x.id === logEntry.id)) {
|
||||
let entryElem = document.createElement('div');
|
||||
entryElem.setAttribute('class', 'logEntry text-left');
|
||||
entryElem.setAttribute('log-id', logEntry.id);
|
||||
entryElem.setAttribute('level', logEntry.level);
|
||||
let infoDiv = document.createElement('div');
|
||||
infoDiv.setAttribute('class', 'infodiv');
|
||||
let lvlSpan = document.createElement('span');
|
||||
lvlSpan.innerText = logEntry.level;
|
||||
lvlSpan.setAttribute('class', 'text-left');
|
||||
infoDiv.appendChild(lvlSpan);
|
||||
let tsSpan = document.createElement('span');
|
||||
tsSpan.setAttribute('timestamp', logEntry.timestamp);
|
||||
tsSpan.innerText = moment(logEntry.timestamp, 'YY-MM-DD-HH-mm-ss').format('MMM Do HH:mm:ss');
|
||||
tsSpan.setAttribute('class', 'text-right');
|
||||
infoDiv.appendChild(tsSpan);
|
||||
entryElem.appendChild(infoDiv);
|
||||
let msgSpan = document.createElement('span');
|
||||
msgSpan.innerText = logEntry.message;
|
||||
msgSpan.setAttribute('class', 'message');
|
||||
entryElem.appendChild(msgSpan);
|
||||
let logContainer = document.querySelector('#log-container');
|
||||
logContainer.insertBefore(entryElem, logContainer.firstChild);
|
||||
}
|
||||
|
||||
latestLogs = d.logs;
|
||||
});
|
||||
}
|
||||
|
||||
function startUpdating() {
|
||||
queryStatic();
|
||||
setInterval(queryStatic, 3600000);
|
||||
queryStatus();
|
||||
setInterval(queryStatus, 2000);
|
||||
queryLogs(50);
|
||||
setInterval(queryLogs, 5000);
|
||||
queryGuilds();
|
||||
setInterval(queryGuilds, 60000);
|
||||
setInterval(() => {
|
||||
let gid = $('#guild-select')[0].value;
|
||||
if (gid && gid !== 'select-default')
|
||||
queryGuildStatus(gid);
|
||||
}, 5000);
|
||||
setInterval(() => {
|
||||
let gid = $('#guild-select')[0].value;
|
||||
if (gid && gid !== 'select-default')
|
||||
queryGuild(gid);
|
||||
}, 600000);
|
||||
$('#guild-select').on('change', (ev) => {
|
||||
let fch = document.querySelector('#guild-select').firstElementChild;
|
||||
if (fch.getAttribute('value') === 'select-default')
|
||||
fch.remove();
|
||||
let guildId = ev.target.value;
|
||||
queryGuild(guildId);
|
||||
});
|
||||
setInterval(() => {
|
||||
let songSd = getSplitDuration(Date.now() - $('#dj-songCurrentTS').attr('start-ts'));
|
||||
document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
|
||||
}, 500);
|
||||
}
|
Loading…
Reference in New Issue