-
Notifications
You must be signed in to change notification settings - Fork 3
/
server.coffee
executable file
·382 lines (291 loc) · 14.4 KB
/
server.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env node
###
# Remote MakeMKV Controller
#
# Provides the server aspect of Remote MakeMKV
#
# @author David Lasley, [email protected]
# @website https://dlasley.net/blog/projects/remote-makemkv/
# @package remote-makemkv
# @license GPLv3
###
udev = require('udev')
http = require('http')
io = require('socket.io')
url = require('url')
fs = require('fs')
path = require('path')
CoffeeScript = require('coffee-script')
MakeMKV = require('./makemkv.coffee')
class MakeMKVServer extends MakeMKV
CLIENT_HTML: path.join(__dirname, 'static', 'client.html')
CLIENT_CSS: path.join(__dirname, 'static', 'client.css')
CLIENT_ICON: path.join(__dirname, 'static', 'favicon.png')
CLIENT_COFFEE: path.join(__dirname, 'client.coffee')
SUCCESS_STRING: 'success'
CMD_ORDER: ['change_out_dir', 'scan_drives', 'disc_info', 'rip_track']
constructor: (port) ->
super(false) #< MakeMKV obj init
@cache = {}
@change_out_dir() #< Prime the out dir cache
## Serve the UI
server = http.createServer((req, res) =>
req.setEncoding 'utf8'
parsed_url = url.parse(req.url, true)
path_ = parsed_url.pathname
console.log(req.method + ' to ' + path_)
switch req.method
when 'POST'
req.on('data', (chunk) =>
data = JSON.parse(chunk.toString())
@do_broadcast(socket, {'app':path_[1..], 'data':data})
res.end(@SUCCESS_STRING)
)
when 'GET'
switch path_
# Statics
when '/' #< Serve static client html
res.writeHead(200, {'Content-Type': 'text/html'})
fs.readFile(@CLIENT_HTML, {encoding:'utf-8'}, (err, data)->res.end(data))
when '/css' #< Serve static client css
res.writeHead(200, {'Content-Type': 'text/css'})
fs.readFile(@CLIENT_CSS, {encoding:'utf-8'}, (err, data)->res.end(data))
when '/favicon.ico' #< Favicon
res.writeHead(200, {'Content-Type': 'image/x-icon'} )
fs.readFile(@CLIENT_ICON, {encoding:'utf-8'}, (err, data)->res.end(data))
# Dynamics
when '/client' #< Serve the client coffeescript
res.writeHead(200, {'Content-Type': 'application/javascript'})
fs.readFile(@CLIENT_COFFEE, {encoding:'utf-8'}, (err, data) ->
res.end(CoffeeScript.compile(data)) #< @todo globalize the load, this is good for testing
)
when '/list_dir' #< List dir
res.writeHead(200, {'Content-Type': 'application/javascript'})
@list_dir(parsed_url.query['id'], (dir)=>res.end(JSON.stringify(dir)))
else
if /\/refresh\//.test(path_)
device = path_.replace('/refresh', '')
@disc_info(device, (data) =>
data['disc_id'] = device
console.log('??? --- ' + data['disc_id'])
@_do_emit(@socket, data)
)
res.writeHead(200, {'Content-Type': 'application/javascript'})
res.end()
).listen(@LISTEN_PORT)
@socket = io(server)
console.log('Listening on ' + @LISTEN_PORT)
monitor = udev.monitor()
monitor.on('change', @_udev_change)
console.log('Set udev hook to monitor device changes')
# Bind socket actions on client connect
@socket.on('connection', (client) =>
single_broadcast = (data) =>
@_do_emit(@socket, data)
# Send cache to client
_display_cache = () =>
@display_cache((msgs)=>
for msg in msgs
client.emit(msg.cmd, msg.data)
)
_display_cache() #< Actually send it
client.on('display_cache', (data) =>
_display_cache()
)
# User has sent command to change save_dir
client.on('change_out_dir', (data) =>
console.log('changing out dir')
@save_out_dir(data, single_broadcast)
)
# User has sent command to scan drives
client.on('scan_drives', (data) =>
console.log('scanning drives')
@_do_emit(@socket, {'cmd':'_panel_disable', 'data':{'disc_id':'all', "busy":true}})
@scan_drives(single_broadcast)
)
# User has sent command to retrieve single disc info
client.on('disc_info', (data) =>
console.log('getting disc info for', data)
@_do_emit(@socket, {'cmd':'_panel_disable', 'data':{'disc_id':data, "busy":true}})
@disc_info(data, single_broadcast)
)
# User has sent command to retrieve single disc info
client.on('rip_track', (data) =>
console.log('getting disc info for', data)
@_do_emit(@socket, {'cmd':'_panel_disable', 'data':{'disc_id':data.drive_id, "busy":true}})
@rip_track(data.save_dir, data.drive_id, data.track_ids, single_broadcast)
)
# User is browsing a directory, only send to them
client.on('list_dir', (data) =>
console.log('listing dir ' + data)
@list_dir(data, (dir) => client.emit('list_dir', dir))
)
client.on('scan_dirs', (data) =>
console.log('scanning dirs ' + data)
@scan_dirs(data, single_broadcast)
)
## Socket debugging
client.on('message', (data) ->
console.log('Client sent:', data)
)
client.on('disconnect', () ->
console.log('Client d/c')
)
)
## Send cached data to client in logic order
# scan_drives, disc_info, rip_track
display_cache: (callback=false) =>
cached = []
for cmd in @CMD_ORDER
if typeof(@cache[cmd]) == 'object'
for namespace of @cache[cmd]
if @cache[cmd][namespace]
cached.push({'cmd':cmd, 'data':@cache[cmd][namespace]})
# Disable busy drive panels
for disc_id, busy of @busy_devices
cached.push({'cmd':'_panel_disable', 'data':{'disc_id':disc_id, 'busy':busy}})
if callback
callback(cached)
else
cached
## Signal emit
# @param socket socket socket
# @param dict msg Msg, {'cmd':(str)signal_to_emit,'data':(dict)}
_do_emit: (socket, msg) ->
cmd = msg['cmd']
data = msg['data']
if data['data'] #< If there's a second data dimension (cached)
data = data['data'] #< Pull and save it instead
namespace = if data['disc_id'] then data['disc_id'] else 'none'
data = @cache_data(cmd, data, namespace)
socket.sockets.emit(cmd, data)
## Cache data to variable for when clients join
# @param str cmd Command that will be emitted
# @param mixed data Data obj
# @param str namespace Namespace to cache data in (multiple single drive cmds)
# @return dict data with cache_refreshed date {'data':mixed, 'cache_refreshed':Date}
cache_data: (cmd, data, namespace='none') =>
if typeof(@cache[cmd]) != 'object'
@cache[cmd] = {}
console.log('Setting cache for cmd ' + cmd + ' in namespace ' + namespace)
@cache[cmd][namespace] = {'cache_refreshed': new Date(), 'data': data }
## Delete now stale entries
#cmd_index = @CMD_ORDER.indexOf(cmd) + 1
#if @CMD_ORDER[cmd_index]
# for i in @CMD_ORDER[cmd_index...]
# if @cache[i]
# console.log('Clearing ' + i + ' ' + namespace + ' was ' + @cache[i][namespace])
# @cache[i][namespace] = undefined
@cache[cmd][namespace]
## Register change to save directory (UI)
change_out_dir: () =>
@cache_data('change_out_dir', @save_to)
## Save change to save directory
# @param str dir New save dir
save_out_dir: (dir, callback=false) =>
@save_to = dir
@change_out_dir()
if callback
callback(@save_to)
else
@save_to
## Scan directories with MakeMKV
# @param list dirs Directories to scan
# @param func callback callback function, receives disc_info every time avail
scan_dirs: (dirs, callback) =>
# @todo - add logic around this instead of just scanning everything..
for dir in dirs
@scan_dir(path.join(@USER_SETTINGS.source_dir, dir), callback)
## Dir relay
# @param str dir Dir to list
# @param func callback callback function
# @return dict Dict of items in folder, matching jstree specifications
list_dir: (dir, callback) =>
source_dir = @USER_SETTINGS.source_dir
if dir in [undefined, '#']
jailed_dir = source_dir
dir = '/'
else
jailed_dir = path.join(source_dir, dir)
console.log(dir + ' ' + jailed_dir)
fs.readdir(jailed_dir, (err, dir_arr) =>
folder_data = []
valid_exts = ['img', 'iso']
valid_exts.push.apply(@sanitizer.VID_EXTS)
if dir_arr
dir_arr.forEach((file) =>
relative_p = path.join(dir, file)
extension = file.split('.').pop()
stat = fs.statSync(path.join(jailed_dir, file))
if stat and stat.isDirectory()
folder_data.push({
text: file, children: true, icon: 'folder', id: file
})
else if extension in valid_exts
folder_data.push({
text: file, children: false, icon: 'file', id: file
})
)
console.log(folder_data)
callback(folder_data)
)
## Scan drives, return info. Also sets @drive_map
# @param func callback Callback function, will receive drives as param
# @return dict drives Dict keyed by drive index, value is movie name
scan_drives: (callback=false) =>
if @toggle_busy(false, true) #< Make sure none of the discs are busy
drives = {'cmd':'scan_drives', 'data':{}}
@drive_map = {}
# Spawn MakeMKV with callback
@_spawn_generic(['-r', 'info'], (code, drive_scan)=>
try
for line in drive_scan
# DRV to make sure it's drive output, /dev to make sure that there is a drive
if line[0..3] == 'DRV:' and line.indexOf('/dev/') != -1
info = line.split(@COL_PATTERN)
# Assign drive_location, strip quotes
drive_location = info[info.length - 2][1..-2]
# [Drive Index] = Movie Name
if info[info.length - 4] != '""'
# Assign drive info, strip quotes
drives['data'][drive_location] = info[info.length - 4][1..-2]
else
drives['data'][drive_location] = false
@drive_map[drive_location] = info[1].split(':')[1] #< Index the drive location to makemkv's drive ID
catch err
console.log('disc_scan: ' + err)
@toggle_busy(false)
if callback
callback(drives)
else
drives
)
## Receiver for udev change event. Fires media info to clients if it is a media disc.
# Also begins disc_info(device.DEVNAME)
_udev_change: (device) =>
if '1' in [device.ID_CDROM_DVD, device.ID_CDROM_BD, device.ID_CDROM_MEDIA]
if device.ID_FS_LABEL
console.log('Disc inserted' + device.DEVNAME)
@_do_emit(@socket, {'cmd':'udev_update', 'data':{
'disc_id': device.DEVNAME,
'label': device.ID_FS_LABEL or device.ID_FS_LABEL_ENC
}})
#@_do_emit(@socket, {'cmd': '_panel_disable', 'data': {
# 'disc_id':device.DEVNAME, "busy":true
#}})
else
console.log("Disc ejected " + device.DEVNAME)
@disc_info(device.DEVNAME, (data) =>
data['disc_id'] = device.DEVNAME
console.log('??? --- ' + data['disc_id'])
@_do_emit(@socket, data)
)
## Error handler
# @param str type Type of error
# @param str msg Error msg
_error: (type, msg) =>
@_do_emit(@socket, {'cmd': '_error', 'data':{
'type': type, 'msg': msg
}})
server = new MakeMKVServer()
server.emitter.on('error', server._error)