b5e180d5adf38465a3e284af0f766d1a310a12f2
[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" then
186
187   res=mpd_send(m,command)
188
189 elseif command=="previous" or command=="next" then
190
191   res=mpd_send(m,"play")
192   res=mpd_send(m,command)
193
194 elseif command=="idle" then
195
196   m.timeout=30
197   res=mpd_send(m,command)
198
199 elseif command=="vold" then
200
201   status=mpd_send(m,"status")
202   volume=tonumber(status["volume"])
203   res=mpd_send(m,"setvol "..(volume-volstep))
204
205 elseif command=="volu" then
206
207   status=mpd_send(m,"status")
208   volume=tonumber(status["volume"])
209   res=mpd_send(m,"setvol "..(volume+volstep))
210
211 elseif string.starts(command,"fastfwd") then
212
213   cmd=split(command,"|")
214   skip=tonumber(cmd[2])
215   if not skip then
216     skip=15
217   end
218
219   status=mpd_send(m,"status")
220   rec_time=status["time"]
221   song=status["song"]
222   
223   if song then
224
225     if rec_time then
226       rec_time=split(rec_time,":")
227       cur_time=tonumber(rec_time[1])
228
229       track_time=tonumber(rec_time[2])
230       cur_time=cur_time+skip
231       if cur_time>track_time then
232         cur_time=track_time
233       end
234
235       mpd_send(m,"seek "..song.." "..cur_time)
236
237     else
238
239       mpd_send(m,"play")
240
241     end  
242   
243   end
244
245   res={}
246
247 elseif string.starts(command,"rewind") then
248
249   cmd=split(command,"|")
250   skip=tonumber(cmd[2])
251   if not skip then
252     skip=15
253   end
254
255   status=mpd_send(m,"status")
256   rec_time=status["time"]
257   song=status["song"]
258   
259   if song then
260
261     if rec_time then
262       rec_time=split(rec_time,":")
263       cur_time=tonumber(rec_time[1])
264
265       track_time=tonumber(rec_time[2])
266       cur_time=cur_time-skip
267       if cur_time<0 then
268         cur_time=0
269       end
270
271       mpd_send(m,"seek "..song.." "..cur_time)
272
273     else
274
275       mpd_send(m,"play")
276       mpd_send(m,"previous")
277
278     end  
279   
280   end
281
282   res={}
283
284 elseif command=="status" then
285
286   res=mpd_send(m,"status")
287   song=res["song"]
288   playlist=mpd_send(m,"playlist",1)
289   pl=process_playlist(playlist)
290
291   if song then 
292     res['current_playing']=pl[song]['name']
293   else
294     res['song']="--"
295     res['current_playing']="---"
296   end
297
298 elseif command=="playlist" then
299
300   playlist=mpd_send(m,"playlist",1)
301   res=process_playlist(playlist)
302
303 elseif command=="repeat" then
304
305   status=mpd_send(m,"status")
306   rep=1-status["repeat"]
307   res=mpd_send(m,"repeat "..rep)
308
309 elseif string.starts(command,"cpl") then
310
311   cmd=split(command,"|")
312   id=cmd[3]
313   command=cmd[2]
314
315   if command=="playitem" then
316     command="play "..id
317     res=mpd_send(m,command)
318   end
319
320   if command=="clear" then
321     res=mpd_send(m,"clear")
322   end
323
324   if command=="remove" then
325     command="delete "..id
326     res=mpd_send(m,command)
327   end
328
329   if command=="moveup" then
330     command="swap "..id.." "..(id-1)
331     res=mpd_send(m,command)
332   end
333
334   if command=="movedown" then
335     command="swap "..id.." "..(id+1)
336     res=mpd_send(m,command)
337   end
338
339 elseif string.starts(command,"lists") then
340
341   cmd=split(command,"|")
342   command=cmd[2]
343
344   if command=="load" then
345     if not cmd[3] then
346       lists=mpd_send(m,"listplaylists",1)
347       res=process_playlists(lists)
348     else
349       res=mpd_send(m,"load "..cmd[3],1)
350     end
351   end
352
353   if command=="save" and cmd[3] then
354     res=mpd_send(m,"save "..cmd[3],1)
355   end
356
357   if command=="delete" and cmd[3] then
358     res=mpd_send(m,"rm "..cmd[3],1)
359   end
360
361   if command=="edit" then
362     if cmd[3] then
363       dir=mpd_send(m,"lsinfo \""..cmd[3].."\"",1)
364     else
365       dir=mpd_send(m,"lsinfo",1)
366     end
367     res=process_directory(dir)
368   end
369
370   if command=="add" then
371     res={}
372     if cmd[3] then
373       res=mpd_send(m,"add \""..cmd[3].."\"")
374     end
375   end
376
377 end
378
379 if not res then
380   print("Content-Type: text/plain\r\n")
381   print("MPD server - unknown command "..command)
382 elseif (res['error_msg']) then
383   print("Content-Type: text/plain\r\n")
384   print("MPD server connection error: "..res['error_msg'])
385 else
386   print "Content-Type: text/plain\r\n"
387   print(json.encode(res))
388 end