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 +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
|
+
[![Build Status](https://travis-ci.org/soundcloud/soundcloud-ruby.png?branch=master)](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:
|