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 # returns just the "lfm" element of xml (as described in parse_xml) or fires an error
84 # callback(err, array)
85 parse_lfm_xml = (text, callback) ->
86 parse_xml text, (err, parsed) ->
92 if element[0] is 'lfm'
93 callback null, element
95 callback "Couldn't find lfm element in server response"
99 # login and get a session key
101 login = (callback) ->
102 load_auth (err, auth) ->
103 return callback(err) if err?
105 http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=auth.getmobilesession&username=#{auth.user}&authToken=#{auth.token}"}, (res) ->
106 if res.statusCode != 200
107 console.log "login response code: #{res.statusCode}"
108 callback "login response code: #{res.statusCode}"
111 res.setEncoding 'utf8'
113 res.on 'data', (chunk) ->
120 # FIXME switch to an xml parser that will tell me when it's done
123 callback "xml parser failed to do anything with login server response"
124 console.log "xml parser didn't exit: #{body}"
125 parser = new xml.SaxParser (cb) ->
126 cb.onStartElementNS (name, attrs, prefix, uri, namespaces) ->
129 cb.onCharacters (str) ->
131 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
135 auth.sk_date = now_ms
136 console.log("got key \"#{content}\"")
137 callback null, content
138 else if element is 'error'
140 callback "login failed: \"#{content}\""
141 # ignore other tags stuff in there
143 parser.parseString body
147 login_cached = (callback) ->
149 callback null, auth.sk
154 tune = (tag, callback) ->
155 login_cached (err, sk) ->
156 return callback(err) if err?
158 args = "method=radio.tune&station=librefm://#{tag}&sk=#{sk}"
159 headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': args.length }
160 req = http.request { host: 'alpha.libre.fm', port: 80, method: 'POST', path: "/2.0/", headers: headers }, (res) ->
161 if res.statusCode != 200
162 # libre.fm always returns 200, even for authentication failures
163 console.log "radio.tune response code: #{res.statusCode}"
164 callback "radio.tune response code: #{res.statusCode}"
167 res.setEncoding 'utf8'
169 res.on 'data', (chunk) ->
170 response_text += chunk
172 parse_lfm_xml response_text, (err, lfm) ->
175 callback "Error while parsing server reply while tuning into \"#{tag}\" station: #{err}"
177 if lfm[1].status is 'ok'
178 console.log "Tuned to #{tag}"
181 for element in lfm[2]
182 if element[0] is 'error'
183 code = element[1].code
184 if code is 4 # invalid authentication token
186 sk_age = now_ms() - auth.sk_date
188 console.log "Server said our auth token was invalid only #{sk_age}ms after we got it."
189 delay = 30000 - sk_age
190 console.log "Waiting another #{delay}ms before requesting another one."
200 if typeof element[2][0]?[2] is 'string'
201 message = JSON.stringify element[2][0][2]
202 console.log "server response from tune: code #{code} message: #{message}"
203 callback "Error during tune: code #{code} message: #{message}"
205 # looked through all elements, and didn't find one with name "error"
206 callback "Error during tune: server responded without success or error message"
208 req.on 'error', (err) ->
209 console.log "tune post http error: #{err}"
210 callback "tune post http error: #{err}"
215 # FIXME call tune automatically or remove "tag" argument
216 get_playlist = (tag, callback) ->
217 login_cached (err, sk) ->
218 return callback(err) if err?
220 console.log "getting playlist with sk=#{sk}"
221 http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=radio.getPlaylist&sk=#{sk}"}, (res) ->
222 if res.statusCode != 200
223 console.log "login response code: #{res.statusCode}"
224 callback "login response code: #{res.statusCode}"
227 res.setEncoding 'utf8'
229 res.on 'data', (chunk) ->
230 response_text += chunk
232 # while testing, got response_text === "BADSESSION"
233 console.log "server said: #{response_text}"
234 parse_xml response_text, (err, response) ->
235 parse_lfm_xml response_text, (err, lfm) ->
238 callback "Error while parsing server reply while requesting playlist: #{err}"
240 for element in response
241 if element[0] is 'playlist'
242 # FIXME write this bit
243 console.log 'Yay we got a playlist!'
244 console.log JSON.stringify element[0]
245 else if element[0] is 'lfm'
246 if element[1].status isnt 'failed'
247 callback "Server responded to our playlist request with #{JSON.stringify response}"
250 # FIXME search for "error" element instead of assuming it's first
251 code = element[2][0]?.code
252 if code is 4 # invalid authentication token
254 sk_age = now_ms() - auth.sk_date
256 console.log "Server said our auth token was invalid only #{sk_age}ms after we got it."
257 delay = 30000 - sk_age
258 console.log "Waiting another #{delay}ms before requesting another one."
265 get_playlist tag, callback
268 if typeof element[2][0]?[2] is 'string'
269 message = JSON.stringify element[2][0][2]
270 console.log "server response from tune: code #{code} message: #{message}"
271 callback "Error during tune: code #{code} message: #{message}"
273 # looked through all elements, and didn't find one with name "error"
274 callback "Error during tune: server responded without success or error message"
276 test = (tag, callback) ->
291 exports.get_playlist = get_playlist
293 exports.login = login # fixme remove this from the API and call it automatically
294 exports.new_auth_token = new_auth_token
295 exports.save_auth = save_auth