xK/plugins/xC/last-fm.lua
Přemysl Eric Janouch 50057d5149
Come up with sillier names for the binaries
I'm not entirely sure, but it looks like some people might not like
jokes about the Holocaust.

On a more serious note, the project has become more serious over
the 7 or so years of its existence.
2021-08-06 16:43:59 +02:00

179 lines
5.1 KiB
Lua

--
-- 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 Eric Janouch <p@janouch.name>
--
-- 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
xC.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 = xC.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
xC.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)