--- /dev/null
+-- Originally from https://awesomewm.org/recipes/mpc/
+
+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