Despite the name "lua-resty-mpd" this will work on regular Lua as well!
This is a library for interacting with Music Player Daemon, over TCP sockets or Unix sockets.
It works with OpenResty's cosockets, cqueues sockets, and LuaSocket. It will try to auto-detect the most appropriate library to use, you can also specify if you'd like to use a particular library.
You can use this library synchronously or asynchronously in nginx and cqueues, on Luasocket, you can only perform synchronous operations.
You can use luarocks
:
luarocks install lua-resty-mpd
Or OPM:
opm get jprjr/lua-resty-mpd
Or grab the amalgamation file from this repo (under lib/
), it's all
the sources of this module combined into a single file.
Since this can use multiple socket libraries, I don't list them as dependencies, you'll need to install luasocket or cqueues on your own. No other external dependencies are required.
Here's an script that just loops over calls to idle().
local mpd = require'resty.mpd'
local client = mpd()
client:settimeout(1000) -- set a low timeout just to demo idle timing out.
client:connect('tcp://127.0.0.1:6600')
-- loop until we've read 5 events
local events = 5
while events > 0 do
local res, err = client:idle()
if err and err ~= 'socket:timeout' then
print('Error: ' .. err)
os.exit(1)
end
for _,event in ipairs(res) do
print('Event: ' .. event)
events = events - 1
-- do something based on the event
end
end
client:close()
Here's an example of asynchronous usage under openresty, using nginx threads:
local mpd = require'resty.mpd'
local client = mpd()
-- If MPD isn't running, bail
assert(client:connect('127.0.0.1'))
-- Holds references to our threads
local threads = {}
-- Each entry is the command to run and a
-- key that should be in the response.
--
-- We do this to verify that each thread is
-- getting the correct response (if we called
-- "status" but didn't get the "state" key, then
-- something went really, really wong.
local commands = {
{ 'status', 'state' },
{ 'stats', 'uptime' },
{ 'replay_gain_status','replay_gain_mode'},
}
-- Start a loop around client:idle().
-- This will write out any idle events (should be zero
-- unless you happen to do something to MPD in the
-- 2 seconds that this script runs for), and
-- exits if it receives an error.
table.insert(threads,ngx.thread.spawn(function()
while true do
print('calling client:idle()')
local events, err = client:idle()
if err and err ~= 'socket:timeout' then
print(string.format('client:idle() error %s',err))
return false, err
end
print(string.format('client:idle() returned %d events',#events))
for _,event in ipairs(events) do
print(string.format('client:idle() event: %s',event))
end
end
end))
-- Start threads to send individual commands.
-- These will interrupt the idle call and force
-- idle to return zero events.
for i=1,#commands do
table.insert(threads,ngx.thread.spawn(function()
local func = commands[i][1]
local key = commands[i][2]
print(string.format('calling client:%s()',func))
local res, err = client[func](client)
if err then
print(string.format('client:%s() error: %s',func,err))
return false, err
end
if not res[key] then
err = string.format('missing key %s',key)
print(string.format('client:%s() error: %s',func,err))
return false,err
end
print(string.format('client:%s() success',func))
return true
end))
end
-- Shut everything down after 2 seconds.
table.insert(threads,ngx.thread.spawn(function()
ngx.sleep(2)
print('calling client:close()')
local ok, err = client:close()
if err then
print(string.format('client:close() err: ' .. err))
end
end))
-- Rejoin all the threads
for i=1,#threads do
local ok, err = ngx.thread.wait(threads[i])
if not ok then error(err) end
end
This is basically the same as the nginx example but with cqueues. No comments in this one since it's virtually identical.
local cqueues = require'cqueues'
local mpd = require'resty.mpd'
local loop = cqueues.new()
local client = mpd.new()
assert(client:connect('127.0.0.1'))
local commands = {
{ 'status', 'state' },
{ 'stats', 'uptime' },
{ 'replay_gain_status','replay_gain_mode'},
}
loop:wrap(function()
while true do
print('calling client:idle()')
local events, err = client:idle()
if err and err ~= 'socket:timeout' then
print(string.format('client:idle() error %s',err))
return false, err
end
print(string.format('client:idle() returned %d events',#events))
for _,event in ipairs(events) do
print(string.format('client:idle() event: %s',event))
end
end
end)
for i=1,#commands do
loop:wrap((function()
local func = commands[i][1]
local key = commands[i][2]
print(string.format('calling client:%s()',func))
local res, err = client[func](client)
if err then
print(string.format('client:%s() error: %s',func,err))
return false, err
end
if not res[key] then
err = string.format('missing key %s',key)
print(string.format('client:%s() error: %s',func,err))
return false,err
end
print(string.format('client:%s() success',func))
return true
end))
end
loop:wrap(function()
cqueues.sleep(2)
print('calling client:close()')
local ok, err = client:close()
if err then
print(string.format('client:close() err: ' .. err))
end
end)
assert(loop:loop())
Returns the socket/condition variable library being used by all
new clients, name
is an optional parameter to choose a particular
library. Valid name
values are:
nginx
- nginx cosockets.cqueues
- cqueues.luasocket
- luasocket.
If a library isn't available, it will instead return the default library.
The returned value is the library in use, you can check the .name
field to see which specific library it is. Example:
lib = mpd:backend('luasocket')
assert(lib.name == 'luasocket')
Creates a new client instance.
You can also call this as mpd.new()
Returns the socket library being used by this particular client,
it behaves the same as mpd:backend
above.
nginx
- nginx cosockets.cqueues
- cqueues.luasocket
- luasocket.
If your client has already called connect
, you're unable to
change the library, you'll need to call close
, change the
library, then reconnect.
Connects to MPD, supports tcp and unix socket connections.
The URL should be in one of two formats:
tcp://host:port
unix:/path/to/socket
host:port
host
(implied port 6600)tcp://host
(implied port 6600)path/to/socket
(does not have to be absolute)
You can also call this as client:connect(host,port)
for TCP connections.
Sets the socket timeout in milliseconds, or use nil
to
represent no timeout.
By default, clients have no timeout and will block forever, please note this includes the nginx/OpenResty backend.
(Technically OpenResty doesn't support having no timeout, so it's set to the maximum value).
Closes the connection, forces any pending operations to error out.
I used to list every implemented function, instead I recommend just looking up the MPD protocol documentation: https://www.musicpd.org/doc/protocol/command_reference.html
Commands return either a table of results or a boolean as the first return value, and an error (if any) as the second.
If the error is from MPD, the message will begin with the string
mpd:
followed by the error number, and the error message in parenthesis,
example:
mpd:50(No such file)
Any socket-related error messaged will begin with socket:
, these
are non-recoverable (you should disconnect/quit/etc). The exception
to this is idle
, see below.
When idle
times out, it automatically sends noidle
to cancel
the current idle
request. Otherwise, your scheduler (nginx threads,
cqueues, etc) could potentially send a command before you
call noidle
from your app, since when idle
ends the next queued
command gets called.
What this means is idle
will always return a list of events,
which may be an empty table in the case of a timeout, you should
check the value of err
to see if there was a timeout (if err
is nil
, then the idle
was canceled intentionally via another
command being queued).
Generally-speaking you just send values like listed in the MPD protocol documentation.
For example, the MPD protocol documentation has the following prototype for the list
command:
list {TYPE} {FILTER} [group {GROUPTYPE}]
This would translate to:
response, err = client:list(type,filter,'group',grouptype)
For functions that take ranges, you use separate parameters for each part of the range. For
example, using the find
command, which lets you specify a window
range:
find {FILTER} [sort {TYPE}] [window {START:END}]
This becomes
response, err = client:find(filter,'sort',type,'window',start,end)
For optional parameters, just leave them out. If you wanted to call
find
with just a filter and window:
response, err = client:find(filter,'window',start,end)
Or for just a filter:
response, err = client:find(filter)
Groups (and nested groups) are fully supported for commands that use them, groups will return an array-like table instead of an object, so as an example:
local res, err = client:list('title','group','album','group','albumartist')
res
will be an array-like table, each entry will contain a title
, album
, and
albumartist
key.
Adds three missing commands:
addtagid
outputset
volume
Behavior change - MPD can return multiple responses with the same key. For example,
if a FLAC file lists multiple COMPOSER
tags, MPD will return:
Album: An Album
Artist: An Artist
Composer: First Composer
Composer: Second Composer
Before version 5.2.0, lua-resty-mpd would only return the last tag, so your response would be something like:
{
album = "An Album",
artist = "An Artist",
composer = "Second Composer",
}
Starting with version 5.2.0, if a duplicate key is detected, then the value will be turned into a table, like:
{
album = "An Album",
artist = "An Artist",
composer = { "First Composer", "Second Composer" },
}
Adds a missing command: deleteid
Adds the new binarylimit
protocol command.
Minor bugfix, return a socket error if not connected.
Complete rewrite, client commands (list, play, etc) should be compatible with older versions, but functions for choosing backend libraries are not.
This was rewritten with asynchronous operations in mind, the
new version can auto-call noidle
as needed without any
hacks like in version 3.
Reverts the automatic noidle via condvar/semaphore, it turned out this wasn't a good idea.
Retains previous enhancements of handling binary responses and being compatible up to MPD 0.22.0.
Bug fix with condition variables/semaphores, seems to be way more reliable now at calling noidle.
Major version bump.
Version 3.0.0 tries to detect if a command is sent
while waiting on an IDLE
command to finish,
and automatically calls noidle
, it does this through
nginx semaphores and cqueues condition variables.
This new behavior is not supported on LuaSocket, you'll
need to call noidle
on your own.
Also handles binary responses and should handle all MPD protocol functions as of MPD 0.22.0.
New feature, now supports cqueues socket library.
Library is auto-detected with the following priority:
- nginx cosockets
- cqueues
- luasocket
This can be overridden at a global level, or per-client.
Bugfix: escape quotes/backslashes when sending.
New feature: new
takes an optional table, see documentation.
Fixes potential race condition in noidle
.
Uses correct socket timeout scale (seconds with luasocket, milliseconds in nginx).
Fixes timed out operations.
idle
change
In previous versions, calling idle
would return a string, with
a special string ("interrupted") in the case of the idle being
canceled with noidle
. In MPD, a call to idle
can return multiple
events.
idle
now returns an array of events, with an empty array used to
represent idle
being canceled.
commands
and notcommands
change
Previous versions returned a table with each command being a key set
to true
.
commands
and notcommands
now returns an array of commands.
Previous versions required the URL to match the formats:
tcp://host:port
unix:/path/to/socket
The URL can additionally use the formats:
host:port
host
(implied port 6600)tcp://host
(implied port 6600)path/to/socket
(does not have to be absolute)
I still recommend the tcp://
or unix:
prefixes to be explicit
MIT license (see LICENSE
)