From 19b09a8cec94d77caa74c50b63079c4eedc6238e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sat, 9 Jan 2016 10:25:17 +0100 Subject: [PATCH] degesch: add a last-fm "now playing" plugin --- NEWS | 6 +- plugins/degesch/last-fm.lua | 155 ++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 plugins/degesch/last-fm.lua diff --git a/NEWS b/NEWS index d9ef75e..ebe7f7f 100644 --- a/NEWS +++ b/NEWS @@ -6,7 +6,11 @@ * degesch: resolve remote addresses asynchronously - * degesch: various bugfixes + * degesch: Lua API was improved and extended + + * degesch: added a basic last.fm "now playing" plugin + + * Various bugfixes 0.9.2 (2015-12-31) diff --git a/plugins/degesch/last-fm.lua b/plugins/degesch/last-fm.lua new file mode 100644 index 0000000..54f5d1c --- /dev/null +++ b/plugins/degesch/last-fm.lua @@ -0,0 +1,155 @@ +-- +-- 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) + -- There's no reasonable Lua package to parse HTTP that I could find + local s, e, v, status, message = string.find (data, "(%S+) (%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 + buffer:log ("Not playing anything right now") + else + local np = "Now playing: \"" .. name .. "\"" + if artist then np = np .. " by " .. artist end + if album then np = np .. " from " .. album end + buffer:log (np) + end +end + +-- Set up the connection and make the request +local on_connected = function (buffer, c, host) + -- 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) + 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) + 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) + running = nil + end, + on_error = function (e) + report_error (buffer, e) + running = nil + end + }) +end + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +-- TODO: +-- /np? to just retrieve the song and print it in the buffer +-- /np! to execute "/me is listening to " .. last retrieved song +-- /np to do both in succession + +-- Hook input to simulate new commands +degesch.hook_input (function (hook, buffer, input) + if input == "/np" then + make_request (buffer) + else + return input + end +end)