lots of refactoring

* plugins are structured as in mineflayer
   - lib/plugin.js is deleted
   - download, delete, lastfm, upload, dynamicmode disabled until they
     can be updated to work directly with PlayerServer
 * GrooveBasin class represents the main server
 * Killer is cleaned up
 * Library does not respond to bus; instead is constructed with
   musicLibPath
 * MpdConf is cleaned up
 * MpdParser is deleted in favor of mpd.js module
 * lib/player.js is deleted. wtf was that doing there?
 * server-side PlayerClient is deleted
 * PlayerServer is cleaned up a bit. ability to directly proxy mpd
   requests is deleted.
This commit is contained in:
Andrew Kelley 2013-08-11 18:54:19 -04:00
parent e1da9b3339
commit 13edb5c3aa
19 changed files with 712 additions and 3247 deletions

8
TODO
View file

@ -1,20 +1,22 @@
High priority:
(andy) node-musicmetadata: get duration from tags
(andy) get duration from mpd on playlist update
(josh) diverge playerclient.js duplication
ditch jspackage in favor of browserfy
Eventually:
refactor plugins to be like mineflayer
fix the broken plugins
eliminate the TODOs in the code
ditch handlebars
replace socket.io with https://github.com/einaros/ws
finish converting mpd-specific protocol (named playlist support)
persist current playlist
ditch watch (prefer https://github.com/paulmillr/chokidar)
work on node-musicmetadata and support more formats/songs (such as wma)
use mpd.js https://github.com/superjoe30/mpd.js
ditch mpd
use diacritics module: https://github.com/superjoe30/diacritics
consider using https://github.com/rvagg/node-levelup
- separate state from configuration
ditch qq fileuploader
extract the skewed random song selection into a separate, tested module
make dynamic mode work with version of mpd that is in ubuntu 0.16.5-1ubuntu4 (raspberry pi is 0.16.7-2)
ditch watch (prefer https://github.com/paulmillr/chokidar)

289
lib/groovebasin.js Normal file
View file

@ -0,0 +1,289 @@
var EventEmitter = require('events').EventEmitter;
var mpd = require('mpd');
var async = require('async');
var http = require('http');
var assert = require('assert');
var socketio = require('socket.io');
var fs = require('fs');
var util = require('util');
var path = require('path');
var mkdirp = require('mkdirp');
var which = require('which');
var express = require('express');
var spawn = require('child_process').spawn;
var requireIndex = require('requireindex');
var plugins = requireIndex(path.join(__dirname, 'plugins'));
var PlayerServer = require('./playerserver');
var Killer = require('./killer');
var Library = require('./library');
var MpdConf = require('./mpdconf');
module.exports = GrooveBasin;
var STATE_VERSION = 4;
var DEFAULT_PERMISSIONS = {
read: true,
add: true,
control: true
};
util.inherits(GrooveBasin, EventEmitter);
function GrooveBasin() {
EventEmitter.call(this);
this.runDir = "run";
this.mpdSocketPath = path.join(this.runDir, "mpd.socket");
this.stateFile = path.join(this.runDir, "state.json");
this.mpdConfPath = path.join(this.runDir, "mpd.conf");
this.mpdPidFile = path.join(this.runDir, "mpd.pid");
this.mpdConf = new MpdConf();
this.mpdConf.setRunDir(this.runDir);
this.app = express();
this.app.disable('x-powered-by');
// initialized later
this.state = null;
this.library = null;
this.socketIo = null;
this.httpServer = null;
}
GrooveBasin.prototype.start = function(options) {
start(this, options || {});
}
GrooveBasin.prototype.makeRunDir = function(cb) {
mkdirp(this.runDir, cb);
}
GrooveBasin.prototype.initState = function(cb) {
initState(this, cb);
}
GrooveBasin.prototype.restoreState = function(cb) {
restoreState(this, cb);
}
// TODO: instead of saving json to disk use leveldb or something like that.
GrooveBasin.prototype.saveState = function(cb) {
cb = cb || noop;
saveState(this, cb);
}
GrooveBasin.prototype.writeMpdConf = function(cb) {
var mc = new MpdConf(this.state.mpd_conf);
this.state.mpd_conf = mc.state;
fs.writeFile(this.mpdConfPath, mc.toMpdConf(), cb);
}
GrooveBasin.prototype.restartMpd = function(cb) {
restartMpd(this, cb);
}
GrooveBasin.prototype.startServer = function(cb) {
startServer(this, cb);
}
GrooveBasin.prototype.connectToMpd = function() {
connectToMpd(this);
}
GrooveBasin.prototype.saveAndSendStatus = function() {
this.saveState();
this.socketIo.sockets.emit('Status', JSON.stringify(this.state.status));
};
GrooveBasin.prototype.rescanLibrary = function() {
console.error("TODO: rescanning library is not yet supported.");
};
function connectToMpd(self) {
var connectTimeout = null;
var connectSuccess = true;
connect();
function tryReconnect() {
if (connectTimeout != null) return;
connectTimeout = setTimeout(function(){
connectTimeout = null;
connect();
}, 1000);
}
function connect() {
var mpdClient = mpd.connect({path: self.mpdSocketPath});
mpdClient.on('end', function(){
if (connectSuccess) console.warn("mpd connection closed");
tryReconnect();
});
mpdClient.on('error', function(){
if (connectSuccess) {
connectSuccess = false;
console.warn("mpd connection error...");
}
tryReconnect();
});
mpdClient.on('ready', function() {
console.log((connectSuccess ? '' : '...') + "mpd connected");
connectSuccess = true;
self.playerServer = new PlayerServer(self.library, mpdClient, authenticate);
self.emit('playerServerInit', self.playerServer);
});
}
function authenticate(pass) {
return self.state.permissions[pass];
}
}
function startServer(self, cb) {
assert.ok(self.httpServer == null);
assert.ok(self.socketIo == null);
self.httpServer = http.createServer(self.app);
self.socketIo = socketio.listen(self.httpServer);
self.socketIo.set('log level', 2);
self.socketIo.sockets.on('connection', onSocketIoConnection);
self.httpServer.listen(self.httpPort, self.httpHost, function() {
self.emit('listening');
console.info("Listening at http://" + self.httpHost + ":" + self.httpPort + "/");
});
self.httpServer.on('close', function() {
console.info("server closed");
});
function onSocketIoConnection(socket){
if (self.playerServer == null) {
console.error("TODO: make PlayerServer not depend on other bullshit so that this works when we have to reconnect. (refresh the browser)");
return;
}
var client = self.playerServer.createClient(socket, self.state.default_permissions);
self.emit('socketConnect', client);
}
}
function startMpd(self, cb){
console.info("starting mpd", self.state.mpd_exe_path);
var args = ['--no-daemon', self.mpdConfPath];
var opts = {
stdio: 'inherit',
detached: true,
};
var child = spawn(self.state.mpd_exe_path, args, opts);
cb();
}
function restartMpd(self, cb) {
mkdirp(self.mpdConf.playlistDirectory(), function(err) {
if (err) return cb(err);
fs.readFile(self.mpdPidFile, {encoding: 'utf8'}, function(err, pidStr) {
if (err && err.code === 'ENOENT') {
startMpd(self, cb);
return;
} else if (err) {
cb(err);
return;
}
var pid = parseInt(pidStr, 10);
console.info("killing mpd", pid);
var killer = new Killer(pid);
killer.on('error', function(err) {
cb(err);
});
killer.on('end', function() {
startMpd(self, cb);
});
killer.kill();
});
});
}
function saveState(self, cb) {
self.emit('aboutToSaveState', self.state);
process.nextTick(function() {
var data = JSON.stringify(self.state, null, 4);
fs.writeFile(self.stateFile, data, function(err) {
if (err) {
console.error("Error saving state to disk:", err.stack);
}
cb();
});
});
}
function restoreState(self, cb) {
fs.readFile(self.stateFile, 'utf8', function(err, data) {
if (err && err.code === 'ENOENT') {
console.warn("No state file. Creating a new one.");
} else if (err) {
return cb(err);
} else {
var loadedState;
try {
loadedState = JSON.parse(data);
} catch (err) {
cb(new Error("state file contains invalid JSON: " + err.message));
return;
}
if (loadedState.state_version !== STATE_VERSION) {
return cb(new Error("State version is " + loadedState.state_version +
" but should be " + STATE_VERSION));
}
self.state = loadedState;
}
self.emit('stateRestored', self.state);
cb();
});
}
function initState(self, cb) {
which('mpd', function(err, mpdExe){
if (err) {
// it's okay. this was just a good default.
console.warn("Unable to find mpd binary in path:", err.stack);
}
self.state = {
state_version: STATE_VERSION,
mpd_exe_path: mpdExe,
status: {},
mpd_conf: self.mpdConf.state,
permissions: {},
default_permissions: DEFAULT_PERMISSIONS
};
self.emit("stateInitialized");
cb();
});
}
function start(self, options) {
self.httpHost = options.host || "0.0.0.0";
self.httpPort = options.port || 16242;
self.on('stateRestored', function() {
self.library = new Library(self.mpdConf.state.music_directory);
self.library.startScan();
});
self.app.use(express.static(path.join(__dirname, '../public')));
self.app.use(express.static(path.join(__dirname, '../src/public')));
for (var pluginName in plugins) {
plugins[pluginName](self, options);
}
async.series([
self.initState.bind(self),
self.makeRunDir.bind(self),
self.restoreState.bind(self),
self.writeMpdConf.bind(self),
self.restartMpd.bind(self),
], function(err) {
assert.ifError(err);
self.connectToMpd();
self.startServer();
});
}
function noop() {}

View file

@ -1,64 +1,54 @@
var POLL_INTERVAL, ESCALATE_TIMEOUT, ERROR_TIMEOUT, Killer;
POLL_INTERVAL = 100;
ESCALATE_TIMEOUT = 3000;
ERROR_TIMEOUT = 2000;
module.exports = Killer = (function(superclass){
Killer.displayName = 'Killer';
var prototype = extend$(Killer, superclass).prototype, constructor = Killer;
function Killer(pid){
var this$ = this instanceof ctor$ ? this : new ctor$;
this$.pid = pid;
return this$;
} function ctor$(){} ctor$.prototype = prototype;
prototype.kill = function(){
this.interval = setInterval(bind$(this, 'check'), POLL_INTERVAL);
this.sig_kill_timeout = setTimeout(bind$(this, 'escalate'), ESCALATE_TIMEOUT);
this.sig = "SIGTERM";
};
prototype.check = function(){
var e;
try {
process.kill(this.pid, this.sig);
} catch (e$) {
e = e$;
this.clean();
if (e.code === 'ESRCH') {
this.emit('end');
} else {
this.emit('error', e);
}
}
};
prototype.clean = function(){
if (this.interval != null) {
clearInterval(this.interval);
}
this.interval = null;
if (this.sig_kill_timeout != null) {
clearTimeout(this.sig_kill_timeout);
}
this.sig_kill_timeout = null;
if (this.error_timeout != null) {
clearTimeout(this.error_timeout);
}
this.error_timeout = null;
};
prototype.escalate = function(){
this.sig = "SIGKILL";
this.error_timeout = setTimeout(bind$(this, 'giveUp'), ERROR_TIMEOUT);
};
prototype.giveUp = function(){
this.clean();
this.emit('error', new Error("Unable to kill " + this.pid + ": timeout"));
};
return Killer;
}(require('events').EventEmitter));
function extend$(sub, sup){
function fun(){} fun.prototype = (sub.superclass = sup).prototype;
(sub.prototype = new fun).constructor = sub;
if (typeof sup.extended == 'function') sup.extended(sub);
return sub;
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = Killer;
var POLL_INTERVAL = 100;
var ESCALATE_TIMEOUT = 3000;
var ERROR_TIMEOUT = 2000;
util.inherits(Killer, EventEmitter);
function Killer(pid) {
EventEmitter.call(this);
this.pid = pid;
}
function bind$(obj, key){
return function(){ return obj[key].apply(obj, arguments) };
}
Killer.prototype.kill = function() {
this.interval = setInterval(this.check.bind(this), POLL_INTERVAL);
this.sig_kill_timeout = setTimeout(this.escalate.bind(this), ESCALATE_TIMEOUT);
this.sig = "SIGTERM";
};
Killer.prototype.check = function() {
try {
process.kill(this.pid, this.sig);
} catch (err) {
this.clean();
if (err.code === 'ESRCH') {
this.emit('end');
} else {
this.emit('error', err);
}
}
};
Killer.prototype.clean = function() {
if (this.interval != null) clearInterval(this.interval);
this.interval = null;
if (this.sig_kill_timeout != null) clearTimeout(this.sig_kill_timeout);
this.sig_kill_timeout = null;
if (this.error_timeout != null) clearTimeout(this.error_timeout);
this.error_timeout = null;
};
Killer.prototype.escalate = function() {
this.sig = "SIGKILL";
this.error_timeout = setTimeout(this.giveUp.bind(this), ERROR_TIMEOUT);
};
Killer.prototype.giveUp = function() {
this.clean();
this.emit('error', new Error("Unable to kill " + this.pid + ": timeout"));
};

View file

@ -12,17 +12,11 @@ var MusicMetadataParser = require('musicmetadata-superjoe30');
module.exports = Library;
util.inherits(Library, EventEmitter);
function Library(bus) {
function Library(musicLibPath) {
EventEmitter.call(this);
this.bus = bus;
this.bus.on('save_state', this.onSaveState.bind(this));
this.musicLibPath = musicLibPath;
}
Library.prototype.onSaveState = function(state){
this.music_lib_path = state.mpd_conf.music_directory;
this.startScan();
};
Library.prototype.get_library = function(cb){
if (this.scan_complete) {
cb(this.library);
@ -43,14 +37,14 @@ function startScan(self) {
var start_time = new Date();
var pend = new Pend();
pend.max = 20;
var musicPath = maybeAddTrailingSlash(self.music_lib_path);
var musicPath = maybeAddTrailingSlash(self.musicLibPath);
var walker = walk.walk(musicPath);
walker.on('file', function(filename, stat) {
if (ignoreFile(filename)) return;
pend.go(function(cb) {
var stream = fs.createReadStream(filename);
var parser = new MusicMetadataParser(stream);
var localFile = path.relative(self.music_lib_path, filename);
var localFile = path.relative(self.musicLibPath, filename);
parser.on('metadata', function(metadata) {
self.library[localFile] = {
file: localFile,

View file

@ -1,64 +1,103 @@
var path, osenv, MpdConf;
path = require('path');
osenv = require('osenv');
module.exports = MpdConf = (function(){
MpdConf.displayName = 'MpdConf';
var prototype = MpdConf.prototype, constructor = MpdConf;
function MpdConf(state){
var this$ = this instanceof ctor$ ? this : new ctor$;
this$.state = state;
if (this$.state == null) {
this$.setDefaultState();
}
return this$;
} function ctor$(){} ctor$.prototype = prototype;
prototype.setDefaultState = function(){
this.state = {};
this.state.audio_httpd = {
format: 'ogg',
quality: 6,
port: 16243
};
this.state.audio_pulse = null;
this.state.audio_alsa = null;
this.state.audio_oss = null;
this.state.run_dir = null;
this.state.music_directory = path.join(osenv.home(), 'music');
var path = require('path');
var osenv = require('osenv');
module.exports = MpdConf;
function MpdConf(state) {
this.state = state;
if (this.state == null) this.setDefaultState();
}
MpdConf.prototype.setDefaultState = function(){
this.state = {};
this.state.audio_httpd = {
format: 'ogg',
quality: 6,
port: 16243
};
prototype.playlistDirectory = function(){
return path.join(this.state.run_dir, "playlists");
};
prototype.setRunDir = function(it){
this.state.run_dir = path.resolve(it);
};
prototype.toMpdConf = function(){
var audio_outputs, quality, bitrate, encoder_value;
audio_outputs = [];
if (this.state.audio_httpd != null) {
if (this.state.audio_httpd.format === 'ogg') {
quality = "quality \"" + this.state.audio_httpd.quality + "\"";
bitrate = "";
encoder_value = "vorbis";
} else if (this.state.audio_httpd.format === 'mp3') {
quality = "";
bitrate = "bitrate \"" + this.state.audio_httpd.bitrate + "\"";
encoder_value = "lame";
}
audio_outputs.push("audio_output {\n type \"httpd\"\n name \"Groove Basin (httpd)\"\n encoder \"" + encoder_value + "\"\n port \"" + this.state.audio_httpd.port + "\"\n bind_to_address \"0.0.0.0\"\n " + quality + "\n " + bitrate + "\n format \"44100:16:2\"\n max_clients \"0\"\n}");
this.state.audio_pulse = null;
this.state.audio_alsa = null;
this.state.audio_oss = null;
this.state.run_dir = null;
this.state.music_directory = path.join(osenv.home(), 'music');
};
MpdConf.prototype.playlistDirectory = function(){
return path.join(this.state.run_dir, "playlists");
};
MpdConf.prototype.setRunDir = function(it){
this.state.run_dir = path.resolve(it);
};
MpdConf.prototype.toMpdConf = function(){
var quality, bitrate, encoder_value;
var audio_outputs = [];
if (this.state.audio_httpd != null) {
if (this.state.audio_httpd.format === 'ogg') {
quality = "quality \"" + this.state.audio_httpd.quality + "\"";
bitrate = "";
encoder_value = "vorbis";
} else if (this.state.audio_httpd.format === 'mp3') {
quality = "";
bitrate = "bitrate \"" + this.state.audio_httpd.bitrate + "\"";
encoder_value = "lame";
}
if (this.state.audio_pulse != null) {
audio_outputs.push("audio_output {\n type \"pulse\"\n name \"Groove Basin (pulse)\"\n}");
}
if (this.state.audio_alsa != null) {
audio_outputs.push("audio_output {\n type \"alsa\"\n name \"Groove Basin (alsa)\"\n}");
}
if (this.state.audio_oss != null) {
audio_outputs.push("audio_output {\n type \"oss\"\n name \"Groove Basin (oss)\"\n}");
}
if (!audio_outputs.length) {
audio_outputs.push("audio_output {\n type \"null\"\n name \"Groove Basin (null)\"\n}");
}
return ("music_directory \"" + this.state.music_directory + "\"\nplaylist_directory \"" + this.playlistDirectory() + "\"\ndb_file \"" + path.join(this.state.run_dir, "mpd.music.db") + "\"\nlog_file \"" + path.join(this.state.run_dir, "mpd.log") + "\"\npid_file \"" + path.join(this.state.run_dir, "mpd.pid") + "\"\nstate_file \"" + path.join(this.state.run_dir, "mpd.state") + "\"\nsticker_file \"" + path.join(this.state.run_dir, "mpd.sticker.db") + "\"\nbind_to_address \"" + path.join(this.state.run_dir, "mpd.socket") + "\"\ngapless_mp3_playback \"yes\"\nauto_update \"yes\"\ndefault_permissions \"read,add,control,admin\"\nreplaygain \"album\"\nvolume_normalization \"yes\"\nmax_command_list_size \"16384\"\nmax_connections \"10\"\nmax_output_buffer_size \"16384\"\nid3v1_encoding \"UTF-8\"\n" + audio_outputs.join("\n") + "\n# this socket is just for debugging with telnet\nbind_to_address \"localhost\"\nport \"16244\"") + "\n";
};
return MpdConf;
}());
audio_outputs.push("audio_output {\n" +
" type \"httpd\"\n" +
" name \"Groove Basin (httpd)\"\n" +
" encoder \"" + encoder_value + "\"\n" +
" port \"" + this.state.audio_httpd.port + "\"\n" +
" bind_to_address \"0.0.0.0\"\n" +
" " + quality + "\n" +
" " + bitrate + "\n" +
" format \"44100:16:2\"\n" +
" max_clients \"0\"\n" +
"}");
}
if (this.state.audio_pulse != null) {
audio_outputs.push("audio_output {\n" +
" type \"pulse\"\n" +
" name \"Groove Basin (pulse)\"\n" +
"}");
}
if (this.state.audio_alsa != null) {
audio_outputs.push("audio_output {\n" +
" type \"alsa\"\n" +
" name \"Groove Basin (alsa)\"\n" +
"}");
}
if (this.state.audio_oss != null) {
audio_outputs.push("audio_output {\n" +
" type \"oss\"\n" +
" name \"Groove Basin (oss)\"\n" +
"}");
}
if (!audio_outputs.length) {
audio_outputs.push("audio_output {\n" +
" type \"null\"\n" +
" name \"Groove Basin (null)\"\n" +
"}");
}
return "music_directory \"" + this.state.music_directory + "\"\n" +
"playlist_directory \"" + this.playlistDirectory() + "\"\n" +
"db_file \"" + path.join(this.state.run_dir, "mpd.music.db") + "\"\n" +
"log_file \"" + path.join(this.state.run_dir, "mpd.log") + "\"\n" +
"pid_file \"" + path.join(this.state.run_dir, "mpd.pid") + "\"\n" +
"state_file \"" + path.join(this.state.run_dir, "mpd.state") + "\"\n" +
"sticker_file \"" + path.join(this.state.run_dir, "mpd.sticker.db") + "\"\n" +
"bind_to_address \"" + path.join(this.state.run_dir, "mpd.socket") + "\"\n" +
"gapless_mp3_playback \"yes\"\n" +
"auto_update \"yes\"\n" +
"default_permissions \"read,add,control,admin\"\n" +
"replaygain \"album\"\n" +
"volume_normalization \"yes\"\n" +
"max_command_list_size \"16384\"\n" +
"max_connections \"10\"\n" +
"max_output_buffer_size \"16384\"\nid3v1_encoding \"UTF-8\"\n" +
audio_outputs.join("\n") +
"\n" +
"# this socket is just for debugging with telnet\n" +
"bind_to_address \"localhost\"\n" +
"port \"16244\"\n";
};

View file

@ -1,226 +0,0 @@
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var trackNameFromFile = require('./futils').trackNameFromFile;
module.exports = MpdParser;
function noop(arg){
var err = arg.err;
if (err) throw err;
}
function parseMaybeUndefNumber(n){
n = parseInt(n, 10);
if (isNaN(n)) {
n = null;
}
return n;
}
function splitOnce(line, separator){
var index;
index = line.indexOf(separator);
return [line.substr(0, index), line.substr(index + separator.length)];
}
function parseMpdObject(msg){
var o, i$, ref$, line, len$, ref1$, key, val;
o = {};
for (i$ = 0, len$ = (ref$ = (fn$())).length; i$ < len$; ++i$) {
ref1$ = ref$[i$], key = ref1$[0], val = ref1$[1];
o[key] = val;
}
return o;
function fn$(){
var i$, ref$, len$, results$ = [];
for (i$ = 0, len$ = (ref$ = msg.split("\n")).length; i$ < len$; ++i$) {
line = ref$[i$];
results$.push(splitOnce(line, ": "));
}
return results$;
}
}
function parseWithSepField(msg, sep_field, skip_fields, flush){
var current_obj, i$, ref$, len$, line, ref1$, key, value;
if (msg === "") {
return [];
}
current_obj = null;
function flushCurrentObj(){
if (current_obj != null) {
flush(current_obj);
}
current_obj = {};
}
for (i$ = 0, len$ = (ref$ = msg.split("\n")).length; i$ < len$; ++i$) {
line = ref$[i$];
ref1$ = splitOnce(line, ': '), key = ref1$[0], value = ref1$[1];
if (key in skip_fields) {
continue;
}
if (key === sep_field) {
flushCurrentObj();
}
current_obj[key] = value;
}
return flushCurrentObj();
}
function parseMpdTracks(msg, flush){
return parseWithSepField(msg, 'file', {
'directory': true
}, flush);
}
function parseMsgToTrackObjects(msg) {
var tracks = [];
parseMpdTracks(msg, function(mpd_track){
var ref$;
var artist_name = ((ref$ = mpd_track.Artist) != null ? ref$ : "").trim();
var track = {
file: mpd_track.file,
name: mpd_track.Title || trackNameFromFile(mpd_track.file),
artist_name: artist_name,
artist_disambiguation: "",
album_artist_name: mpd_track.AlbumArtist || artist_name,
album_name: ((ref$ = mpd_track.Album) != null ? ref$ : "").trim(),
track: parseMaybeUndefNumber(mpd_track.Track),
time: parseInt(mpd_track.Time, 10),
year: parseMaybeUndefNumber(mpd_track.Date)
};
tracks.push(track);
});
return tracks;
}
util.inherits(MpdParser, EventEmitter);
function MpdParser(mpd_socket) {
EventEmitter.call(this);
this.mpd_socket = mpd_socket;
this.mpd_socket.on('data', function(data){
this.receive(data);
}.bind(this));
this.buffer = "";
this.msg_handler_queue = [];
this.idling = false;
}
MpdParser.prototype.current_song_and_status_command = "command_list_begin\ncurrentsong\nstatus\ncommand_list_end";
MpdParser.prototype.sendRequest = function(command, cb){
var this$ = this;
cb = cb || noop;
if (command.indexOf("sticker") === -1) {
console.log("sending to mpd:", JSON.stringify(command));
}
if (this.idling) {
this.send("noidle\n");
}
this.sendWithCallback(command, function(response){
cb(this$.parseResponse(command, response));
});
this.sendWithCallback("idle", this.handleIdleResultsLoop.bind(this));
this.idling = true;
};
MpdParser.prototype.parseResponse = function(complete_command, response){
var items, item;
var err = response.err;
var msg = response.msg;
if (err != null) {
return response;
}
if (complete_command === this.current_song_and_status_command) {
return parseMpdObject(msg);
}
var command_name = complete_command.match(/^\S*/)[0];
return {
msg: (function(){
switch (command_name) {
case 'listallinfo':
return parseMsgToTrackObjects(msg);
case 'lsinfo':
return parseMsgToTrackObjects(msg)[0];
case 'status':
return parseMpdObject(msg);
case 'playlistinfo':
items = [];
parseMpdTracks(msg, function(track){
return items.push({
id: parseInt(track.Id, 10),
file: track.file,
});
});
return items;
case 'currentsong':
item = null;
parseMpdTracks(msg, function(track){
return item = {
id: parseInt(track.Id, 10),
pos: parseInt(track.Pos, 10),
file: track.file,
};
});
return item;
default:
return msg;
}
}())
};
};
MpdParser.prototype.send = function(data){
this.mpd_socket.write(data);
};
MpdParser.prototype.handleMessage = function(arg){
this.msg_handler_queue.shift()(arg);
};
MpdParser.prototype.receive = function(data){
var m, msg, line, code, str, err;
this.buffer += data;
for (;;) {
m = this.buffer.match(/^(OK|ACK|list_OK)(.*)$/m);
if (m == null) {
return;
}
msg = this.buffer.substring(0, m.index);
line = m[0], code = m[1], str = m[2];
if (code === "ACK") {
this.emit('error', str);
err = new Error(str);
this.handleMessage({
err: err
});
} else if (line.indexOf("OK MPD") !== 0) {
this.handleMessage({
msg: msg
});
}
this.buffer = this.buffer.substring(msg.length + line.length + 1);
}
};
MpdParser.prototype.handleIdleResults = function(msg){
var systems, i$, ref$, len$, system;
systems = [];
for (i$ = 0, len$ = (ref$ = msg.trim().split("\n")).length; i$ < len$; ++i$) {
system = ref$[i$];
if (system.length > 0) {
systems.push(system.substring(9));
}
}
if (systems.length) {
this.emit('status', systems);
}
};
MpdParser.prototype.sendWithCallback = function(cmd, cb){
cb = cb || noop;
this.msg_handler_queue.push(cb);
this.send(cmd + "\n");
};
MpdParser.prototype.handleIdleResultsLoop = function(arg){
var err, msg;
err = arg.err, msg = arg.msg;
if (err) {
throw err;
}
this.handleIdleResults(msg);
if (this.msg_handler_queue.length === 0) {
this.sendWithCallback("idle", this.handleIdleResultsLoop.bind(this));
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,6 @@
var util = require('util');
var mpd = require('mpd');
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
module.exports = PlayerServer;
@ -30,35 +32,23 @@ var command_permissions = {
stop: 'control'
};
var refreshCommands = ['currentsong', 'status'];
util.inherits(PlayerServer, EventEmitter);
function PlayerServer(library, parser, authenticate) {
function PlayerServer(library, mpdClient, authenticate) {
var self = this;
self.library = library;
self.parser = parser;
self.mpdClient = mpdClient;
self.authenticate = authenticate;
self.parser.on('status', function(systems){
var i$, len$, system;
console.log('changed systems:', systems);
var refresh_mpd_status = false;
var client_systems = [];
for (i$ = 0, len$ = systems.length; i$ < len$; ++i$) {
system = systems[i$];
if (system === 'stored_playlist' || system === 'sticker') {
client_systems.push(system);
} else if (system === 'playlist' || system === 'player' || system === 'mixer' || system === 'options') {
refresh_mpd_status = true;
}
}
if (client_systems.length) {
self.emit('status', client_systems);
}
if (refresh_mpd_status) {
refreshMpdStatus(self);
}
});
self.parser.on('error', function(msg){
console.error("mpd error:", msg);
self.mpdClient.on('system', function(system){
console.log('changed system:', system);
});
self.mpdClient.on('system-stored_playlist', clientSystemChanged);
self.mpdClient.on('system-sticker', clientSystemChanged);
self.mpdClient.on('system-playlist', doRefreshMpdStatus);
self.mpdClient.on('system-player', doRefreshMpdStatus);
self.mpdClient.on('system-mixer', doRefreshMpdStatus);
self.mpdClient.on('system-options', doRefreshMpdStatus);
self.playlist = {};
self.current_id = null;
self.repeat = {
@ -68,9 +58,15 @@ function PlayerServer(library, parser, authenticate) {
self.is_playing = false;
self.mpd_is_playing = false;
self.mpd_should_be_playing_id = null;
self.parser.sendRequest("clear");
self.mpdClient.sendCommand("clear");
self.track_start_date = null;
self.paused_time = 0;
function clientSystemChanged(system) {
self.emit('status', [system]);
}
function doRefreshMpdStatus(system) {
refreshMpdStatus(self);
}
}
PlayerServer.prototype.createClient = function(socket, permissions) {
var self = this;
@ -79,8 +75,7 @@ PlayerServer.prototype.createClient = function(socket, permissions) {
socket.on('request', function(request){
request = JSON.parse(request);
self.request(client, request.cmd, function(arg){
var response;
response = JSON.stringify(import$({
var response = JSON.stringify(import$({
callback_id: request.callback_id
}, arg));
socket.emit('PlayerResponse', response);
@ -107,160 +102,141 @@ PlayerServer.prototype.createClient = function(socket, permissions) {
});
return client;
};
PlayerServer.prototype.request = function(client, request, cb){
var self = this;
var name, suppress_reply, reply_object, id, ref$, item, i$, len$, sort_key, commands, command;
if (cb == null) cb = function(){};
function check_permission(name){
function requestObject(self, client, request, cb) {
var name = request.name;
if (!checkPermission(name)) return;
var suppress_reply = false;
var reply_object = null;
var id, item, ref$, i$, len$, sort_key;
switch (name) {
case 'listallinfo':
suppress_reply = true;
self.library.get_library(function(library){
return cb({
msg: library
});
});
break;
case 'password':
self.emit('password', request.password);
break;
case 'playlistinfo':
reply_object = self.playlist;
break;
case 'currentsong':
reply_object = self.current_id;
break;
case 'status':
reply_object = {
volume: null,
repeat: self.repeat.repeat,
single: self.repeat.single,
state: self.is_playing ? 'play' : 'pause',
track_start_date: self.track_start_date,
paused_time: self.paused_time,
};
break;
case 'addid':
for (id in (ref$ = request.items)) {
item = ref$[id];
self.playlist[id] = {
file: item.file,
sort_key: item.sort_key,
is_random: item.is_random,
};
}
playlistChanged(self);
break;
case 'deleteid':
for (i$ = 0, len$ = (ref$ = request.ids).length; i$ < len$; ++i$) {
id = ref$[i$];
delete self.playlist[id];
}
playlistChanged(self);
break;
case 'move':
for (id in (ref$ = request.items)) {
sort_key = ref$[id].sort_key;
self.playlist[id].sort_key = sort_key;
}
playlistChanged(self);
break;
case 'clear':
self.playlist = {};
playlistChanged(self);
break;
case 'shuffle':
break;
case 'repeat':
self.repeat.repeat = request.repeat;
self.repeat.single = request.single;
break;
case 'play':
if (self.current_id == null) self.current_id = findNext(self.playlist, null);
self.is_playing = true;
playlistChanged(self);
break;
case 'pause':
if (self.is_playing) {
self.is_playing = false;
self.paused_time = (new Date() - self.track_start_date) / 1000;
playlistChanged(self);
}
break;
case 'stop':
self.is_playing = false;
playlistChanged(self, {
seekto: 0
});
break;
case 'seek':
playlistChanged(self, {
seekto: request.pos
});
break;
case 'next':
case 'previous':
break;
case 'playid':
self.current_id = request.track_id;
self.is_playing = true;
playlistChanged(self, {
seekto: 0
});
break;
default:
throw new Error("invalid command " + JSON.stringify(name));
}
if (!suppress_reply) {
if (reply_object != null) {
cb({
msg: reply_object
});
} else {
cb({});
}
}
function checkPermission(name) {
var permission = command_permissions[name];
if (permission === null) {
return true;
}
if (client.permissions[permission]) {
return true;
}
var err = "command " + JSON.stringify(name) + " requires permission " + JSON.stringify(permission);
if (permission === null) return true;
if (client.permissions[permission]) return true;
var err = "command " + JSON.stringify(name) + " requires permission " +
JSON.stringify(permission);
console.warn("permissions error:", err);
cb({
err: err
});
cb({ err: err });
return false;
}
if (typeof request === 'object') {
name = request.name;
if (!check_permission(name)) {
return;
}
suppress_reply = false;
reply_object = null;
switch (name) {
case 'listallinfo':
suppress_reply = true;
self.library.get_library(function(library){
return cb({
msg: library
});
});
break;
case 'password':
self.emit('password', request.password);
break;
case 'playlistinfo':
reply_object = self.playlist;
break;
case 'currentsong':
reply_object = self.current_id;
break;
case 'status':
reply_object = {
volume: null,
repeat: self.repeat.repeat,
single: self.repeat.single,
state: self.is_playing ? 'play' : 'pause',
track_start_date: self.track_start_date,
paused_time: self.paused_time,
};
break;
case 'addid':
for (id in (ref$ = request.items)) {
item = ref$[id];
self.playlist[id] = {
file: item.file,
sort_key: item.sort_key,
is_random: item.is_random,
};
}
playlistChanged(self);
break;
case 'deleteid':
for (i$ = 0, len$ = (ref$ = request.ids).length; i$ < len$; ++i$) {
id = ref$[i$];
delete self.playlist[id];
}
playlistChanged(self);
break;
case 'move':
for (id in (ref$ = request.items)) {
sort_key = ref$[id].sort_key;
self.playlist[id].sort_key = sort_key;
}
playlistChanged(self);
break;
case 'clear':
self.playlist = {};
playlistChanged(self);
break;
case 'shuffle':
break;
case 'repeat':
self.repeat.repeat = request.repeat;
self.repeat.single = request.single;
break;
case 'play':
if (self.current_id == null) self.current_id = findNext(self.playlist, null);
self.is_playing = true;
playlistChanged(self);
break;
case 'pause':
if (self.is_playing) {
self.is_playing = false;
self.paused_time = (new Date() - self.track_start_date) / 1000;
playlistChanged(self);
}
break;
case 'stop':
self.is_playing = false;
playlistChanged(self, {
seekto: 0
});
break;
case 'seek':
playlistChanged(self, {
seekto: request.pos
});
break;
case 'next':
case 'previous':
break;
case 'playid':
self.current_id = request.track_id;
self.is_playing = true;
playlistChanged(self, {
seekto: 0
});
break;
default:
throw new Error("invalid command " + JSON.stringify(name));
}
if (!suppress_reply) {
if (reply_object != null) {
cb({
msg: reply_object
});
} else {
cb({});
}
}
} else {
name = request.split(/\s/)[0];
if (name === 'command_list_begin') {
commands = request.split('\n');
commands.shift();
commands.pop();
for (i$ = 0, len$ = commands.length; i$ < len$; ++i$) {
command = commands[i$];
name = command.split(/\s/)[0];
if (!check_permission(name)) {
return;
}
}
} else {
if (!check_permission(name)) {
return;
}
}
self.parser.sendRequest(request, cb);
}
PlayerServer.prototype.request = function(client, request, cb){
cb = cb || noop;
if (typeof request !== 'object') {
console.warn("ignoring invalid command:", request);
cb({err: "invalid command: " + JSON.stringify(request)});
return;
}
requestObject(this, client, request, cb);
};
function operatorCompare(a, b) {
@ -316,7 +292,7 @@ function playlistChanged(self, o) {
if (self.is_playing) {
if (self.current_id !== self.mpd_should_be_playing_id) {
commands.push("clear");
commands.push("addid \"" + qEscape(self.playlist[self.current_id].file) + "\"");
commands.push(mpd.cmd("addid", [self.playlist[self.current_id].file]));
commands.push("play");
self.mpd_should_be_playing_id = self.current_id;
if (o.seekto === 0) {
@ -327,7 +303,7 @@ function playlistChanged(self, o) {
self.track_start_date = new Date();
}
if (o.seekto != null) {
var seek_command = "seek 0 " + Math.round(o.seekto);
var seek_command = mpd.cmd("seek", [0, Math.round(o.seekto)]);
if (commands[commands.length - 1] === "play") {
commands.splice(commands.length - 1, 0, seek_command);
} else {
@ -348,13 +324,7 @@ function playlistChanged(self, o) {
if (self.paused_time == null) self.paused_time = 0;
}
if (commands.length) {
if (commands.length > 1) {
commands.unshift("command_list_begin");
commands.push("command_list_end");
}
self.parser.sendRequest(commands.join("\n"));
}
if (commands.length) self.mpdClient.sendCommands(commands);
disambiguateSortKeys(self);
@ -367,9 +337,6 @@ function import$(obj, src){
return obj;
}
function qEscape(str){
return str.toString().replace(/"/g, '\\"');
}
function findNext(object, from_id){
var ref$;
var from_key = (ref$ = object[from_id]) != null ? ref$.sort_key : void 8;
@ -385,7 +352,12 @@ function findNext(object, from_id){
return result;
}
function refreshMpdStatus(self) {
self.parser.sendRequest(self.parser.current_song_and_status_command, function(o){
self.mpdClient.sendCommands(refreshCommands, function(err, msg) {
if (err) {
console.error("mpd status error:", err.stack);
return;
}
var o = parseMpdObject(msg);
var mpd_was_playing = self.mpd_is_playing;
self.mpd_is_playing = o.state === 'play';
if (self.mpd_should_be_playing_id != null && mpd_was_playing && !self.mpd_is_playing) {
@ -394,3 +366,15 @@ function refreshMpdStatus(self) {
}
});
}
function parseMpdObject(msg) {
var o = {};
msg.split("\n").forEach(function(line) {
var index = line.indexOf(": ");
var key = line.substr(0, index);
var value = line.substr(index + 2);
o[key] = value;
});
return o;
}
function noop() {}

View file

@ -1,33 +0,0 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = Plugin;
util.inherits(Plugin, EventEmitter);
function Plugin() {
var self = this;
self.mpd = null;
self.is_enabled = true;
self.checkEnabledMiddleware = function(req, resp, next){
if (self.is_enabled) {
next();
} else {
resp.writeHead(500, {
'content-type': 'text/json'
});
resp.end(JSON.stringify({
success: false,
reason: "DisabledEndpoint"
}));
}
};
self.whenEnabled = function(middleware){
return function(req, res, next){
if (self.is_enabled) {
middleware(req, res, next);
} else {
next();
}
};
};
}

View file

@ -1,20 +1,14 @@
var Plugin = require('../plugin');
var util = require('util');
var CHATS_LIMIT = 100;
var USER_NAME_LIMIT = 20;
module.exports = Chat;
util.inherits(Chat, Plugin);
function Chat(bus) {
Plugin.call(this);
function Chat(gb) {
this.gb = gb;
this.users = [];
bus.on('save_state', saveState.bind(this));
bus.on('restore_state', restoreState.bind(this));
bus.on('socket_connect', onSocketConnection.bind(this));
var scrubStaleUserNames_bound = scrubStaleUserNames.bind(this);
bus.on('mpd', function(mpd) { mpd.on('chat', scrubStaleUserNames); });
gb.on('aboutToSaveState', saveState.bind(this));
gb.on('stateRestored', restoreState.bind(this));
gb.on('socketConnect', onSocketConnection.bind(this));
}
function restoreState(state) {
@ -47,7 +41,7 @@ function onSocketConnection(socket) {
if (self.chats.length > CHATS_LIMIT) {
self.chats.splice(0, self.chats.length - CHATS_LIMIT);
}
self.emit('status_changed');
self.gb.saveAndSendStatus();
});
socket.on('SetUserName', function(data){
var user_name;
@ -58,7 +52,7 @@ function onSocketConnection(socket) {
} else {
delete self.user_names[user_id];
}
self.emit('status_changed');
self.gb.saveAndSendStatus();
});
socket.on('disconnect', function(){
var res$, i$, ref$, len$, id;
@ -70,9 +64,9 @@ function onSocketConnection(socket) {
}
}
self.users = res$;
scrubStaleUserNames.bind(self)();
scrubStaleUserNames.call(self);
});
self.emit('status_changed');
self.gb.saveAndSendStatus();
}
function scrubStaleUserNames() {
@ -91,5 +85,5 @@ function scrubStaleUserNames() {
delete this.user_names[user_id];
}
}
this.emit('status_changed');
this.gb.saveAndSendStatus();
}

View file

@ -1,39 +1,38 @@
var Plugin = require('../plugin');
var util = require('util');
var fs = require('fs');
var path = require('path');
module.exports = Delete;
util.inherits(Delete, Plugin);
function Delete(bus) {
var self = this;
Plugin.call(self);
bus.on('save_state', function(state) {
state.status.delete_enabled = this.is_enabled;
function Delete(gb) {
this.gb = gb;
this.is_enabled = true;
setup(this);
}
function setup(self) {
self.gb.on('aboutToSaveState', function(state) {
state.status.delete_enabled = self.is_enabled;
});
bus.on('mpd', function(mpd){
self.mpd = mpd;
});
bus.on('socket_connect', onSocketConnection.bind(self));
bus.on('restore_state', function(state) {
if ((self.music_directory = state.mpd_conf.music_directory) == null) {
self.gb.on('socketConnect', onSocketConnection.bind(self));
self.gb.on('stateRestored', function(state) {
self.music_directory = state.mpd_conf.music_directory;
if (self.music_directory == null) {
self.is_enabled = false;
console.warn("No music directory set. Delete disabled.");
return;
}
});
}
function onSocketConnection(client) {
var self = this;
client.on('DeleteFromLibrary', function(data){
var files, file;
client.on('DeleteFromLibrary', function(data) {
if (!client.permissions.admin) {
console.warn("User without admin permission trying to delete songs");
return;
}
files = JSON.parse(data);
file = null;
var files = JSON.parse(data);
var file = null;
function next(err){
var file;
if (err) {
@ -42,9 +41,10 @@ function onSocketConnection(client) {
console.info("deleted " + file);
}
if ((file = files.shift()) == null) {
self.mpd.scanFiles(files);
self.gb.rescanLibrary();
} else {
fs.unlink(path.join(self.music_directory, file), next);
// TODO remove from library?
}
}
next();

View file

@ -1,5 +1,3 @@
var Plugin = require('../plugin');
var util = require('util');
var fs = require('fs');
var zipstream = require('zipstream');
var path = require('path');
@ -9,12 +7,13 @@ var findit = require('findit');
module.exports = Download;
util.inherits(Download, Plugin);
function Download(bus) {
function Download(gb) {
var self = this;
Plugin.apply(self);
self.is_enabled = false;
self.is_ready = false;
console.error("TODO: fix download plugin (download plugin disabled)");
return;
bus.on('save_state', function(state){
return state.status.download_enabled = self.is_enabled;
});

View file

@ -1,37 +1,36 @@
var Plugin = require('../plugin');
var util = require('util');
var history_size = 10;
var future_size = 10;
var LAST_QUEUED_STICKER = "groovebasin.last-queued";
module.exports = DynamicMode;
util.inherits(DynamicMode, Plugin);
function DynamicMode(bus) {
var self = this;
Plugin.call(self);
self.previous_ids = {};
self.got_stickers = false;
self.last_queued = {};
bus.on('save_state', bind$(self, 'saveState'));
bus.on('restore_state', bind$(self, 'restoreState'));
bus.on('mpd', bind$(self, 'setMpd'));
bus.on('socket_connect', bind$(self, 'onSocketConnection'));
function DynamicMode(gb) {
this.gb = gb;
this.previous_ids = {};
this.got_stickers = false;
this.last_queued = {};
this.is_enabled = false;
console.error("TODO: update dynamic mode to work directly with PlayerServer");
return;
this.gb.on('aboutToSaveState', this.saveState.bind(this));
this.gb.on('stateRestored', this.restoreState.bind(this));
this.gb.on('playerServerInit', this.playerServerInit.bind(this));
this.gb.on('socketConnect', this.onSocketConnection.bind(this));
}
DynamicMode.prototype.restoreState = function(state){
var ref$;
this.is_on = (ref$ = state.status.dynamic_mode) != null ? ref$ : false;
this.is_on = state.status.dynamic_mode == null ? false : state.status.dynamic_mode;
};
DynamicMode.prototype.saveState = function(state){
state.status.dynamic_mode = this.is_on;
state.status.dynamic_mode_enabled = this.is_enabled;
};
DynamicMode.prototype.setMpd = function(mpd){
this.mpd = mpd;
this.mpd.on('statusupdate', bind$(this, 'checkDynamicMode'));
this.mpd.on('playlistupdate', bind$(this, 'checkDynamicMode'));
this.mpd.on('libraryupdate', bind$(this, 'updateStickers'));
DynamicMode.prototype.playerServerInit = function(){
this.gb.playerServer.on('statusupdate', this.checkDynamicMode.bind(this));
this.gb.playerServer.on('playlistupdate', this.checkDynamicMode.bind(this));
this.gb.playerServer.on('libraryupdate', this.updateStickers.bind(this));
this.updateStickers();
};
DynamicMode.prototype.onSocketConnection = function(socket){
@ -58,7 +57,7 @@ DynamicMode.prototype.onSocketConnection = function(socket){
self.emit('status_changed');
}
});
socket.on('Permissions', bind$(this, 'updateStickers'));
socket.on('Permissions', this.updateStickers.bind(this));
};
DynamicMode.prototype.checkDynamicMode = function(){
var reason;
@ -189,6 +188,3 @@ DynamicMode.prototype.getRandomSongFiles = function(count){
}
return files;
};
function bind$(obj, key){
return function(){ return obj[key].apply(obj, arguments) };
}

View file

@ -1,18 +1,18 @@
var Plugin = require('../plugin');
var util = require('util');
var LastFmNode = require('lastfm').LastFmNode;
module.exports = LastFm;
util.inherits(LastFm, Plugin);
function LastFm(bus) {
var self = this;
Plugin.call(self);
self.previous_now_playing_id = null;
self.last_playing_item = null;
self.playing_start = new Date();
self.playing_time = 0;
self.previous_play_state = null;
console.error("TODO: fix last.fm plugin (disabled)");
return;
setTimeout(bind$(self, 'flushScrobbleQueue'), 120000);
bus.on('save_state', bind$(self, 'saveState'));
bus.on('restore_state', bind$(self, 'restoreState'));

View file

@ -1,20 +1,20 @@
var Plugin = require('../plugin');
var util = require('util');
module.exports = Stream;
util.inherits(Stream, Plugin);
function Stream(bus) {
var self = this;
Plugin.call(self);
self.port = null;
self.format = null;
bus.on('save_state', function(state){
function Stream(gb) {
this.gb = gb;
this.port = null;
this.format = null;
setup(this);
}
function setup(self) {
self.gb.on('aboutToSaveState', function(state) {
state.status.stream_httpd_port = self.port;
state.status.stream_httpd_format = self.format;
});
bus.on('restore_state', function(state){
var ref$;
ref$ = state.mpd_conf.audio_httpd, self.port = ref$.port, self.format = ref$.format;
self.gb.on('stateRestored', function(state) {
var ahttpd = state.mpd_conf.audio_httpd;
self.port = ahttpd.port;
self.format = ahttpd.format;
});
}

View file

@ -1,5 +1,3 @@
var Plugin = require('../plugin');
var util = require('util');
var mkdirp = require('mkdirp');
var fs = require('fs');
var path = require('path');
@ -17,10 +15,11 @@ var multipart = express.multipart({
module.exports = Upload;
util.inherits(Upload, Plugin);
function Upload(bus) {
Plugin.call(this);
function Upload(gb) {
this.is_enabled = false;
console.error("TODO: fix upload plugin (upload disabled)");
return;
bus.on('app', bind$(this, 'setUpRoutes'));
bus.on('mpd', bind$(this, 'setMpd'));
bus.on('save_state', bind$(this, 'saveState'));

View file

@ -1,329 +1,14 @@
var fs = require('fs');
var http = require('http');
var net = require('net');
var socketio = require('socket.io');
var socketio_client = require('socket.io-client');
var express = require('express');
var path = require('path');
var assert = require('assert');
var mkdirp = require('mkdirp');
var PlayerClient = require('./playerclient');
var MpdParser = require('./mpdparser');
var PlayerServer = require('./playerserver');
var async = require('async');
var which = require('which');
var Library = require('./library');
var MpdConf = require('./mpdconf');
var Killer = require('./killer');
var spawn = require('child_process').spawn;
var EventEmitter = require('events').EventEmitter;
if (!process.env.NODE_ENV) process.env.NODE_ENV = "dev";
var HOST = process.env.HOST || "0.0.0.0";
var PORT = parseInt(process.env.PORT, 10) || 16242;
var RUN_DIR = "run";
var MPD_SOCKET_PATH = path.join(RUN_DIR, "mpd.socket");
var STATE_FILE = path.join(RUN_DIR, "state.json");
var MPD_CONF_PATH = path.join(RUN_DIR, "mpd.conf");
var MPD_PID_FILE = path.join(RUN_DIR, "mpd.pid");
var mpd_conf = new MpdConf();
mpd_conf.setRunDir(RUN_DIR);
var player_server = null;
var my_player = null;
var state = null;
var app = null;
var io = null;
var plugins = {
objects: {},
bus: new EventEmitter(),
initialize: function(cb){
var PLUGIN_PATH, this$ = this;
PLUGIN_PATH = path.join(__dirname, "plugins");
fs.readdir(PLUGIN_PATH, function(err, files){
var i$, len$, file, name, Plugin, plugin;
if (err) {
return cb(err);
}
for (i$ = 0, len$ = files.length; i$ < len$; ++i$) {
file = files[i$];
if (!/\.js$/.test(file)) {
continue;
}
name = path.basename(file, ".js");
Plugin = require("./plugins/" + name);
plugin = this$.objects[name] = new Plugin(this$.bus);
plugin.on('state_changed', saveState);
plugin.on('status_changed', saveAndSendStatus);
}
cb();
});
},
featuresList: function(){
var name, ref$, plugin, results$ = [];
for (name in (ref$ = this.objects)) {
plugin = ref$[name];
results$.push([name, plugin.is_enabled]);
}
return results$;
}
};
var library = new Library(plugins.bus);
function makeRunDir(cb){
mkdirp(RUN_DIR, cb);
}
var STATE_VERSION = 4;
var DEFAULT_PERMISSIONS = {
read: true,
add: true,
control: true
};
function initState(cb){
which('mpd', function(err, mpd_exe){
if (err) {
console.warn("Unable to find mpd binary in path: " + err.stack);
}
state = {
state_version: STATE_VERSION,
mpd_exe_path: mpd_exe,
status: {},
mpd_conf: mpd_conf.state,
permissions: {},
default_permissions: DEFAULT_PERMISSIONS
};
cb();
});
}
function startSocketIo(){
var app_server;
app = express();
app.disable('x-powered-by');
app_server = http.createServer(app);
if (io != null) {
try {
io.server.close();
} catch (e$) {}
}
io = socketio.listen(app_server);
io.set('log level', 2);
io.sockets.on('connection', onSocketIoConnection);
app_server.listen(PORT, HOST, function(){
if (typeof process.send === 'function') {
process.send('online');
}
console.info("Listening at http://" + HOST + ":" + PORT);
connectMasterPlayer();
});
app_server.on('close', function(){
console.info("server closed");
});
process.on('message', function(message){
if (message === 'shutdown') {
process.exit(0);
}
});
}
function startPlugins(){
var i$, ref$, len$, ref1$, name, enabled;
console.log('starting plugins');
app.use(express.static(path.join(__dirname, '../public')));
app.use(express.static(path.join(__dirname, '../src/public')));
plugins.bus.emit('app', app);
plugins.bus.emit('mpd', my_player);
plugins.bus.emit('save_state', state);
for (i$ = 0, len$ = (ref$ = plugins.featuresList()).length; i$ < len$; ++i$) {
ref1$ = ref$[i$], name = ref1$[0], enabled = ref1$[1];
if (enabled) {
console.info(name + " is enabled.");
} else {
console.warn(name + " is disabled.");
}
}
}
function oncePerEventLoopFunc(fn){
var queued, cbs;
queued = false;
cbs = [];
return function(cb){
if (cb != null) {
cbs.push(cb);
}
if (queued) {
return;
}
queued = true;
process.nextTick(function(){
queued = false;
fn(function(){
var i$, ref$, len$, cb;
for (i$ = 0, len$ = (ref$ = cbs).length; i$ < len$; ++i$) {
cb = ref$[i$];
cb.apply(this, arguments);
}
});
});
};
}
var saveState = oncePerEventLoopFunc(function(cb){
plugins.bus.emit('save_state', state);
fs.writeFile(STATE_FILE, JSON.stringify(state, null, 4), "utf8", function(err){
if (err) {
console.error("Error saving state to disk: " + err.stack);
}
cb(err);
});
var GrooveBasin = require('./groovebasin');
var gb = new GrooveBasin();
gb.on('listening', function() {
if (process.send) process.send('online');
});
function restoreState(cb){
fs.readFile(STATE_FILE, 'utf8', function(err, data){
var loaded_state, e;
if ((err != null ? err.code : void 8) === 'ENOENT') {
console.warn("No state file. Creating a new one.");
} else if (err) {
return cb(err);
} else {
try {
loaded_state = JSON.parse(data);
} catch (e$) {
e = e$;
return cb(new Error("state file contains invalid JSON: " + e));
}
if (loaded_state.state_version !== STATE_VERSION) {
return cb(new Error("State version is " + loaded_state.state_version + " but should be " + STATE_VERSION));
}
state = loaded_state;
}
plugins.bus.emit('restore_state', state);
plugins.bus.emit('save_state', state);
cb();
});
}
function saveAndSendStatus(){
saveState();
io.sockets.emit('Status', JSON.stringify(state.status));
}
function writeMpdConf(cb){
mpd_conf = new MpdConf(state.mpd_conf);
state.mpd_conf = mpd_conf.state;
fs.writeFile(MPD_CONF_PATH, mpd_conf.toMpdConf(), cb);
}
function onSocketIoConnection(socket){
var client;
client = player_server.createClient(socket, state.default_permissions);
plugins.bus.emit('socket_connect', client);
}
function restartMpd(cb){
mkdirp(mpd_conf.playlistDirectory(), function(err){
if (err) {
return cb(err);
}
fs.readFile(MPD_PID_FILE, 'utf8', function(err, pid_str){
var pid, killer;
if (err) {
if ((err != null ? err.code : void 8) === 'ENOENT') {
startMpd(cb);
} else {
cb(err);
}
} else {
pid = parseInt(pid_str, 10);
console.info("killing mpd", pid);
killer = new Killer(pid);
killer.on('error', function(err){
cb(err);
});
killer.on('end', function(){
startMpd(cb);
});
killer.kill();
}
});
});
}
function startMpd(cb){
var child;
console.info("starting mpd", state.mpd_exe_path);
child = spawn(state.mpd_exe_path, ['--no-daemon', MPD_CONF_PATH], {
stdio: 'inherit',
detached: true
});
cb();
}
function makeConnectFunction(name, arg$){
var createSocket, onSuccess, connect_timeout, connect_success;
createSocket = arg$.createSocket, onSuccess = arg$.onSuccess;
connect_timeout = null;
function tryReconnect(){
if (connect_timeout != null) {
return;
}
connect_timeout = setTimeout(function(){
connect_timeout = null;
connect();
}, 1000);
}
connect_success = true;
function connect(){
var socket;
socket = createSocket();
socket.on('close', function(){
if (connect_success) {
console.warn(name + " connection closed");
}
tryReconnect();
});
socket.on('error', function(){
if (connect_success) {
connect_success = false;
console.warn(name + " connection error...");
}
tryReconnect();
});
socket.on('connect', function(){
console.log((connect_success ? '' : '...') + "" + name + " connected");
connect_success = true;
onSuccess(socket);
});
}
return connect;
}
var connectToMpd = makeConnectFunction('mpd', {
createSocket: function(){
var socket;
socket = net.connect({
path: MPD_SOCKET_PATH
});
socket.setEncoding('utf8');
return socket;
},
onSuccess: function(socket){
var mpd_parser = new MpdParser(socket);
var authenticate = function(pass){
return state.permissions[pass];
};
player_server = new PlayerServer(library, mpd_parser, authenticate);
startSocketIo();
}
process.on('message', function(message){
if (message === 'shutdown') process.exit(0);
});
var connectMasterPlayer = makeConnectFunction('master player', {
createSocket: function(){
return socketio_client.connect("http://localhost:" + PORT);
},
onSuccess: function(socket){
my_player = new PlayerClient(socket);
my_player.on('MpdError', function(msg){
console.error(msg);
});
socket.emit('SetUserName', '[server]');
my_player.authenticate(state.admin_password);
startPlugins();
}
});
async.series([
initState,
makeRunDir,
plugins.initialize.bind(plugins),
restoreState,
writeMpdConf,
restartMpd,
], function(err) {
assert.ifError(err);
connectToMpd();
gb.start({
host: process.env.HOST,
port: process.env.PORT,
});

View file

@ -33,7 +33,9 @@
"walkdir": "0.0.7",
"pend": "~1.1.0",
"musicmetadata-superjoe30": "~0.3.1",
"zfill": "0.0.1"
"zfill": "0.0.1",
"mpd": "~1.0.2",
"requireindex": "~1.0.1"
},
"devDependencies": {
"handlebars": "1.0.7",