working-mpd-client
Version:
371 lines (291 loc) • 9.5 kB
JavaScript
/* based on https://github.com/andrewrk/mpd.js */
/* Public methods:
init
destroy
sendCommand
sendCommandList
*/
/* Public events:
destroyed
ready
ready-core
ready-idle
reconnecting
reconnecting-core
reconnecting-idle
reconnected
reconnected-core
reconnected-idle
disconnected
disconnected-core
disconnected-idle
connected-core
connected-idle
changed
*/
const EventEmitter = require('events').EventEmitter
const util = require('util')
const net = require('net')
const DEBUG = false
module.exports = MpdClient
util.inherits(MpdClient, EventEmitter)
function MpdClient(params) {
// example params:
// { connectOptions: { host: 'localhost'port: 6600 },
// reconnectOptions: { isUse: true, reconnectDelay: 1000 } }
this._paramConnectOptions = params.connectOptions
this._paramReconnectOptions = params.reconnectOptions
this._valueIsInit = false
this._valueCore = {
client: null,
callbackQueue: [],
dateBuffer: '',
isReady: false,
name: 'core'
}
this._valueIdle = {
client: null,
callbackQueue: [],
dateBuffer: '',
isReady: false,
name: 'idle'
}
}
MpdClient.prototype.init = function() {
if (!this._valueIsInit) {
this._funcClientInit(this._valueCore)
this._funcClientInit(this._valueIdle)
this.on('ready-idle', this._funcIdleInit.bind(this))
this.on('reconnected-idle', this._funcIdleInit.bind(this))
// a connection will be interrupted if no messages send in 4 minutes
this.on('ready-core', this._funcNoIdleInit.bind(this))
this._valueIsInit = true
}
return this
}
MpdClient.prototype.destroy = function() {
if (this._valueIsInit) {
this._valueCore.client.removeAllListeners()
this._valueIdle.client.removeAllListeners()
if (!this._valueCore.client.destroyed) {
this._valueCore.client.destroy()
this.emit('disconnected-core')
this.emit('disconnected')
}
if (!this._valueIdle.client.destroyed) {
this._valueIdle.client.destroy()
this.emit('disconnected-idle')
}
this._funcCloseAllRequests(this._valueCore.client, 'core')
this._funcCloseAllRequests(this._valueIdle.client, 'idle')
this._valueCore.dataBuffer = ''
this._valueIdle.dataBufer = ''
this._valueCore.isReady = false
this._valueIdle.isReady = false
this._valueIsInit = false
this.emit('destroyed')
}
return this
}
MpdClient.prototype.sendCommand = function(rawCommand, callback) {
// this method sends the command and push the callback into
// the callbacks queue as a MPD server processes commands one by one
if (!callback) {
callback = createDummyCallback(rawCommand).bind(this)
}
this._sendCommandWithCallback(
this._valueCore.client,
this._valueCore.callbackQueue,
rawCommand,
callback)
return this
}
MpdClient.prototype.sendCommandList = function(rawCommandList, callback) {
if (!callback) {
callback = createDummyCallback(rawCommandList).bind(this)
}
const commandList = rawCommandList.map((rawCommand) => {
return new Command(rawCommand)
})
const fullCmd = 'command_list_begin\n' + commandList.join('') + 'command_list_end'
this.sendCommand(fullCmd, callback)
return this
}
MpdClient.prototype._sendCommandWithCallback = function(client, queue, rawCommand, callback) {
queue.push(callback)
this._funcClientSendData(client, new Command(rawCommand))
}
MpdClient.prototype._sendCommandWithoutCallback = function(client, rawCommand) {
this._funcClientSendData(client, new Command(rawCommand))
}
MpdClient.prototype._funcClientSendData = function(client, data) {
if (!client.destroyed) {
client.write(data.toString())
}
}
function createDummyCallback(command) {
return function(err) {
if (err) {
this.emit('warn', {
desc: 'the server responded with an error to the command that did not register a callback!',
command,
error: err
})
}
}
}
function Command(rawCommand) {
if (typeof rawCommand === 'string') {
this.cmd = rawCommand
this.args = []
} else if (!rawCommand.hasOwnProperty('args')) {
this.cmd = rawCommand.cmd
this.args = []
} else if (!util.isArray(rawCommand.args)) {
this.cmd = rawCommand.cmd
this.args = [rawCommand.args]
} else {
this.cmd = rawCommand.cmd
this.args = rawCommand.args
}
}
Command.prototype.toString = function() {
return this.cmd + ' ' + this.args.map((arg) => {
// escapes double quotes
return (arg !== undefined) ? ('"' + arg.toString().replace(/"/g, '\\"') + '"') : ' '
}).join(' ') + '\n'
}
// Core
MpdClient.prototype._funcClientInit = function(clientProps) {
clientProps.client = net.connect(this._paramConnectOptions)
clientProps.client.on('connect', () => {
this.emit('connected-' + clientProps.name)
if (clientProps.name === 'core') {
this.emit('connected')
}
})
clientProps.client.on('error', (error) => {
this.emit('error', {
desc: 'The connection has been interrupted. ' + clientProps.name,
error
})
})
clientProps.client.on('data', this._funcCreateClientOnDataHandler(clientProps).bind(this))
clientProps.client.on('close', this._funcCreateClientReconecter(clientProps).bind(this))
clientProps.client.setEncoding('utf8')
}
MpdClient.prototype._funcCreateClientReconecter = function(clientProps) {
return function() {
clientProps.client.removeAllListeners()
this._funcCloseAllRequests(clientProps.callbackQueue, clientProps.name)
clientProps.dateBuffer = ''
if (this._paramReconnectOptions.isUse) {
this.emit('warn', {
desc: 'The client attempts to restore the connection. ' + clientProps.name,
reconnectDelay: this._paramReconnectOptions.reconnectDelay
})
this.emit('reconnecting-' + clientProps.name)
if (clientProps.name === 'core') {
this.emit('reconnecting')
}
setTimeout(() => {
this._funcClientInit(clientProps)
}, this._paramReconnectOptions.reconnectDelay)
} else {
this.emit('error', {
desc: 'The connection has been interrupted. A reconnect attempt will not be performed.'
})
this.emit('disconnected-' + clientProps.name)
if (clientProps.name === 'core') {
this.emit('disconnected')
}
}
}
}
MpdClient.prototype._funcCloseAllRequests = function(queue, clientName) {
while (queue.length) {
const callback = queue.shift()
callback({
desc: 'The connection has been closed. ' + clientName
})
}
}
MpdClient.prototype._funcCreateClientOnDataHandler = function(clientProps) {
return function(data) {
clientProps.dateBuffer += data
let welcom = clientProps.dateBuffer.match(/(^OK MPD.*?\n)/m)
let end = clientProps.dateBuffer.match(/(^OK(?:\n|$)|^ACK\s\[.*?\].*(?:\n|$))/m)
if (DEBUG) {
console.log('-------' + clientProps.name + '-------')
console.log('new data part')
console.log(data)
console.log('-------' + clientProps.name + '-------')
console.log()
console.log('-------' + clientProps.name + '-------')
console.log('dateBuffer')
console.log(clientProps.dateBuffer)
console.log('-------' + clientProps.name + '-------')
console.log()
console.log('-------' + clientProps.name + '-------')
console.log('queue length')
console.log(clientProps.callbackQueue.length)
console.log('-------' + clientProps.name + '-------')
console.log()
}
while (welcom || end) {
if (welcom) {
if (!clientProps.isReady) {
clientProps.isReady = true
if (this._valueIdle.isReady && this._valueCore.isReady) this.emit('ready')
this.emit('ready-' + clientProps.name)
} else {
this.emit('reconnected-' + clientProps.name)
if (clientProps.name === 'core') this.emit('reconnected')
}
clientProps.dateBuffer = clientProps.dateBuffer.substring(welcom[0].length + welcom.index)
} else {
const result = clientProps.dateBuffer.substring(0, end.index)
clientProps.dateBuffer = clientProps.dateBuffer.substring(end[0].length + end.index)
const callback = clientProps.callbackQueue.shift()
if (end[0].match(/^ACK\s\[.*?\].*(?:\n|$)/)) callback(end[0])
else callback(null, result.trim())
}
welcom = clientProps.dateBuffer.match(/(^OK MPD.*?\n)/m)
end = clientProps.dateBuffer.match(/(^OK(?:\n|$)|^ACK\s\[.*?\].*(?:\n|$))/m)
}
}
}
// Idle
MpdClient.prototype._funcIdleHandler = function(err, result) {
if (err) {
this.emit('warn', {
desc: 'The server responded with an error on a idle request.',
error: err
})
} else {
result.split('\n').forEach((event) => {
// changed: player
const changed = event.substring('changed: '.length)
this.emit('changed', changed)
})
}
this._funcIdleInit()
}
MpdClient.prototype._funcIdleInit = function() {
if (!this._valueIdle.client.destroyed) {
this._sendCommandWithCallback(
this._valueIdle.client,
this._valueIdle.callbackQueue,
'idle',
this._funcIdleHandler.bind(this))
}
}
// Ping
MpdClient.prototype._funcNoIdleInit = function() {
setInterval(() => {
if (this._valueCore.client) {
this._sendCommandWithoutCallback(this._valueCore.client, 'noidle')
}
}, 60 * 1000)
}