-
Notifications
You must be signed in to change notification settings - Fork 0
/
mpc.lua
226 lines (200 loc) · 5.39 KB
/
mpc.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
local lgi = require "lgi"
local GLib = lgi.GLib
local Gio = lgi.Gio
local mpc = {}
local function parse_password(host)
-- This function is based on mpd_parse_host_password() from libmpdclient
local position = string.find(host, "@")
if not position then
return host
end
return string.sub(host, position + 1), string.sub(host, 1, position - 1)
end
function mpc.new(host, port, password, error_handler, ...)
host = host or os.getenv("MPD_HOST") or "localhost"
port = port or os.getenv("MPD_PORT") or 6600
if not password then
host, password = parse_password(host)
end
local self = setmetatable({
_host = host,
_port = port,
_password = password,
_error_handler = error_handler or function() end,
_connected = false,
_try_reconnect = false,
_idle_commands = { ... }
}, { __index = mpc })
self:_connect()
return self
end
function mpc:_error(err)
self._connected = false
self._error_handler(err)
self._try_reconnect = not self._try_reconnect
if self._try_reconnect then
self:_connect()
end
end
function mpc:_connect()
if self._connected then return end
-- Reset all of our state
self._reply_handlers = {}
self._pending_reply = {}
self._idle_commands_pending = false
self._idle = false
self._connected = true
-- Set up a new connection
local address
if string.sub(self._host, 1, 1) == "/" then
-- It's a unix socket
address = Gio.UnixSocketAddress.new(self._host)
else
-- Do a TCP connection
address = Gio.NetworkAddress.new(self._host, self._port)
end
local client = Gio.SocketClient()
local conn, err = client:connect(address)
if not conn then
self:_error(err)
return false
end
local input, output = conn:get_input_stream(), conn:get_output_stream()
self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)
-- Read the welcome message
self._input:read_line()
if self._password and self._password ~= "" then
self:_send("password " .. self._password)
end
-- Set up the reading loop. This will asynchronously read lines by
-- calling itself.
local do_read
do_read = function()
self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
local line, err = obj:read_line_finish(res)
-- Ugly API. On success we get string, length-of-string
-- and on error we get nil, error. Other versions of lgi
-- behave differently.
if line == nil or tostring(line) == "" then
err = "Connection closed"
end
if type(err) ~= "number" then
self._output, self._input = nil, nil
self:_error(err)
else
do_read()
line = tostring(line)
if line == "OK" or line:match("^ACK ") then
local success = line == "OK"
local arg
if success then
arg = self._pending_reply
else
arg = { line }
end
local handler = self._reply_handlers[1]
table.remove(self._reply_handlers, 1)
self._pending_reply = {}
handler(success, arg)
else
local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
if key then
self._pending_reply[string.lower(key)] = value
end
end
end
end)
end
do_read()
-- To synchronize the state on startup, send the idle commands now. As a
-- side effect, this will enable idle state.
self:_send_idle_commands(true)
return self
end
function mpc:_send_idle_commands(skip_stop_idle)
-- We use a ping to unset this to make sure we never get into a busy
-- loop sending idle / unidle commands. Next call to
-- _send_idle_commands() might be ignored!
if self._idle_commands_pending then
return
end
if not skip_stop_idle then
self:_stop_idle()
end
self._idle_commands_pending = true
for i = 1, #self._idle_commands, 2 do
self:_send(self._idle_commands[i], self._idle_commands[i+1])
end
self:_send("ping", function()
self._idle_commands_pending = false
end)
self:_start_idle()
end
function mpc:_start_idle()
if self._idle then
error("Still idle?!")
end
self:_send("idle", function(success, reply)
if reply.changed then
-- idle mode was disabled by mpd
self:_send_idle_commands()
end
end)
self._idle = true
end
function mpc:_stop_idle()
if not self._idle then
error("Not idle?!")
end
self._output:write("noidle\n")
self._idle = false
end
function mpc:_send(command, callback)
if self._idle then
error("Still idle in send()?!")
end
self._output:write(command .. "\n")
table.insert(self._reply_handlers, callback or function() end)
end
function mpc:send(...)
self:_connect()
if not self._connected then
return
end
local args = { ... }
if not self._idle then
error("Something is messed up, we should be idle here...")
end
self:_stop_idle()
for i = 1, #args, 2 do
self:_send(args[i], args[i+1])
end
self:_start_idle()
end
function mpc:toggle_play()
self:send("status", function(success, status)
if status.state == "stop" then
self:send("play")
else
self:send("pause")
end
end)
end
--[[
-- Example on how to use this (standalone)
local host, port, password = nil, nil, nil
local m = mpc.new(host, port, password, error,
"status", function(success, status) print("status is", status.state) end)
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
-- Test command submission
m:send("status", function(_, s) print(s.state) end,
"currentsong", function(_, s) print(s.title) end)
m:send("status", function(_, s) print(s.state) end)
-- Force a reconnect
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
m._conn:close()
end)
end)
GLib.MainLoop():run()
--]]
return mpc