JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
fix get_playlist flow/parsing
[libre-fm-client-daemon.git] / api.coffee
1 fs = require 'fs'
2 http = require 'http'
3 crypto = require 'crypto'
4 xml = require 'node-xml'
5
6 # return the current time in miliseconds (since the epoch)
7 now_ms = ->
8         return new Date().getTime()
9
10 # soooo annoying that setTimeout takes the ms arg last
11 timeout = (ms, func) -> setTimeout func, ms
12
13 auth = {}
14
15 md5 = (str) ->
16         sum = crypto.createHash 'md5'
17         sum.update str
18         return sum.digest 'hex'
19
20 new_auth_token = (user, pass) ->
21         token = md5(user + md5(pass))
22         auth.user = user
23         auth.token = token
24         return token
25
26 auth_file = "#{process.env.HOME}/.libre.fm-cmus.auth"
27 save_auth = (user, pass, callback) ->
28         token = new_auth_token(user, pass)
29         text = JSON.stringify user: user, token: new_auth_token(user, pass)
30         fs.writeFile auth_file, text, 'utf8', callback
31         return token
32
33 # load login credentials from settings file
34 load_auth = (callback) ->
35         if auth.user and auth.token
36                 callback auth
37                 return
38
39         fs.readFile auth_file, 'utf8', (err, data) ->
40                 if err
41                         callback err
42                 else
43                         auth = JSON.parse data
44                         callback null, auth
45
46 # elements look like: ['name', {attr: value, attr2: value2,...}, [contents]]
47 # text nodes look like elements with blank names: ['', {}, 'foo']
48 # so <foo bar="baz">qux<quux></quux>corge</foo> -> ['foo', {bar:'baz'}, [['',{},'qux'], ['cuux', {}, []], ['',{},'corge']]]
49 # callback(err, array)
50 parse_xml = (str, callback) ->
51         done = false
52         nests = [[]]
53         huh = ->
54                 # FIXME switch to an xml parser that will tell me when it's done
55                 unless done
56                         done = true
57                         callback "xml parser failed to do anything with login server response"
58                         console.log "xml parser didn't exit: #{str}"
59         parser = new xml.SaxParser (cb) ->
60                 cb.onStartElementNS (name, attr_tuples, prefix, uri, namespaces) ->
61                         attrs = {}
62                         for tuple in attr_tuples
63                                 attrs[tuple[0]] = tuple[1]
64                         element = [name, attrs, []]
65                         nests[0].push element
66                         nests.unshift element[2]
67                 cb.onCharacters (str) ->
68                         if nests[0][nests[0].length - 1]?[0] is ''
69                                 nests[0][nests[0].length - 1][2] += str
70                         else
71                                 nests[0].push ['', {}, str]
72                 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
73                         nests.shift()
74                         if nests.length is 1
75                                 done = true
76                                 callback null, nests[0]
77                 cb.onEndDocument huh
78         parser.parseString str
79         timeout 1, huh
80
81 exports.parse_xml = parse_xml
82
83 # pass the js representation (nested arrays, etc)
84 # returns the lfm object or null
85 find_lfm_element = (parsed, callback) ->
86         for element in parsed
87                 if element[0] is 'lfm'
88                         return element
89         return null
90
91 # returns just the "lfm" element of xml (as described in parse_xml) or fires an error
92 # callback(err, array)
93 parse_lfm_xml = (text, callback) ->
94         parse_xml text, (err, parsed) ->
95                 if err?
96                         # report this parse error
97                         callback err
98                         return
99                 lfm = find_lfm_element parsed
100                 if lfm?
101                         callback null, lfm
102                 else
103                         callback "Couldn't find lfm element in server response"
104         return
105
106 # login and get a session key
107 # callback(err, sk)
108 login = (callback) ->
109         load_auth (err, auth) ->
110                 return callback(err) if err?
111
112                 http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=auth.getmobilesession&username=#{auth.user}&authToken=#{auth.token}"}, (res) ->
113                         if res.statusCode != 200
114                                 console.log "login response code: #{res.statusCode}"
115                                 callback "login response code: #{res.statusCode}"
116                                 return
117
118                         res.setEncoding 'utf8'
119                         body = ''
120                         res.on 'data', (chunk) ->
121                                 body += chunk
122                         res.on 'end', ->
123                                 element = ''
124                                 content = ''
125                                 done = false
126                                 huh = ->
127                                         # FIXME switch to an xml parser that will tell me when it's done
128                                         unless done
129                                                 done = true
130                                                 callback "xml parser failed to do anything with login server response"
131                                                 console.log "xml parser didn't exit: #{body}"
132                                 parser = new xml.SaxParser (cb) ->
133                                         cb.onStartElementNS (name, attrs, prefix, uri, namespaces) ->
134                                                 element = name
135                                                 content = ''
136                                         cb.onCharacters (str) ->
137                                                 content += str
138                                         cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
139                                                 if element is 'key'
140                                                         done = true
141                                                         auth.sk = content
142                                                         auth.sk_date = now_ms
143                                                         console.log("got key \"#{content}\"")
144                                                         callback null, content
145                                                 else if element is 'error'
146                                                         done = true
147                                                         callback "login failed: \"#{content}\""
148                                                 # ignore other tags stuff in there
149                                         cb.onEndDocument huh
150                                 parser.parseString body
151                                 timeout 1, huh
152
153 # callback(err, sk)
154 login_cached = (callback) ->
155         if auth.sk
156                 callback null, auth.sk
157         login callback
158
159
160 # callback(err)
161 tune = (tag, callback) ->
162         login_cached (err, sk) ->
163                 return callback(err) if err?
164
165                 args = "method=radio.tune&station=librefm://#{tag}&sk=#{sk}"
166                 headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': args.length }
167                 req = http.request { host: 'alpha.libre.fm', port: 80, method: 'POST', path: "/2.0/", headers: headers }, (res) ->
168                         if res.statusCode != 200
169                                 # libre.fm always returns 200, even for authentication failures
170                                 console.log "radio.tune response code: #{res.statusCode}"
171                                 callback "radio.tune response code: #{res.statusCode}"
172                                 return
173                         else
174                                 res.setEncoding 'utf8'
175                                 response_text = ''
176                                 res.on 'data', (chunk) ->
177                                         response_text += chunk
178                                 res.on 'end', ->
179                                         parse_lfm_xml response_text, (err, lfm) ->
180                                                 if err
181                                                         # parse error
182                                                         callback "Error while parsing server reply while tuning into \"#{tag}\" station: #{err}"
183                                                         return
184                                                 if lfm[1].status is 'ok'
185                                                         console.log "Tuned to #{tag}"
186                                                         callback()
187                                                         return
188                                                 for element in lfm[2]
189                                                         if element[0] is 'error'
190                                                                 code = element[1].code
191                                                                 if code is 4 # invalid authentication token
192                                                                         delay = 0
193                                                                         sk_age = now_ms() - auth.sk_date
194                                                                         if sk_age < 30000
195                                                                                 console.log "Server said our auth token was invalid only #{sk_age}ms after we got it."
196                                                                                 delay = 30000 - sk_age
197                                                                                 console.log "Waiting another #{delay}ms before requesting another one."
198                                                                         # get new auth token
199                                                                         timeout delay, ->
200                                                                                 login (err, sk) ->
201                                                                                         if err
202                                                                                                 callback err
203                                                                                                 return
204                                                                                         tune tag, callback
205                                                                                         return
206                                                                 else
207                                                                         if typeof element[2][0]?[2] is 'string'
208                                                                                 message = JSON.stringify element[2][0][2]
209                                                                         console.log "server response from tune: code #{code} message: #{message}"
210                                                                         callback "Error during tune: code #{code} message: #{message}"
211                                                                 return
212                                                 # looked through all elements, and didn't find one with name "error"
213                                                 callback "Error during tune: server responded without success or error message"
214
215                 req.on 'error', (err) ->
216                         console.log "tune post http error: #{err}"
217                         callback "tune post http error: #{err}"
218
219                 req.write args
220                 req.end()
221
222 # FIXME call tune automatically or remove "tag" argument
223 get_playlist = (tag, callback) ->
224         login_cached (err, sk) ->
225                 return callback(err) if err?
226
227                 tune tag, (err)->
228                         return callback(err) if err?
229
230                         console.log "getting playlist with sk=#{sk}"
231                         http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=radio.getPlaylist&sk=#{sk}"}, (res) ->
232                                 if res.statusCode != 200
233                                         callback "getPlaylist http response code: #{res.statusCode}"
234                                         return
235
236                                 res.setEncoding 'utf8'
237                                 response_text = ''
238                                 res.on 'data', (chunk) ->
239                                         response_text += chunk
240                                 res.on 'end', ->
241                                         # while testing, got response_text === "BADSESSION"
242                                         console.log "server responded"
243                                         # console.log "server said: #{response_text}"
244                                         parse_xml response_text, (err, response) ->
245                                                 if err?
246                                                         return callback "Error while parsing server reply while requesting playlist: #{err}"
247                                                 for element in response
248                                                         if element[0] is 'playlist'
249                                                                 # FIXME write this bit
250                                                                 console.log 'Yay we got a playlist!'
251                                                                 console.log JSON.stringify element[0]
252                                                         else if element[0] is 'lfm'
253                                                                 if element[1].status isnt 'failed'
254                                                                         callback "Server responded to our playlist request with #{JSON.stringify response}"
255                                                                         return
256
257                                                                 # FIXME search for "error" element instead of assuming it's first
258                                                                 code = element[2][0]?.code
259                                                                 if code is 4 # invalid authentication token
260                                                                         delay = 0
261                                                                         sk_age = now_ms() - auth.sk_date
262                                                                         if sk_age < 30000
263                                                                                 console.log "Server said our auth token was invalid only #{sk_age}ms after we got it."
264                                                                                 delay = 30000 - sk_age
265                                                                                 console.log "Waiting another #{delay}ms before requesting another one."
266                                                                         # get new auth token
267                                                                         timeout delay, ->
268                                                                                 login (err, sk) ->
269                                                                                         if err
270                                                                                                 callback err
271                                                                                                 return
272                                                                                         get_playlist tag, callback
273                                                                                         return
274                                                                 else
275                                                                         if typeof element[2][0]?[2] is 'string'
276                                                                                 message = JSON.stringify element[2][0][2]
277                                                                         console.log "server response from tune: code #{code} message: #{message}"
278                                                                         callback "Error during tune: code #{code} message: #{message}"
279                                                                 return
280                                                 # looked through all elements, and didn't find one with name "error"
281                                                 callback "Error during tune: server responded without success or error message"
282
283 test = (tag, callback) ->
284         get_playlist tag, ->
285                 if err?
286                         console.log err
287                         callback err
288                         return
289                 console.log 'yay'
290                 callback()
291
292 exports.test = test
293 exports.get_playlist = get_playlist
294 exports.tune = tune
295 exports.login = login # fixme remove this from the API and call it automatically
296 exports.new_auth_token = new_auth_token
297 exports.save_auth = save_auth