0200a8a37cfa92c3e6ccb314a7410048734d77fe
[mpd-lua.git] / mpd.lua
1 #!/usr/bin/lua
2
3 function url_decode(str)
4   if not str then return nil end
5   str = string.gsub (str, "+", " ")
6   str = string.gsub (str, "%%(%x%x)", function(h) return
7     string.char(tonumber(h,16)) end)
8   str = string.gsub (str, "\r\n", "\n")
9   return str
10 end
11
12 function split(s, delimiter)
13     local result = {};
14     for match in (s..delimiter):gmatch("(.-)"..delimiter) do
15         result[#result+1] = match;
16     end
17     return result;
18 end
19
20 function string.starts(String,Start)
21    return string.sub(String,1,string.len(Start))==Start
22 end
23
24 function process_playlist(playlist)
25   local res={}
26   for _,record in pairs(playlist) do
27     local splitted = split(record,": ")
28     local name = splitted[2]
29     local splitted = split(splitted[1],":")
30     local id = splitted[1]
31     local rectype = splitted[2]
32     local rec = {}
33     if not (id == "OK") then
34       rec["id"] = id
35       rec["type"] = rectype
36       rec["name"] = name
37       res[id] = rec
38     end
39   end
40   return res
41 end
42
43 function process_playlists(playlists)
44   local res={}
45   for _,record in pairs(playlists) do
46     local splitted = split(record,": ")
47     if splitted[1]=="playlist" then
48       res[#res+1] = splitted[2]
49     end
50   end
51   return res
52 end
53
54 function process_directory(directory)
55   local res={}
56   for _,record in pairs(directory) do
57     local splitted = split(record,": ")
58     if splitted[1]=="directory" or splitted[1]=="file" then
59       local rec={}
60       rec["type"] = splitted[1]
61       rec["name"] = splitted[2]
62       res[#res+1] = rec
63     end
64   end
65   return res
66 end
67
68 require "uci"
69 require("socket")
70
71 function mpd_new(settings)
72     local client = {}
73     if settings == nil then settings = {} end
74
75     client.hostname = settings.hostname or "localhost"
76     client.port     = settings.port or 6600
77     client.desc     = settings.desc or client.hostname
78     client.password = settings.password
79     client.timeout  = settings.timeout or 1
80     client.retry    = settings.retry or 60
81
82     return client
83 end
84
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
164 json=require("json")
165
166 x = uci.cursor()
167 settings = {}
168 settings['host'] = x.get("mpd","server","host") or "localhost"
169 settings['port'] = x.get("mpd","server","port") or 6600
170 password = x.get("mpd","server","password")
171 if password then
172   settings["password"] = password
173 end
174
175 m = mpd_new(settings)
176
177 command = url_decode(os.getenv('QUERY_STRING'))
178
179 if not command or command=="" then
180   command="status"
181 end
182
183 if command=="play" or command=="pause" or command=="stop" or command=="previous" or command=="next" then
184   res=mpd_send(m,command)
185 elseif command=="vold" then
186   status=mpd_send(m,"status")
187   volume=tonumber(status["volume"])
188   res=mpd_send(m,"setvol "..(volume-3))
189 elseif command=="volu" then
190   status=mpd_send(m,"status")
191   volume=tonumber(status["volume"])
192   res=mpd_send(m,"setvol "..(volume+3))
193 elseif command=="status" then
194   res=mpd_send(m,"status")
195   song=res["song"]
196   playlist=mpd_send(m,"playlist",1)
197   pl=process_playlist(playlist)
198   if song then 
199     res['current_playing']=pl[song]['name']
200   else
201     res['current_playing']="No songs selected"
202   end
203 elseif command=="playlist" then
204   playlist=mpd_send(m,"playlist",1)
205   res=process_playlist(playlist)
206 elseif command=="repeat" then
207   status=mpd_send(m,"status")
208   rep=1-status["repeat"]
209   res=mpd_send(m,"repeat "..rep)
210 elseif string.starts(command,"cpl") then
211   cmd=split(command,"|")
212   id=cmd[3]
213   command=cmd[2]
214   if command=="playitem" then
215     command="play "..id
216     res=mpd_send(m,command)
217   end
218   if command=="clear" then
219     res=mpd_send(m,"clear")
220   end
221   if command=="remove" then
222     command="delete "..id
223     res=mpd_send(m,command)
224   end
225   if command=="moveup" then
226     command="swap "..id.." "..(id-1)
227     res=mpd_send(m,command)
228   end
229   if command=="movedown" then
230     command="swap "..id.." "..(id+1)
231     res=mpd_send(m,command)
232   end
233 elseif string.starts(command,"lists") then
234   cmd=split(command,"|")
235   command=cmd[2]
236   if command=="load" then
237     if not cmd[3] then
238       lists=mpd_send(m,"listplaylists",1)
239       res=process_playlists(lists)
240     else
241       res=mpd_send(m,"load "..cmd[3],1)
242     end
243   end
244   if command=="save" and cmd[3] then
245     res=mpd_send(m,"save "..cmd[3],1)
246   end
247   if command=="delete" and cmd[3] then
248     res=mpd_send(m,"rm "..cmd[3],1)
249   end
250   if command=="edit" then
251     if cmd[3] then
252       dir=mpd_send(m,"lsinfo \""..cmd[3].."\"",1)
253     else
254       dir=mpd_send(m,"lsinfo",1)
255     end
256     res=process_directory(dir)
257   end
258   if command=="add" then
259     res={}
260     if cmd[3] then
261       res=mpd_send(m,"add \""..cmd[3].."\"")
262     end
263   end
264 end
265
266 if not res then
267   print("Content-Type: text/plain\r\n")
268   print("MPD server - unknown command "..command)
269 elseif (res['error_msg']) then
270   print("Content-Type: text/plain\r\n")
271   print("MPD server connection error: "..res['error_msg'])
272 else
273   print "Content-Type: text/plain\r\n"
274   print(json.encode(res))
275 end