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,99 @@
|
|
|
1
|
+
const { promisify } = require('util')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const stat = promisify(fs.stat)
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const log = require('../lib/Log')('Media')
|
|
6
|
+
const getCdgName = require('../lib/getCdgName')
|
|
7
|
+
const KoaRouter = require('koa-router')
|
|
8
|
+
const router = KoaRouter({ prefix: '/api/media' })
|
|
9
|
+
const Library = require('../Library')
|
|
10
|
+
const Media = require('./Media')
|
|
11
|
+
const Prefs = require('../Prefs')
|
|
12
|
+
const Queue = require('../Queue')
|
|
13
|
+
const Rooms = require('../Rooms')
|
|
14
|
+
const {
|
|
15
|
+
LIBRARY_PUSH_SONG,
|
|
16
|
+
QUEUE_PUSH
|
|
17
|
+
} = require('../../shared/actionTypes')
|
|
18
|
+
|
|
19
|
+
// stream a media file
|
|
20
|
+
router.get('/:mediaId', async (ctx, next) => {
|
|
21
|
+
const { type } = ctx.query
|
|
22
|
+
|
|
23
|
+
if (!ctx.user.isAdmin) {
|
|
24
|
+
ctx.throw(401)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const mediaId = parseInt(ctx.params.mediaId, 10)
|
|
28
|
+
|
|
29
|
+
if (Number.isNaN(mediaId) || !type) {
|
|
30
|
+
ctx.throw(422, 'invalid mediaId or type')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// get media info
|
|
34
|
+
const res = await Media.search({ mediaId })
|
|
35
|
+
|
|
36
|
+
if (!res.result.length) {
|
|
37
|
+
ctx.throw(404, 'mediaId not found')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { pathId, relPath } = res.entities[mediaId]
|
|
41
|
+
|
|
42
|
+
// get base path
|
|
43
|
+
const { paths } = await Prefs.get()
|
|
44
|
+
const basePath = paths.entities[pathId].path
|
|
45
|
+
|
|
46
|
+
let file = path.join(basePath, relPath)
|
|
47
|
+
|
|
48
|
+
if (type === 'cdg') {
|
|
49
|
+
file = await getCdgName(file)
|
|
50
|
+
|
|
51
|
+
if (!file) {
|
|
52
|
+
ctx.throw(404, 'The .cdg file could not be found')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// get file info
|
|
57
|
+
const stats = await stat(file)
|
|
58
|
+
ctx.length = stats.size
|
|
59
|
+
ctx.type = Media.mimeTypes[path.extname(file).replace('.', '').toLowerCase()]
|
|
60
|
+
|
|
61
|
+
if (typeof ctx.type === 'undefined') {
|
|
62
|
+
ctx.throw(404, `Unknown mime type for extension: ${path.extname(file)}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log.verbose('streaming %s (%sMB): %s', ctx.type, (ctx.length / 1000000).toFixed(2), file)
|
|
66
|
+
ctx.body = fs.createReadStream(file)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// set isPreferred flag
|
|
70
|
+
router.all('/:mediaId/prefer', async (ctx, next) => {
|
|
71
|
+
if (!ctx.user.isAdmin) {
|
|
72
|
+
ctx.throw(401)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const mediaId = parseInt(ctx.params.mediaId, 10)
|
|
76
|
+
|
|
77
|
+
if (Number.isNaN(mediaId) || (ctx.request.method !== 'PUT' && ctx.request.method !== 'DELETE')) {
|
|
78
|
+
ctx.throw(422)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const songId = await Media.setPreferred(mediaId, ctx.request.method === 'PUT')
|
|
82
|
+
ctx.status = 200
|
|
83
|
+
|
|
84
|
+
// emit (potentially) updated queues to each room
|
|
85
|
+
for (const { room, roomId } of Rooms.getActive(ctx.io)) {
|
|
86
|
+
ctx.io.to(room).emit('action', {
|
|
87
|
+
type: QUEUE_PUSH,
|
|
88
|
+
payload: await Queue.get(roomId),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// emit (potentially) new duration
|
|
93
|
+
ctx.io.emit('action', {
|
|
94
|
+
type: LIBRARY_PUSH_SONG,
|
|
95
|
+
payload: await Library.getSong(songId),
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
module.exports = router
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const Rooms = require('../Rooms')
|
|
2
|
+
const {
|
|
3
|
+
PLAYER_CMD_OPTIONS,
|
|
4
|
+
PLAYER_CMD_NEXT,
|
|
5
|
+
PLAYER_CMD_PAUSE,
|
|
6
|
+
PLAYER_CMD_PLAY,
|
|
7
|
+
PLAYER_CMD_VOLUME,
|
|
8
|
+
PLAYER_EMIT_STATUS,
|
|
9
|
+
PLAYER_EMIT_LEAVE,
|
|
10
|
+
PLAYER_REQ_OPTIONS,
|
|
11
|
+
PLAYER_REQ_NEXT,
|
|
12
|
+
PLAYER_REQ_PLAY,
|
|
13
|
+
PLAYER_REQ_PAUSE,
|
|
14
|
+
PLAYER_REQ_VOLUME,
|
|
15
|
+
PLAYER_STATUS,
|
|
16
|
+
PLAYER_LEAVE,
|
|
17
|
+
} = require('../../shared/actionTypes')
|
|
18
|
+
|
|
19
|
+
// ------------------------------------
|
|
20
|
+
// Action Handlers
|
|
21
|
+
// ------------------------------------
|
|
22
|
+
const ACTION_HANDLERS = {
|
|
23
|
+
[PLAYER_REQ_OPTIONS]: async (sock, { payload }) => {
|
|
24
|
+
// @todo: emit to players only
|
|
25
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
26
|
+
type: PLAYER_CMD_OPTIONS,
|
|
27
|
+
payload,
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
[PLAYER_REQ_NEXT]: async (sock, { payload }) => {
|
|
31
|
+
// @todo: emit to players only
|
|
32
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
33
|
+
type: PLAYER_CMD_NEXT,
|
|
34
|
+
})
|
|
35
|
+
},
|
|
36
|
+
[PLAYER_REQ_PAUSE]: async (sock, { payload }) => {
|
|
37
|
+
// @todo: emit to players only
|
|
38
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
39
|
+
type: PLAYER_CMD_PAUSE,
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
[PLAYER_REQ_PLAY]: async (sock, { payload }) => {
|
|
43
|
+
// @todo: emit to players only
|
|
44
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
45
|
+
type: PLAYER_CMD_PLAY,
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
[PLAYER_REQ_VOLUME]: async (sock, { payload }) => {
|
|
49
|
+
// @todo: emit to players only
|
|
50
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
51
|
+
type: PLAYER_CMD_VOLUME,
|
|
52
|
+
payload,
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
[PLAYER_EMIT_STATUS]: async (sock, { payload }) => {
|
|
56
|
+
// so we can tell the room when players leave and
|
|
57
|
+
// relay last known player status on client join
|
|
58
|
+
sock._lastPlayerStatus = payload
|
|
59
|
+
|
|
60
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
61
|
+
type: PLAYER_STATUS,
|
|
62
|
+
payload,
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
[PLAYER_EMIT_LEAVE]: async (sock, { payload }) => {
|
|
66
|
+
sock._lastPlayerStatus = null
|
|
67
|
+
|
|
68
|
+
// any players left in room?
|
|
69
|
+
if (!Rooms.isPlayerPresent(sock.server, sock.user.roomId)) {
|
|
70
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
71
|
+
type: PLAYER_LEAVE,
|
|
72
|
+
payload: { socketId: sock.id },
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = ACTION_HANDLERS
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const db = require('../lib/Database').db
|
|
3
|
+
const sql = require('sqlate')
|
|
4
|
+
const crypto = require('crypto')
|
|
5
|
+
const log = require('../lib/Log')('Prefs')
|
|
6
|
+
|
|
7
|
+
class Prefs {
|
|
8
|
+
/**
|
|
9
|
+
* Gets prefs (includes media paths, does not inlcude JWT secret key)
|
|
10
|
+
* @return {Promise} Prefs object
|
|
11
|
+
*/
|
|
12
|
+
static async get () {
|
|
13
|
+
const prefs = {
|
|
14
|
+
paths: { result: [], entities: {} }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
const query = sql`
|
|
19
|
+
SELECT * FROM prefs
|
|
20
|
+
WHERE key != "jwtKey"
|
|
21
|
+
`
|
|
22
|
+
const rows = await db.all(String(query), query.parameters)
|
|
23
|
+
|
|
24
|
+
// json-decode key/val pairs
|
|
25
|
+
rows.forEach(row => {
|
|
26
|
+
prefs[row.key] = JSON.parse(row.data)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// include media paths
|
|
31
|
+
{
|
|
32
|
+
const query = sql`
|
|
33
|
+
SELECT * FROM paths
|
|
34
|
+
ORDER BY priority
|
|
35
|
+
`
|
|
36
|
+
const rows = await db.all(String(query), query.parameters)
|
|
37
|
+
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
prefs.paths.entities[row.pathId] = row
|
|
40
|
+
prefs.paths.result.push(row.pathId)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return prefs
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set a preference key
|
|
49
|
+
* @param {string} key Prefs key name
|
|
50
|
+
* @param {any} data to be JSON encoded
|
|
51
|
+
* @return {Promise} Success/fail boolean
|
|
52
|
+
*/
|
|
53
|
+
static async set (key, data) {
|
|
54
|
+
const query = sql`
|
|
55
|
+
REPLACE INTO prefs (key, data)
|
|
56
|
+
VALUES (${key}, ${JSON.stringify(data)})
|
|
57
|
+
`
|
|
58
|
+
const res = await db.run(String(query), query.parameters)
|
|
59
|
+
return res.changes === 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add media path
|
|
64
|
+
* @param {string} dir Absolute path
|
|
65
|
+
* @return {Promise} pathId (Number) of newly-added path
|
|
66
|
+
*/
|
|
67
|
+
static async addPath (dir) {
|
|
68
|
+
const prefs = await Prefs.get()
|
|
69
|
+
const { result, entities } = prefs.paths
|
|
70
|
+
|
|
71
|
+
// is it a subfolder of an already-added folder?
|
|
72
|
+
if (result.some(pathId => (dir + path.sep).indexOf(entities[pathId].path + path.sep) === 0)) {
|
|
73
|
+
throw new Error('Folder has already been added')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fields = new Map()
|
|
77
|
+
fields.set('path', dir)
|
|
78
|
+
// priority defaults to one higher than current highest
|
|
79
|
+
fields.set('priority', result.length ? entities[result[result.length - 1]].priority + 1 : 0)
|
|
80
|
+
|
|
81
|
+
const query = sql`
|
|
82
|
+
INSERT INTO paths ${sql.tuple(Array.from(fields.keys()).map(sql.column))}
|
|
83
|
+
VALUES ${sql.tuple(Array.from(fields.values()))}
|
|
84
|
+
`
|
|
85
|
+
const res = await db.run(String(query), query.parameters)
|
|
86
|
+
|
|
87
|
+
if (!Number.isInteger(res.lastID)) {
|
|
88
|
+
throw new Error('invalid lastID from path insert')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return res.lastID
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove media path
|
|
96
|
+
* @param {Number} pathId
|
|
97
|
+
* @return {Promise}
|
|
98
|
+
*/
|
|
99
|
+
static async removePath (pathId) {
|
|
100
|
+
const query = sql`
|
|
101
|
+
DELETE FROM paths
|
|
102
|
+
WHERE pathId = ${pathId}
|
|
103
|
+
`
|
|
104
|
+
await db.run(String(query), query.parameters)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set media path priorities
|
|
109
|
+
* @param {Array} pathIds
|
|
110
|
+
* @return {Promise}
|
|
111
|
+
*/
|
|
112
|
+
static async setPathPriority (pathIds) {
|
|
113
|
+
if (!Array.isArray(pathIds)) {
|
|
114
|
+
throw new Error('pathIds must be an array')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const query = sql`
|
|
118
|
+
UPDATE paths
|
|
119
|
+
SET priority = CASE pathId
|
|
120
|
+
${sql.concat(pathIds.map((pathId, i) => sql`WHEN ${pathId} THEN ${i} `))}
|
|
121
|
+
END
|
|
122
|
+
WHERE pathId IN ${sql.tuple(pathIds)}
|
|
123
|
+
`
|
|
124
|
+
await db.run(String(query), query.parameters)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get JWT secret key from db
|
|
129
|
+
* @return {Promise} jwtKey (string)
|
|
130
|
+
*/
|
|
131
|
+
static async getJwtKey (forceRotate = false) {
|
|
132
|
+
if (forceRotate) return this.rotateJwtKey()
|
|
133
|
+
|
|
134
|
+
const query = sql`
|
|
135
|
+
SELECT * FROM prefs
|
|
136
|
+
WHERE key = "jwtKey"
|
|
137
|
+
`
|
|
138
|
+
const row = await db.get(String(query), query.parameters)
|
|
139
|
+
|
|
140
|
+
if (row && row.data) {
|
|
141
|
+
const jwtKey = JSON.parse(row.data)
|
|
142
|
+
if (jwtKey.length === 64) return jwtKey
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.rotateJwtKey()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create or rotate JWT secret key
|
|
150
|
+
* @return {Promise} jwtKey (string)
|
|
151
|
+
*/
|
|
152
|
+
static async rotateJwtKey () {
|
|
153
|
+
const jwtKey = crypto.randomBytes(48).toString('base64') // 64 char
|
|
154
|
+
log.info('Rotating JWT secret key (length=%s)', jwtKey.length)
|
|
155
|
+
|
|
156
|
+
const query = sql`
|
|
157
|
+
REPLACE INTO prefs (key, data)
|
|
158
|
+
VALUES ("jwtKey", ${JSON.stringify(jwtKey)})
|
|
159
|
+
`
|
|
160
|
+
const res = await db.run(String(query), query.parameters)
|
|
161
|
+
if (res.changes) return jwtKey
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = Prefs
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const log = require('../lib/Log')('Prefs')
|
|
3
|
+
const KoaRouter = require('koa-router')
|
|
4
|
+
const router = KoaRouter({ prefix: '/api/prefs' })
|
|
5
|
+
const getFolders = require('../lib/getFolders')
|
|
6
|
+
const getWindowsDrives = require('../lib/getWindowsDrives')
|
|
7
|
+
const Prefs = require('./Prefs')
|
|
8
|
+
|
|
9
|
+
// start media scan
|
|
10
|
+
router.get('/scan', async (ctx, next) => {
|
|
11
|
+
if (!ctx.user.isAdmin) {
|
|
12
|
+
ctx.throw(401)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ctx.status = 200
|
|
16
|
+
ctx.startScanner()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// stop media scan
|
|
20
|
+
router.get('/scan/stop', async (ctx, next) => {
|
|
21
|
+
if (!ctx.user.isAdmin) {
|
|
22
|
+
ctx.throw(401)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ctx.status = 200
|
|
26
|
+
ctx.stopScanner()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// get preferences and media paths
|
|
30
|
+
router.get('/', async (ctx, next) => {
|
|
31
|
+
const prefs = await Prefs.get()
|
|
32
|
+
|
|
33
|
+
// must be admin or firstrun
|
|
34
|
+
if (prefs.isFirstRun || ctx.user.isAdmin) {
|
|
35
|
+
ctx.body = prefs
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// there are no non-admin prefs but we don't want to
|
|
40
|
+
// trigger a fetch error on the frontend
|
|
41
|
+
ctx.body = {}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// add media file path
|
|
45
|
+
router.post('/path', async (ctx, next) => {
|
|
46
|
+
const dir = decodeURIComponent(ctx.query.dir)
|
|
47
|
+
|
|
48
|
+
if (!ctx.user.isAdmin) {
|
|
49
|
+
ctx.throw(401)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// required
|
|
53
|
+
if (!dir) {
|
|
54
|
+
ctx.throw(422, 'Invalid path')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await Prefs.addPath(dir)
|
|
58
|
+
|
|
59
|
+
// respond with updated prefs
|
|
60
|
+
ctx.body = await Prefs.get()
|
|
61
|
+
|
|
62
|
+
// update library
|
|
63
|
+
ctx.startScanner()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// remove media file path
|
|
67
|
+
router.delete('/path/:pathId', async (ctx, next) => {
|
|
68
|
+
if (!ctx.user.isAdmin) {
|
|
69
|
+
ctx.throw(401)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pathId = parseInt(ctx.params.pathId, 10)
|
|
73
|
+
|
|
74
|
+
if (isNaN(pathId)) {
|
|
75
|
+
ctx.throw(422, 'Invalid pathId')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await Prefs.removePath(pathId)
|
|
79
|
+
|
|
80
|
+
// respond with updated prefs
|
|
81
|
+
ctx.body = await Prefs.get()
|
|
82
|
+
|
|
83
|
+
// update library
|
|
84
|
+
ctx.startScanner()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// get folder listing for path browser
|
|
88
|
+
router.get('/path/ls', async (ctx, next) => {
|
|
89
|
+
if (!ctx.user.isAdmin) {
|
|
90
|
+
ctx.throw(401)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const dir = decodeURIComponent(ctx.query.dir)
|
|
94
|
+
|
|
95
|
+
// windows is a special snowflake and gets an
|
|
96
|
+
// extra top level of available drive letters
|
|
97
|
+
if (dir === '' && process.platform === 'win32') {
|
|
98
|
+
const drives = await getWindowsDrives()
|
|
99
|
+
|
|
100
|
+
ctx.body = {
|
|
101
|
+
current: '',
|
|
102
|
+
parent: false,
|
|
103
|
+
children: drives,
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
const current = path.resolve(dir)
|
|
107
|
+
const parent = path.resolve(dir, '../')
|
|
108
|
+
|
|
109
|
+
const list = await getFolders(dir)
|
|
110
|
+
log.verbose('%s listed path: %s', ctx.user.name, current)
|
|
111
|
+
|
|
112
|
+
ctx.body = {
|
|
113
|
+
current,
|
|
114
|
+
// if at root, windows gets a special top level
|
|
115
|
+
parent: parent === current ? (process.platform === 'win32' ? '' : false) : parent,
|
|
116
|
+
children: list.map(p => ({
|
|
117
|
+
path: p,
|
|
118
|
+
label: p.replace(current + path.sep, '')
|
|
119
|
+
})).filter(c => !(c.label.startsWith('.') || c.label.startsWith('/.')))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
module.exports = router
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const log = require('../lib/Log')(`server[${process.pid}]`)
|
|
2
|
+
const Library = require('../Library')
|
|
3
|
+
const Prefs = require('./Prefs')
|
|
4
|
+
const {
|
|
5
|
+
LIBRARY_PUSH,
|
|
6
|
+
PREFS_SET_PATH_PRIORITY,
|
|
7
|
+
PREFS_PUSH,
|
|
8
|
+
PREFS_SET,
|
|
9
|
+
_ERROR,
|
|
10
|
+
} = require('../../shared/actionTypes')
|
|
11
|
+
|
|
12
|
+
const ACTION_HANDLERS = {
|
|
13
|
+
[PREFS_SET]: async (sock, { payload }, acknowledge) => {
|
|
14
|
+
if (!sock.user.isAdmin) {
|
|
15
|
+
acknowledge({
|
|
16
|
+
type: PREFS_SET + _ERROR,
|
|
17
|
+
error: 'Unauthorized',
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await Prefs.set(payload.key, payload.data)
|
|
22
|
+
log.info('%s (%s) set pref %s = %s', sock.user.name, sock.id, payload.key, payload.data)
|
|
23
|
+
|
|
24
|
+
await pushPrefs(sock)
|
|
25
|
+
},
|
|
26
|
+
[PREFS_SET_PATH_PRIORITY]: async (sock, { payload }, acknowledge) => {
|
|
27
|
+
if (!sock.user.isAdmin) {
|
|
28
|
+
acknowledge({
|
|
29
|
+
type: PREFS_SET_PATH_PRIORITY + _ERROR,
|
|
30
|
+
error: 'Unauthorized',
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await Prefs.setPathPriority(payload)
|
|
35
|
+
log.info('%s re-prioritized media folders; pushing library to all', sock.user.name)
|
|
36
|
+
|
|
37
|
+
await pushPrefs(sock)
|
|
38
|
+
|
|
39
|
+
// invalidate cache
|
|
40
|
+
Library.cache.version = null
|
|
41
|
+
|
|
42
|
+
sock.server.emit('action', {
|
|
43
|
+
type: LIBRARY_PUSH,
|
|
44
|
+
payload: await Library.get(),
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// helper to push prefs to admins
|
|
50
|
+
const pushPrefs = async (sock) => {
|
|
51
|
+
const admins = []
|
|
52
|
+
|
|
53
|
+
for (const s of sock.server.sockets.sockets.values()) {
|
|
54
|
+
if (s.user && s.user.isAdmin) {
|
|
55
|
+
admins.push(s.id)
|
|
56
|
+
sock.server.to(s.id)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (admins.length) {
|
|
61
|
+
sock.server.emit('action', {
|
|
62
|
+
type: PREFS_PUSH,
|
|
63
|
+
payload: await Prefs.get(),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = ACTION_HANDLERS
|