napster 0.0.0 → 0.1.0
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.
- checksums.yaml +4 -4
- data/.gitignore +39 -5
- data/.rubocop.yml +4 -0
- data/README.md +336 -8
- data/Rakefile +3 -3
- data/bin/console +3 -3
- data/lib/napster.rb +27 -4
- data/lib/napster/client.rb +276 -0
- data/lib/napster/me.rb +58 -0
- data/lib/napster/models/album.rb +97 -0
- data/lib/napster/models/artist.rb +93 -0
- data/lib/napster/models/chart.rb +34 -0
- data/lib/napster/models/content.rb +28 -0
- data/lib/napster/models/favorite.rb +130 -0
- data/lib/napster/models/favorite_status.rb +34 -0
- data/lib/napster/models/follower.rb +39 -0
- data/lib/napster/models/following.rb +62 -0
- data/lib/napster/models/library.rb +136 -0
- data/lib/napster/models/library_date_time.rb +33 -0
- data/lib/napster/models/member.rb +124 -0
- data/lib/napster/models/playlist.rb +279 -0
- data/lib/napster/models/profile.rb +75 -0
- data/lib/napster/models/tag.rb +74 -0
- data/lib/napster/models/track.rb +72 -0
- data/lib/napster/models/uploaded_image.rb +36 -0
- data/lib/napster/moniker.rb +29 -0
- data/lib/napster/request.rb +24 -0
- data/lib/napster/response_error.rb +14 -0
- data/lib/napster/version.rb +1 -1
- data/lib/string_helper.rb +10 -0
- data/napster.gemspec +30 -17
- metadata +132 -11
data/lib/napster.rb
CHANGED
@@ -1,5 +1,28 @@
|
|
1
|
-
require
|
1
|
+
require 'faraday'
|
2
|
+
require 'oj'
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
require 'string_helper'
|
5
|
+
require 'napster/client'
|
6
|
+
require 'napster/me'
|
7
|
+
require 'napster/moniker'
|
8
|
+
require 'napster/response_error'
|
9
|
+
require 'napster/request'
|
10
|
+
require 'napster/version'
|
11
|
+
|
12
|
+
# Models
|
13
|
+
require 'napster/models/artist'
|
14
|
+
require 'napster/models/album'
|
15
|
+
require 'napster/models/chart'
|
16
|
+
require 'napster/models/content'
|
17
|
+
require 'napster/models/follower'
|
18
|
+
require 'napster/models/following'
|
19
|
+
require 'napster/models/favorite'
|
20
|
+
require 'napster/models/favorite_status'
|
21
|
+
require 'napster/models/library'
|
22
|
+
require 'napster/models/library_date_time'
|
23
|
+
require 'napster/models/member'
|
24
|
+
require 'napster/models/playlist'
|
25
|
+
require 'napster/models/profile'
|
26
|
+
require 'napster/models/tag'
|
27
|
+
require 'napster/models/track'
|
28
|
+
require 'napster/models/uploaded_image'
|
@@ -0,0 +1,276 @@
|
|
1
|
+
module Napster
|
2
|
+
# The Client class implements a client object that prepares
|
3
|
+
# information such as api_key, api_secret, and :redirect_uri
|
4
|
+
# needed to call Napster API.
|
5
|
+
class Client
|
6
|
+
MODELS_LIST = %w(artist album track genre member playlist tag
|
7
|
+
station radio favorite).freeze
|
8
|
+
AUTH_METHODS = [:password_grant, :oauth2].freeze
|
9
|
+
|
10
|
+
attr_accessor :api_key,
|
11
|
+
:api_secret,
|
12
|
+
:redirect_uri,
|
13
|
+
:username,
|
14
|
+
:password,
|
15
|
+
:state,
|
16
|
+
:auth_code,
|
17
|
+
:access_token,
|
18
|
+
:refresh_token,
|
19
|
+
:expires_in,
|
20
|
+
:request
|
21
|
+
|
22
|
+
# Instantiate a client object
|
23
|
+
# @note request attribute is always overwritten by Napster::Request
|
24
|
+
# object.
|
25
|
+
# @param options [Hash] Required options are :api_key and :api_secret
|
26
|
+
def initialize(options)
|
27
|
+
options.each do |name, value|
|
28
|
+
instance_variable_set("@#{name}", value)
|
29
|
+
end
|
30
|
+
|
31
|
+
request_hash = {
|
32
|
+
api_key: @api_key,
|
33
|
+
api_secret: @api_secret
|
34
|
+
}
|
35
|
+
@request = Napster::Request.new(request_hash)
|
36
|
+
authenticate_client
|
37
|
+
set_models
|
38
|
+
end
|
39
|
+
|
40
|
+
# Make a post request to Napster API
|
41
|
+
# @param path [String] API path
|
42
|
+
# @param body [Hash] Body for the post request
|
43
|
+
# @param options [Hash] Faraday adapter options
|
44
|
+
# @return [Hash] parsed response from Napster API
|
45
|
+
def post(path, body = {}, options = {})
|
46
|
+
validate_request(path, options)
|
47
|
+
raw_response = @request.faraday.post do |req|
|
48
|
+
req.url path, options[:params]
|
49
|
+
req.body = body
|
50
|
+
req.headers['apikey'] = @api_key
|
51
|
+
if options[:headers]
|
52
|
+
options[:headers].each do |key, value|
|
53
|
+
req.headers[key] = value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
handle_response(raw_response)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Make a get request to Napster API
|
61
|
+
# @param path [String] API path
|
62
|
+
# @param options [Hash] Faraday adapter options accepting
|
63
|
+
# headers
|
64
|
+
# @return [Hash] parsed response from Napster API
|
65
|
+
def get(path, options = {})
|
66
|
+
validate_request(path, options)
|
67
|
+
raw_response = @request.faraday.get do |req|
|
68
|
+
req.url path, options[:params]
|
69
|
+
req.headers['apikey'] = @api_key
|
70
|
+
if options[:headers]
|
71
|
+
options[:headers].each do |key, value|
|
72
|
+
req.headers[key] = value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
handle_response(raw_response)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Make a put request to Napster API
|
80
|
+
# @param path [String] API path
|
81
|
+
# @param body [Hash] Body for the put request
|
82
|
+
# @param options [Hash] Faraday apapter options accepting
|
83
|
+
# headers, params
|
84
|
+
# @return [Hash] parsed response from Napster API
|
85
|
+
def put(path, body = {}, options = {})
|
86
|
+
validate_request(path, options)
|
87
|
+
raw_response = @request.faraday.put do |req|
|
88
|
+
req.url path, options[:params]
|
89
|
+
req.body = body
|
90
|
+
req.headers['apikey'] = @api_key
|
91
|
+
if options[:headers]
|
92
|
+
options[:headers].each do |key, value|
|
93
|
+
req.headers[key] = value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
handle_response(raw_response)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Make a delete request to Napster API
|
101
|
+
# @param path [String] API path
|
102
|
+
# @param options [Hash] Faraday apapter options accepting
|
103
|
+
# headers, params
|
104
|
+
# @return [Hash] parsed response from Napster API
|
105
|
+
def delete(path, options = {})
|
106
|
+
validate_request(path, options)
|
107
|
+
raw_response = @request.faraday.delete do |req|
|
108
|
+
req.url path, options[:params]
|
109
|
+
req.headers['apikey'] = @api_key
|
110
|
+
if options[:headers]
|
111
|
+
options[:headers].each do |key, value|
|
112
|
+
req.headers[key] = value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
handle_response(raw_response)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Smarter method for authentication via password_grant or oauth2
|
120
|
+
# @return [Client]
|
121
|
+
def connect
|
122
|
+
return authenticate(:password_grant) if authenticate_via_password_grant?
|
123
|
+
return authenticate(:oauth2) if authenticate_via_oauth2?
|
124
|
+
raise ArgumentError
|
125
|
+
end
|
126
|
+
|
127
|
+
# Main method for authenticating against Napster API
|
128
|
+
# @param auth_method [Symbol] authentication methods that are
|
129
|
+
# :password_grant or :oauth2
|
130
|
+
# @return [Hash] response from Napster API
|
131
|
+
def authenticate(auth_method)
|
132
|
+
validate_authenticate(auth_method)
|
133
|
+
|
134
|
+
return auth_password_grant if auth_method == :password_grant
|
135
|
+
return auth_oauth2 if auth_method == :oauth2
|
136
|
+
end
|
137
|
+
|
138
|
+
# Get URL for OAuth2 authentication flow
|
139
|
+
# @return [String] OAuth2 authentication URL
|
140
|
+
def authentication_url
|
141
|
+
validate_authentication_url
|
142
|
+
query_params = {
|
143
|
+
client_id: @api_key,
|
144
|
+
redirect_uri: @redirect_uri,
|
145
|
+
response_type: 'code'
|
146
|
+
}
|
147
|
+
query_params[:state] = @state if @state
|
148
|
+
query_params_string = URI.encode_www_form(query_params)
|
149
|
+
Napster::Request::HOST_URL + '/oauth/authorize?' + query_params_string
|
150
|
+
end
|
151
|
+
|
152
|
+
# Include Me module for calling authenticated methods
|
153
|
+
def me
|
154
|
+
Napster::Me.new(self)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Helper method for .new, choose authentication method, and authenticate
|
160
|
+
# @return [Client] Authenticated client
|
161
|
+
def authenticate_client
|
162
|
+
return authenticate(:password_grant) if authenticate_via_password_grant?
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
# Helper method for #authenticate_client, check if password_grant auth is
|
167
|
+
# possible with current attributes.
|
168
|
+
# @return [Boolean]
|
169
|
+
def authenticate_via_password_grant?
|
170
|
+
@api_key && @api_secret && @username && @password
|
171
|
+
end
|
172
|
+
|
173
|
+
def authenticate_via_oauth2?
|
174
|
+
@api_key && @api_secret && @redirect_uri && @auth_code
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate_request(path, options)
|
178
|
+
raise ArgumentError, 'path is missing' unless path
|
179
|
+
raise ArgumentError, 'options should be a hash' unless options.is_a?(Hash)
|
180
|
+
if options[:headers] && !options[:headers].is_a?(Hash)
|
181
|
+
raise ArgumentError, 'options[:headers] should be a hash'
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def validate_authenticate(auth_method)
|
186
|
+
unless auth_method.is_a?(Symbol)
|
187
|
+
err = 'Authentication method must be passed as a symbol'
|
188
|
+
raise ArgumentError, err
|
189
|
+
end
|
190
|
+
|
191
|
+
unless AUTH_METHODS.include?(auth_method)
|
192
|
+
err = "Wrong authentication method. Valid methods are #{AUTH_METHODS}"
|
193
|
+
raise ArgumentError, err
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def auth_password_grant
|
198
|
+
validate_auth_password_grant
|
199
|
+
body = post('/oauth/token',
|
200
|
+
auth_password_grant_post_body,
|
201
|
+
auth_password_grant_post_options)
|
202
|
+
@access_token = body['access_token']
|
203
|
+
@refresh_token = body['refresh_token']
|
204
|
+
@expires_in = body['expires_in']
|
205
|
+
|
206
|
+
self
|
207
|
+
end
|
208
|
+
|
209
|
+
def auth_password_grant_post_body
|
210
|
+
{
|
211
|
+
response_type: 'code',
|
212
|
+
grant_type: 'password',
|
213
|
+
username: @username,
|
214
|
+
password: @password
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
def auth_password_grant_post_options
|
219
|
+
basic_auth = Faraday::Request::BasicAuthentication.header(@api_key,
|
220
|
+
@api_secret)
|
221
|
+
{ headers: { Authorization: basic_auth } }
|
222
|
+
end
|
223
|
+
|
224
|
+
def validate_auth_password_grant
|
225
|
+
raise 'The client is missing username' unless @username
|
226
|
+
raise 'The client is missing password' unless @password
|
227
|
+
end
|
228
|
+
|
229
|
+
def auth_oauth2
|
230
|
+
validate_auth_oauth2
|
231
|
+
response_body = post('/oauth/access_token', auth_oauth2_post_body, {})
|
232
|
+
@access_token = response_body['access_token']
|
233
|
+
@refresh_token = response_body['refresh_token']
|
234
|
+
@expires_in = response_body['expires_in']
|
235
|
+
self
|
236
|
+
end
|
237
|
+
|
238
|
+
def auth_oauth2_post_body
|
239
|
+
{
|
240
|
+
client_id: @api_key,
|
241
|
+
client_secret: @api_secret,
|
242
|
+
response_type: 'code',
|
243
|
+
grant_type: 'authorization_code',
|
244
|
+
code: @auth_code,
|
245
|
+
redirect_uri: @redirect_uri
|
246
|
+
}
|
247
|
+
end
|
248
|
+
|
249
|
+
def validate_auth_oauth2
|
250
|
+
raise 'The client is missing redirect_uri' unless @redirect_uri
|
251
|
+
raise 'The client is missing auth_code' unless @auth_code
|
252
|
+
end
|
253
|
+
|
254
|
+
def validate_authentication_url
|
255
|
+
raise 'The client is missing redirect_uri' unless @redirect_uri
|
256
|
+
end
|
257
|
+
|
258
|
+
def set_models
|
259
|
+
MODELS_LIST.each do |model|
|
260
|
+
define_singleton_method("#{model}s") do
|
261
|
+
Object.const_get(model_class_name(model)).new(client: self)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
self
|
265
|
+
end
|
266
|
+
|
267
|
+
def model_class_name(model)
|
268
|
+
"Napster::Models::#{model.capitalize}"
|
269
|
+
end
|
270
|
+
|
271
|
+
def handle_response(response)
|
272
|
+
raise ResponseError.new(response) if response && !response.success?
|
273
|
+
Oj.load(response.body)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
data/lib/napster/me.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Napster
|
2
|
+
# Authenticated endpoints under /me namespace
|
3
|
+
class Me
|
4
|
+
MODELS_LIST = %w(favorite playlist follower tag).freeze
|
5
|
+
attr_accessor :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
validate_access_token(client)
|
9
|
+
@client = client
|
10
|
+
set_models
|
11
|
+
end
|
12
|
+
|
13
|
+
def listening_history(params)
|
14
|
+
get_options = {
|
15
|
+
params: params,
|
16
|
+
headers: {
|
17
|
+
Authorization: 'Bearer ' + @client.access_token,
|
18
|
+
'Content-Type' => 'application/json',
|
19
|
+
'Accept-Version' => '2.0.0'
|
20
|
+
}
|
21
|
+
}
|
22
|
+
response = @client.get('/me/listens', get_options)
|
23
|
+
Napster::Models::Track
|
24
|
+
.collection(data: response['tracks'], client: @client)
|
25
|
+
end
|
26
|
+
|
27
|
+
def library
|
28
|
+
Napster::Models::Library.new(client: @client)
|
29
|
+
end
|
30
|
+
|
31
|
+
def profile
|
32
|
+
Napster::Models::Profile.new(client: @client)
|
33
|
+
end
|
34
|
+
|
35
|
+
def following
|
36
|
+
Napster::Models::Following.new(client: @client)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def validate_access_token(client)
|
42
|
+
raise 'The client is missing access_token' unless client.access_token
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_models
|
46
|
+
MODELS_LIST.each do |model|
|
47
|
+
define_singleton_method("#{model}s") do
|
48
|
+
Object.const_get(model_class_name(model)).new(client: @client)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def model_class_name(model)
|
55
|
+
"Napster::Models::#{model.capitalize}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
using StringHelper
|
2
|
+
|
3
|
+
module Napster
|
4
|
+
module Models
|
5
|
+
# Album model
|
6
|
+
class Album
|
7
|
+
ATTRIBUTES = [:type,
|
8
|
+
:id,
|
9
|
+
:upc,
|
10
|
+
:shortcut,
|
11
|
+
:href,
|
12
|
+
:name,
|
13
|
+
:released,
|
14
|
+
:originally_released,
|
15
|
+
:label,
|
16
|
+
:copyright,
|
17
|
+
:tags,
|
18
|
+
:disc_count,
|
19
|
+
:track_count,
|
20
|
+
:explicit,
|
21
|
+
:single,
|
22
|
+
:account_partner,
|
23
|
+
:artist_name,
|
24
|
+
:contributing_artists].freeze
|
25
|
+
|
26
|
+
ATTRIBUTES.each do |attribute|
|
27
|
+
attr_accessor attribute
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_accessor :client
|
31
|
+
|
32
|
+
def initialize(arg)
|
33
|
+
@client = arg[:client] if arg[:client]
|
34
|
+
return unless arg[:data]
|
35
|
+
|
36
|
+
ATTRIBUTES.each do |attribute|
|
37
|
+
send("#{attribute}=", arg[:data][attribute.to_s.camel_case_lower])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.collection(arg)
|
42
|
+
arg[:data].map do |album|
|
43
|
+
Album.new(data: album, client: @client)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Top level methods
|
48
|
+
|
49
|
+
def new_releases(params)
|
50
|
+
response = @client.get('/albums/new', params: params)
|
51
|
+
Album.collection(data: response['albums'], client: @client)
|
52
|
+
end
|
53
|
+
|
54
|
+
def staff_picks(params)
|
55
|
+
response = @client.get('/albums/picks', params: params)
|
56
|
+
Album.collection(data: response['albums'], client: @client)
|
57
|
+
end
|
58
|
+
|
59
|
+
def top(params)
|
60
|
+
response = @client.get('/albums/top', params: params)
|
61
|
+
Album.collection(data: response['albums'], client: @client)
|
62
|
+
end
|
63
|
+
|
64
|
+
def find(arg)
|
65
|
+
return find_by_id(arg) if Napster::Moniker.check(arg, :album)
|
66
|
+
find_by_name(arg)
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_by_id(id)
|
70
|
+
response = @client.get("/albums/#{id}")
|
71
|
+
Album.new(data: response['albums'].first, client: @client)
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_all_by_name(name)
|
75
|
+
options = {
|
76
|
+
params: {
|
77
|
+
q: name,
|
78
|
+
type: 'album'
|
79
|
+
}
|
80
|
+
}
|
81
|
+
response = @client.get('/search', options)
|
82
|
+
Album.collection(data: response['data'])
|
83
|
+
end
|
84
|
+
|
85
|
+
def find_by_name(name)
|
86
|
+
find_all_by_name(name).first
|
87
|
+
end
|
88
|
+
|
89
|
+
# Instance methods
|
90
|
+
|
91
|
+
def tracks(params)
|
92
|
+
response = @client.get("/albums/#{@id}/tracks", params: params)
|
93
|
+
Track.collection(data: response['tracks'], client: @client)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|