-- Originally from https://awesomewm.org/recipes/mpc/ local lgi = require "lgi" local GLib = lgi.GLib local Gio = lgi.Gio local trace_mpc = false 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_subsystems = { ... } }, { __index = mpc }) self:_connect() return self end function mpc:_write(command) if trace_mpc then print("write: " .. command) end self._output:write(command .. "\n") 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 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) if next(self._reply_handlers) == nil then self:_start_idle() end else local _, _, key, value = string.find(line, "([^:]+):%s(.+)") if key then local k = string.lower(key) if k == "binary" then value = tonumber(value) local data = {} while value > 0 do local b = assert(obj:read_bytes(value)) table.insert(data, b.data) value = value - #b end local w = obj:read_bytes(1) -- read newline at end of binary self._pending_reply[k] = table.concat(data) elseif k == "changed" then if not self._pending_reply[k] then self._pending_reply[k] = {} end self._pending_reply[k][value] = true else self._pending_reply[k] = value end end end do_read() end end) end do_read() -- To synchronize the state on startup, send the idle commands now. for i = 1, #self._idle_subsystems, 2 do self._idle_subsystems[i+1](self) end return self end function mpc:_start_idle() if self._idle then error("start_idle but already idle") end self:_send("idle", function(success, reply) self._idle = false if reply.changed then -- idle mode was disabled by mpd for i = 1, #self._idle_subsystems, 2 do local subsys = self._idle_subsystems[i] if reply.changed[subsys] then self._idle_subsystems[i+1](self) end end end end) self._idle = true end function mpc:_stop_idle() if not self._idle then error("stop_idle but not idle") end self:_write("noidle") self._idle = false end function mpc:_send(command, callback) if self._idle then error("Still idle in send()?!") end self:_write(command) table.insert(self._reply_handlers, callback or function() end) end function mpc:send(...) self:_connect() if not self._connected then return end if self._idle then self:_stop_idle() end local args = { ... } for i = 1, #args, 2 do self:_send(args[i], args[i+1]) end 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 function clamp(x, min, max) return math.min(math.max(x, min), max) end function mpc:change_volume(change) self:send("status", function(_, status) new_vol = clamp(tonumber(status.volume) + change, 0, 100) self:send("setvol " .. new_vol) end) end function mpc:currentsong() local currentsong self:send("currentsong", function(err, song) if err then error(err) end currentsong = song end) return currentsong end local function escape(str) return "\"" .. str .. "\"" end function mpc:albumart(uri, handler) local image_table = {} local get_art_at get_art_at = function(off) self:send("albumart " .. escape(uri) .. " " .. tostring(off), function(success, data) if not success then handler(success, data) end table.insert(image_table, data.binary) if data.binary and #data.binary > 0 then get_art_at(off + #data.binary) else data.binary = table.concat(image_table) handler(success, data) end end) end get_art_at(0) 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