1 -- Originally from https://awesomewm.org/recipes/mpc/
3 local lgi = require "lgi"
9 local function parse_password(host)
10 -- This function is based on mpd_parse_host_password() from libmpdclient
11 local position = string.find(host, "@")
15 return string.sub(host, position + 1), string.sub(host, 1, position - 1)
18 function mpc.new(host, port, password, error_handler, ...)
19 host = host or os.getenv("MPD_HOST") or "localhost"
20 port = port or os.getenv("MPD_PORT") or 6600
22 host, password = parse_password(host)
24 local self = setmetatable({
28 _error_handler = error_handler or function() end,
30 _try_reconnect = false,
31 _idle_commands = { ... }
37 function mpc:_error(err)
38 self._connected = false
39 self._error_handler(err)
40 self._try_reconnect = not self._try_reconnect
41 if self._try_reconnect then
46 function mpc:_connect()
47 if self._connected then return end
48 -- Reset all of our state
49 self._reply_handlers = {}
50 self._pending_reply = {}
51 self._idle_commands_pending = false
53 self._connected = true
55 -- Set up a new connection
57 if string.sub(self._host, 1, 1) == "/" then
59 address = Gio.UnixSocketAddress.new(self._host)
61 -- Do a TCP connection
62 address = Gio.NetworkAddress.new(self._host, self._port)
64 local client = Gio.SocketClient()
65 local conn, err = client:connect(address)
72 local input, output = conn:get_input_stream(), conn:get_output_stream()
73 self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)
75 -- Read the welcome message
76 self._input:read_line()
78 if self._password and self._password ~= "" then
79 self:_send("password " .. self._password)
82 -- Set up the reading loop. This will asynchronously read lines by
86 self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
87 local line, err = obj:read_line_finish(res)
88 -- Ugly API. On success we get string, length-of-string
89 -- and on error we get nil, error. Other versions of lgi
90 -- behave differently.
91 if line == nil or tostring(line) == "" then
92 err = "Connection closed"
94 if type(err) ~= "number" then
95 self._output, self._input = nil, nil
99 if line == "OK" or line:match("^ACK ") then
100 local success = line == "OK"
103 arg = self._pending_reply
107 local handler = self._reply_handlers[1]
108 table.remove(self._reply_handlers, 1)
109 self._pending_reply = {}
110 handler(success, arg)
112 local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
114 if key == "binary" then
115 value = tonumber(value)
118 local b = assert(obj:read_bytes(value))
119 table.insert(data, b.data)
122 local w = obj:read_bytes(1) -- read newline at end of binary
123 self._pending_reply[string.lower(key)] = table.concat(data)
125 self._pending_reply[string.lower(key)] = value
135 -- To synchronize the state on startup, send the idle commands now. As a
136 -- side effect, this will enable idle state.
137 self:_send_idle_commands(true)
142 function mpc:_send_idle_commands(skip_stop_idle)
143 -- We use a ping to unset this to make sure we never get into a busy
144 -- loop sending idle / unidle commands. Next call to
145 -- _send_idle_commands() might be ignored!
146 if self._idle_commands_pending then
149 if not skip_stop_idle then
153 self._idle_commands_pending = true
154 for i = 1, #self._idle_commands, 2 do
155 self:_send(self._idle_commands[i], self._idle_commands[i+1])
157 self:_send("ping", function()
158 self._idle_commands_pending = false
163 function mpc:_start_idle()
165 error("Still idle?!")
167 self:_send("idle", function(success, reply)
168 if reply.changed then
169 -- idle mode was disabled by mpd
170 self:_send_idle_commands()
176 function mpc:_stop_idle()
177 if not self._idle then
180 self._output:write("noidle\n")
184 function mpc:_send(command, callback)
186 error("Still idle in send()?!")
188 self._output:write(command .. "\n")
189 table.insert(self._reply_handlers, callback or function() end)
192 function mpc:send(...)
194 if not self._connected then
198 if not self._idle then
199 error("Something is messed up, we should be idle here...")
202 for i = 1, #args, 2 do
203 self:_send(args[i], args[i+1])
208 function mpc:toggle_play()
209 self:send("status", function(success, status)
210 if status.state == "stop" then
218 function clamp(x, min, max)
219 return math.min(math.max(x, min), max)
222 function mpc:change_volume(change)
223 self:send("status", function(_, status)
224 new_vol = clamp(tonumber(status.volume) + change, 0, 100)
225 self:send("setvol " .. new_vol)
229 function mpc:currentsong()
231 self:send("currentsong", function(err, song)
232 if err then error(err) end
240 -- Example on how to use this (standalone)
242 local host, port, password = nil, nil, nil
243 local m = mpc.new(host, port, password, error,
244 "status", function(success, status) print("status is", status.state) end)
246 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
247 -- Test command submission
248 m:send("status", function(_, s) print(s.state) end,
249 "currentsong", function(_, s) print(s.title) end)
250 m:send("status", function(_, s) print(s.state) end)
252 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
257 GLib.MainLoop():run()