logitechmediaserver
Version:
Listen to and control Logitech Media Server (previously Squeezebox Server).
251 lines (208 loc) • 10.2 kB
JavaScript
var util = require('util');
var net = require('net');
var EventEmitter = require('events').EventEmitter;
var SqueezePlayer = require('./squeezeplayer');
// Add a trivial method to the net.Stream prototype object to
// enable debugging during development. It just appends \n and writes to the stream.
net.Stream.prototype.writeln = function(s) {
// Uncomment the next line to see data as it's written to telnet
// console.log("> " + s);
this.write(s + "\n");
}
// The LogitechMediaServer object is an event emitter with a few properties.
// After creating it, call .start() and wait for the "registration_finished" event.
// The port number is optional, if not provided, the default value of 9090 is used.
function LogitechMediaServer(address, port) {
var self = this;
self.address = address;
self.port = port || 9090;
}
util.inherits(LogitechMediaServer, EventEmitter);
// Start listening to the telnet server provided by Logitech Media Server.
// Username/password are optional, if both are provided we will log in using those credentials.
LogitechMediaServer.prototype.start = function(username, password) {
var self = this;
// Listen on self.port to self.address
self.telnet = net.createConnection(self.port, self.address);
self.line_parser = new LineParser(self.telnet);
// The LineParser just emits a "line" event for each line of data
// that the LMS telnet connection emits
self.line_parser.on("line", function(data) {
// Uncomment the next line to see text lines coming back from telnet
// console.log("< " + data.toString().replace(/\n/g,"\\n"));
self.handleLine(data);
});
if (username && password) {
// Start things off by logging in.
self.telnet.writeln("login " + username + " " + password);
}
else {
// Start things off by asking how many players are connected.
// See .handle() - the response to 'player count ?' is how the code
// discovers info about all the known players.
self.telnet.writeln("player count ?");
}
}
LogitechMediaServer.prototype.handle = function(buffer, keyword, callback) {
// If data starts with keyword, call the callback with the remainder, and return true.
// Otherwise just return false.
// e.g. "player count 3\n", "player count" would call the callback with "3"
// seems as if the telnet server URL encodes things
var data = decodeURIComponent(buffer.toString());
// Look for (start of string)(keyword) (data)(end of string)
var m = data.match("^" + keyword + "\\s(.*?)$");
if (m) {
// call the callback with the remainder of the string
callback(m[1], buffer);
return true;
}
return false;
}
LogitechMediaServer.prototype.handle_with_id = function(buffer, keyword, callback) {
// Similar to .handle, but look for a MAC address:
// EITHER xx:xx:xx:xx:xx:xx followed by keyword, follwed by data
// OR xx:xx:xx:xx:xx:xx followed by data (keyword should be set to null for this)
var self = this;
// seems as if the telnet server URL encodes things
var data = decodeURIComponent(buffer.toString());
// step through the known players
for (mac in self.players) {
var player = self.players[mac];
if (keyword) {
// look for (start)(mac) (keyword) (data)(end)
var m = data.match("^" + player.id + "\\s" + keyword + "\\s(.*?)$");
if (m) {
callback(player, m[1], buffer);
return true;
}
// perhaps it's just a line like "00:00:00:00:00:00 stop". i.e. data is nonexistent
// look for (start)(mac) (keyword)(end)
var m = data.match("^" + player.id + "\\s" + keyword + "$");
if (m) {
callback(player, null, buffer);
return true;
}
} else {
// look for (start)(mac) (data)(end)
var m = data.match("^" + player.id + "\\s(.*?)$");
if (m) {
callback(player, m[1], buffer);
return true;
}
}
};
return false;
}
// Passed a player index and a player MAC address, add to in-memory dictionary of players
LogitechMediaServer.prototype.registerPlayer = function(pnum, pid) {
var self = this;
self.players[pid] = new SqueezePlayer(self.telnet);
self.players[pid].id = pid;
self.players[pid].index = pnum;
// Check whether this is the last player we're waiting for, if so emit "registration_finished"
if (Object.keys(self.players).length == self.numPlayers) {
self.emit("registration_finished");
// Can now start listening for all sorts of things!
self.telnet.writeln("listen 1");
}
}
// Parse incoming data stream splitting on \n and emitting "line" events
function LineParser(stream) {
var self = this;
self.stream = stream;
self.buffer = "";
self.stream.on("data", function(d) { self.parse(d) });
}
util.inherits(LineParser, EventEmitter);
LineParser.prototype.parse = function(data) {
this.buffer += data;
var split = this.buffer.indexOf("\n");
while (split > -1) {
this.emit('line', this.buffer.slice(0,split));
this.buffer = this.buffer.slice(split+1);
split = this.buffer.indexOf("\n");
}
}
// Called with each line received from the telnet connection, this function looks for
// various commands and acts on them. Anything unhandled falls out at the bottom
// (currently gets logged to console), except for unhandled stuff that relates to a player.
// Those lines get passed to the player object for handling.
LogitechMediaServer.prototype.handleLine = function(buffer) {
var self = this;
var handled = false;
// Guts of this function is pretty much a list of commands and callbacks.
// Could definitely be made more efficient, or a bit DRYer, but it's just a bunch of string comparisons.
// "login username ********" response is what kicks things off if we are using password protection (see .start())
if (self.handle(buffer, "login", function (params, buffer) {
// Start things off by asking how many players are connected.
self.telnet.writeln("player count ?");
})) { handled = true } ;
// "player count" response is what kicks things off in the first place (see .start() or above)
if (self.handle(buffer, "player count", function(params, buffer) {
// reset in-memory knowledge of players
self.numPlayers = parseInt(params);
self.players = {};
// Now issue a "player id" request for each player
for (var p=0; p<self.numPlayers; p++) {
self.telnet.writeln("player id " + p + " ?");
}
})) { handled=true } ;
// This response is received for each player, so store 'em in memory as a dic
if (self.handle(buffer, "player id", function(params, b) {
params = params.split(" ");
var pnum = parseInt(params[0]);
var pid = params[1];
self.registerPlayer(pnum, pid);
// Now that we know the id & mac, ask for more info. name and signalstrength, also power state
self.telnet.writeln(pid + " signalstrength ?");
self.telnet.writeln(pid + " name ?");
self.telnet.writeln(pid + " power ?");
self.telnet.writeln(pid + " mixer volume ?");
})) { handled = true } ;
// Just handle the "listen" response (LMS should just respond with 'listen 1' at the beginning)
if (self.handle(buffer, "listen", function() {})) { handled = true };
// ~~~~~~~~~~~~~~ keywords below here are those which are associated with an individual player ~~~~~~~~~~~~~~~~~~
if (self.handle_with_id(buffer, "signalstrength", function(player, params, b) {
player.setProperty("signalstrength", parseInt(params));
})) { handled = true } ;
if (self.handle_with_id(buffer, "power", function(player, params, b) {
player.setProperty("power", parseInt(params));
if (player.power == 1) {
// Wait a tiny bit while player is powering up and then ask what state the player is in
setTimeout(function() { player.runTelnetCmd("mode ?") }, 1500);
} else {
player.setProperty("mode", "off");
}
})) { handled = true } ;
if (self.handle_with_id(buffer, "name", function(player, params, b) {
player.setProperty("name", params);
})) { handled = true };
if (self.handle_with_id(buffer, "current_title", function(player, params, b) {
player.setProperty("current_title", params);
})) { handled = true };
if (self.handle_with_id(buffer, "mode", function(player, params, b) {
player.setProperty("mode", params); // "play", "stop" or "pause"
})) { handled = true };
if (self.handle_with_id(buffer, "play", function(player, params, b) {
player.setProperty("mode", "play");
// player has started playing something. Let's find out what!
player.runTelnetCmd("current_title ?");
})) { handled = true };
if (self.handle_with_id(buffer, "stop", function(player, params, b) {
player.setProperty("mode", "stop");
})) { handled = true };
if (self.handle_with_id(buffer, "pause", function(player, params, b) {
player.setProperty("mode", "pause");
})) { handled = true };
if (!handled) {
// handle any string received that starts with an id and isn't handled yet by passing events
// to the player objects.
if (!self.handle_with_id(buffer, null, function(player, params, b) {
player.handleServerData(params, b);
})) {
// anything else, just log to console for now. Could be an event of its own.
console.log("unhandled line", decodeURIComponent(buffer.toString()));
}
}
}
module.exports = LogitechMediaServer;