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,208 @@
1
+ const path = require('path')
2
+ const db = require('../lib/Database').db
3
+ const sql = require('sqlate')
4
+
5
+ class Queue {
6
+ /**
7
+ * Add a songId to a room's queue
8
+ *
9
+ * @param {object} roomId, songId, userId
10
+ * @return {Promise}
11
+ */
12
+ static async add ({ roomId, songId, userId }) {
13
+ const fields = new Map()
14
+ fields.set('roomId', roomId)
15
+ fields.set('songId', songId)
16
+ fields.set('userId', userId)
17
+ fields.set('prevQueueId', sql`(
18
+ SELECT queueId
19
+ FROM queue
20
+ WHERE roomId = ${roomId} AND queueId NOT IN (
21
+ SELECT prevQueueId
22
+ FROM queue
23
+ WHERE prevQueueId IS NOT NULL
24
+ )
25
+ )`)
26
+
27
+ const query = sql`
28
+ INSERT INTO queue ${sql.tuple(Array.from(fields.keys()).map(sql.column))}
29
+ VALUES ${sql.tuple(Array.from(fields.values()))}
30
+ `
31
+ const res = await db.run(String(query), query.parameters)
32
+
33
+ if (res.changes !== 1) {
34
+ throw new Error('Could not add song to queue')
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get queued items for a given room
40
+ *
41
+ * @param {Number} roomId
42
+ * @return {Promise}
43
+ */
44
+ static async get (roomId) {
45
+ const result = []
46
+ const entities = {}
47
+ const map = new Map()
48
+ let curQueueId = null
49
+
50
+ const query = sql`
51
+ SELECT queueId, songId, userId, prevQueueId,
52
+ media.mediaId, media.relPath, media.rgTrackGain, media.rgTrackPeak,
53
+ users.name AS userDisplayName, users.dateUpdated,
54
+ MAX(isPreferred) AS isPreferred
55
+ FROM queue
56
+ INNER JOIN users USING(userId)
57
+ INNER JOIN media USING(songId)
58
+ INNER JOIN paths USING(pathId)
59
+ WHERE roomId = ${roomId}
60
+ GROUP BY queueId
61
+ ORDER BY queueId, paths.priority ASC
62
+ `
63
+ const rows = await db.all(String(query), query.parameters)
64
+
65
+ for (const row of rows) {
66
+ entities[row.queueId] = row
67
+ entities[row.queueId].mediaType = this.getType(row.relPath)
68
+
69
+ // don't send over the wire
70
+ delete entities[row.queueId].relPath
71
+ delete entities[row.queueId].isPreferred
72
+
73
+ if (row.prevQueueId === null) {
74
+ // found the first item
75
+ result.push(row.queueId)
76
+ curQueueId = row.queueId
77
+ } else {
78
+ // map indexed by prevQueueId
79
+ map.set(row.prevQueueId, row.queueId)
80
+ }
81
+ }
82
+
83
+ while (result.length < rows.length) {
84
+ // get the item whose prevQueueId references the current one
85
+ const nextQueueId = entities[map.get(curQueueId)].queueId
86
+ result.push(nextQueueId)
87
+ curQueueId = nextQueueId
88
+ }
89
+
90
+ return { result, entities }
91
+ }
92
+
93
+ /**
94
+ * Move a queue item
95
+ * @param {object} prevQueueId, queueId, roomId
96
+ * @return {Promise} undefined
97
+ */
98
+ static async move ({ prevQueueId, queueId, roomId }) {
99
+ if (queueId === prevQueueId) {
100
+ throw new Error('Invalid prevQueueId')
101
+ }
102
+
103
+ if (prevQueueId === -1) prevQueueId = null
104
+
105
+ const query = sql`
106
+ UPDATE queue
107
+ SET prevQueueId = CASE
108
+ WHEN queueId = newChild THEN ${queueId}
109
+ WHEN queueId = curChild AND curParent IS NOT NULL AND newChild IS NOT NULL THEN curParent
110
+ WHEN queueId = ${queueId} THEN ${prevQueueId}
111
+ ELSE queue.prevQueueId
112
+ END
113
+ FROM (SELECT
114
+ (
115
+ SELECT prevQueueId
116
+ FROM queue
117
+ WHERE queueId = ${queueId}
118
+ ) AS curParent,
119
+ (
120
+ SELECT queueId
121
+ FROM queue
122
+ WHERE prevQueueId = ${queueId}
123
+ ) AS curChild,
124
+ (
125
+ SELECT queueId
126
+ FROM queue
127
+ WHERE queueId != ${queueId}
128
+ AND prevQueueId ${prevQueueId === null ? sql`IS NULL` : sql`= ${prevQueueId}`}
129
+ AND roomId = ${roomId}
130
+ ) AS newChild
131
+ )
132
+ WHERE roomId = ${roomId}
133
+ `
134
+ await db.run(String(query), query.parameters)
135
+ }
136
+
137
+ /**
138
+ * Delete a queue item
139
+ *
140
+ * We could DELETE first and get the deleted item's prevQueueId using
141
+ * RETURNING, but the DELETE and UPDATE need to be wrapped in a transaction
142
+ * (so the prevQueueId foreign key check is deferred). Also, v0.9 betas didn't
143
+ * have prevQueueId DEFFERABLE, and so will still error at DELETE (do we care?)
144
+ *
145
+ * @param {object} queueId, userId
146
+ * @return {Promise} undefined
147
+ */
148
+ static async remove (queueId) {
149
+ // close the soon-to-be gap first
150
+ const updateQuery = sql`
151
+ UPDATE queue
152
+ SET prevQueueId = curParent
153
+ FROM (
154
+ SELECT
155
+ (
156
+ SELECT prevQueueId
157
+ FROM queue
158
+ WHERE queueId = ${queueId}
159
+ ) AS curParent,
160
+ (
161
+ SELECT queueId
162
+ FROM queue
163
+ WHERE prevQueueId = ${queueId}
164
+ ) AS curChild
165
+ )
166
+ WHERE queueId = curChild
167
+ `
168
+ await db.run(String(updateQuery), updateQuery.parameters)
169
+
170
+ // delete item
171
+ const deleteQuery = sql`
172
+ DELETE FROM queue
173
+ WHERE queueId = ${queueId}
174
+ `
175
+ const deleteRes = await db.run(String(deleteQuery), deleteQuery.parameters)
176
+
177
+ if (!deleteRes.changes) {
178
+ throw new Error(`Could not remove queueId: ${queueId}`)
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Check if a user owns a queue item
184
+ * @param {number} userId
185
+ * @param {number} queueId
186
+ * @return {boolean}
187
+ */
188
+ static async isOwner (userId, queueId) {
189
+ const query = sql`
190
+ SELECT COUNT(*) AS count
191
+ FROM queue
192
+ WHERE userId = ${userId} AND queueId = ${queueId}
193
+ `
194
+ const res = await db.get(String(query), query.parameters)
195
+ return res.count === 1
196
+ }
197
+
198
+ /**
199
+ * Get media type from file extension
200
+ * @param {string} file filename
201
+ * @return {string} player component
202
+ */
203
+ static getType (file) {
204
+ return /\.mp4/i.test(path.extname(file)) ? 'mp4' : 'cdg'
205
+ }
206
+ }
207
+
208
+ module.exports = Queue
@@ -0,0 +1,3 @@
1
+ const Queue = require('./Queue')
2
+
3
+ module.exports = Queue
@@ -0,0 +1,99 @@
1
+ const Queue = require('./Queue')
2
+ const Rooms = require('../Rooms')
3
+
4
+ const {
5
+ QUEUE_ADD,
6
+ QUEUE_MOVE,
7
+ QUEUE_REMOVE,
8
+ QUEUE_PUSH,
9
+ } = require('../../shared/actionTypes')
10
+
11
+ // ------------------------------------
12
+ // Action Handlers
13
+ // ------------------------------------
14
+ const ACTION_HANDLERS = {
15
+ [QUEUE_ADD]: async (sock, { payload }, acknowledge) => {
16
+ const { songId } = payload
17
+
18
+ try {
19
+ await Rooms.validate(sock.user.roomId, null, { validatePassword: false })
20
+ } catch (err) {
21
+ return acknowledge({
22
+ type: QUEUE_ADD + '_ERROR',
23
+ error: err.message,
24
+ })
25
+ }
26
+
27
+ await Queue.add({
28
+ roomId: sock.user.roomId,
29
+ songId,
30
+ userId: sock.user.userId,
31
+ })
32
+
33
+ // success
34
+ acknowledge({ type: QUEUE_ADD + '_SUCCESS' })
35
+
36
+ // to all in room
37
+ sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
38
+ type: QUEUE_PUSH,
39
+ payload: await Queue.get(sock.user.roomId)
40
+ })
41
+ },
42
+ [QUEUE_MOVE]: async (sock, { payload }, acknowledge) => {
43
+ const { queueId, prevQueueId } = payload
44
+
45
+ try {
46
+ await Rooms.validate(sock.user.roomId, null, { validatePassword: false })
47
+ } catch (err) {
48
+ return acknowledge({
49
+ type: QUEUE_MOVE + '_ERROR',
50
+ error: err.message,
51
+ })
52
+ }
53
+
54
+ if (!sock.user.isAdmin && !await Queue.isOwner(sock.user.userId, queueId)) {
55
+ return acknowledge({
56
+ type: QUEUE_MOVE + '_ERROR',
57
+ error: 'Cannot move another user\'s song',
58
+ })
59
+ }
60
+
61
+ await Queue.move({
62
+ prevQueueId,
63
+ queueId,
64
+ roomId: sock.user.roomId,
65
+ })
66
+
67
+ // success
68
+ acknowledge({ type: QUEUE_MOVE + '_SUCCESS' })
69
+
70
+ // tell room
71
+ sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
72
+ type: QUEUE_PUSH,
73
+ payload: await Queue.get(sock.user.roomId)
74
+ })
75
+ },
76
+ [QUEUE_REMOVE]: async (sock, { payload }, acknowledge) => {
77
+ const { queueId } = payload
78
+
79
+ if (!sock.user.isAdmin && !await Queue.isOwner(sock.user.userId, queueId)) {
80
+ return acknowledge({
81
+ type: QUEUE_REMOVE + '_ERROR',
82
+ error: 'Cannot remove another user\'s song',
83
+ })
84
+ }
85
+
86
+ await Queue.remove(queueId)
87
+
88
+ // success
89
+ acknowledge({ type: QUEUE_REMOVE + '_SUCCESS' })
90
+
91
+ // tell room
92
+ sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
93
+ type: QUEUE_PUSH,
94
+ payload: await Queue.get(sock.user.roomId)
95
+ })
96
+ },
97
+ }
98
+
99
+ module.exports = ACTION_HANDLERS
@@ -0,0 +1,114 @@
1
+ const bcrypt = require('../lib/bcrypt')
2
+ const db = require('../lib/Database').db
3
+ const sql = require('sqlate')
4
+
5
+ class Rooms {
6
+ /**
7
+ * Get all rooms
8
+ *
9
+ * @param {Boolean} closed Whether to include rooms with status "closed"
10
+ * @return {Promise}
11
+ */
12
+ static async get (closed = false) {
13
+ const result = []
14
+ const entities = {}
15
+ const whereClause = closed ? sql`true` : sql`status = "open"`
16
+
17
+ const query = sql`
18
+ SELECT * FROM rooms
19
+ WHERE ${whereClause}
20
+ ORDER BY dateCreated DESC
21
+ `
22
+ const res = await db.all(String(query), query.parameters)
23
+
24
+ res.forEach(row => {
25
+ row.dateCreated = row.dateCreated.substring(0, 10)
26
+ row.hasPassword = !!row.password
27
+ delete row.password
28
+
29
+ result.push(row.roomId)
30
+ entities[row.roomId] = row
31
+ })
32
+
33
+ return { result, entities }
34
+ }
35
+
36
+ /**
37
+ * Validate a room against optional criteria
38
+ *
39
+ * @param {Number} roomId
40
+ * @param {[String]} password Room password
41
+ * @param {[Object]} opts (bool) isOpen, (bool) validatePassword
42
+ * @return {Promise} True if validated, otherwise throws an error
43
+ */
44
+ static async validate (roomId, password, { isOpen = true, validatePassword = true } = {}) {
45
+ const query = sql`
46
+ SELECT * FROM rooms
47
+ WHERE roomId = ${roomId}
48
+ `
49
+ const room = await db.get(String(query), query.parameters)
50
+
51
+ if (!room) {
52
+ throw new Error('Room not found')
53
+ }
54
+
55
+ if (isOpen && room.status !== 'open') {
56
+ throw new Error('Room is no longer open')
57
+ }
58
+
59
+ if (validatePassword && room.password) {
60
+ if (!password) {
61
+ throw new Error('Room password is required')
62
+ }
63
+
64
+ if (!await bcrypt.compare(password, room.password)) {
65
+ throw new Error('Incorrect room password')
66
+ }
67
+ }
68
+
69
+ return true
70
+ }
71
+
72
+ static prefix (roomId = '') {
73
+ return `ROOM_ID_${roomId}`
74
+ }
75
+
76
+ /**
77
+ * Utility method to list active rooms on a socket.io instance
78
+ *
79
+ * @param {Object} io The socket.io instance
80
+ * @return {Array} Array of objects as { room, roomId }
81
+ */
82
+ static getActive (io) {
83
+ const rooms = []
84
+
85
+ for (const room of io.sockets.adapter.rooms.keys()) {
86
+ // ignore auto-generated per-user rooms
87
+ if (room.startsWith(Rooms.prefix())) {
88
+ const roomId = parseInt(room.substring(Rooms.prefix().length), 10)
89
+ rooms.push({ room, roomId })
90
+ }
91
+ }
92
+
93
+ return rooms
94
+ }
95
+
96
+ /**
97
+ * Utility method to determine if a player is in a room
98
+ *
99
+ * @param {Object} io The socket.io instance
100
+ * @param {Object} roomId Room to check
101
+ * @return {Boolean}
102
+ */
103
+ static isPlayerPresent (io, roomId) {
104
+ for (const sock of io.of('/').sockets.values()) {
105
+ if (sock.user && sock.user.roomId === roomId && sock._lastPlayerStatus) {
106
+ return true
107
+ }
108
+ }
109
+
110
+ return false
111
+ }
112
+ }
113
+
114
+ module.exports = Rooms
@@ -0,0 +1,3 @@
1
+ const Rooms = require('./Rooms')
2
+
3
+ module.exports = Rooms
@@ -0,0 +1,146 @@
1
+ const bcrypt = require('../lib/bcrypt')
2
+ const db = require('../lib/Database').db
3
+ const sql = require('sqlate')
4
+ const KoaRouter = require('koa-router')
5
+ const router = KoaRouter({ prefix: '/api' })
6
+ const log = require('../lib/Log')('Rooms')
7
+ const Rooms = require('../Rooms')
8
+
9
+ const BCRYPT_ROUNDS = 12
10
+ const NAME_MIN_LENGTH = 1
11
+ const NAME_MAX_LENGTH = 50
12
+ const PASSWORD_MIN_LENGTH = 5
13
+ const STATUSES = ['open', 'closed']
14
+
15
+ // list rooms
16
+ router.get('/rooms', async (ctx, next) => {
17
+ // non-admins can only see open rooms
18
+ const res = await Rooms.get(ctx.user.isAdmin)
19
+
20
+ res.result.forEach(roomId => {
21
+ const room = ctx.io.sockets.adapter.rooms.get(Rooms.prefix(roomId))
22
+ res.entities[roomId].numUsers = room ? room.size : 0
23
+ })
24
+
25
+ ctx.body = res
26
+ })
27
+
28
+ // create room
29
+ router.post('/rooms', async (ctx, next) => {
30
+ if (!ctx.user.isAdmin) {
31
+ ctx.throw(401)
32
+ }
33
+
34
+ const { name, password, status } = ctx.request.body
35
+
36
+ if (!name || !name.trim() || name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
37
+ ctx.throw(400, `Room name must have ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`)
38
+ }
39
+
40
+ if (password && password.length < PASSWORD_MIN_LENGTH) {
41
+ ctx.throw(400, `Room password must have at least ${PASSWORD_MIN_LENGTH} characters`)
42
+ }
43
+
44
+ if (!status || !STATUSES.includes(status)) {
45
+ ctx.throw(400, 'Invalid room status')
46
+ }
47
+
48
+ const fields = new Map()
49
+ fields.set('name', name.trim())
50
+ fields.set('password', password ? await bcrypt.hash(password, BCRYPT_ROUNDS) : null)
51
+ fields.set('status', status)
52
+ fields.set('dateCreated', Math.floor(Date.now() / 1000))
53
+
54
+ const query = sql`
55
+ INSERT INTO rooms ${sql.tuple(Array.from(fields.keys()).map(sql.column))}
56
+ VALUES ${sql.tuple(Array.from(fields.values()))}
57
+ `
58
+ const res = await db.run(String(query), query.parameters)
59
+
60
+ if (res.changes) {
61
+ log.verbose('%s created room "%s" (roomId: %s)', ctx.user.name, name, res.lastID)
62
+ }
63
+
64
+ // send updated room list
65
+ ctx.body = await Rooms.get(true)
66
+ })
67
+
68
+ // update room
69
+ router.put('/rooms/:roomId', async (ctx, next) => {
70
+ if (!ctx.user.isAdmin) {
71
+ ctx.throw(401)
72
+ }
73
+
74
+ const { name, password, status } = ctx.request.body
75
+ const roomId = parseInt(ctx.params.roomId, 10)
76
+
77
+ if (!name || !name.trim() || name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
78
+ ctx.throw(400, `Room name must have ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`)
79
+ }
80
+
81
+ if (password && password.length < PASSWORD_MIN_LENGTH) {
82
+ ctx.throw(400, `Room password must have at least ${PASSWORD_MIN_LENGTH} characters`)
83
+ }
84
+
85
+ if (!status || !STATUSES.includes(status)) {
86
+ ctx.throw(400, 'Invalid room status')
87
+ }
88
+
89
+ const fields = new Map()
90
+ fields.set('name', name.trim())
91
+ fields.set('status', status)
92
+ fields.set('roomId', roomId)
93
+
94
+ // falsey value will unset password
95
+ if (typeof password !== 'undefined') {
96
+ fields.set('password', password ? await bcrypt.hash(password, BCRYPT_ROUNDS) : null)
97
+ }
98
+
99
+ const query = sql`
100
+ UPDATE rooms
101
+ SET ${sql.tuple(Array.from(fields.keys()).map(sql.column))} = ${sql.tuple(Array.from(fields.values()))}
102
+ WHERE roomId = ${roomId}
103
+ `
104
+ const res = await db.run(String(query), query.parameters)
105
+
106
+ if (res.changes) {
107
+ log.verbose('%s updated roomId %s', ctx.user.name, ctx.params.roomId)
108
+ }
109
+
110
+ // send updated room list
111
+ ctx.body = await Rooms.get(true)
112
+ })
113
+
114
+ // remove room
115
+ router.delete('/rooms/:roomId', async (ctx, next) => {
116
+ if (!ctx.user.isAdmin) {
117
+ ctx.throw(401)
118
+ }
119
+
120
+ const roomId = parseInt(ctx.params.roomId, 10)
121
+
122
+ if (typeof roomId !== 'number') {
123
+ ctx.throw(422, 'Invalid roomId')
124
+ }
125
+
126
+ // remove room's queue first
127
+ const queueQuery = sql`
128
+ DELETE FROM queue
129
+ WHERE roomId = ${roomId}
130
+ `
131
+ await db.run(String(queueQuery), queueQuery.parameters)
132
+
133
+ // remove room
134
+ const roomQuery = sql`
135
+ DELETE FROM rooms
136
+ WHERE roomId = ${roomId}
137
+ `
138
+ await db.run(String(roomQuery), roomQuery.parameters)
139
+
140
+ log.verbose('%s deleted roomId %s', ctx.user.name, roomId)
141
+
142
+ // send updated room list
143
+ ctx.body = await Rooms.get(true)
144
+ })
145
+
146
+ module.exports = router