]> git.rmz.io Git - dotfiles.git/blob - awesome/widgets/mpc.lua
awesome/mpd_widget: Update popup contents
[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 mpc = {}
8
9 local function parse_password(host)
10 -- This function is based on mpd_parse_host_password() from libmpdclient
11 local position = string.find(host, "@")
12 if not position then
13 return host
14 end
15 return string.sub(host, position + 1), string.sub(host, 1, position - 1)
16 end
17
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
21 if not password then
22 host, password = parse_password(host)
23 end
24 local self = setmetatable({
25 _host = host,
26 _port = port,
27 _password = password,
28 _error_handler = error_handler or function() end,
29 _connected = false,
30 _try_reconnect = false,
31 _idle_commands = { ... }
32 }, { __index = mpc })
33 self:_connect()
34 return self
35 end
36
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
42 self:_connect()
43 end
44 end
45
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
52 self._idle = false
53 self._connected = true
54
55 -- Set up a new connection
56 local address
57 if string.sub(self._host, 1, 1) == "/" then
58 -- It's a unix socket
59 address = Gio.UnixSocketAddress.new(self._host)
60 else
61 -- Do a TCP connection
62 address = Gio.NetworkAddress.new(self._host, self._port)
63 end
64 local client = Gio.SocketClient()
65 local conn, err = client:connect(address)
66
67 if not conn then
68 self:_error(err)
69 return false
70 end
71
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)
74
75 -- Read the welcome message
76 self._input:read_line()
77
78 if self._password and self._password ~= "" then
79 self:_send("password " .. self._password)
80 end
81
82 -- Set up the reading loop. This will asynchronously read lines by
83 -- calling itself.
84 local do_read
85 do_read = function()
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"
93 end
94 if type(err) ~= "number" then
95 self._output, self._input = nil, nil
96 self:_error(err)
97 else
98 do_read()
99 line = tostring(line)
100 if line == "OK" or line:match("^ACK ") then
101 local success = line == "OK"
102 local arg
103 if success then
104 arg = self._pending_reply
105 else
106 arg = { line }
107 end
108 local handler = self._reply_handlers[1]
109 table.remove(self._reply_handlers, 1)
110 self._pending_reply = {}
111 handler(success, arg)
112 else
113 local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
114 if key then
115 self._pending_reply[string.lower(key)] = value
116 end
117 end
118 end
119 end)
120 end
121 do_read()
122
123 -- To synchronize the state on startup, send the idle commands now. As a
124 -- side effect, this will enable idle state.
125 self:_send_idle_commands(true)
126
127 return self
128 end
129
130 function mpc:_send_idle_commands(skip_stop_idle)
131 -- We use a ping to unset this to make sure we never get into a busy
132 -- loop sending idle / unidle commands. Next call to
133 -- _send_idle_commands() might be ignored!
134 if self._idle_commands_pending then
135 return
136 end
137 if not skip_stop_idle then
138 self:_stop_idle()
139 end
140
141 self._idle_commands_pending = true
142 for i = 1, #self._idle_commands, 2 do
143 self:_send(self._idle_commands[i], self._idle_commands[i+1])
144 end
145 self:_send("ping", function()
146 self._idle_commands_pending = false
147 end)
148 self:_start_idle()
149 end
150
151 function mpc:_start_idle()
152 if self._idle then
153 error("Still idle?!")
154 end
155 self:_send("idle", function(success, reply)
156 if reply.changed then
157 -- idle mode was disabled by mpd
158 self:_send_idle_commands()
159 end
160 end)
161 self._idle = true
162 end
163
164 function mpc:_stop_idle()
165 if not self._idle then
166 error("Not idle?!")
167 end
168 self._output:write("noidle\n")
169 self._idle = false
170 end
171
172 function mpc:_send(command, callback)
173 if self._idle then
174 error("Still idle in send()?!")
175 end
176 self._output:write(command .. "\n")
177 table.insert(self._reply_handlers, callback or function() end)
178 end
179
180 function mpc:send(...)
181 self:_connect()
182 if not self._connected then
183 return
184 end
185 local args = { ... }
186 if not self._idle then
187 error("Something is messed up, we should be idle here...")
188 end
189 self:_stop_idle()
190 for i = 1, #args, 2 do
191 self:_send(args[i], args[i+1])
192 end
193 self:_start_idle()
194 end
195
196 function mpc:toggle_play()
197 self:send("status", function(success, status)
198 if status.state == "stop" then
199 self:send("play")
200 else
201 self:send("pause")
202 end
203 end)
204 end
205
206 function clamp(x, min, max)
207 return math.min(math.max(x, min), max)
208 end
209
210 function mpc:change_volume(change)
211 self:send("status", function(_, status)
212 new_vol = clamp(tonumber(status.volume) + change, 0, 100)
213 self:send("setvol " .. new_vol)
214 end)
215 end
216
217 function mpc:currentsong()
218 local currentsong
219 self:send("currentsong", function(err, song)
220 if err then error(err) end
221 currentsong = song
222 end)
223 return currentsong
224 end
225
226 --[[
227
228 -- Example on how to use this (standalone)
229
230 local host, port, password = nil, nil, nil
231 local m = mpc.new(host, port, password, error,
232 "status", function(success, status) print("status is", status.state) end)
233
234 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
235 -- Test command submission
236 m:send("status", function(_, s) print(s.state) end,
237 "currentsong", function(_, s) print(s.title) end)
238 m:send("status", function(_, s) print(s.state) end)
239 -- Force a reconnect
240 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
241 m._conn:close()
242 end)
243 end)
244
245 GLib.MainLoop():run()
246 --]]
247
248 return mpc