node-red-contrib-mpd2
Version:
Node-RED nodes to interact with Music Player Daemon
254 lines (216 loc) • 9.96 kB
JavaScript
;
const mpd = require('mpd2');
const { cmd } = mpd;
module.exports = function(RED) {
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_BASE_DELAY = 1000;
function MPDNode(config) {
RED.nodes.createNode(this, config);
const node = this;
this.server = RED.nodes.getNode(config.server);
node.command = config.command;
node.client = null;
node.connected = false;
node.connecting = false;
node.reconnectAttempts = 0;
function setConnectionStatus(connected, connecting, text, fill = 'red', shape = 'ring') {
node.connected = connected;
node.connecting = connecting;
node.status({fill, shape, text});
}
function encodeUrlForCommand(url) {
if (!url || typeof url !== 'string') return url;
if (url.startsWith('http://') || url.startsWith('https://')) {
const protocol = url.startsWith('https://') ? 'https://' : 'http://';
const urlWithoutProtocol = url.substring(protocol.length);
return protocol + urlWithoutProtocol.split('').map(char => {
if (char === '%') return char;
return encodeURI(char);
}).join('');
}
if (!url.startsWith('"') && !url.endsWith('"') && !url.startsWith("'") && !url.endsWith("'")) {
return '"' + url + '"';
}
return url;
}
function parseResponse(command, response) {
const parseMap = {
status: mpd.parseObject,
stats: mpd.parseObject,
currentsong: mpd.parseObject,
listplaylists: mpd.parseList,
list: mpd.parseList,
listall: mpd.parseList
};
for (const key in parseMap) {
if (command.startsWith(key)) {
return parseMap[key](response);
}
}
if (command === 'playlistinfo' || command === 'playlistid' || command === 'listplaylistinfo') {
return mpd.parseNestedList(response);
}
return mpd.autoparseValues(response);
}
async function connect() {
if (node.connecting || node.connected) return;
if (node.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
node.error('Max reconnection attempts reached');
return;
}
node.connecting = true;
node.reconnectAttempts++;
setConnectionStatus(false, true, 'Connecting...', 'yellow', 'ring');
const connectionConfig = {
host: node.server.host,
port: node.server.port
};
if (node.server.password) {
connectionConfig.password = node.server.password;
}
try {
const client = await mpd.connect(connectionConfig);
node.client = client;
node.reconnectAttempts = 0;
setConnectionStatus(true, false, 'Connected', 'green', 'dot');
client.on('system', async (subsystem) => {
try {
if (subsystem === 'player') {
const statusResponse = await client.sendCommand('status');
let status;
try {
status = mpd.parseObject(statusResponse);
} catch (e) {
status = statusResponse;
}
if (status.state === 'play') {
const songResponse = await client.sendCommand('currentsong');
let songInfo;
try {
songInfo = mpd.parseObject(songResponse);
} catch (e) {
songInfo = songResponse;
}
node.send([null, {
event: subsystem,
result: status,
currentsong: songInfo
}]);
} else {
node.send([null, {
event: subsystem,
result: status
}]);
}
} else {
node.send([null, { event: subsystem }]);
}
} catch (err) {
node.error(`System event error: ${err.message}`, {error: err});
node.send([null, { event: subsystem, error: err.message }]);
}
});
client.on('close', () => {
setConnectionStatus(false, false, 'Connection closed', 'red', 'ring');
});
client.on('error', (err) => {
node.error(`MPD error: ${err.message}`, {error: err});
setConnectionStatus(false, false, 'ERROR: ' + err.message, 'red', 'dot');
});
} catch (err) {
node.connecting = false;
node.error(`MPD connection error: ${err.message}`, {error: err});
setConnectionStatus(false, false, 'Connection failed', 'red', 'dot');
const delay = RECONNECT_BASE_DELAY * Math.pow(2, node.reconnectAttempts - 1);
setTimeout(connect, delay);
}
}
node.on('input', function(msg) {
if (!node.connected) {
connect();
return;
}
let command = '';
let args = [];
if (typeof msg.payload === 'string') {
command = msg.payload;
if (command.startsWith('add ')) {
const spaceIndex = command.indexOf(' ');
if (spaceIndex > -1) {
const url = command.substring(spaceIndex + 1);
command = 'add ' + encodeUrlForCommand(url);
}
}
} else if (typeof msg.payload === 'object' && msg.payload !== null) {
if (msg.payload.command) {
command = msg.payload.command;
if (msg.payload.args) {
args = Array.isArray(msg.payload.args) ? msg.payload.args : [msg.payload.args];
if (args.length > 0 && typeof args[0] === 'string' &&
(args[0].startsWith('http://') || args[0].startsWith('https://'))) {
args[0] = encodeUrlForCommand(args[0]);
}
}
}
}
if (!command && node.command) {
command = node.command;
}
if (command) {
const commandMap = {
'toggle': 'pause',
'current': 'currentsong',
'ls': 'lsinfo'
};
if (commandMap[command]) {
command = commandMap[command];
}
const mpdCommand = args.length > 0 ? cmd(command, args) : command;
node.client.sendCommand(mpdCommand)
.then(response => {
try {
msg.result = parseResponse(command, response);
} catch (e) {
msg.result = response;
}
node.send([msg, null]);
})
.catch(err => {
msg.error = err;
node.send([msg, null]);
});
}
});
node.on('close', function(done) {
if (node.client) {
node.client.disconnect()
.then(() => {
node.connected = false;
node.client = null;
done();
})
.catch(err => {
node.error(`Disconnect error: ${err.message}`, {error: err});
node.client = null;
done();
});
} else {
done();
}
});
if (this.server) {
connect();
} else {
node.error("MPD server configuration missing");
setConnectionStatus(false, false, "MPD server configuration missing", 'red', 'dot');
}
}
function MPDServerNode(config) {
RED.nodes.createNode(this, config);
this.host = config.host || 'localhost';
this.port = config.port || 6600;
this.password = config.password || '';
}
RED.nodes.registerType("mpd2", MPDNode);
RED.nodes.registerType("mpd2-server", MPDServerNode);
};