karaoke-eternal 1.0.0 → 2.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/build/client/447.83b0127845c2fa8729fe.js +1 -0
- package/build/client/715.83b0127845c2fa8729fe.js +1 -0
- package/build/client/718.83b0127845c2fa8729fe.js +1 -0
- package/build/client/851.83b0127845c2fa8729fe.js +1 -0
- package/build/{845.4be526e3a94d53aeceae.css → client/958.83b0127845c2fa8729fe.css} +53 -6
- package/build/client/958.83b0127845c2fa8729fe.js +1 -0
- package/build/{index.html → client/index.html} +1 -1
- package/build/{licenses.txt → client/licenses.txt} +208 -496
- package/build/client/main.83b0127845c2fa8729fe.css +2341 -0
- package/build/client/main.83b0127845c2fa8729fe.js +1 -0
- package/build/server/Library/Library.js +297 -0
- package/build/server/Library/ipc.js +13 -0
- package/build/server/Library/router.js +20 -0
- package/build/server/Library/socket.js +35 -0
- package/build/server/Media/Media.js +170 -0
- package/build/server/Media/fileTypes.js +8 -0
- package/build/server/Media/ipc.js +13 -0
- package/build/server/Media/router.js +97 -0
- package/build/server/Player/socket.js +66 -0
- package/build/server/Prefs/Prefs.js +181 -0
- package/build/server/Prefs/router.js +151 -0
- package/build/server/Prefs/socket.js +52 -0
- package/build/server/Queue/Queue.js +203 -0
- package/build/server/Queue/socket.js +83 -0
- package/build/server/Rooms/Rooms.js +171 -0
- package/build/server/Rooms/router.js +97 -0
- package/build/server/Rooms/socket.js +23 -0
- package/build/server/Scanner/FileScanner/FileScanner.js +166 -0
- package/build/server/Scanner/FileScanner/getConfig.js +32 -0
- package/build/server/Scanner/FileScanner/getFiles.js +61 -0
- package/build/server/Scanner/MetaParser/MetaParser.js +77 -0
- package/build/server/Scanner/MetaParser/defaultMiddleware.js +170 -0
- package/build/server/Scanner/Scanner.js +26 -0
- package/build/server/Scanner/ScannerQueue.js +62 -0
- package/build/server/User/User.js +206 -0
- package/build/server/User/router.js +366 -0
- package/build/server/lib/Database.js +39 -0
- package/build/server/lib/Errors.js +6 -0
- package/build/server/lib/IPCBridge.js +128 -0
- package/build/server/lib/Log.js +31 -0
- package/build/server/lib/accumulatedThrottle.js +16 -0
- package/build/server/lib/bcrypt.js +23 -0
- package/build/server/lib/cli.js +131 -0
- package/build/server/lib/getCdgName.js +18 -0
- package/build/server/lib/getFolders.js +8 -0
- package/build/server/lib/getHotMiddleware.js +22 -0
- package/build/server/lib/getIPAddress.js +14 -0
- package/build/server/lib/getPermutations.js +17 -0
- package/build/server/lib/getWindowsDrives.js +17 -0
- package/build/server/lib/parseCookie.js +13 -0
- package/build/server/lib/pushQueuesAndLibrary.js +22 -0
- package/{server → build/server}/lib/schemas/001-initial-schema.sql +26 -26
- package/build/server/lib/schemas/004-paths-rooms-data.sql +7 -0
- package/build/server/lib/schemas/005-roles.sql +32 -0
- package/build/server/lib/util.js +39 -0
- package/build/server/main.js +124 -0
- package/build/server/scannerWorker.js +59 -0
- package/build/server/serverWorker.js +219 -0
- package/build/server/socket.js +134 -0
- package/build/server/watcherWorker.js +51 -0
- package/build/shared/actionTypes.js +113 -0
- package/build/shared/types.js +1 -0
- package/package.json +111 -86
- package/build/267.4be526e3a94d53aeceae.js +0 -1
- package/build/591.4be526e3a94d53aeceae.js +0 -1
- package/build/598.4be526e3a94d53aeceae.js +0 -1
- package/build/799.4be526e3a94d53aeceae.js +0 -1
- package/build/845.4be526e3a94d53aeceae.js +0 -1
- package/build/main.4be526e3a94d53aeceae.css +0 -2034
- package/build/main.4be526e3a94d53aeceae.js +0 -1
- package/server/Library/Library.js +0 -340
- package/server/Library/index.js +0 -3
- package/server/Library/ipc.js +0 -18
- package/server/Library/router.js +0 -27
- package/server/Library/socket.js +0 -47
- package/server/Media/Media.js +0 -207
- package/server/Media/index.js +0 -3
- package/server/Media/ipc.js +0 -19
- package/server/Media/router.js +0 -99
- package/server/Player/socket.js +0 -78
- package/server/Prefs/Prefs.js +0 -165
- package/server/Prefs/index.js +0 -3
- package/server/Prefs/router.js +0 -124
- package/server/Prefs/socket.js +0 -68
- package/server/Queue/Queue.js +0 -208
- package/server/Queue/index.js +0 -3
- package/server/Queue/socket.js +0 -99
- package/server/Rooms/Rooms.js +0 -114
- package/server/Rooms/index.js +0 -3
- package/server/Rooms/router.js +0 -146
- package/server/Scanner/FileScanner/FileScanner.js +0 -225
- package/server/Scanner/FileScanner/getConfig.js +0 -35
- package/server/Scanner/FileScanner/getFiles.js +0 -63
- package/server/Scanner/FileScanner/index.js +0 -3
- package/server/Scanner/MetaParser/MetaParser.js +0 -49
- package/server/Scanner/MetaParser/defaultMiddleware.js +0 -197
- package/server/Scanner/MetaParser/index.js +0 -3
- package/server/Scanner/Scanner.js +0 -33
- package/server/User/User.js +0 -139
- package/server/User/index.js +0 -3
- package/server/User/router.js +0 -442
- package/server/lib/Database.js +0 -55
- package/server/lib/IPCBridge.js +0 -115
- package/server/lib/Log.js +0 -71
- package/server/lib/bcrypt.js +0 -24
- package/server/lib/cli.js +0 -136
- package/server/lib/electron.js +0 -81
- package/server/lib/getCdgName.js +0 -20
- package/server/lib/getDevMiddleware.js +0 -51
- package/server/lib/getFolders.js +0 -10
- package/server/lib/getHotMiddleware.js +0 -27
- package/server/lib/getIPAddress.js +0 -16
- package/server/lib/getPermutations.js +0 -21
- package/server/lib/getWindowsDrives.js +0 -30
- package/server/lib/parseCookie.js +0 -12
- package/server/lib/pushQueuesAndLibrary.js +0 -29
- package/server/main.js +0 -135
- package/server/scannerWorker.js +0 -58
- package/server/serverWorker.js +0 -242
- package/server/socket.js +0 -173
- package/shared/actionTypes.js +0 -103
- /package/build/{7ce9eb3fe454f54745a4.woff2 → client/7ce9eb3fe454f54745a4.woff2} +0 -0
- /package/build/{598.4be526e3a94d53aeceae.css → client/851.83b0127845c2fa8729fe.css} +0 -0
- /package/build/{a35814dd9eb496e3d7cc.woff2 → client/a35814dd9eb496e3d7cc.woff2} +0 -0
- /package/build/{e419b95dccb58b362811.woff2 → client/e419b95dccb58b362811.woff2} +0 -0
- /package/{server → build/server}/lib/schemas/002-replaygain.sql +0 -0
- /package/{server → build/server}/lib/schemas/003-queue-linked-list.sql +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import getLogger from './Log.js';
|
|
2
|
+
import { _ERROR, _SUCCESS } from '../../shared/actionTypes.js';
|
|
3
|
+
const log = getLogger('IPCBridge');
|
|
4
|
+
const PROCESS_NAME = process.env.KES_CHILD_PROCESS || 'main';
|
|
5
|
+
const isParent = typeof process.env.KES_CHILD_PROCESS === 'undefined'; // @todo
|
|
6
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
7
|
+
class IPCParent {
|
|
8
|
+
static children = new Map();
|
|
9
|
+
static handlers = {};
|
|
10
|
+
static send(action, pid) {
|
|
11
|
+
// log.debug(`${PROCESS_NAME} emit: `, action.type)
|
|
12
|
+
if (!this.children.size)
|
|
13
|
+
throw new Error('no child processes');
|
|
14
|
+
if (!pid) {
|
|
15
|
+
this.children.forEach(p => p.send(action));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const subprocess = this.children.get(pid);
|
|
19
|
+
if (subprocess)
|
|
20
|
+
subprocess.send(action);
|
|
21
|
+
}
|
|
22
|
+
// 'this' keyword won't work when this method is passed as the
|
|
23
|
+
// message handler callback, so using the class name (IPCParent)
|
|
24
|
+
static handle(action) {
|
|
25
|
+
const { meta, type } = action;
|
|
26
|
+
// log.debug(`${PROCESS_NAME} rcv:`, type)
|
|
27
|
+
if (!type || typeof IPCParent.handlers[type] !== 'function') {
|
|
28
|
+
log.verbose(`${PROCESS_NAME}: no handler for action: ${type}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// synchronous handler: just fire and forget
|
|
32
|
+
if (!(IPCParent.handlers[type] instanceof AsyncFunction)) {
|
|
33
|
+
IPCParent.handlers[type](action);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// async handler: emit the result back to child
|
|
37
|
+
IPCParent.handlers[type](action).then((res) => {
|
|
38
|
+
IPCParent.send({
|
|
39
|
+
...action,
|
|
40
|
+
type: type + _SUCCESS,
|
|
41
|
+
payload: res,
|
|
42
|
+
}, meta?.pid);
|
|
43
|
+
return;
|
|
44
|
+
}).catch((err) => {
|
|
45
|
+
IPCParent.send({
|
|
46
|
+
...action,
|
|
47
|
+
type: type + _ERROR,
|
|
48
|
+
error: err,
|
|
49
|
+
}, meta?.pid);
|
|
50
|
+
log.error(`${PROCESS_NAME}: error in ipc action ${type}: ${err.message}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
static addChild(subprocess) {
|
|
54
|
+
// parent: handle messages from child process
|
|
55
|
+
subprocess.on('message', action => this.handle(action));
|
|
56
|
+
this.children.set(subprocess.pid, subprocess);
|
|
57
|
+
}
|
|
58
|
+
static removeChild(subprocess) {
|
|
59
|
+
this.children.delete(subprocess.pid);
|
|
60
|
+
}
|
|
61
|
+
static use(obj) {
|
|
62
|
+
this.handlers = {
|
|
63
|
+
...this.handlers,
|
|
64
|
+
...obj,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
class IPCChild {
|
|
69
|
+
static handlers = {};
|
|
70
|
+
static requests = {};
|
|
71
|
+
static reqId = 0;
|
|
72
|
+
static send(action) {
|
|
73
|
+
// console.log(`${PROCESS_NAME} emit: `, action.type)
|
|
74
|
+
process.send(action);
|
|
75
|
+
}
|
|
76
|
+
// 'this' keyword won't work when this method is passed as the
|
|
77
|
+
// message handler callback, so using the class name (IPCChild)
|
|
78
|
+
static handle(action) {
|
|
79
|
+
const { error, meta, type } = action;
|
|
80
|
+
// is this a response to a pending request?
|
|
81
|
+
if (meta?.pid === process.pid && IPCChild.requests[meta.reqId]) {
|
|
82
|
+
if (error) {
|
|
83
|
+
IPCChild.requests[meta.reqId].reject(error);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
IPCChild.requests[meta.reqId].resolve(action.payload);
|
|
87
|
+
}
|
|
88
|
+
// console.log(`${PROCESS_NAME} ack:`, type)
|
|
89
|
+
delete IPCChild.requests[meta.ipcId];
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// console.log(`${PROCESS_NAME} rcv:`, type)
|
|
93
|
+
// handle request
|
|
94
|
+
if (!type || typeof IPCChild.handlers[type] !== 'function') {
|
|
95
|
+
log.verbose(`${PROCESS_NAME}: no handler for action: ${type}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
IPCChild.handlers[type](action);
|
|
99
|
+
}
|
|
100
|
+
// used by child processes only
|
|
101
|
+
static req(action) {
|
|
102
|
+
const promise = new Promise((resolve, reject) => {
|
|
103
|
+
this.requests[++this.reqId] = { resolve, reject };
|
|
104
|
+
});
|
|
105
|
+
action = {
|
|
106
|
+
...action,
|
|
107
|
+
meta: {
|
|
108
|
+
...action.meta,
|
|
109
|
+
reqId: this.reqId,
|
|
110
|
+
pid: process.pid,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
this.send(action);
|
|
114
|
+
return promise;
|
|
115
|
+
}
|
|
116
|
+
static use(obj) {
|
|
117
|
+
this.handlers = {
|
|
118
|
+
...this.handlers,
|
|
119
|
+
...obj,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export default isParent ? IPCParent : IPCChild;
|
|
124
|
+
if (!isParent) {
|
|
125
|
+
// child: handle messages from parent process
|
|
126
|
+
// this also prevents child processes from automatically exiting
|
|
127
|
+
process.on('message', IPCChild.handle);
|
|
128
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import log from 'electron-log/node.js';
|
|
2
|
+
const LEVELS = [false, 'error', 'warn', 'info', 'verbose', 'debug'];
|
|
3
|
+
class Logger {
|
|
4
|
+
static #instance;
|
|
5
|
+
static init(logId, cfg) {
|
|
6
|
+
// defaults
|
|
7
|
+
log.transports.console.level = 'debug';
|
|
8
|
+
log.transports.file.level = false;
|
|
9
|
+
log.transports.file.fileName = logId + '.log';
|
|
10
|
+
log.transports.file.setAppName('Karaoke Eternal Server');
|
|
11
|
+
for (const transport in cfg) {
|
|
12
|
+
for (const key in cfg[transport]) {
|
|
13
|
+
if (key === 'level')
|
|
14
|
+
log.transports[transport].level = LEVELS[cfg[transport].level];
|
|
15
|
+
else
|
|
16
|
+
log.transports[transport][key] = cfg[transport][key];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
Logger.#instance = log;
|
|
20
|
+
return log;
|
|
21
|
+
}
|
|
22
|
+
static getLogger(scope = '') {
|
|
23
|
+
if (!Logger.#instance)
|
|
24
|
+
throw new Error('logger not initialized');
|
|
25
|
+
return Logger.#instance.scope(scope);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// for each process/worker to initialize their logger
|
|
29
|
+
export const initLogger = Logger.init;
|
|
30
|
+
// default export
|
|
31
|
+
export default Logger.getLogger;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function accumulatedThrottle(callback, wait) {
|
|
2
|
+
let timeoutID;
|
|
3
|
+
let accumulated = [];
|
|
4
|
+
return function (...args) {
|
|
5
|
+
accumulated.push(args);
|
|
6
|
+
if (!timeoutID) {
|
|
7
|
+
timeoutID = setTimeout(function () {
|
|
8
|
+
callback(accumulated);
|
|
9
|
+
accumulated = [];
|
|
10
|
+
clearTimeout(timeoutID);
|
|
11
|
+
timeoutID = undefined;
|
|
12
|
+
}, wait);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export default accumulatedThrottle;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import bcrypt from 'bcrypt';
|
|
2
|
+
function hash(myPlaintextPassword, saltRounds) {
|
|
3
|
+
return new Promise(function (resolve, reject) {
|
|
4
|
+
bcrypt.hash(myPlaintextPassword, saltRounds, function (err, hash) {
|
|
5
|
+
if (err)
|
|
6
|
+
return reject(err);
|
|
7
|
+
return resolve(hash);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function compare(data, hash) {
|
|
12
|
+
return new Promise(function (resolve, reject) {
|
|
13
|
+
bcrypt.compare(data, hash, function (err, matched) {
|
|
14
|
+
if (err)
|
|
15
|
+
return reject(err);
|
|
16
|
+
return resolve(matched);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export default {
|
|
21
|
+
hash,
|
|
22
|
+
compare,
|
|
23
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import yargs from 'yargs';
|
|
6
|
+
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
// Resolve package root by walking up to nearest package.json
|
|
8
|
+
function findProjectRoot(startDir) {
|
|
9
|
+
let dir = startDir;
|
|
10
|
+
const root = path.parse(dir).root;
|
|
11
|
+
while (dir !== root) {
|
|
12
|
+
const pkg = path.join(dir, 'package.json');
|
|
13
|
+
if (fs.existsSync(pkg))
|
|
14
|
+
return dir;
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
return startDir;
|
|
18
|
+
}
|
|
19
|
+
const baseDir = findProjectRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
20
|
+
const env = {
|
|
21
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
22
|
+
KES_CONSOLE_COLORS: process.env.KES_CONSOLE_COLORS
|
|
23
|
+
? !['0', 'false'].includes(process.env.KES_CONSOLE_COLORS?.toLowerCase())
|
|
24
|
+
: undefined,
|
|
25
|
+
KES_PATH_ASSETS: path.join(baseDir, 'assets'),
|
|
26
|
+
KES_PATH_DATA: process.env.KES_PATH_DATA || getAppPath('Karaoke Eternal Server'),
|
|
27
|
+
KES_PATH_WEBROOT: path.join(baseDir, 'build', 'client'),
|
|
28
|
+
KES_PORT: parseInt(process.env.KES_PORT, 10) || 0,
|
|
29
|
+
KES_ROTATE_KEY: ['1', 'true'].includes(process.env.KES_ROTATE_KEY?.toLowerCase()),
|
|
30
|
+
KES_SCAN: process.env.KES_SCAN?.trim(),
|
|
31
|
+
KES_SCANNER_CONSOLE_LEVEL: parseInt(process.env.KES_SCANNER_CONSOLE_LEVEL, 10) || undefined,
|
|
32
|
+
KES_SCANNER_LOG_LEVEL: parseInt(process.env.KES_SCANNER_LOG_LEVEL, 10) || undefined,
|
|
33
|
+
KES_SERVER_CONSOLE_LEVEL: parseInt(process.env.KES_SERVER_CONSOLE_LEVEL, 10) || undefined,
|
|
34
|
+
KES_SERVER_LOG_LEVEL: parseInt(process.env.KES_SERVER_LOG_LEVEL, 10) || undefined,
|
|
35
|
+
KES_URL_PATH: process.env.KES_URL_PATH || '/',
|
|
36
|
+
// support PUID/PGID convention
|
|
37
|
+
KES_PUID: parseInt(process.env.PUID, 10) || undefined,
|
|
38
|
+
KES_PGID: parseInt(process.env.PGID, 10) || undefined,
|
|
39
|
+
};
|
|
40
|
+
const argv = yargs(hideBin(process.argv))
|
|
41
|
+
.version(false) // disable default handler
|
|
42
|
+
.option('data', {
|
|
43
|
+
describe: 'Absolute path of folder for database files',
|
|
44
|
+
requiresArg: true,
|
|
45
|
+
type: 'string',
|
|
46
|
+
})
|
|
47
|
+
.option('p', {
|
|
48
|
+
alias: 'port',
|
|
49
|
+
describe: 'Web server port (default=0/auto)',
|
|
50
|
+
number: true,
|
|
51
|
+
requiresArg: true,
|
|
52
|
+
})
|
|
53
|
+
.option('rotateKey', {
|
|
54
|
+
describe: 'Rotate the session key at startup',
|
|
55
|
+
})
|
|
56
|
+
.option('scan', {
|
|
57
|
+
describe: 'Run the media scanner at startup. Accepts a comma-separated list of pathIds, or "all"',
|
|
58
|
+
type: 'string',
|
|
59
|
+
})
|
|
60
|
+
.option('scannerConsoleLevel', {
|
|
61
|
+
describe: 'Media scanner console output level (default=4)',
|
|
62
|
+
number: true,
|
|
63
|
+
requiresArg: true,
|
|
64
|
+
})
|
|
65
|
+
.option('scannerLogLevel', {
|
|
66
|
+
describe: 'Media scanner log file level (default=3)',
|
|
67
|
+
number: true,
|
|
68
|
+
requiresArg: true,
|
|
69
|
+
})
|
|
70
|
+
.option('serverConsoleLevel', {
|
|
71
|
+
describe: 'Web server console output level (default=4)',
|
|
72
|
+
number: true,
|
|
73
|
+
requiresArg: true,
|
|
74
|
+
})
|
|
75
|
+
.option('serverLogLevel', {
|
|
76
|
+
describe: 'Web server log file level (default=3)',
|
|
77
|
+
number: true,
|
|
78
|
+
requiresArg: true,
|
|
79
|
+
})
|
|
80
|
+
.option('urlPath', {
|
|
81
|
+
describe: 'Web server URL base path (default=/)',
|
|
82
|
+
requiresArg: true,
|
|
83
|
+
type: 'string',
|
|
84
|
+
})
|
|
85
|
+
.option('v', {
|
|
86
|
+
alias: 'version',
|
|
87
|
+
describe: 'Output the Karaoke Eternal Server version and exit',
|
|
88
|
+
})
|
|
89
|
+
.usage('$0')
|
|
90
|
+
.usage(' Logging options use the following numeric levels:')
|
|
91
|
+
.usage(' 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=debug')
|
|
92
|
+
.parseSync();
|
|
93
|
+
if (argv.version) {
|
|
94
|
+
console.log(process.env.npm_package_version);
|
|
95
|
+
process.exit(0); // eslint-disable-line n/no-process-exit
|
|
96
|
+
}
|
|
97
|
+
if (argv.rotateKey) {
|
|
98
|
+
env.KES_ROTATE_KEY = true;
|
|
99
|
+
}
|
|
100
|
+
// CLI options take precedence over env vars
|
|
101
|
+
const opts = {
|
|
102
|
+
data: 'KES_PATH_DATA',
|
|
103
|
+
port: 'KES_PORT',
|
|
104
|
+
scan: 'KES_SCAN',
|
|
105
|
+
scannerConsoleLevel: 'KES_SCANNER_CONSOLE_LEVEL',
|
|
106
|
+
scannerLogLevel: 'KES_SCANNER_LOG_LEVEL',
|
|
107
|
+
serverConsoleLevel: 'KES_SERVER_CONSOLE_LEVEL',
|
|
108
|
+
serverLogLevel: 'KES_SERVER_LOG_LEVEL',
|
|
109
|
+
urlPath: 'KES_URL_PATH',
|
|
110
|
+
};
|
|
111
|
+
for (const opt in opts) {
|
|
112
|
+
if (typeof argv[opt] !== 'undefined') {
|
|
113
|
+
env[opts[opt]] = argv[opt];
|
|
114
|
+
process.env[opts[opt]] = String(argv[opt]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export default env;
|
|
118
|
+
function getAppPath(appName) {
|
|
119
|
+
const home = os.homedir ? os.homedir() : process.env.HOME;
|
|
120
|
+
switch (process.platform) {
|
|
121
|
+
case 'darwin': {
|
|
122
|
+
return path.join(home, 'Library', 'Application Support', appName);
|
|
123
|
+
}
|
|
124
|
+
case 'win32': {
|
|
125
|
+
return process.env.APPDATA || path.join(home, 'AppData', 'Roaming', appName);
|
|
126
|
+
}
|
|
127
|
+
default: {
|
|
128
|
+
return process.env.XDG_CONFIG_HOME || path.join(home, '.config', appName);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { promisify } from 'util';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import getPerms from './getPermutations.js';
|
|
4
|
+
const stat = promisify(fs.stat);
|
|
5
|
+
export default async function getCdgName(file) {
|
|
6
|
+
// upper and lowercase permutations since fs may be case-sensitive
|
|
7
|
+
for (const ext of getPerms('cdg')) {
|
|
8
|
+
const cdg = file.substring(0, file.lastIndexOf('.') + 1) + ext;
|
|
9
|
+
try {
|
|
10
|
+
await stat(cdg);
|
|
11
|
+
return cdg;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// try another permutation
|
|
15
|
+
}
|
|
16
|
+
} // end for
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
const readdir = promisify(fs.readdir);
|
|
5
|
+
const getFolders = dir => readdir(dir, { withFileTypes: true })
|
|
6
|
+
.then(list => Promise.all(list.map(ent => ent.isDirectory() ? path.resolve(dir, ent.name) : null)))
|
|
7
|
+
.then(list => list.filter(f => !!f).sort());
|
|
8
|
+
export default getFolders;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// based on https://github.com/tnnevol/webpack-hot-middleware-for-koa2
|
|
2
|
+
// eslint-disable-next-line n/no-unpublished-import
|
|
3
|
+
import webpackHotMiddleware from 'webpack-hot-middleware';
|
|
4
|
+
export default (compiler, opts) => {
|
|
5
|
+
const middleware = webpackHotMiddleware(compiler, opts);
|
|
6
|
+
return async (ctx, next) => {
|
|
7
|
+
const { end: originalEnd } = ctx.res;
|
|
8
|
+
const runNext = await new Promise((resolve) => {
|
|
9
|
+
ctx.res.end = function () {
|
|
10
|
+
originalEnd.apply(this, arguments);
|
|
11
|
+
resolve(false);
|
|
12
|
+
};
|
|
13
|
+
// call express-style middleware
|
|
14
|
+
middleware(ctx.req, ctx.res, () => {
|
|
15
|
+
resolve(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
if (runNext) {
|
|
19
|
+
await next();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// https://gist.github.com/savokiss/96de34d4ca2d37cbb8e0799798c4c2d3
|
|
2
|
+
import os from 'os';
|
|
3
|
+
export default function () {
|
|
4
|
+
const interfaces = os.networkInterfaces();
|
|
5
|
+
for (const devName in interfaces) {
|
|
6
|
+
const iface = interfaces[devName];
|
|
7
|
+
for (let i = 0; i < iface.length; i++) {
|
|
8
|
+
const alias = iface[i];
|
|
9
|
+
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
|
10
|
+
return alias.address;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// return all uppercase and lowercase permutations of str
|
|
2
|
+
// based on https://stackoverflow.com/a/27995370
|
|
3
|
+
function getPermutations(str) {
|
|
4
|
+
const results = [];
|
|
5
|
+
const arr = str.split('');
|
|
6
|
+
const len = Math.pow(arr.length, 2);
|
|
7
|
+
for (let i = 0; i < len; i++) {
|
|
8
|
+
for (let k = 0, j = i; k < arr.length; k++, j >>= 1) {
|
|
9
|
+
arr[k] = (j & 1) ? arr[k].toUpperCase() : arr[k].toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
const combo = arr.join('');
|
|
12
|
+
results.push(combo);
|
|
13
|
+
}
|
|
14
|
+
// remove duplicates
|
|
15
|
+
return results.filter((ext, pos, self) => self.indexOf(ext) === pos);
|
|
16
|
+
}
|
|
17
|
+
export default getPermutations;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
export default function () {
|
|
3
|
+
const possibleDrives = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(letter => `${letter}:\\`);
|
|
4
|
+
const existingDrives = possibleDrives.filter((drive) => {
|
|
5
|
+
try {
|
|
6
|
+
fs.accessSync(drive, fs.constants.R_OK);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return existingDrives.map(drive => ({
|
|
14
|
+
path: drive,
|
|
15
|
+
label: drive.substring(0, 2),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// cookie helper based on
|
|
2
|
+
// http://stackoverflow.com/questions/3393854/get-and-set-a-single-cookie-with-node-js-http-server
|
|
3
|
+
export default function parseCookie(cookie) {
|
|
4
|
+
const list = {};
|
|
5
|
+
if (cookie) {
|
|
6
|
+
cookie.split(';')
|
|
7
|
+
.forEach((c) => {
|
|
8
|
+
const parts = c.split('=');
|
|
9
|
+
list[parts.shift().trim()] = decodeURI(parts.join('='));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return list;
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Library from '../Library/Library.js';
|
|
2
|
+
import Queue from '../Queue/Queue.js';
|
|
3
|
+
import Rooms from '../Rooms/Rooms.js';
|
|
4
|
+
import { LIBRARY_PUSH, QUEUE_PUSH } from '../../shared/actionTypes.js';
|
|
5
|
+
async function pushQueuesAndLibrary(io) {
|
|
6
|
+
// emit (potentially) updated queues to each room
|
|
7
|
+
// it's important that this happens before the library is pushed,
|
|
8
|
+
// otherwise queue items might reference newly non-existent songs
|
|
9
|
+
for (const { room, roomId } of Rooms.getActive(io)) {
|
|
10
|
+
io.to(room).emit('action', {
|
|
11
|
+
type: QUEUE_PUSH,
|
|
12
|
+
payload: await Queue.get(roomId),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
// invalidate cache
|
|
16
|
+
Library.cache.version = null;
|
|
17
|
+
io.emit('action', {
|
|
18
|
+
type: LIBRARY_PUSH,
|
|
19
|
+
payload: await Library.get(),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export default pushQueuesAndLibrary;
|
|
@@ -33,23 +33,23 @@ CREATE TABLE IF NOT EXISTS "prefs" (
|
|
|
33
33
|
|
|
34
34
|
INSERT INTO prefs (key,data) VALUES ('isFirstRun','true');
|
|
35
35
|
|
|
36
|
-
CREATE TABLE IF NOT EXISTS "queue" (
|
|
37
|
-
"queueId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
38
|
-
"roomId" integer NOT NULL REFERENCES rooms(roomId) DEFERRABLE INITIALLY DEFERRED,
|
|
39
|
-
"songId" integer NOT NULL,
|
|
40
|
-
"userId" integer NOT NULL REFERENCES users(userId) DEFERRABLE INITIALLY DEFERRED
|
|
41
|
-
);
|
|
42
|
-
|
|
36
|
+
CREATE TABLE IF NOT EXISTS "queue" (
|
|
37
|
+
"queueId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
38
|
+
"roomId" integer NOT NULL REFERENCES rooms(roomId) DEFERRABLE INITIALLY DEFERRED,
|
|
39
|
+
"songId" integer NOT NULL,
|
|
40
|
+
"userId" integer NOT NULL REFERENCES users(userId) DEFERRABLE INITIALLY DEFERRED
|
|
41
|
+
);
|
|
42
|
+
|
|
43
43
|
CREATE INDEX IF NOT EXISTS idxRoom ON "queue" ("roomId" ASC);
|
|
44
44
|
|
|
45
|
-
CREATE TABLE IF NOT EXISTS "rooms" (
|
|
46
|
-
"roomId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
47
|
-
"name" text NOT NULL,
|
|
48
|
-
"status" text NOT NULL,
|
|
49
|
-
"password" text,
|
|
50
|
-
"dateCreated"
|
|
51
|
-
);
|
|
52
|
-
|
|
45
|
+
CREATE TABLE IF NOT EXISTS "rooms" (
|
|
46
|
+
"roomId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
47
|
+
"name" text NOT NULL,
|
|
48
|
+
"status" text NOT NULL,
|
|
49
|
+
"password" text,
|
|
50
|
+
"dateCreated" integer NOT NULL DEFAULT(0)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
53
|
CREATE TABLE IF NOT EXISTS "songs" (
|
|
54
54
|
"songId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
55
55
|
"artistId" integer NOT NULL REFERENCES artists(artistId) DEFERRABLE INITIALLY DEFERRED,
|
|
@@ -73,17 +73,17 @@ CREATE TABLE IF NOT EXISTS "songStars" (
|
|
|
73
73
|
|
|
74
74
|
CREATE UNIQUE INDEX IF NOT EXISTS idxUserSong ON "songStars" ("userId" ASC, "songId" ASC);
|
|
75
75
|
|
|
76
|
-
CREATE TABLE IF NOT EXISTS "users" (
|
|
77
|
-
"userId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
78
|
-
"username" text NOT NULL,
|
|
79
|
-
"password" text NOT NULL,
|
|
80
|
-
"name" text NOT NULL,
|
|
81
|
-
"isAdmin" integer(1) NOT NULL,
|
|
82
|
-
"image" blob,
|
|
83
|
-
"dateCreated" integer NOT NULL DEFAULT(0),
|
|
84
|
-
"dateUpdated" integer NOT NULL DEFAULT(0)
|
|
85
|
-
);
|
|
86
|
-
|
|
76
|
+
CREATE TABLE IF NOT EXISTS "users" (
|
|
77
|
+
"userId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
78
|
+
"username" text NOT NULL,
|
|
79
|
+
"password" text NOT NULL,
|
|
80
|
+
"name" text NOT NULL,
|
|
81
|
+
"isAdmin" integer(1) NOT NULL,
|
|
82
|
+
"image" blob,
|
|
83
|
+
"dateCreated" integer NOT NULL DEFAULT(0),
|
|
84
|
+
"dateUpdated" integer NOT NULL DEFAULT(0)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
87
|
CREATE UNIQUE INDEX IF NOT EXISTS idxUsername ON "users" ("username" ASC);
|
|
88
88
|
|
|
89
89
|
-- Down
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- Up
|
|
2
|
+
CREATE TABLE IF NOT EXISTS "roles" (
|
|
3
|
+
"roleId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
4
|
+
"name" text NOT NULL COLLATE NOCASE,
|
|
5
|
+
"data" text NOT NULL DEFAULT('{}')
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idxName ON "roles" ("name" ASC);
|
|
9
|
+
|
|
10
|
+
INSERT INTO roles (name) VALUES ('admin'), ('player'), ('standard'), ('guest');
|
|
11
|
+
|
|
12
|
+
ALTER TABLE users ADD COLUMN "roleId" integer NOT NULL DEFAULT 0 REFERENCES roles(roleId) DEFERRABLE INITIALLY DEFERRED;
|
|
13
|
+
|
|
14
|
+
UPDATE users SET roleId = CASE
|
|
15
|
+
WHEN isAdmin = 1 THEN (SELECT roleId FROM roles WHERE name = 'admin')
|
|
16
|
+
WHEN isAdmin = 0 THEN (SELECT roleId FROM roles WHERE name = 'standard')
|
|
17
|
+
ELSE (SELECT roleId FROM roles WHERE name = 'standard')
|
|
18
|
+
END;
|
|
19
|
+
|
|
20
|
+
ALTER TABLE users DROP COLUMN isAdmin;
|
|
21
|
+
|
|
22
|
+
-- Down
|
|
23
|
+
ALTER TABLE users ADD COLUMN isAdmin integer DEFAULT 0;
|
|
24
|
+
|
|
25
|
+
UPDATE users SET isAdmin = CASE
|
|
26
|
+
WHEN roleId = (SELECT roleId FROM roles WHERE name = 'admin') THEN 1
|
|
27
|
+
ELSE 0
|
|
28
|
+
END;
|
|
29
|
+
|
|
30
|
+
ALTER TABLE users DROP COLUMN roleId;
|
|
31
|
+
|
|
32
|
+
DROP TABLE roles;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Gets the normalized file extension, in lowercase and including the period.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} filename The filename to extract the extension from.
|
|
7
|
+
* @returns {string} The extension in lowercase with a period, or an empty string.
|
|
8
|
+
*/
|
|
9
|
+
export const getExt = filename => path.extname(filename).toLowerCase();
|
|
10
|
+
export const parsePathIds = (str) => {
|
|
11
|
+
const nums = [];
|
|
12
|
+
// multiple ids?
|
|
13
|
+
if (str && str.includes(',')) {
|
|
14
|
+
const parts = str.split(',');
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
const n = parseInt(part.trim(), 10);
|
|
17
|
+
if (!isNaN(n))
|
|
18
|
+
nums.push(n);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// single id?
|
|
23
|
+
const n = parseInt(str, 10);
|
|
24
|
+
if (!isNaN(n))
|
|
25
|
+
nums.push(n);
|
|
26
|
+
}
|
|
27
|
+
if (nums.length)
|
|
28
|
+
return nums;
|
|
29
|
+
return !!str;
|
|
30
|
+
};
|
|
31
|
+
export const randomChars = (length) => {
|
|
32
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
33
|
+
const bytes = crypto.randomBytes(length);
|
|
34
|
+
let result = '';
|
|
35
|
+
for (let i = 0; i < length; i++) {
|
|
36
|
+
result += chars[bytes[i] % chars.length];
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|