--
-- 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, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- 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["#text"] end
return node
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
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.abort () end
running = degesch.connect ("ws.audioscrobbler.com", 80, {
on_success = function (c, host)
on_connected (buffer, c, host, action)
running = nil
end,
on_error = function (e)
report_error (buffer, e)
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)