3 crypto = require 'crypto'
4 xml = require 'node-xml'
6 # return the current time in miliseconds (since the epoch)
8 return new Date().getTime()
10 # soooo annoying that setTimeout takes the ms arg last
11 timeout = (ms, func) -> setTimeout func, ms
16 sum = crypto.createHash 'md5'
18 return sum.digest 'hex'
20 new_auth_token = (user, pass) ->
21 token = md5(user + md5(pass))
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
33 # load login credentials from settings file
34 load_auth = (callback) ->
35 if auth.user and auth.token
39 fs.readFile auth_file, 'utf8', (err, data) ->
43 auth = JSON.parse data
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) ->
54 # FIXME switch to an xml parser that will tell me when it's done
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) ->
62 for tuple in attr_tuples
63 attrs[tuple[0]] = tuple[1]
64 element = [name, attrs, []]
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
71 nests[0].push ['', {}, str]
72 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
76 callback null, nests[0]
78 parser.parseString str
81 exports.parse_xml = parse_xml
83 # pass the js representation (nested arrays, etc)
84 # returns the lfm object or null
85 find_lfm_element = (parsed, callback) ->
87 if element[0] is 'lfm'
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) ->
96 # report this parse error
99 lfm = find_lfm_element parsed
103 callback "Couldn't find lfm element in server response"
106 # login and get a session key
108 login = (callback) ->
109 load_auth (err, auth) ->
110 return callback(err) if err?
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}"
118 res.setEncoding 'utf8'
120 res.on 'data', (chunk) ->
127 # FIXME switch to an xml parser that will tell me when it's done
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) ->
136 cb.onCharacters (str) ->
138 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
142 auth.sk_date = now_ms
143 console.log("got key \"#{content}\"")
144 callback null, content
145 else if element is 'error'
147 callback "login failed: \"#{content}\""
148 # ignore other tags stuff in there
150 parser.parseString body
154 login_cached = (callback) ->
156 callback null, auth.sk
161 tune = (tag, callback) ->
162 login_cached (err, sk) ->
163 return callback(err) if err?
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}"
174 res.setEncoding 'utf8'
176 res.on 'data', (chunk) ->
177 response_text += chunk
179 parse_lfm_xml response_text, (err, lfm) ->
182 callback "Error while parsing server reply while tuning into \"#{tag}\" station: #{err}"
184 if lfm[1].status is 'ok'
185 console.log "Tuned to #{tag}"
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
193 sk_age = now_ms() - auth.sk_date
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."
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}"
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"
215 req.on 'error', (err) ->
216 console.log "tune post http error: #{err}"
217 callback "tune post http error: #{err}"
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?
228 return callback(err) if err?
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}"
236 res.setEncoding 'utf8'
238 res.on 'data', (chunk) ->
239 response_text += chunk
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) ->
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}"
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
261 sk_age = now_ms() - auth.sk_date
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."
272 get_playlist tag, callback
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}"
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"
283 test = (tag, callback) ->
293 exports.get_playlist = get_playlist
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