]> git.rmz.io Git - dotfiles.git/blob - awesome/widgets/mpc.lua
awesome/mpc: use a keep alive timer instead of ping on error
[dotfiles.git] / awesome / widgets / mpc.lua
1 -- Originally from https://awesomewm.org/recipes/mpc/
2
3 local lgi = require "lgi"
4 local GLib = lgi.GLib
5 local Gio = lgi.Gio
6
7 local trace_mpc = false
8
9 local mpc = {}
10
11 local function parse_password(host)
12 -- This function is based on mpd_parse_host_password() from libmpdclient
13 local position = string.find(host, "@")
14 if not position then
15 return host
16 end
17 return string.sub(host, position + 1), string.sub(host, 1, position - 1)
18 end
19
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
23 if not password then
24 host, password = parse_password(host)
25 end
26 local self = setmetatable({
27 _host = host,
28 _port = port,
29 _password = password,
30 _error_handler = error_handler or function() end,
31 _connected = false,
32 _try_reconnect = false,
33 _idle_subsystems = { ... }
34 }, { __index = mpc })
35 self:connect()
36 return self
37 end
38
39 function mpc:_write(command)
40 if trace_mpc then
41 print("write: " .. command)
42 end
43 self._output:write(command .. "\n")
44 end
45
46 function mpc:_error(err)
47 self._connected = false
48 self._error_handler(err)
49 self._try_reconnect = not self._try_reconnect
50 if self._try_reconnect then
51 self:_connect()
52 end
53 end
54
55 function mpc:connect()
56 if self._connected then return end
57 -- Reset all of our state
58 self._reply_handlers = {}
59 self._pending_reply = {}
60 self._idle_commands_pending = false
61 self._idle = false
62 self._connected = true
63
64 -- Set up a new connection
65 local address
66 if string.sub(self._host, 1, 1) == "/" then
67 -- It's a unix socket
68 address = Gio.UnixSocketAddress.new(self._host)
69 else
70 -- Do a TCP connection
71 address = Gio.NetworkAddress.new(self._host, self._port)
72 end
73 local client = Gio.SocketClient()
74 local conn, err = client:connect(address)
75
76 if not conn then
77 self:_error(err)
78 return false
79 end
80
81 local input, output = conn:get_input_stream(), conn:get_output_stream()
82 self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)
83
84 -- Read the welcome message
85 self._input:read_line()
86
87 if self._password and self._password ~= "" then
88 self:_send("password " .. self._password)
89 end
90
91 -- Set up the reading loop. This will asynchronously read lines by
92 -- calling itself.
93 local do_read
94 do_read = function()
95 self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
96 local line, err = obj:read_line_finish(res)
97 -- Ugly API. On success we get string, length-of-string
98 -- and on error we get nil, error. Other versions of lgi
99 -- behave differently.
100 if line == nil or tostring(line) == "" then
101 err = "Connection closed"
102 end
103 if type(err) ~= "number" then
104 self._output, self._input = nil, nil
105 self:_error(err)
106 return
107 end
108
109 line = tostring(line)
110 if line == "OK" or line:match("^ACK ") then
111 local success = line == "OK"
112 local arg
113 if success then
114 arg = self._pending_reply
115 else
116 arg = { line }
117 end
118 local handler = self._reply_handlers[1]
119 table.remove(self._reply_handlers, 1)
120 self._pending_reply = {}
121 handler(success, arg)
122
123 if next(self._reply_handlers) == nil then
124 self:_start_idle()
125 end
126 else
127 local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
128 if key then
129 local k = string.lower(key)
130 if k == "binary" then
131 value = tonumber(value)
132 local data = {}
133 while value > 0 do
134 local b = assert(obj:read_bytes(value))
135 table.insert(data, b.data)
136 value = value - #b
137 end
138 local w = obj:read_bytes(1) -- read newline at end of binary
139 self._pending_reply[k] = table.concat(data)
140 elseif k == "changed" then
141 if not self._pending_reply[k] then
142 self._pending_reply[k] = {}
143 end
144
145 self._pending_reply[k][value] = true
146 else
147 self._pending_reply[k] = value
148 end
149 end
150 end
151 do_read()
152 end)
153 end
154 do_read()
155
156 -- To synchronize the state on startup, send the idle commands now.
157 for i = 1, #self._idle_subsystems, 2 do
158 self._idle_subsystems[i+1](self)
159 end
160
161 return self
162 end
163
164 function mpc:_start_idle()
165 if self._idle then
166 error("start_idle but already idle")
167 end
168 self:_send("idle", function(success, reply)
169 self._idle = false
170 if reply.changed then
171 -- idle mode was disabled by mpd
172 for i = 1, #self._idle_subsystems, 2 do
173 local subsys = self._idle_subsystems[i]
174 if reply.changed[subsys] then
175 self._idle_subsystems[i+1](self)
176 end
177 end
178 end
179 end)
180 self._idle = true
181 end
182
183 function mpc:_stop_idle()
184 if not self._idle then
185 error("stop_idle but not idle")
186 end
187 self:_write("noidle")
188 self._idle = false
189 end
190
191 function mpc:_send(command, callback)
192 if self._idle then
193 error("Still idle in send()?!")
194 end
195 self:_write(command)
196 table.insert(self._reply_handlers, callback or function() end)
197 end
198
199 function mpc:send(...)
200 self:connect()
201 if not self._connected then
202 return
203 end
204 if self._idle then
205 self:_stop_idle()
206 end
207 local args = { ... }
208 for i = 1, #args, 2 do
209 self:_send(args[i], args[i+1])
210 end
211 end
212
213 function mpc:toggle_play()
214 self:send("status", function(success, status)
215 if status.state == "stop" then
216 self:send("play")
217 else
218 self:send("pause")
219 end
220 end)
221 end
222
223 function clamp(x, min, max)
224 return math.min(math.max(x, min), max)
225 end
226
227 function mpc:change_volume(change)
228 self:send("status", function(_, status)
229 new_vol = clamp(tonumber(status.volume) + change, 0, 100)
230 self:send("setvol " .. new_vol)
231 end)
232 end
233
234 function mpc:currentsong()
235 local currentsong
236 self:send("currentsong", function(err, song)
237 if err then error(err) end
238 currentsong = song
239 end)
240 return currentsong
241 end
242
243 local function escape(str)
244 return "\"" .. str .. "\""
245 end
246
247 function mpc:albumart(uri, handler)
248 local image_table = {}
249 local get_art_at
250 get_art_at = function(off)
251 self:send("albumart " .. escape(uri) .. " " .. tostring(off), function(success, data)
252 if not success then
253 handler(success, data)
254 end
255 table.insert(image_table, data.binary)
256 if data.binary and #data.binary > 0 then
257 get_art_at(off + #data.binary)
258 else
259 data.binary = table.concat(image_table)
260 handler(success, data)
261 end
262 end)
263 end
264 get_art_at(0)
265 end
266
267 --[[
268
269 -- Example on how to use this (standalone)
270
271 local host, port, password = nil, nil, nil
272 local m = mpc.new(host, port, password, error,
273 "status", function(success, status) print("status is", status.state) end)
274
275 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
276 -- Test command submission
277 m:send("status", function(_, s) print(s.state) end,
278 "currentsong", function(_, s) print(s.title) end)
279 m:send("status", function(_, s) print(s.state) end)
280 -- Force a reconnect
281 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
282 m._conn:close()
283 end)
284 end)
285
286 GLib.MainLoop():run()
287 --]]
288
289 return mpc