diff --git a/recipes/mpc.lua b/recipes/mpc.lua index e355959..9af3e24 100644 --- a/recipes/mpc.lua +++ b/recipes/mpc.lua @@ -2,6 +2,8 @@ local lgi = require "lgi" local GLib = lgi.GLib local Gio = lgi.Gio +local gears = require "gears" + local mpc = {} local function parse_password(host) @@ -13,42 +15,63 @@ local function parse_password(host) return string.sub(host, position + 1), string.sub(host, 1, position - 1) end -function mpc.new(host, port, password, error_handler, ...) +function mpc.new(host, port, password, error_handler, reconnect_interval, ...) 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 = { ... } + _idle_commands = { ... }, + _conn = nil, + _output = nil, + _input = nil, + _reconnect_timer = nil, + _reconnect_interval = reconnect_interval, + _try_reconnect = true }, { __index = mpc }) + self:_connect() return self end function mpc:_error(err) + self._error_handler(err, self) +end + +function mpc:_reset() + self._output = nil + self._input = nil + self._reply_handlers = {} + self._pending_reply = {} + self._idle_commands_pending = false + self._idle = false self._connected = false - self._error_handler(err) - self._try_reconnect = not self._try_reconnect - if self._try_reconnect then - self:_connect() +end + +function mpc:_reconnect() + if not self._reconnect_timer then + self:_error("cannot reconnect") + return end + + if not self._try_reconnect then + return + end + + self._reconnect_timer:again() 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 + self:_reset() -- Set up a new connection local address @@ -60,69 +83,94 @@ function mpc:_connect() 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 + local conn + if not self._conn then + if not self._reconnect_timer and self._try_reconnect then + -- timer requires positive value + local interval = self._reconnect_interval >= 0 and self._reconnect_interval or 0 + self._reconnect_timer = gears.timer.start_new(interval, function() + -- user disabled reconnect + if self._reconnect_interval < 0 then + self._try_reconnect = false + end + + self:_reset() + conn, err = client:connect(address) + + if not conn then + self:_error(err) + return true + end + + self._connected = true + + 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 + + return false + end) + else + self:_reconnect() + end 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 + self._reconnect_timer:connect_signal("stop", function() + self: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) + end) +end +function mpc:do_read() -- 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) + 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" + self:_error(err) + self:_reconnect() + return + end + + if type(err) ~= "number" then + self:_error(err) + self:_reconnect() + else + self: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 - local _, _, key, value = string.find(line, "([^:]+):%s(.+)") - if key then - self._pending_reply[string.lower(key)] = value - end + 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 - 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 + end) end function mpc:_send_idle_commands(skip_stop_idle) @@ -148,6 +196,7 @@ end function mpc:_start_idle() if self._idle then + self:_error("still idle?!") error("Still idle?!") end self:_send("idle", function(success, reply) @@ -161,6 +210,7 @@ end function mpc:_stop_idle() if not self._idle then + self:_error("Not idle?!") error("Not idle?!") end self._output:write("noidle\n") @@ -169,6 +219,7 @@ end function mpc:_send(command, callback) if self._idle then + self:_error("Still idle in send()?!") error("Still idle in send()?!") end self._output:write(command .. "\n") @@ -176,12 +227,14 @@ function mpc:_send(command, callback) end function mpc:send(...) - self:_connect() - if not self._connected then + if not self._conn then + self:_reconnect() return end + local args = { ... } if not self._idle then + self:_error("Something is messed up, we should be idle here...") error("Something is messed up, we should be idle here...") end self:_stop_idle() @@ -204,9 +257,10 @@ end --[[ -- Example on how to use this (standalone) +-- set negative reconnect_interval to disable reconnect in case initial connection failed -local host, port, password = nil, nil, nil -local m = mpc.new(host, port, password, error, +local host, port, password, reconnect_interval = nil, nil, nil, 0 +local m = mpc.new(host, port, password, error_handler, reconnect_interval "status", function(success, status) print("status is", status.state) end) GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function() diff --git a/recipes/mpc.mdwn b/recipes/mpc.mdwn index cc8cca8..bebe578 100644 --- a/recipes/mpc.mdwn +++ b/recipes/mpc.mdwn @@ -39,7 +39,6 @@ updates when the current MPD state changes. local mpc = require("mpc") local textbox = require("wibox.widget.textbox") - local timer = require("gears.timer") local mpd_widget = textbox() local state, title, artist, file = "stop", "", "", "" local function update_widget() @@ -53,15 +52,14 @@ updates when the current MPD state changes. end mpd_widget.text = text end - local connection + local function error_handler(err) mpd_widget:set_text("Error: " .. tostring(err)) - -- Try a reconnect soon-ish - timer.start_new(10, function() - connection:send("ping") - end) end - connection = mpc.new(nil, nil, nil, error_handler, + + local reconnect_interval = 1 -- negative to disable + local connection + connection = mpc.new(nil, nil, nil, error_handler, reconnect_interval, "status", function(_, result) state = result.state end,