1 -- Originally from https://awesomewm.org/recipes/mpc/
3 local lgi = require "lgi"
7 local trace_mpc = false
11 local function parse_password(host)
12 -- This function is based on mpd_parse_host_password() from libmpdclient
13 local position = string.find(host, "@")
17 return string.sub(host, position + 1), string.sub(host, 1, position - 1)
20 function mpc.new(host, port, password, error_handler, ...)
21 host = host or os.getenv("MPD_HOST") or "localhost"
22 port = port or os.getenv("MPD_PORT") or 6600
24 host, password = parse_password(host)
26 local self = setmetatable({
30 _error_handler = error_handler or function() end,
32 _try_reconnect = false,
33 _idle_subsystems = { ... }
39 function mpc:_write(command)
41 print("write: " .. command)
43 self._output:write(command .. "\n")
46 function mpc:_error(err)
47 self._connected = false
48 self._error_handler(err)
51 function mpc:connect()
52 if self._connected then return end
53 -- Reset all of our state
54 self._reply_handlers = {}
55 self._pending_reply = {}
56 self._idle_commands_pending = false
58 self._connected = true
60 -- Set up a new connection
62 if string.sub(self._host, 1, 1) == "/" then
64 address = Gio.UnixSocketAddress.new(self._host)
66 -- Do a TCP connection
67 address = Gio.NetworkAddress.new(self._host, self._port)
69 local client = Gio.SocketClient()
70 local conn, err = client:connect(address)
77 local input, output = conn:get_input_stream(), conn:get_output_stream()
78 self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)
80 -- Read the welcome message
81 self._input:read_line()
83 if self._password and self._password ~= "" then
84 self:_send("password " .. self._password)
87 -- Set up the reading loop. This will asynchronously read lines by
91 self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
92 local line, err = obj:read_line_finish(res)
93 -- Ugly API. On success we get string, length-of-string
94 -- and on error we get nil, error. Other versions of lgi
95 -- behave differently.
96 if line == nil or tostring(line) == "" then
97 err = "Connection closed"
99 if type(err) ~= "number" then
100 self._output, self._input = nil, nil
105 line = tostring(line)
106 if line == "OK" or line:match("^ACK ") then
107 local success = line == "OK"
110 arg = self._pending_reply
114 local handler = self._reply_handlers[1]
115 table.remove(self._reply_handlers, 1)
116 self._pending_reply = {}
117 handler(success, arg)
119 if next(self._reply_handlers) == nil then
123 local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
125 local k = string.lower(key)
126 if k == "binary" then
127 value = tonumber(value)
130 local b = assert(obj:read_bytes(value))
131 table.insert(data, b.data)
134 local w = obj:read_bytes(1) -- read newline at end of binary
135 self._pending_reply[k] = table.concat(data)
136 elseif k == "changed" then
137 if not self._pending_reply[k] then
138 self._pending_reply[k] = {}
141 self._pending_reply[k][value] = true
143 self._pending_reply[k] = value
152 -- To synchronize the state on startup, send the idle commands now.
153 for i = 1, #self._idle_subsystems, 2 do
154 self._idle_subsystems[i+1](self)
160 function mpc:_start_idle()
162 error("start_idle but already idle")
164 self:_send("idle", function(success, reply)
166 if reply.changed then
167 -- idle mode was disabled by mpd
168 for i = 1, #self._idle_subsystems, 2 do
169 local subsys = self._idle_subsystems[i]
170 if reply.changed[subsys] then
171 self._idle_subsystems[i+1](self)
179 function mpc:_stop_idle()
180 if not self._idle then
181 error("stop_idle but not idle")
183 self:_write("noidle")
187 function mpc:_send(command, callback)
189 error("Still idle in send()?!")
192 table.insert(self._reply_handlers, callback or function() end)
195 function mpc:send(...)
197 if not self._connected then
204 for i = 1, #args, 2 do
205 self:_send(args[i], args[i+1])
209 function mpc:toggle_play()
210 self:send("status", function(success, status)
211 if status.state == "stop" then
227 function clamp(x, min, max)
228 return math.min(math.max(x, min), max)
231 function mpc:change_volume(change)
232 self:send("status", function(_, status)
233 new_vol = clamp(tonumber(status.volume) + change, 0, 100)
234 self:send("setvol " .. new_vol)
238 function mpc:currentsong()
240 self:send("currentsong", function(err, song)
241 if err then error(err) end
247 local function escape(str)
248 return "\"" .. str .. "\""
251 function mpc:albumart(uri, handler)
252 local image_table = {}
254 get_art_at = function(off)
255 self:send("albumart " .. escape(uri) .. " " .. tostring(off), function(success, data)
257 handler(success, data)
259 table.insert(image_table, data.binary)
260 if data.binary and #data.binary > 0 then
261 get_art_at(off + #data.binary)
263 data.binary = table.concat(image_table)
264 handler(success, data)
273 -- Example on how to use this (standalone)
275 local host, port, password = nil, nil, nil
276 local m = mpc.new(host, port, password, error,
277 "status", function(success, status) print("status is", status.state) end)
279 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
280 -- Test command submission
281 m:send("status", function(_, s) print(s.state) end,
282 "currentsong", function(_, s) print(s.title) end)
283 m:send("status", function(_, s) print(s.state) end)
285 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
290 GLib.MainLoop():run()