Add a recipe for an MPD widget (#71)

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2017-01-24 09:09:11 +01:00 committed by Daniel Hahler
parent 3e8fadcb74
commit 12734ef793
3 changed files with 272 additions and 1 deletions

View File

@ -11,3 +11,4 @@ The recipes section is where you can find useful snippets and tutorials on how t
## Widgets ## Widgets
* [Lain Widget Library](https://github.com/copycat-killer/lain) * [Lain Widget Library](https://github.com/copycat-killer/lain)
* [[MPD current song|recipes/mpc]]

200
recipes/mpc.lua Normal file
View File

@ -0,0 +1,200 @@
local lgi = require "lgi"
local GLib = lgi.GLib
local Gio = lgi.Gio
local mpc = {}
function mpc.new(host, port, password, error_handler, ...)
local self = setmetatable({
_host = host,
_port = port,
_password = password,
_error_handler = error_handler or function() end,
_connected = false,
_try_reconnect = false,
_idle_commands = { ... }
}, { __index = mpc })
self:_connect()
return self
end
function mpc:_error(err)
self._connected = false
self._error_handler(err)
self._try_reconnect = not self._try_reconnect
if self._try_reconnect then
self:_connect()
end
end
function mpc:_connect()
if self._connected then return end
-- Reset all of our state
self._reply_handlers = {}
self._pending_reply = {}
self._idle_commands_pending = false
self._idle = false
self._connected = true
-- Set up a new TCP connection
local client = Gio.SocketClient()
local conn, err = client:connect_to_host(self._host, self._port)
if not conn then
self:_error(err)
return false
end
local input, output = conn:get_input_stream(), conn:get_output_stream()
self._conn, self._output, self._input = conn, output, Gio.DataInputStream.new(input)
-- Read the welcome message
self._input:read_line()
if self._password and self._password ~= "" then
self:_send("password " .. self._password)
end
-- Set up the reading loop. This will asynchronously read lines by
-- calling itself.
local do_read
do_read = function()
self._input:read_line_async(GLib.PRIORITY_DEFAULT, nil, function(obj, res)
local line, err = obj:read_line_finish(res)
-- Ugly API. On success we get string, length-of-string
-- and on error we get nil, error
--if tostring(line) == "" and err == 1 then
if tostring(line) == "" then
err = "Connection closed"
end
if type(err) ~= "number" then
self._output, self._input = nil, nil
self:_error(err)
else
do_read()
line = tostring(line)
if line == "OK" or line:match("^ACK ") then
local success = line == "OK"
local arg
if success then
arg = self._pending_reply
else
arg = { line }
end
self._reply_handlers[1](success, arg)
table.remove(self._reply_handlers, 1)
self._pending_reply = {}
else
local _, _, key, value = string.find(line, "([^:]+):%s(.+)")
if key then
self._pending_reply[string.lower(key)] = value
end
end
end
end)
end
do_read()
-- To synchronize the state on startup, send the idle commands now. As a
-- side effect, this will enable idle state.
self:_send_idle_commands(true)
return self
end
function mpc:_send_idle_commands(skip_stop_idle)
-- We use a ping to unset this to make sure we never get into a busy
-- loop sending idle / unidle commands. Next call to
-- _send_idle_commands() might be ignored!
if self._idle_commands_pending then
return
end
if not skip_stop_idle then
self:_stop_idle()
end
self._idle_commands_pending = true
for i = 1, #self._idle_commands, 2 do
self:_send(self._idle_commands[i], self._idle_commands[i+1])
end
self:_send("ping", function()
self._idle_commands_pending = false
end)
self:_start_idle()
end
function mpc:_start_idle()
if self._idle then
error("Still idle?!")
end
self:_send("idle", function(success, reply)
if reply.changed then
-- idle mode was disabled by mpd
self:_send_idle_commands()
end
end)
self._idle = true
end
function mpc:_stop_idle()
if not self._idle then
error("Not idle?!")
end
self._output:write("noidle\n")
self._idle = false
end
function mpc:_send(command, callback)
if self._idle then
error("Still idle in send()?!")
end
self._output:write(command .. "\n")
table.insert(self._reply_handlers, callback or function() end)
end
function mpc:send(...)
self:_connect()
if not self._connected then
return
end
local args = { ... }
if not self._idle then
error("Something is messed up, we should be idle here...")
end
self:_stop_idle()
for i = 1, #args, 2 do
self:_send(args[i], args[i+1])
end
self:_start_idle()
end
function mpc:toggle_play()
self:send("status", function(success, status)
if status.state == "stop" then
self:send("play")
else
self:send("pause")
end
end)
end
return mpc
--[[
-- Example on how to use this (standalone)
local m = mpc.new("localhost", 6600, nil, "status", function(success, status) print("status is", status) end)
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
-- Test command submission
m:send("status", function(_, s) print(s.state) end,
"currentsong", function(_, s) print(s.title) end)
m:send("status", function(_, s) print(s.state) end)
-- Force a reconnect
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function()
m._conn:close()
end)
end)
GLib.MainLoop():run()
--]]

70
recipes/mpc.mdwn Normal file
View File

@ -0,0 +1,70 @@
# MPD integration
This page describes an integration with the [Music Player
Daemon](https://www.musicpd.org/). This consists of two parts: A [[pure Lua
library for talking to mpc|mpc.lua]] and an example on how to use this for a
widget with awesome.
## The library
The library provides a function `mpc.new` that creates a new object representing
a connection to MPD. It can be used as follows:
local connection = require("mpc").new(host, port, password, error_handler, idle_commands...)
This will establish a TCP connection to the given host and port and, if a
password is given, log in to the MPD server. Whenever an error happens (for
example the connection is lost or the password is rejected), the given error
handler function is called with the error as its argument. The next time the
connection is used, an automatic reconnection is attempted.
A description of the MPD protocol can be fined
[here](https://www.musicpd.org/doc/protocol/). This library only provides
low-level access to the protocol. However, special support for the idle command
is provided via extra arguments to the `new` function. This will be made clear
in an example below.
For example, to get information about the currently playing song:
connection:send("currentsong", function(success, data)
if not success then print("command failed") end
print("Information about the current song:")
require("gears.debug").dump(data)
end)
## A sample widget
The following keeps a textbox up-to-date with the MPD status. It automatically
updates when the current MPD state changes.
local mpc = require("mpc")
local textbox = require("wibox.widget.textbox")
local mpd_widget = textbox()
local state, title, artist, file = "stop", "", "", ""
local function update_widget()
local text = "Current MPD status: "
text = text .. tostring(artist or "") .. " - " .. tostring(title or "")
if state == "pause" then
text = text .. " (paused)"
end
if state == "stop" then
text = text .. " (stopped)"
end
mpd_widget.text = text
end
local function error_handler(err)
mpd_widget:set_text("Error: " .. tostring(err))
end
local connection = mpc.new("localhost", 6600, "", error_handler,
"status", function(_, result)
state = result.state
end,
"currentsong", function(_, result)
title, artist, file = result.title, result.artist, result.file
pcall(update_widget)
end)
If you actually want to be able to control MPD's behaviour, you could for
example do the following to pause/unpause when clicking on the widget:
mpd_widget:buttons(awful.button({}, 1, function() connection:toggle_play() end))