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:
parent
e1da9b3339
commit
13edb5c3aa
19 changed files with 712 additions and 3247 deletions
8
TODO
8
TODO
|
|
@ -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
289
lib/groovebasin.js
Normal 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() {}
|
||||
116
lib/killer.js
116
lib/killer.js
|
|
@ -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"));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
163
lib/mpdconf.js
163
lib/mpdconf.js
|
|
@ -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";
|
||||
};
|
||||
|
|
|
|||
226
lib/mpdparser.js
226
lib/mpdparser.js
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
1070
lib/player.js
1070
lib/player.js
File diff suppressed because it is too large
Load diff
1179
lib/playerclient.js
1179
lib/playerclient.js
File diff suppressed because it is too large
Load diff
|
|
@ -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() {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
335
lib/server.js
335
lib/server.js
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue