crypto = require 'crypto'
xml = require 'node-xml'
+# return the current time in miliseconds (since the epoch)
+now_ms = ->
+ return new Date().getTime()
+
# soooo annoying that setTimeout takes the ms arg last
timeout = (ms, func) -> setTimeout func, ms
else
callback null, JSON.parse data
+# elements look like: ['name', {attr: value, attr2: value2,...}, [contents]]
+# text nodes look like elements with blank names: ['', {}, 'foo']
+# so <foo bar="baz">qux<quux></quux>corge</foo> -> ['foo', {bar:'baz'}, [['',{},'qux'], ['cuux', {}, []], ['',{},'corge']]]
+# callback(err, array)
+parse_xml = (str, callback) ->
+ done = false
+ nests = [[]]
+ huh = ->
+ # FIXME switch to an xml parser that will tell me when it's done
+ unless done
+ done = true
+ callback "xml parser failed to do anything with login server response"
+ console.log "xml parser didn't exit: #{str}"
+ parser = new xml.SaxParser (cb) ->
+ cb.onStartElementNS (name, attr_tuples, prefix, uri, namespaces) ->
+ attrs = {}
+ for tuple in attr_tuples
+ attrs[tuple[0]] = tuple[1]
+ element = [name, attrs, []]
+ nests[0].push element
+ nests.unshift element[2]
+ cb.onCharacters (str) ->
+ if nests[0][nests[0].length - 1]?[0] is ''
+ nests[0][nests[0].length - 1][2] += str
+ else
+ nests[0].push ['', {}, str]
+ cb.onEndElementNS (name, attrs, prefix, uri, namespaces) ->
+ nests.shift()
+ if nests.length is 1
+ done = true
+ callback null, nests[0]
+ cb.onEndDocument huh
+ parser.parseString str
+ timeout 1, huh
+
+exports.parse_xml = parse_xml
+
+# returns just the "lfm" element of xml (as described in parse_xml) or fires an error
+# callback(err, array)
+parse_lfm_xml = (text, callback) ->
+ parse_xml text, (err, parsed) ->
+ if err
+ # parse error
+ callback err
+ return
+ for element in parsed
+ if element[0] is 'lfm'
+ callback null, element
+ return
+ callback "Couldn't find lfm element in server response"
+ return
+ return
+
# login and get a session key
+# callback(err, sk)
login = (callback) ->
load_auth (err, auth) ->
return callback(err) if err?
if element is 'key'
done = true
auth.sk = content
+ auth.sk_date = now_ms
console.log("got key \"#{content}\"")
callback null, content
else if element is 'error'
parser.parseString body
timeout 1, huh
+# callback(err, sk)
+login_cached = (callback) ->
+ if auth.sk
+ callback null, auth.sk
+ login callback
+
+
+# callback(err)
+tune = (tag, callback) ->
+ login_cached (err, sk) ->
+ return callback(err) if err?
+
+ args = "method=radio.tune&station=librefm://#{tag}&sk=#{sk}"
+ headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': args.length }
+ req = http.request { host: 'alpha.libre.fm', port: 80, method: 'POST', path: "/2.0/", headers: headers }, (res) ->
+ if res.statusCode != 200
+ # libre.fm always returns 200, even for authentication failures
+ console.log "radio.tune response code: #{res.statusCode}"
+ callback "radio.tune response code: #{res.statusCode}"
+ return
+ else
+ res.setEncoding 'utf8'
+ response_text = ''
+ res.on 'data', (chunk) ->
+ response_text += chunk
+ res.on 'end', ->
+ parse_lfm_xml response_text, (err, lfm) ->
+ if err
+ # parse error
+ callback "Error while parsing server reply while tuning into \"#{tag}\" station: #{err}"
+ return
+ if lfm[1].status is 'ok'
+ console.log "Tuned to #{tag}"
+ callback()
+ return
+ for element in lfm[2]
+ if element[0] is 'error'
+ code = element[1].code
+ if code is 4 # invalid authentication token
+ delay = 0
+ sk_age = now_ms() - auth.sk_date
+ if sk_age < 30000
+ console.log "Server said our auth token was invalid only #{sk_age}ms after we got it."
+ delay = 30000 - sk_age
+ console.log "Waiting another #{delay}ms before requesting another one."
+ # get new auth token
+ timeout delay, ->
+ login (err, sk) ->
+ if err
+ callback err
+ return
+ tune tag, callback
+ return
+ else
+ if typeof element[2][0]?[2] is 'string'
+ message = JSON.stringify element[2][0][2]
+ console.log "server response from tune: code #{code} message: #{message}"
+ callback "Error during tune: code #{code} message: #{message}"
+ return
+ # looked through all elements, and didn't find one with name "error"
+ callback "Error during tune: server responded without success or error message"
+
+ req.on 'error', (err) ->
+ console.log "tune post http error: #{err}"
+ callback "tune post http error: #{err}"
+
+ req.write args
+ req.end()
+exports.tune = tune
exports.login = login # fixme remove this from the API and call it automatically
exports.new_auth_token = new_auth_token
exports.save_auth = save_auth