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.
Files changed (79) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +49 -0
  3. package/assets/app.ico +0 -0
  4. package/assets/app.png +0 -0
  5. package/assets/favicon.ico +0 -0
  6. package/assets/mic-blackTemplate.png +0 -0
  7. package/assets/mic-blackTemplate@2x.png +0 -0
  8. package/assets/mic-white.png +0 -0
  9. package/assets/mic-white@2x.png +0 -0
  10. package/assets/robots.txt +2 -0
  11. package/build/267.4be526e3a94d53aeceae.js +1 -0
  12. package/build/591.4be526e3a94d53aeceae.js +1 -0
  13. package/build/598.4be526e3a94d53aeceae.css +5 -0
  14. package/build/598.4be526e3a94d53aeceae.js +1 -0
  15. package/build/799.4be526e3a94d53aeceae.js +1 -0
  16. package/build/7ce9eb3fe454f54745a4.woff2 +0 -0
  17. package/build/845.4be526e3a94d53aeceae.css +132 -0
  18. package/build/845.4be526e3a94d53aeceae.js +1 -0
  19. package/build/a35814dd9eb496e3d7cc.woff2 +0 -0
  20. package/build/e419b95dccb58b362811.woff2 +0 -0
  21. package/build/index.html +1 -0
  22. package/build/licenses.txt +1400 -0
  23. package/build/main.4be526e3a94d53aeceae.css +2034 -0
  24. package/build/main.4be526e3a94d53aeceae.js +1 -0
  25. package/package.json +144 -0
  26. package/server/Library/Library.js +340 -0
  27. package/server/Library/index.js +3 -0
  28. package/server/Library/ipc.js +18 -0
  29. package/server/Library/router.js +27 -0
  30. package/server/Library/socket.js +47 -0
  31. package/server/Media/Media.js +207 -0
  32. package/server/Media/index.js +3 -0
  33. package/server/Media/ipc.js +19 -0
  34. package/server/Media/router.js +99 -0
  35. package/server/Player/socket.js +78 -0
  36. package/server/Prefs/Prefs.js +165 -0
  37. package/server/Prefs/index.js +3 -0
  38. package/server/Prefs/router.js +124 -0
  39. package/server/Prefs/socket.js +68 -0
  40. package/server/Queue/Queue.js +208 -0
  41. package/server/Queue/index.js +3 -0
  42. package/server/Queue/socket.js +99 -0
  43. package/server/Rooms/Rooms.js +114 -0
  44. package/server/Rooms/index.js +3 -0
  45. package/server/Rooms/router.js +146 -0
  46. package/server/Scanner/FileScanner/FileScanner.js +225 -0
  47. package/server/Scanner/FileScanner/getConfig.js +35 -0
  48. package/server/Scanner/FileScanner/getFiles.js +63 -0
  49. package/server/Scanner/FileScanner/index.js +3 -0
  50. package/server/Scanner/MetaParser/MetaParser.js +49 -0
  51. package/server/Scanner/MetaParser/defaultMiddleware.js +197 -0
  52. package/server/Scanner/MetaParser/index.js +3 -0
  53. package/server/Scanner/Scanner.js +33 -0
  54. package/server/User/User.js +139 -0
  55. package/server/User/index.js +3 -0
  56. package/server/User/router.js +442 -0
  57. package/server/lib/Database.js +55 -0
  58. package/server/lib/IPCBridge.js +115 -0
  59. package/server/lib/Log.js +71 -0
  60. package/server/lib/bcrypt.js +24 -0
  61. package/server/lib/cli.js +136 -0
  62. package/server/lib/electron.js +81 -0
  63. package/server/lib/getCdgName.js +20 -0
  64. package/server/lib/getDevMiddleware.js +51 -0
  65. package/server/lib/getFolders.js +10 -0
  66. package/server/lib/getHotMiddleware.js +27 -0
  67. package/server/lib/getIPAddress.js +16 -0
  68. package/server/lib/getPermutations.js +21 -0
  69. package/server/lib/getWindowsDrives.js +30 -0
  70. package/server/lib/parseCookie.js +12 -0
  71. package/server/lib/pushQueuesAndLibrary.js +29 -0
  72. package/server/lib/schemas/001-initial-schema.sql +98 -0
  73. package/server/lib/schemas/002-replaygain.sql +9 -0
  74. package/server/lib/schemas/003-queue-linked-list.sql +16 -0
  75. package/server/main.js +135 -0
  76. package/server/scannerWorker.js +58 -0
  77. package/server/serverWorker.js +242 -0
  78. package/server/socket.js +173 -0
  79. package/shared/actionTypes.js +103 -0
@@ -0,0 +1,98 @@
1
+ -- Up
2
+ CREATE TABLE IF NOT EXISTS "artists" (
3
+ "artistId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
4
+ "name" text NOT NULL COLLATE NOCASE,
5
+ "nameNorm" text NOT NULL COLLATE NOCASE
6
+ );
7
+
8
+ CREATE UNIQUE INDEX IF NOT EXISTS idxNameNorm ON "artists" ("nameNorm" ASC);
9
+
10
+ CREATE TABLE IF NOT EXISTS "media" (
11
+ mediaId integer PRIMARY KEY AUTOINCREMENT NOT NULL,
12
+ songId integer NOT NULL REFERENCES songs(songId) DEFERRABLE INITIALLY DEFERRED,
13
+ pathId integer NOT NULL,
14
+ relPath text NOT NULL,
15
+ duration integer NOT NULL,
16
+ isPreferred integer(1) NOT NULL DEFAULT(0),
17
+ dateAdded integer NOT NULL DEFAULT(0),
18
+ dateUpdated integer NOT NULL DEFAULT(0)
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idxSong ON "media" ("songId" ASC);
22
+
23
+ CREATE TABLE IF NOT EXISTS "paths" (
24
+ "pathId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
25
+ "path" text NOT NULL,
26
+ "priority" integer NOT NULL
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS "prefs" (
30
+ "key" text PRIMARY KEY NOT NULL,
31
+ "data" text NOT NULL
32
+ );
33
+
34
+ INSERT INTO prefs (key,data) VALUES ('isFirstRun','true');
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
+
43
+ CREATE INDEX IF NOT EXISTS idxRoom ON "queue" ("roomId" ASC);
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" text NOT NULL
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS "songs" (
54
+ "songId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
55
+ "artistId" integer NOT NULL REFERENCES artists(artistId) DEFERRABLE INITIALLY DEFERRED,
56
+ "title" text NOT NULL COLLATE NOCASE,
57
+ "titleNorm" text NOT NULL COLLATE NOCASE
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idxTitleNorm ON "songs" ("titleNorm" ASC);
61
+
62
+ CREATE TABLE IF NOT EXISTS "artistStars" (
63
+ "userId" integer NOT NULL REFERENCES users(userId) DEFERRABLE INITIALLY DEFERRED,
64
+ "artistId" integer NOT NULL
65
+ );
66
+
67
+ CREATE UNIQUE INDEX IF NOT EXISTS idxUserArtist ON "artistStars" ("userId" ASC, "artistId" ASC);
68
+
69
+ CREATE TABLE IF NOT EXISTS "songStars" (
70
+ "userId" integer NOT NULL REFERENCES users(userId) DEFERRABLE INITIALLY DEFERRED,
71
+ "songId" integer NOT NULL
72
+ );
73
+
74
+ CREATE UNIQUE INDEX IF NOT EXISTS idxUserSong ON "songStars" ("userId" ASC, "songId" ASC);
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
+
87
+ CREATE UNIQUE INDEX IF NOT EXISTS idxUsername ON "users" ("username" ASC);
88
+
89
+ -- Down
90
+ DROP TABLE artists;
91
+ DROP TABLE media;
92
+ DROP TABLE prefs;
93
+ DROP TABLE queue;
94
+ DROP TABLE rooms;
95
+ DROP TABLE songs;
96
+ DROP TABLE artistStars;
97
+ DROP TABLE songStars;
98
+ DROP TABLE users;
@@ -0,0 +1,9 @@
1
+ -- Up
2
+ ALTER TABLE "media" ADD COLUMN "rgTrackGain" REAL;
3
+ ALTER TABLE "media" ADD COLUMN "rgTrackPeak" REAL;
4
+
5
+ INSERT OR IGNORE INTO prefs (key,data) VALUES ('isReplayGainEnabled','false');
6
+
7
+ -- Down
8
+ ALTER TABLE "media" DROP COLUMN "rgTrackGain";
9
+ ALTER TABLE "media" DROP COLUMN "rgTrackPeak";
@@ -0,0 +1,16 @@
1
+ -- Up
2
+ ALTER TABLE "queue" ADD COLUMN "prevQueueId" INTEGER REFERENCES queue(queueId) DEFERRABLE INITIALLY DEFERRED;
3
+
4
+ CREATE INDEX IF NOT EXISTS idxPrevQueueId ON "queue" ("prevQueueId" ASC);
5
+
6
+ UPDATE queue
7
+ SET prevQueueId = (
8
+ SELECT MAX(q.queueId)
9
+ FROM queue q
10
+ WHERE q.queueId < queue.queueId AND q.roomId = queue.roomId
11
+ );
12
+
13
+ -- Down
14
+ DROP INDEX IF EXISTS idxPrevQueueId;
15
+
16
+ ALTER TABLE "queue" DROP COLUMN "prevQueueId";
package/server/main.js ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ const childProcess = require('child_process')
3
+ const path = require('path')
4
+ const env = require('./lib/cli')
5
+ const { Log } = require('./lib/Log')
6
+
7
+ const log = new Log('server', {
8
+ console: Log.resolve(env.KES_CONSOLE_LEVEL, env.NODE_ENV === 'development' ? 5 : 4),
9
+ file: Log.resolve(env.KES_LOG_LEVEL, env.NODE_ENV === 'development' ? 0 : 3),
10
+ }).setDefaultInstance().logger.scope(`main[${process.pid}]`)
11
+
12
+ const scannerLog = new Log('scanner', {
13
+ console: Log.resolve(process.env.KES_SCAN_CONSOLE_LEVEL, process.env.NODE_ENV === 'development' ? 5 : 4),
14
+ file: Log.resolve(process.env.KES_SCAN_LOG_LEVEL, process.env.NODE_ENV === 'development' ? 0 : 3),
15
+ }).logger.scope('scanner')
16
+
17
+ const Database = require('./lib/Database')
18
+ const IPC = require('./lib/IPCBridge')
19
+ const refs = {}
20
+ const {
21
+ SCANNER_CMD_START,
22
+ SCANNER_CMD_STOP,
23
+ SERVER_WORKER_ERROR,
24
+ SCANNER_WORKER_LOG,
25
+ SERVER_WORKER_STATUS,
26
+ } = require('../shared/actionTypes')
27
+
28
+ // handle scanner logs
29
+ // @todo: this doesn't need to be async
30
+ IPC.use({
31
+ [SCANNER_WORKER_LOG]: async (action) => {
32
+ scannerLog[action.payload.level](action.payload.msg)
33
+ }
34
+ })
35
+
36
+ // log non-default settings
37
+ for (const key in env) {
38
+ if (process.env[key]) log.verbose(`${key}=${process.env[key]}`)
39
+ }
40
+
41
+ // support PUID/PGID convention (group MUST be set before user!)
42
+ if (Number.isInteger(env.KES_PGID)) {
43
+ log.verbose(`PGID=${env.KES_PGID}`)
44
+ process.setgid(env.KES_PGID)
45
+ }
46
+
47
+ if (Number.isInteger(env.KES_PUID)) {
48
+ log.verbose(`PUID=${env.KES_PUID}`)
49
+ process.setuid(env.KES_PUID)
50
+ }
51
+
52
+ // close db before exiting (can't do async in the 'exit' handler)
53
+ process.on('SIGTERM', shutdown)
54
+ process.on('SIGINT', shutdown)
55
+
56
+ // make sure child processes don't hang around
57
+ process.on('exit', function () {
58
+ if (refs.server) refs.server.kill()
59
+ if (refs.scanner) refs.scanner.kill()
60
+ })
61
+
62
+ // debug: log stack trace for unhandled promise rejections
63
+ process.on('unhandledRejection', (reason, p) => {
64
+ log.error('Unhandled Rejection:', p, 'reason:', reason)
65
+ })
66
+
67
+ // detect electron
68
+ if (process.versions.electron) {
69
+ refs.electron = require('./lib/electron.js')({ env })
70
+ env.KES_PATH_DATA = refs.electron.app.getPath('userData')
71
+ }
72
+
73
+ Database.open({
74
+ file: path.join(env.KES_PATH_DATA, 'database.sqlite3'),
75
+ ro: false,
76
+ }).then(db => {
77
+ if (refs.electron) {
78
+ process.on('serverWorker', action => {
79
+ const { type, payload } = action
80
+
81
+ if (type === SERVER_WORKER_STATUS) {
82
+ return refs.electron.setStatus('url', payload.url)
83
+ } else if (type === SERVER_WORKER_ERROR) {
84
+ return refs.electron.setError(action.error)
85
+ }
86
+ })
87
+ }
88
+
89
+ // start web server
90
+ require('./serverWorker.js')({ env, startScanner, stopScanner })
91
+ }).catch(err => {
92
+ log.error(err.message)
93
+ process.exit(1)
94
+ })
95
+
96
+ function startScanner (onExit) {
97
+ if (refs.scanner === undefined) {
98
+ log.info('Starting media scanner process')
99
+ refs.scanner = childProcess.fork(path.join(__dirname, 'scannerWorker.js'), [], {
100
+ env: { ...env, KES_CHILD_PROCESS: 'scanner' },
101
+ gid: Number.isInteger(env.KES_PGID) ? env.KES_PGID : undefined,
102
+ uid: Number.isInteger(env.KES_PUID) ? env.KES_PUID : undefined,
103
+ })
104
+
105
+ refs.scanner.on('exit', (code, signal) => {
106
+ log.info(`Media scanner exited (${signal || code})`)
107
+ IPC.removeChild(refs.scanner)
108
+ delete refs.scanner
109
+
110
+ if (typeof onExit === 'function') onExit()
111
+ })
112
+
113
+ IPC.addChild(refs.scanner)
114
+ } else {
115
+ IPC.send({ type: SCANNER_CMD_START })
116
+ }
117
+ }
118
+
119
+ function stopScanner () {
120
+ if (refs.scanner) {
121
+ IPC.send({ type: SCANNER_CMD_STOP })
122
+ }
123
+ }
124
+
125
+ function shutdown (signal) {
126
+ log.info('Received %s', signal)
127
+
128
+ Database.close().then(() => {
129
+ log.info('Goodbye for now...')
130
+ process.exit(0)
131
+ }).catch(err => {
132
+ log.error(err.message)
133
+ process.exit(1)
134
+ })
135
+ }
@@ -0,0 +1,58 @@
1
+ const path = require('path')
2
+ const log = require('./lib/Log')(`scanner[${process.pid}]`)
3
+ const Database = require('./lib/Database')
4
+ const IPC = require('./lib/IPCBridge')
5
+ const {
6
+ SCANNER_CMD_START,
7
+ SCANNER_CMD_STOP,
8
+ } = require('../shared/actionTypes')
9
+
10
+ let FileScanner, Prefs
11
+ let _Scanner
12
+ let _isScanQueued = true
13
+
14
+ Database.open({
15
+ file: path.join(process.env.KES_PATH_DATA, 'database.sqlite3'),
16
+ ro: true,
17
+ }).then(db => {
18
+ Prefs = require('./Prefs')
19
+ FileScanner = require('./Scanner/FileScanner')
20
+
21
+ IPC.use({
22
+ [SCANNER_CMD_START]: async () => {
23
+ log.info('Media scan requested (restarting)')
24
+ _isScanQueued = true
25
+ cancelScan()
26
+ },
27
+ [SCANNER_CMD_STOP]: async () => {
28
+ log.info('Stopping media scan (user requested)')
29
+ _isScanQueued = false
30
+ cancelScan()
31
+ }
32
+ })
33
+
34
+ startScan()
35
+ }).catch(err => {
36
+ log.error(err.message)
37
+ process.exit(1)
38
+ })
39
+
40
+ async function startScan () {
41
+ log.info('Starting media scan')
42
+
43
+ while (_isScanQueued) {
44
+ _isScanQueued = false
45
+
46
+ const prefs = await Prefs.get()
47
+ _Scanner = new FileScanner(prefs)
48
+ await _Scanner.scan()
49
+ } // end while
50
+
51
+ process.exit(0)
52
+ }
53
+
54
+ function cancelScan () {
55
+ if (_Scanner) {
56
+ _Scanner.cancel()
57
+ }
58
+ }
@@ -0,0 +1,242 @@
1
+ const log = require('./lib/Log')('server')
2
+ const path = require('path')
3
+ const getIPAddress = require('./lib/getIPAddress')
4
+ const http = require('http')
5
+ const fs = require('fs')
6
+ const { promisify } = require('util')
7
+ const parseCookie = require('./lib/parseCookie')
8
+ const jwtVerify = require('jsonwebtoken').verify
9
+ const Koa = require('koa')
10
+ const koaRouter = require('koa-router')
11
+ const koaBody = require('koa-body')
12
+ const koaFavicon = require('koa-favicon')
13
+ const koaLogger = require('koa-logger')
14
+ const koaMount = require('koa-mount')
15
+ const koaRange = require('koa-range')
16
+ const koaStatic = require('koa-static')
17
+
18
+ const Prefs = require('./Prefs')
19
+ const libraryRouter = require('./Library/router')
20
+ const mediaRouter = require('./Media/router')
21
+ const prefsRouter = require('./Prefs/router')
22
+ const roomsRouter = require('./Rooms/router')
23
+ const userRouter = require('./User/router')
24
+ const pushQueuesAndLibrary = require('./lib/pushQueuesAndLibrary')
25
+ const SocketIO = require('socket.io')
26
+ const socketActions = require('./socket')
27
+ const IPC = require('./lib/IPCBridge')
28
+ const IPCLibraryActions = require('./Library/ipc')
29
+ const IPCMediaActions = require('./Media/ipc')
30
+ const {
31
+ SERVER_WORKER_STATUS,
32
+ SERVER_WORKER_ERROR,
33
+ } = require('../shared/actionTypes')
34
+
35
+ async function serverWorker ({ env, startScanner, stopScanner }) {
36
+ const indexFile = path.join(env.KES_PATH_WEBROOT, 'index.html')
37
+ const urlPath = env.KES_URL_PATH.replace(/\/?$/, '/') // force trailing slash
38
+ const jwtKey = await Prefs.getJwtKey(env.KES_ROTATE_KEY)
39
+ const app = new Koa()
40
+ let server, io
41
+
42
+ // called when middleware is finalized
43
+ function createServer () {
44
+ server = http.createServer(app.callback())
45
+
46
+ // http server error handler
47
+ server.on('error', function (err) {
48
+ log.error(err)
49
+
50
+ process.emit('serverWorker', {
51
+ type: SERVER_WORKER_ERROR,
52
+ error: err.message,
53
+ })
54
+
55
+ // not much we can do without a working server
56
+ process.exit(1)
57
+ })
58
+
59
+ // create socket.io server
60
+ io = SocketIO(server, {
61
+ path: urlPath + 'socket.io',
62
+ serveClient: false,
63
+ })
64
+
65
+ // attach socket.io handlers
66
+ socketActions(io, jwtKey)
67
+
68
+ // attach IPC action handlers
69
+ IPC.use(IPCLibraryActions(io))
70
+ IPC.use(IPCMediaActions(io))
71
+
72
+ // success callback in 3rd arg
73
+ server.listen(env.KES_PORT, () => {
74
+ const port = server.address().port
75
+ const url = `http://${getIPAddress()}${port === 80 ? '' : ':' + port}${urlPath}`
76
+ log.info(`Web server running at ${url}`)
77
+
78
+ process.emit('serverWorker', {
79
+ type: SERVER_WORKER_STATUS,
80
+ payload: { url },
81
+ })
82
+
83
+ // scanning on startup?
84
+ if (env.KES_SCAN) startScanner(() => pushQueuesAndLibrary(io))
85
+ })
86
+ }
87
+
88
+ // --------------------
89
+ // Begin Koa middleware
90
+ // --------------------
91
+
92
+ // server error handler
93
+ app.on('error', (err, ctx) => {
94
+ if (err.code === 'EPIPE') {
95
+ // these are common since browsers make multiple requests for media files
96
+ log.verbose(err.message)
97
+ return
98
+ }
99
+
100
+ // silence 4xx response "errors" (koa-logger should show these anyway)
101
+ if (ctx.response && ctx.response.status.toString().startsWith('4')) {
102
+ return
103
+ }
104
+
105
+ if (err.stack) log.error(err.stack)
106
+ else log.error(err)
107
+ })
108
+
109
+ // middleware error handler
110
+ app.use(async (ctx, next) => {
111
+ try {
112
+ await next()
113
+ } catch (err) {
114
+ ctx.status = err.status || 500
115
+ ctx.body = err.message
116
+ ctx.app.emit('error', err, ctx)
117
+ }
118
+ })
119
+
120
+ // http request/response logging
121
+ app.use(koaLogger((str, args) => (args.length === 6 && args[3] >= 500) ? log.error(str) : log.debug(str)))
122
+
123
+ app.use(koaFavicon(path.join(env.KES_PATH_ASSETS, 'favicon.ico')))
124
+ app.use(koaRange)
125
+ app.use(koaBody({ multipart: true }))
126
+
127
+ // all http requests
128
+ app.use(async (ctx, next) => {
129
+ ctx.jwtKey = jwtKey // used by login route
130
+
131
+ // skip JWT/session validation if non-API request or logging in/out
132
+ if (!ctx.request.path.startsWith(`${urlPath}api/`) ||
133
+ ctx.request.path === `${urlPath}api/login` ||
134
+ ctx.request.path === `${urlPath}api/logout`) {
135
+ return next()
136
+ }
137
+
138
+ // verify JWT
139
+ try {
140
+ const { keToken } = parseCookie(ctx.request.header.cookie)
141
+ ctx.user = jwtVerify(keToken, jwtKey)
142
+ } catch (err) {
143
+ ctx.user = {
144
+ dateUpdated: null,
145
+ isAdmin: false,
146
+ name: null,
147
+ roomId: null,
148
+ userId: null,
149
+ username: null,
150
+ }
151
+ }
152
+
153
+ // validated
154
+ ctx.io = io
155
+ ctx.startScanner = () => startScanner(() => pushQueuesAndLibrary(io))
156
+ ctx.stopScanner = stopScanner
157
+
158
+ await next()
159
+ })
160
+
161
+ // http api endpoints
162
+ const baseRouter = koaRouter({
163
+ prefix: urlPath.replace(/\/$/, '') // avoid double slashes with /api prefix
164
+ })
165
+
166
+ baseRouter.use(libraryRouter.routes())
167
+ baseRouter.use(mediaRouter.routes())
168
+ baseRouter.use(prefsRouter.routes())
169
+ baseRouter.use(roomsRouter.routes())
170
+ baseRouter.use(userRouter.routes())
171
+ app.use(baseRouter.routes())
172
+
173
+ // serve index.html with dynamic base tag at the main SPA routes
174
+ const createIndexMiddleware = content => {
175
+ const indexRoutes = [
176
+ urlPath,
177
+ ...['account', 'library', 'queue', 'player'].map(r => urlPath + r + '/')
178
+ ]
179
+
180
+ content = content.replace('<base href="/">', `<base href="${urlPath}">`)
181
+
182
+ return async (ctx, next) => {
183
+ // use a trailing slash for matching purposes
184
+ const reqPath = ctx.request.path.replace(/\/?$/, '/')
185
+
186
+ if (!indexRoutes.includes(reqPath)) {
187
+ return next()
188
+ }
189
+
190
+ ctx.set('content-type', 'text/html')
191
+ ctx.body = content
192
+ ctx.status = 200
193
+ }
194
+ }
195
+
196
+ if (env.NODE_ENV !== 'development') {
197
+ // make sure we handle index.html before koaStatic,
198
+ // otherwise it'll be served without dynamic base tag
199
+ app.use(createIndexMiddleware(await promisify(fs.readFile)(indexFile, 'utf8')))
200
+
201
+ // serve build and asset folders
202
+ app.use(koaMount(urlPath, koaStatic(env.KES_PATH_WEBROOT)))
203
+ app.use(koaMount(`${urlPath}assets`, koaStatic(env.KES_PATH_ASSETS)))
204
+
205
+ createServer()
206
+ return
207
+ }
208
+
209
+ // ----------------------
210
+ // Development middleware
211
+ // ----------------------
212
+ log.info('Enabling webpack dev and HMR middleware')
213
+ const webpack = require('webpack')
214
+ const webpackConfig = require('../config/webpack.config')
215
+ const compiler = webpack(webpackConfig)
216
+
217
+ compiler.hooks.done.tap('indexPlugin', async (params) => {
218
+ const indexContent = await new Promise((resolve, reject) => {
219
+ compiler.outputFileSystem.readFile(indexFile, 'utf8', (err, result) => {
220
+ if (err) return reject(err)
221
+ return resolve(result)
222
+ })
223
+ })
224
+
225
+ // @todo make this less hacky
226
+ if (!server) {
227
+ app.use(createIndexMiddleware(indexContent))
228
+ createServer()
229
+ }
230
+ })
231
+
232
+ const devMiddleware = require('./lib/getDevMiddleware')(compiler, { publicPath: urlPath })
233
+ app.use(devMiddleware)
234
+
235
+ const hotMiddleware = require('./lib/getHotMiddleware')(compiler)
236
+ app.use(hotMiddleware)
237
+
238
+ // serve assets since webpack-dev-server is unaware of this folder
239
+ app.use(koaMount(`${urlPath}assets`, koaStatic(env.KES_PATH_ASSETS)))
240
+ }
241
+
242
+ module.exports = serverWorker