karaoke-eternal 1.0.0 → 2.0.0-beta.5
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.a51d7d3f87c474adad54.js +1 -0
- package/build/client/715.a51d7d3f87c474adad54.js +1 -0
- package/build/client/718.a51d7d3f87c474adad54.js +1 -0
- package/build/client/851.a51d7d3f87c474adad54.js +1 -0
- package/build/{845.4be526e3a94d53aeceae.css → client/958.a51d7d3f87c474adad54.css} +53 -6
- package/build/client/958.a51d7d3f87c474adad54.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.a51d7d3f87c474adad54.css +2341 -0
- package/build/client/main.a51d7d3f87c474adad54.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.a51d7d3f87c474adad54.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,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import childProcess from 'child_process';
|
|
3
|
+
import env from './lib/cli.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { initLogger } from './lib/Log.js';
|
|
6
|
+
import { parsePathIds } from './lib/util.js';
|
|
7
|
+
import { PREFS_PATHS_CHANGED, REQUEST_SCAN, REQUEST_SCAN_STOP, SCANNER_WORKER_EXITED, WATCHER_WORKER_EVENT, WATCHER_WORKER_WATCH, } from '../shared/actionTypes.js';
|
|
8
|
+
const log = initLogger('server', {
|
|
9
|
+
console: {
|
|
10
|
+
level: env.KES_SERVER_CONSOLE_LEVEL ?? (env.NODE_ENV === 'development' ? 5 : 4),
|
|
11
|
+
useStyles: env.KES_CONSOLE_COLORS ?? undefined,
|
|
12
|
+
},
|
|
13
|
+
file: {
|
|
14
|
+
level: env.KES_SERVER_LOG_LEVEL ?? (env.NODE_ENV === 'development' ? 0 : 3),
|
|
15
|
+
},
|
|
16
|
+
}).scope(`main[${process.pid}]`);
|
|
17
|
+
const refs = {};
|
|
18
|
+
const shutdownHandlers = [];
|
|
19
|
+
let IPC;
|
|
20
|
+
process.on(PREFS_PATHS_CHANGED, startWatcher);
|
|
21
|
+
// log non-default settings
|
|
22
|
+
for (const key in env) {
|
|
23
|
+
if (process.env[key])
|
|
24
|
+
log.verbose(`${key}=${process.env[key]}`);
|
|
25
|
+
}
|
|
26
|
+
// support PUID/PGID convention (group MUST be set before user!)
|
|
27
|
+
if (Number.isInteger(env.KES_PGID)) {
|
|
28
|
+
log.verbose(`PGID=${env.KES_PGID}`);
|
|
29
|
+
process.setgid(env.KES_PGID);
|
|
30
|
+
}
|
|
31
|
+
if (Number.isInteger(env.KES_PUID)) {
|
|
32
|
+
log.verbose(`PUID=${env.KES_PUID}`);
|
|
33
|
+
process.setuid(env.KES_PUID);
|
|
34
|
+
}
|
|
35
|
+
// handle shutdown gracefully
|
|
36
|
+
['SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException'].forEach((event) => {
|
|
37
|
+
process.on(event, shutdown);
|
|
38
|
+
});
|
|
39
|
+
// make sure child processes don't hang around
|
|
40
|
+
process.on('exit', () => Object.values(refs).forEach(ref => ref.kill()));
|
|
41
|
+
// debug: log stack trace for unhandled promise rejections
|
|
42
|
+
process.on('unhandledRejection', (reason) => {
|
|
43
|
+
log.error('Unhandled Rejection:', reason);
|
|
44
|
+
});
|
|
45
|
+
(async function () {
|
|
46
|
+
// init database
|
|
47
|
+
const { open, close } = await import('./lib/Database.js');
|
|
48
|
+
shutdownHandlers.push(close);
|
|
49
|
+
await open({
|
|
50
|
+
file: path.join(env.KES_PATH_DATA, 'database.sqlite3'),
|
|
51
|
+
ro: false,
|
|
52
|
+
});
|
|
53
|
+
// init IPC listener
|
|
54
|
+
const IPCBridge = await import('./lib/IPCBridge.js');
|
|
55
|
+
IPC = IPCBridge.default;
|
|
56
|
+
IPC.use({
|
|
57
|
+
[WATCHER_WORKER_EVENT]: ({ payload }) => {
|
|
58
|
+
startScanner(payload.pathId);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
// start web server
|
|
62
|
+
const serverWorker = await import('./serverWorker.js');
|
|
63
|
+
serverWorker.default({ env, startScanner, stopScanner, shutdownHandlers });
|
|
64
|
+
// scanning on startup?
|
|
65
|
+
const pathIds = parsePathIds(env.KES_SCAN);
|
|
66
|
+
if (pathIds)
|
|
67
|
+
startScanner(pathIds);
|
|
68
|
+
// any paths with watching enabled?
|
|
69
|
+
const { default: Prefs } = await import('./Prefs/Prefs.js');
|
|
70
|
+
const { paths } = await Prefs.get();
|
|
71
|
+
if (paths.result.find(pathId => paths.entities[pathId].prefs?.isWatchingEnabled)) {
|
|
72
|
+
startWatcher(paths);
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
function startWatcher(paths) {
|
|
76
|
+
if (refs.watcher === undefined) {
|
|
77
|
+
log.info('Starting folder watcher process');
|
|
78
|
+
refs.watcher = childProcess.fork(path.join(import.meta.dirname, 'watcherWorker.js'), [], {
|
|
79
|
+
env: { KES_ENV_JSON: JSON.stringify(env), KES_CHILD_PROCESS: 'watcher' },
|
|
80
|
+
gid: Number.isInteger(env.KES_PGID) ? env.KES_PGID : undefined,
|
|
81
|
+
uid: Number.isInteger(env.KES_PUID) ? env.KES_PUID : undefined,
|
|
82
|
+
});
|
|
83
|
+
refs.watcher.on('exit', (code, signal) => {
|
|
84
|
+
log.info(`Folder watcher process exited (${signal || code})`);
|
|
85
|
+
IPC.removeChild(refs.watcher);
|
|
86
|
+
delete refs.watcher;
|
|
87
|
+
});
|
|
88
|
+
IPC.addChild(refs.watcher);
|
|
89
|
+
}
|
|
90
|
+
IPC.send({
|
|
91
|
+
type: WATCHER_WORKER_WATCH,
|
|
92
|
+
payload: { paths },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function startScanner(pathIds) {
|
|
96
|
+
if (refs.scanner === undefined) {
|
|
97
|
+
log.info('Starting media scanner process');
|
|
98
|
+
refs.scanner = childProcess.fork(path.join(import.meta.dirname, 'scannerWorker.js'), [pathIds.toString()], {
|
|
99
|
+
env: { KES_ENV_JSON: JSON.stringify(env), KES_CHILD_PROCESS: 'scanner' },
|
|
100
|
+
gid: Number.isInteger(env.KES_PGID) ? env.KES_PGID : undefined,
|
|
101
|
+
uid: Number.isInteger(env.KES_PUID) ? env.KES_PUID : undefined,
|
|
102
|
+
});
|
|
103
|
+
refs.scanner.on('exit', (code, signal) => {
|
|
104
|
+
IPC.removeChild(refs.scanner);
|
|
105
|
+
delete refs.scanner;
|
|
106
|
+
process.emit(SCANNER_WORKER_EXITED, { signal, code });
|
|
107
|
+
log.info(`Media scanner process exited (${signal || code})`);
|
|
108
|
+
});
|
|
109
|
+
IPC.addChild(refs.scanner);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
IPC.send({ type: REQUEST_SCAN, payload: { pathIds } });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function stopScanner() {
|
|
116
|
+
if (refs.scanner) {
|
|
117
|
+
IPC.send({ type: REQUEST_SCAN_STOP });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function shutdown(signal) {
|
|
121
|
+
log.info('Received %s', signal);
|
|
122
|
+
await Promise.allSettled(shutdownHandlers.map(f => f()));
|
|
123
|
+
process.exit(0); // eslint-disable-line n/no-process-exit
|
|
124
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { parsePathIds } from './lib/util.js';
|
|
3
|
+
import { initLogger } from './lib/Log.js';
|
|
4
|
+
import { REQUEST_SCAN, REQUEST_SCAN_STOP, SCANNER_WORKER_STATUS, } from '../shared/actionTypes.js';
|
|
5
|
+
const env = JSON.parse(process.env.KES_ENV_JSON);
|
|
6
|
+
const log = initLogger('scanner', {
|
|
7
|
+
console: {
|
|
8
|
+
level: env.KES_SCANNER_CONSOLE_LEVEL ?? (env.NODE_ENV === 'development' ? 5 : 4),
|
|
9
|
+
useStyles: env.KES_CONSOLE_COLORS ?? undefined,
|
|
10
|
+
},
|
|
11
|
+
file: {
|
|
12
|
+
level: env.KES_SCANNER_LOG_LEVEL ?? (env.NODE_ENV === 'development' ? 0 : 3),
|
|
13
|
+
},
|
|
14
|
+
}).scope(`scanner[${process.pid}]`);
|
|
15
|
+
let IPC;
|
|
16
|
+
(async function () {
|
|
17
|
+
// init database
|
|
18
|
+
const { open } = await import('./lib/Database.js');
|
|
19
|
+
await open({
|
|
20
|
+
file: path.join(env.KES_PATH_DATA, 'database.sqlite3'),
|
|
21
|
+
ro: true,
|
|
22
|
+
});
|
|
23
|
+
// init IPC listener
|
|
24
|
+
const IPCBridge = await import('./lib/IPCBridge.js');
|
|
25
|
+
IPC = IPCBridge.default;
|
|
26
|
+
IPC.use({
|
|
27
|
+
[REQUEST_SCAN]: ({ payload }) => {
|
|
28
|
+
q.queue(payload.pathIds); // no need to await; fire and forget
|
|
29
|
+
},
|
|
30
|
+
[REQUEST_SCAN_STOP]: () => {
|
|
31
|
+
q.stop();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const { default: ScannerQueue } = await import('./Scanner/ScannerQueue.js');
|
|
35
|
+
const q = new ScannerQueue(onIteration, onDone);
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
log.debug('received arguments: %s', args);
|
|
38
|
+
if (!args.length) {
|
|
39
|
+
process.exit(1); // eslint-disable-line n/no-process-exit
|
|
40
|
+
}
|
|
41
|
+
const pathIds = parsePathIds(args[0]);
|
|
42
|
+
log.debug('parsed pathIds: %s', pathIds);
|
|
43
|
+
q.queue(pathIds);
|
|
44
|
+
})();
|
|
45
|
+
// @todo
|
|
46
|
+
function onIteration(stats) {
|
|
47
|
+
return stats;
|
|
48
|
+
}
|
|
49
|
+
function onDone() {
|
|
50
|
+
IPC.send({
|
|
51
|
+
type: SCANNER_WORKER_STATUS,
|
|
52
|
+
payload: {
|
|
53
|
+
isScanning: false,
|
|
54
|
+
pct: 100,
|
|
55
|
+
text: 'Finished',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
process.exit(0); // eslint-disable-line n/no-process-exit
|
|
59
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import getLogger from './lib/Log.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import getIPAddress from './lib/getIPAddress.js';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import parseCookie from './lib/parseCookie.js';
|
|
8
|
+
import jsonWebToken from 'jsonwebtoken';
|
|
9
|
+
import Koa from 'koa';
|
|
10
|
+
import koaRouter from '@koa/router';
|
|
11
|
+
import { koaBody } from 'koa-body';
|
|
12
|
+
import koaFavicon from 'koa-favicon';
|
|
13
|
+
import koaLogger from 'koa-logger';
|
|
14
|
+
import koaMount from 'koa-mount';
|
|
15
|
+
import koaRange from 'koa-range';
|
|
16
|
+
import koaStatic from 'koa-static';
|
|
17
|
+
import Media from './Media/Media.js';
|
|
18
|
+
import Prefs from './Prefs/Prefs.js';
|
|
19
|
+
import libraryRouter from './Library/router.js';
|
|
20
|
+
import mediaRouter from './Media/router.js';
|
|
21
|
+
import prefsRouter from './Prefs/router.js';
|
|
22
|
+
import roomsRouter from './Rooms/router.js';
|
|
23
|
+
import userRouter from './User/router.js';
|
|
24
|
+
import pushQueuesAndLibrary from './lib/pushQueuesAndLibrary.js';
|
|
25
|
+
import { Server as SocketIO } from 'socket.io';
|
|
26
|
+
import socketActions from './socket.js';
|
|
27
|
+
import IPC from './lib/IPCBridge.js';
|
|
28
|
+
import IPCLibraryActions from './Library/ipc.js';
|
|
29
|
+
import IPCMediaActions from './Media/ipc.js';
|
|
30
|
+
import { SCANNER_WORKER_EXITED, SERVER_WORKER_STATUS, SERVER_WORKER_ERROR } from '../shared/actionTypes.js';
|
|
31
|
+
const log = getLogger('server');
|
|
32
|
+
const { verify: jwtVerify } = jsonWebToken;
|
|
33
|
+
async function serverWorker({ env, startScanner, stopScanner, shutdownHandlers }) {
|
|
34
|
+
const indexFile = path.join(env.KES_PATH_WEBROOT, 'index.html');
|
|
35
|
+
const urlPath = env.KES_URL_PATH.replace(/\/?$/, '/'); // force trailing slash
|
|
36
|
+
const jwtKey = await Prefs.getJwtKey(env.KES_ROTATE_KEY);
|
|
37
|
+
const app = new Koa();
|
|
38
|
+
let server, io;
|
|
39
|
+
// called when middleware is finalized
|
|
40
|
+
function createServer() {
|
|
41
|
+
server = http.createServer(app.callback());
|
|
42
|
+
// http server error handler
|
|
43
|
+
server.on('error', function (err) {
|
|
44
|
+
log.error(err.message);
|
|
45
|
+
process.emit('serverWorker', {
|
|
46
|
+
type: SERVER_WORKER_ERROR,
|
|
47
|
+
error: err.message,
|
|
48
|
+
});
|
|
49
|
+
// not much we can do without a working server
|
|
50
|
+
throw err;
|
|
51
|
+
});
|
|
52
|
+
// create socket.io server
|
|
53
|
+
io = new SocketIO(server, {
|
|
54
|
+
path: urlPath + 'socket.io',
|
|
55
|
+
serveClient: false,
|
|
56
|
+
});
|
|
57
|
+
// attach socket.io handlers
|
|
58
|
+
socketActions(io, jwtKey);
|
|
59
|
+
// attach IPC action handlers
|
|
60
|
+
IPC.use(IPCLibraryActions(io));
|
|
61
|
+
IPC.use(IPCMediaActions(io));
|
|
62
|
+
// success callback in 3rd arg
|
|
63
|
+
server.listen(env.KES_PORT, () => {
|
|
64
|
+
const port = server.address().port;
|
|
65
|
+
const url = `http://${getIPAddress()}${port === 80 ? '' : ':' + port}${urlPath}`;
|
|
66
|
+
log.info(`Web server running at ${url}`);
|
|
67
|
+
process.emit('serverWorker', {
|
|
68
|
+
type: SERVER_WORKER_STATUS,
|
|
69
|
+
payload: { url },
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
// when scanner exits cleanly
|
|
73
|
+
process.on(SCANNER_WORKER_EXITED, async ({ code }) => {
|
|
74
|
+
if (code !== 0)
|
|
75
|
+
return;
|
|
76
|
+
await Media.cleanup();
|
|
77
|
+
await pushQueuesAndLibrary(io);
|
|
78
|
+
});
|
|
79
|
+
// handle shutdown gracefully
|
|
80
|
+
shutdownHandlers.push(() => new Promise((resolve) => {
|
|
81
|
+
// also calls http server's close method, which ultimately handles the callback
|
|
82
|
+
io.close(resolve);
|
|
83
|
+
// HMR keep-alive connections can prevent http server from fully closing
|
|
84
|
+
server.closeAllConnections();
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
// --------------------
|
|
88
|
+
// Begin Koa middleware
|
|
89
|
+
// --------------------
|
|
90
|
+
// server error handler
|
|
91
|
+
app.on('error', (err, ctx) => {
|
|
92
|
+
if (err.code === 'EPIPE') {
|
|
93
|
+
// these are common since browsers make multiple requests for media files
|
|
94
|
+
log.verbose(err.message);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// silence 4xx response "errors" (koa-logger should show these anyway)
|
|
98
|
+
if (ctx.response && ctx.response.status.toString().startsWith('4')) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (err.stack)
|
|
102
|
+
log.error(err.stack);
|
|
103
|
+
else
|
|
104
|
+
log.error(err);
|
|
105
|
+
});
|
|
106
|
+
// middleware error handler
|
|
107
|
+
app.use(async (ctx, next) => {
|
|
108
|
+
try {
|
|
109
|
+
await next();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
ctx.status = err.status || 500;
|
|
113
|
+
ctx.body = err.message;
|
|
114
|
+
ctx.app.emit('error', err, ctx);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// http request/response logging
|
|
118
|
+
app.use(koaLogger((str, args) => (args.length === 6 && args[3] >= 500) ? log.error(str) : log.debug(str)));
|
|
119
|
+
app.use(koaFavicon(path.join(env.KES_PATH_ASSETS, 'favicon.ico')));
|
|
120
|
+
app.use(koaRange);
|
|
121
|
+
app.use(koaBody({ multipart: true }));
|
|
122
|
+
// all http requests
|
|
123
|
+
app.use(async (ctx, next) => {
|
|
124
|
+
ctx.jwtKey = jwtKey; // used by login route
|
|
125
|
+
// skip JWT/session validation if non-API request or logging in/out
|
|
126
|
+
if (!ctx.request.path.startsWith(`${urlPath}api/`)
|
|
127
|
+
|| ctx.request.path === `${urlPath}api/login`
|
|
128
|
+
|| ctx.request.path === `${urlPath}api/logout`) {
|
|
129
|
+
return next();
|
|
130
|
+
}
|
|
131
|
+
// verify JWT
|
|
132
|
+
try {
|
|
133
|
+
const { keToken } = parseCookie(ctx.request.header.cookie);
|
|
134
|
+
ctx.user = jwtVerify(keToken, jwtKey);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
ctx.user = {
|
|
138
|
+
dateUpdated: null,
|
|
139
|
+
isAdmin: false,
|
|
140
|
+
isGuest: false,
|
|
141
|
+
name: null,
|
|
142
|
+
roomId: null,
|
|
143
|
+
userId: null,
|
|
144
|
+
username: null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// validated
|
|
148
|
+
ctx.io = io;
|
|
149
|
+
ctx.startScanner = startScanner;
|
|
150
|
+
ctx.stopScanner = stopScanner;
|
|
151
|
+
await next();
|
|
152
|
+
});
|
|
153
|
+
// http api endpoints
|
|
154
|
+
const baseRouter = new koaRouter({
|
|
155
|
+
prefix: urlPath.replace(/\/$/, ''), // avoid double slashes with /api prefix
|
|
156
|
+
});
|
|
157
|
+
baseRouter.use(libraryRouter.routes());
|
|
158
|
+
baseRouter.use(mediaRouter.routes());
|
|
159
|
+
baseRouter.use(prefsRouter.routes());
|
|
160
|
+
baseRouter.use(roomsRouter.routes());
|
|
161
|
+
baseRouter.use(userRouter.routes());
|
|
162
|
+
app.use(baseRouter.routes());
|
|
163
|
+
// serve index.html with dynamic base tag at the main SPA routes
|
|
164
|
+
const createIndexMiddleware = (content) => {
|
|
165
|
+
const indexRoutes = [
|
|
166
|
+
urlPath,
|
|
167
|
+
...['account', 'library', 'queue', 'player'].map(r => urlPath + r + '/'),
|
|
168
|
+
];
|
|
169
|
+
content = content.replace('<base href="/">', `<base href="${urlPath}">`);
|
|
170
|
+
return async (ctx, next) => {
|
|
171
|
+
// use a trailing slash for matching purposes
|
|
172
|
+
const reqPath = ctx.request.path.replace(/\/?$/, '/');
|
|
173
|
+
if (!indexRoutes.includes(reqPath)) {
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
ctx.set('content-type', 'text/html');
|
|
177
|
+
ctx.body = content;
|
|
178
|
+
ctx.status = 200;
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
if (env.NODE_ENV !== 'development') {
|
|
182
|
+
// make sure we handle index.html before koaStatic,
|
|
183
|
+
// otherwise it'll be served without dynamic base tag
|
|
184
|
+
app.use(createIndexMiddleware(await promisify(fs.readFile)(indexFile, 'utf8')));
|
|
185
|
+
// serve build and asset folders
|
|
186
|
+
app.use(koaMount(urlPath, koaStatic(env.KES_PATH_WEBROOT)));
|
|
187
|
+
app.use(koaMount(`${urlPath}assets`, koaStatic(env.KES_PATH_ASSETS)));
|
|
188
|
+
createServer();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// ----------------------
|
|
192
|
+
// Development middleware
|
|
193
|
+
// ----------------------
|
|
194
|
+
log.info('Enabling webpack dev and HMR middleware');
|
|
195
|
+
const { default: webpack } = await import('webpack'); // eslint-disable-line n/no-unpublished-import
|
|
196
|
+
const { default: webpackConfig } = await import('../config/webpack.config.js'); // eslint-disable-line n/no-unpublished-import
|
|
197
|
+
const compiler = webpack(webpackConfig);
|
|
198
|
+
compiler.hooks.done.tap('indexPlugin', async () => {
|
|
199
|
+
const indexContent = await new Promise((resolve, reject) => {
|
|
200
|
+
compiler.outputFileSystem.readFile(indexFile, 'utf8', (err, result) => {
|
|
201
|
+
if (err)
|
|
202
|
+
return reject(err);
|
|
203
|
+
return resolve(result);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
// @todo make this less hacky
|
|
207
|
+
if (!server) {
|
|
208
|
+
app.use(createIndexMiddleware(indexContent));
|
|
209
|
+
createServer();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const { default: webpackDevMiddleware } = await import('webpack-dev-middleware'); // eslint-disable-line n/no-unpublished-import
|
|
213
|
+
app.use(webpackDevMiddleware.koaWrapper(compiler, { publicPath: urlPath }));
|
|
214
|
+
const { default: hotMiddleware } = await import('./lib/getHotMiddleware.js');
|
|
215
|
+
app.use(hotMiddleware(compiler));
|
|
216
|
+
// serve assets since webpack-dev-server is unaware of this folder
|
|
217
|
+
app.use(koaMount(`${urlPath}assets`, koaStatic(env.KES_PATH_ASSETS)));
|
|
218
|
+
}
|
|
219
|
+
export default serverWorker;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import getLogger from './lib/Log.js';
|
|
2
|
+
import jsonWebToken from 'jsonwebtoken';
|
|
3
|
+
import parseCookie from './lib/parseCookie.js';
|
|
4
|
+
import Library from './Library/Library.js';
|
|
5
|
+
import LibrarySocket from './Library/socket.js';
|
|
6
|
+
import PlayerSocket from './Player/socket.js';
|
|
7
|
+
import Prefs from './Prefs/Prefs.js';
|
|
8
|
+
import PrefsSocket from './Prefs/socket.js';
|
|
9
|
+
import Rooms from './Rooms/Rooms.js';
|
|
10
|
+
import RoomsSocket from './Rooms/socket.js';
|
|
11
|
+
import Queue from './Queue/Queue.js';
|
|
12
|
+
import QueueSocket from './Queue/socket.js';
|
|
13
|
+
import { LIBRARY_PUSH, QUEUE_PUSH, STARS_PUSH, STAR_COUNTS_PUSH, PLAYER_STATUS, PLAYER_LEAVE, PREFS_PUSH, SOCKET_AUTH_ERROR, _ERROR, } from '../shared/actionTypes.js';
|
|
14
|
+
const log = getLogger('server');
|
|
15
|
+
const handlers = {
|
|
16
|
+
...LibrarySocket,
|
|
17
|
+
...QueueSocket,
|
|
18
|
+
...PlayerSocket,
|
|
19
|
+
...PrefsSocket,
|
|
20
|
+
...RoomsSocket,
|
|
21
|
+
};
|
|
22
|
+
const { verify: jwtVerify } = jsonWebToken;
|
|
23
|
+
export default function (io, jwtKey) {
|
|
24
|
+
io.on('connection', async (sock) => {
|
|
25
|
+
const { keToken } = parseCookie(sock.handshake.headers.cookie);
|
|
26
|
+
const clientLibraryVersion = parseInt(sock.handshake.query.library, 10);
|
|
27
|
+
const clientStarsVersion = parseInt(sock.handshake.query.stars, 10);
|
|
28
|
+
// authenticate the JWT sent via cookie in http handshake
|
|
29
|
+
try {
|
|
30
|
+
sock.user = jwtVerify(keToken, jwtKey);
|
|
31
|
+
// success
|
|
32
|
+
log.verbose('%s (%s) connected from %s', sock.user.name, sock.id, sock.handshake.address);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
io.to(sock.id).emit('action', {
|
|
36
|
+
type: SOCKET_AUTH_ERROR,
|
|
37
|
+
});
|
|
38
|
+
sock.user = null;
|
|
39
|
+
sock.disconnect();
|
|
40
|
+
log.verbose('disconnected %s (%s)', sock.handshake.address, err.message);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// attach disconnect handler
|
|
44
|
+
sock.on('disconnect', (reason) => {
|
|
45
|
+
log.verbose('%s (%s) disconnected (%s)', sock.user.name, sock.id, reason);
|
|
46
|
+
if (typeof sock.user.roomId !== 'number')
|
|
47
|
+
return;
|
|
48
|
+
// beyond this point assumes there is a room
|
|
49
|
+
log.verbose('%s (%s) left room %s (%s; %s in room)', sock.user.name, sock.id, sock.user.roomId, reason, sock.adapter.rooms.size);
|
|
50
|
+
// any players left in room?
|
|
51
|
+
if (!Rooms.isPlayerPresent(io, sock.user.roomId)) {
|
|
52
|
+
io.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
53
|
+
type: PLAYER_LEAVE,
|
|
54
|
+
payload: { socketId: sock.id },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// attach action handler
|
|
59
|
+
sock.on('action', async (action, acknowledge) => {
|
|
60
|
+
const { type } = action;
|
|
61
|
+
if (!sock.user) {
|
|
62
|
+
return acknowledge({
|
|
63
|
+
type: SOCKET_AUTH_ERROR,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (typeof handlers[type] !== 'function') {
|
|
67
|
+
log.error('No handler for socket action: %s', type);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await handlers[type](sock, action, acknowledge);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
log.error(err);
|
|
75
|
+
return acknowledge({
|
|
76
|
+
type: type + _ERROR,
|
|
77
|
+
error: `Error in ${type}: ${err.message}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// push prefs (admin only)
|
|
82
|
+
if (sock.user.isAdmin) {
|
|
83
|
+
log.verbose('pushing prefs to %s (%s)', sock.user.name, sock.id);
|
|
84
|
+
io.to(sock.id).emit('action', {
|
|
85
|
+
type: PREFS_PUSH,
|
|
86
|
+
payload: await Prefs.get(),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// push library (only if client's is outdated)
|
|
90
|
+
if (clientLibraryVersion !== Library.cache.version) {
|
|
91
|
+
log.verbose('pushing library to %s (%s) (client=%s, server=%s)', sock.user.name, sock.id, clientLibraryVersion, Library.cache.version);
|
|
92
|
+
io.to(sock.id).emit('action', {
|
|
93
|
+
type: LIBRARY_PUSH,
|
|
94
|
+
payload: await Library.get(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// push user's stars
|
|
98
|
+
io.to(sock.id).emit('action', {
|
|
99
|
+
type: STARS_PUSH,
|
|
100
|
+
payload: await Library.getUserStars(sock.user.userId),
|
|
101
|
+
});
|
|
102
|
+
// push star counts (only if client's is outdated)
|
|
103
|
+
if (clientStarsVersion !== Library.starCountsCache.version) {
|
|
104
|
+
log.verbose('pushing star counts to %s (%s) (client=%s, server=%s)', sock.user.name, sock.id, clientStarsVersion, Library.starCountsCache.version);
|
|
105
|
+
io.to(sock.id).emit('action', {
|
|
106
|
+
type: STAR_COUNTS_PUSH,
|
|
107
|
+
payload: await Library.getStarCounts(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// it's possible for an admin to not be in a room
|
|
111
|
+
if (typeof sock.user.roomId !== 'number')
|
|
112
|
+
return;
|
|
113
|
+
// beyond this point assumes there is a room
|
|
114
|
+
// add user to room
|
|
115
|
+
sock.join(Rooms.prefix(sock.user.roomId));
|
|
116
|
+
// if there's a player in room, emit its last known status
|
|
117
|
+
// @todo this just emits the first status found
|
|
118
|
+
for (const s of io.of('/').sockets.values()) {
|
|
119
|
+
if (s.user && s.user.roomId === sock.user.roomId && s._lastPlayerStatus) {
|
|
120
|
+
io.to(sock.id).emit('action', {
|
|
121
|
+
type: PLAYER_STATUS,
|
|
122
|
+
payload: s._lastPlayerStatus,
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
log.verbose('%s (%s) joined room %s (%s in room)', sock.user.name, sock.id, sock.user.roomId, sock.adapter.rooms.size);
|
|
128
|
+
// send room's queue
|
|
129
|
+
io.to(sock.id).emit('action', {
|
|
130
|
+
type: QUEUE_PUSH,
|
|
131
|
+
payload: await Queue.get(sock.user.roomId),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import pathLib from 'path';
|
|
3
|
+
import { initLogger } from './lib/Log.js';
|
|
4
|
+
import accumulatedThrottle from './lib/accumulatedThrottle.js';
|
|
5
|
+
import fileTypes from './Media/fileTypes.js';
|
|
6
|
+
import { WATCHER_WORKER_EVENT, WATCHER_WORKER_WATCH, } from '../shared/actionTypes.js';
|
|
7
|
+
const env = JSON.parse(process.env.KES_ENV_JSON);
|
|
8
|
+
const log = initLogger('scanner', {
|
|
9
|
+
console: {
|
|
10
|
+
level: env.KES_SCANNER_CONSOLE_LEVEL ?? (env.NODE_ENV === 'development' ? 5 : 4),
|
|
11
|
+
useStyles: env.KES_CONSOLE_COLORS ?? undefined,
|
|
12
|
+
},
|
|
13
|
+
file: {
|
|
14
|
+
level: env.KES_SCANNER_LOG_LEVEL ?? (env.NODE_ENV === 'development' ? 0 : 3),
|
|
15
|
+
},
|
|
16
|
+
}).scope(`watcher[${process.pid}]`);
|
|
17
|
+
const refs = [];
|
|
18
|
+
const searchExts = Object.keys(fileTypes).filter(ext => fileTypes[ext].scan !== false);
|
|
19
|
+
(async function () {
|
|
20
|
+
const { default: IPC } = await import('./lib/IPCBridge.js');
|
|
21
|
+
IPC.use({
|
|
22
|
+
[WATCHER_WORKER_WATCH]: ({ payload }) => {
|
|
23
|
+
while (refs.length) {
|
|
24
|
+
const ref = refs.shift();
|
|
25
|
+
ref.close();
|
|
26
|
+
}
|
|
27
|
+
const { result, entities } = payload.paths;
|
|
28
|
+
const pathIds = result.filter(pathId => entities[pathId]?.prefs?.isWatchingEnabled);
|
|
29
|
+
if (!pathIds.length) {
|
|
30
|
+
log.info('no paths with watching enabled; exiting');
|
|
31
|
+
process.exit(0); // eslint-disable-line n/no-process-exit
|
|
32
|
+
}
|
|
33
|
+
log.info('watching %s path(s):', pathIds.length);
|
|
34
|
+
pathIds.forEach((pathId) => {
|
|
35
|
+
log.info(' => %s', entities[pathId].path);
|
|
36
|
+
const cb = accumulatedThrottle((events) => {
|
|
37
|
+
const event = events.find(([, filename]) => searchExts.includes(pathLib.extname(filename).toLowerCase()));
|
|
38
|
+
if (!event)
|
|
39
|
+
return;
|
|
40
|
+
log.info('event in path: %s (filename=%s) (type=%s)', entities[pathId].path, event[1], event[0]);
|
|
41
|
+
IPC.send({
|
|
42
|
+
type: WATCHER_WORKER_EVENT,
|
|
43
|
+
payload: { pathId },
|
|
44
|
+
});
|
|
45
|
+
}, 1000);
|
|
46
|
+
const ref = fs.watch(entities[pathId].path, { recursive: true }, cb);
|
|
47
|
+
refs.push(ref);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
})();
|