local lgi = require "lgi" local GLib = lgi.GLib local Gio = lgi.Gio local mpc = {} local function parse_password(host) -- This function is based on mpd_parse_host_password() from libmpdclient local position = string.find(host, "@") if not position then return host end return string.sub(host, position + 1), string.sub(host, 1, position - 1) end function mpc.new(host, port, password, error_handler, ...) host = host or os.getenv("MPD_HOST") or "localhost" port = port or os.getenv("MPD_PORT") or 6600 if not password then host, password = parse_password(host) end local self = setmetatable({ _host = host, _port = port, _password = password, _error_handler = error_handler or function() end, _connected = false, _try_reconnect = false, _idle_commands = { ... } }, { __index = mpc }) self:_connect() return self end function mpc:_error(err) self._connected = false self._error_handler(err) self._try_reconnect = not self._try_reconnect if self._try_reconnect then self:_connect() end end function mpc:_connect() if self._connected then return end -- Reset all of our state self._reply_handlers = {} self._pending_reply = {} self._idle_commands_pending = false self._idle = false self._connected = true -- Set up a new connection local address if string.sub(self._host, 1, 1) == "/" then -- It's a unix socket address = Gio.UnixSocketAddress.new(self._host) else -- Do a TCP connection address = Gio.NetworkAddress.new(self._host, self._port) end local client = Gio.SocketClient() local conn, err = client:connect(address) if not conn then self:_error(err) return false end local input, output = conn:get_input_stream(), conn:get_output_stream() self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input) -- Read the welcome message self._input:read_line() if self._password and self._password ~= "" then self:_send("password " .. self._password) end -- Set up the reading loop. This will asynchronously read lines by -- calling itself. local do_read do_read = function() self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res) local line, err = obj:read_line_finish(res) -- Ugly API. On success we get string, length-of-string -- and on error we get nil, error. Other versions of lgi -- behave differently. if line == nil or tostring(line) == "" then err = "Connection closed" end if type(err) ~= "number" then self._output, self._input = nil, nil self:_error(err) else do_read() line = tostring(line) if line == "OK" or line:match("^ACK ") then local success = line == "OK" local arg if success then arg = self._pending_reply else arg = { line } end local handler = self._reply_handlers[1] table.remove(self._reply_handlers, 1) self._pending_reply = {} handler(success, arg) else local _, _, key, value = string.find(line, "([^:]+):%s(.+)") if key then self._pending_reply[string.lower(key)] = value end end end end) end do_read() -- To synchronize the state on startup, send the idle commands now. As a -- side effect, this will enable idle state. self:_send_idle_commands(true) return self end function mpc:_send_idle_commands(skip_stop_idle) -- We use a ping to unset this to make sure we never get into a busy -- loop sending idle / unidle commands. Next call to -- _send_idle_commands() might be ignored! if self._idle_commands_pending then return end if not skip_stop_idle then self:_stop_idle() end self._idle_commands_pending = true for i = 1, #self._idle_commands, 2 do self:_send(self._idle_commands[i], self._idle_commands[i+1]) end self:_send("ping", function() self._idle_commands_pending = false end) self:_start_idle() end function mpc:_start_idle() if self._idle then error("Still idle?!") end self:_send("idle", function(success, reply) if reply.changed then -- idle mode was disabled by mpd self:_send_idle_commands() end end) self._idle = true end function mpc:_stop_idle() if not self._idle then error("Not idle?!") end self._output:write("noidle\n") self._idle = false end function mpc:_send(command, callback) if self._idle then error("Still idle in send()?!") end self._output:write(command .. "\n") table.insert(self._reply_handlers, callback or function() end) end function mpc:send(...) self:_connect() if not self._connected then return end local args = { ... } if not self._idle then error("Something is messed up, we should be idle here...") end self:_stop_idle() for i = 1, #args, 2 do self:_send(args[i], args[i+1]) end self:_start_idle() end function mpc:toggle_play() self:send("status", function(success, status) if status.state == "stop" then self:send("play") else self:send("pause") end end) end --[[ -- Example on how to use this (standalone) local host, port, password = nil, nil, nil local m = mpc.new(host, port, password, error, "status", function(success, status) print("status is", status.state) end) GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function() -- Test command submission m:send("status", function(_, s) print(s.state) end, "currentsong", function(_, s) print(s.title) end) m:send("status", function(_, s) print(s.state) end) -- Force a reconnect GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function() m._conn:close() end) end) GLib.MainLoop():run() --]] return mpc