1 -- Originally from https://awesomewm.org/recipes/mpc/
 
   3 local lgi = require "lgi"
 
   7 local trace_mpc = false
 
  11 local function parse_password(host)
 
  12     -- This function is based on mpd_parse_host_password() from libmpdclient
 
  13     local position = string.find(host, "@")
 
  17     return string.sub(host, position + 1), string.sub(host, 1, position - 1)
 
  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
 
  24         host, password = parse_password(host)
 
  26     local self = setmetatable({
 
  30         _error_handler = error_handler or function() end,
 
  32         _try_reconnect = false,
 
  33         _idle_subsystems = { ... }
 
  39 function mpc:_write(command)
 
  41         print("write: " .. command)
 
  43     self._output:write(command .. "\n")
 
  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
 
  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
 
  62     self._connected = true
 
  64     -- Set up a new connection
 
  66     if string.sub(self._host, 1, 1) == "/" then
 
  68         address = Gio.UnixSocketAddress.new(self._host)
 
  70         -- Do a TCP connection
 
  71         address = Gio.NetworkAddress.new(self._host, self._port)
 
  73     local client = Gio.SocketClient()
 
  74     local conn, err = client:connect(address)
 
  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)
 
  84     -- Read the welcome message
 
  85     self._input:read_line()
 
  87     if self._password and self._password ~= "" then
 
  88         self:_send("password " .. self._password)
 
  91     -- Set up the reading loop. This will asynchronously read lines by
 
  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"
 
 103             if type(err) ~= "number" then
 
 104                 self._output, self._input = nil, nil
 
 109             line = tostring(line)
 
 110             if line == "OK" or line:match("^ACK ") then
 
 111                 local success = line == "OK"
 
 114                     arg = self._pending_reply
 
 118                 local handler = self._reply_handlers[1]
 
 119                 table.remove(self._reply_handlers, 1)
 
 120                 self._pending_reply = {}
 
 121                 handler(success, arg)
 
 123                 if next(self._reply_handlers) == nil then
 
 127                 local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
 
 129                     local k = string.lower(key)
 
 130                     if k == "binary" then
 
 131                         value = tonumber(value)
 
 134                             local b = assert(obj:read_bytes(value))
 
 135                             table.insert(data, b.data)
 
 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] = {}
 
 145                         self._pending_reply[k][value] = true
 
 147                         self._pending_reply[k] = value
 
 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)
 
 164 function mpc:_start_idle()
 
 166         error("start_idle but already idle")
 
 168     self:_send("idle", function(success, reply)
 
 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)
 
 183 function mpc:_stop_idle()
 
 184     if not self._idle then
 
 185         error("stop_idle but not idle")
 
 187     self:_write("noidle")
 
 191 function mpc:_send(command, callback)
 
 193         error("Still idle in send()?!")
 
 196     table.insert(self._reply_handlers, callback or function() end)
 
 199 function mpc:send(...)
 
 201     if not self._connected then
 
 208     for i = 1, #args, 2 do
 
 209         self:_send(args[i], args[i+1])
 
 213 function mpc:toggle_play()
 
 214     self:send("status", function(success, status)
 
 215         if status.state == "stop" then
 
 223 function clamp(x, min, max)
 
 224     return math.min(math.max(x, min), max)
 
 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)
 
 234 function mpc:currentsong()
 
 236     self:send("currentsong", function(err, song)
 
 237         if err then error(err) end
 
 243 local function escape(str)
 
 244     return "\"" .. str .. "\""
 
 247 function mpc:albumart(uri, handler)
 
 248     local image_table = {}
 
 250     get_art_at = function(off)
 
 251         self:send("albumart " .. escape(uri) .. " " .. tostring(off), function(success, data)
 
 253                 handler(success, data)
 
 255             table.insert(image_table, data.binary)
 
 256             if data.binary and #data.binary > 0 then
 
 257                 get_art_at(off + #data.binary)
 
 259                 data.binary = table.concat(image_table)
 
 260                 handler(success, data)
 
 269 -- Example on how to use this (standalone)
 
 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)
 
 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)
 
 281     GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
 
 286 GLib.MainLoop():run()