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