Using online music services with Ruby

[Or : Streaming Rhapsody or last.fm with Ruby]

Ruby has excellent libraries (HTTP, SSL, SOAP, XML, etc.) that can be used to write your own music player for streaming music from online services like Rhapsody or last.fm – so why not use them? Here are code snippets to help you getting started! They show you…

  • …how to make a HTTPS GET or POST request using SSL (https://…)
  • …how to easily parse the XML response of such a request
  • …how to stream music data (e.g. MP3) from some HTTP URL and how to pipe that stream data to another process (e.g. mplayer) for playing it back while streaming
  • …how to use SOAP requests with custom SSL certificates (https://…)
  • …how to encrypt/decrypt 64-bit DES ECB data

My own Ruby (1.8)-based Rhapsody player has 400 lines of Ruby code, my lasm.fm player has 200 lines of Ruby code  –   Do you think other languages can do better 😉 ?

Using HTTPS POST and SSL (HTTPS GET works similar using the Net::HTTP::Get class)

require 'net/http'
require 'net/https'

dict['api_key'] = @apikey
url = "https://someurl"

uri = URI.parse(url)
sock = Net::HTTP.new(uri.host, uri.port)
sock.use_ssl = true

req = Net::HTTP::Post.new(url)
req.set_form_data(dict)
res = sock.start{|http| http.request(req) }
puts res.body

Parsing XML response data

require 'rexml/document'
res = http_post( {'method'=> 'radio.getPlaylist', 'sk' => @sk })

doc = REXML::Document.new(res)

printf("%-30s %-50s %-40sn", "title", "album", "creator")
doc.elements.each("*/*/*/track") do |element|
  title    = element.elements["title"].text
  album    = element.elements["album"].text
  creator  = element.elements["creator"].text
  location = element.elements["location"].text
  image    = element.elements["image"].text
  printf("%-30s %-50s %-40sn", title, album, creator)
end

Streaming HTTP music data  (e.g. MP3) and piping it to another process (e.g. mplayer)

    uri = URI.parse(url)
    Net::HTTP.start(uri.host) do |http|
      http.request_get(uri.path) do |resp|
        pipe = IO.popen("mplayer -cache 256 -", "w")
        resp.read_body do |segment|
          if segment.length > 0
            pipe.write(segment)
          end
        end
        pipe.close
      end
    end

Of course, you can always pipe using bash..

macbook-pro:~ nero$ lastfm.rb | mplayer -cache 64 -

...however this wouldn't allow you to write something else to STDOUT except the music data.

Using SOAP requests and custom SSL certificates

require 'openssl'
require "rubygems"
gem "httpclient", "2.1.5.2"
gem 'soap4r'
require 'soap/rpc/driver'

driverPlay = SOAP::RPC::Driver.new('https://someurl', 'urn:someurn')
driverPlay.options["protocol.http.ssl_config.verify_mode"] = nil
driverPlay.options["protocol.http.ssl_config.client_cert"] = File.join(@dir, "somefile.cert.pem")
driverPlay.options["protocol.http.ssl_config.client_key"] = File.join(@dir, "somefile.key.pem")
driverPlay.return_response_as_xml = true
# define some method
driverPlay.add_method('startPlaybackSession', 'developerKey', 'cobrandId', 'logon', 'password', 'clientType')

Using 64-bit DES ECB encryption
(the key is to use ‘cipher.padding = 0’  – it took me 2 hours to figure that out …)

  require 'openssl'
  def testDES
    puts '*** TESTDES ***'
    key         = "x01x23x45x67x89xabxcdxef"
    plain       = "x01x23x45x67x89xabxcdxe7"
    cryptdata   = "xc9x57x44x25x6ax5exd3x1d"

    puts "key      = " + key.unpack("H*").to_s
    puts "plain    = " + plain.unpack("H*").to_s
    #puts OpenSSL::Cipher.ciphers

    puts "encrypt..."
    cipher = OpenSSL::Cipher::Cipher.new('des-ecb')
    cipher.encrypt
    cipher.key = key
    cipher.padding = 0
    res = cipher.update(plain)
    puts "res      = " + res.unpack("H*").to_s
    puts "crypted  = " + cryptdata.unpack("H*").to_s

    puts "decrypt..."
    cipher.decrypt
    res = cipher.update(res) + cipher.final
    puts "res      = " + res.unpack("H*").to_s
    puts "plain    = " + plain.unpack("H*").to_s

    puts "triple..."
    cipher.decrypt
    plain = cipher.update(plain) + cipher.final
    cipher.encrypt
    cipher.key = plain
    plain = cipher.update(plain)
    cipher.decrypt
    cipher.key = key
    res = cipher.update(plain) + cipher.final
    puts "res      = " + res.unpack("H*").to_s
  end