-- -- last-fm.lua: "now playing" feature using the last.fm API -- -- Dependencies: lua-cjson (from luarocks e.g.) -- -- I call this style closure-oriented programming -- -- Copyright (c) 2016, Přemysl Janouch -- -- Permission to use, copy, modify, and/or distribute this software for any -- purpose with or without fee is hereby granted. -- -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -- local cjson = require "cjson" -- Setup configuration to load last.fm API credentials from local user, api_key degesch.setup_config { user = { type = "string", comment = "last.fm username", on_change = function (v) user = v end }, api_key = { type = "string", comment = "last.fm API key", on_change = function (v) api_key = v end }, } -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- Generic error reporting local report_error = function (buffer, error) buffer:log ("last-fm error: " .. error) end -- Process data return by the server and extract the now playing song local process = function (buffer, data, action) -- There's no reasonable Lua package to parse HTTP that I could find local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n") if not s then return "server returned unexpected data" end if status ~= "200" then return status .. " " .. message end local s, e = string.find (data, "\r\n\r\n") if not s then return "server returned unexpected data" end local parser = cjson.new () data = parser.decode (string.sub (data, e + 1)) if not data.recenttracks or not data.recenttracks.track then return "invalid response" end -- Need to make some sense of the XML automatically converted to JSON local text_of = function (node) if type (node) ~= "table" then return node end return node["#text"] ~= "" and node["#text"] or nil end local name, artist, album for i, track in ipairs (data.recenttracks.track) do if track["@attr"] and track["@attr"].nowplaying then if track.name then name = text_of (track.name) end if track.artist then artist = text_of (track.artist) end if track.album then album = text_of (track.album) end end end if not name then action (false) else local np = "\"" .. name .. "\"" if artist then np = np .. " by " .. artist end if album then np = np .. " from " .. album end action (np) end end -- Set up the connection and make the request local on_connected = function (buffer, c, host, action) -- Buffer data in the connection object c.data = "" c.on_data = function (data) c.data = c.data .. data end -- And process it after we receive everything c.on_eof = function () error = process (buffer, c.data, action) if error then report_error (buffer, error) end c:close () end c.on_error = function (e) report_error (buffer, e) end -- Make the unencrypted HTTP request local url = "/2.0/?method=user.getrecenttracks&user=" .. user .. "&limit=1&api_key=" .. api_key .. "&format=json" c:send ("GET " .. url .. " HTTP/1.1\r\n") c:send ("User-agent: last-fm.lua\r\n") c:send ("Host: " .. host .. "\r\n") c:send ("Connection: close\r\n") c:send ("\r\n") end -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- Avoid establishing more than one connection at a time local running -- Initiate a connection to last.fm servers async, await = degesch.async, coroutine.yield local make_request = function (buffer, action) if not user or not api_key then report_error (buffer, "configuration is incomplete") return end if running then running:cancel () end running = async.go (function () local c, host, e = await (async.dial ("ws.audioscrobbler.com", 80)) if e then report_error (buffer, e) else on_connected (buffer, c, host, action) end running = nil end) end -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - local now_playing local tell_song = function (buffer) if now_playing == nil then buffer:log ("last-fm: I don't know what you're listening to") elseif not now_playing then buffer:log ("last-fm: not playing anything right now") else buffer:log ("last-fm: now playing: " .. now_playing) end end local send_song = function (buffer) if not now_playing then tell_song (buffer) else buffer:execute ("/me is listening to " .. now_playing) end end -- Hook input to simulate new commands degesch.hook_input (function (hook, buffer, input) if input == "/np" then make_request (buffer, function (np) now_playing = np send_song (buffer) end) elseif input == "/np?" then make_request (buffer, function (np) now_playing = np tell_song (buffer) end) elseif input == "/np!" then send_song (buffer) else return input end end)