soundcloud 0.3.1 → 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +20 -0
- data/README.md +167 -133
- data/lib/soundcloud/array_response_wrapper.rb +11 -7
- data/lib/soundcloud/client.rb +211 -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
- data/lib/soundcloud.rb +18 -204
- metadata +33 -69
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: df5b0cab01fdc8985e0059c4fbaebbb29672f58c17c33ce1cce2765861eeee3c
|
4
|
+
data.tar.gz: 730fc7f919b60e2aa582e755560284ebc07957d51054115da4093adfa71ced0c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 93d8a10bd11b13c43b18e6c5449a3e640f0f43e93eb40473869f3326d84e4f6c4c347929acf9e93ddb0e4f1fa6cf8df75f68d25818636f6738efbe2a2dfafd54
|
7
|
+
data.tar.gz: 71e706b85e323052f13bd39c1f32368c4cbaaccb41ceaf5e2eed65438502dc025e8adf30191af06ae9ea35efad492faad3a85443ef2cb0d57b4ad0a0b29c6e66
|
data/LICENSE.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009-2015 SoundCloud Ltd., Johannes Wagener, Erik Michaels-Ober
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,155 +1,189 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
# ⚠️⚠️DEPRECATED - NO LONGER MAINTAINED⚠️⚠️
|
2
|
+
This repository is no longer maintained by the SoundCloud team due to capacity constraints. We're instead focusing our efforts on improving the API & the developer platform. Please note, at the time of updating this, the repo is already not in sync with the latest API changes.
|
3
|
+
|
4
|
+
We recommend the community to fork this repo in order to maintain the SDK. We'd be more than happy to make a reference on our developer that the developers can use different SDKs build by the community. In case you need to reach out to us, please head over to https://github.com/soundcloud/api/issues
|
5
|
+
|
6
|
+
---
|
7
|
+
|
8
|
+
# SoundCloud API Wrapper
|
9
|
+
|
10
|
+
[![Build Status](https://travis-ci.org/soundcloud/soundcloud-ruby.png?branch=master)](https://travis-ci.org/soundcloud/soundcloud-ruby)
|
5
11
|
|
6
|
-
##
|
7
|
-
|
8
|
-
|
9
|
-
* crack
|
10
|
-
* multipart-upload
|
11
|
-
* hashie
|
12
|
+
## Description
|
13
|
+
The official SoundCloud API wrapper. It provides simple methods to handle
|
14
|
+
authorization and to execute HTTP calls.
|
12
15
|
|
13
16
|
## Installation
|
14
|
-
|
17
|
+
```sh
|
18
|
+
gem install soundcloud
|
19
|
+
```
|
15
20
|
|
16
21
|
## Examples
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
|
23
|
+
The following examples are for the [latest gem version](https://rubygems.org/gems/soundcloud).
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
SoundCloud::VERSION
|
27
|
+
# => "0.3.4"
|
28
|
+
```
|
29
|
+
|
30
|
+
#### Print links of the 10 most recent tracks
|
31
|
+
```ruby
|
32
|
+
# register a client with YOUR_CLIENT_ID as client_id_
|
33
|
+
client = SoundCloud.new(:client_id => YOUR_CLIENT_ID)
|
34
|
+
# get newest tracks
|
35
|
+
tracks = client.get('/tracks', :limit => 10)
|
36
|
+
# print each link
|
37
|
+
tracks.each do |track|
|
38
|
+
puts track.permalink_url
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
27
42
|
#### OAuth2 user credentials flow and print the username of the authenticated user
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
43
|
+
```ruby
|
44
|
+
# register a new client, which will exchange the username, password for an access_token
|
45
|
+
# NOTE: the SoundCloud API Docs advise not to use the user credentials flow in a web app.
|
46
|
+
# In any case, never store the password of a user.
|
47
|
+
client = SoundCloud.new({
|
48
|
+
:client_id => YOUR_CLIENT_ID,
|
49
|
+
:client_secret => YOUR_CLIENT_SECRET,
|
50
|
+
:username => 'some@email.com',
|
51
|
+
:password => 'userpass'
|
52
|
+
})
|
53
|
+
|
54
|
+
# print logged in username
|
55
|
+
puts client.get('/me').username
|
56
|
+
```
|
40
57
|
|
41
58
|
#### OAuth2 authorization code flow
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
59
|
+
```ruby
|
60
|
+
client = SoundCloud.new({
|
61
|
+
:client_id => YOUR_CLIENT_ID,
|
62
|
+
:client_secret => YOUR_CLIENT_SECRET,
|
63
|
+
:redirect_uri => YOUR_REDIRECT_URI,
|
64
|
+
})
|
65
|
+
|
66
|
+
redirect client.authorize_url()
|
67
|
+
# the user should be redirected to "https://soundcloud.com/connect?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI"
|
68
|
+
# after granting access he will be redirected back to YOUR_REDIRECT_URI
|
69
|
+
# in your respective handler you can build an exchange token from the transmitted code
|
70
|
+
client.exchange_token(:code => params[:code])
|
71
|
+
```
|
53
72
|
|
54
73
|
#### 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
|
-
|
74
|
+
```ruby
|
75
|
+
# register a new client which will exchange an existing refresh_token for an access_token
|
76
|
+
client = SoundCloud.new({
|
77
|
+
:client_id => YOUR_CLIENT_ID,
|
78
|
+
:client_secret => YOUR_CLIENT_SECRET,
|
79
|
+
:refresh_token => SOME_REFRESH_TOKEN
|
80
|
+
})
|
81
|
+
|
82
|
+
# upload a new track with audio.mp3 as audio and image.jpg as artwork
|
83
|
+
track = client.post('/tracks', :track => {
|
84
|
+
:title => 'a new track',
|
85
|
+
:asset_data => File.new('audio.mp3')
|
86
|
+
})
|
87
|
+
|
88
|
+
# print new tracks link
|
89
|
+
puts track.permalink_url
|
90
|
+
```
|
70
91
|
|
71
92
|
#### 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}")
|
93
|
+
```ruby
|
94
|
+
# register the client
|
95
|
+
client = SoundCloud.new(:client_id => YOUR_CLIENT_ID)
|
96
|
+
|
97
|
+
# call the resolve endpoint with a track url
|
98
|
+
track = client.get('/resolve', :url => "http://soundcloud.com/forss/flickermood")
|
99
|
+
|
100
|
+
# print the track id
|
101
|
+
puts track.id
|
102
|
+
```
|
88
103
|
|
89
104
|
### 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
|
-
|
105
|
+
```ruby
|
106
|
+
# initializing a client with an access token
|
107
|
+
client = SoundCloud.new(:access_token => SOME_ACCESS_TOKEN)
|
108
|
+
|
109
|
+
# updating the users profile description
|
110
|
+
client.put("/me", :user => {:description => "a new description"})
|
111
|
+
```
|
112
|
+
|
113
|
+
### Add a track to a playlist / set
|
114
|
+
```ruby
|
115
|
+
client = SoundCloud.new(:access_token => "A_VALID_TOKEN")
|
116
|
+
|
117
|
+
# get my last playlist
|
118
|
+
playlist = client.get("/me/playlists").first
|
119
|
+
|
120
|
+
# get ids of contained tracks
|
121
|
+
track_ids = playlist.tracks.map(&:id) # => [22448500, 21928809]
|
122
|
+
|
123
|
+
# adding a new track 21778201
|
124
|
+
track_ids << 21778201 # => [22448500, 21928809, 21778201]
|
125
|
+
|
126
|
+
# map array of ids to array of track objects:
|
127
|
+
tracks = track_ids.map{|id| {:id => id}} # => [{:id=>22448500}, {:id=>21928809}, {:id=>21778201}]
|
128
|
+
|
129
|
+
# send update/put request to playlist
|
130
|
+
playlist = client.put(playlist.uri, :playlist => {
|
131
|
+
:tracks => tracks
|
132
|
+
})
|
133
|
+
|
134
|
+
# print the list of track ids of the updated playlist:
|
135
|
+
p playlist.tracks.map(&:id)
|
136
|
+
```
|
119
137
|
|
120
138
|
## 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
|
-
|
139
|
+
#### SoundCloud.new(options={})
|
140
|
+
Stores the passed options and call exchange_token in case options are passed
|
141
|
+
that allow an exchange of tokens.
|
142
|
+
|
143
|
+
#### SoundCloud#exchange_token(options={})
|
144
|
+
Stores the passed options and try to exchange tokens if no access_token is
|
145
|
+
present and:
|
146
|
+
|
147
|
+
* `refresh_token`, `client_id` and `client_secret` is present.
|
148
|
+
* `client_id`, `client_secret`, `username`, and `password` is present
|
149
|
+
* `client_id`, `client_secret`, `redirect_uri`, and `code` is present
|
150
|
+
|
151
|
+
#### SoundCloud#authorize_url(options={})
|
152
|
+
Stores the passed options except for `state` and `display` and return an
|
153
|
+
authorize url. The `client_id` and `redirect_uri` options need to present to
|
154
|
+
generate the authorize url. The `state` and `display` options can be used to
|
155
|
+
set the parameters accordingly in the authorize url.
|
156
|
+
|
157
|
+
#### SoundCloud#get, SoundCloud#post, SoundCloud#put, SoundCloud#delete, SoundCloud#head
|
158
|
+
These methods expose all available HTTP methods. They all share the signature
|
159
|
+
`(path_or_uri, query={}, options={})`. The query hash will be merged with the
|
160
|
+
options hash and passed to httparty. Depending on if the client is authorized
|
161
|
+
it will either add the client_id or the access_token as a query parameter. In
|
162
|
+
case an access_token is expired and a `refresh_token`, `client_id` and
|
163
|
+
`client_secret` is present it will try to refresh the `access_token` and retry
|
164
|
+
the call. The response is either a Hashie::Mash or an array of Hashie::Mashes.
|
165
|
+
The mashes expose all resource attributes as methods and the original response
|
166
|
+
through `HashResponseWrapper#response`.
|
167
|
+
|
168
|
+
#### SoundCloud#client_id, client_secret, access_token, refresh_token, use_ssl?
|
142
169
|
These methods are accessors for the stored options.
|
143
170
|
|
144
|
-
|
145
|
-
A Proc passed to on_exchange_token will be called each time a token was
|
171
|
+
#### SoundCloud#on_exchange_token
|
172
|
+
A Proc passed to on_exchange_token will be called each time a token was
|
173
|
+
successfully exchanged or refreshed
|
146
174
|
|
147
|
-
|
148
|
-
Returns a date based on the expires_in attribute returned from a token
|
175
|
+
#### SoundCloud#expires_at
|
176
|
+
Returns a date based on the `expires_in` attribute returned from a token
|
177
|
+
exchange.
|
149
178
|
|
150
|
-
|
151
|
-
Will return true or false depending on if expires_at is in the past.
|
179
|
+
#### SoundCloud#expired?
|
180
|
+
Will return true or false depending on if `expires_at` is in the past.
|
152
181
|
|
153
182
|
#### Error Handling
|
154
|
-
In case a request was not successful a
|
155
|
-
The original HTTParty response is available through
|
183
|
+
In case a request was not successful a SoundCloud::ResponseError will be
|
184
|
+
raised. The original HTTParty response is available through
|
185
|
+
`SoundCloud::ResponseError#response`.
|
186
|
+
|
187
|
+
## Documentation
|
188
|
+
|
189
|
+
For more code examples, please visit the [SoundCloud API Documentation](http://developers.soundcloud.com/docs).
|
@@ -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,211 @@
|
|
1
|
+
require 'soundcloud/version'
|
2
|
+
|
3
|
+
module SoundCloud
|
4
|
+
class Client
|
5
|
+
include HTTMultiParty
|
6
|
+
USER_AGENT = "SoundCloud Ruby Wrapper #{SoundCloud::VERSION}"
|
7
|
+
CLIENT_ID_PARAM_NAME = :client_id
|
8
|
+
API_SUBHOST = 'api'
|
9
|
+
AUTHORIZE_PATH = '/connect'
|
10
|
+
TOKEN_PATH = '/oauth2/token'
|
11
|
+
DEFAULT_OPTIONS = {
|
12
|
+
:site => 'soundcloud.com',
|
13
|
+
:on_exchange_token => lambda {}
|
14
|
+
}
|
15
|
+
|
16
|
+
attr_accessor :options
|
17
|
+
headers({"User-Agent" => USER_AGENT})
|
18
|
+
|
19
|
+
def initialize(options={})
|
20
|
+
store_options(options)
|
21
|
+
if access_token.nil? && (options_for_refresh_flow_present? || options_for_credentials_flow_present? || options_for_code_flow_present? || client_secret)
|
22
|
+
exchange_token
|
23
|
+
end
|
24
|
+
raise ArgumentError, "At least a client_id or an access_token must be present" if client_id.nil? && access_token.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(path, query={}, options={})
|
28
|
+
handle_response {
|
29
|
+
self.class.get(*construct_query_arguments(path, options.merge(:query => query)))
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def post(path, body={}, options={})
|
34
|
+
handle_response {
|
35
|
+
self.class.post(*construct_query_arguments(path, options.merge(:body => body), :body))
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def put(path, body={}, options={})
|
40
|
+
handle_response {
|
41
|
+
self.class.put(*construct_query_arguments(path, options.merge(:body => body), :body))
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(path, query={}, options={})
|
46
|
+
handle_response {
|
47
|
+
self.class.delete(*construct_query_arguments(path, options.merge(:query => query)))
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def head(path, query={}, options={})
|
52
|
+
handle_response {
|
53
|
+
self.class.head(*construct_query_arguments(path, options.merge(:query => query)))
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# accessors for options
|
58
|
+
def client_id
|
59
|
+
@options[:client_id]
|
60
|
+
end
|
61
|
+
|
62
|
+
def client_secret
|
63
|
+
@options[:client_secret]
|
64
|
+
end
|
65
|
+
|
66
|
+
def access_token
|
67
|
+
@options[:access_token]
|
68
|
+
end
|
69
|
+
|
70
|
+
def refresh_token
|
71
|
+
@options[:refresh_token]
|
72
|
+
end
|
73
|
+
|
74
|
+
def redirect_uri
|
75
|
+
@options[:redirect_uri]
|
76
|
+
end
|
77
|
+
|
78
|
+
def expires_at
|
79
|
+
@options[:expires_at]
|
80
|
+
end
|
81
|
+
|
82
|
+
def expired?
|
83
|
+
(expires_at.nil? || expires_at < Time.now)
|
84
|
+
end
|
85
|
+
|
86
|
+
def use_ssl?
|
87
|
+
!!(@options[:use_ssl?] || access_token)
|
88
|
+
end
|
89
|
+
|
90
|
+
def site
|
91
|
+
@options[:site]
|
92
|
+
end
|
93
|
+
alias host site
|
94
|
+
|
95
|
+
def api_host
|
96
|
+
[API_SUBHOST, host].join('.')
|
97
|
+
end
|
98
|
+
|
99
|
+
def authorize_url(options={})
|
100
|
+
additional_params = [:display, :state, :scope].map do |param_name|
|
101
|
+
value = options.delete(param_name)
|
102
|
+
"#{param_name}=#{CGI.escape value}" unless value.nil?
|
103
|
+
end.compact.join("&")
|
104
|
+
store_options(options)
|
105
|
+
"https://#{host}#{AUTHORIZE_PATH}?response_type=code_and_token&client_id=#{client_id}&redirect_uri=#{URI.escape(redirect_uri)}&#{additional_params}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def exchange_token(options={})
|
109
|
+
store_options(options)
|
110
|
+
raise ArgumentError, 'client_id and client_secret is required to retrieve an access_token' if client_id.nil? || client_secret.nil?
|
111
|
+
|
112
|
+
params = if options_for_refresh_flow_present?
|
113
|
+
{
|
114
|
+
:grant_type => 'refresh_token',
|
115
|
+
:refresh_token => refresh_token,
|
116
|
+
}
|
117
|
+
elsif options_for_credentials_flow_present?
|
118
|
+
puts "Warning: Password grant is deprecated, see https://developers.soundcloud.com/blog/security-updates-api"
|
119
|
+
{
|
120
|
+
:grant_type => 'password',
|
121
|
+
:username => @options[:username],
|
122
|
+
:password => @options[:password],
|
123
|
+
}
|
124
|
+
elsif options_for_code_flow_present?
|
125
|
+
{
|
126
|
+
:grant_type => 'authorization_code',
|
127
|
+
:redirect_uri => @options[:redirect_uri],
|
128
|
+
:code => @options[:code],
|
129
|
+
}
|
130
|
+
elsif client_secret
|
131
|
+
{ :grant_type => 'client_credentials' }
|
132
|
+
end
|
133
|
+
|
134
|
+
params.merge!(:client_id => client_id, :client_secret => client_secret)
|
135
|
+
|
136
|
+
response = handle_response(false) {
|
137
|
+
self.class.post("https://#{api_host}#{TOKEN_PATH}", :query => params)
|
138
|
+
}
|
139
|
+
|
140
|
+
@options.merge!(:access_token => response.access_token, :refresh_token => response.refresh_token)
|
141
|
+
@options[:expires_at] = Time.now + response.expires_in if response.expires_in
|
142
|
+
@options[:on_exchange_token].call(*[(self if @options[:on_exchange_token].arity == 1)].compact)
|
143
|
+
response
|
144
|
+
end
|
145
|
+
|
146
|
+
def on_exchange_token(&block)
|
147
|
+
store_options(:on_exchange_token => block)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def handle_response(refreshing_enabled=true, &block)
|
153
|
+
response = block.call
|
154
|
+
if response && !response.success?
|
155
|
+
if response.code == 401 && refreshing_enabled && options_for_refresh_flow_present?
|
156
|
+
exchange_token
|
157
|
+
# TODO it should return the original
|
158
|
+
handle_response(false, &block)
|
159
|
+
else
|
160
|
+
raise ResponseError.new(response)
|
161
|
+
end
|
162
|
+
elsif response_is_a?(response, Hash)
|
163
|
+
HashResponseWrapper.new(response)
|
164
|
+
elsif response_is_a?(response, Array)
|
165
|
+
ArrayResponseWrapper.new(response)
|
166
|
+
elsif response && response.success?
|
167
|
+
response
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def options_for_refresh_flow_present?
|
172
|
+
!!@options[:refresh_token]
|
173
|
+
end
|
174
|
+
|
175
|
+
def options_for_credentials_flow_present?
|
176
|
+
!!(@options[:username] && @options[:password])
|
177
|
+
end
|
178
|
+
|
179
|
+
def options_for_code_flow_present?
|
180
|
+
!!(@options[:code] && @options[:redirect_uri])
|
181
|
+
end
|
182
|
+
|
183
|
+
def store_options(options={})
|
184
|
+
@options ||= DEFAULT_OPTIONS.dup
|
185
|
+
@options.merge!(options)
|
186
|
+
end
|
187
|
+
|
188
|
+
def construct_query_arguments(path_or_uri, options={}, body_or_query=:query)
|
189
|
+
uri = URI.parse(path_or_uri)
|
190
|
+
path = uri.path
|
191
|
+
scheme = use_ssl? ? 'https' : 'http'
|
192
|
+
options = options.dup
|
193
|
+
options[body_or_query] ||= {}
|
194
|
+
options[body_or_query][:format] = "json"
|
195
|
+
if access_token
|
196
|
+
options[:headers] = { 'Authorization' => "OAuth #{access_token}" }
|
197
|
+
else
|
198
|
+
puts "Warning: Authorization via ClientId is deprecated, see https://developers.soundcloud.com/blog/security-updates-api"
|
199
|
+
options[body_or_query][CLIENT_ID_PARAM_NAME] = client_id
|
200
|
+
end
|
201
|
+
[
|
202
|
+
"#{scheme}://#{api_host}#{path}#{uri.query ? "?#{uri.query}" : ""}",
|
203
|
+
options
|
204
|
+
]
|
205
|
+
end
|
206
|
+
|
207
|
+
def response_is_a?(response, type)
|
208
|
+
response.is_a?(type) || (response.respond_to?(:parsed_response) && response.parsed_response.is_a?(type))
|
209
|
+
end
|
210
|
+
end
|
211
|
+
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.5'
|
3
3
|
end
|
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
|
metadata
CHANGED
@@ -1,130 +1,94 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: soundcloud
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
5
|
-
prerelease:
|
4
|
+
version: 0.3.5
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Johannes Wagener
|
9
|
-
|
8
|
+
- Erik Michaels-Ober
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-09-24 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
|
-
none: false
|
34
17
|
requirements:
|
35
|
-
- -
|
18
|
+
- - "~>"
|
36
19
|
- !ruby/object:Gem::Version
|
37
|
-
version:
|
20
|
+
version: 0.3.0
|
38
21
|
type: :runtime
|
39
22
|
prerelease: false
|
40
23
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
24
|
requirements:
|
43
|
-
- -
|
25
|
+
- - "~>"
|
44
26
|
- !ruby/object:Gem::Version
|
45
|
-
version:
|
27
|
+
version: 0.3.0
|
46
28
|
- !ruby/object:Gem::Dependency
|
47
29
|
name: hashie
|
48
30
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
31
|
requirements:
|
51
|
-
- -
|
32
|
+
- - ">="
|
52
33
|
- !ruby/object:Gem::Version
|
53
34
|
version: '0'
|
54
35
|
type: :runtime
|
55
36
|
prerelease: false
|
56
37
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
38
|
requirements:
|
59
|
-
- -
|
39
|
+
- - ">="
|
60
40
|
- !ruby/object:Gem::Version
|
61
41
|
version: '0'
|
62
42
|
- !ruby/object:Gem::Dependency
|
63
|
-
name:
|
64
|
-
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
|
-
requirements:
|
67
|
-
- - ~>
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: 2.5.0
|
70
|
-
type: :development
|
71
|
-
prerelease: false
|
72
|
-
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
|
-
requirements:
|
75
|
-
- - ~>
|
76
|
-
- !ruby/object:Gem::Version
|
77
|
-
version: 2.5.0
|
78
|
-
- !ruby/object:Gem::Dependency
|
79
|
-
name: fakeweb
|
43
|
+
name: bundler
|
80
44
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
45
|
requirements:
|
83
|
-
- -
|
46
|
+
- - "~>"
|
84
47
|
- !ruby/object:Gem::Version
|
85
|
-
version: '0'
|
48
|
+
version: '1.0'
|
86
49
|
type: :development
|
87
50
|
prerelease: false
|
88
51
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
52
|
requirements:
|
91
|
-
- -
|
53
|
+
- - "~>"
|
92
54
|
- !ruby/object:Gem::Version
|
93
|
-
version: '0'
|
94
|
-
description:
|
55
|
+
version: '1.0'
|
56
|
+
description: The official SoundCloud API wrapper. It provides simple methods to handle
|
57
|
+
authorization and to execute HTTP calls.
|
95
58
|
email:
|
96
|
-
-
|
59
|
+
- api@soundcloud.com
|
97
60
|
executables: []
|
98
61
|
extensions: []
|
99
62
|
extra_rdoc_files: []
|
100
63
|
files:
|
64
|
+
- LICENSE.md
|
65
|
+
- README.md
|
66
|
+
- lib/soundcloud.rb
|
101
67
|
- lib/soundcloud/array_response_wrapper.rb
|
68
|
+
- lib/soundcloud/client.rb
|
102
69
|
- lib/soundcloud/hash_response_wrapper.rb
|
70
|
+
- lib/soundcloud/response_error.rb
|
103
71
|
- lib/soundcloud/version.rb
|
104
|
-
|
105
|
-
- README.md
|
106
|
-
homepage: http://dev.soundcloud.com
|
72
|
+
homepage: https://dev.soundcloud.com
|
107
73
|
licenses: []
|
108
|
-
|
74
|
+
metadata: {}
|
75
|
+
post_install_message:
|
109
76
|
rdoc_options: []
|
110
77
|
require_paths:
|
111
78
|
- lib
|
112
79
|
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
-
none: false
|
114
80
|
requirements:
|
115
|
-
- -
|
81
|
+
- - ">="
|
116
82
|
- !ruby/object:Gem::Version
|
117
83
|
version: '0'
|
118
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
-
none: false
|
120
85
|
requirements:
|
121
|
-
- -
|
86
|
+
- - ">="
|
122
87
|
- !ruby/object:Gem::Version
|
123
|
-
version: 1.3.
|
88
|
+
version: 1.3.5
|
124
89
|
requirements: []
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
summary: A simple Soundcloud API wrapper
|
90
|
+
rubygems_version: 3.1.2
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: The official SoundCloud API wrapper.
|
130
94
|
test_files: []
|