X-Git-Url: https://jasonwoof.com/gitweb/?p=libre-fm-client-daemon.git;a=blobdiff_plain;f=api.coffee;h=214d82c597a744d40b1a9af4b2f8850ac89a7a87;hp=2ac50c5672aed38f9d6bfcbac2fb7434e6ad37ea;hb=HEAD;hpb=9f281941f885f6fdcf878ce4c8b2cdb0f525c2a9 diff --git a/api.coffee b/api.coffee index 2ac50c5..214d82c 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 @@ -36,9 +40,71 @@ load_auth = (callback) -> if err callback err else - callback null, JSON.parse data + auth = JSON.parse data + callback null, auth + +# 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 + +# pass the js representation (nested arrays, etc) +# returns the lfm object or null +find_lfm_element = (parsed, callback) -> + for element in parsed + if element[0] is 'lfm' + return element + return null + +# 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? + # report this parse error + callback err + return + lfm = find_lfm_element parsed + if lfm? + callback null, lfm + else + callback "Couldn't find lfm element in server response" + return # login and get a session key +# callback(err, sk) login = (callback) -> load_auth (err, auth) -> return callback(err) if err? @@ -73,6 +139,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 +150,148 @@ 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() + +# FIXME call tune automatically or remove "tag" argument +get_playlist = (tag, callback) -> + login_cached (err, sk) -> + return callback(err) if err? + + tune tag, (err)-> + return callback(err) if err? + + console.log "getting playlist with sk=#{sk}" + http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=radio.getPlaylist&sk=#{sk}"}, (res) -> + if res.statusCode != 200 + callback "getPlaylist http response code: #{res.statusCode}" + return + + res.setEncoding 'utf8' + response_text = '' + res.on 'data', (chunk) -> + response_text += chunk + res.on 'end', -> + # while testing, got response_text === "BADSESSION" + console.log "server responded" + # console.log "server said: #{response_text}" + parse_xml response_text, (err, response) -> + if err? + return callback "Error while parsing server reply while requesting playlist: #{err}" + for element in response + if element[0] is 'playlist' + # FIXME write this bit + console.log 'Yay we got a playlist!' + console.log JSON.stringify element[0] + else if element[0] is 'lfm' + if element[1].status isnt 'failed' + callback "Server responded to our playlist request with #{JSON.stringify response}" + return + + # FIXME search for "error" element instead of assuming it's first + code = element[2][0]?.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 + get_playlist 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" + +test = (tag, callback) -> + get_playlist tag, -> + if err? + console.log err + callback err + return + console.log 'yay' + callback() +exports.test = test +exports.get_playlist = get_playlist +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