var Duplex = require('stream').Duplex; var util = require('util'); var Player = require('./player'); var path = require('path'); var GrooveBasinProtocol = require('./protocol_parser'); // TODO: in mpd mode, when inserting tracks in dynamic mode, // insert them before the random ones module.exports = MpdProtocol; var GROOVEBASIN_PROTOCOL_UUID = "b417684f-52af-4880-be4c-cb85593fb736"; var ERR_CODE_NOT_LIST = 1; var ERR_CODE_ARG = 2; var ERR_CODE_PASSWORD = 3; var ERR_CODE_PERMISSION = 4; var ERR_CODE_UNKNOWN = 5; var ERR_CODE_NO_EXIST = 50; var ERR_CODE_PLAYLIST_MAX = 51; var ERR_CODE_SYSTEM = 52; var ERR_CODE_PLAYLIST_LOAD = 53; var ERR_CODE_UPDATE_ALREADY = 54; var ERR_CODE_PLAYER_SYNC = 55; var ERR_CODE_EXIST = 56; var tagTypes = { file: { caseCorrect: "File", grooveTag: "file", }, artist: { caseCorrect: "Artist", grooveTag: "artistName", }, artistsort: { caseCorrect: "ArtistSort", grooveTag: "artistName", sort: true, }, album: { caseCorrect: "Album", grooveTag: "albumName", }, albumartist: { caseCorrect: "AlbumArtist", grooveTag: "albumArtistName", }, albumartistsort: { caseCorrect: "AlbumArtistSort", grooveTag: "albumArtistName", sort: true, }, title: { caseCorrect: "Title", grooveTag: "name", }, track: { caseCorrect: "Track", grooveTag: "track", }, name: { caseCorrect: "Name", grooveTag: "name", }, genre: { caseCorrect: "Genre", grooveTag: "genre", }, date: { caseCorrect: "Date", grooveTag: "year", }, composer: { caseCorrect: "Composer", grooveTag: "composerName", }, performer: { caseCorrect: "Performer", grooveTag: "performerName", }, disc: { caseCorrect: "Disc", grooveTag: "disc", }, }; var commands = { "add": { fn: addCmd, permission: 'add', args: [ { name: 'uri', type: 'string', }, ], }, "addid": { permission: 'add', args: [ { name: 'uri', type: 'string', }, { name: 'position', type: 'integer', optional: true, }, ], fn: function (self, args, cb) { var pos = args.position == null ? self.player.tracksInOrder.length : args.position; var dbFile = self.player.dbFilesByPath[args.uri]; if (!dbFile) return cb(ERR_CODE_NO_EXIST, "Not found"); var ids = self.player.insertTracks(pos, [dbFile.key], false); self.push("Id: " + self.apiServer.toMpdId(ids[0]) + "\n"); cb(); }, }, "channels": { permission: 'read', fn: function (self, args, cb) { cb(); }, }, "clear": { permission: 'control', fn: function (self, args, cb) { self.player.clearPlaylist(); cb(); }, }, "clearerror": { permission: 'control', fn: function (self, args, cb) { cb(); }, }, "close": { fn: function (self, args, cb) { self.push(null); }, }, "commands": { fn: function (self, args, cb) { for (var commandName in commands) { self.push("command: " + commandName + "\n"); } cb(); }, }, "consume": { permission: 'control', args: [ { name: 'state', type: 'boolean', }, ], fn: function (self, args, cb) { self.player.setDynamicModeOn(args.state); cb(); }, }, "count": { permission: 'read', args: [ { name: 'tag', type: 'string', }, { name: 'needle', type: 'string', }, ], fn: function (self, args, cb) { var tagType = tagTypes[args.tag.toLowerCase()]; if (!tagType) return cb(ERR_CODE_ARG, "incorrect arguments"); var filters = [{ field: tagType.grooveTag, value: args.needle, }]; var songs = 0; forEachMatchingTrack(self, filters, true, function(track) { songs += 1; }); self.push("songs: " + songs + "\n"); self.push("playtime: 0\n"); cb(); }, }, "currentsong": { permission: 'read', fn: function (self, args, cb) { var currentTrack = self.player.currentTrack; if (!currentTrack) return cb(); var start = currentTrack.index; var end = start + 1; writePlaylistInfo(self, start, end); cb(); }, }, "delete": { permission: 'control', args: [ { name: 'indexRange', type: 'range', }, ], fn: function (self, args, cb) { var start = args.indexRange.start; var end = args.indexRange.end; var ids = []; for (var i = start; i < end; i += 1) { var track = self.player.tracksInOrder[i]; if (!track) { cb(ERR_CODE_ARG, "Bad song index"); return; } ids.push(track.id); } self.player.removePlaylistItems(ids); cb(); }, }, "deleteid": { permission: 'control', args: [ { name: 'id', type: 'id', }, ], fn: function(self, args, cb) { self.player.removePlaylistItems([args.id]); cb(); }, }, "find": { permission: 'read', manualArgParsing: true, fn: function (self, args, cb) { findOrSearch(self, args, true, cb); }, }, "findadd": { permission: 'control', manualArgParsing: true, fn: function (self, args, cb) { findOrSearchAdd(self, args, true, cb); }, }, "idle": {}, // handled in a special case "list": { permission: 'read', manualArgParsing: true, fn: function (self, args, cb) { if (args.length < 1) { cb(ERR_CODE_ARG, "too few arguments for \"list\""); return; } var tagTypeId = args[0].toLowerCase(); if (args.length === 2 && tagTypeId !== 'album') { cb(ERR_CODE_ARG, "should be \"Album\" for 3 arguments"); return; } if (args.length !== 2 && args.length % 2 !== 1) { cb(ERR_CODE_ARG, "not able to parse args"); return; } var targetTagType = tagTypes[tagTypeId]; if (!targetTagType) return cb(ERR_CODE_ARG, "\"" + args[0] + "\" is not known"); var caseCorrect = targetTagType.caseCorrect; var filters = []; if (args.length === 2) { filters.push({ field: 'artistName', value: args[1], }); } else { for (var i = 1; i < args.length; i += 2) { var tagType = tagTypes[args[i].toLowerCase()]; if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known"); filters.push({ field: tagType.grooveTag, value: args[i+1], }); } } var set = {}; forEachMatchingTrack(self, filters, true, function(track) { var field = track[targetTagType.grooveTag]; if (!set[field]) { set[field] = true; self.push(caseCorrect + ": " + field + "\n"); } }); cb(); }, }, "listall": { permission: 'read', args: [ { name: 'uri', type: 'string', optional: true, }, ], fn: function (self, args, cb) { forEachListAll(self, args, writeFileOnly, cb); function writeFileOnly(track) { self.push("file: " + track.file + "\n"); } }, }, "listallinfo": { permission: 'read', args: [ { name: 'uri', type: 'string', optional: true, }, ], fn: function (self, args, cb) { forEachListAll(self, args, doWriteTrackInfo, cb); function doWriteTrackInfo(track) { writeTrackInfo(self, track); } } }, "lsinfo": { permission: 'read', args: [ { name: 'uri', type: 'string', optional: true, }, ], fn: function (self, args, cb) { var dirName = args.uri || ""; var dirEntry = self.player.dirs[dirName]; if (!dirEntry) return cb(ERR_CODE_NO_EXIST, "Not found"); var baseName, relPath; var dbFilesByPath = self.player.dbFilesByPath; for (baseName in dirEntry.entries) { relPath = path.join(dirName, baseName); var dbTrack = dbFilesByPath[relPath]; if (dbTrack) writeTrackInfo(self, dbTrack); } for (baseName in dirEntry.dirEntries) { relPath = path.join(dirName, baseName); var childEntry = self.player.dirs[relPath]; self.push("directory: " + relPath + "\n"); self.push("Last-Modified: " + new Date(childEntry.mtime).toISOString() + "\n"); } cb(); }, }, "move": { permission: 'control', args: [ { name: 'fromRange', type: 'range', }, { name: 'pos', type: 'integer', }, ], fn: function (self, args, cb) { self.player.moveRangeToPos(args.fromRange.start, args.fromRange.end, args.pos); cb(); }, }, "moveid": { permission: 'control', args: [ { name: 'id', type: 'id', }, { name: 'pos', type: 'integer', }, ], fn: function (self, args, cb) { self.player.moveIdsToPos([args.id], args.pos); cb(); }, }, "next": { permission: 'control', fn: function (self, args, cb) { self.player.next(); cb(); } }, "notcommands": { fn: function (self, args, cb) { for (var commandName in commands) { var cmd = commands[commandName]; if (cmd.permission != null && !self.permissions[cmd.permission]) { self.push("command: " + commandName + "\n"); } } cb(); }, }, "outputs": { permission: 'read', fn: function (self, args, cb) { self.push("outputid: 0\n"); self.push("outputname: default detected output\n"); self.push("outputenabled: 1\n"); self.push("outputid: 1\n"); self.push("outputname: GrooveBasin HTTP Stream\n"); self.push("outputenabled: 1\n"); cb(); }, }, "password": { args: [ { name: 'password', type: 'string', }, ], fn: function (self, args, cb) { var perms = self.authenticate(args.password); var success = perms != null; if (!success) return cb(ERR_CODE_PASSWORD, "incorrect password"); self.permissions = perms; cb(); }, }, "pause": { permission: 'control', args: [ { name: 'pause', type: 'boolean', optional: true, }, ], fn: function (self, args, cb) { if (args.pause == null) { // toggle if (self.player.isPlaying) { self.player.pause(); } else { self.player.play(); } } else { if (args.pause) { self.player.pause(); } else { self.player.play(); } } cb(); }, }, "ping": { fn: function (self, args, cb) { cb(); } }, "play": { permission: 'control', fn: function (self, args, cb) { var currentTrack = self.player.currentTrack; if ((args.songPos == null || args.songPos === -1) && currentTrack) { self.player.play(); cb(); return; } var currentIndex = currentTrack ? currentTrack.index : 0; var index = (args.songPos == null || args.songPos === -1) ? currentIndex : args.songPos; self.player.seekToIndex(index, 0); cb(); }, args: [ { name: 'songPos', type: 'integer', optional: true, }, ], }, "playid": { permission: 'control', args: [ { name: 'id', type: 'id', optional: true, }, ], fn: function (self, args, cb) { var id = args.id == null ? self.player.tracksInOrder[0].id : args.id; var item = self.player.playlist[id]; if (!item) return cb(ERR_CODE_NO_EXIST, "No such song"); self.player.seek(id, 0); cb(); }, }, "playlist": { permission: 'read', fn: function (self, args, cb) { var trackTable = self.player.libraryIndex.trackTable; self.player.tracksInOrder.forEach(function(track, index) { var dbTrack = trackTable[track.key]; self.push(index + ":file: " + dbTrack.file + "\n"); }); cb(); } }, "playlistid": { permission: 'read', args: [ { name: 'id', type: 'id', optional: true, }, ], fn: function (self, args, cb) { var start = 0; var end = self.player.tracksInOrder.length; if (args.id != null) { start = self.player.playlist[args.id].index; end = start + 1; } writePlaylistInfo(self, start, end); cb(); }, }, "playlistinfo": { permission: 'read', args: [ { name: 'indexRange', type: 'range', optional: true, }, ], fn: function (self, args, cb) { var start = 0; var end = self.player.tracksInOrder.length; if (args.indexRange != null) { start = args.indexRange.start; end = args.indexRange.end; } writePlaylistInfo(self, start, end); cb(); }, }, "plchanges": { permission: 'read', args: [ { name: "version", type: "integer", }, ], fn: function(self, args, cb) { writePlaylistInfo(self, 0, self.player.tracksInOrder.length); cb(); }, }, "plchangesposid": { permission: 'read', args: [ { name: "version", type: "integer", }, ], fn: function (self, args, cb) { var tracksInOrder = self.player.tracksInOrder; for (var i = 0; i < tracksInOrder.length; i += 1) { var item = tracksInOrder[i]; self.push("cpos: " + i + "\n"); self.push("Id: " + self.apiServer.toMpdId(item.id) + "\n"); } cb(); }, }, "previous": { permission: 'control', fn: function (self, args, cb) { self.player.prev(); cb(); } }, "repeat": { permission: 'control', args: [ { name: 'on', type: 'boolean', }, ], fn: function (self, args, cb) { if (args.on && self.player.repeat === Player.REPEAT_OFF) { self.player.setRepeat(self.apiServer.singleMode ? Player.REPEAT_ONE : Player.REPEAT_ALL); } else if (!args.on && self.player.repeat !== Player.REPEAT_OFF) { self.player.setRepeat(Player.REPEAT_OFF); } cb(); }, }, "replay_gain_status": { permission: 'read', fn: function (self, args, cb) { self.push("replay_gain_mode: auto\n"); cb(); }, }, "rescan": { permission: 'admin', args: [ { name: 'uri', type: 'string', optional: true, }, ], fn: function (self, args, cb) { handleUpdate(self, args, true, cb); }, }, "search": { permission: 'read', manualArgParsing: true, fn: function (self, args, cb) { findOrSearch(self, args, false, cb); }, }, "searchadd": { permission: 'add', manualArgParsing: true, fn: function (self, args, cb) { findOrSearchAdd(self, args, false, cb); }, }, "seek": { permission: 'control', args: [ { name: 'index', type: 'integer', }, { name: 'pos', type: 'float', }, ], fn: function (self, args, cb) { self.player.seekToIndex(args.index, args.pos); cb(); }, }, "seekcur": { permission: 'control', args: [ { name: 'pos', type: 'float', }, ], fn: function (self, args, cb) { var currentTrack = self.player.currentTrack; if (!currentTrack) return cb(ERR_CODE_PLAYER_SYNC, "Not playing"); self.player.seek(currentTrack.id, args.pos); cb(); }, }, "seekid": { permission: 'control', args: [ { name: 'id', type: 'id', }, { name: 'pos', type: 'float', }, ], fn: function (self, args, cb) { self.player.seek(args.id, args.pos); cb(); }, }, "setvol": { permission: 'control', args: [ { name: 'vol', type: 'float', }, ], fn: function (self, args, cb) { self.player.setVolume(args.vol / 100); cb(); }, }, "shuffle": { permission: 'control', fn: function (self, args, cb) { self.player.shufflePlaylist(); cb(); }, }, "single": { permission: 'control', args: [ { name: 'single', type: 'boolean', }, ], fn: function (self, args, cb) { self.apiServer.setSingleMode(args.single); if (self.apiServer.singleMode && self.player.repeat === Player.REPEAT_ALL) { self.player.setRepeat(Player.REPEAT_ONE); } else if (!self.apiServer.singleMode && self.player.repeat === Player.REPEAT_ONE) { self.player.setRepeat(Player.REPEAT_ALL); } cb(); }, }, "stats": { permission: 'read', fn: statsCmd, }, "status": { permission: 'read', fn: statusCmd, }, "stop": { permission: 'control', fn: function (self, args, cb) { self.player.stop(); cb(); }, }, "swap": { permission: 'control', args: [ { name: 'pos1', type: 'integer', }, { name: 'pos2', type: 'integer', }, ], fn: function (self, args, cb) { swapItems(self, self.player.tracksInOrder[args.pos1], self.player.tracksInOrder[args.pos2], cb); }, }, "swapid": { permission: 'control', args: [ { name: 'id1', type: 'id', }, { name: 'id2', type: 'id', }, ], fn: function (self, args, cb) { swapItems(self, self.player.playlist[args.id1], self.player.playlist[args.id2], cb); }, }, "tagtypes": { permission: 'read', fn: function (self, args, cb) { for (var tagTypeId in tagTypes) { var tagType = tagTypes[tagTypeId]; self.push("tagtype: " + tagType.caseCorrect + "\n"); } cb(); }, }, "update": { permission: 'control', args: [ { name: 'uri', type: 'string', optional: true, }, ], fn: function (self, args, cb) { handleUpdate(self, args, false, cb); }, }, "urlhandlers": { permission: 'read', fn: function (self, args, cb) { cb(); // no URL handlers }, }, }; var argParsers = { 'integer': parseInteger, 'float': parseFloat, 'range': parseRange, 'boolean': parseBoolean, 'string': parseString, 'id': parseId, }; var stateCount = 0; var STATE_CMD = stateCount++; var STATE_CMD_SPACE = stateCount++; var STATE_ARG = stateCount++; var STATE_ARG_QUOTE = stateCount++; var STATE_ARG_ESC = stateCount++; var cmdListStateCount = 0; var CMD_LIST_STATE_NONE = cmdListStateCount++; var CMD_LIST_STATE_LIST = cmdListStateCount++; var bootTime = new Date(); util.inherits(MpdProtocol, Duplex); function MpdProtocol(options) { var streamOptions = extend(extend({}, options.streamOptions || {}), {decodeStrings: false}); Duplex.call(this, streamOptions); this.player = options.player; this.authenticate = options.authenticate; this.permissions = options.permissions; this.apiServer = options.apiServer; this.playerServer = options.playerServer; this.buffer = ""; this.bufferIndex = 0; this.cmdListState = CMD_LIST_STATE_NONE; this.cmdList = []; this.okMode = false; this.isIdle = false; this.commandQueue = []; this.ongoingCommand = false; this.grooveBasinProtocol = new GrooveBasinProtocol(options); this.updatedSubsystems = { database: false, update: false, stored_playlist: false, playlist: false, player: false, mixer: false, output: false, options: false, sticker: false, subscription: false, message: false, }; this._read = mpdRead; this._write = mpdWrite; this.initialize(); } MpdProtocol.prototype.initialize = function() { this.push("OK MPD 0.19.0\n"); }; MpdProtocol.prototype.upgradeProtocol = function() { var self = this; self.apiServer.handleClientEnd(self); self.grooveBasinProtocol.on('end', function() { self.push(null); }); self.playerServer.handleNewClient(self.grooveBasinProtocol); self._read = grooveBasinRead; self._write = grooveBasinWrite; self.grooveBasinProtocol.on('readable', function() { var chunk; while (chunk = self.grooveBasinProtocol.read()) { self.push(chunk); } }); self.grooveBasinProtocol.write(self.buffer, 'utf8', noop); self.buffer = ""; }; function grooveBasinRead(size) { } function grooveBasinWrite(chunk, encoding, callback) { this.grooveBasinProtocol.write(chunk, encoding, callback); } function mpdRead(size) {} function mpdWrite(chunk, encoding, callback) { var self = this; this.buffer += chunk; while (this.buffer.length) { var newlinePos = this.buffer.indexOf("\n", this.bufferIndex); if (newlinePos === -1) { this.bufferIndex = this.buffer.length; callback(); return; } var lineLength = newlinePos - 1; if (this.buffer[lineLength] !== "\r") lineLength += 1; var line = this.buffer.substring(0, lineLength); this.buffer = this.buffer.substring(newlinePos + 1); this.bufferIndex = 0; handleLine(line); } callback(); function handleLine(line) { var state = STATE_CMD; var cmd = ""; var args = []; var curArg = ""; for (var i = 0; i < line.length; i += 1) { var c = line[i]; switch (state) { case STATE_CMD: if (isSpace(c)) { state = STATE_CMD_SPACE; } else { cmd += c; } break; case STATE_CMD_SPACE: if (c === '"') { curArg = ""; state = STATE_ARG_QUOTE; } else if (!isSpace(c)) { curArg = c; state = STATE_ARG; } break; case STATE_ARG: if (isSpace(c)) { args.push(curArg); curArg = ""; state = STATE_CMD_SPACE; } else { curArg += c; } break; case STATE_ARG_QUOTE: if (c === '"') { args.push(curArg); curArg = ""; state = STATE_CMD_SPACE; } else if (c === "\\") { state = STATE_ARG_ESC; } else { curArg += c; } break; case STATE_ARG_ESC: curArg += c; state = STATE_ARG_QUOTE; break; default: throw new Error("unrecognized state"); } } if (state === STATE_ARG) { args.push(curArg); } self.commandQueue.push([cmd, args]); flushQueue(); } function flushQueue() { if (self.ongoingCommand) return; var queueItem = self.commandQueue.shift(); if (!queueItem) return; var cmd = queueItem[0]; var args = queueItem[1]; self.ongoingCommand = true; handleCommand(cmd, args, function() { self.ongoingCommand = false; flushQueue(); }); } function handleCommand(cmdName, args, cb) { var cmdIndex = 0; switch (self.cmdListState) { case CMD_LIST_STATE_NONE: if (cmdName === 'command_list_begin' && args.length === 0) { self.cmdListState = CMD_LIST_STATE_LIST; self.cmdList = []; self.okMode = false; cb(); return; } else if (cmdName === 'command_list_ok_begin' && args.length === 0) { self.cmdListState = CMD_LIST_STATE_LIST; self.cmdList = []; self.okMode = true; cb(); return; } else { runOneCommand(cmdName, args, 0, function(ok) { if (ok) self.push("OK\n"); cb(); }); return; } break; case CMD_LIST_STATE_LIST: if (cmdName === 'command_list_end' && args.length === 0) { self.cmdListState = CMD_LIST_STATE_NONE; runAndCheckOneCommand(); return; } else { self.cmdList.push([cmdName, args]); cb(); return; } break; default: throw new Error("unrecognized state"); } function runAndCheckOneCommand() { var commandPayload = self.cmdList.shift(); if (!commandPayload) { self.push("OK\n"); cb(); return; } var thisCmdName = commandPayload[0]; var thisCmdArgs = commandPayload[1]; runOneCommand(thisCmdName, thisCmdArgs, cmdIndex++, function(ok) { if (!ok) { cb(); return; } if (self.okMode) self.push("list_OK\n"); runAndCheckOneCommand(); }); } function runOneCommand(cmdName, args, index, cb) { if (cmdName === 'noidle') { var ok = self.isIdle; self.isIdle = false; cb(ok); return; } if (self.isIdle) { self.push(null); cb(false); return; } if (cmdName === 'idle') { if (!self.permissions.read) { cmdDone(ERR_CODE_PERMISSION, "you don't have permission for \"" + cmdName + "\""); } else { self.handleIdle(args); } cb(false); return; } if (cmdName === 'protocolupgrade') { if (args.length !== 1 || args[0] !== GROOVEBASIN_PROTOCOL_UUID) { cmdDone(ERR_CODE_ARG, "invalid arguments"); return; } self.upgradeProtocol(); return; } execOneCommand(cmdName, args, cmdDone); function cmdDone(code, msg) { if (code) { console.warn("cmd err:", cmdName, JSON.stringify(args), msg); if (code === ERR_CODE_UNKNOWN) cmdName = ""; self.push("ACK [" + code + "@" + index + "] {" + cmdName + "} " + msg + "\n"); cb(false); return; } cb(true); } } function execOneCommand(cmdName, args, cb) { if (!cmdName.length) return cb(ERR_CODE_UNKNOWN, "No command given"); var cmd = commands[cmdName]; if (!cmd) return cb(ERR_CODE_UNKNOWN, "unknown command \"" + cmdName + "\""); var perm = cmd.permission; if (perm != null && !self.permissions[perm]) { cb(ERR_CODE_PERMISSION, "you don't have permission for \"" + cmdName + "\""); return; } var argsParam; if (cmd.manualArgParsing) { argsParam = args; } else { var min = 0; var max = 0; var i; var cmdArgs = cmd.args || []; for (i = 0; i < cmdArgs.length; i += 1) { if (!cmdArgs[i].optional) min += 1; max += 1; } if (args.length < min) { cb(ERR_CODE_ARG, "too few arguments for \"" + cmdName + "\""); return; } if (args.length > max) { cb(ERR_CODE_ARG, "too many arguments for \"" + cmdName + "\""); return; } var namedArgs = {}; for (i = 0; i < args.length; i += 1) { var arg = args[i]; var argInfo = cmdArgs[i]; var parseArg = argParsers[argInfo.type]; if (!parseArg) throw new Error("unrecognized arg type: " + argInfo.type); var ret = parseArg.call(self, arg, argInfo); if (ret.msg) { cb(ERR_CODE_ARG, ret.msg); return; } namedArgs[argInfo.name] = ret.value; } argsParam = namedArgs; } console.info("ok mpd command", cmdName, JSON.stringify(argsParam)); cmd.fn(self, argsParam, cb); } } } MpdProtocol.prototype.handleIdle = function(args) { var anyUpdated = false; for (var subsystem in this.updatedSubsystems) { var isUpdated = this.updatedSubsystems[subsystem]; if (isUpdated) { this.push("changed: " + subsystem + "\n"); anyUpdated = true; this.updatedSubsystems[subsystem] = false; } } if (anyUpdated) { this.push("OK\n"); this.isIdle = false; return; } this.isIdle = true; }; function isSpace(c) { return c === '\t' || c === ' '; } function parseBoolean(str) { return { value: !!parseInt(str, 10), msg: null, }; } function parseFloat(str) { var x = parseInt(str, 10); return { value: x, msg: isNaN(x) ? ("Number expected: " + str) : null, }; } function parseInteger(str) { var x = parseInt(str, 10); return { value: x, msg: isNaN(x) ? ("Integer expected: " + str) : null, }; } function parseRange(str, argInfo) { var msg = null; var start = null; var end = null; var parts = str.split(":"); if (parts.length === 2) { start = parseInt(parts[0], 10); end = parseInt(parts[1], 10); } else if (parts.length === 1) { start = parseInt(parts[0], 10); if (start === -1 && argInfo.optional) { return { value: null, msg: null, }; } end = start + 1; } if (start == null || end == null || isNaN(start) || isNaN(end)) { msg = "Integer or range expected: " + str; } else if (start < 0 || end < 0) { msg = "Number is negative: " + str; } else if (end < start) { msg = "Bad song index"; } return { value: { start: start, end: end, }, msg: msg, }; } function parseString(str) { return { value: str, msg: null, }; } function parseId(str) { var results = parseInteger.call(this, str); if (results.msg) return results; var grooveBasinId = this.apiServer.fromMpdId(results.value); var msg = grooveBasinId ? null : "No such song"; return { value: grooveBasinId, msg: null, }; } function writeTrackInfo(self, dbTrack) { self.push("file: " + dbTrack.file + "\n"); if (dbTrack.mtime != null) { self.push("Last-Modified: " + new Date(dbTrack.mtime).toISOString() + "\n"); } if (dbTrack.duration != null) { self.push("Time: " + Math.round(dbTrack.duration) + "\n"); } if (dbTrack.artistName) { self.push("Artist: " + dbTrack.artistName + "\n"); } if (dbTrack.albumName) { self.push("Album: " + dbTrack.albumName + "\n"); } if (dbTrack.albumArtistName) { self.push("AlbumArtist: " + dbTrack.albumArtistName + "\n"); } if (dbTrack.genre) { self.push("Genre: " + dbTrack.genre + "\n"); } if (dbTrack.name) { self.push("Title: " + dbTrack.name + "\n"); } if (dbTrack.track != null) { if (dbTrack.trackCount != null) { self.push("Track: " + dbTrack.track + "/" + dbTrack.trackCount + "\n"); } else { self.push("Track: " + dbTrack.track + "\n"); } } if (dbTrack.composerName) { self.push("Composer: " + dbTrack.composerName + "\n"); } if (dbTrack.disc != null) { if (dbTrack.discCount != null) { self.push("Disc: " + dbTrack.disc + "/" + dbTrack.discCount + "\n"); } else { self.push("Disc: " + dbTrack.disc + "\n"); } } if (dbTrack.year != null) { self.push("Date: " + dbTrack.year + "\n"); } } function writePlaylistInfo(self, start, end) { var trackTable = self.player.libraryIndex.trackTable; for (var i = start; i < end; i += 1) { var item = self.player.tracksInOrder[i]; var track = trackTable[item.key]; writeTrackInfo(self, track); self.push("Pos: " + i + "\n"); self.push("Id: " + self.apiServer.toMpdId(item.id) + "\n"); } } function forEachMatchingTrack(self, filters, caseSensitive, fn) { // TODO: support 'in' as tag type var trackTable = self.player.libraryIndex.trackTable; if (!caseSensitive) { filters.forEach(function(filter) { filter.value = filter.value.toLowerCase(); }); } for (var key in trackTable) { var track = trackTable[key]; var matches = false; for (var filterIndex = 0; filterIndex < filters.length; filterIndex += 1) { var filter = filters[filterIndex]; var filterField = String(track[filter.field]); if (!filterField) continue; if (!caseSensitive && filterField) filterField = filterField.toLowerCase(); /* assumes: * caseSensitive implies "find" * !caseSensitive implies "search" */ if (caseSensitive) { if (filterField === filter.value) { matches = true; break; } } else if (filterField.indexOf(filter.value) > -1) { matches = true; break; } } if (matches) fn(track); } } function forEachListAll(self, args, onTrack, cb) { var dirName = args.uri || ""; var dirEntry = self.player.dirs[dirName]; if (!dirEntry) return cb(ERR_CODE_NO_EXIST, "Not found"); printOneDir(dirEntry); cb(); function printOneDir(dirEntry) { var baseName, relPath; if (dirEntry.dirName) { // exclude root self.push("directory: " + dirEntry.dirName + "\n"); self.push("Last-Modified: " + new Date(dirEntry.mtime).toISOString() + "\n"); } var dbFilesByPath = self.player.dbFilesByPath; for (baseName in dirEntry.entries) { relPath = path.join(dirEntry.dirName, baseName); var dbTrack = dbFilesByPath[relPath]; if (dbTrack) onTrack(dbTrack); } for (baseName in dirEntry.dirEntries) { relPath = path.join(dirEntry.dirName, baseName); var childEntry = self.player.dirs[relPath]; if (childEntry) { printOneDir(childEntry); } } } } function parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish) { if (args.length < 2) { cb(ERR_CODE_ARG, "too few arguments for \"find\""); return; } if (args.length % 2 !== 0) { cb(ERR_CODE_ARG, "incorrect arguments"); return; } var filters = []; for (var i = 0; i < args.length; i += 2) { var tagsToSearch = []; if (args[i].toLowerCase() === "any") { // Special case the any key. Just search everything. for (var tagType in tagTypes) { tagsToSearch.push(tagTypes[tagType]); } } else { var tagType = tagTypes[args[i].toLowerCase()]; if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known"); tagsToSearch.push(tagType); } for (var j = 0; j < tagsToSearch.length; j++) { filters.push({ field: tagsToSearch[j].grooveTag, value: args[i+1], }); } forEachMatchingTrack(self, filters, caseSensitive, onTrack); } onFinish(); } function handleUpdate(self, args, forceRescan, cb) { var dirEntry = self.player.dirs[args.uri || ""]; if (!dirEntry) { cb(ERR_CODE_ARG, "Malformed path"); return; } self.player.requestUpdateDb(dirEntry.dirName, forceRescan); self.push("updating_db: 1\n"); cb(); } function findOrSearch(self, args, caseSensitive, cb) { parseFindArgs(self, args, caseSensitive, onTrack, cb, cb); function onTrack(track) { writeTrackInfo(self, track); } } function findOrSearchAdd(self, args, caseSensitive, cb) { var keys = []; parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish); function onTrack(track) { keys.push(track.key); } function onFinish() { self.player.appendTracks(keys, false); cb(); } } function swapItems(self, item1, item2, cb) { if (!item1 || !item2) return cb(ERR_CODE_ARG, "No such song"); var o = {}; o[item1.id] = {sortKey: item2.sortKey}; o[item2.id] = {sortKey: item1.sortKey}; self.player.movePlaylistItems(o); cb(); } function addCmd(self, args, cb) { var dbFilesByPath = self.player.dbFilesByPath; var dbFile = dbFilesByPath[args.uri]; if (dbFile) { self.player.appendTracks([dbFile.key], false); cb(); return; } var keys = []; var dirEntry = self.player.dirs[args.uri]; if (!dirEntry) return cb(ERR_CODE_NO_EXIST, "Not found"); addDir(dirEntry); if (keys.length === 0) { cb(ERR_CODE_NO_EXIST, "Not found"); return; } self.player.appendTracks(keys, false); cb(); function addDir(dirEntry) { var baseName; for (baseName in dirEntry.entries) { var relPath = path.join(dirEntry.dirName, baseName); var dbFile = dbFilesByPath[relPath]; if (dbFile) keys.push(dbFile.key); } for (baseName in dirEntry.dirEntries) { var childEntry = self.player.dirs[path.join(dirEntry.dirName, baseName)]; addDir(childEntry); } } } function statsCmd(self, args, cb) { var uptime = Math.floor((new Date() - bootTime) / 1000); var libraryIndex = self.player.libraryIndex; var artists = libraryIndex.artistList.length; var albums = libraryIndex.albumList.length; var songs = 0; var trackTable = libraryIndex.trackTable; var dbPlaytime = 0; for (var key in trackTable) { var dbTrack = trackTable[key]; songs += 1; dbPlaytime += dbTrack.duration; } dbPlaytime = Math.floor(dbPlaytime); var dbUpdate = Math.floor(new Date().getTime() / 1000); self.push("artists: " + artists + "\n"); self.push("albums: " + albums + "\n"); self.push("songs: " + songs + "\n"); self.push("uptime: " + uptime + "\n"); self.push("playtime: 0\n"); // TODO keep track of this? self.push("db_playtime: " + dbPlaytime + "\n"); self.push("db_update: " + dbUpdate + "\n"); cb(); } function statusCmd(self, args, cb) { var volume = Math.round(self.player.volume * 100); var repeat, single; switch (self.player.repeat) { case Player.REPEAT_ONE: repeat = 1; single = 1; break; case Player.REPEAT_ALL: repeat = 1; single = 0; break; case Player.REPEAT_OFF: repeat = 0; single = +self.apiServer.singleMode; break; } var consume = +self.player.dynamicModeOn; var playlistLength = self.player.tracksInOrder.length; var currentTrack = self.player.currentTrack; var state; if (self.player.isPlaying) { state = 'play'; } else if (currentTrack) { state = 'pause'; } else { state = 'stop'; } var song = null; var songId = null; var nextSong = null; var nextSongId = null; var elapsed = null; var time = null; var trackTable = self.player.libraryIndex.trackTable; if (currentTrack) { song = currentTrack.index; songId = self.apiServer.toMpdId(currentTrack.id); var nextTrack = self.player.tracksInOrder[currentTrack.index + 1]; if (nextTrack) { nextSong = nextTrack.index; nextSongId = self.apiServer.toMpdId(nextTrack.id); } var dbTrack = trackTable[currentTrack.key]; elapsed = self.player.getCurPos(); time = Math.round(elapsed) + ":" + Math.round(dbTrack.duration); } self.push("volume: " + volume + "\n"); self.push("repeat: " + repeat + "\n"); self.push("random: 0\n"); self.push("single: " + single + "\n"); self.push("consume: " + consume + "\n"); self.push("playlist: 0\n"); // TODO what to do with this? self.push("playlistlength: " + playlistLength + "\n"); self.push("xfade: 0\n"); self.push("mixrampdb: 0.000000\n"); self.push("mixrampdelay: nan\n"); self.push("state: " + state + "\n"); if (song != null) { self.push("song: " + song + "\n"); self.push("songid: " + songId + "\n"); if (nextSong != null) { self.push("nextsong: " + nextSong + "\n"); self.push("nextsongid: " + nextSongId + "\n"); } self.push("time: " + time + "\n"); self.push("elapsed: " + elapsed + "\n"); self.push("bitrate: 192\n"); // TODO make this not hardcoded? self.push("audio: 44100:24:2\n"); // TODO make this not hardcoded? } cb(); } function extend(o, src) { for (var key in src) o[key] = src[key]; return o; } function noop() {}