mpd killing rampage

This commit is contained in:
Andrew Kelley 2013-09-18 18:34:05 -04:00
parent 4226183402
commit d32fff0a3f
14 changed files with 62 additions and 784 deletions

View file

@ -5,12 +5,6 @@ No-nonsense music client and server for your home or office.
Run it on a server connected to your main speakers. Guests can connect with
their laptops, tablets, and phones, and play and share music.
Depends on [mpd](http://musicpd.org) version 0.17+ for the backend. Some might
call this project an mpd client. (Note, version 0.17 is only available from
source as of writing this; see below instructions regarding mpd installation.)
[Live demo](http://superjoe.zapto.org:16242/)
## Features
* Lightning-fast, responsive UI. You can hardly tell that the music server is
@ -29,16 +23,14 @@ source as of writing this; see below instructions regarding mpd installation.)
## Get Started
Make sure you have [Node](http://nodejs.org) >=0.8.0 installed and
[mpd](http://musicpd.org) version >=0.17.0 (see below) running, then:
1. Make sure you have the latest stable [Node.js](http://nodejs.org) installed.
2. Install [libgroove](https://github.com/superjoe30/libgroove).
```
$ npm install --production groovebasin
$ npm start groovebasin
```
At this point, Groove Basin will issue warnings telling you what to do next.
## Screenshots
![Search + drag/drop support](http://superjoesoftware.com/temp/groove-basin-0.0.4.png)
@ -46,59 +38,13 @@ At this point, Groove Basin will issue warnings telling you what to do next.
![Keyboard shortcuts](http://superjoesoftware.com/temp/groove-basin-0.0.4-shortcuts.png)
![Last.fm Scrobbling](http://superjoesoftware.com/temp/groove-basin-0.0.4-lastfm.png)
## Mpd
Groove Basin depends on [mpd](http://musicpd.org) version 0.17+.
To compile from source, start here
```
$ git clone git://git.musicpd.org/master/mpd.git
```
and follow mpd's instructions from there.
### Configuration
* `default_permissions` - Recommended to remove `admin` so that anonymous
users can't do nefarious things.
* `password` - Recommended to add a password for yourself to give yourself `admin` permissions.
* `read` - allows reading the library, current playlist, and playback status.
* `add` - allows adding songs, loading playlists, and uploading songs.
* `control` - allows controlling playback state and manipulating playlists.
* `admin` - allows updating the db, killing mpd, deleting songs from the
library, and updating song tags.
* `audio_output` - Uncomment the "httpd" one and configure the port to enable
streaming. Recommended "vorbis" encoder for better browser support.
* `sticker_file` - Groove Basin will not run without one set.
* `gapless_mp3_playback` - "yes" recommended. <3 gapless playback.
* `volume_normalization` - "yes" recommended. Replaygain scanners are not
implemented for all the formats that can be played back. Volume normalization
works on all formats.
* `max_command_list_size` - "16384" recommended. You do not want mpd crashing
when you try to remove a ton of songs from the playlist at once.
* `auto_update` - "yes" recommended. Required for uploaded songs to show up
in your library.
## Configuring Groove Basin
## Configuration
Groove Basin is configured using environment variables. Available options
and their defaults:
HOST="0.0.0.0"
PORT="16242"
MPD_CONF="/etc/mpd.conf"
STATE_FILE=".state.json"
NODE_ENV="dev"
LASTFM_API_KEY=<not shown>
@ -106,11 +52,6 @@ and their defaults:
## Developing
Install dependencies and run mpd as described in the Get Started section.
Clone the repository using `git clone --recursive` or if you have
already cloned, do `git submodule update --init --recursive`.
```
$ npm run dev
```

1
TODO
View file

@ -21,5 +21,4 @@ Eventually:
* replace socket.io with https://github.com/einaros/ws
* 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)

View file

@ -1,6 +1,4 @@
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');
@ -8,15 +6,13 @@ 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 osenv = require('osenv');
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;
@ -32,13 +28,8 @@ 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);
mkdirp.sync(this.runDir);
this.app = express();
this.app.disable('x-powered-by');
@ -54,14 +45,6 @@ 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);
}
@ -72,24 +55,10 @@ GrooveBasin.prototype.saveState = function(cb) {
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));
@ -99,47 +68,7 @@ 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) {
function startServer(self) {
assert.ok(self.httpServer == null);
assert.ok(self.socketIo == null);
@ -164,42 +93,6 @@ function startServer(self, cb) {
}
}
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() {
@ -238,52 +131,45 @@ function restoreState(self, 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.once('stateRestored', function() {
self.library = new Library(self.state.musicDirectory);
self.library.startScan();
self.library.on('library', function() {
// TODO: this is weird. PlayerServer and Library should be the same object IMO
self.playerServer = new PlayerServer(self.library, authenticate);
startServer(self);
});
});
self.app.use(express.static(path.join(__dirname, '../public')));
self.app.use(express.static(path.join(__dirname, '../src/public')));
self.state = {
musicDirectory: path.join(osenv.home(), "music"),
state_version: STATE_VERSION,
status: {},
permissions: {},
default_permissions: DEFAULT_PERMISSIONS
};
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();
restoreState(self, function(err) {
if (err) {
console.error("unable to restore state:", err.stack);
return;
}
});
function authenticate(pass) {
return self.state.permissions[pass];
}
}
function noop() {}

View file

@ -1,54 +0,0 @@
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;
}
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

@ -1,103 +0,0 @@
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
};
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";
}
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,5 +1,4 @@
var util = require('util');
var mpd = require('mpd');
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
@ -142,20 +141,10 @@ action('stop', {
});
util.inherits(PlayerServer, EventEmitter);
function PlayerServer(library, mpdClient, authenticate) {
function PlayerServer(library, authenticate) {
var self = this;
self.library = library;
self.mpdClient = mpdClient;
self.authenticate = authenticate;
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 = {
@ -163,17 +152,11 @@ function PlayerServer(library, mpdClient, authenticate) {
single: false
};
self.is_playing = false;
self.mpd_is_playing = false;
self.mpd_should_be_playing_id = null;
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) {
@ -214,28 +197,16 @@ PlayerServer.prototype.authenticateWithPassword = function(client, password) {
// items looks like [{file, sort_key}]
PlayerServer.prototype.addItems = function(items, tagAsRandom) {
tagAsRandom = !!tagAsRandom;
var wantInfoTracksByFile = {};
var commands = [];
for (var id in items) {
var item = items[id];
var playlistItem = {
file: item.file,
sort_key: item.sort_key,
is_random: tagAsRandom,
time: library[id].time, // <--- TODO this is pseudocode
};
this.playlist[id] = playlistItem;
wantInfoTracksByFile[item.file] = playlistItem;
commands.push(mpd.cmd('listallinfo', [item.file]));
}
this.mpdClient.sendCommands(commands, function(err, msg) {
if (err) console.error("Error getting time info for tracks:", err.stack);
var objects = parseMpdObjects(msg);
objects.forEach(function(o) {
wantInfoTracksByFile[o.file].time = parseInt(o.Time, 10);
this.emit('status', ['playlist', 'player']);
}.bind(this));
}.bind(this));
playlistChanged(this);
}
PlayerServer.prototype.clearPlaylist = function() {
@ -381,46 +352,7 @@ function playlistChanged(self, o) {
self.paused_time = 0;
o.seekto = null;
}
var commands = [];
if (self.is_playing) {
if (self.current_id !== self.mpd_should_be_playing_id) {
var file = self.playlist[self.current_id].file;
commands.push("clear");
commands.push(mpd.cmd("addid", [file]));
commands.push("play");
self.mpd_should_be_playing_id = self.current_id;
if (o.seekto === 0) {
o.seekto = null;
} else if (self.paused_time) {
o.seekto = self.paused_time;
}
self.track_start_date = new Date();
}
if (o.seekto != null) {
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 {
commands.push(seek_command);
}
self.track_start_date = new Date() - o.seekto * 1000;
}
self.paused_time = null;
} else {
if (self.mpd_should_be_playing_id != null) {
commands.push("clear");
self.mpd_should_be_playing_id = null;
}
if (o.seekto != null) {
self.paused_time = o.seekto;
}
self.track_start_date = null;
if (self.paused_time == null) self.paused_time = 0;
}
if (commands.length) self.mpdClient.sendCommands(commands)
self.emit('status', ['playlist', 'player']);
// TODO something
disambiguateSortKeys(self);
}
@ -440,50 +372,7 @@ function findNext(object, from_id){
return result;
}
var refreshMpdCommands = ['currentsong', 'status'];
function refreshMpdStatus(self) {
self.mpdClient.sendCommands(refreshMpdCommands, 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) {
self.current_id = findNext(self.playlist, self.current_id);
return playlistChanged(self);
}
});
}
function parseMpdObjects(msg) {
var list = [];
var o = null;
msg.split("\n").forEach(function(line) {
var index = line.indexOf(": ");
var key = line.substr(0, index);
var value = line.substr(index + 2);
if (key === 'file') {
if (o) list.push(o);
o = {};
}
o[key] = value;
});
if (o) list.push(o);
return list;
}
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

@ -15,7 +15,8 @@ function setup(self) {
});
self.gb.on('socketConnect', onSocketConnection.bind(self));
self.gb.on('stateRestored', function(state) {
self.music_directory = state.mpd_conf.music_directory;
// TODO set self.music_directory based on state
//self.music_directory = ??
if (self.music_directory == null) {
self.is_enabled = false;
console.warn("No music directory set. Delete disabled.");

View file

@ -19,7 +19,9 @@ function Download(gb) {
});
bus.on('restore_state', function(state){
self.is_enabled = true;
if ((self.music_directory = state.mpd_conf.music_directory) == null) {
// TODO set music_directory based on config
// self.music_directory = ??
if (!self.music_directory) {
self.is_enabled = false;
console.warn("No music directory set. Download plugin disabled.");
}
@ -48,45 +50,14 @@ function Download(gb) {
return self.sendZipOfFiles(zip_name, files, req, resp);
});
app.get('/download/album/:album', self.checkEnabledMiddleware, function(req, resp){
var album, res$, i$, ref$, len$, track, files, zip_name;
album = self.mpd.library.album_table[req.params.album];
if (album == null) {
resp.statusCode = 404;
resp.end();
return;
}
res$ = [];
for (i$ = 0, len$ = (ref$ = album.tracks).length; i$ < len$; ++i$) {
track = ref$[i$];
res$.push(path.join(self.music_directory, track.file));
}
files = res$;
zip_name = safePath(album.name) + ".zip";
return self.sendZipOfFiles(zip_name, files, req, resp);
// TODO implement
});
return app.get('/download/artist/:artist', self.checkEnabledMiddleware, function(req, resp){
var artist, zip_name, files, i$, ref$, len$, album, j$, ref1$, len1$, track;
artist = self.mpd.library.artist_table[req.params.artist];
if (artist == null) {
resp.statusCode = 404;
resp.end();
return;
}
zip_name = safePath(artist.name) + ".zip";
files = [];
for (i$ = 0, len$ = (ref$ = artist.albums).length; i$ < len$; ++i$) {
album = ref$[i$];
for (j$ = 0, len1$ = (ref1$ = album.tracks).length; j$ < len1$; ++j$) {
track = ref1$[j$];
files.push(path.join(self.music_directory, track.file));
}
}
return self.sendZipOfFiles(zip_name, files, req, resp);
return app.get('/download/artist/:artist', self.checkEnabledMiddleware,
function(req, resp)
{
// TODO: implement
});
});
bus.on('mpd', function(mpd){
self.mpd = mpd;
});
}
Download.prototype.downloadPath = function(dl_path, zip_name, req, resp){

View file

@ -66,125 +66,11 @@ DynamicMode.prototype.checkDynamicMode = function(){
}
};
DynamicMode.prototype.checkDynamicModeOrWhyNot = function(){
var item_list, ref$, current_id, current_index, all_ids, new_files, last_key, i, len$, item, now, i$, file, delete_count, add_count, self = this;
if (!this.is_enabled) {
return "disabled";
}
if (!Object.keys(this.mpd.library.track_table).length) {
return "no tracks";
}
if (!this.got_stickers) {
return "no stickers";
}
item_list = this.mpd.playlist.item_list;
current_id = (ref$ = this.mpd.status.current_item) != null ? ref$.id : void 8;
current_index = -1;
all_ids = {};
new_files = [];
last_key = null;
for (i = 0, len$ = item_list.length; i < len$; ++i) {
item = item_list[i];
if (!(item.track != null && item.id != null)) {
return "item with no track";
}
if (item.id === current_id) {
current_index = i;
}
if (last_key == null || last_key < item.sort_key) {
last_key = item.sort_key;
}
all_ids[item.id] = true;
if (this.previous_ids[item.id] == null) {
new_files.push(item.track.file);
}
}
now = new Date();
this.mpd.setStickers(new_files, LAST_QUEUED_STICKER, JSON.stringify(now), function(err){
if (err) {
console.warn("dynamic mode set stickers error:", err);
}
});
for (i$ = 0, len$ = new_files.length; i$ < len$; ++i$) {
file = new_files[i$];
this.last_queued[file] = now;
}
if (current_index === -1) {
current_index = 0;
}
if (this.is_on) {
delete_count = Math.max(current_index - history_size, 0);
if (history_size < 0) {
delete_count = 0;
}
this.mpd.removeIds((function(){
var to$, results$ = [];
for (i = 0, to$ = delete_count; i < to$; ++i) {
results$.push(item_list[i].id);
}
return results$;
}()));
add_count = Math.max(future_size - (item_list.length - current_index), 0);
this.mpd.queueFiles(this.getRandomSongFiles(add_count), last_key, null, true);
}
this.previous_ids = all_ids;
if (delete_count + add_count > 0) {
this.emit('status_changed');
}
return null;
// TODO: implement
};
DynamicMode.prototype.updateStickers = function(){
var self = this;
this.mpd.findStickers('/', LAST_QUEUED_STICKER, function(err, stickers){
var sticker, file, value, track;
if (err) {
console.error('dynamicmode find sticker error:', err);
return;
}
for (sticker in stickers) {
file = sticker[0], value = sticker[1];
track = self.mpd.library.track_table[file];
self.last_queued[file] = new Date(value);
}
self.got_stickers = true;
});
// TODO: implement
};
DynamicMode.prototype.getRandomSongFiles = function(count){
var never_queued, sometimes_queued, file, ref$, track, max_weight, triangle_area, rectangle_area, total_size, files, i, index, self = this;
if (count === 0) {
return [];
}
never_queued = [];
sometimes_queued = [];
for (file in (ref$ = this.mpd.library.track_table)) {
track = ref$[file];
if (this.last_queued[file] != null) {
sometimes_queued.push(track);
} else {
never_queued.push(track);
}
}
sometimes_queued.sort(function(a, b){
return self.last_queued[b.file].getTime() - self.last_queued[a.file].getTime();
});
max_weight = sometimes_queued.length;
triangle_area = Math.floor(max_weight * max_weight / 2);
if (max_weight === 0) {
max_weight = 1;
}
rectangle_area = max_weight * never_queued.length;
total_size = triangle_area + rectangle_area;
if (total_size === 0) {
return [];
}
files = [];
for (i = 0; i < count; ++i) {
index = Math.random() * total_size;
if (index < triangle_area) {
track = sometimes_queued[Math.floor(Math.sqrt(index))];
} else {
track = never_queued[Math.floor((index - triangle_area) / max_weight)];
}
files.push(track.file);
}
return files;
// TODO: implement
};

View file

@ -17,7 +17,6 @@ function LastFm(bus) {
bus.on('save_state', bind$(self, 'saveState'));
bus.on('restore_state', bind$(self, 'restoreState'));
bus.on('socket_connect', bind$(self, 'onSocketConnection'));
bus.on('mpd', bind$(self, 'setMpd'));
}
LastFm.prototype.restoreState = function(state){
var ref$;
@ -36,14 +35,6 @@ LastFm.prototype.saveState = function(state){
state.status.lastfm_api_key = this.api_key;
state.lastfm_secret = this.api_secret;
};
LastFm.prototype.setMpd = function(mpd){
var self = this;
this.mpd = mpd;
this.mpd.on('statusupdate', function(){
self.updateNowPlaying();
self.checkScrobble();
});
};
LastFm.prototype.onSocketConnection = function(socket){
var self = this;
socket.on('LastfmGetSession', function(data){
@ -115,82 +106,10 @@ LastFm.prototype.checkTrackNumber = function(trackNumber){
}
};
LastFm.prototype.checkScrobble = function(){
var this_item, ref$, track, min_amt, max_amt, half_amt, session_key, len$, username, ref1$;
this_item = this.mpd.status.current_item;
if (this.mpd.status.state === 'play') {
if (this.previous_play_state !== 'play') {
this.playing_start = new Date(new Date().getTime() - this.playing_time);
this.previous_play_state = this.mpd.status.state;
}
}
this.playing_time = new Date().getTime() - this.playing_start.getTime();
if ((this_item != null ? this_item.id : void 8) === ((ref$ = this.last_playing_item) != null ? ref$.id : void 8)) {
return;
}
if ((track = (ref$ = this.last_playing_item) != null ? ref$.track : void 8) != null) {
min_amt = 15 * 1000;
max_amt = 4 * 60 * 1000;
half_amt = track.time / 2 * 1000;
if (this.playing_time >= min_amt && (this.playing_time >= max_amt || this.playing_time >= half_amt)) {
if (track.artist_name) {
for (session_key = 0, len$ = (ref$ = this.scrobblers).length; session_key < len$; ++session_key) {
username = ref$[session_key];
this.queueScrobble({
sk: session_key,
timestamp: Math.round(this.playing_start.getTime() / 1000),
album: ((ref1$ = track.album) != null ? ref1$.name : void 8) || "",
track: track.name || "",
artist: track.artist_name || "",
albumArtist: track.album_artist_name || "",
duration: track.time || "",
trackNumber: this.checkTrackNumber(track.track)
});
}
this.flushScrobbleQueue();
} else {
console.warn("Not scrobbling " + track.name + " - missing artist.");
}
}
}
this.last_playing_item = this_item;
this.previous_play_state = this.mpd.status.state;
this.playing_start = new Date();
this.playing_time = 0;
// TODO: implement
};
LastFm.prototype.updateNowPlaying = function(){
var ref$, track, username, session_key, ref1$;
if (this.mpd.status.state !== 'play') {
return;
}
if ((track = (ref$ = this.mpd.status.current_item) != null ? ref$.track : void 8) == null) {
return;
}
if (this.previous_now_playing_id === this.mpd.status.current_item.id) {
return;
}
this.previous_now_playing_id = this.mpd.status.current_item.id;
if (!track.artist_name) {
console.warn("Not updating last.fm now playing for " + track.name + ": missing artist");
return;
}
for (username in (ref$ = this.scrobblers)) {
session_key = ref$[username];
this.lastfm.request("track.updateNowPlaying", {
sk: session_key,
track: track.name || "",
artist: track.artist_name || "",
album: ((ref1$ = track.album) != null ? ref1$.name : void 8) || "",
albumArtist: track.album_artist_name || "",
trackNumber: this.checkTrackNumber(track.track),
duration: track.time || "",
handlers: {
error: fn$
}
});
}
function fn$(error){
console.error("error from last.fm track.updateNowPlaying: " + error.message);
}
// TODO: implement
};
function bind$(obj, key){
return function(){ return obj[key].apply(obj, arguments) };

View file

@ -8,15 +8,5 @@ function Stream(gb) {
}
function setup(self) {
self.gb.on('stateRestored', function(state) {
var ahttpd = state.mpd_conf.audio_httpd;
self.port = ahttpd.port;
self.format = ahttpd.format;
});
self.gb.on('socketConnect', function(client) {
client.emit('StreamInfo', {
port: self.port,
format: self.format,
});
});
// TODO implement
}

View file

@ -21,7 +21,6 @@ function Upload(gb) {
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'));
bus.on('restore_state', bind$(this, 'restoreState'));
bus.on('socket_connect', bind$(this, 'onSocketConnection'));
@ -30,7 +29,9 @@ Upload.prototype.restoreState = function(state){
var ref$;
this.want_to_queue = (ref$ = state.want_to_queue) != null ? ref$ : [];
this.is_enabled = true;
if ((this.music_directory = state.mpd_conf.music_directory) == null) {
// TODO set this.music_directory based on config
// this.music_directory = ??
if (!this.music_directory) {
this.is_enabled = false;
console.warn("No music directory set. Upload disabled.");
return;
@ -40,10 +41,6 @@ Upload.prototype.saveState = function(state){
state.want_to_queue = this.want_to_queue;
state.status.upload_enabled = this.is_enabled;
};
Upload.prototype.setMpd = function(mpd){
this.mpd = mpd;
this.mpd.on('libraryupdate', bind$(this, 'flushWantToQueue'));
};
Upload.prototype.onSocketConnection = function(socket){
var this$ = this;
socket.on('ImportTrackUrl', function(url_string){
@ -73,29 +70,7 @@ Upload.prototype.onSocketConnection = function(socket){
Upload.prototype.importFile = function(temp_file, remote_filename, cb){
var this$ = this;
if (cb == null) cb = function(){};
this.mpd.getFileInfo("file://" + temp_file, function(err, track){
var suggested_path, relative_path, dest;
if (err) {
console.warn("Unable to read tags to get a suggested upload path: " + err.stack);
suggested_path = safePath(remote_filename);
} else {
suggested_path = getSuggestedPath(track, remote_filename);
}
relative_path = path.join('incoming', suggested_path);
dest = path.join(this$.music_directory, relative_path);
mkdirp(path.dirname(dest), function(err){
if (err) {
console.error(err);
return cb(err);
}
mv(temp_file, dest, function(err){
this$.want_to_queue.push(relative_path);
this$.emit('state_changed');
console.info("Track was uploaded: " + dest);
cb(err);
});
});
});
// TODO implement this
};
Upload.prototype.setUpRoutes = function(app){
var this$ = this;
@ -117,25 +92,6 @@ Upload.prototype.setUpRoutes = function(app){
}));
});
};
Upload.prototype.flushWantToQueue = function(){
var i, files, file;
i = 0;
files = [];
while (i < this.want_to_queue.length) {
file = this.want_to_queue[i];
if (this.mpd.library.track_table[file] != null) {
files.push(file);
this.want_to_queue.splice(i, 1);
} else {
i++;
}
}
this.mpd.queueFiles(files);
this.mpd.queueFilesInStoredPlaylist(files, "Incoming");
if (files.length) {
this.emit('state_changed');
}
};
function bind$(obj, key){
return function(){ return obj[key].apply(obj, arguments) };
}

View file

@ -1,6 +1,6 @@
{
"name": "groovebasin",
"description": "No-nonsense music client and daemon based on mpd",
"description": "Web-based music server and client inspired by Amarok 1.4",
"author": "Andrew Kelley <superjoe30@gmail.com>",
"version": "0.2.0",
"licenses": [
@ -23,20 +23,17 @@
"zipstream": "~0.2.1",
"express": "~3.3.8",
"temp": "~0.5.1",
"async": "~0.1.22",
"superagent": "~0.15.4",
"mkdirp": "~0.3.5",
"mv": "0.0.5",
"which": "~1.0.5",
"osenv": "0.0.3",
"walkdir": "0.0.7",
"pend": "~1.1.0",
"musicmetadata-superjoe30": "~0.5.0",
"zfill": "0.0.1",
"mpd": "~1.0.2",
"requireindex": "~1.0.1",
"mess": "~0.1.1",
"diacritics": "~1.0.0"
"diacritics": "~1.0.0",
"groove": "0.0.0",
"osenv": "0.0.3"
},
"devDependencies": {
"handlebars": "1.0.7",

View file

@ -5,7 +5,7 @@ exports.init = init;
var trying_to_stream = false;
var actually_streaming = false;
var streaming_buffering = false;
var mpd = null;
var player = null;
var port = null;
var format = null;
@ -57,7 +57,7 @@ function getUrl(){
}
function updatePlayer() {
var should_stream = trying_to_stream && mpd.status.state === "play";
var should_stream = trying_to_stream && player.status.state === "play";
if (actually_streaming === should_stream) return;
if (should_stream) {
soundManager.destroySound('stream');
@ -88,15 +88,15 @@ function setUpUi() {
$stream_btn.on('click', toggleStatus);
}
function init(mpdInstance, socket) {
mpd = mpdInstance;
function init(playerInstance, socket) {
player = playerInstance;
soundManager.setup({
url: "/vendor/soundmanager2/",
flashVersion: 9,
debugMode: false
});
mpd.on('statusupdate', updatePlayer);
player.on('statusupdate', updatePlayer);
socket.on('StreamInfo', function(info) {
port = info.port;
format = info.format;