]> git.rmz.io Git - dotfiles.git/blob - awesome/widgets/mpc.lua
awesome: remove lowercase runi letters
[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 end
50
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
57 self._idle = false
58 self._connected = true
59
60 -- Set up a new connection
61 local address
62 if string.sub(self._host, 1, 1) == "/" then
63 -- It's a unix socket
64 address = Gio.UnixSocketAddress.new(self._host)
65 else
66 -- Do a TCP connection
67 address = Gio.NetworkAddress.new(self._host, self._port)
68 end
69 local client = Gio.SocketClient()
70 local conn, err = client:connect(address)
71
72 if not conn then
73 self:_error(err)
74 return false
75 end
76
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)
79
80 -- Read the welcome message
81 self._input:read_line()
82
83 if self._password and self._password ~= "" then
84 self:_send("password " .. self._password)
85 end
86
87 -- Set up the reading loop. This will asynchronously read lines by
88 -- calling itself.
89 local do_read
90 do_read = function()
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"
98 end
99 if type(err) ~= "number" then
100 self._output, self._input = nil, nil
101 self:_error(err)
102 return
103 end
104
105 line = tostring(line)
106 if line == "OK" or line:match("^ACK ") then
107 local success = line == "OK"
108 local arg
109 if success then
110 arg = self._pending_reply
111 else
112 arg = { line }
113 end
114 local handler = self._reply_handlers[1]
115 table.remove(self._reply_handlers, 1)
116 self._pending_reply = {}
117 handler(success, arg)
118
119 if next(self._reply_handlers) == nil then
120 self:_start_idle()
121 end
122 else
123 local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
124 if key then
125 local k = string.lower(key)
126 if k == "binary" then
127 value = tonumber(value)
128 local data = {}
129 while value > 0 do
130 local b = assert(obj:read_bytes(value))
131 table.insert(data, b.data)
132 value = value - #b
133 end
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] = {}
139 end
140
141 self._pending_reply[k][value] = true
142 else
143 self._pending_reply[k] = value
144 end
145 end
146 end
147 do_read()
148 end)
149 end
150 do_read()
151
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)
155 end
156
157 return self
158 end
159
160 function mpc:_start_idle()
161 if self._idle then
162 error("start_idle but already idle")
163 end
164 self:_send("idle", function(success, reply)
165 self._idle = false
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)
172 end
173 end
174 end
175 end)
176 self._idle = true
177 end
178
179 function mpc:_stop_idle()
180 if not self._idle then
181 error("stop_idle but not idle")
182 end
183 self:_write("noidle")
184 self._idle = false
185 end
186
187 function mpc:_send(command, callback)
188 if self._idle then
189 error("Still idle in send()?!")
190 end
191 self:_write(command)
192 table.insert(self._reply_handlers, callback or function() end)
193 end
194
195 function mpc:send(...)
196 self:connect()
197 if not self._connected then
198 return
199 end
200 if self._idle then
201 self:_stop_idle()
202 end
203 local args = { ... }
204 for i = 1, #args, 2 do
205 self:_send(args[i], args[i+1])
206 end
207 end
208
209 function mpc:toggle_play()
210 self:send("status", function(success, status)
211 if status.state == "stop" then
212 self:send("play")
213 else
214 self:send("pause")
215 end
216 end)
217 end
218
219 function clamp(x, min, max)
220 return math.min(math.max(x, min), max)
221 end
222
223 function mpc:change_volume(change)
224 self:send("status", function(_, status)
225 new_vol = clamp(tonumber(status.volume) + change, 0, 100)
226 self:send("setvol " .. new_vol)
227 end)
228 end
229
230 function mpc:currentsong()
231 local currentsong
232 self:send("currentsong", function(err, song)
233 if err then error(err) end
234 currentsong = song
235 end)
236 return currentsong
237 end
238
239 local function escape(str)
240 return "\"" .. str .. "\""
241 end
242
243 function mpc:albumart(uri, handler)
244 local image_table = {}
245 local get_art_at
246 get_art_at = function(off)
247 self:send("albumart " .. escape(uri) .. " " .. tostring(off), function(success, data)
248 if not success then
249 handler(success, data)
250 end
251 table.insert(image_table, data.binary)
252 if data.binary and #data.binary > 0 then
253 get_art_at(off + #data.binary)
254 else
255 data.binary = table.concat(image_table)
256 handler(success, data)
257 end
258 end)
259 end
260 get_art_at(0)
261 end
262
263 --[[
264
265 -- Example on how to use this (standalone)
266
267 local host, port, password = nil, nil, nil
268 local m = mpc.new(host, port, password, error,
269 "status", function(success, status) print("status is", status.state) end)
270
271 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
272 -- Test command submission
273 m:send("status", function(_, s) print(s.state) end,
274 "currentsong", function(_, s) print(s.title) end)
275 m:send("status", function(_, s) print(s.state) end)
276 -- Force a reconnect
277 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
278 m._conn:close()
279 end)
280 end)
281
282 GLib.MainLoop():run()
283 --]]
284
285 return mpc