soundcloud 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,155 +1,170 @@
1
- # Soundcloud API Wrapper
2
- ## Description
3
- The Soundcloud gem is a thin wrapper for the Soundcloud API based off of the httparty gem.
4
- It provides simple methods to handle authorization and to execute HTTP calls.
1
+ # SoundCloud API Wrapper
2
+
3
+ [![Build Status](https://travis-ci.org/soundcloud/soundcloud-ruby.png?branch=master)](https://travis-ci.org/soundcloud/soundcloud-ruby)
5
4
 
6
- ## Requirements
7
- * httmultiparty
8
- * httparty
9
- * crack
10
- * multipart-upload
11
- * hashie
5
+ ## Description
6
+ The official SoundCloud API wrapper. It provides simple methods to handle
7
+ authorization and to execute HTTP calls.
12
8
 
13
9
  ## Installation
14
- gem install soundcloud
10
+ ```sh
11
+ gem install soundcloud
12
+ ```
15
13
 
16
14
  ## Examples
17
15
  #### Print links of the 10 hottest tracks
18
- # register a client with YOUR_CLIENT_ID as client_id_
19
- client = Soundcloud.new(:client_id => YOUR_CLIENT_ID)
20
- # get 10 hottest tracks
21
- tracks = client.get('/tracks', :limit => 10, :order => 'hotness')
22
- # print each link
23
- tracks.each do |track|
24
- puts track.permalink_url
25
- end
26
-
16
+ ```ruby
17
+ # register a client with YOUR_CLIENT_ID as client_id_
18
+ client = SoundCloud.new(:client_id => YOUR_CLIENT_ID)
19
+ # get 10 hottest tracks
20
+ tracks = client.get('/tracks', :limit => 10, :order => 'hotness')
21
+ # print each link
22
+ tracks.each do |track|
23
+ puts track.permalink_url
24
+ end
25
+ ```
26
+
27
27
  #### OAuth2 user credentials flow and print the username of the authenticated user
28
- # register a new client, which will exchange the username, password for an access_token
29
- # NOTE: the SoundCloud API Docs advise not to use the user credentials flow in a web app.
30
- # In any case, never store the password of a user.
31
- client = Soundcloud.new({
32
- :client_id => YOUR_CLIENT_ID,
33
- :client_secret => YOUR_CLIENT_SECRET,
34
- :username => 'some@email.com',
35
- :password => 'userpass'
36
- })
37
-
38
- # print logged in username
39
- puts client.get('/me').username
28
+ ```ruby
29
+ # register a new client, which will exchange the username, password for an access_token
30
+ # NOTE: the SoundCloud API Docs advise not to use the user credentials flow in a web app.
31
+ # In any case, never store the password of a user.
32
+ client = SoundCloud.new({
33
+ :client_id => YOUR_CLIENT_ID,
34
+ :client_secret => YOUR_CLIENT_SECRET,
35
+ :username => 'some@email.com',
36
+ :password => 'userpass'
37
+ })
38
+
39
+ # print logged in username
40
+ puts client.get('/me').username
41
+ ```
40
42
 
41
43
  #### OAuth2 authorization code flow
42
- client = Soundcloud.new({
43
- :client_id => YOUR_CLIENT_ID,
44
- :client_secret => YOUR_CLIENT_SECRET,
45
- :redirect_uri => YOUR_REDIRECT_URI,
46
- })
47
-
48
- redirect client.authorize_url()
49
- # the user should be redirected to "https://soundcloud.com/connect?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI"
50
- # after granting access he will be redirected back to YOUR_REDIRECT_URI
51
- # in your respective handler you can build an exchange token from the transmitted code
52
- client.exchange_token(:code => params[:code])
44
+ ```ruby
45
+ client = SoundCloud.new({
46
+ :client_id => YOUR_CLIENT_ID,
47
+ :client_secret => YOUR_CLIENT_SECRET,
48
+ :redirect_uri => YOUR_REDIRECT_URI,
49
+ })
50
+
51
+ redirect client.authorize_url()
52
+ # the user should be redirected to "https://soundcloud.com/connect?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI"
53
+ # after granting access he will be redirected back to YOUR_REDIRECT_URI
54
+ # in your respective handler you can build an exchange token from the transmitted code
55
+ client.exchange_token(:code => params[:code])
56
+ ```
53
57
 
54
58
  #### OAuth2 refresh token flow, upload a track and print its link
55
- # register a new client which will exchange an existing refresh_token for an access_token
56
- client = Soundcloud.new({
57
- :client_id => YOUR_CLIENT_ID,
58
- :client_secret => YOUR_CLIENT_SECRET,
59
- :refresh_token => SOME_REFRESH_TOKEN
60
- })
61
-
62
- # upload a new track with track.mp3 as audio and image.jpg as artwork
63
- track = client.post('/tracks', :track => {
64
- :title => 'a new track',
65
- :asset_data => File.new('audio.mp3')
66
- })
67
-
68
- # print new tracks link
69
- puts track.permalink_url
59
+ ```ruby
60
+ # register a new client which will exchange an existing refresh_token for an access_token
61
+ client = SoundCloud.new({
62
+ :client_id => YOUR_CLIENT_ID,
63
+ :client_secret => YOUR_CLIENT_SECRET,
64
+ :refresh_token => SOME_REFRESH_TOKEN
65
+ })
66
+
67
+ # upload a new track with track.mp3 as audio and image.jpg as artwork
68
+ track = client.post('/tracks', :track => {
69
+ :title => 'a new track',
70
+ :asset_data => File.new('audio.mp3')
71
+ })
72
+
73
+ # print new tracks link
74
+ puts track.permalink_url
75
+ ```
70
76
 
71
77
  #### Resolve a track url and print its id
72
- # register the client
73
- client = Soundcloud.new(:client_id => YOUR_CLIENT_ID)
74
-
75
- # call the resolve endpoint with a track url
76
- track = client.get('/resolve', :url => "http://soundcloud.com/forss/flickermood")
77
-
78
- # print the track id
79
- puts track.id
80
-
81
- #### Register a client for http://sandbox-soundcloud.com with an existing access_token and start following a user
82
- # register a client for http://sandbox-soundcloud.com with existing access_token
83
- client = Soundcloud.new(:site => 'sandbox-soundcloud.com', :access_token => SOME_ACCESS_TOKEN)
84
-
85
- # create a new following
86
- user_id_to_follow = 123
87
- client.put("/me/followings/#{user_id_to_follow}")
78
+ ```ruby
79
+ # register the client
80
+ client = SoundCloud.new(:client_id => YOUR_CLIENT_ID)
81
+
82
+ # call the resolve endpoint with a track url
83
+ track = client.get('/resolve', :url => "http://soundcloud.com/forss/flickermood")
84
+
85
+ # print the track id
86
+ puts track.id
87
+ ```
88
88
 
89
89
  ### Initializing a client with an access token and updating the users profile description
90
- # initializing a client with an access token
91
- client = Soundcloud.new(:access_token => SOME_ACCESS_TOKEN)
92
-
93
- # updating the users profile description
94
- client.put("/me", :user => {:description => "a new description"})
95
-
96
-
97
- ### Add a track to a playlist / set
98
- client = Soundcloud.new(:access_token => "A_VALID_TOKEN")
99
-
100
- # get my last playlist
101
- playlist = client.get("/me/playlists").first
102
-
103
- # get ids of contained tracks
104
- track_ids = playlist.tracks.map(&:id) # => [22448500, 21928809]
105
-
106
- # adding a new track 21778201
107
- track_ids << 21778201 # => [22448500, 21928809, 21778201]
108
-
109
- # map array of ids to array of track objects:
110
- tracks = track_ids.map { |id| {:id => id} } # => [{:id=>22448500}, {:id=>21928809}, {:id=>21778201}]
111
-
112
- # send update/put request to playlist
113
- playlist = client.put(playlist.uri, :playlist => {
114
- :tracks => tracks
115
- })
116
-
117
- # print the list of track ids of the updated playlist:
118
- p playlist.tracks.map(&:id)
90
+ ```ruby
91
+ # initializing a client with an access token
92
+ client = SoundCloud.new(:access_token => SOME_ACCESS_TOKEN)
93
+
94
+ # updating the users profile description
95
+ client.put("/me", :user => {:description => "a new description"})
96
+ ```
97
+
98
+ ### Add a track to a playlist / set
99
+ ```ruby
100
+ client = SoundCloud.new(:access_token => "A_VALID_TOKEN")
101
+
102
+ # get my last playlist
103
+ playlist = client.get("/me/playlists").first
104
+
105
+ # get ids of contained tracks
106
+ track_ids = playlist.tracks.map(&:id) # => [22448500, 21928809]
107
+
108
+ # adding a new track 21778201
109
+ track_ids << 21778201 # => [22448500, 21928809, 21778201]
110
+
111
+ # map array of ids to array of track objects:
112
+ tracks = track_ids.map{|id| {:id => id}} # => [{:id=>22448500}, {:id=>21928809}, {:id=>21778201}]
113
+
114
+ # send update/put request to playlist
115
+ playlist = client.put(playlist.uri, :playlist => {
116
+ :tracks => tracks
117
+ })
118
+
119
+ # print the list of track ids of the updated playlist:
120
+ p playlist.tracks.map(&:id)
121
+ ```
119
122
 
120
123
  ## Interface
121
- #### Soundcloud.new(options={})
122
- Stores the passed options and call exchange_token in case options are passed that allow an exchange of tokens.
123
-
124
- #### Soundcloud#exchange_token(options={})
125
- Stores the passed options and try to exchange tokens if no access_token is present and:
126
- - refresh_token, client_id and client_secret is present.
127
- - client_id, client_secret, username, password is present
128
- - client_id, client_secret, redirect_uri, code is present
129
-
130
- #### Soundcloud#authorize_url(options={})
131
- Stores the passed options except for ``state`` and ``display`` and return an authorize url.
132
- The ``client_id`` and ``redirect_uri`` options need to present to generate the authorize url.
133
- The ``state`` and ``display`` options can be used to set the parameters accordingly in the authorize url.
134
-
135
- #### Soundcloud#get, Soundcloud#post, Soundcloud#put, Soundcloud#delete, Soundcloud#head
136
- These methods expose all available HTTP methods. They all share the signature (path_or_uri, query={}, options={}).
137
- The query hash will be merged with the options hash and passed to httparty. Depending on if the client is authorized it will either add the client_id or the access_token as a query parameter.
138
- In case an access_token is expired and a refresh_token, client_id and client_secret is present it will try to refresh the access_token and retry the call.
139
- The response is either a Hashie::Mash or an array of Hashie::Mashs. The mashs expose all resource attributes as methods and the original response through #response.
140
-
141
- #### Soundcloud#client_id, client_secret, access_token, refresh_token, use_ssl?
124
+ #### SoundCloud.new(options={})
125
+ Stores the passed options and call exchange_token in case options are passed
126
+ that allow an exchange of tokens.
127
+
128
+ #### SoundCloud#exchange_token(options={})
129
+ Stores the passed options and try to exchange tokens if no access_token is
130
+ present and:
131
+
132
+ * `refresh_token`, `client_id` and `client_secret` is present.
133
+ * `client_id`, `client_secret`, `username`, and `password` is present
134
+ * `client_id`, `client_secret`, `redirect_uri`, and `code` is present
135
+
136
+ #### SoundCloud#authorize_url(options={})
137
+ Stores the passed options except for `state` and `display` and return an
138
+ authorize url. The `client_id` and `redirect_uri` options need to present to
139
+ generate the authorize url. The `state` and `display` options can be used to
140
+ set the parameters accordingly in the authorize url.
141
+
142
+ #### SoundCloud#get, SoundCloud#post, SoundCloud#put, SoundCloud#delete, SoundCloud#head
143
+ These methods expose all available HTTP methods. They all share the signature
144
+ `(path_or_uri, query={}, options={})`. The query hash will be merged with the
145
+ options hash and passed to httparty. Depending on if the client is authorized
146
+ it will either add the client_id or the access_token as a query parameter. In
147
+ case an access_token is expired and a `refresh_token`, `client_id` and
148
+ `client_secret` is present it will try to refresh the `access_token` and retry
149
+ the call. The response is either a Hashie::Mash or an array of Hashie::Mashes.
150
+ The mashes expose all resource attributes as methods and the original response
151
+ through `HashResponseWrapper#response`.
152
+
153
+ #### SoundCloud#client_id, client_secret, access_token, refresh_token, use_ssl?
142
154
  These methods are accessors for the stored options.
143
155
 
144
- ### Soundcloud#on_exchange_token
145
- A Proc passed to on_exchange_token will be called each time a token was successfully exchanged or refreshed
156
+ #### SoundCloud#on_exchange_token
157
+ A Proc passed to on_exchange_token will be called each time a token was
158
+ successfully exchanged or refreshed
146
159
 
147
- ### Soundcloud#expires_at
148
- Returns a date based on the expires_in attribute returned from a token exchange.
160
+ #### SoundCloud#expires_at
161
+ Returns a date based on the `expires_in` attribute returned from a token
162
+ exchange.
149
163
 
150
- ### Soundcloud#expired?
151
- Will return true or false depending on if expires_at is in the past.
164
+ #### SoundCloud#expired?
165
+ Will return true or false depending on if `expires_at` is in the past.
152
166
 
153
167
  #### Error Handling
154
- In case a request was not successful a Soundcloud::ResponseError will be raised.
155
- The original HTTParty response is available through Soundcloud::ResponseError#response.
168
+ In case a request was not successful a SoundCloud::ResponseError will be
169
+ raised. The original HTTParty response is available through
170
+ `SoundCloud::ResponseError#response`.
@@ -1,217 +1,31 @@
1
- require 'httmultiparty'
2
1
  require 'hashie'
2
+ require 'httmultiparty'
3
3
  require 'uri'
4
- require 'soundcloud/version'
5
-
6
- class Soundcloud
7
- class ResponseError < HTTParty::ResponseError
8
- def message
9
- error = response.parsed_response['error'] || response.parsed_response['errors']['error']
10
- "HTTP status: #{StatusCodes.interpret_status(response.code)} Error: #{error}"
11
- rescue
12
- "HTTP status: #{StatusCodes.interpret_status(response.code)}"
13
- end
14
-
15
- module StatusCodes
16
- STATUS_CODES = {
17
- 100 => "Continue",
18
- 101 => "Switching Protocols",
19
- 102 => "Processing",
20
-
21
- 200 => "OK",
22
- 201 => "Created",
23
- 202 => "Accepted",
24
- 203 => "Non-Authoritative Information",
25
- 204 => "No Content",
26
- 205 => "Reset Content",
27
- 206 => "Partial Content",
28
- 207 => "Multi-Status",
29
- 226 => "IM Used",
30
-
31
- 300 => "Multiple Choices",
32
- 301 => "Moved Permanently",
33
- 302 => "Found",
34
- 303 => "See Other",
35
- 304 => "Not Modified",
36
- 305 => "Use Proxy",
37
- 307 => "Temporary Redirect",
38
-
39
- 400 => "Bad Request",
40
- 401 => "Unauthorized",
41
- 402 => "Payment Required",
42
- 403 => "Forbidden",
43
- 404 => "Not Found",
44
- 405 => "Method Not Allowed",
45
- 406 => "Not Acceptable",
46
- 407 => "Proxy Authentication Required",
47
- 408 => "Request Timeout",
48
- 409 => "Conflict",
49
- 410 => "Gone",
50
- 411 => "Length Required",
51
- 412 => "Precondition Failed",
52
- 413 => "Request Entity Too Large",
53
- 414 => "Request-URI Too Long",
54
- 415 => "Unsupported Media Type",
55
- 416 => "Requested Range Not Satisfiable",
56
- 417 => "Expectation Failed",
57
- 422 => "Unprocessable Entity",
58
- 423 => "Locked",
59
- 424 => "Failed Dependency",
60
- 426 => "Upgrade Required",
61
-
62
- 500 => "Internal Server Error",
63
- 501 => "Not Implemented",
64
- 502 => "Bad Gateway",
65
- 503 => "Service Unavailable",
66
- 504 => "Gateway Timeout",
67
- 505 => "HTTP Version Not Supported",
68
- 507 => "Insufficient Storage",
69
- 510 => "Not Extended"
70
- }
71
-
72
- def self.interpret_status(status)
73
- "#{status} #{STATUS_CODES[status.to_i]}".strip
74
- end
75
- end
76
- end
77
-
78
- class UnauthorizedResponseError < ResponseError; end
79
- USER_AGENT = "SoundCloud Ruby Wrapper #{VERSION}"
80
-
81
- include HTTMultiParty
82
- CLIENT_ID_PARAM_NAME = :client_id
83
- API_SUBHOST = 'api'
84
- AUTHORIZE_PATH = '/connect'
85
- TOKEN_PATH = '/oauth2/token'
86
- DEFAULT_OPTIONS = {
87
- :site => 'soundcloud.com',
88
- :on_exchange_token => lambda {}
89
- }
90
-
91
- attr_accessor :options
92
- headers({"User-Agent" => USER_AGENT})
93
-
94
- def initialize(options={})
95
- store_options(options)
96
- if access_token.nil? && (options_for_refresh_flow_present? ||
97
- options_for_credentials_flow_present? || options_for_code_flow_present?)
98
- exchange_token
99
- end
100
-
101
- raise ArgumentError, "At least a client_id or an access_token must be present" if client_id.nil? && access_token.nil?
102
- end
103
-
104
- def get (path, query={}, options={}); handle_response { self.class.get( *construct_query_arguments(path, options.merge(:query => query)) ) } end
105
- def post (path, body={}, options={}); handle_response { self.class.post( *construct_query_arguments(path, options.merge(:body => body), :body) ) } end
106
- def put (path, body={}, options={}); handle_response { self.class.put( *construct_query_arguments(path, options.merge(:body => body), :body) ) } end
107
- def delete(path, query={}, options={}); handle_response { self.class.delete( *construct_query_arguments(path, options.merge(:query => query)) ) } end
108
- def head (path, query={}, options={}); handle_response { self.class.head( *construct_query_arguments(path, options.merge(:query => query)) ) } end
109
-
110
- # accessors for options
111
- def client_id; @options[:client_id]; end
112
- def client_secret; @options[:client_secret]; end
113
- def access_token; @options[:access_token]; end
114
- def refresh_token; @options[:refresh_token]; end
115
- def redirect_uri; @options[:redirect_uri]; end
116
- def expires_at; @options[:expires_at]; end
117
4
 
118
- def expired?
119
- (expires_at.nil? || expires_at < Time.now)
120
- end
121
-
122
- def use_ssl?;
123
- !! @options[:use_ssl?] || access_token
124
- end
125
-
126
- def site; @options[:site]; end
127
-
128
- def host; site; end
129
- def api_host; [API_SUBHOST, host].join('.'); end
130
-
131
- def authorize_url(options={})
132
- additional_params = [:display, :state, :scope].map do |param_name|
133
- value = options.delete(param_name)
134
- "#{param_name}=#{CGI.escape value}" unless value.nil?
135
- end.compact.join("&")
136
-
137
- store_options(options)
138
- "https://#{host}#{AUTHORIZE_PATH}?response_type=code_and_token&client_id=#{client_id}&redirect_uri=#{URI.escape redirect_uri}&#{additional_params}"
139
- end
5
+ require 'soundcloud/array_response_wrapper'
6
+ require 'soundcloud/client'
7
+ require 'soundcloud/hash_response_wrapper'
8
+ require 'soundcloud/response_error'
9
+ require 'soundcloud/version'
140
10
 
141
- def exchange_token(options={})
142
- store_options(options)
143
- raise ArgumentError, 'client_id and client_secret is required to retrieve an access_token' if client_id.nil? || client_secret.nil?
144
- client_params = {:client_id => client_id, :client_secret => client_secret}
145
- params = if options_for_refresh_flow_present?
146
- {:grant_type => 'refresh_token', :refresh_token => refresh_token}
147
- elsif options_for_credentials_flow_present?
148
- {:grant_type => 'password', :username => @options[:username], :password => @options[:password]}
149
- elsif options_for_code_flow_present?
150
- {:grant_type => 'authorization_code', :redirect_uri => @options[:redirect_uri], :code => @options[:code]}
151
- end
152
- params.merge!(client_params)
153
- response = handle_response(false) {
154
- self.class.post("https://#{api_host}#{TOKEN_PATH}", :query => params)
155
- }
156
- @options.merge!(:access_token => response.access_token, :refresh_token => response.refresh_token)
157
- @options[:expires_at] = Time.now + response.expires_in if response.expires_in
158
- @options[:on_exchange_token].call(*[(self if @options[:on_exchange_token].arity == 1)].compact)
159
- response
160
- end
11
+ module SoundCloud
161
12
 
162
- def on_exchange_token(&block)
163
- store_options(:on_exchange_token => block)
13
+ def new(options={})
14
+ Client.new(options)
164
15
  end
16
+ module_function :new
165
17
 
166
- private
167
- def handle_response(refreshing_enabled=true, &block)
168
- response = block.call
169
- if response && !response.success?
170
- if response.code == 401 && refreshing_enabled && options_for_refresh_flow_present?
171
- exchange_token
172
- # TODO it should return the original
173
- handle_response(false, &block)
174
- else
175
- raise ResponseError.new(response)
176
- end
177
- elsif response.is_a? Hash
178
- HashResponseWrapper.new(response)
179
- elsif response.is_a? Array
180
- ArrayResponseWrapper.new(response)
181
- elsif response && response.success?
182
- response
183
- end
18
+ def method_missing(method_name, *args, &block)
19
+ return super unless respond_to_missing?(method_name)
20
+ Client.send(method_name, *args, &block)
184
21
  end
22
+ module_function :method_missing
185
23
 
186
- def options_for_refresh_flow_present?; !! @options[:refresh_token]; end
187
- def options_for_credentials_flow_present?; !! @options[:username] && @options[:password]; end
188
- def options_for_code_flow_present?; !! @options[:code] && @options[:redirect_uri]; end
189
-
190
- def store_options(options={})
191
- @options ||= DEFAULT_OPTIONS.dup
192
- @options.merge! options
24
+ def respond_to_missing?(method_name, include_private=false)
25
+ Client.respond_to?(method_name, include_private)
193
26
  end
27
+ module_function :respond_to_missing?
194
28
 
195
-
196
- def construct_query_arguments(path_or_uri, options={}, body_or_query=:query)
197
- uri = URI.parse(path_or_uri)
198
- path = uri.path
199
-
200
- scheme = use_ssl? ? 'https' : 'http'
201
- options = options.dup
202
- options[body_or_query] ||= {}
203
- options[body_or_query][:format] = "json"
204
- if access_token
205
- options[body_or_query][:oauth_token] = access_token
206
- else
207
- options[body_or_query][CLIENT_ID_PARAM_NAME] = client_id
208
- end
209
- [
210
- "#{scheme}://#{api_host}#{path}#{uri.query ? "?#{uri.query}" : ""}",
211
- options
212
- ]
213
- end
214
29
  end
215
30
 
216
- require 'soundcloud/array_response_wrapper'
217
- require 'soundcloud/hash_response_wrapper'
31
+ Soundcloud = SoundCloud
@@ -1,8 +1,12 @@
1
- class Soundcloud::ArrayResponseWrapper < Array
2
- attr_reader :response
3
- def initialize(response=[])
4
- mashes = response.map { |o| Hashie::Mash.new(o) }
5
- self.replace(mashes)
6
- @response = response
1
+ module SoundCloud
2
+ class ArrayResponseWrapper < Array
3
+ attr_reader :response
4
+ def initialize(response=[])
5
+ @response = response
6
+ mashes = response.map do |object|
7
+ Hashie::Mash.new(object)
8
+ end
9
+ replace(mashes)
10
+ end
7
11
  end
8
- end
12
+ end
@@ -0,0 +1,199 @@
1
+ module SoundCloud
2
+ class Client
3
+ include HTTMultiParty
4
+ USER_AGENT = "SoundCloud Ruby Wrapper #{VERSION}"
5
+ CLIENT_ID_PARAM_NAME = :client_id
6
+ API_SUBHOST = 'api'
7
+ AUTHORIZE_PATH = '/connect'
8
+ TOKEN_PATH = '/oauth2/token'
9
+ DEFAULT_OPTIONS = {
10
+ :site => 'soundcloud.com',
11
+ :on_exchange_token => lambda {}
12
+ }
13
+
14
+ attr_accessor :options
15
+ headers({"User-Agent" => USER_AGENT})
16
+
17
+ def initialize(options={})
18
+ store_options(options)
19
+ if access_token.nil? && (options_for_refresh_flow_present? || options_for_credentials_flow_present? || options_for_code_flow_present?)
20
+ exchange_token
21
+ end
22
+ raise ArgumentError, "At least a client_id or an access_token must be present" if client_id.nil? && access_token.nil?
23
+ end
24
+
25
+ def get(path, query={}, options={})
26
+ handle_response {
27
+ self.class.get(*construct_query_arguments(path, options.merge(:query => query)))
28
+ }
29
+ end
30
+
31
+ def post(path, body={}, options={})
32
+ handle_response {
33
+ self.class.post(*construct_query_arguments(path, options.merge(:body => body), :body))
34
+ }
35
+ end
36
+
37
+ def put(path, body={}, options={})
38
+ handle_response {
39
+ self.class.put(*construct_query_arguments(path, options.merge(:body => body), :body))
40
+ }
41
+ end
42
+
43
+ def delete(path, query={}, options={})
44
+ handle_response {
45
+ self.class.delete(*construct_query_arguments(path, options.merge(:query => query)))
46
+ }
47
+ end
48
+
49
+ def head(path, query={}, options={})
50
+ handle_response {
51
+ self.class.head(*construct_query_arguments(path, options.merge(:query => query)))
52
+ }
53
+ end
54
+
55
+ # accessors for options
56
+ def client_id
57
+ @options[:client_id]
58
+ end
59
+
60
+ def client_secret
61
+ @options[:client_secret]
62
+ end
63
+
64
+ def access_token
65
+ @options[:access_token]
66
+ end
67
+
68
+ def refresh_token
69
+ @options[:refresh_token]
70
+ end
71
+
72
+ def redirect_uri
73
+ @options[:redirect_uri]
74
+ end
75
+
76
+ def expires_at
77
+ @options[:expires_at]
78
+ end
79
+
80
+ def expired?
81
+ (expires_at.nil? || expires_at < Time.now)
82
+ end
83
+
84
+ def use_ssl?
85
+ !!(@options[:use_ssl?] || access_token)
86
+ end
87
+
88
+ def site
89
+ @options[:site]
90
+ end
91
+ alias host site
92
+
93
+ def api_host
94
+ [API_SUBHOST, host].join('.')
95
+ end
96
+
97
+ def authorize_url(options={})
98
+ additional_params = [:display, :state, :scope].map do |param_name|
99
+ value = options.delete(param_name)
100
+ "#{param_name}=#{CGI.escape value}" unless value.nil?
101
+ end.compact.join("&")
102
+ store_options(options)
103
+ "https://#{host}#{AUTHORIZE_PATH}?response_type=code_and_token&client_id=#{client_id}&redirect_uri=#{URI.escape(redirect_uri)}&#{additional_params}"
104
+ end
105
+
106
+ def exchange_token(options={})
107
+ store_options(options)
108
+ raise ArgumentError, 'client_id and client_secret is required to retrieve an access_token' if client_id.nil? || client_secret.nil?
109
+ client_params = {:client_id => client_id, :client_secret => client_secret}
110
+ params = if options_for_refresh_flow_present?
111
+ {
112
+ :grant_type => 'refresh_token',
113
+ :refresh_token => refresh_token,
114
+ }
115
+ elsif options_for_credentials_flow_present?
116
+ {
117
+ :grant_type => 'password',
118
+ :username => @options[:username],
119
+ :password => @options[:password],
120
+ }
121
+ elsif options_for_code_flow_present?
122
+ {
123
+ :grant_type => 'authorization_code',
124
+ :redirect_uri => @options[:redirect_uri],
125
+ :code => @options[:code],
126
+ }
127
+ end
128
+ params.merge!(client_params)
129
+ response = handle_response(false) {
130
+ self.class.post("https://#{api_host}#{TOKEN_PATH}", :query => params)
131
+ }
132
+ @options.merge!(:access_token => response.access_token, :refresh_token => response.refresh_token)
133
+ @options[:expires_at] = Time.now + response.expires_in if response.expires_in
134
+ @options[:on_exchange_token].call(*[(self if @options[:on_exchange_token].arity == 1)].compact)
135
+ response
136
+ end
137
+
138
+ def on_exchange_token(&block)
139
+ store_options(:on_exchange_token => block)
140
+ end
141
+
142
+ private
143
+
144
+ def handle_response(refreshing_enabled=true, &block)
145
+ response = block.call
146
+ if response && !response.success?
147
+ if response.code == 401 && refreshing_enabled && options_for_refresh_flow_present?
148
+ exchange_token
149
+ # TODO it should return the original
150
+ handle_response(false, &block)
151
+ else
152
+ raise ResponseError.new(response)
153
+ end
154
+ elsif response.is_a?(Hash)
155
+ HashResponseWrapper.new(response)
156
+ elsif response.is_a?(Array)
157
+ ArrayResponseWrapper.new(response)
158
+ elsif response && response.success?
159
+ response
160
+ end
161
+ end
162
+
163
+ def options_for_refresh_flow_present?
164
+ !!@options[:refresh_token]
165
+ end
166
+
167
+ def options_for_credentials_flow_present?
168
+ !!(@options[:username] && @options[:password])
169
+ end
170
+
171
+ def options_for_code_flow_present?
172
+ !!(@options[:code] && @options[:redirect_uri])
173
+ end
174
+
175
+ def store_options(options={})
176
+ @options ||= DEFAULT_OPTIONS.dup
177
+ @options.merge!(options)
178
+ end
179
+
180
+ def construct_query_arguments(path_or_uri, options={}, body_or_query=:query)
181
+ uri = URI.parse(path_or_uri)
182
+ path = uri.path
183
+ scheme = use_ssl? ? 'https' : 'http'
184
+ options = options.dup
185
+ options[body_or_query] ||= {}
186
+ options[body_or_query][:format] = "json"
187
+ if access_token
188
+ options[body_or_query][:oauth_token] = access_token
189
+ else
190
+ options[body_or_query][CLIENT_ID_PARAM_NAME] = client_id
191
+ end
192
+ [
193
+ "#{scheme}://#{api_host}#{path}#{uri.query ? "?#{uri.query}" : ""}",
194
+ options
195
+ ]
196
+ end
197
+
198
+ end
199
+ end
@@ -1,7 +1,9 @@
1
- class Soundcloud::HashResponseWrapper < Hashie::Mash
2
- attr_reader :response
3
- def initialize(response=nil, *args)
4
- super(response, *args)
5
- @response = response
1
+ module SoundCloud
2
+ class HashResponseWrapper < Hashie::Mash
3
+ attr_reader :response
4
+ def initialize(response=nil, *args)
5
+ @response = response
6
+ super(response, *args)
7
+ end
6
8
  end
7
9
  end
@@ -0,0 +1,44 @@
1
+ module SoundCloud
2
+ class ResponseError < HTTParty::ResponseError
3
+ STATUS_CODES = {
4
+ 400 => "Bad Request",
5
+ 401 => "Unauthorized",
6
+ 402 => "Payment Required",
7
+ 403 => "Forbidden",
8
+ 404 => "Not Found",
9
+ 405 => "Method Not Allowed",
10
+ 406 => "Not Acceptable",
11
+ 407 => "Proxy Authentication Required",
12
+ 408 => "Request Timeout",
13
+ 409 => "Conflict",
14
+ 410 => "Gone",
15
+ 411 => "Length Required",
16
+ 412 => "Precondition Failed",
17
+ 413 => "Request Entity Too Large",
18
+ 414 => "Request-URI Too Long",
19
+ 415 => "Unsupported Media Type",
20
+ 416 => "Requested Range Not Satisfiable",
21
+ 417 => "Expectation Failed",
22
+ 422 => "Unprocessable Entity",
23
+ 423 => "Locked",
24
+ 424 => "Failed Dependency",
25
+ 426 => "Upgrade Required",
26
+ 500 => "Internal Server Error",
27
+ 501 => "Not Implemented",
28
+ 502 => "Bad Gateway",
29
+ 503 => "Service Unavailable",
30
+ 504 => "Gateway Timeout",
31
+ 505 => "HTTP Version Not Supported",
32
+ 507 => "Insufficient Storage",
33
+ 510 => "Not Extended"
34
+ }
35
+
36
+ def message
37
+ error = response.parsed_response['error'] || response.parsed_response['errors']['error']
38
+ "HTTP status: #{response.code} #{STATUS_CODES[response.code]} Error: #{error}"
39
+ rescue
40
+ "HTTP status: #{response.code} #{STATUS_CODES[response.code]}"
41
+ end
42
+
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
- class Soundcloud
2
- VERSION = '0.3.1'
1
+ module SoundCloud
2
+ VERSION = '0.3.2'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soundcloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,64 +9,48 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-04 00:00:00.000000000 Z
12
+ date: 2013-08-19 00:00:00.000000000 Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: httparty
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ! '>='
20
- - !ruby/object:Gem::Version
21
- version: 0.7.3
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: 0.7.3
30
14
  - !ruby/object:Gem::Dependency
31
15
  name: httmultiparty
32
16
  requirement: !ruby/object:Gem::Requirement
33
17
  none: false
34
18
  requirements:
35
- - - ! '>='
19
+ - - ~>
36
20
  - !ruby/object:Gem::Version
37
- version: '0.3'
21
+ version: 0.3.0
38
22
  type: :runtime
39
23
  prerelease: false
40
24
  version_requirements: !ruby/object:Gem::Requirement
41
25
  none: false
42
26
  requirements:
43
- - - ! '>='
27
+ - - ~>
44
28
  - !ruby/object:Gem::Version
45
- version: '0.3'
29
+ version: 0.3.0
46
30
  - !ruby/object:Gem::Dependency
47
31
  name: hashie
48
32
  requirement: !ruby/object:Gem::Requirement
49
33
  none: false
50
34
  requirements:
51
- - - ! '>='
35
+ - - ~>
52
36
  - !ruby/object:Gem::Version
53
- version: '0'
37
+ version: '2.0'
54
38
  type: :runtime
55
39
  prerelease: false
56
40
  version_requirements: !ruby/object:Gem::Requirement
57
41
  none: false
58
42
  requirements:
59
- - - ! '>='
43
+ - - ~>
60
44
  - !ruby/object:Gem::Version
61
- version: '0'
45
+ version: '2.0'
62
46
  - !ruby/object:Gem::Dependency
63
- name: rspec
47
+ name: bundler
64
48
  requirement: !ruby/object:Gem::Requirement
65
49
  none: false
66
50
  requirements:
67
51
  - - ~>
68
52
  - !ruby/object:Gem::Version
69
- version: 2.5.0
53
+ version: '1.0'
70
54
  type: :development
71
55
  prerelease: false
72
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -74,24 +58,9 @@ dependencies:
74
58
  requirements:
75
59
  - - ~>
76
60
  - !ruby/object:Gem::Version
77
- version: 2.5.0
78
- - !ruby/object:Gem::Dependency
79
- name: fakeweb
80
- requirement: !ruby/object:Gem::Requirement
81
- none: false
82
- requirements:
83
- - - ! '>='
84
- - !ruby/object:Gem::Version
85
- version: '0'
86
- type: :development
87
- prerelease: false
88
- version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
- requirements:
91
- - - ! '>='
92
- - !ruby/object:Gem::Version
93
- version: '0'
94
- description: A simple Soundcloud API wrapper based of httparty, multipart-post, httmultiparty
61
+ version: '1.0'
62
+ description: The official SoundCloud API wrapper. It provides simple methods to handle
63
+ authorization and to execute HTTP calls.
95
64
  email:
96
65
  - johannes@soundcloud.com
97
66
  executables: []
@@ -99,7 +68,9 @@ extensions: []
99
68
  extra_rdoc_files: []
100
69
  files:
101
70
  - lib/soundcloud/array_response_wrapper.rb
71
+ - lib/soundcloud/client.rb
102
72
  - lib/soundcloud/hash_response_wrapper.rb
73
+ - lib/soundcloud/response_error.rb
103
74
  - lib/soundcloud/version.rb
104
75
  - lib/soundcloud.rb
105
76
  - README.md
@@ -120,11 +91,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
91
  requirements:
121
92
  - - ! '>='
122
93
  - !ruby/object:Gem::Version
123
- version: 1.3.6
94
+ version: 1.3.5
124
95
  requirements: []
125
- rubyforge_project: soundcloud
126
- rubygems_version: 1.8.24
96
+ rubyforge_project:
97
+ rubygems_version: 1.8.23
127
98
  signing_key:
128
99
  specification_version: 3
129
- summary: A simple Soundcloud API wrapper
100
+ summary: The official SoundCloud API wrapper.
130
101
  test_files: []
102
+ has_rdoc: