Skip to content

Instantly share code, notes, and snippets.

@jarcode-foss
Created January 9, 2018 00:05
Show Gist options
  • Save jarcode-foss/de223e93d51011d3bd71ec56a97601b9 to your computer and use it in GitHub Desktop.
Save jarcode-foss/de223e93d51011d3bd71ec56a97601b9 to your computer and use it in GitHub Desktop.
MPD client code with plain Lua and C
/* GIST NOTE: this is part of a much larger collection of lua utilities for AwesomeWM
and has some (very tiny) lua code that wraps these C functions and places them into
a table. */
/* License: GPLv3
Copyright 2018 Levi Webb */
/* MINIMAL TCP SOCKET SUPPORT */
typedef struct tsoc_con {
int fd, port, n;
struct sockaddr_in addr;
struct hostent *server;
} tsoc_con;
LE_API int le_tsoc_new(lua_State* state) {
const char* host = luaL_checkstring(state, 1); /* host */
int port = luaL_checkinteger(state, 2); /* port */
tsoc_con* c = lua_newuserdata(state, sizeof(struct tsoc_con));
c->port = port;
c->fd = socket(AF_INET, SOCK_STREAM, 0);
if (c->fd < 0) {
luaL_error(state, "le_tsoc_init(): error opening socket: %d", c->fd);
}
c->server = gethostbyname(host);
if (!c->server) {
luaL_error(state, "le_tsoc_init(): no such host");
}
memset(&c->addr, 0, sizeof(struct sockaddr_in));
c->addr.sin_family = AF_INET;
memcpy(&c->addr.sin_addr.s_addr, c->server->h_addr, c->server->h_length);
c->addr.sin_port = htons(port);
if (connect(c->fd, (struct sockaddr*) &c->addr, sizeof(struct sockaddr_in)) < 0) {
luaL_error(state, "le_tsoc_init(): connection failed: %s", strerror(errno));
}
return 1;
}
/* num (con, string) - (instance, bytes to write) */
LE_API int le_tsoc_write(lua_State* state) {
if (lua_gettop(state) == 0 || !lua_isuserdata(state, 1)) {
luaL_error(state, "le_tsock_write(): expected (userdata, string)");
}
tsoc_con* c = lua_touserdata(state, 1);
size_t len;
const char* data = luaL_checklstring(state, 2, &len);
int n = write(c->fd, data, len);
if (n < 0) {
luaL_error(state, "le_tsoc_write(): write failed: %s", strerror(errno));
}
lua_pushinteger(state, n);
return 1;
}
/* string (con[, num]) - (instance, bytes read | readline if nil) */
LE_API int le_tsoc_read(lua_State* state) {
char* buf = NULL;
bool line = false;
int amt = 0;
if (lua_gettop(state) >= 1) {
if (!lua_isuserdata(state, 1))
goto invalid;
if (lua_gettop(state) >= 2) {
if (lua_isnumber(state, 2)) {
amt = lua_tointeger(state, 2);
} else if (lua_isnil(state, 2)) {
line = true;
} else goto invalid;
} else line = true;
} else goto invalid;
tsoc_con* c = lua_touserdata(state, 1);
if (line) {
size_t sz = 64, idx = 0;
buf = malloc(sz);
char bit;
int align = 0; /* whether the current read index is aligned to the start of a UTF-8 character encoding */
do {
int n = read(c->fd, &bit, 1);
if (n < 0) {
luaL_error(state, "le_tsoc_read(): failed to read: %s", strerror(errno));
} else if (n == 0) {
luaL_error(state, "le_tsoc_read(): no data recieved on read() call (is the descriptor configured wrong?)");
}
/* resize buf */
if (idx == sz) {
sz *= 2;
buf = realloc(buf, sz);
}
buf[idx++] = bit;
if (align == 0) {
align = utf8_next(&bit);
/* we are aligned to the start of an encoding, so we can check for the newline delimeter */
if (bit == '\n')
break;
}
align--;
} while (true);
lua_pushlstring(state, buf, idx - 1);
} else {
buf = malloc(amt);
int n = read(c->fd, &buf, amt);
if (n != amt) {
luaL_error(state, "le_tsoc_read(): couldn't read all %d bytes", n);
}
lua_pushlstring(state, buf, amt);
}
if (buf != NULL) free(buf);
return 1;
invalid:
if (buf != NULL) free(buf);
luaL_error(state, "le_tsoc_read(): expected (userdata[, number])");
return 0;
}
/* nil (con) - (instance) */
LE_API int le_tsoc_close(lua_State* state) {
if (lua_gettop(state) == 0 || !lua_isuserdata(state, 1)) {
luaL_error(state, "le_tsoc_close(): expected (userdata)");
}
tsoc_con* c = lua_touserdata(state, 1);
close(c->fd);
c->fd = 0;
return 0;
}
LE_API int le_gcobj(lua_State* state) {
if (lua_gettop(state) == 0 || !lua_isfunction(state, 1)) {
luaL_error(state, "le_gcobj(): expected (function)");
}
lua_newuserdata(state, 1); // > obj = :userdata:
lua_newtable(state); // > metatable = {}
lua_pushstring(state, "__gc"); // metatable key
lua_pushvalue(state, 0); // copy function argument
lua_rawset(state, -3); // > metatable.__gc = argument
lua_setmetatable(state, -2); // > setmetatable(obj, metatable)
return 1; // > return obj
}
-- GIST NOTE: `require "external"` is part of a larger collection of native functions I
-- have for my personal AwesomeWM codebase. The C code responsible for the `tsoc` functions
-- is available below in the `lua_external.c` file, however it is not complete.
-- License: GPLv3
-- Copyright 2018 Levi Webb
local le = require "external"
local socket = le.tsoc;
local QUOTE = string.byte("\"")
local SPACE = string.byte(" ")
local mpd = { mt = {} }
mpd.__index = mpd;
-- quickly split string into portions like so:
-- "foo biz \"foo bar\" rab \"off\"" -> { "foo", "biz", "foo bar", "rab", "off" }
local function argparse(s)
local tbl = {} -- arguments
local idx = 1 -- UTF8 aligned byte index in string
local aidx = 1; -- argument character index
local at = "" -- argument currently parsing
local aq = false -- if argument is quoted
while idx <= s:len() do
local c = string.byte(s, idx)
local n = le.utf8_next(c)
local skipchar = false
-- if first char is a quote, parse as a quoted string arg
if aidx == 1 and c == QUOTE then
skipchar = true
aq = true
end
-- reset if end of arg
if (not aq and c == SPACE) or (aq and c == QUOTE) then
skipchar = true
aidx = 1
aq = false
if at ~= "" then
tbl[#tbl + 1] = at
at = ""
end
else
aidx = aidx + 1
end
-- append utf8 data if the character wasn't a space delimeter or quote
if not skipchar then
at = at .. string.sub(s, idx, idx + (n - 1))
end
idx = idx + n
end
if at ~= "" then
tbl[#tbl + 1] = at
at = ""
end
return tbl
end
function mpd:send(args)
local formatted = {}
for k, v in pairs(args) do
if string.match(v, " ") ~= nil then
formatted[k] = "\"" .. string.gsub(v, "\"", "\\\"") .. "\""
else
formatted[k] = v
end
end
self.con:write(table.concat(formatted, " ") .. "\n")
end
function mpd.mt:__call(args)
local self = {
con = socket {
host = args.host or "localhost",
port = args.port or 6600
},
state = {}
}
setmetatable(self, mpd)
local r, s = self:handle_ret(nil, true)
if not r then
report(string.format("Failed to connect to MPD: %s", s))
return nil
end
self:update_state() -- get initial state
if args.consume == false then
self:execute { "consume", "0" }
elseif args.consume == true then
self:execute { "consume", "1" }
end
return self
end
-- returns (ok, errmsg | version | tbl)
function mpd:handle_ret(handler, version)
local s = argparse(self.con:read())
if s[1] == "OK" then
if not version then
return true, nil
else
assert(s[2] == "MPD")
return true, s[3]
end
elseif s[1] == "ACK" then
return false, table.concat(s, " ", 2)
else
if handler == nil then
error(string.format("recieved response '%s' when handling command return!", s[1]))
elseif handler ~= true then
handler(s)
else
return true, s
end
end
end
-- execute a command and handle return data in a list
function mpd:execute(args)
self:send(args)
local tbl = {}
while true do
local ok, s = self:handle_ret(true)
if not ok then
tbl[#tbl + 1] = { error = s }
break
else
if s == nil then break end -- end of return
tbl[#tbl + 1] = s
end
end
return tbl
end
function mpd:setvol(vol)
vol = math.floor(vol)
if vol > 100 then vol = 100 end
if vol < 0 then vol = 0 end
self:execute { "setvol", tostring(vol) }
end
function mpd:play(songpos)
if songpos then
self:execute { "play" }
else
self:execute { "play", tostring(songpos) }
end
end
function mpd:playid(songid)
if songid then
self:execute { "playid" }
else
self:execute { "playid", tostring(songid) }
end
end
function mpd:pause() self:execute { "pause", "1" } end
function mpd:resume() self:execute { "pause", "0" } end
function mpd:stop() self:execute { "stop" } end
function mpd:next() self:execute { "next" } end
function mpd:prev() self:execute { "previous" } end
function mpd:setrandom(bool) self:execute { "random", bool and "1" or "0" } end
function mpd:setsingle(bool) self:execute { "single", bool and "1" or "0" } end
function mpd:setrepeat(bool) self:execute { "repeat", bool and "1" or "0" } end
function mpd:setpause(bool) self:execute { "pause", bool and "1" or "0" } end
function mpd:update() self:execute { "update" } end
function mpd:clear() self:execute { "clear" } end
function mpd:add(uri) self:execute { "add", uri } end
function mpd:shuffle() self:execute { "shuffle" } end
function mpd:play(position) self:execute { "play", tostring(position) } end
function mpd:reload(dir)
self:update()
self:clear()
for _, v in pairs(le.list(dir)) do
self:add(v)
end
self:shuffle()
self:play(1)
end
local state_mappings = nil
do
function int(self, key, value) self.state[key] = tonumber(value) end
function bool(self, key, value) self.state[key] = value ~= "0" end
function str(self, key, value) self.state[key] = value end
state_mappings = {
["volume"] = int,
["repeat"] = bool,
["random"] = bool,
["single"] = bool,
["consume"] = bool,
["playlist"] = int,
["playlistlength"] = int,
["state"] = str,
["song"] = int,
["songid"] = int,
["nextsong"] = int,
["nextsongid"] = int,
["time"] = int,
["elapsed"] = int,
["duration"] = int,
["bitrate"] = int,
["xfade"] = int,
["mixrampdb"] = int,
["mixrampdelay"] = int,
["audio"] = str,
["updating_db"] = int,
["error"] = str
}
end
-- update state
function mpd:update_state()
for _, v in ipairs(self:execute { "status" }) do
if v.error then
error(string.format("unexpected error while updating state: %s", v.error))
end
local s = string.sub(v[1], 1, string.len(v[1]) - 1)
local f = state_mappings[s]
if f then f(self, s, v[2]) end
if s == "state" then
self.state.pause = v[2] == "pause"
end
end
for _, v in ipairs(self:execute { "currentsong" }) do
if v.error then
error(string.format("unexpected error while updating currentsong: %s", v.error))
end
if v[1] == "file:" then
self.state.file = table.concat(v, " ", 2)
end
end
end
return setmetatable(mpd, mpd.mt)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment