UNPKG

node-red-contrib-mpd2

Version:

Node-RED nodes to interact with Music Player Daemon

254 lines (216 loc) 9.96 kB
'use strict'; 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); };