JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
fix get_playlist flow/parsing
[libre-fm-client-daemon.git] / api.coffee
index 2ac50c5..214d82c 100644 (file)
@@ -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 <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
+
+# 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