93dab215f13fc52f81f24a423af1876b583331df
[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 if hasuci then
211
212   x = uci.cursor()
213
214   settings = {}
215   settings['host'] = x.get("mpd","server","host") or "localhost"
216   settings['port'] = x.get("mpd","server","port") or 6600
217   settings['timeout'] = x.get("mpd","server","timeout") or 1
218
219   volstep = x.get("mpd","control","volume_step") or 3
220
221   password = x.get("mpd","server","password")
222
223 else
224
225   config="/etc/mpd-lua.json"
226
227   settings={}
228   local open = io.open
229   file = open(config, "r")
230   if file then
231     content = file:read "*a"
232     file:close()
233     settings=json.decode(content)
234   end
235     
236   settings['host'] = settings['host'] or "localhost"
237   settings['port'] = settings["port"] or 6600
238   settings['timeout'] = settings["timeout"] or 1
239
240   volstep = settings["volstep"] or 3
241
242 end
243   
244 if password then
245   settings["password"] = password
246 end
247
248 m = mpd_new(settings)
249
250 command = url_decode(os.getenv('QUERY_STRING'))
251
252 if not command or command=="" then
253   command="idle"
254 end
255
256 if command=="play" or command=="pause" or command=="stop" then
257
258   res=mpd_send(m,command)
259
260 elseif command=="previous" or command=="next" then
261
262   res=mpd_send(m,"play")
263   res=mpd_send(m,command)
264
265 elseif command=="idle" then
266
267   m.timeout=30
268   res=mpd_send(m,command)
269
270 elseif command=="vold" then
271
272   status=mpd_send(m,"status")
273   volume=tonumber(status["volume"])
274   res=mpd_send(m,"setvol "..(volume-volstep))
275
276 elseif command=="volu" then
277
278   status=mpd_send(m,"status")
279   volume=tonumber(status["volume"])
280   res=mpd_send(m,"setvol "..(volume+volstep))
281
282 elseif string.starts(command,"fastfwd") then
283
284   cmd=split(command,"|")
285   skip=tonumber(cmd[2])
286   if not skip then
287     skip=15
288   end
289
290   status=mpd_send(m,"status")
291   rec_time=status["time"]
292   song=status["songid"]
293   
294   if song then
295
296     if rec_time then
297       rec_time=split(rec_time,":")
298       cur_time=tonumber(rec_time[1])
299
300       track_time=tonumber(rec_time[2])
301       if track_time then
302         cur_time=cur_time+skip
303         if cur_time>track_time then
304           cur_time=track_time
305         end
306       end  
307       mpd_send(m,"seekid "..song.." "..cur_time)
308
309     else
310
311       mpd_send(m,"play")
312
313     end  
314   
315   end
316
317   res={}
318
319 elseif string.starts(command,"rewind") then
320
321   cmd=split(command,"|")
322   skip=tonumber(cmd[2])
323   if not skip then
324     skip=15
325   end
326
327   status=mpd_send(m,"status")
328   rec_time=status["time"]
329   song=status["songid"]
330   
331   if song then
332
333     if rec_time then
334       rec_time=split(rec_time,":")
335       cur_time=tonumber(rec_time[1])
336       cur_time=cur_time-skip
337       if cur_time<0 then
338         cur_time=0
339       end
340
341       print(song)
342       print(cur_time)
343       mpd_send(m,"seekid "..song.." "..cur_time)
344         
345     else
346
347       mpd_send(m,"play")
348       mpd_send(m,"previous")
349
350     end  
351   
352   end
353
354   res={}
355
356 elseif command=="status" then
357
358   res=mpd_send(m,"status")
359   song=tonumber(res["songid"])
360   if song then 
361     playlist=mpd_send(m,"playlistid "..song,1)
362     pl=process_playlist(playlist)
363     res['current_playing']=pl[1]['title']
364   else  
365     res['current_playing']="---"
366   end
367
368 elseif command=="playlist" then
369
370   playlist=mpd_send(m,"playlistinfo",1)
371   res=process_playlist(playlist)
372
373 elseif command=="repeat" then
374
375   status=mpd_send(m,"status")
376   rep=1-status["repeat"]
377   res=mpd_send(m,"repeat "..rep)
378
379 elseif string.starts(command,"cpl") then
380
381   cmd=split(command,"|")
382   id=cmd[3]
383   command=cmd[2]
384
385   if command=="playitem" then
386     command="playid "..id
387     res=mpd_send(m,command)
388   end
389
390   if command=="clear" then
391     res=mpd_send(m,"clear")
392   end
393
394   if command=="remove" then
395     command="deleteid "..id
396     res=mpd_send(m,command)
397   end
398
399   if command=="moveup" then
400     playlist=mpd_send(m,"playlistinfo ",1)
401     pl=process_playlist(playlist)
402     idx=find_by_id(pl,id)
403     if idx>1 then
404       command="swap "..(idx-1).." "..(idx-2)
405     end  
406     res=mpd_send(m,command)
407   end
408
409   if command=="movedown" then
410     playlist=mpd_send(m,"playlistinfo ",1)
411     pl=process_playlist(playlist)
412     idx=find_by_id(pl,id)
413     if idx<#pl then
414       command="swap "..(idx-1).." "..(idx)
415     end  
416     res=mpd_send(m,command)
417   end
418
419 elseif string.starts(command,"lists") then
420
421   cmd=split(command,"|")
422   command=cmd[2]
423
424   if command=="load" then
425     if not cmd[3] then
426       lists=mpd_send(m,"listplaylists",1)
427       res=process_playlists(lists)
428     else
429       res=mpd_send(m,"load \""..cmd[3].."\"",1)
430     end
431   end
432
433   if command=="save" and cmd[3] then
434     res=mpd_send(m,"save \""..cmd[3].."\"",1)
435   end
436
437   if command=="delete" and cmd[3] then
438     res=mpd_send(m,"rm \""..cmd[3].."\"",1)
439   end
440
441   if command=="edit" then
442     if cmd[3] then
443       dir=mpd_send(m,"lsinfo \""..cmd[3].."\"",1)
444     else
445       dir=mpd_send(m,"lsinfo",1)
446     end
447     res=process_directory(dir)
448   end
449
450   if command=="add" then
451     res={}
452     if cmd[3] then
453       res=mpd_send(m,"add \""..cmd[3].."\"")
454       if (res['errormsg']) then
455         path=url_encode(cmd[3])
456         res=mpd_send(m,"add \""..path.."\"")
457       end
458     end
459   end
460
461 end
462
463 if not res then
464   print("Content-Type: text/plain\r\n")
465   print("MPD server - unknown command "..command)
466 elseif (res['errormsg']) then
467   print("Content-Type: text/plain\r\n")
468   print("MPD server connection error: "..res['errormsg'])
469 else
470   print "Content-Type: text/plain\r\n"
471   print(json.encode(res))
472 end