From 19b09a8cec94d77caa74c50b63079c4eedc6238e Mon Sep 17 00:00:00 2001
From: Přemysl Janouch <p.janouch@gmail.com>
Date: Sat, 9 Jan 2016 10:25:17 +0100
Subject: 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 <p.janouch@gmail.com>
+--
+-- 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)
-- 
cgit v1.2.3-70-g09d2