1583 lines
38 KiB
JavaScript
1583 lines
38 KiB
JavaScript
var Duplex = require('stream').Duplex;
|
|
var util = require('util');
|
|
var Player = require('./player');
|
|
var path = require('path');
|
|
var GrooveBasinProtocol = require('./protocol_parser');
|
|
|
|
// TODO: in mpd mode, when inserting tracks in dynamic mode,
|
|
// insert them before the random ones
|
|
|
|
module.exports = MpdProtocol;
|
|
|
|
var GROOVEBASIN_PROTOCOL_UUID = "b417684f-52af-4880-be4c-cb85593fb736";
|
|
|
|
var ERR_CODE_NOT_LIST = 1;
|
|
var ERR_CODE_ARG = 2;
|
|
var ERR_CODE_PASSWORD = 3;
|
|
var ERR_CODE_PERMISSION = 4;
|
|
var ERR_CODE_UNKNOWN = 5;
|
|
var ERR_CODE_NO_EXIST = 50;
|
|
var ERR_CODE_PLAYLIST_MAX = 51;
|
|
var ERR_CODE_SYSTEM = 52;
|
|
var ERR_CODE_PLAYLIST_LOAD = 53;
|
|
var ERR_CODE_UPDATE_ALREADY = 54;
|
|
var ERR_CODE_PLAYER_SYNC = 55;
|
|
var ERR_CODE_EXIST = 56;
|
|
|
|
var tagTypes = {
|
|
file: {
|
|
caseCorrect: "File",
|
|
grooveTag: "file",
|
|
},
|
|
artist: {
|
|
caseCorrect: "Artist",
|
|
grooveTag: "artistName",
|
|
},
|
|
artistsort: {
|
|
caseCorrect: "ArtistSort",
|
|
grooveTag: "artistName",
|
|
sort: true,
|
|
},
|
|
album: {
|
|
caseCorrect: "Album",
|
|
grooveTag: "albumName",
|
|
},
|
|
albumartist: {
|
|
caseCorrect: "AlbumArtist",
|
|
grooveTag: "albumArtistName",
|
|
},
|
|
albumartistsort: {
|
|
caseCorrect: "AlbumArtistSort",
|
|
grooveTag: "albumArtistName",
|
|
sort: true,
|
|
},
|
|
title: {
|
|
caseCorrect: "Title",
|
|
grooveTag: "name",
|
|
},
|
|
track: {
|
|
caseCorrect: "Track",
|
|
grooveTag: "track",
|
|
},
|
|
name: {
|
|
caseCorrect: "Name",
|
|
grooveTag: "name",
|
|
},
|
|
genre: {
|
|
caseCorrect: "Genre",
|
|
grooveTag: "genre",
|
|
},
|
|
date: {
|
|
caseCorrect: "Date",
|
|
grooveTag: "year",
|
|
},
|
|
composer: {
|
|
caseCorrect: "Composer",
|
|
grooveTag: "composerName",
|
|
},
|
|
performer: {
|
|
caseCorrect: "Performer",
|
|
grooveTag: "performerName",
|
|
},
|
|
disc: {
|
|
caseCorrect: "Disc",
|
|
grooveTag: "disc",
|
|
},
|
|
};
|
|
|
|
var commands = {
|
|
"add": {
|
|
fn: addCmd,
|
|
permission: 'add',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
},
|
|
],
|
|
},
|
|
"addid": {
|
|
permission: 'add',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
},
|
|
{
|
|
name: 'position',
|
|
type: 'integer',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var pos = args.position == null ? self.player.tracksInOrder.length : args.position;
|
|
var dbFile = self.player.dbFilesByPath[args.uri];
|
|
if (!dbFile) return cb(ERR_CODE_NO_EXIST, "Not found");
|
|
var ids = self.player.insertTracks(pos, [dbFile.key], false);
|
|
self.push("Id: " + self.apiServer.toMpdId(ids[0]) + "\n");
|
|
cb();
|
|
},
|
|
},
|
|
"channels": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
cb();
|
|
},
|
|
},
|
|
"clear": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
self.player.clearPlaylist();
|
|
cb();
|
|
},
|
|
},
|
|
"clearerror": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
cb();
|
|
},
|
|
},
|
|
"close": {
|
|
fn: function (self, args, cb) {
|
|
self.push(null);
|
|
},
|
|
},
|
|
"commands": {
|
|
fn: function (self, args, cb) {
|
|
for (var commandName in commands) {
|
|
self.push("command: " + commandName + "\n");
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"consume": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'state',
|
|
type: 'boolean',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.player.setDynamicModeOn(args.state);
|
|
cb();
|
|
},
|
|
},
|
|
"count": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: 'tag',
|
|
type: 'string',
|
|
},
|
|
{
|
|
name: 'needle',
|
|
type: 'string',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var tagType = tagTypes[args.tag.toLowerCase()];
|
|
if (!tagType) return cb(ERR_CODE_ARG, "incorrect arguments");
|
|
var filters = [{
|
|
field: tagType.grooveTag,
|
|
value: args.needle,
|
|
}];
|
|
var songs = 0;
|
|
forEachMatchingTrack(self, filters, true, function(track) {
|
|
songs += 1;
|
|
});
|
|
self.push("songs: " + songs + "\n");
|
|
self.push("playtime: 0\n");
|
|
cb();
|
|
},
|
|
},
|
|
"currentsong": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
var currentTrack = self.player.currentTrack;
|
|
if (!currentTrack) return cb();
|
|
|
|
var start = currentTrack.index;
|
|
var end = start + 1;
|
|
writePlaylistInfo(self, start, end);
|
|
cb();
|
|
},
|
|
},
|
|
"delete": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'indexRange',
|
|
type: 'range',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var start = args.indexRange.start;
|
|
var end = args.indexRange.end;
|
|
var ids = [];
|
|
for (var i = start; i < end; i += 1) {
|
|
var track = self.player.tracksInOrder[i];
|
|
if (!track) {
|
|
cb(ERR_CODE_ARG, "Bad song index");
|
|
return;
|
|
}
|
|
ids.push(track.id);
|
|
}
|
|
self.player.removePlaylistItems(ids);
|
|
cb();
|
|
},
|
|
},
|
|
"deleteid": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'id',
|
|
type: 'id',
|
|
},
|
|
],
|
|
fn: function(self, args, cb) {
|
|
self.player.removePlaylistItems([args.id]);
|
|
cb();
|
|
},
|
|
},
|
|
"find": {
|
|
permission: 'read',
|
|
manualArgParsing: true,
|
|
fn: function (self, args, cb) {
|
|
findOrSearch(self, args, true, cb);
|
|
},
|
|
},
|
|
"findadd": {
|
|
permission: 'control',
|
|
manualArgParsing: true,
|
|
fn: function (self, args, cb) {
|
|
findOrSearchAdd(self, args, true, cb);
|
|
},
|
|
},
|
|
"idle": {}, // handled in a special case
|
|
"list": {
|
|
permission: 'read',
|
|
manualArgParsing: true,
|
|
fn: function (self, args, cb) {
|
|
if (args.length < 1) {
|
|
cb(ERR_CODE_ARG, "too few arguments for \"list\"");
|
|
return;
|
|
}
|
|
var tagTypeId = args[0].toLowerCase();
|
|
if (args.length === 2 && tagTypeId !== 'album') {
|
|
cb(ERR_CODE_ARG, "should be \"Album\" for 3 arguments");
|
|
return;
|
|
}
|
|
if (args.length !== 2 && args.length % 2 !== 1) {
|
|
cb(ERR_CODE_ARG, "not able to parse args");
|
|
return;
|
|
}
|
|
var targetTagType = tagTypes[tagTypeId];
|
|
if (!targetTagType) return cb(ERR_CODE_ARG, "\"" + args[0] + "\" is not known");
|
|
var caseCorrect = targetTagType.caseCorrect;
|
|
|
|
var filters = [];
|
|
if (args.length === 2) {
|
|
filters.push({
|
|
field: 'artistName',
|
|
value: args[1],
|
|
});
|
|
} else {
|
|
for (var i = 1; i < args.length; i += 2) {
|
|
var tagType = tagTypes[args[i].toLowerCase()];
|
|
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known");
|
|
filters.push({
|
|
field: tagType.grooveTag,
|
|
value: args[i+1],
|
|
});
|
|
}
|
|
}
|
|
|
|
var set = {};
|
|
forEachMatchingTrack(self, filters, true, function(track) {
|
|
var field = track[targetTagType.grooveTag];
|
|
if (!set[field]) {
|
|
set[field] = true;
|
|
self.push(caseCorrect + ": " + field + "\n");
|
|
}
|
|
});
|
|
|
|
cb();
|
|
},
|
|
},
|
|
"listall": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
forEachListAll(self, args, writeFileOnly, cb);
|
|
|
|
function writeFileOnly(track) {
|
|
self.push("file: " + track.file + "\n");
|
|
}
|
|
},
|
|
},
|
|
"listallinfo": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
forEachListAll(self, args, doWriteTrackInfo, cb);
|
|
|
|
function doWriteTrackInfo(track) {
|
|
writeTrackInfo(self, track);
|
|
}
|
|
}
|
|
},
|
|
"lsinfo": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var dirName = args.uri || "";
|
|
var dirEntry = self.player.dirs[dirName];
|
|
if (!dirEntry) return cb(ERR_CODE_NO_EXIST, "Not found");
|
|
var baseName, relPath;
|
|
var dbFilesByPath = self.player.dbFilesByPath;
|
|
for (baseName in dirEntry.entries) {
|
|
relPath = path.join(dirName, baseName);
|
|
var dbTrack = dbFilesByPath[relPath];
|
|
if (dbTrack) writeTrackInfo(self, dbTrack);
|
|
}
|
|
for (baseName in dirEntry.dirEntries) {
|
|
relPath = path.join(dirName, baseName);
|
|
var childEntry = self.player.dirs[relPath];
|
|
self.push("directory: " + relPath + "\n");
|
|
self.push("Last-Modified: " + new Date(childEntry.mtime).toISOString() + "\n");
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"move": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'fromRange',
|
|
type: 'range',
|
|
},
|
|
{
|
|
name: 'pos',
|
|
type: 'integer',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.player.moveRangeToPos(args.fromRange.start, args.fromRange.end, args.pos);
|
|
cb();
|
|
},
|
|
},
|
|
"moveid": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'id',
|
|
type: 'id',
|
|
},
|
|
{
|
|
name: 'pos',
|
|
type: 'integer',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.player.moveIdsToPos([args.id], args.pos);
|
|
cb();
|
|
},
|
|
},
|
|
"next": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
self.player.next();
|
|
cb();
|
|
}
|
|
},
|
|
"notcommands": {
|
|
fn: function (self, args, cb) {
|
|
for (var commandName in commands) {
|
|
var cmd = commands[commandName];
|
|
if (cmd.permission != null && !self.permissions[cmd.permission]) {
|
|
self.push("command: " + commandName + "\n");
|
|
}
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"outputs": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
self.push("outputid: 0\n");
|
|
self.push("outputname: default detected output\n");
|
|
self.push("outputenabled: 1\n");
|
|
self.push("outputid: 1\n");
|
|
self.push("outputname: GrooveBasin HTTP Stream\n");
|
|
self.push("outputenabled: 1\n");
|
|
cb();
|
|
},
|
|
},
|
|
"password": {
|
|
args: [
|
|
{
|
|
name: 'password',
|
|
type: 'string',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var perms = self.authenticate(args.password);
|
|
var success = perms != null;
|
|
if (!success) return cb(ERR_CODE_PASSWORD, "incorrect password");
|
|
self.permissions = perms;
|
|
cb();
|
|
},
|
|
},
|
|
"pause": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'pause',
|
|
type: 'boolean',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
if (args.pause == null) {
|
|
// toggle
|
|
if (self.player.isPlaying) {
|
|
self.player.pause();
|
|
} else {
|
|
self.player.play();
|
|
}
|
|
} else {
|
|
if (args.pause) {
|
|
self.player.pause();
|
|
} else {
|
|
self.player.play();
|
|
}
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"ping": {
|
|
fn: function (self, args, cb) {
|
|
cb();
|
|
}
|
|
},
|
|
"play": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
var currentTrack = self.player.currentTrack;
|
|
if ((args.songPos == null || args.songPos === -1) && currentTrack) {
|
|
self.player.play();
|
|
cb();
|
|
return;
|
|
}
|
|
var currentIndex = currentTrack ? currentTrack.index : 0;
|
|
var index = (args.songPos == null || args.songPos === -1) ? currentIndex : args.songPos;
|
|
self.player.seekToIndex(index, 0);
|
|
cb();
|
|
},
|
|
args: [
|
|
{
|
|
name: 'songPos',
|
|
type: 'integer',
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"playid": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'id',
|
|
type: 'id',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var id = args.id == null ? self.player.tracksInOrder[0].id : args.id;
|
|
var item = self.player.playlist[id];
|
|
if (!item) return cb(ERR_CODE_NO_EXIST, "No such song");
|
|
self.player.seek(id, 0);
|
|
cb();
|
|
},
|
|
},
|
|
"playlist": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
var trackTable = self.player.libraryIndex.trackTable;
|
|
self.player.tracksInOrder.forEach(function(track, index) {
|
|
var dbTrack = trackTable[track.key];
|
|
self.push(index + ":file: " + dbTrack.file + "\n");
|
|
});
|
|
cb();
|
|
}
|
|
},
|
|
"playlistid": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: 'id',
|
|
type: 'id',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var start = 0;
|
|
var end = self.player.tracksInOrder.length;
|
|
if (args.id != null) {
|
|
start = self.player.playlist[args.id].index;
|
|
end = start + 1;
|
|
}
|
|
writePlaylistInfo(self, start, end);
|
|
cb();
|
|
},
|
|
},
|
|
"playlistinfo": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: 'indexRange',
|
|
type: 'range',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var start = 0;
|
|
var end = self.player.tracksInOrder.length;
|
|
|
|
if (args.indexRange != null) {
|
|
start = args.indexRange.start;
|
|
end = args.indexRange.end;
|
|
}
|
|
|
|
writePlaylistInfo(self, start, end);
|
|
cb();
|
|
},
|
|
},
|
|
"plchanges": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: "version",
|
|
type: "integer",
|
|
},
|
|
],
|
|
fn: function(self, args, cb) {
|
|
writePlaylistInfo(self, 0, self.player.tracksInOrder.length);
|
|
cb();
|
|
},
|
|
},
|
|
"plchangesposid": {
|
|
permission: 'read',
|
|
args: [
|
|
{
|
|
name: "version",
|
|
type: "integer",
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var tracksInOrder = self.player.tracksInOrder;
|
|
for (var i = 0; i < tracksInOrder.length; i += 1) {
|
|
var item = tracksInOrder[i];
|
|
self.push("cpos: " + i + "\n");
|
|
self.push("Id: " + self.apiServer.toMpdId(item.id) + "\n");
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"previous": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
self.player.prev();
|
|
cb();
|
|
}
|
|
},
|
|
"repeat": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'on',
|
|
type: 'boolean',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
if (args.on && self.player.repeat === Player.REPEAT_OFF) {
|
|
self.player.setRepeat(self.apiServer.singleMode ? Player.REPEAT_ONE : Player.REPEAT_ALL);
|
|
} else if (!args.on && self.player.repeat !== Player.REPEAT_OFF) {
|
|
self.player.setRepeat(Player.REPEAT_OFF);
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"replay_gain_status": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
self.push("replay_gain_mode: auto\n");
|
|
cb();
|
|
},
|
|
},
|
|
"rescan": {
|
|
permission: 'admin',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
handleUpdate(self, args, true, cb);
|
|
},
|
|
},
|
|
"search": {
|
|
permission: 'read',
|
|
manualArgParsing: true,
|
|
fn: function (self, args, cb) {
|
|
findOrSearch(self, args, false, cb);
|
|
},
|
|
},
|
|
"searchadd": {
|
|
permission: 'add',
|
|
manualArgParsing: true,
|
|
fn: function (self, args, cb) {
|
|
findOrSearchAdd(self, args, false, cb);
|
|
},
|
|
},
|
|
"seek": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'index',
|
|
type: 'integer',
|
|
},
|
|
{
|
|
name: 'pos',
|
|
type: 'float',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.player.seekToIndex(args.index, args.pos);
|
|
cb();
|
|
},
|
|
},
|
|
"seekcur": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'pos',
|
|
type: 'float',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
var currentTrack = self.player.currentTrack;
|
|
if (!currentTrack) return cb(ERR_CODE_PLAYER_SYNC, "Not playing");
|
|
self.player.seek(currentTrack.id, args.pos);
|
|
cb();
|
|
},
|
|
},
|
|
"seekid": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'id',
|
|
type: 'id',
|
|
},
|
|
{
|
|
name: 'pos',
|
|
type: 'float',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.player.seek(args.id, args.pos);
|
|
cb();
|
|
},
|
|
},
|
|
"setvol": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'vol',
|
|
type: 'float',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.player.setVolume(args.vol / 100);
|
|
cb();
|
|
},
|
|
},
|
|
"shuffle": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
self.player.shufflePlaylist();
|
|
cb();
|
|
},
|
|
},
|
|
"single": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'single',
|
|
type: 'boolean',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
self.apiServer.setSingleMode(args.single);
|
|
if (self.apiServer.singleMode && self.player.repeat === Player.REPEAT_ALL) {
|
|
self.player.setRepeat(Player.REPEAT_ONE);
|
|
} else if (!self.apiServer.singleMode && self.player.repeat === Player.REPEAT_ONE) {
|
|
self.player.setRepeat(Player.REPEAT_ALL);
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"stats": {
|
|
permission: 'read',
|
|
fn: statsCmd,
|
|
},
|
|
"status": {
|
|
permission: 'read',
|
|
fn: statusCmd,
|
|
},
|
|
"stop": {
|
|
permission: 'control',
|
|
fn: function (self, args, cb) {
|
|
self.player.stop();
|
|
cb();
|
|
},
|
|
},
|
|
"swap": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'pos1',
|
|
type: 'integer',
|
|
},
|
|
{
|
|
name: 'pos2',
|
|
type: 'integer',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
swapItems(self,
|
|
self.player.tracksInOrder[args.pos1],
|
|
self.player.tracksInOrder[args.pos2], cb);
|
|
},
|
|
},
|
|
"swapid": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'id1',
|
|
type: 'id',
|
|
},
|
|
{
|
|
name: 'id2',
|
|
type: 'id',
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
swapItems(self, self.player.playlist[args.id1], self.player.playlist[args.id2], cb);
|
|
},
|
|
},
|
|
"tagtypes": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
for (var tagTypeId in tagTypes) {
|
|
var tagType = tagTypes[tagTypeId];
|
|
self.push("tagtype: " + tagType.caseCorrect + "\n");
|
|
}
|
|
cb();
|
|
},
|
|
},
|
|
"update": {
|
|
permission: 'control',
|
|
args: [
|
|
{
|
|
name: 'uri',
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
],
|
|
fn: function (self, args, cb) {
|
|
handleUpdate(self, args, false, cb);
|
|
},
|
|
},
|
|
"urlhandlers": {
|
|
permission: 'read',
|
|
fn: function (self, args, cb) {
|
|
cb(); // no URL handlers
|
|
},
|
|
},
|
|
};
|
|
|
|
var argParsers = {
|
|
'integer': parseInteger,
|
|
'float': parseFloat,
|
|
'range': parseRange,
|
|
'boolean': parseBoolean,
|
|
'string': parseString,
|
|
'id': parseId,
|
|
};
|
|
|
|
var stateCount = 0;
|
|
var STATE_CMD = stateCount++;
|
|
var STATE_CMD_SPACE = stateCount++;
|
|
var STATE_ARG = stateCount++;
|
|
var STATE_ARG_QUOTE = stateCount++;
|
|
var STATE_ARG_ESC = stateCount++;
|
|
|
|
var cmdListStateCount = 0;
|
|
var CMD_LIST_STATE_NONE = cmdListStateCount++;
|
|
var CMD_LIST_STATE_LIST = cmdListStateCount++;
|
|
|
|
var bootTime = new Date();
|
|
|
|
util.inherits(MpdProtocol, Duplex);
|
|
function MpdProtocol(options) {
|
|
var streamOptions = extend(extend({}, options.streamOptions || {}), {decodeStrings: false});
|
|
Duplex.call(this, streamOptions);
|
|
this.player = options.player;
|
|
this.authenticate = options.authenticate;
|
|
this.permissions = options.permissions;
|
|
this.apiServer = options.apiServer;
|
|
this.playerServer = options.playerServer;
|
|
|
|
this.buffer = "";
|
|
this.bufferIndex = 0;
|
|
this.cmdListState = CMD_LIST_STATE_NONE;
|
|
this.cmdList = [];
|
|
this.okMode = false;
|
|
this.isIdle = false;
|
|
this.commandQueue = [];
|
|
this.ongoingCommand = false;
|
|
this.grooveBasinProtocol = new GrooveBasinProtocol(options);
|
|
this.updatedSubsystems = {
|
|
database: false,
|
|
update: false,
|
|
stored_playlist: false,
|
|
playlist: false,
|
|
player: false,
|
|
mixer: false,
|
|
output: false,
|
|
options: false,
|
|
sticker: false,
|
|
subscription: false,
|
|
message: false,
|
|
};
|
|
this._read = mpdRead;
|
|
this._write = mpdWrite;
|
|
this.initialize();
|
|
}
|
|
|
|
MpdProtocol.prototype.initialize = function() {
|
|
this.push("OK MPD 0.19.0\n");
|
|
};
|
|
|
|
MpdProtocol.prototype.upgradeProtocol = function() {
|
|
var self = this;
|
|
self.apiServer.handleClientEnd(self);
|
|
self.grooveBasinProtocol.on('end', function() {
|
|
self.push(null);
|
|
});
|
|
self.playerServer.handleNewClient(self.grooveBasinProtocol);
|
|
self._read = grooveBasinRead;
|
|
self._write = grooveBasinWrite;
|
|
self.grooveBasinProtocol.on('readable', function() {
|
|
var chunk;
|
|
while (chunk = self.grooveBasinProtocol.read()) {
|
|
self.push(chunk);
|
|
}
|
|
});
|
|
self.grooveBasinProtocol.write(self.buffer, 'utf8', noop);
|
|
self.buffer = "";
|
|
};
|
|
|
|
function grooveBasinRead(size) { }
|
|
|
|
function grooveBasinWrite(chunk, encoding, callback) {
|
|
this.grooveBasinProtocol.write(chunk, encoding, callback);
|
|
}
|
|
|
|
function mpdRead(size) {}
|
|
|
|
function mpdWrite(chunk, encoding, callback) {
|
|
var self = this;
|
|
|
|
this.buffer += chunk;
|
|
while (this.buffer.length) {
|
|
var newlinePos = this.buffer.indexOf("\n", this.bufferIndex);
|
|
if (newlinePos === -1) {
|
|
this.bufferIndex = this.buffer.length;
|
|
callback();
|
|
return;
|
|
}
|
|
var lineLength = newlinePos - 1;
|
|
if (this.buffer[lineLength] !== "\r") lineLength += 1;
|
|
|
|
var line = this.buffer.substring(0, lineLength);
|
|
this.buffer = this.buffer.substring(newlinePos + 1);
|
|
this.bufferIndex = 0;
|
|
handleLine(line);
|
|
}
|
|
callback();
|
|
|
|
function handleLine(line) {
|
|
var state = STATE_CMD;
|
|
var cmd = "";
|
|
var args = [];
|
|
var curArg = "";
|
|
for (var i = 0; i < line.length; i += 1) {
|
|
var c = line[i];
|
|
switch (state) {
|
|
case STATE_CMD:
|
|
if (isSpace(c)) {
|
|
state = STATE_CMD_SPACE;
|
|
} else {
|
|
cmd += c;
|
|
}
|
|
break;
|
|
case STATE_CMD_SPACE:
|
|
if (c === '"') {
|
|
curArg = "";
|
|
state = STATE_ARG_QUOTE;
|
|
} else if (!isSpace(c)) {
|
|
curArg = c;
|
|
state = STATE_ARG;
|
|
}
|
|
break;
|
|
case STATE_ARG:
|
|
if (isSpace(c)) {
|
|
args.push(curArg);
|
|
curArg = "";
|
|
state = STATE_CMD_SPACE;
|
|
} else {
|
|
curArg += c;
|
|
}
|
|
break;
|
|
case STATE_ARG_QUOTE:
|
|
if (c === '"') {
|
|
args.push(curArg);
|
|
curArg = "";
|
|
state = STATE_CMD_SPACE;
|
|
} else if (c === "\\") {
|
|
state = STATE_ARG_ESC;
|
|
} else {
|
|
curArg += c;
|
|
}
|
|
break;
|
|
case STATE_ARG_ESC:
|
|
curArg += c;
|
|
state = STATE_ARG_QUOTE;
|
|
break;
|
|
default:
|
|
throw new Error("unrecognized state");
|
|
}
|
|
}
|
|
if (state === STATE_ARG) {
|
|
args.push(curArg);
|
|
}
|
|
self.commandQueue.push([cmd, args]);
|
|
flushQueue();
|
|
}
|
|
|
|
function flushQueue() {
|
|
if (self.ongoingCommand) return;
|
|
var queueItem = self.commandQueue.shift();
|
|
if (!queueItem) return;
|
|
var cmd = queueItem[0];
|
|
var args = queueItem[1];
|
|
self.ongoingCommand = true;
|
|
handleCommand(cmd, args, function() {
|
|
self.ongoingCommand = false;
|
|
flushQueue();
|
|
});
|
|
}
|
|
|
|
function handleCommand(cmdName, args, cb) {
|
|
var cmdIndex = 0;
|
|
|
|
switch (self.cmdListState) {
|
|
case CMD_LIST_STATE_NONE:
|
|
if (cmdName === 'command_list_begin' && args.length === 0) {
|
|
self.cmdListState = CMD_LIST_STATE_LIST;
|
|
self.cmdList = [];
|
|
self.okMode = false;
|
|
cb();
|
|
return;
|
|
} else if (cmdName === 'command_list_ok_begin' && args.length === 0) {
|
|
self.cmdListState = CMD_LIST_STATE_LIST;
|
|
self.cmdList = [];
|
|
self.okMode = true;
|
|
cb();
|
|
return;
|
|
} else {
|
|
runOneCommand(cmdName, args, 0, function(ok) {
|
|
if (ok) self.push("OK\n");
|
|
cb();
|
|
});
|
|
return;
|
|
}
|
|
break;
|
|
case CMD_LIST_STATE_LIST:
|
|
if (cmdName === 'command_list_end' && args.length === 0) {
|
|
self.cmdListState = CMD_LIST_STATE_NONE;
|
|
|
|
runAndCheckOneCommand();
|
|
return;
|
|
} else {
|
|
self.cmdList.push([cmdName, args]);
|
|
cb();
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error("unrecognized state");
|
|
}
|
|
|
|
function runAndCheckOneCommand() {
|
|
var commandPayload = self.cmdList.shift();
|
|
if (!commandPayload) {
|
|
self.push("OK\n");
|
|
cb();
|
|
return;
|
|
}
|
|
var thisCmdName = commandPayload[0];
|
|
var thisCmdArgs = commandPayload[1];
|
|
runOneCommand(thisCmdName, thisCmdArgs, cmdIndex++, function(ok) {
|
|
if (!ok) {
|
|
cb();
|
|
return;
|
|
}
|
|
if (self.okMode) self.push("list_OK\n");
|
|
runAndCheckOneCommand();
|
|
});
|
|
}
|
|
|
|
function runOneCommand(cmdName, args, index, cb) {
|
|
if (cmdName === 'noidle') {
|
|
var ok = self.isIdle;
|
|
self.isIdle = false;
|
|
cb(ok);
|
|
return;
|
|
}
|
|
if (self.isIdle) {
|
|
self.push(null);
|
|
cb(false);
|
|
return;
|
|
}
|
|
if (cmdName === 'idle') {
|
|
if (!self.permissions.read) {
|
|
cmdDone(ERR_CODE_PERMISSION, "you don't have permission for \"" + cmdName + "\"");
|
|
} else {
|
|
self.handleIdle(args);
|
|
}
|
|
cb(false);
|
|
return;
|
|
}
|
|
if (cmdName === 'protocolupgrade') {
|
|
if (args.length !== 1 || args[0] !== GROOVEBASIN_PROTOCOL_UUID) {
|
|
cmdDone(ERR_CODE_ARG, "invalid arguments");
|
|
return;
|
|
}
|
|
self.upgradeProtocol();
|
|
return;
|
|
}
|
|
execOneCommand(cmdName, args, cmdDone);
|
|
|
|
function cmdDone(code, msg) {
|
|
if (code) {
|
|
console.warn("cmd err:", cmdName, JSON.stringify(args), msg);
|
|
if (code === ERR_CODE_UNKNOWN) cmdName = "";
|
|
self.push("ACK [" + code + "@" + index + "] {" + cmdName + "} " + msg + "\n");
|
|
cb(false);
|
|
return;
|
|
}
|
|
cb(true);
|
|
}
|
|
}
|
|
|
|
function execOneCommand(cmdName, args, cb) {
|
|
if (!cmdName.length) return cb(ERR_CODE_UNKNOWN, "No command given");
|
|
var cmd = commands[cmdName];
|
|
if (!cmd) return cb(ERR_CODE_UNKNOWN, "unknown command \"" + cmdName + "\"");
|
|
|
|
var perm = cmd.permission;
|
|
if (perm != null && !self.permissions[perm]) {
|
|
cb(ERR_CODE_PERMISSION, "you don't have permission for \"" + cmdName + "\"");
|
|
return;
|
|
}
|
|
|
|
var argsParam;
|
|
if (cmd.manualArgParsing) {
|
|
argsParam = args;
|
|
} else {
|
|
var min = 0;
|
|
var max = 0;
|
|
var i;
|
|
var cmdArgs = cmd.args || [];
|
|
for (i = 0; i < cmdArgs.length; i += 1) {
|
|
if (!cmdArgs[i].optional) min += 1;
|
|
max += 1;
|
|
}
|
|
if (args.length < min) {
|
|
cb(ERR_CODE_ARG, "too few arguments for \"" + cmdName + "\"");
|
|
return;
|
|
}
|
|
if (args.length > max) {
|
|
cb(ERR_CODE_ARG, "too many arguments for \"" + cmdName + "\"");
|
|
return;
|
|
}
|
|
var namedArgs = {};
|
|
for (i = 0; i < args.length; i += 1) {
|
|
var arg = args[i];
|
|
var argInfo = cmdArgs[i];
|
|
|
|
var parseArg = argParsers[argInfo.type];
|
|
if (!parseArg) throw new Error("unrecognized arg type: " + argInfo.type);
|
|
var ret = parseArg.call(self, arg, argInfo);
|
|
if (ret.msg) {
|
|
cb(ERR_CODE_ARG, ret.msg);
|
|
return;
|
|
}
|
|
namedArgs[argInfo.name] = ret.value;
|
|
}
|
|
argsParam = namedArgs;
|
|
}
|
|
console.info("ok mpd command", cmdName, JSON.stringify(argsParam));
|
|
cmd.fn(self, argsParam, cb);
|
|
}
|
|
}
|
|
}
|
|
|
|
MpdProtocol.prototype.handleIdle = function(args) {
|
|
var anyUpdated = false;
|
|
for (var subsystem in this.updatedSubsystems) {
|
|
var isUpdated = this.updatedSubsystems[subsystem];
|
|
if (isUpdated) {
|
|
this.push("changed: " + subsystem + "\n");
|
|
anyUpdated = true;
|
|
this.updatedSubsystems[subsystem] = false;
|
|
}
|
|
}
|
|
if (anyUpdated) {
|
|
this.push("OK\n");
|
|
this.isIdle = false;
|
|
return;
|
|
}
|
|
this.isIdle = true;
|
|
};
|
|
|
|
function isSpace(c) {
|
|
return c === '\t' || c === ' ';
|
|
}
|
|
|
|
function parseBoolean(str) {
|
|
return {
|
|
value: !!parseInt(str, 10),
|
|
msg: null,
|
|
};
|
|
}
|
|
|
|
function parseFloat(str) {
|
|
var x = parseInt(str, 10);
|
|
return {
|
|
value: x,
|
|
msg: isNaN(x) ? ("Number expected: " + str) : null,
|
|
};
|
|
}
|
|
|
|
function parseInteger(str) {
|
|
var x = parseInt(str, 10);
|
|
return {
|
|
value: x,
|
|
msg: isNaN(x) ? ("Integer expected: " + str) : null,
|
|
};
|
|
}
|
|
|
|
function parseRange(str, argInfo) {
|
|
var msg = null;
|
|
var start = null;
|
|
var end = null;
|
|
var parts = str.split(":");
|
|
if (parts.length === 2) {
|
|
start = parseInt(parts[0], 10);
|
|
end = parseInt(parts[1], 10);
|
|
} else if (parts.length === 1) {
|
|
start = parseInt(parts[0], 10);
|
|
if (start === -1 && argInfo.optional) {
|
|
return {
|
|
value: null,
|
|
msg: null,
|
|
};
|
|
}
|
|
end = start + 1;
|
|
}
|
|
if (start == null || end == null || isNaN(start) || isNaN(end)) {
|
|
msg = "Integer or range expected: " + str;
|
|
} else if (start < 0 || end < 0) {
|
|
msg = "Number is negative: " + str;
|
|
} else if (end < start) {
|
|
msg = "Bad song index";
|
|
}
|
|
return {
|
|
value: {
|
|
start: start,
|
|
end: end,
|
|
},
|
|
msg: msg,
|
|
};
|
|
}
|
|
|
|
function parseString(str) {
|
|
return {
|
|
value: str,
|
|
msg: null,
|
|
};
|
|
}
|
|
|
|
function parseId(str) {
|
|
var results = parseInteger.call(this, str);
|
|
if (results.msg) return results;
|
|
var grooveBasinId = this.apiServer.fromMpdId(results.value);
|
|
var msg = grooveBasinId ? null : "No such song";
|
|
return {
|
|
value: grooveBasinId,
|
|
msg: null,
|
|
};
|
|
}
|
|
|
|
function writeTrackInfo(self, dbTrack) {
|
|
self.push("file: " + dbTrack.file + "\n");
|
|
if (dbTrack.mtime != null) {
|
|
self.push("Last-Modified: " + new Date(dbTrack.mtime).toISOString() + "\n");
|
|
}
|
|
if (dbTrack.duration != null) {
|
|
self.push("Time: " + Math.round(dbTrack.duration) + "\n");
|
|
}
|
|
if (dbTrack.artistName) {
|
|
self.push("Artist: " + dbTrack.artistName + "\n");
|
|
}
|
|
if (dbTrack.albumName) {
|
|
self.push("Album: " + dbTrack.albumName + "\n");
|
|
}
|
|
if (dbTrack.albumArtistName) {
|
|
self.push("AlbumArtist: " + dbTrack.albumArtistName + "\n");
|
|
}
|
|
if (dbTrack.genre) {
|
|
self.push("Genre: " + dbTrack.genre + "\n");
|
|
}
|
|
if (dbTrack.name) {
|
|
self.push("Title: " + dbTrack.name + "\n");
|
|
}
|
|
if (dbTrack.track != null) {
|
|
if (dbTrack.trackCount != null) {
|
|
self.push("Track: " + dbTrack.track + "/" + dbTrack.trackCount + "\n");
|
|
} else {
|
|
self.push("Track: " + dbTrack.track + "\n");
|
|
}
|
|
}
|
|
if (dbTrack.composerName) {
|
|
self.push("Composer: " + dbTrack.composerName + "\n");
|
|
}
|
|
if (dbTrack.disc != null) {
|
|
if (dbTrack.discCount != null) {
|
|
self.push("Disc: " + dbTrack.disc + "/" + dbTrack.discCount + "\n");
|
|
} else {
|
|
self.push("Disc: " + dbTrack.disc + "\n");
|
|
}
|
|
}
|
|
if (dbTrack.year != null) {
|
|
self.push("Date: " + dbTrack.year + "\n");
|
|
}
|
|
}
|
|
|
|
function writePlaylistInfo(self, start, end) {
|
|
var trackTable = self.player.libraryIndex.trackTable;
|
|
for (var i = start; i < end; i += 1) {
|
|
var item = self.player.tracksInOrder[i];
|
|
var track = trackTable[item.key];
|
|
writeTrackInfo(self, track);
|
|
self.push("Pos: " + i + "\n");
|
|
self.push("Id: " + self.apiServer.toMpdId(item.id) + "\n");
|
|
}
|
|
}
|
|
|
|
function forEachMatchingTrack(self, filters, caseSensitive, fn) {
|
|
// TODO: support 'any' and 'in' as tag types
|
|
var trackTable = self.player.libraryIndex.trackTable;
|
|
if (!caseSensitive) {
|
|
filters.forEach(function(filter) {
|
|
filter.value = filter.value.toLowerCase();
|
|
});
|
|
}
|
|
for (var key in trackTable) {
|
|
var track = trackTable[key];
|
|
var matches = true;
|
|
for (var filterIndex = 0; filterIndex < filters.length; filterIndex += 1) {
|
|
var filter = filters[filterIndex];
|
|
var filterField = track[filter.field];
|
|
if (!caseSensitive && filterField) filterField = filterField.toLowerCase();
|
|
if (filterField !== filter.value) {
|
|
matches = false;
|
|
break;
|
|
}
|
|
}
|
|
if (matches) fn(track);
|
|
}
|
|
}
|
|
|
|
function forEachListAll(self, args, onTrack, cb) {
|
|
var dirName = args.uri || "";
|
|
var dirEntry = self.player.dirs[dirName];
|
|
if (!dirEntry) return cb(ERR_CODE_NO_EXIST, "Not found");
|
|
printOneDir(dirEntry);
|
|
cb();
|
|
|
|
function printOneDir(dirEntry) {
|
|
var baseName, relPath;
|
|
if (dirEntry.dirName) { // exclude root
|
|
self.push("directory: " + dirEntry.dirName + "\n");
|
|
self.push("Last-Modified: " + new Date(dirEntry.mtime).toISOString() + "\n");
|
|
}
|
|
var dbFilesByPath = self.player.dbFilesByPath;
|
|
for (baseName in dirEntry.entries) {
|
|
relPath = path.join(dirEntry.dirName, baseName);
|
|
var dbTrack = dbFilesByPath[relPath];
|
|
if (dbTrack) onTrack(dbTrack);
|
|
}
|
|
for (baseName in dirEntry.dirEntries) {
|
|
relPath = path.join(dirEntry.dirName, baseName);
|
|
var childEntry = self.player.dirs[relPath];
|
|
if (childEntry) {
|
|
printOneDir(childEntry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish) {
|
|
if (args.length < 2) {
|
|
cb(ERR_CODE_ARG, "too few arguments for \"find\"");
|
|
return;
|
|
}
|
|
if (args.length % 2 !== 0) {
|
|
cb(ERR_CODE_ARG, "incorrect arguments");
|
|
return;
|
|
}
|
|
var filters = [];
|
|
for (var i = 0; i < args.length; i += 2) {
|
|
var tagType = tagTypes[args[i].toLowerCase()];
|
|
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known");
|
|
filters.push({
|
|
field: tagType.grooveTag,
|
|
value: args[i+1],
|
|
});
|
|
forEachMatchingTrack(self, filters, caseSensitive, onTrack);
|
|
}
|
|
onFinish();
|
|
}
|
|
|
|
function handleUpdate(self, args, forceRescan, cb) {
|
|
var dirEntry = self.player.dirs[args.uri || ""];
|
|
if (!dirEntry) {
|
|
cb(ERR_CODE_ARG, "Malformed path");
|
|
return;
|
|
}
|
|
self.player.requestUpdateDb(dirEntry.dirName, forceRescan);
|
|
self.push("updating_db: 1\n");
|
|
cb();
|
|
}
|
|
|
|
function findOrSearch(self, args, caseSensitive, cb) {
|
|
parseFindArgs(self, args, caseSensitive, onTrack, cb, cb);
|
|
function onTrack(track) {
|
|
writeTrackInfo(self, track);
|
|
}
|
|
}
|
|
|
|
function findOrSearchAdd(self, args, caseSensitive, cb) {
|
|
var keys = [];
|
|
parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish);
|
|
|
|
function onTrack(track) {
|
|
keys.push(track.key);
|
|
}
|
|
|
|
function onFinish() {
|
|
self.player.appendTracks(keys, false);
|
|
cb();
|
|
}
|
|
}
|
|
|
|
function swapItems(self, item1, item2, cb) {
|
|
if (!item1 || !item2) return cb(ERR_CODE_ARG, "No such song");
|
|
var o = {};
|
|
o[item1.id] = {sortKey: item2.sortKey};
|
|
o[item2.id] = {sortKey: item1.sortKey};
|
|
self.player.movePlaylistItems(o);
|
|
cb();
|
|
}
|
|
|
|
function addCmd(self, args, cb) {
|
|
var dbFilesByPath = self.player.dbFilesByPath;
|
|
var dbFile = dbFilesByPath[args.uri];
|
|
|
|
if (dbFile) {
|
|
self.player.appendTracks([dbFile.key], false);
|
|
cb();
|
|
return;
|
|
}
|
|
|
|
var keys = [];
|
|
var dirEntry = self.player.dirs[args.uri];
|
|
if (!dirEntry) return cb(ERR_CODE_NO_EXIST, "Not found");
|
|
addDir(dirEntry);
|
|
if (keys.length === 0) {
|
|
cb(ERR_CODE_NO_EXIST, "Not found");
|
|
return;
|
|
}
|
|
self.player.appendTracks(keys, false);
|
|
cb();
|
|
|
|
function addDir(dirEntry) {
|
|
var baseName;
|
|
for (baseName in dirEntry.entries) {
|
|
var relPath = path.join(dirEntry.dirName, baseName);
|
|
var dbFile = dbFilesByPath[relPath];
|
|
if (dbFile) keys.push(dbFile.key);
|
|
}
|
|
for (baseName in dirEntry.dirEntries) {
|
|
var childEntry = self.player.dirs[path.join(dirEntry.dirName, baseName)];
|
|
addDir(childEntry);
|
|
}
|
|
}
|
|
}
|
|
|
|
function statsCmd(self, args, cb) {
|
|
var uptime = Math.floor((new Date() - bootTime) / 1000);
|
|
|
|
var libraryIndex = self.player.libraryIndex;
|
|
var artists = libraryIndex.artistList.length;
|
|
var albums = libraryIndex.albumList.length;
|
|
var songs = 0;
|
|
var trackTable = libraryIndex.trackTable;
|
|
var dbPlaytime = 0;
|
|
for (var key in trackTable) {
|
|
var dbTrack = trackTable[key];
|
|
songs += 1;
|
|
dbPlaytime += dbTrack.duration;
|
|
}
|
|
dbPlaytime = Math.floor(dbPlaytime);
|
|
var dbUpdate = Math.floor(new Date().getTime() / 1000);
|
|
self.push("artists: " + artists + "\n");
|
|
self.push("albums: " + albums + "\n");
|
|
self.push("songs: " + songs + "\n");
|
|
self.push("uptime: " + uptime + "\n");
|
|
self.push("playtime: 0\n"); // TODO keep track of this?
|
|
self.push("db_playtime: " + dbPlaytime + "\n");
|
|
self.push("db_update: " + dbUpdate + "\n");
|
|
|
|
cb();
|
|
}
|
|
|
|
function statusCmd(self, args, cb) {
|
|
var volume = Math.round(self.player.volume * 100);
|
|
|
|
var repeat, single;
|
|
switch (self.player.repeat) {
|
|
case Player.REPEAT_ONE:
|
|
repeat = 1;
|
|
single = 1;
|
|
break;
|
|
case Player.REPEAT_ALL:
|
|
repeat = 1;
|
|
single = 0;
|
|
break;
|
|
case Player.REPEAT_OFF:
|
|
repeat = 0;
|
|
single = +self.apiServer.singleMode;
|
|
break;
|
|
}
|
|
var consume = +self.player.dynamicModeOn;
|
|
var playlistLength = self.player.tracksInOrder.length;
|
|
var currentTrack = self.player.currentTrack;
|
|
var state;
|
|
if (self.player.isPlaying) {
|
|
state = 'play';
|
|
} else if (currentTrack) {
|
|
state = 'pause';
|
|
} else {
|
|
state = 'stop';
|
|
}
|
|
|
|
var song = null;
|
|
var songId = null;
|
|
var nextSong = null;
|
|
var nextSongId = null;
|
|
var elapsed = null;
|
|
var time = null;
|
|
var trackTable = self.player.libraryIndex.trackTable;
|
|
if (currentTrack) {
|
|
song = currentTrack.index;
|
|
songId = self.apiServer.toMpdId(currentTrack.id);
|
|
var nextTrack = self.player.tracksInOrder[currentTrack.index + 1];
|
|
if (nextTrack) {
|
|
nextSong = nextTrack.index;
|
|
nextSongId = self.apiServer.toMpdId(nextTrack.id);
|
|
}
|
|
|
|
var dbTrack = trackTable[currentTrack.key];
|
|
elapsed = self.player.getCurPos();
|
|
time = Math.round(elapsed) + ":" + Math.round(dbTrack.duration);
|
|
}
|
|
|
|
self.push("volume: " + volume + "\n");
|
|
self.push("repeat: " + repeat + "\n");
|
|
self.push("random: 0\n");
|
|
self.push("single: " + single + "\n");
|
|
self.push("consume: " + consume + "\n");
|
|
self.push("playlist: 0\n"); // TODO what to do with this?
|
|
self.push("playlistlength: " + playlistLength + "\n");
|
|
self.push("xfade: 0\n");
|
|
self.push("mixrampdb: 0.000000\n");
|
|
self.push("mixrampdelay: nan\n");
|
|
self.push("state: " + state + "\n");
|
|
if (song != null) {
|
|
self.push("song: " + song + "\n");
|
|
self.push("songid: " + songId + "\n");
|
|
if (nextSong != null) {
|
|
self.push("nextsong: " + nextSong + "\n");
|
|
self.push("nextsongid: " + nextSongId + "\n");
|
|
}
|
|
self.push("time: " + time + "\n");
|
|
self.push("elapsed: " + elapsed + "\n");
|
|
self.push("bitrate: 192\n"); // TODO make this not hardcoded?
|
|
self.push("audio: 44100:24:2\n"); // TODO make this not hardcoded?
|
|
}
|
|
|
|
cb();
|
|
}
|
|
|
|
function extend(o, src) {
|
|
for (var key in src) o[key] = src[key];
|
|
return o;
|
|
}
|
|
|
|
function noop() {}
|