soundcloud 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: