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,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
|