MPD protocol: support async commands and implement add

This commit is contained in:
Andrew Kelley 2014-02-24 18:24:38 -05:00
parent 4c78a11b64
commit 03404fa01f

View file

@ -1,4 +1,6 @@
var net = require('net');
var path = require('path');
var findit = require('findit');
var Player = require('../player');
module.exports = MpdServer;
@ -128,6 +130,7 @@ MpdServer.prototype.initialize = function(cb) {
return;
}
self.bootTime = new Date();
self.singleMode = false;
var server = net.createServer(onSocketConnection);
server.listen(mpdPort, mpdHost, function() {
console.info("MPD Protocol listening at " + mpdHost + ":" + mpdPort);
@ -140,6 +143,8 @@ MpdServer.prototype.initialize = function(cb) {
var cmdList = [];
var okMode = false;
var isIdle = false;
var commandQueue = [];
var ongoingCommand = false;
var updatedSubsystems = {
database: false,
update: false,
@ -253,86 +258,129 @@ MpdServer.prototype.initialize = function(cb) {
if (state === STATE_ARG) {
args.push(curArg);
}
handleCommand(cmd, args);
commandQueue.push([cmd, args]);
flushQueue();
}
function handleCommand(cmdName, args) {
function flushQueue() {
if (ongoingCommand) return;
var queueItem = commandQueue.shift();
if (!queueItem) return;
var cmd = queueItem[0];
var args = queueItem[1];
ongoingCommand = true;
handleCommand(cmd, args, function() {
ongoingCommand = false;
flushQueue();
});
}
function handleCommand(cmdName, args, cb) {
var cmdIndex = 0;
switch (cmdListState) {
case CMD_LIST_STATE_NONE:
if (cmdName === 'command_list_begin' && args.length === 0) {
cmdListState = CMD_LIST_STATE_LIST;
cmdList = [];
okMode = false;
cb();
return;
} else if (cmdName === 'command_list_ok_begin' && args.length === 0) {
cmdListState = CMD_LIST_STATE_LIST;
cmdList = [];
okMode = true;
cb();
return;
} else {
if (!runOneCommand(cmdName, args, 0)) {
socket.write("OK\n");
}
runOneCommand(cmdName, args, 0, function(ok) {
if (ok) socket.write("OK\n");
cb();
});
return;
}
break;
case CMD_LIST_STATE_LIST:
if (cmdName === 'command_list_end' && args.length === 0) {
var errorOccurred = false;
for (var i = 0; i < cmdList.length; i += 1) {
var commandPayload = cmdList[i];
var thisCmdName = commandPayload[0];
var thisCmdArgs = commandPayload[1];
if (runOneCommand(thisCmdName, thisCmdArgs, i)) {
errorOccurred = true;
break;
} else if (okMode) {
socket.write("list_OK\n");
}
}
if (!errorOccurred) {
socket.write("OK\n");
}
cmdList = [];
cmdListState = CMD_LIST_STATE_NONE;
runAndCheckOneCommand();
return;
} else {
cmdList.push([cmdName, args]);
cb();
return;
}
break;
default:
throw new Error("unrecognized state");
}
function runAndCheckOneCommand() {
var commandPayload = cmdList.shift();
if (!commandPayload) {
socket.write("OK\n");
cb();
return;
}
var thisCmdName = commandPayload[0];
var thisCmdArgs = commandPayload[1];
runOneCommand(thisCmdName, thisCmdArgs, cmdIndex++, function(ok) {
if (!ok) {
cb();
return;
} else if (okMode) {
socket.write("list_OK\n");
}
});
}
}
function runOneCommand(cmdName, args, index) {
function runOneCommand(cmdName, args, index, cb) {
if (cmdName === 'noidle') {
handleNoIdle(args);
return true;
cb(false);
return;
}
if (isIdle) {
socket.end();
return true;
cb(false);
return;
}
if (cmdName === 'idle') {
handleIdle(args);
return true;
cb(false);
return;
}
var returnValue = execOneCommand(cmdName, args);
if (returnValue) {
var code = returnValue[0];
var msg = returnValue[1];
if (/unimplemented/.test(msg)) {
console.info("needed command:", cmdName, JSON.stringify(args));
execOneCommand(cmdName, args, cmdDone);
function cmdDone(code, msg) {
if (code) {
if (/unimplemented/.test(msg)) {
console.info("needed command:", cmdName, JSON.stringify(args));
}
if (code === ERR_CODE_UNKNOWN) cmdName = "";
socket.write("ACK [" + code + "@" + index + "] {" + cmdName + "} " + msg + "\n");
cb(false);
return;
}
if (code === ERR_CODE_UNKNOWN) cmdName = "";
socket.write("ACK [" + code + "@" + index + "] {" + cmdName + "} " + msg + "\n");
return true;
cb(true);
}
}
function execOneCommand(cmdName, args) {
if (!cmdName.length) return [ERR_CODE_UNKNOWN, "No command given"];
function execOneCommand(cmdName, args, cb) {
if (!cmdName.length) return cb(ERR_CODE_UNKNOWN, "No command given");
var cmd = commands[cmdName];
if (!cmd) return [ERR_CODE_UNKNOWN, "unknown command \"" + cmdName + "\""];
return cmd(self, socket, args);
if (!cmd) return cb(ERR_CODE_UNKNOWN, "unknown command \"" + cmdName + "\"");
if (cmd.length < 4) {
var returnValue = cmd(self, socket, args);
var code = returnValue && returnValue[0];
var msg = returnValue && returnValue[1];
cb(code, msg);
return;
}
cmd(self, socket, args, cb);
}
function handleIdle(args) {
@ -413,8 +461,37 @@ function writeTrackInfo(socket, dbTrack) {
}
}
function addCmd(self, socket, args) {
return [ERR_CODE_UNKNOWN, "unimplemented"];
function addCmd(self, socket, args, cb) {
if (args.length !== 1) {
return [ERR_CODE_ARG, "wrong number of arguments for \"add\""];
}
var uri = args[0];
var musicDir = self.gb.config.musicDirectory;
var walker = findit(path.join(musicDir, uri));
var files = [];
walker.on('file', function(file) {
console.info("found file", file);
files.push(file);
});
walker.on('error', function(err) {
walker.removeAllListeners();
console.error("unable to walk file system:", err.stack);
cb(ERR_CODE_UNKNOWN, "Unknown error");
});
walker.on('end', function() {
var keys = [];
for (var i = 0; i < files.length; i += 1) {
var file = files[i];
var relPath = path.relative(musicDir, file);
var dbFile = self.gb.player.dbFilesByPath[relPath];
console.info("found dbFile", dbFile);
if (dbFile) keys.push(dbFile.key);
}
console.info("append keys", keys);
self.gb.player.appendTracks(keys, false);
cb();
});
}
function addidCmd(self, socket, args) {
@ -803,15 +880,33 @@ function sendmessageCmd(self, socket, args) {
}
function setvolCmd(self, socket, args) {
return [ERR_CODE_UNKNOWN, "unimplemented"];
if (args.length !== 1) {
return [ERR_CODE_ARG, "wrong number of arguments for \"setvol\""];
}
var vol100 = parseFloat(args[0]);
if (isNaN(vol100)) return [ERR_CODE_ARG, "Integer expected: " + args[0]];
self.gb.player.setVolume(vol100 / 100);
}
function shuffleCmd(self, socket, args) {
return [ERR_CODE_UNKNOWN, "unimplemented"];
self.gb.player.shufflePlaylist();
}
function singleCmd(self, socket, args) {
return [ERR_CODE_UNKNOWN, "unimplemented"];
switch (self.gb.player.repeat) {
case Player.REPEAT_ONE:
self.gb.player.setRepeat(Player.REPEAT_ALL);
self.singleMode = false;
break;
case Player.REPEAT_ALL:
self.gb.player.setRepeat(Player.REPEAT_ONE);
self.singleMode = true;
break;
case Player.REPEAT_OFF:
self.singleMode = !self.singleMode;
break;
}
}
function statsCmd(self, socket, args) {
@ -854,7 +949,7 @@ function statusCmd(self, socket, args) {
break;
case Player.REPEAT_OFF:
repeat = 0;
single = 0;
single = +self.singleMode;
break;
}
var playlistLength = self.gb.player.tracksInOrder.length;