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.
Files changed (128) hide show
  1. package/README.md +10 -10
  2. package/build/client/447.a51d7d3f87c474adad54.js +1 -0
  3. package/build/client/715.a51d7d3f87c474adad54.js +1 -0
  4. package/build/client/718.a51d7d3f87c474adad54.js +1 -0
  5. package/build/client/851.a51d7d3f87c474adad54.js +1 -0
  6. package/build/{845.4be526e3a94d53aeceae.css → client/958.a51d7d3f87c474adad54.css} +53 -6
  7. package/build/client/958.a51d7d3f87c474adad54.js +1 -0
  8. package/build/{index.html → client/index.html} +1 -1
  9. package/build/{licenses.txt → client/licenses.txt} +208 -496
  10. package/build/client/main.a51d7d3f87c474adad54.css +2341 -0
  11. package/build/client/main.a51d7d3f87c474adad54.js +1 -0
  12. package/build/server/Library/Library.js +297 -0
  13. package/build/server/Library/ipc.js +13 -0
  14. package/build/server/Library/router.js +20 -0
  15. package/build/server/Library/socket.js +35 -0
  16. package/build/server/Media/Media.js +170 -0
  17. package/build/server/Media/fileTypes.js +8 -0
  18. package/build/server/Media/ipc.js +13 -0
  19. package/build/server/Media/router.js +97 -0
  20. package/build/server/Player/socket.js +66 -0
  21. package/build/server/Prefs/Prefs.js +181 -0
  22. package/build/server/Prefs/router.js +151 -0
  23. package/build/server/Prefs/socket.js +52 -0
  24. package/build/server/Queue/Queue.js +203 -0
  25. package/build/server/Queue/socket.js +83 -0
  26. package/build/server/Rooms/Rooms.js +171 -0
  27. package/build/server/Rooms/router.js +97 -0
  28. package/build/server/Rooms/socket.js +23 -0
  29. package/build/server/Scanner/FileScanner/FileScanner.js +166 -0
  30. package/build/server/Scanner/FileScanner/getConfig.js +32 -0
  31. package/build/server/Scanner/FileScanner/getFiles.js +61 -0
  32. package/build/server/Scanner/MetaParser/MetaParser.js +77 -0
  33. package/build/server/Scanner/MetaParser/defaultMiddleware.js +170 -0
  34. package/build/server/Scanner/Scanner.js +26 -0
  35. package/build/server/Scanner/ScannerQueue.js +62 -0
  36. package/build/server/User/User.js +206 -0
  37. package/build/server/User/router.js +366 -0
  38. package/build/server/lib/Database.js +39 -0
  39. package/build/server/lib/Errors.js +6 -0
  40. package/build/server/lib/IPCBridge.js +128 -0
  41. package/build/server/lib/Log.js +31 -0
  42. package/build/server/lib/accumulatedThrottle.js +16 -0
  43. package/build/server/lib/bcrypt.js +23 -0
  44. package/build/server/lib/cli.js +131 -0
  45. package/build/server/lib/getCdgName.js +18 -0
  46. package/build/server/lib/getFolders.js +8 -0
  47. package/build/server/lib/getHotMiddleware.js +22 -0
  48. package/build/server/lib/getIPAddress.js +14 -0
  49. package/build/server/lib/getPermutations.js +17 -0
  50. package/build/server/lib/getWindowsDrives.js +17 -0
  51. package/build/server/lib/parseCookie.js +13 -0
  52. package/build/server/lib/pushQueuesAndLibrary.js +22 -0
  53. package/{server → build/server}/lib/schemas/001-initial-schema.sql +26 -26
  54. package/build/server/lib/schemas/004-paths-rooms-data.sql +7 -0
  55. package/build/server/lib/schemas/005-roles.sql +32 -0
  56. package/build/server/lib/util.js +39 -0
  57. package/build/server/main.js +124 -0
  58. package/build/server/scannerWorker.js +59 -0
  59. package/build/server/serverWorker.js +219 -0
  60. package/build/server/socket.js +134 -0
  61. package/build/server/watcherWorker.js +51 -0
  62. package/build/shared/actionTypes.js +113 -0
  63. package/build/shared/types.js +1 -0
  64. package/package.json +111 -86
  65. package/build/267.4be526e3a94d53aeceae.js +0 -1
  66. package/build/591.4be526e3a94d53aeceae.js +0 -1
  67. package/build/598.4be526e3a94d53aeceae.js +0 -1
  68. package/build/799.4be526e3a94d53aeceae.js +0 -1
  69. package/build/845.4be526e3a94d53aeceae.js +0 -1
  70. package/build/main.4be526e3a94d53aeceae.css +0 -2034
  71. package/build/main.4be526e3a94d53aeceae.js +0 -1
  72. package/server/Library/Library.js +0 -340
  73. package/server/Library/index.js +0 -3
  74. package/server/Library/ipc.js +0 -18
  75. package/server/Library/router.js +0 -27
  76. package/server/Library/socket.js +0 -47
  77. package/server/Media/Media.js +0 -207
  78. package/server/Media/index.js +0 -3
  79. package/server/Media/ipc.js +0 -19
  80. package/server/Media/router.js +0 -99
  81. package/server/Player/socket.js +0 -78
  82. package/server/Prefs/Prefs.js +0 -165
  83. package/server/Prefs/index.js +0 -3
  84. package/server/Prefs/router.js +0 -124
  85. package/server/Prefs/socket.js +0 -68
  86. package/server/Queue/Queue.js +0 -208
  87. package/server/Queue/index.js +0 -3
  88. package/server/Queue/socket.js +0 -99
  89. package/server/Rooms/Rooms.js +0 -114
  90. package/server/Rooms/index.js +0 -3
  91. package/server/Rooms/router.js +0 -146
  92. package/server/Scanner/FileScanner/FileScanner.js +0 -225
  93. package/server/Scanner/FileScanner/getConfig.js +0 -35
  94. package/server/Scanner/FileScanner/getFiles.js +0 -63
  95. package/server/Scanner/FileScanner/index.js +0 -3
  96. package/server/Scanner/MetaParser/MetaParser.js +0 -49
  97. package/server/Scanner/MetaParser/defaultMiddleware.js +0 -197
  98. package/server/Scanner/MetaParser/index.js +0 -3
  99. package/server/Scanner/Scanner.js +0 -33
  100. package/server/User/User.js +0 -139
  101. package/server/User/index.js +0 -3
  102. package/server/User/router.js +0 -442
  103. package/server/lib/Database.js +0 -55
  104. package/server/lib/IPCBridge.js +0 -115
  105. package/server/lib/Log.js +0 -71
  106. package/server/lib/bcrypt.js +0 -24
  107. package/server/lib/cli.js +0 -136
  108. package/server/lib/electron.js +0 -81
  109. package/server/lib/getCdgName.js +0 -20
  110. package/server/lib/getDevMiddleware.js +0 -51
  111. package/server/lib/getFolders.js +0 -10
  112. package/server/lib/getHotMiddleware.js +0 -27
  113. package/server/lib/getIPAddress.js +0 -16
  114. package/server/lib/getPermutations.js +0 -21
  115. package/server/lib/getWindowsDrives.js +0 -30
  116. package/server/lib/parseCookie.js +0 -12
  117. package/server/lib/pushQueuesAndLibrary.js +0 -29
  118. package/server/main.js +0 -135
  119. package/server/scannerWorker.js +0 -58
  120. package/server/serverWorker.js +0 -242
  121. package/server/socket.js +0 -173
  122. package/shared/actionTypes.js +0 -103
  123. /package/build/{7ce9eb3fe454f54745a4.woff2 → client/7ce9eb3fe454f54745a4.woff2} +0 -0
  124. /package/build/{598.4be526e3a94d53aeceae.css → client/851.a51d7d3f87c474adad54.css} +0 -0
  125. /package/build/{a35814dd9eb496e3d7cc.woff2 → client/a35814dd9eb496e3d7cc.woff2} +0 -0
  126. /package/build/{e419b95dccb58b362811.woff2 → client/e419b95dccb58b362811.woff2} +0 -0
  127. /package/{server → build/server}/lib/schemas/002-replaygain.sql +0 -0
  128. /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
+ })();