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 +147 -132
- data/lib/soundcloud.rb +18 -204
- data/lib/soundcloud/array_response_wrapper.rb +11 -7
- data/lib/soundcloud/client.rb +199 -0
- data/lib/soundcloud/hash_response_wrapper.rb +7 -5
- data/lib/soundcloud/response_error.rb +44 -0
- data/lib/soundcloud/version.rb +2 -2
- metadata +22 -50
data/README.md
CHANGED
@@ -1,155 +1,170 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
It provides simple methods to handle authorization and to execute HTTP calls.
|
1
|
+
# SoundCloud API Wrapper
|
2
|
+
|
3
|
+
[](https://travis-ci.org/soundcloud/soundcloud-ruby)
|
5
4
|
|
6
|
-
##
|
7
|
-
|
8
|
-
|
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
|
-
|
10
|
+
```sh
|
11
|
+
gem install soundcloud
|
12
|
+
```
|
15
13
|
|
16
14
|
## Examples
|
17
15
|
#### Print links of the 10 hottest tracks
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
####
|
122
|
-
Stores the passed options and call exchange_token in case options are passed
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
145
|
-
A Proc passed to on_exchange_token will be called each time a token was
|
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
|
-
|
148
|
-
Returns a date based on the expires_in attribute returned from a token
|
160
|
+
#### SoundCloud#expires_at
|
161
|
+
Returns a date based on the `expires_in` attribute returned from a token
|
162
|
+
exchange.
|
149
163
|
|
150
|
-
|
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
|
155
|
-
The original HTTParty response is available through
|
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`.
|
data/lib/soundcloud.rb
CHANGED
@@ -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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
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
|
163
|
-
|
13
|
+
def new(options={})
|
14
|
+
Client.new(options)
|
164
15
|
end
|
16
|
+
module_function :new
|
165
17
|
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
187
|
-
|
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
|
-
|
217
|
-
require 'soundcloud/hash_response_wrapper'
|
31
|
+
Soundcloud = SoundCloud
|
@@ -1,8 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
data/lib/soundcloud/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.3.
|
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.
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
78
|
-
|
79
|
-
|
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.
|
94
|
+
version: 1.3.5
|
124
95
|
requirements: []
|
125
|
-
rubyforge_project:
|
126
|
-
rubygems_version: 1.8.
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.8.23
|
127
98
|
signing_key:
|
128
99
|
specification_version: 3
|
129
|
-
summary:
|
100
|
+
summary: The official SoundCloud API wrapper.
|
130
101
|
test_files: []
|
102
|
+
has_rdoc:
|