]> git.llucax.com Git - personal/ion3-config.git/blob - statusd_mpd.lua
Port psi_status.sh to Python and make it a server.
[personal/ion3-config.git] / statusd_mpd.lua
1 -- statusd for MPD (Music Player Daemon) using a single persistent connection
2 --
3 -- bugs/requests/comments: Leandro Lucarella <llucax@gmail.com>
4 --
5 -- Based on statusd_exec.lua from Antti Vähäkotamäki (the async code) and
6 -- statusd_mpd.lua from delirium@hackish.org (the mpd glue code).
7 --
8 -- Usage is self documented in the default settings section. It provides the
9 -- same functionality from statusd_mpd.luam from delirium@hackish.org but it
10 -- uses a single persistent connection to MPD and does better usage from hints
11 -- (the status is "hinted important" when mpd status or volume change (and
12 -- optionally, up to important_time seconds from the begining of each song).
13 --
14 -- This is free software; you can redistribute it and/or modify it under
15 -- the terms of the GNU Lesser General Public License as published by
16 -- the Free Software Foundation; either version 2.1 of the License, or
17 -- (at your option) any later version.
18
19 local defaults={
20     -- netcat path
21     netcat = 'netcat',
22
23     -- 500 or less makes seconds increment relatively smoothly while playing
24     update_interval = 1000,
25
26     -- mpd server info (localhost:6600 are mpd defaults)
27     address = "localhost",
28     port = 6600,
29
30     -- mpd password (if any)
31     password = nil,
32
33     -- seconds to consider the mpd in 'important' hint (0 never, -1 always)
34     important_time = 10, -- seconds
35
36     -- display template
37     -- ---
38     -- can use the following:
39     --   track metadata: %artist, %title, %num, %album, %year, %len
40     --   current track position: %pos
41     --   volume: %volume
42     --   escape for the percent character: %%
43
44     -- a default template
45     template = "%artist - %num - %title (%pos / %len)",
46
47     -- output debug filename (false or nil for no debug output)
48     debug = false,
49 }
50
51 local settings = table.join(statusd.get_config("mpd"), defaults)
52
53 local timer
54 local last_volume
55 local last_state
56 local debug_file
57
58 -- Function for starting and restarting execution of a process
59
60 local function start_execution()
61
62     -- Set up a function for receiving the data
63     -- and informing the statusbar
64
65     local function process_input(data)
66
67         -- At first, we are not initialized
68         local inited = false
69
70         -- When we get new data, output the last complete line
71
72         while data do
73
74             -- debug
75             if settings.debug then
76                 debug_file:write("ion-statusd [statusd_mpd]: DEBUG mpd data:\n"
77                                 .. data .. "\n\n")
78                 debug_file:flush()
79             end
80
81             if not inited then
82                 -- welcome msg
83                 if string.sub(data, 1, 6) ~= "OK MPD" then
84                     statusd.inform("mpd_hint", "critical")
85                     statusd.inform("mpd", "bad mpd response on init")
86                 end
87
88                 -- 'password' response (if necessary)
89                 if settings.password ~= nil then
90                     if string.sub(data, 1, 2) ~= "OK" then
91                         statusd.inform("mpd_hint", "critical")
92                         statusd.inform("mpd_hint", "bad mpd password")
93                     end
94                 end
95
96                 -- everything ok, receive more data
97                 inited = true
98                 data = coroutine.yield()
99             end
100
101             -- process only if we have 2 OK responses
102             local start, end_ = string.find(data, "\nOK")
103             if start and string.find(data, "\nOK", end_) then
104
105                 local info = {}
106                 local hint = "normal"
107
108                 for attrib, val in string.gfind(data, "([^\n]-): ([^\n]*)") do
109
110                     if attrib == "time" then
111                         _, _, info.pos, info.len = string.find(val,
112                                                                 "(%d+):(%d+)")
113                         if settings.important_time == -1
114                                 or info.pos+0 <= settings.important_time
115                                         and settings.important_time ~= 0 then
116                             hint = "important"
117                         end
118                         info.pos = string.format("%d:%02d",
119                                                     math.floor(info.pos / 60),
120                                                     math.mod(info.pos, 60))
121                         info.len = string.format("%d:%02d",
122                                                     math.floor(info.len / 60),
123                                                     math.mod(info.len, 60))
124                     elseif attrib == "state" then
125                         info.state = val
126                         if info.state ~= last_state then
127                             hint = "important"
128                         end
129                         last_state = info.state
130                     elseif attrib == "volume" then
131                         info.volume = val .. "%"
132                         if info.volume ~= last_volume then
133                             hint = "important"
134                         end
135                         last_volume = info.volume
136                     elseif attrib == "Artist" then
137                         info.artist = val
138                     elseif attrib == "Title" then
139                         info.title = val
140                     elseif attrib == "Album" then
141                         info.album = val
142                     elseif attrib == "Track" then
143                         info.num = val
144                     elseif attrib == "Date" then
145                         info.year = val
146                     end
147                 end
148
149                 -- done querying; now build the string
150                 statusd.inform("mpd_hint", hint)
151                 if info.state == "play" then
152                     local mpd_st = settings.template
153                     -- fill in %values
154                     mpd_st = string.gsub(mpd_st, "%%([%w%_]+)",
155                             function (x) return(info[x]  or "") end)
156                     mpd_st = string.gsub(mpd_st, "%%%%", "%%")
157                     statusd.inform("mpd", mpd_st)
158                 elseif info.state == "pause" then
159                     statusd.inform("mpd", "Paused")
160                 else
161                     statusd.inform("mpd", "No song playing")
162                 end
163
164                 -- Wait for bgread to signal that more data
165                 -- is available or the program has exited
166
167                 data = coroutine.yield()
168             else
169                 -- There was no enough data to interpret a complete status
170                 data = data .. coroutine.yield()
171             end
172         end
173
174         -- Program has exited.
175         -- Start a timer for a new execution if set.
176
177         timer:set(settings.update_interval,
178                   function() start_execution() end)
179     end
180
181     -- Set up a simple function for printing errors
182
183     local function process_error(data)
184         while data do
185             io.stderr:write("ion-statusd [statusd_mpd]: " .. data)
186             io.stderr:flush()
187             data = coroutine.yield()
188         end
189     end
190
191     -- Execute the program in the background and create
192     -- coroutine functions to handle input and error data
193     local sleep = settings.update_interval / 1000
194     sleep = string.gsub(sleep, '(%d+),(%d+)', '%1.%2')
195     local cmd = 'while sleep ' .. sleep .. '; do ' ..
196                 'echo "status\ncurrentsong"; done | ' ..
197                 settings.netcat .. ' ' .. settings.address .. ' ' ..
198                 settings.port
199     if settings.password ~= nil then
200         cmd = "echo 'password " .. settings.password .. "'; " .. cmd
201     end
202     statusd.popen_bgread(cmd, coroutine.wrap(process_input),
203                               coroutine.wrap(process_error))
204
205 end
206
207 -- Now start execution of all defined processes
208
209 if settings.debug then debug_file = io.open("ion_statusd_mpd.log", "a+") end
210 timer = statusd.create_timer()
211 start_execution()
212
213 -- vim: set et sts=4 sw=4 :