f421b755ba9d0aa817d2dc5ce4f4e721d0f71bbc
[mpd-lua.git] / mpd.lua
1 #!/usr/bin/lua
2
3 local hasuci,uci = pcall(require,"uci")
4
5 require("socket")
6 json=require("json")
7
8 function url_decode(str)
9   if not str then return nil end
10   str = string.gsub (str, "+", " ")
11   str = string.gsub (str, "%%(%x%x)", function(h) return
12     string.char(tonumber(h,16)) end)
13   str = string.gsub (str, "\r\n", "\n")
14   return str
15 end
16
17 function url_encode(str)
18
19   --Ensure all newlines are in CRLF form
20   str = string.gsub (str, "\r?\n", "\r\n")
21
22   --Percent-encode all non-unreserved characters
23   --as per RFC 3986, Section 2.3
24   --(except for space, which gets plus-encoded)
25   str = string.gsub (str, "([^%w%-%.%_%~:/])",
26     function (c) return string.format ("%%%02X", string.byte(c)) end)
27
28   --Convert spaces to plus signs
29   str = string.gsub (str, " ", "+")
30
31   return str
32 end
33
34 function split(s, delimiter)
35     local result = {};
36     for match in (s..delimiter):gmatch("(.-)"..delimiter) do
37         result[#result+1] = match;
38     end
39     return result;
40 end
41
42 function string.starts(String,Start)
43    return string.sub(String,1,string.len(Start))==Start
44 end
45
46 function filename(path)
47   local path_elems=split(path,'/')
48   return path_elems[#path_elems]
49 end
50
51 function process_playlist(playlist)
52   local res={}
53   local rec={}
54   for _,record in pairs(playlist) do
55     if record=="OK" then 
56       break
57     end  
58     local splitted = split(record,": ")
59     local fieldname = string.lower(splitted[1])
60     local fieldvalue = splitted[2]
61     rec[fieldname] = fieldvalue
62     if fieldname=="id" then
63       local title = rec['title']
64       if title then
65         local filtered_title=string.gsub(string.lower(title),'track','')
66         local filtered_title=string.gsub(filtered_title,'[ _.-]','')
67       else
68         filtered_title=""
69       end  
70       if not filtered_title or string.len(filtered_title)<4 then
71         if title then
72           rec['title']=title..' - '..filename(rec['file'])
73         else
74           rec['title']=filename(rec['file'])
75         end  
76       end
77       res[#res+1] = rec
78       rec={}
79     end
80   end
81   return res
82 end
83
84 function find_by_id(playlist,id)
85   for key,record in pairs(playlist) do
86     if record['id']==id then
87       return key
88     end
89   end
90   return nil
91 end
92
93 function process_playlists(playlists)
94   local res={}
95   for _,record in pairs(playlists) do
96     local splitted = split(record,": ")
97     if splitted[1]=="playlist" then
98       res[#res+1] = splitted[2]
99     end
100   end
101   return res
102 end
103
104 function process_directory(directory)
105   local res={}
106   for _,record in pairs(directory) do
107     local splitted = split(record,": ")
108     if splitted[1]=="directory" or splitted[1]=="file" then
109       local rec={}
110       rec["type"] = splitted[1]
111       rec["name"] = splitted[2]
112       res[#res+1] = rec
113     end
114   end
115   return res
116 end
117
118 function mpd_new(settings)
119     local client = {}
120
121     if settings == nil then settings = {} end
122
123     client.hostname = settings.hostname or "localhost"
124     client.port     = settings.port or 6600
125     client.desc     = settings.desc or client.hostname
126     client.password = settings.password
127     client.timeout  = settings.timeout or 1
128     client.retry    = settings.retry or 60
129
130     return client
131 end
132
133 function mpd_send(mpd,action,raw)
134
135     local command = string.format("%s\n", action)
136     local values = {}
137
138     -- connect to MPD server if not already done.
139     if not mpd.connected then
140         local now = os.time();
141         if not mpd.last_try or (now - mpd.last_try) > mpd.retry then
142             mpd.socket = socket.tcp()
143             mpd.socket:settimeout(mpd.timeout, 't')
144             mpd.last_try = os.time()
145             mpd.connected = mpd.socket:connect(mpd.hostname, mpd.port)
146             if not mpd.connected then
147                 return { errormsg = "could not connect" }
148             end
149             mpd.last_error = nil
150
151             -- Read the server's hello message
152             local line = mpd.socket:receive("*l")
153             if not line:match("^OK MPD") then -- Invalid hello message?
154                 mpd.connected = false
155                 return { errormsg = string.format("invalid hello message: %s", line) }
156             else
157                 _, _, mpd.version = string.find(line, "^OK MPD ([0-9.]+)")
158             end
159
160             -- send the password if needed
161             if mpd.password then
162                 local rsp = mpd_send(mpd,string.format("password %s", mpd.password))
163                 if rsp.errormsg then
164                     return rsp
165                 end
166             end
167         else
168             local retry_sec = mpd.retry - (now - mpd.last_try)
169             return { errormsg = string.format("%s (retrying in %d sec)", mpd.last_error, retry_sec) }
170         end
171     end
172
173     mpd.socket:send(command)
174
175     local line = ""; err=0
176     while not line:match("^OK$") do
177         line, err = mpd.socket:receive("*l")
178         if not line then -- closed,timeout (mpd killed?)
179             mpd.last_error = err
180             mpd.connected = false
181             mpd.socket:close()
182             return mpd_send(mpd,action)
183         end
184
185         if line:match("^ACK") then
186             return { errormsg = line }
187         end
188
189         if not raw then
190
191           local pattern = string.format("(%s)", ": ")
192           local i = string.find (line, pattern, 0)
193
194           if i ~= nil then
195               local key=string.sub(line,1,i-1)
196               local value=string.sub(line,i+2,-1)
197               values[string.lower(key)] = value
198           end
199
200         else
201
202           values[#values+1]=line
203
204         end
205     end
206
207     return values
208 end
209
210 hasuci = false
211
212 if hasuci then
213
214   x = uci.cursor()
215
216   settings = {}
217   settings['host'] = x.get("mpd","server","host") or "localhost"
218   settings['port'] = x.get("mpd","server","port") or 6600
219   settings['timeout'] = x.get("mpd","server","timeout") or 1
220
221   volstep = x.get("mpd","control","volume_step") or 3
222
223   password = x.get("mpd","server","password")
224
225 else
226
227   config="/etc/mpd-lua.json"
228
229   settings={}
230   local open = io.open
231   file = open(config, "r")
232   if file then
233     content = file:read "*a"
234     file:close()
235     settings=json.decode(content)
236   end
237     
238   settings['host'] = settings['host'] or "localhost"
239   settings['port'] = settings["port"] or 6600
240   settings['timeout'] = settings["timeout"] or 1
241
242   volstep = settings["volstep"] or 3
243
244 end
245   
246 if password then
247   settings["password"] = password
248 end
249
250 m = mpd_new(settings)
251
252 command = url_decode(os.getenv('QUERY_STRING'))
253
254 if not command or command=="" then
255   command="idle"
256 end
257
258 if command=="play" or command=="pause" or command=="stop" then
259
260   res=mpd_send(m,command)
261
262 elseif command=="previous" or command=="next" then
263
264   res=mpd_send(m,"play")
265   res=mpd_send(m,command)
266
267 elseif command=="idle" then
268
269   m.timeout=30
270   res=mpd_send(m,command)
271
272 elseif command=="vold" then
273
274   status=mpd_send(m,"status")
275   volume=tonumber(status["volume"])
276   res=mpd_send(m,"setvol "..(volume-volstep))
277
278 elseif command=="volu" then
279
280   status=mpd_send(m,"status")
281   volume=tonumber(status["volume"])
282   res=mpd_send(m,"setvol "..(volume+volstep))
283
284 elseif string.starts(command,"fastfwd") then
285
286   cmd=split(command,"|")
287   skip=tonumber(cmd[2])
288   if not skip then
289     skip=15
290   end
291
292   status=mpd_send(m,"status")
293   rec_time=status["time"]
294   song=status["songid"]
295   
296   if song then
297
298     if rec_time then
299       rec_time=split(rec_time,":")
300       cur_time=tonumber(rec_time[1])
301
302       track_time=tonumber(rec_time[2])
303       if track_time then
304         cur_time=cur_time+skip
305         if cur_time>track_time then
306           cur_time=track_time
307         end
308       end  
309       mpd_send(m,"seekid "..song.." "..cur_time)
310
311     else
312
313       mpd_send(m,"play")
314
315     end  
316   
317   end
318
319   res={}
320
321 elseif string.starts(command,"rewind") then
322
323   cmd=split(command,"|")
324   skip=tonumber(cmd[2])
325   if not skip then
326     skip=15
327   end
328
329   status=mpd_send(m,"status")
330   rec_time=status["time"]
331   song=status["songid"]
332   
333   if song then
334
335     if rec_time then
336       rec_time=split(rec_time,":")
337       cur_time=tonumber(rec_time[1])
338       cur_time=cur_time-skip
339       if cur_time<0 then
340         cur_time=0
341       end
342
343       print(song)
344       print(cur_time)
345       mpd_send(m,"seekid "..song.." "..cur_time)
346         
347     else
348
349       mpd_send(m,"play")
350       mpd_send(m,"previous")
351
352     end  
353   
354   end
355
356   res={}
357
358 elseif command=="status" then
359
360   res=mpd_send(m,"status")
361   song=tonumber(res["songid"])
362   if song then 
363     playlist=mpd_send(m,"playlistid "..song,1)
364     pl=process_playlist(playlist)
365     res['current_playing']=pl[1]['title']
366   else  
367     res['current_playing']="---"
368   end
369
370 elseif command=="playlist" then
371
372   playlist=mpd_send(m,"playlistinfo",1)
373   res=process_playlist(playlist)
374
375 elseif command=="repeat" then
376
377   status=mpd_send(m,"status")
378   rep=1-status["repeat"]
379   res=mpd_send(m,"repeat "..rep)
380
381 elseif string.starts(command,"cpl") then
382
383   cmd=split(command,"|")
384   id=cmd[3]
385   command=cmd[2]
386
387   if command=="playitem" then
388     command="playid "..id
389     res=mpd_send(m,command)
390   end
391
392   if command=="clear" then
393     res=mpd_send(m,"clear")
394   end
395
396   if command=="remove" then
397     command="deleteid "..id
398     res=mpd_send(m,command)
399   end
400
401   if command=="moveup" then
402     playlist=mpd_send(m,"playlistinfo ",1)
403     pl=process_playlist(playlist)
404     idx=find_by_id(pl,id)
405     if idx>1 then
406       command="swap "..(idx-1).." "..(idx-2)
407     end  
408     res=mpd_send(m,command)
409   end
410
411   if command=="movedown" then
412     playlist=mpd_send(m,"playlistinfo ",1)
413     pl=process_playlist(playlist)
414     idx=find_by_id(pl,id)
415     if idx<#pl then
416       command="swap "..(idx-1).." "..(idx)
417     end  
418     res=mpd_send(m,command)
419   end
420
421 elseif string.starts(command,"lists") then
422
423   cmd=split(command,"|")
424   command=cmd[2]
425
426   if command=="load" then
427     if not cmd[3] then
428       lists=mpd_send(m,"listplaylists",1)
429       res=process_playlists(lists)
430     else
431       res=mpd_send(m,"load \""..cmd[3].."\"",1)
432     end
433   end
434
435   if command=="save" and cmd[3] then
436     res=mpd_send(m,"save \""..cmd[3].."\"",1)
437   end
438
439   if command=="delete" and cmd[3] then
440     res=mpd_send(m,"rm \""..cmd[3].."\"",1)
441   end
442
443   if command=="edit" then
444     if cmd[3] then
445       dir=mpd_send(m,"lsinfo \""..cmd[3].."\"",1)
446     else
447       dir=mpd_send(m,"lsinfo",1)
448     end
449     res=process_directory(dir)
450   end
451
452   if command=="add" then
453     res={}
454     if cmd[3] then
455       res=mpd_send(m,"add \""..cmd[3].."\"")
456       if (res['errormsg']) then
457         path=url_encode(cmd[3])
458         res=mpd_send(m,"add \""..path.."\"")
459       end
460     end
461   end
462
463 end
464
465 if not res then
466   print("Content-Type: text/plain\r\n")
467   print("MPD server - unknown command "..command)
468 elseif (res['errormsg']) then
469   print("Content-Type: text/plain\r\n")
470   print("MPD server connection error: "..res['errormsg'])
471 else
472   print "Content-Type: text/plain\r\n"
473   print(json.encode(res))
474 end