| 1 | // Written by Mike Frysinger <vapier@gmail.com>. Released into the public domain. Suck it. |
| 2 | |
| 3 | function Mpc(socket, cb_update_state, debug_enabled) { |
| 4 | this._socket = socket; |
| 5 | this._cb_update_state = cb_update_state; |
| 6 | this._debug_enabled = debug_enabled; |
| 7 | this._queue = ['init']; |
| 8 | this._recv_buffer = []; |
| 9 | this._recv_buffer_last = 0; |
| 10 | this.state = {}; |
| 11 | } |
| 12 | |
| 13 | Mpc.prototype.log = function(lvl, msg, obj) { |
| 14 | if (this._debug_enabled & lvl) |
| 15 | console.log('mpc: ' + msg, obj); |
| 16 | } |
| 17 | |
| 18 | Mpc.prototype.err = function(msg, obj) { |
| 19 | console.error('mpc: ' + msg, obj); |
| 20 | } |
| 21 | |
| 22 | Mpc.prototype.set_debug = function(val) { |
| 23 | this._debug_enabled = val; |
| 24 | } |
| 25 | |
| 26 | Mpc.prototype.send = function(msg) { |
| 27 | var _this = this; |
| 28 | this._queue.push(msg); |
| 29 | this._socket.send(msg, function(x) { |
| 30 | _this.log(0x1, 'send: ' + msg + ':', x); |
| 31 | if (x.bytesWritten < 0) { |
| 32 | _this.log(0x1, 'reconnecting...'); |
| 33 | _this._socket.reconnect(); |
| 34 | _this.queue = [msg]; |
| 35 | _this._socket.send(msg); |
| 36 | } |
| 37 | }); |
| 38 | } |
| 39 | |
| 40 | Mpc.prototype._parse_result = function(lines) { |
| 41 | var state = {}; |
| 42 | var keys = []; |
| 43 | var key, val, i; |
| 44 | |
| 45 | lines.forEach(function(line) { |
| 46 | i = line.indexOf(':'); |
| 47 | if (i == -1) |
| 48 | return; // Ignores the OK line |
| 49 | key = line.substr(0, i); |
| 50 | keys.push(key); |
| 51 | val = line.substr(i + 2); |
| 52 | state[key] = val; |
| 53 | }); |
| 54 | |
| 55 | return { |
| 56 | 'state': state, |
| 57 | 'keys': keys, |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | Mpc.prototype.recv_msg = function(lines) { |
| 62 | var state, keys, ret; |
| 63 | var curr = this._queue.shift(); |
| 64 | this.log(0x2, 'recv: [' + curr + ']:', lines.join('\n')); |
| 65 | if (lines[0].substr(0, 4) == 'ACK ') |
| 66 | this.err(curr, lines.join('\n')); |
| 67 | curr = curr.split(' '); |
| 68 | |
| 69 | switch (curr[0]) { |
| 70 | case 'playlistinfo': |
| 71 | var i = 2, playlist = [], song; |
| 72 | while (i < lines.length) { |
| 73 | if (lines[i].substr(0, 5) == 'file:') { |
| 74 | song = this._parse_result(lines.splice(0, i + 1)).state; |
| 75 | playlist = playlist.concat(song); |
| 76 | i = 0; |
| 77 | } else |
| 78 | ++i; |
| 79 | } |
| 80 | this.state.Playlist = playlist; |
| 81 | this._cb_update_state(this.state); |
| 82 | break; |
| 83 | |
| 84 | case 'currentsong': |
| 85 | this.state.Currentsong = this._parse_result(lines).state; |
| 86 | this._cb_update_state(this.state); |
| 87 | break; |
| 88 | |
| 89 | case 'stats': |
| 90 | case 'status': |
| 91 | ret = this._parse_result(lines); |
| 92 | state = ret.state; |
| 93 | keys = ret.keys; |
| 94 | |
| 95 | // When mpd is stopped, it gives us back crap values for some things. |
| 96 | if ('state' in state && state.state == 'stop') { |
| 97 | if ('volume' in state && state.volume == '-1') |
| 98 | keys.splice(keys.indexOf('volume'), 1); |
| 99 | } |
| 100 | // Now merge the current state with the previous one so that we don't |
| 101 | // lose information like volume or song position. |
| 102 | var curr_state = this.state; |
| 103 | keys.forEach(function(key) { |
| 104 | curr_state[key] = state[key]; |
| 105 | }); |
| 106 | |
| 107 | this._cb_update_state(curr_state); |
| 108 | break; |
| 109 | |
| 110 | default: |
| 111 | this._cb_update_state(lines, curr); |
| 112 | break; |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | Mpc.prototype.recv = function(msg) { |
| 117 | /* We can get back a bunch of responses in a row, so parse them out */ |
| 118 | var lines = this._recv_buffer = this._recv_buffer.concat(msg.split('\n')); |
| 119 | var i = 0; |
| 120 | while (i < lines.length) { |
| 121 | if (lines[i] == 'OK' || lines[i].substr(0, 3) == 'OK ' || |
| 122 | lines[i].substr(0, 4) == 'ACK ') { |
| 123 | this.recv_msg(lines.splice(0, i + 1)); |
| 124 | i = 0; |
| 125 | } else |
| 126 | ++i; |
| 127 | } |
| 128 | |
| 129 | if (lines.length && this._recv_buffer_last != lines.length) { |
| 130 | // Keep sucking in data so long as more exists. |
| 131 | this._recv_buffer_last = lines.length; |
| 132 | this._socket.poll(); |
| 133 | } else |
| 134 | this._recv_buffer_last = lines.length; |
| 135 | } |
| 136 | |
| 137 | /* |
| 138 | * Command generator helpers. |
| 139 | */ |
| 140 | |
| 141 | Mpc.__make_send_void = function(cmd) { |
| 142 | return function() { this.send(cmd); } |
| 143 | } |
| 144 | |
| 145 | Mpc.__make_send_arg1 = function(cmd) { |
| 146 | return function(a1) { |
| 147 | if (a1 === undefined) |
| 148 | this.err(cmd + ': function requires one argument'); |
| 149 | else |
| 150 | this.send(cmd + ' ' + a1); |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | Mpc.__make_send_arg2 = function(cmd) { |
| 155 | return function(a1, a2) { |
| 156 | if (a1 === undefined || a2 === undefined) |
| 157 | this.err(cmd + ': function requires two arguments'); |
| 158 | else |
| 159 | this.send(cmd + ' ' + a1 + ' ' + a2); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | Mpc.__make_send_arg3 = function(cmd) { |
| 164 | return function(a1, a2, a3) { |
| 165 | if (a1 === undefined || a2 === undefined || a3 == undefined) |
| 166 | this.err(cmd + ': function requires three arguments'); |
| 167 | else |
| 168 | this.send(cmd + ' ' + a1 + ' ' + a2 + ' ' + a3); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | Mpc.__make_send_opt = function(cmd) { |
| 173 | return function(arg) { |
| 174 | if (arg === undefined) |
| 175 | arg = ''; |
| 176 | this.send(cmd + ' ' + arg); |
| 177 | }; |
| 178 | } |
| 179 | |
| 180 | Mpc.__make_send_range = function(cmd, min, max, def) { |
| 181 | return function(arg) { |
| 182 | if (arg === undefined) |
| 183 | arg = def; |
| 184 | if (arg >= min && arg <= max) |
| 185 | this.send(cmd + ' ' + arg); |
| 186 | else |
| 187 | this.err(cmd + ': arg must be [' + min + ',' + max + '] but got "' + arg + '"'); |
| 188 | }; |
| 189 | } |
| 190 | |
| 191 | /* |
| 192 | * Querying MPD's status |
| 193 | * http://www.musicpd.org/doc/protocol/ch03.html#idp118752 |
| 194 | */ |
| 195 | |
| 196 | // clearerror |
| 197 | Mpc.prototype.clearerror = Mpc.__make_send_void('clearerror'); |
| 198 | // currentsong |
| 199 | Mpc.prototype.currentsong = Mpc.__make_send_void('currentsong'); |
| 200 | // idle [SUBSYSTEMS...] |
| 201 | // TODO |
| 202 | // status |
| 203 | Mpc.prototype.status = Mpc.__make_send_void('status'); |
| 204 | // stats |
| 205 | Mpc.prototype.stats = Mpc.__make_send_void('stats'); |
| 206 | |
| 207 | /* |
| 208 | * Playback options |
| 209 | * http://www.musicpd.org/doc/protocol/ch03s02.html |
| 210 | */ |
| 211 | |
| 212 | // consume {STATE} |
| 213 | Mpc.prototype.consume = Mpc.__make_send_range('consume', 0, 1, 1); |
| 214 | // crossfade {SECONDS} |
| 215 | Mpc.prototype.crossfade = Mpc.__make_send_arg1('crossfade'); |
| 216 | // mixrampdb {deciBels} |
| 217 | Mpc.prototype.mixrampdb = Mpc.__make_send_arg1('mixrampdb'); |
| 218 | // mixrampdelay {SECONDS|nan} |
| 219 | // Note: Probably should handle javascript NaN here. |
| 220 | Mpc.prototype.mixrampdelay = Mpc.__make_send_arg1('mixrampdelay'); |
| 221 | // random {STATE} |
| 222 | Mpc.prototype.random = Mpc.__make_send_range('random', 0, 1, 1); |
| 223 | // repeat {STATE} |
| 224 | Mpc.prototype.repeat = Mpc.__make_send_range('repeat', 0, 1, 1); |
| 225 | // setvol {VOL} |
| 226 | Mpc.prototype.setvol = Mpc.__make_send_range('setvol', 0, 100); |
| 227 | // single {STATE} |
| 228 | Mpc.prototype.single = Mpc.__make_send_range('single', 0, 1, 1); |
| 229 | // replay_gain_mode {MODE} |
| 230 | Mpc.prototype.replay_gain_mode = Mpc.__make_send_arg1('replay_gain_mode'); |
| 231 | // replay_gain_status |
| 232 | |
| 233 | /* |
| 234 | * Controlling playback |
| 235 | * http://www.musicpd.org/doc/protocol/ch03s03.html |
| 236 | */ |
| 237 | |
| 238 | // next |
| 239 | Mpc.prototype.next = Mpc.__make_send_void('next'); |
| 240 | // pause {PAUSE} |
| 241 | Mpc.prototype.pause = Mpc.__make_send_range('pause', 0, 1, 1); |
| 242 | // play [SONGPOS] |
| 243 | Mpc.prototype.play = Mpc.__make_send_opt('play'); |
| 244 | // playid [SONGID] |
| 245 | Mpc.prototype.playid = Mpc.__make_send_opt('playid'); |
| 246 | // previous |
| 247 | Mpc.prototype.previous = Mpc.__make_send_void('previous'); |
| 248 | // seek {SONGPOS} {TIME} |
| 249 | Mpc.prototype.seek = Mpc.__make_send_arg2('seek'); |
| 250 | // seekid {SONGID} {TIME} |
| 251 | Mpc.prototype.seekid = Mpc.__make_send_arg2('seekid'); |
| 252 | // seekcur {TIME} |
| 253 | Mpc.prototype.seekcur = Mpc.__make_send_arg1('seekcur'); |
| 254 | // stop |
| 255 | Mpc.prototype.stop = Mpc.__make_send_void('stop'); |
| 256 | |
| 257 | /* |
| 258 | * The current playlist |
| 259 | * http://www.musicpd.org/doc/protocol/ch03s04.html |
| 260 | */ |
| 261 | |
| 262 | // add {URI} |
| 263 | Mpc.prototype.add = Mpc.__make_send_arg1('add'); |
| 264 | // addid {URI} [POSITION] |
| 265 | // TODO: handle position |
| 266 | Mpc.prototype.addid = Mpc.__make_send_arg1('addid'); |
| 267 | // clear |
| 268 | Mpc.prototype.clear = Mpc.__make_send_void('clear'); |
| 269 | // delete [{POS} | {START:END}] |
| 270 | Mpc.prototype.delete = Mpc.__make_send_arg1('delete'); |
| 271 | // deleteid {SONGID} |
| 272 | Mpc.prototype.deleteid = Mpc.__make_send_arg1('deleteid'); |
| 273 | // move [{FROM} | {START:END}] {TO} |
| 274 | Mpc.prototype.move = Mpc.__make_send_arg2('move'); |
| 275 | // moveid {FROM} {TO} |
| 276 | Mpc.prototype.moveid = Mpc.__make_send_arg2('moveid'); |
| 277 | // playlist |
| 278 | Mpc.prototype.playlist = Mpc.__make_send_void('playlist'); |
| 279 | // playlistfind {TAG} {NEEDLE} |
| 280 | Mpc.prototype.playlistfind = Mpc.__make_send_arg2('playlistfind'); |
| 281 | // playlistid {SONGID} |
| 282 | Mpc.prototype.playlistid = Mpc.__make_send_arg1('playlistid'); |
| 283 | // playlistinfo [[SONGPOS] | [START:END]] |
| 284 | Mpc.prototype.playlistinfo = Mpc.__make_send_opt('playlistinfo'); |
| 285 | // playlistsearch {TAG} {NEEDLE} |
| 286 | Mpc.prototype.playlistsearch = Mpc.__make_send_arg2('playlistsearch'); |
| 287 | // plchanges {VERSION} |
| 288 | Mpc.prototype.plchanges = Mpc.__make_send_arg1('plchanges'); |
| 289 | // plchangesposid {VERSION} |
| 290 | Mpc.prototype.plchangesposid = Mpc.__make_send_arg1('plchangesposid'); |
| 291 | // prio {PRIORITY} {START:END...} |
| 292 | Mpc.prototype.prio = Mpc.__make_send_arg2('prio'); |
| 293 | // prioid {PRIORITY} {ID...} |
| 294 | Mpc.prototype.prioid = Mpc.__make_send_arg2('prioid'); |
| 295 | // shuffle [START:END] |
| 296 | Mpc.prototype.shuffle = Mpc.__make_send_opt('shuffle'); |
| 297 | // swap {SONG1} {SONG2} |
| 298 | Mpc.prototype.swap = Mpc.__make_send_arg2('swap'); |
| 299 | // swapid {SONG1} {SONG2} |
| 300 | Mpc.prototype.swapid = Mpc.__make_send_arg2('swapid'); |
| 301 | |
| 302 | /* |
| 303 | * Stored playlists |
| 304 | * http://www.musicpd.org/doc/protocol/ch03s05.html |
| 305 | */ |
| 306 | |
| 307 | // listplaylist {NAME} |
| 308 | Mpc.prototype.listplaylist = Mpc.__make_send_arg1('listplaylist'); |
| 309 | // listplaylistinfo {NAME} |
| 310 | Mpc.prototype.listplaylistinfo = Mpc.__make_send_arg1('listplaylistinfo'); |
| 311 | // listplaylists |
| 312 | Mpc.prototype.listplaylists = Mpc.__make_send_void('listplaylists'); |
| 313 | // load {NAME} [START:END] |
| 314 | // TODO: handle optional start:end |
| 315 | Mpc.prototype.load = Mpc.__make_send_arg1('load'); |
| 316 | // playlistadd {NAME} {URI} |
| 317 | Mpc.prototype.playlistadd = Mpc.__make_send_arg2('playlistadd'); |
| 318 | // playlistclear {NAME} |
| 319 | Mpc.prototype.playlistclear = Mpc.__make_send_arg1('playlistclear'); |
| 320 | // playlistdelete {NAME} {SONGPOS} |
| 321 | Mpc.prototype.playlistdelete = Mpc.__make_send_arg2('playlistdelete'); |
| 322 | // playlistmove {NAME} {SONGID} {SONGPOS} |
| 323 | Mpc.prototype.playlistmove = Mpc.__make_send_arg3('playlistmove'); |
| 324 | // rename {NAME} {NEW_NAME} |
| 325 | Mpc.prototype.rename = Mpc.__make_send_arg2('rename'); |
| 326 | // rm {NAME} |
| 327 | Mpc.prototype.rm = Mpc.__make_send_arg1('rm'); |
| 328 | // save {NAME} |
| 329 | Mpc.prototype.save = Mpc.__make_send_arg1('save'); |
| 330 | |
| 331 | /* |
| 332 | * The music database |
| 333 | * http://www.musicpd.org/doc/protocol/ch03s06.html |
| 334 | */ |
| 335 | |
| 336 | /* |
| 337 | * Connection settings |
| 338 | * http://www.musicpd.org/doc/protocol/ch03s08.html |
| 339 | */ |
| 340 | |
| 341 | // close |
| 342 | Mpc.prototype.close = Mpc.__make_send_void('close'); |
| 343 | // kill |
| 344 | Mpc.prototype.kill = Mpc.__make_send_void('kill'); |
| 345 | // password {PASSWORD} |
| 346 | Mpc.prototype.password = Mpc.__make_send_arg1('password'); |
| 347 | // ping |
| 348 | Mpc.prototype.ping = Mpc.__make_send_void('ping'); |
| 349 | |
| 350 | /* |
| 351 | * Audio output devices |
| 352 | * http://www.musicpd.org/doc/protocol/ch03s09.html |
| 353 | */ |
| 354 | |
| 355 | // disableoutput {ID} |
| 356 | Mpc.prototype.disableoutput = Mpc.__make_send_arg1('disableoutput'); |
| 357 | // enableoutput {ID} |
| 358 | Mpc.prototype.enableoutput = Mpc.__make_send_arg1('enableoutput'); |
| 359 | // outputs |
| 360 | Mpc.prototype.outputs = Mpc.__make_send_void('outputs'); |
| 361 | |
| 362 | /* |
| 363 | * Reflection |
| 364 | * http://www.musicpd.org/doc/protocol/ch03s10.html |
| 365 | */ |
| 366 | |
| 367 | // config |
| 368 | Mpc.prototype.config = Mpc.__make_send_void('config'); |
| 369 | // commands |
| 370 | Mpc.prototype.commands = Mpc.__make_send_void('commands'); |
| 371 | // notcommands |
| 372 | Mpc.prototype.notcommands = Mpc.__make_send_void('notcommands'); |
| 373 | // tagtypes |
| 374 | Mpc.prototype.tagtypes = Mpc.__make_send_void('tagtypes'); |
| 375 | // urlhandlers |
| 376 | Mpc.prototype.urlhandlers = Mpc.__make_send_void('urlhandlers'); |
| 377 | // decoders |
| 378 | Mpc.prototype.decoders = Mpc.__make_send_void('decoders'); |
| 379 | |
| 380 | /* |
| 381 | * Client to client |
| 382 | * http://www.musicpd.org/doc/protocol/ch03s11.html |
| 383 | */ |
| 384 | |
| 385 | // subscribe {NAME} |
| 386 | Mpc.prototype.subscribe = Mpc.__make_send_arg1('subscribe'); |
| 387 | // unsubscribe {NAME} |
| 388 | Mpc.prototype.unsubscribe = Mpc.__make_send_arg1('unsubscribe'); |
| 389 | // channels |
| 390 | Mpc.prototype.channels = Mpc.__make_send_void('channels'); |
| 391 | // readmessages |
| 392 | Mpc.prototype.readmessages = Mpc.__make_send_void('readmessages'); |
| 393 | // sendmessage {CHANNEL} {TEXT} |
| 394 | Mpc.prototype.sendmessage = Mpc.__make_send_arg2('sendmessage'); |