From ce4aac238338dab4f774dea2815e28eb50a8e56e Mon Sep 17 00:00:00 2001 From: Jason Woofenden Date: Sun, 16 Oct 2011 00:21:27 -0400 Subject: [PATCH] tuned station --- api.coffee | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/api.coffee b/api.coffee index 2ac50c5..52e6933 100644 --- a/api.coffee +++ b/api.coffee @@ -3,6 +3,10 @@ http = require 'http' 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 @@ -38,7 +42,61 @@ load_auth = (callback) -> 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 quxcorge -> ['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? @@ -73,6 +131,7 @@ login = (callback) -> 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' @@ -83,7 +142,76 @@ login = (callback) -> 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 -- 1.7.10.4