Skip to content
Browse files

initial commit

  • Loading branch information...
0 parents commit d6512c84ebdb900e36f3bde41678a55d6a29b1f1 @jnewland committed Jun 28, 2012
Showing with 419 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +8 −0 Gemfile
  3. +39 −0 Gemfile.lock
  4. +1 −0 Procfile
  5. +23 −0 README.md
  6. +101 −0 lib/nowplaying.rb
  7. +160 −0 lib/om.rb
  8. +82 −0 lib/rdio.rb
5 .gitignore
@@ -0,0 +1,5 @@
+bin/
+log
+.bundle
+vendor/gems
+.env
8 Gemfile
@@ -0,0 +1,8 @@
+source :rubygems
+
+gem 'sinatra'
+
+group :development do
+ gem 'heroku'
+ gem 'foreman'
+end
39 Gemfile.lock
@@ -0,0 +1,39 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ addressable (2.2.8)
+ excon (0.14.2)
+ foreman (0.47.0)
+ thor (>= 0.13.6)
+ heroku (2.28.7)
+ heroku-api (~> 0.2.6)
+ launchy (>= 0.3.2)
+ netrc (~> 0.7.5)
+ rest-client (~> 1.6.1)
+ rubyzip
+ heroku-api (0.2.6)
+ excon (~> 0.14.0)
+ launchy (2.1.0)
+ addressable (~> 2.2.6)
+ mime-types (1.19)
+ netrc (0.7.5)
+ rack (1.4.1)
+ rack-protection (1.2.0)
+ rack
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ rubyzip (0.9.9)
+ sinatra (1.3.2)
+ rack (~> 1.3, >= 1.3.6)
+ rack-protection (~> 1.2)
+ tilt (~> 1.3, >= 1.3.3)
+ thor (0.15.3)
+ tilt (1.3.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ foreman
+ heroku
+ sinatra
1 Procfile
@@ -0,0 +1 @@
+web: bundle exec ruby lib/nowplaying.rb
23 README.md
@@ -0,0 +1,23 @@
+## Running on Heroku
+
+Clone this repo.
+
+In the clone:
+
+ heroku create --stack cedar jnewland-rdio-nowplaying
+
+Set the following config at heroku:
+
+ heroku config:add RDIO_CONSUMER_KEY=foo
+ heroku config:add RDIO_CONSUMER_SECRET=foo@foo.com
+ heroku config:add POLL_INTERVAL=10
+
+Ship it:
+
+ git push heroku master
+
+Fire up a web process:
+
+ heroku scale web=1
+
+Hit up [papertrail](https://papertrailapp.com/events) and check on the logs.
101 lib/nowplaying.rb
@@ -0,0 +1,101 @@
+#!/usr/bin/env ruby
+
+# (c) 2011 Rdio Inc
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+require 'sinatra'
+require 'uri'
+$LOAD_PATH << './lib'
+require 'rdio'
+
+RDIO_CONSUMER_KEY = ENV['RDIO_CONSUMER_KEY']
+RDIO_CONSUMER_SECRET = ENV['RDIO_CONSUMER_SECRET']
+
+enable :sessions
+
+get '/' do
+ access_token = session[:at]
+ access_token_secret = session[:ats]
+ if access_token and access_token_secret
+ rdio = Rdio.new([RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET],
+ [access_token, access_token_secret])
+
+ user_key = rdio.call('currentUser')['result']['key']
+ play_data = rdio.call('get', { :keys => user_key, :extras => 'lastSongPlayed,lastSongPlayTime'})['result'][user_key]
+
+ play_time = play_data['lastSongPlayTime']
+ song_key = play_data['lastSongPlayed']['key']
+
+ song = rdio.call('get', { :keys => song_key, :extras => 'bigIcon'})['result'][song_key]
+
+ response = "
+ <html><head><title>Now Playing</title></head><body>
+ <img src='%s' />
+ <h1>%s</h1>
+ <h2>%s</h2>
+ <h3>%s</h3>
+ " % [song['bigIcon'], song['name'], song['album'], song['artist']]
+ response += '</body></html>'
+ return response
+ else
+ redirect to('/login')
+ end
+end
+
+get '/login' do
+ session.clear
+ # begin the authentication process
+ rdio = Rdio.new([RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET])
+ callback_url = (URI.join request.url, '/callback').to_s
+ url = rdio.begin_authentication(callback_url)
+ # save our request token in the session
+ session[:rt] = rdio.token[0]
+ session[:rts] = rdio.token[1]
+ # go to Rdio to authenticate the app
+ redirect url
+end
+
+get '/callback' do
+ # get the state from cookies and the query string
+ request_token = session[:rt]
+ request_token_secret = session[:rts]
+ verifier = params[:oauth_verifier]
+ # make sure we have everything we need
+ if request_token and request_token_secret and verifier
+ # exchange the verifier and request token for an access token
+ rdio = Rdio.new([RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET],
+ [request_token, request_token_secret])
+ rdio.complete_authentication(verifier)
+ # save the access token in cookies (and discard the request token)
+ session[:at] = rdio.token[0]
+ session[:ats] = rdio.token[1]
+ session.delete(:rt)
+ session.delete(:rts)
+ # go to the home page
+ redirect to('/')
+ else
+ # we're missing something important
+ redirect to('/logout')
+ end
+end
+
+get '/logout' do
+ session.clear
+ redirect to('/')
+end
160 lib/om.rb
@@ -0,0 +1,160 @@
+# om is oauth-mini - a simple implementation of a useful subset of OAuth.
+# It's designed to be useful and reusable but not general purpose.
+#
+# (c) 2011 Rdio Inc
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# A simple OAuth client implementation. Do less better.
+# Here are the restrictions:
+# - only HMAC-SHA1 is supported
+# - only WWW-Authentiate form signatures are generated
+#
+# To sign a request:
+# auth = om([consumer_key,consumer_secret], url, params)
+# send Authorization: <auth>
+# when POSTing <params> to <url>
+# Optional additional arguments are:
+# token = [oauth_token, oauth_token_secret]
+# method = "POST"
+# realm = "Realm-for-authorization-header"
+
+require 'uri'
+require 'cgi'
+require 'digest'
+require 'digest/sha1'
+
+if not "".respond_to?(:encoding)
+ # ruby 1.8 doesn't know about unicode :(
+ require 'iconv'
+ # we will just check that bytes are valid UTF-8
+ $__om_utf8_checker = Iconv.new("UTF-8", "UTF-8")
+end
+
+def om(consumer, url, post_params, token=nil, method='POST', realm=nil, timestamp=nil, nonce=nil)
+ # A one-shot simple OAuth signature generator
+
+ # the method must be upper-case
+ method = method.upcase
+
+ # we want params as an Array of name / value pairs
+ if post_params.is_a?(Array)
+ params = post_params
+ else
+ params = post_params.collect { |x| x }
+ end
+
+ # we want those pairs to be strings
+ params = params.collect { |k,v| [k.to_s, v.to_s]}
+
+ # normalize the URL
+ url = URI.parse(url)
+ # scheme is lower-case
+ url.scheme = url.scheme.downcase
+ # remove username & password
+ url.user = url.password = nil
+ # host is lowercase
+ url.host = url.host.downcase
+
+ # add URL params to the params
+ if url.query
+ CGI.parse(url.query).each { |k,vs| vs.each { |v| params.push([k,v]) } }
+ end
+
+ # remove the params and fragment
+ url.query = nil
+ url.fragment = nil
+
+ # add OAuth params
+ params = params + [
+ ['oauth_version', '1.0'],
+ ['oauth_timestamp', timestamp || Time.now.to_i.to_s],
+ ['oauth_nonce', nonce || rand(1000000).to_s],
+ ['oauth_signature_method', 'HMAC-SHA1'],
+ ['oauth_consumer_key', consumer[0]],
+ ]
+
+ # the consumer secret is the first half of the HMAC-SHA1 key
+ hmac_key = consumer[1] + '&'
+
+ if token != nil
+ # include a token in params
+ params.push ['oauth_token', token[0]]
+ # and the token secret in the HMAC-SHA1 key
+ hmac_key += token[1]
+ end
+
+ def percent_encode(s)
+ if s.respond_to?(:encoding)
+ # Ruby 1.9 knows about encodings, convert the string to UTF-8
+ s = s.encode(Encoding::UTF_8)
+ else
+ # Ruby 1.8 does not, just check that it's valid UTF-8
+ begin
+ $__om_utf8_checker.iconv(s)
+ rescue Iconv::IllegalSequence => exception
+ throw ArgumentError.new("Non-UTF-8 string: "+s.inspect)
+ end
+ end
+ chars = s.bytes.map do |b|
+ c = b.chr
+ if ((c >= '0' and c <= '9') or
+ (c >= 'A' and c <= 'Z') or
+ (c >= 'a' and c <= 'z') or
+ c == '-' or c == '.' or c == '_' or c == '~')
+ c
+ else
+ '%%%02X' % b
+ end
+ end
+ chars.join
+ end
+
+ # Sort lexicographically, first after key, then after value.
+ params.sort!
+ # escape the key/value pairs and combine them into a string
+ normalized_params = (params.collect {|p| percent_encode(p[0])+'='+percent_encode(p[1])}).join '&'
+
+ # build the signature base string
+ signature_base_string = (percent_encode(method) +
+ '&' + percent_encode(url.to_s) +
+ '&' + percent_encode(normalized_params))
+
+ # HMAC-SHA1
+ hmac = Digest::HMAC.new(hmac_key, Digest::SHA1)
+ hmac.update(signature_base_string)
+
+ # Calculate the digest base 64. Drop the trailing \n
+ oauth_signature = [hmac.digest].pack('m0').strip
+
+ # Build the Authorization header
+ if realm
+ authorization_params = [['realm', realm]]
+ else
+ authorization_params = []
+ end
+ authorization_params.push(['oauth_signature', oauth_signature])
+
+ # we only want certain params in the auth header
+ oauth_params = ['oauth_version', 'oauth_timestamp', 'oauth_nonce',
+ 'oauth_signature_method', 'oauth_signature',
+ 'oauth_consumer_key', 'oauth_token']
+ authorization_params.concat(params.select { |param| nil != oauth_params.index(param[0]) })
+
+ return 'OAuth ' + (authorization_params.collect {|param| '%s="%s"' % param}).join(', ')
+end
82 lib/rdio.rb
@@ -0,0 +1,82 @@
+# (c) 2011 Rdio Inc
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+require 'om'
+require 'uri'
+require 'net/http'
+require 'cgi'
+require 'json'
+
+class Rdio
+ # the consumer and token can be accessed
+ attr_accessor :consumer, :token
+
+ def initialize(consumer, token=nil)
+ @consumer = consumer
+ @token = token
+ end
+
+ def begin_authentication(callback_url)
+ # request a request token from the server
+ response = signed_post('http://api.rdio.com/oauth/request_token',
+ {'oauth_callback' => callback_url})
+ # parse the response
+ parsed = CGI.parse(response)
+ # save the token
+ @token = [parsed['oauth_token'][0], parsed['oauth_token_secret'][0]]
+ # return an URL that the user can use to authorize this application
+ return parsed['login_url'][0] + '?oauth_token=' + parsed['oauth_token'][0]
+ end
+
+ def complete_authentication(verifier)
+ # request an access token
+ response = signed_post('http://api.rdio.com/oauth/access_token',
+ {'oauth_verifier' => verifier})
+ # parse the response
+ parsed = CGI.parse(response)
+ # save the token
+ @token = [parsed['oauth_token'][0], parsed['oauth_token_secret'][0]]
+ end
+
+ def call(method, params={})
+ # make a copy of the dict
+ params = params.clone
+ # put the method in the dict
+ params['method'] = method
+ # call to the server and parse the response
+ return JSON.load(signed_post('http://api.rdio.com/1/', params))
+ end
+
+ private
+
+ def signed_post(url, params)
+ auth = om(@consumer, url, params, @token)
+ url = URI.parse(url)
+ http = Net::HTTP.new(url.host, url.port)
+ req = Net::HTTP::Post.new(url.path, {'Authorization' => auth})
+ req.set_form_data(params)
+ res = http.request(req)
+ return res.body
+ end
+
+ def method_missing(method, *params)
+ call(method.to_s, params[0])
+ end
+
+end

0 comments on commit d6512c8

Please sign in to comment.
Something went wrong with that request. Please try again.