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 callback null, JSON.parse data
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) ->
53 # FIXME switch to an xml parser that will tell me when it's done
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) ->
61 for tuple in attr_tuples
62 attrs[tuple[0]] = tuple[1]
63 element = [name, attrs, []]
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
70 nests[0].push ['', {}, str]
71 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
75 callback null, nests[0]
77 parser.parseString str
80 exports.parse_xml = parse_xml
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) ->
91 if element[0] is 'lfm'
92 callback null, element
94 callback "Couldn't find lfm element in server response"
98 # login and get a session key
100 login = (callback) ->
101 load_auth (err, auth) ->
102 return callback(err) if err?
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}"
110 res.setEncoding 'utf8'
112 res.on 'data', (chunk) ->
119 # FIXME switch to an xml parser that will tell me when it's done
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) ->
128 cb.onCharacters (str) ->
130 cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
134 auth.sk_date = now_ms
135 console.log("got key \"#{content}\"")
136 callback null, content
137 else if element is 'error'
139 callback "login failed: \"#{content}\""
140 # ignore other tags stuff in there
142 parser.parseString body
146 login_cached = (callback) ->
148 callback null, auth.sk
153 tune = (tag, callback) ->
154 login_cached (err, sk) ->
155 return callback(err) if err?
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}"
166 res.setEncoding 'utf8'
168 res.on 'data', (chunk) ->
169 response_text += chunk
171 parse_lfm_xml response_text, (err, lfm) ->
174 callback "Error while parsing server reply while tuning into \"#{tag}\" station: #{err}"
176 if lfm[1].status is 'ok'
177 console.log "Tuned to #{tag}"
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
185 sk_age = now_ms() - auth.sk_date
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."
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}"
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"
207 req.on 'error', (err) ->
208 console.log "tune post http error: #{err}"
209 callback "tune post http error: #{err}"
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