JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
52e6933392a4c3f003cc5550118ed8e61ccac985
[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                         callback null, JSON.parse data
44
45 # elements look like: ['name', {attr: value, attr2: value2,...}, [contents]]
46 # text nodes look like elements with blank names: ['', {}, 'foo']
47 # so <foo bar="baz">qux<quux></quux>corge</foo> -> ['foo', {bar:'baz'}, [['',{},'qux'], ['cuux', {}, []], ['',{},'corge']]]
48 # callback(err, array)
49 parse_xml = (str, callback) ->
50         done = false
51         nests = [[]]
52         huh = ->
53                 # FIXME switch to an xml parser that will tell me when it's done
54                 unless done
55                         done = true
56                         callback "xml parser failed to do anything with login server response"
57                         console.log "xml parser didn't exit: #{str}"
58         parser = new xml.SaxParser (cb) ->
59                 cb.onStartElementNS (name, attr_tuples, prefix, uri, namespaces) ->
60                         attrs = {}
61                         for tuple in attr_tuples
62                                 attrs[tuple[0]] = tuple[1]
63                         element = [name, attrs, []]
64                         nests[0].push element
65                         nests.unshift element[2]
66                 cb.onCharacters (str) ->
67                         if nests[0][nests[0].length - 1]?[0] is ''
68                                 nests[0][nests[0].length - 1][2] += str
69                         else
70                                 nests[0].push ['', {}, str]
71                 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
72                         nests.shift()
73                         if nests.length is 1
74                                 done = true
75                                 callback null, nests[0]
76                 cb.onEndDocument huh
77         parser.parseString str
78         timeout 1, huh
79
80 exports.parse_xml = parse_xml
81
82 # returns just the "lfm" element of xml (as described in parse_xml) or fires an error
83 # callback(err, array)
84 parse_lfm_xml = (text, callback) ->
85         parse_xml text, (err, parsed) ->
86                 if err
87                         # parse error
88                         callback err
89                         return
90                 for element in parsed
91                         if element[0] is 'lfm'
92                                 callback null, element
93                                 return
94                 callback "Couldn't find lfm element in server response"
95                 return
96         return
97
98 # login and get a session key
99 # callback(err, sk)
100 login = (callback) ->
101         load_auth (err, auth) ->
102                 return callback(err) if err?
103
104                 http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=auth.getmobilesession&username=#{auth.user}&authToken=#{auth.token}"}, (res) ->
105                         if res.statusCode != 200
106                                 console.log "login response code: #{res.statusCode}"
107                                 callback "login response code: #{res.statusCode}"
108                                 return
109
110                         res.setEncoding 'utf8'
111                         body = ''
112                         res.on 'data', (chunk) ->
113                                 body += chunk
114                         res.on 'end', ->
115                                 element = ''
116                                 content = ''
117                                 done = false
118                                 huh = ->
119                                         # FIXME switch to an xml parser that will tell me when it's done
120                                         unless done
121                                                 done = true
122                                                 callback "xml parser failed to do anything with login server response"
123                                                 console.log "xml parser didn't exit: #{body}"
124                                 parser = new xml.SaxParser (cb) ->
125                                         cb.onStartElementNS (name, attrs, prefix, uri, namespaces) ->
126                                                 element = name
127                                                 content = ''
128                                         cb.onCharacters (str) ->
129                                                 content += str
130                                         cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
131                                                 if element is 'key'
132                                                         done = true
133                                                         auth.sk = content
134                                                         auth.sk_date = now_ms
135                                                         console.log("got key \"#{content}\"")
136                                                         callback null, content
137                                                 else if element is 'error'
138                                                         done = true
139                                                         callback "login failed: \"#{content}\""
140                                                 # ignore other tags stuff in there
141                                         cb.onEndDocument huh
142                                 parser.parseString body
143                                 timeout 1, huh
144
145 # callback(err, sk)
146 login_cached = (callback) ->
147         if auth.sk
148                 callback null, auth.sk
149         login callback
150
151
152 # callback(err)
153 tune = (tag, callback) ->
154         login_cached (err, sk) ->
155                 return callback(err) if err?
156
157                 args = "method=radio.tune&station=librefm://#{tag}&sk=#{sk}"
158                 headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': args.length }
159                 req = http.request { host: 'alpha.libre.fm', port: 80, method: 'POST', path: "/2.0/", headers: headers }, (res) ->
160                         if res.statusCode != 200
161                                 # libre.fm always returns 200, even for authentication failures
162                                 console.log "radio.tune response code: #{res.statusCode}"
163                                 callback "radio.tune response code: #{res.statusCode}"
164                                 return
165                         else
166                                 res.setEncoding 'utf8'
167                                 response_text = ''
168                                 res.on 'data', (chunk) ->
169                                         response_text += chunk
170                                 res.on 'end', ->
171                                         parse_lfm_xml response_text, (err, lfm) ->
172                                                 if err
173                                                         # parse error
174                                                         callback "Error while parsing server reply while tuning into \"#{tag}\" station: #{err}"
175                                                         return
176                                                 if lfm[1].status is 'ok'
177                                                         console.log "Tuned to #{tag}"
178                                                         callback()
179                                                         return
180                                                 for element in lfm[2]
181                                                         if element[0] is 'error'
182                                                                 code = element[1].code
183                                                                 if code is 4 # invalid authentication token
184                                                                         delay = 0
185                                                                         sk_age = now_ms() - auth.sk_date
186                                                                         if sk_age < 30000
187                                                                                 console.log "Server said our auth token was invalid only #{sk_age}ms after we got it."
188                                                                                 delay = 30000 - sk_age
189                                                                                 console.log "Waiting another #{delay}ms before requesting another one."
190                                                                         # get new auth token
191                                                                         timeout delay, ->
192                                                                                 login (err, sk) ->
193                                                                                         if err
194                                                                                                 callback err
195                                                                                                 return
196                                                                                         tune tag, callback
197                                                                                         return
198                                                                 else
199                                                                         if typeof element[2][0]?[2] is 'string'
200                                                                                 message = JSON.stringify element[2][0][2]
201                                                                         console.log "server response from tune: code #{code} message: #{message}"
202                                                                         callback "Error during tune: code #{code} message: #{message}"
203                                                                 return
204                                                 # looked through all elements, and didn't find one with name "error"
205                                                 callback "Error during tune: server responded without success or error message"
206
207                 req.on 'error', (err) ->
208                         console.log "tune post http error: #{err}"
209                         callback "tune post http error: #{err}"
210
211                 req.write args
212                 req.end()
213
214 exports.tune = tune
215 exports.login = login # fixme remove this from the API and call it automatically
216 exports.new_auth_token = new_auth_token
217 exports.save_auth = save_auth