fs = require 'fs' 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 auth = {} md5 = (str) -> sum = crypto.createHash 'md5' sum.update str return sum.digest 'hex' new_auth_token = (user, pass) -> token = md5(user + md5(pass)) auth.user = user auth.token = token return token auth_file = "#{process.env.HOME}/.libre.fm-cmus.auth" save_auth = (user, pass, callback) -> token = new_auth_token(user, pass) text = JSON.stringify user: user, token: new_auth_token(user, pass) fs.writeFile auth_file, text, 'utf8', callback return token # load login credentials from settings file load_auth = (callback) -> if auth.user and auth.token callback auth return fs.readFile auth_file, 'utf8', (err, data) -> if err callback err else 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? http.get { host: 'alpha.libre.fm', port: 80, path: "/2.0/?method=auth.getmobilesession&username=#{auth.user}&authToken=#{auth.token}"}, (res) -> if res.statusCode != 200 console.log "login response code: #{res.statusCode}" callback "login response code: #{res.statusCode}" return res.setEncoding 'utf8' body = '' res.on 'data', (chunk) -> body += chunk res.on 'end', -> element = '' content = '' done = false 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: #{body}" parser = new xml.SaxParser (cb) -> cb.onStartElementNS (name, attrs, prefix, uri, namespaces) -> element = name content = '' cb.onCharacters (str) -> content += str cb.onEndElementNS (name, attrs, prefix, uri, namespaces) -> 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' done = true callback "login failed: \"#{content}\"" # ignore other tags stuff in there cb.onEndDocument huh 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