Created
January 9, 2018 00:05
-
-
Save jarcode-foss/de223e93d51011d3bd71ec56a97601b9 to your computer and use it in GitHub Desktop.
MPD client code with plain Lua and C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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