karaoke-eternal 1.0.0
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/LICENSE +5 -0
- package/README.md +49 -0
- package/assets/app.ico +0 -0
- package/assets/app.png +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/mic-blackTemplate.png +0 -0
- package/assets/mic-blackTemplate@2x.png +0 -0
- package/assets/mic-white.png +0 -0
- package/assets/mic-white@2x.png +0 -0
- package/assets/robots.txt +2 -0
- package/build/267.4be526e3a94d53aeceae.js +1 -0
- package/build/591.4be526e3a94d53aeceae.js +1 -0
- package/build/598.4be526e3a94d53aeceae.css +5 -0
- package/build/598.4be526e3a94d53aeceae.js +1 -0
- package/build/799.4be526e3a94d53aeceae.js +1 -0
- package/build/7ce9eb3fe454f54745a4.woff2 +0 -0
- package/build/845.4be526e3a94d53aeceae.css +132 -0
- package/build/845.4be526e3a94d53aeceae.js +1 -0
- package/build/a35814dd9eb496e3d7cc.woff2 +0 -0
- package/build/e419b95dccb58b362811.woff2 +0 -0
- package/build/index.html +1 -0
- package/build/licenses.txt +1400 -0
- package/build/main.4be526e3a94d53aeceae.css +2034 -0
- package/build/main.4be526e3a94d53aeceae.js +1 -0
- package/package.json +144 -0
- package/server/Library/Library.js +340 -0
- package/server/Library/index.js +3 -0
- package/server/Library/ipc.js +18 -0
- package/server/Library/router.js +27 -0
- package/server/Library/socket.js +47 -0
- package/server/Media/Media.js +207 -0
- package/server/Media/index.js +3 -0
- package/server/Media/ipc.js +19 -0
- package/server/Media/router.js +99 -0
- package/server/Player/socket.js +78 -0
- package/server/Prefs/Prefs.js +165 -0
- package/server/Prefs/index.js +3 -0
- package/server/Prefs/router.js +124 -0
- package/server/Prefs/socket.js +68 -0
- package/server/Queue/Queue.js +208 -0
- package/server/Queue/index.js +3 -0
- package/server/Queue/socket.js +99 -0
- package/server/Rooms/Rooms.js +114 -0
- package/server/Rooms/index.js +3 -0
- package/server/Rooms/router.js +146 -0
- package/server/Scanner/FileScanner/FileScanner.js +225 -0
- package/server/Scanner/FileScanner/getConfig.js +35 -0
- package/server/Scanner/FileScanner/getFiles.js +63 -0
- package/server/Scanner/FileScanner/index.js +3 -0
- package/server/Scanner/MetaParser/MetaParser.js +49 -0
- package/server/Scanner/MetaParser/defaultMiddleware.js +197 -0
- package/server/Scanner/MetaParser/index.js +3 -0
- package/server/Scanner/Scanner.js +33 -0
- package/server/User/User.js +139 -0
- package/server/User/index.js +3 -0
- package/server/User/router.js +442 -0
- package/server/lib/Database.js +55 -0
- package/server/lib/IPCBridge.js +115 -0
- package/server/lib/Log.js +71 -0
- package/server/lib/bcrypt.js +24 -0
- package/server/lib/cli.js +136 -0
- package/server/lib/electron.js +81 -0
- package/server/lib/getCdgName.js +20 -0
- package/server/lib/getDevMiddleware.js +51 -0
- package/server/lib/getFolders.js +10 -0
- package/server/lib/getHotMiddleware.js +27 -0
- package/server/lib/getIPAddress.js +16 -0
- package/server/lib/getPermutations.js +21 -0
- package/server/lib/getWindowsDrives.js +30 -0
- package/server/lib/parseCookie.js +12 -0
- package/server/lib/pushQueuesAndLibrary.js +29 -0
- package/server/lib/schemas/001-initial-schema.sql +98 -0
- package/server/lib/schemas/002-replaygain.sql +9 -0
- package/server/lib/schemas/003-queue-linked-list.sql +16 -0
- package/server/main.js +135 -0
- package/server/scannerWorker.js +58 -0
- package/server/serverWorker.js +242 -0
- package/server/socket.js +173 -0
- package/shared/actionTypes.js +103 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const log = require('./Log')('IPCBridge')
|
|
2
|
+
const {
|
|
3
|
+
_ERROR,
|
|
4
|
+
_SUCCESS,
|
|
5
|
+
} = require('../../shared/actionTypes')
|
|
6
|
+
|
|
7
|
+
const PROCESS_NAME = process.env.KES_CHILD_PROCESS || 'main'
|
|
8
|
+
const children = []
|
|
9
|
+
let handlers = {}
|
|
10
|
+
const reqs = {}
|
|
11
|
+
const isParent = typeof process.env.KES_CHILD_PROCESS === 'undefined' // @todo
|
|
12
|
+
const isChild = !isParent
|
|
13
|
+
let actionId = 0
|
|
14
|
+
|
|
15
|
+
class IPCBridge {
|
|
16
|
+
static send (action) {
|
|
17
|
+
// log.debug(`${PROCESS_NAME} send: `, action.type)
|
|
18
|
+
|
|
19
|
+
if (isChild) {
|
|
20
|
+
process.send(action)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!children.length) throw new Error('no child processes')
|
|
25
|
+
children.forEach(p => p.send(action))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static req (action) {
|
|
29
|
+
const id = ++actionId
|
|
30
|
+
const promise = new Promise((resolve, reject) => {
|
|
31
|
+
reqs[id] = { resolve, reject }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
action = {
|
|
35
|
+
...action,
|
|
36
|
+
meta: {
|
|
37
|
+
...action.meta,
|
|
38
|
+
ipcId: id,
|
|
39
|
+
ipcName: PROCESS_NAME,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.send(action)
|
|
44
|
+
|
|
45
|
+
return promise
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static _handle (action) {
|
|
49
|
+
const { error, meta, type } = action
|
|
50
|
+
|
|
51
|
+
// is it an ACK for an outstanding request?
|
|
52
|
+
if (meta && meta.ipcName === PROCESS_NAME && reqs[meta.ipcId]) {
|
|
53
|
+
if (error) {
|
|
54
|
+
reqs[meta.ipcId].reject(error)
|
|
55
|
+
} else {
|
|
56
|
+
reqs[meta.ipcId].resolve(action.payload)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// log.debug(`${PROCESS_NAME} ack:`, type)
|
|
60
|
+
|
|
61
|
+
delete reqs[meta.ipcId]
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// log.debug(`${PROCESS_NAME} rcv:`, type)
|
|
66
|
+
|
|
67
|
+
// handle request
|
|
68
|
+
if (!type || typeof handlers[type] !== 'function') {
|
|
69
|
+
log.debug(`${PROCESS_NAME} no handler for action: ${type}`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// @todo handle non-promises?
|
|
74
|
+
handlers[type](action).then(res => {
|
|
75
|
+
if (meta && !meta.noAck) {
|
|
76
|
+
this.send({
|
|
77
|
+
...action,
|
|
78
|
+
type: type + _SUCCESS,
|
|
79
|
+
payload: res,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}).catch(err => {
|
|
83
|
+
this.send({
|
|
84
|
+
...action,
|
|
85
|
+
type: type + _ERROR,
|
|
86
|
+
error: err,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
log.debug(`${PROCESS_NAME} error in ipc action ${type}: ${err.message}`)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static addChild (p) {
|
|
94
|
+
// message from child process
|
|
95
|
+
p.on('message', action => this._handle(action))
|
|
96
|
+
children.push(p)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static removeChild (p) { children.splice(children.indexOf(p), 1) }
|
|
100
|
+
|
|
101
|
+
// @todo make real middleware?
|
|
102
|
+
static use (obj) {
|
|
103
|
+
handlers = {
|
|
104
|
+
...handlers,
|
|
105
|
+
...obj,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = IPCBridge
|
|
111
|
+
|
|
112
|
+
// make sure IPC channel stays open
|
|
113
|
+
if (isChild) {
|
|
114
|
+
process.on('message', IPCBridge._handle)
|
|
115
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const util = require('util')
|
|
2
|
+
const _levels = [false, 'error', 'warn', 'info', 'verbose', 'debug']
|
|
3
|
+
let _defaultInstance
|
|
4
|
+
|
|
5
|
+
class Log {
|
|
6
|
+
constructor (logId, cfg) {
|
|
7
|
+
this.logger = require('electron-log').create(logId)
|
|
8
|
+
|
|
9
|
+
// defaults
|
|
10
|
+
this.logger.transports.console.level = 'debug'
|
|
11
|
+
this.logger.transports.file.level = false
|
|
12
|
+
this.logger.transports.file.fileName = logId + '.log'
|
|
13
|
+
|
|
14
|
+
for (const transport in cfg) {
|
|
15
|
+
this.logger.transports[transport].level = cfg[transport]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setDefaultInstance () {
|
|
20
|
+
_defaultInstance = this
|
|
21
|
+
return this
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static resolve (userLevel, defaultLevel) {
|
|
25
|
+
return typeof _levels[userLevel] === 'undefined'
|
|
26
|
+
? _levels[defaultLevel]
|
|
27
|
+
: _levels[userLevel]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class IPCLog {
|
|
32
|
+
constructor (scope = '') {
|
|
33
|
+
const IPC = require('./IPCBridge')
|
|
34
|
+
const { SCANNER_WORKER_LOG } = require('../../shared/actionTypes')
|
|
35
|
+
const send = (level, str, ...args) => {
|
|
36
|
+
IPC.send({
|
|
37
|
+
type: SCANNER_WORKER_LOG,
|
|
38
|
+
payload: {
|
|
39
|
+
level,
|
|
40
|
+
msg: `${scope ? scope + ': ' : ''}${util.format(str, ...args)}`,
|
|
41
|
+
},
|
|
42
|
+
meta: {
|
|
43
|
+
noAck: true,
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
error: send.bind(this, 'error'),
|
|
50
|
+
warn: send.bind(this, 'warn'),
|
|
51
|
+
info: send.bind(this, 'info'),
|
|
52
|
+
verbose: send.bind(this, 'verbose'),
|
|
53
|
+
debug: send.bind(this, 'debug'),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getLogger (scope = '') {
|
|
59
|
+
if (!_defaultInstance) throw new Error('no default logger instance')
|
|
60
|
+
return _defaultInstance.logger.scope(scope)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getIPCLogger (scope = '') {
|
|
64
|
+
return new IPCLog(scope)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// default export
|
|
68
|
+
module.exports = process.env.KES_CHILD_PROCESS ? getIPCLogger : getLogger
|
|
69
|
+
|
|
70
|
+
// used by main.js to instantiate the loggers
|
|
71
|
+
module.exports.Log = Log
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const bcrypt = require('bcrypt')
|
|
2
|
+
|
|
3
|
+
function hash (myPlaintextPassword, saltRounds) {
|
|
4
|
+
return new Promise(function (resolve, reject) {
|
|
5
|
+
bcrypt.hash(myPlaintextPassword, saltRounds, function (err, hash) {
|
|
6
|
+
if (err) { return reject(err) }
|
|
7
|
+
return resolve(hash)
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function compare (data, hash) {
|
|
13
|
+
return new Promise(function (resolve, reject) {
|
|
14
|
+
bcrypt.compare(data, hash, function (err, matched) {
|
|
15
|
+
if (err) { return reject(err) }
|
|
16
|
+
return resolve(matched)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
hash,
|
|
23
|
+
compare,
|
|
24
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const baseDir = path.resolve(path.dirname(require.main.filename), '..')
|
|
4
|
+
|
|
5
|
+
const env = {
|
|
6
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
7
|
+
KES_CONSOLE_LEVEL: parseInt(process.env.KES_CONSOLE_LEVEL, 10),
|
|
8
|
+
KES_LOG_LEVEL: parseInt(process.env.KES_LOG_LEVEL, 10),
|
|
9
|
+
KES_PATH_ASSETS: path.join(baseDir, 'assets'),
|
|
10
|
+
KES_PATH_DATA: process.env.KES_PATH_DATA || getAppPath('Karaoke Eternal Server'),
|
|
11
|
+
KES_PATH_WEBROOT: path.join(baseDir, 'build'),
|
|
12
|
+
KES_PORT: parseInt(process.env.KES_PORT, 10) || 0,
|
|
13
|
+
KES_ROTATE_KEY: ['1', 'true'].includes(process.env.KES_ROTATE_KEY?.toLowerCase()),
|
|
14
|
+
KES_SCAN: ['1', 'true'].includes(process.env.KES_SCAN?.toLowerCase()),
|
|
15
|
+
KES_SCAN_CONSOLE_LEVEL: parseInt(process.env.KES_SCAN_CONSOLE_LEVEL, 10),
|
|
16
|
+
KES_SCAN_LOG_LEVEL: parseInt(process.env.KES_SCAN_LOG_LEVEL, 10),
|
|
17
|
+
KES_URL_PATH: process.env.KES_URL_PATH || '/',
|
|
18
|
+
// support PUID/PGID convention
|
|
19
|
+
KES_PUID: parseInt(process.env.PUID, 10),
|
|
20
|
+
KES_PGID: parseInt(process.env.PGID, 10),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const yargs = require('yargs')
|
|
24
|
+
.version(false) // disable default handler
|
|
25
|
+
.option('consoleLevel', {
|
|
26
|
+
describe: 'Web server console output level (default=4)',
|
|
27
|
+
number: true,
|
|
28
|
+
requiresArg: true,
|
|
29
|
+
})
|
|
30
|
+
.option('data', {
|
|
31
|
+
describe: 'Absolute path of folder for database files',
|
|
32
|
+
requiresArg: true,
|
|
33
|
+
type: 'string',
|
|
34
|
+
})
|
|
35
|
+
.option('logLevel', {
|
|
36
|
+
describe: 'Web server log file level (default=3)',
|
|
37
|
+
number: true,
|
|
38
|
+
requiresArg: true,
|
|
39
|
+
})
|
|
40
|
+
.option('p', {
|
|
41
|
+
alias: 'port',
|
|
42
|
+
describe: 'Web server port (default=0/auto)',
|
|
43
|
+
number: true,
|
|
44
|
+
requiresArg: true,
|
|
45
|
+
})
|
|
46
|
+
.option('rotateKey', {
|
|
47
|
+
describe: 'Rotate the session key at startup',
|
|
48
|
+
})
|
|
49
|
+
.option('scan', {
|
|
50
|
+
describe: 'Run the media scanner at startup',
|
|
51
|
+
})
|
|
52
|
+
.option('scanConsoleLevel', {
|
|
53
|
+
describe: 'Media scanner console output level (default=4)',
|
|
54
|
+
number: true,
|
|
55
|
+
requiresArg: true,
|
|
56
|
+
})
|
|
57
|
+
.option('scanLogLevel', {
|
|
58
|
+
describe: 'Media scanner log file level (default=3)',
|
|
59
|
+
number: true,
|
|
60
|
+
requiresArg: true,
|
|
61
|
+
})
|
|
62
|
+
.option('urlPath', {
|
|
63
|
+
describe: 'Web server URL base path (default=/)',
|
|
64
|
+
requiresArg: true,
|
|
65
|
+
type: 'string',
|
|
66
|
+
})
|
|
67
|
+
.option('v', {
|
|
68
|
+
alias: 'version',
|
|
69
|
+
describe: 'Output the Karaoke Eternal Server version and exit',
|
|
70
|
+
})
|
|
71
|
+
.usage('$0')
|
|
72
|
+
.usage(' Logging options use the following numeric levels:')
|
|
73
|
+
.usage(' 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=debug')
|
|
74
|
+
|
|
75
|
+
let argv = yargs.argv
|
|
76
|
+
|
|
77
|
+
let _app
|
|
78
|
+
if (process.versions.electron) {
|
|
79
|
+
_app = require('electron').app
|
|
80
|
+
|
|
81
|
+
// see https://github.com/yargs/yargs/blob/master/docs/api.md#argv
|
|
82
|
+
if (_app.isPackaged) {
|
|
83
|
+
argv = yargs.parse(process.argv.slice(1))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (argv.version) {
|
|
88
|
+
console.log(_app ? _app.getVersion() : process.env.npm_package_version)
|
|
89
|
+
process.exit(0)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// CLI options take precendence over env vars
|
|
93
|
+
if (argv.scan) {
|
|
94
|
+
env.KES_SCAN = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (argv.rotateKey) {
|
|
98
|
+
env.KES_ROTATE_KEY = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const opts = {
|
|
102
|
+
data: 'KES_PATH_DATA',
|
|
103
|
+
port: 'KES_PORT',
|
|
104
|
+
scanConsoleLevel: 'KES_SCAN_CONSOLE_LEVEL',
|
|
105
|
+
scanLogLevel: 'KES_SCAN_LOG_LEVEL',
|
|
106
|
+
serverConsoleLevel: 'KES_CONSOLE_LEVEL',
|
|
107
|
+
serverLogLevel: 'KES_LOG_LEVEL',
|
|
108
|
+
urlPath: 'KES_URL_PATH',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const opt in opts) {
|
|
112
|
+
if (typeof argv[opt] !== 'undefined') {
|
|
113
|
+
env[opts[opt]] = argv[opt]
|
|
114
|
+
process.env[opts[opt]] = argv[opt]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = env
|
|
119
|
+
|
|
120
|
+
function getAppPath (appName) {
|
|
121
|
+
const home = os.homedir ? os.homedir() : process.env.HOME
|
|
122
|
+
|
|
123
|
+
switch (process.platform) {
|
|
124
|
+
case 'darwin': {
|
|
125
|
+
return path.join(home, 'Library', 'Application Support', appName)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'win32': {
|
|
129
|
+
return process.env.APPDATA || path.join(home, 'AppData', 'Roaming', appName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
default: {
|
|
133
|
+
return process.env.XDG_CONFIG_HOME || path.join(home, '.config', appName)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { app, shell, clipboard, dialog, BrowserWindow, Tray, Menu } = require('electron')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const log = require('./Log')(`main:electron[${process.pid}]`)
|
|
4
|
+
|
|
5
|
+
// Keep a global reference of the window object, if you don't, the window will
|
|
6
|
+
// be closed automatically when the JavaScript object is garbage collected.
|
|
7
|
+
let win
|
|
8
|
+
let tray = null
|
|
9
|
+
const status = {
|
|
10
|
+
url: '',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = ({ env }) => {
|
|
14
|
+
// event handlers
|
|
15
|
+
app.on('ready', createWindow)
|
|
16
|
+
app.on('quit', (e, code) => { log.info(`exiting (${code})`) })
|
|
17
|
+
|
|
18
|
+
function createWindow () {
|
|
19
|
+
win = new BrowserWindow({
|
|
20
|
+
show: false,
|
|
21
|
+
skipTaskbar: true, // windows
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// macOS
|
|
25
|
+
if (app.dock) {
|
|
26
|
+
app.dock.hide()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (process.platform === 'win32') {
|
|
30
|
+
// white 32x32
|
|
31
|
+
tray = new Tray(path.join(env.KES_PATH_ASSETS, 'mic-white@2x.png'))
|
|
32
|
+
} else {
|
|
33
|
+
// blackish 32x32 (template works in light and dark macOS modes)
|
|
34
|
+
tray = new Tray(path.join(env.KES_PATH_ASSETS, 'mic-blackTemplate.png'))
|
|
35
|
+
tray.setPressedImage(path.join(env.KES_PATH_ASSETS, 'mic-white.png'))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
tray.setToolTip('Karaoke Eternal Server v' + app.getVersion())
|
|
39
|
+
tray.on('double-click', launchBrowser)
|
|
40
|
+
updateMenu()
|
|
41
|
+
|
|
42
|
+
// Emitted when the window is closed.
|
|
43
|
+
win.on('closed', () => {
|
|
44
|
+
win = null
|
|
45
|
+
tray = null
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function launchBrowser () {
|
|
50
|
+
if (status.url) {
|
|
51
|
+
shell.openExternal(status.url)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function setError (msg) {
|
|
56
|
+
dialog.showErrorBox('Karaoke Eternal Server', `Error: ${msg}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setStatus (key, val) {
|
|
60
|
+
status[key] = val
|
|
61
|
+
updateMenu()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function updateMenu () {
|
|
65
|
+
if (!tray) return
|
|
66
|
+
|
|
67
|
+
const menu = [
|
|
68
|
+
{ label: 'Karaoke Eternal Server v' + app.getVersion(), enabled: false },
|
|
69
|
+
{ label: status.url, enabled: false },
|
|
70
|
+
{ type: 'separator' },
|
|
71
|
+
{ label: 'Open in browser', click: launchBrowser },
|
|
72
|
+
{ label: 'Copy URL', click: () => clipboard.writeText(status.url) },
|
|
73
|
+
{ type: 'separator' },
|
|
74
|
+
{ label: 'Quit Karaoke Eternal Server', role: 'quit' },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
tray.setContextMenu(Menu.buildFromTemplate(menu))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { app, setStatus, setError }
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { promisify } = require('util')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const stat = promisify(fs.stat)
|
|
4
|
+
const getPerms = require('./getPermutations')
|
|
5
|
+
|
|
6
|
+
module.exports = async function getCdgName (file) {
|
|
7
|
+
// upper and lowercase permutations since fs may be case-sensitive
|
|
8
|
+
for (const ext of getPerms('cdg')) {
|
|
9
|
+
const cdg = file.substring(0, file.lastIndexOf('.') + 1) + ext
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await stat(cdg)
|
|
13
|
+
return cdg
|
|
14
|
+
} catch (err) {
|
|
15
|
+
// try another permutation
|
|
16
|
+
}
|
|
17
|
+
} // end for
|
|
18
|
+
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright © 2016 Andrew Powell
|
|
3
|
+
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be
|
|
9
|
+
included in all copies or substantial portions of this Source Code Form.
|
|
10
|
+
*/
|
|
11
|
+
const webpackDevMiddleware = require('webpack-dev-middleware')
|
|
12
|
+
|
|
13
|
+
module.exports = (compiler, opts) => {
|
|
14
|
+
const middleware = webpackDevMiddleware(compiler, opts)
|
|
15
|
+
|
|
16
|
+
return (ctx, next) => {
|
|
17
|
+
// wait for webpack-dev-middleware to signal that the build is ready
|
|
18
|
+
const ready = new Promise((resolve, reject) => {
|
|
19
|
+
for (const comp of [].concat(compiler.compilers || compiler)) {
|
|
20
|
+
comp.hooks.failed.tap('KoaWebpack', (error) => {
|
|
21
|
+
reject(error)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
middleware.waitUntilValid(() => {
|
|
26
|
+
resolve(true)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// tell webpack-dev-middleware to handle the request
|
|
31
|
+
const init = new Promise((resolve) => {
|
|
32
|
+
// call express-style middleware
|
|
33
|
+
middleware(
|
|
34
|
+
ctx.req,
|
|
35
|
+
{
|
|
36
|
+
end: (content) => {
|
|
37
|
+
// eslint-disable-next-line no-param-reassign
|
|
38
|
+
ctx.body = content
|
|
39
|
+
resolve()
|
|
40
|
+
},
|
|
41
|
+
getHeader: ctx.get.bind(ctx),
|
|
42
|
+
setHeader: ctx.set.bind(ctx),
|
|
43
|
+
locals: ctx.state
|
|
44
|
+
},
|
|
45
|
+
() => resolve(next())
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return Promise.all([ready, init])
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { promisify } = require('util')
|
|
4
|
+
const readdir = promisify(fs.readdir)
|
|
5
|
+
|
|
6
|
+
const getFolders = dir => readdir(dir, { withFileTypes: true })
|
|
7
|
+
.then(list => Promise.all(list.map(ent => ent.isDirectory() ? path.resolve(dir, ent.name) : null)))
|
|
8
|
+
.then(list => list.filter(f => !!f).sort())
|
|
9
|
+
|
|
10
|
+
module.exports = getFolders
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// based on https://github.com/tnnevol/webpack-hot-middleware-for-koa2
|
|
2
|
+
|
|
3
|
+
const webpackHotMiddleware = require('webpack-hot-middleware')
|
|
4
|
+
|
|
5
|
+
module.exports = (compiler, opts) => {
|
|
6
|
+
const middleware = webpackHotMiddleware(compiler, opts)
|
|
7
|
+
|
|
8
|
+
return async (ctx, next) => {
|
|
9
|
+
const { end: originalEnd } = ctx.res
|
|
10
|
+
|
|
11
|
+
const runNext = await new Promise(resolve => {
|
|
12
|
+
ctx.res.end = function () {
|
|
13
|
+
originalEnd.apply(this, arguments)
|
|
14
|
+
resolve(false)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// call express-style middleware
|
|
18
|
+
middleware(ctx.req, ctx.res, () => {
|
|
19
|
+
resolve(true)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (runNext) {
|
|
24
|
+
await next()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// https://gist.github.com/savokiss/96de34d4ca2d37cbb8e0799798c4c2d3
|
|
2
|
+
module.exports = function () {
|
|
3
|
+
const interfaces = require('os').networkInterfaces()
|
|
4
|
+
|
|
5
|
+
for (const devName in interfaces) {
|
|
6
|
+
const iface = interfaces[devName]
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < iface.length; i++) {
|
|
9
|
+
const alias = iface[i]
|
|
10
|
+
|
|
11
|
+
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
|
12
|
+
return alias.address
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
|
|
8
|
+
for (let i = 0; i < len; i++) {
|
|
9
|
+
for (let k = 0, j = i; k < arr.length; k++, j >>= 1) {
|
|
10
|
+
arr[k] = (j & 1) ? arr[k].toUpperCase() : arr[k].toLowerCase()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const combo = arr.join('')
|
|
14
|
+
results.push(combo)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// remove duplicates
|
|
18
|
+
return results.filter((ext, pos, self) => self.indexOf(ext) === pos)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = getPermutations
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const childProcess = require('child_process')
|
|
2
|
+
const command = 'wmic logicaldisk get Caption, ProviderName'
|
|
3
|
+
// sample output:
|
|
4
|
+
//
|
|
5
|
+
// Caption ProviderName
|
|
6
|
+
// C:
|
|
7
|
+
// D:
|
|
8
|
+
// E: \\vboxsrv\Downloads
|
|
9
|
+
// F: \\vboxsrv\Karaoke
|
|
10
|
+
|
|
11
|
+
module.exports = function () {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
childProcess.exec(command, (err, stdout) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
return reject(err)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// split to lines
|
|
19
|
+
const rows = stdout.split(/\r?\n/)
|
|
20
|
+
|
|
21
|
+
// first line is heading(s)
|
|
22
|
+
rows.shift()
|
|
23
|
+
|
|
24
|
+
resolve(rows.filter(r => !!r.trim()).map(r => ({
|
|
25
|
+
path: r.trim().substring(0, 2) + '\\',
|
|
26
|
+
label: r,
|
|
27
|
+
})))
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// cookie helper based on
|
|
2
|
+
// http://stackoverflow.com/questions/3393854/get-and-set-a-single-cookie-with-node-js-http-server
|
|
3
|
+
module.exports = function parseCookie (cookie) {
|
|
4
|
+
const list = {}
|
|
5
|
+
|
|
6
|
+
cookie && cookie.split(';').forEach(c => {
|
|
7
|
+
const parts = c.split('=')
|
|
8
|
+
list[parts.shift().trim()] = decodeURI(parts.join('='))
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
return list
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const Library = require('../Library')
|
|
2
|
+
const Queue = require('../Queue')
|
|
3
|
+
const Rooms = require('../Rooms')
|
|
4
|
+
const {
|
|
5
|
+
LIBRARY_PUSH,
|
|
6
|
+
QUEUE_PUSH,
|
|
7
|
+
} = require('../../shared/actionTypes')
|
|
8
|
+
|
|
9
|
+
async function pushQueuesAndLibrary (io) {
|
|
10
|
+
// emit (potentially) updated queues to each room
|
|
11
|
+
// it's important that this happens before the library is pushed,
|
|
12
|
+
// otherwise queue items might reference newly non-existent songs
|
|
13
|
+
for (const { room, roomId } of Rooms.getActive(io)) {
|
|
14
|
+
io.to(room).emit('action', {
|
|
15
|
+
type: QUEUE_PUSH,
|
|
16
|
+
payload: await Queue.get(roomId),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// invalidate cache
|
|
21
|
+
Library.cache.version = null
|
|
22
|
+
|
|
23
|
+
io.emit('action', {
|
|
24
|
+
type: LIBRARY_PUSH,
|
|
25
|
+
payload: await Library.get(),
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = pushQueuesAndLibrary
|