Add a recipe for an MPD widget (#71)
Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
parent
3e8fadcb74
commit
12734ef793
|
@ -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]]
|
||||||
|
|
|
@ -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()
|
||||||
|
--]]
|
|
@ -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))
|
Loading…
Reference in New Issue