31275b69277cf1ae3210969c6537ddfcec155622
[mpd-lua.git] / mpd.lua
1 #!/usr/bin/lua
2
3 require "uci"
4 require("socket")
5 json=require("json")
6
7 function url_decode(str)
8   if not str then return nil end
9   str = string.gsub (str, "+", " ")
10   str = string.gsub (str, "%%(%x%x)", function(h) return
11     string.char(tonumber(h,16)) end)
12   str = string.gsub (str, "\r\n", "\n")
13   return str
14 end
15
16 function split(s, delimiter)
17     local result = {};
18     for match in (s..delimiter):gmatch("(.-)"..delimiter) do
19         result[#result+1] = match;
20     end
21     return result;
22 end
23
24 function string.starts(String,Start)
25    return string.sub(String,1,string.len(Start))==Start
26 end
27
28 function process_playlist(playlist)
29   local res={}
30   for _,record in pairs(playlist) do
31     local splitted = split(record,": ")
32     local name = splitted[2]
33     local splitted = split(splitted[1],":")
34     local id = splitted[1]
35     local rectype = splitted[2]
36     local rec = {}
37     if not (id == "OK") then
38       rec["id"] = id
39       rec["type"] = rectype
40       rec["name"] = name
41       res[id] = rec
42     end
43   end
44   return res
45 end
46
47 function process_playlists(playlists)
48   local res={}
49   for _,record in pairs(playlists) do
50     local splitted = split(record,": ")
51     if splitted[1]=="playlist" then
52       res[#res+1] = splitted[2]
53     end
54   end
55   return res
56 end
57
58 function process_directory(directory)
59   local res={}
60   for _,record in pairs(directory) do
61     local splitted = split(record,": ")
62     if splitted[1]=="directory" or splitted[1]=="file" then
63       local rec={}
64       rec["type"] = splitted[1]
65       rec["name"] = splitted[2]
66       res[#res+1] = rec
67     end
68   end
69   return res
70 end
71
72 function mpd_new(settings)
73     local client = {}
74     if settings == nil then settings = {} end
75
76     client.hostname = settings.hostname or "localhost"
77     client.port     = settings.port or 6600
78     client.desc     = settings.desc or client.hostname
79     client.password = settings.password
80     client.timeout  = settings.timeout or 1
81     client.retry    = settings.retry or 60
82
83     return client
84 end
85
86 function mpd_send(mpd,action,raw)
87
88     local command = string.format("%s\n", action)
89     local values = {}
90
91     -- connect to MPD server if not already done.
92     if not mpd.connected then
93         local now = os.time();
94         if not mpd.last_try or (now - mpd.last_try) > mpd.retry then
95             mpd.socket = socket.tcp()
96             mpd.socket:settimeout(mpd.timeout, 't')
97             mpd.last_try = os.time()
98             mpd.connected = mpd.socket:connect(mpd.hostname, mpd.port)
99             if not mpd.connected then
100                 return { errormsg = "could not connect" }
101             end
102             mpd.last_error = nil
103
104             -- Read the server's hello message
105             local line = mpd.socket:receive("*l")
106             if not line:match("^OK MPD") then -- Invalid hello message?
107                 mpd.connected = false
108                 return { errormsg = string.format("invalid hello message: %s", line) }
109             else
110                 _, _, mpd.version = string.find(line, "^OK MPD ([0-9.]+)")
111             end
112
113             -- send the password if needed
114             if mpd.password then
115                 local rsp = mpd_send(mpd,string.format("password %s", mpd.password))
116                 if rsp.errormsg then
117                     return rsp
118                 end
119             end
120         else
121             local retry_sec = mpd.retry - (now - mpd.last_try)
122             return { errormsg = string.format("%s (retrying in %d sec)", mpd.last_error, retry_sec) }
123         end
124     end
125
126     mpd.socket:send(command)
127
128     local line = ""; err=0
129     while not line:match("^OK$") do
130         line, err = mpd.socket:receive("*l")
131         if not line then -- closed,timeout (mpd killed?)
132             mpd.last_error = err
133             mpd.connected = false
134             mpd.socket:close()
135             return mpd_send(mpd,action)
136         end
137
138         if line:match("^ACK") then
139             return { errormsg = line }
140         end
141
142         if not raw then
143
144           local pattern = string.format("(%s)", ": ")
145           local i = string.find (line, pattern, 0)
146
147           if i ~= nil then
148               local key=string.sub(line,1,i-1)
149               local value=string.sub(line,i+2,-1)
150               values[string.lower(key)] = value
151           end
152
153         else
154
155           values[#values+1]=line
156
157         end
158     end
159
160     return values
161 end
162
163 x = uci.cursor()
164
165 settings = {}
166 settings['host'] = x.get("mpd","server","host") or "localhost"
167 settings['port'] = x.get("mpd","server","port") or 6600
168 settings['timeout'] = x.get("mpd","server","timeout") or 1
169
170 volstep = x.get("mpd","control","volume_step") or 3
171
172 password = x.get("mpd","server","password")
173 if password then
174   settings["password"] = password
175 end
176
177 m = mpd_new(settings)
178
179 command = url_decode(os.getenv('QUERY_STRING'))
180
181 if not command or command=="" then
182   command="idle"
183 end
184
185 if command=="play" or command=="pause" or command=="stop" or command=="previous" or command=="next" then
186
187   res=mpd_send(m,command)
188
189 elseif command=="idle" then
190
191   m.timeout=30
192   res=mpd_send(m,command)
193
194 elseif command=="vold" then
195
196   status=mpd_send(m,"status")
197   volume=tonumber(status["volume"])
198   res=mpd_send(m,"setvol "..(volume-volstep))
199
200 elseif command=="volu" then
201
202   status=mpd_send(m,"status")
203   volume=tonumber(status["volume"])
204   res=mpd_send(m,"setvol "..(volume+volstep))
205
206 elseif command=="status" then
207
208   res=mpd_send(m,"status")
209   song=res["song"]
210   playlist=mpd_send(m,"playlist",1)
211   pl=process_playlist(playlist)
212
213   if song then 
214     res['current_playing']=pl[song]['name']
215   else
216     res['current_playing']="No songs selected"
217   end
218
219 elseif command=="playlist" then
220
221   playlist=mpd_send(m,"playlist",1)
222   res=process_playlist(playlist)
223
224 elseif command=="repeat" then
225
226   status=mpd_send(m,"status")
227   rep=1-status["repeat"]
228   res=mpd_send(m,"repeat "..rep)
229
230 elseif string.starts(command,"cpl") then
231
232   cmd=split(command,"|")
233   id=cmd[3]
234   command=cmd[2]
235
236   if command=="playitem" then
237     command="play "..id
238     res=mpd_send(m,command)
239   end
240
241   if command=="clear" then
242     res=mpd_send(m,"clear")
243   end
244
245   if command=="remove" then
246     command="delete "..id
247     res=mpd_send(m,command)
248   end
249
250   if command=="moveup" then
251     command="swap "..id.." "..(id-1)
252     res=mpd_send(m,command)
253   end
254
255   if command=="movedown" then
256     command="swap "..id.." "..(id+1)
257     res=mpd_send(m,command)
258   end
259
260 elseif string.starts(command,"lists") then
261
262   cmd=split(command,"|")
263   command=cmd[2]
264
265   if command=="load" then
266     if not cmd[3] then
267       lists=mpd_send(m,"listplaylists",1)
268       res=process_playlists(lists)
269     else
270       res=mpd_send(m,"load "..cmd[3],1)
271     end
272   end
273
274   if command=="save" and cmd[3] then
275     res=mpd_send(m,"save "..cmd[3],1)
276   end
277
278   if command=="delete" and cmd[3] then
279     res=mpd_send(m,"rm "..cmd[3],1)
280   end
281
282   if command=="edit" then
283     if cmd[3] then
284       dir=mpd_send(m,"lsinfo \""..cmd[3].."\"",1)
285     else
286       dir=mpd_send(m,"lsinfo",1)
287     end
288     res=process_directory(dir)
289   end
290
291   if command=="add" then
292     res={}
293     if cmd[3] then
294       res=mpd_send(m,"add \""..cmd[3].."\"")
295     end
296   end
297
298 end
299
300 if not res then
301   print("Content-Type: text/plain\r\n")
302   print("MPD server - unknown command "..command)
303 elseif (res['error_msg']) then
304   print("Content-Type: text/plain\r\n")
305   print("MPD server connection error: "..res['error_msg'])
306 else
307   print "Content-Type: text/plain\r\n"
308   print(json.encode(res))
309 end