spotify-ruby 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +10 -2
- data/.rubocop.yml +16 -5
- data/.travis.yml +10 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/COVERAGE.md +98 -92
- data/LICENSE +1 -1
- data/README.md +223 -63
- data/Rakefile +5 -12
- data/lib/spotify.rb +2 -2
- data/lib/spotify/accounts.rb +130 -0
- data/lib/spotify/accounts/session.rb +173 -0
- data/lib/spotify/sdk.rb +43 -60
- data/lib/spotify/sdk/album.rb +84 -0
- data/lib/spotify/sdk/artist.rb +162 -0
- data/lib/spotify/sdk/base.rb +34 -16
- data/lib/spotify/sdk/connect.rb +51 -2
- data/lib/spotify/sdk/connect/device.rb +282 -7
- data/lib/spotify/sdk/connect/playback_state.rb +141 -0
- data/lib/spotify/sdk/image.rb +44 -0
- data/lib/spotify/sdk/item.rb +134 -0
- data/lib/spotify/sdk/me.rb +83 -0
- data/lib/spotify/sdk/me/info.rb +108 -0
- data/lib/spotify/sdk/model.rb +40 -23
- data/lib/spotify/version.rb +8 -5
- data/spotify-ruby.gemspec +24 -9
- metadata +87 -38
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/lib/spotify/auth.rb +0 -111
- data/lib/spotify/sdk/initialization.rb +0 -76
- data/lib/spotify/sdk/initialization/base.rb +0 -74
- data/lib/spotify/sdk/initialization/oauth_access_token.rb +0 -34
- data/lib/spotify/sdk/initialization/plain_string.rb +0 -26
- data/lib/spotify/sdk/initialization/query_hash.rb +0 -45
- data/lib/spotify/sdk/initialization/query_string.rb +0 -51
- data/lib/spotify/sdk/initialization/url_string.rb +0 -49
data/Rakefile
CHANGED
@@ -3,28 +3,21 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
5
|
require "rubocop/rake_task"
|
6
|
-
require "
|
7
|
-
require "rdoc/task"
|
6
|
+
require "yard"
|
8
7
|
|
9
8
|
# RSpec
|
10
9
|
# For testing the Spotify Ruby project.
|
11
10
|
RSpec::Core::RakeTask.new(:spec)
|
12
11
|
|
13
|
-
# Coveralls
|
14
|
-
# Making sure we have complete code coverage.
|
15
|
-
Coveralls::RakeTask.new
|
16
|
-
task test_with_coveralls: [:spec, :features, "coveralls:push"]
|
17
|
-
|
18
12
|
# Rubocop
|
19
13
|
# Making sure our code is linted.
|
20
14
|
RuboCop::RakeTask.new
|
21
15
|
|
22
|
-
#
|
16
|
+
# YARD
|
23
17
|
# Making all of the code documentable.
|
24
|
-
|
25
|
-
|
26
|
-
rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
|
18
|
+
YARD::Rake::YardocTask.new do |t|
|
19
|
+
t.files = ["lib/**/*.rb"]
|
27
20
|
end
|
28
21
|
|
29
22
|
task default: :spec
|
30
|
-
task ci: [
|
23
|
+
task ci: %i[spec rubocop]
|
data/lib/spotify.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "httparty"
|
4
|
-
require "oauth2"
|
5
4
|
|
6
5
|
require "active_support"
|
7
6
|
require "active_support/core_ext"
|
8
7
|
|
9
8
|
require "spotify/version"
|
10
|
-
require "spotify/
|
9
|
+
require "spotify/accounts"
|
10
|
+
require "spotify/accounts/session"
|
11
11
|
require "spotify/sdk"
|
12
12
|
|
13
13
|
##
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
##
|
5
|
+
# Spotify::Accounts deals with authorization using the Spotify Accounts API.
|
6
|
+
#
|
7
|
+
class Accounts
|
8
|
+
##
|
9
|
+
# An entire list of Spotify's OAuth scopes. Stored
|
10
|
+
# in the form of a symbolized array.
|
11
|
+
# Example: `[:scope1, :scope2]`
|
12
|
+
#
|
13
|
+
# @see https://developer.spotify.com/documentation/general/guides/scopes/
|
14
|
+
#
|
15
|
+
# Last updated: 23 June 2018
|
16
|
+
#
|
17
|
+
SCOPES = %i[
|
18
|
+
playlist-read-private
|
19
|
+
playlist-read-collaborative
|
20
|
+
playlist-modify-public
|
21
|
+
playlist-modify-private
|
22
|
+
ugc-image-upload
|
23
|
+
user-follow-modify
|
24
|
+
user-follow-read
|
25
|
+
user-library-read
|
26
|
+
user-library-modify
|
27
|
+
user-read-private
|
28
|
+
user-read-birthdate
|
29
|
+
user-read-email
|
30
|
+
user-top-read
|
31
|
+
user-read-playback-state
|
32
|
+
user-modify-playback-state
|
33
|
+
user-read-currently-playing
|
34
|
+
user-read-recently-played
|
35
|
+
streaming
|
36
|
+
app-remote-control
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
##
|
40
|
+
# Initialize the Spotify Accounts object.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# @accounts = Spotify::Accounts.new({
|
44
|
+
# client_id: "[client id goes here]",
|
45
|
+
# client_secret: "[client secret goes here]",
|
46
|
+
# redirect_uri: "http://localhost"
|
47
|
+
# })
|
48
|
+
#
|
49
|
+
# @accounts = Spotify::Accounts.new
|
50
|
+
# @accounts.client_id = "[client id goes here]"
|
51
|
+
# @accounts.client_secret = "[client secret goes here]"
|
52
|
+
# @accounts.redirect_uri = "http://localhost"
|
53
|
+
#
|
54
|
+
# @param [Hash] config The configuration containing your Client ID, Client Secret, and your Redirect URL.
|
55
|
+
#
|
56
|
+
# @see https://developer.spotify.com/dashboard/
|
57
|
+
#
|
58
|
+
def initialize(config={})
|
59
|
+
@client_id = config.delete(:client_id)
|
60
|
+
@client_secret = config.delete(:client_secret)
|
61
|
+
@redirect_uri = config.delete(:redirect_uri)
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_accessor :client_id, :client_secret, :redirect_uri
|
65
|
+
|
66
|
+
##
|
67
|
+
# Get a HTTP URL to send user for authorizing with Spotify.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# @accounts = Spotify::Accounts.new({
|
71
|
+
# client_id: "[client id goes here]",
|
72
|
+
# client_secret: "[client secret goes here]",
|
73
|
+
# redirect_uri: "http://localhost"
|
74
|
+
# })
|
75
|
+
#
|
76
|
+
# @auth.authorize_url
|
77
|
+
# @auth.authorize_url({ scope: "user-read-private user-top-read" })
|
78
|
+
#
|
79
|
+
# @param [Hash] override_params Optional hash containing any overriding values for parameters.
|
80
|
+
# Parameters used are client_id, redirect_uri, response_type and scope.
|
81
|
+
# @return [String] A fully qualified Spotify authorization URL to send the user to.
|
82
|
+
#
|
83
|
+
# @see https://developer.spotify.com/documentation/general/guides/authorization-guide/
|
84
|
+
#
|
85
|
+
def authorize_url(override_params={})
|
86
|
+
validate_credentials!
|
87
|
+
params = {
|
88
|
+
client_id: @client_id,
|
89
|
+
redirect_uri: @redirect_uri,
|
90
|
+
response_type: "code",
|
91
|
+
scope: SCOPES.join(" ")
|
92
|
+
}.merge(override_params)
|
93
|
+
"https://accounts.spotify.com/oauth/authorize?%s" % params.to_query
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Start a session from your authentication code.
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# @accounts = Spotify::Accounts.new({
|
101
|
+
# client_id: "[client id goes here]",
|
102
|
+
# client_secret: "[client secret goes here]",
|
103
|
+
# redirect_uri: "http://localhost"
|
104
|
+
# })
|
105
|
+
#
|
106
|
+
# @accounts.exchange_for_session("code")
|
107
|
+
#
|
108
|
+
# @param [String] code The code provided back to your application upon authorization.
|
109
|
+
# @return [Spotify::Accounts::Session] session The session object.
|
110
|
+
#
|
111
|
+
# @see https://developer.spotify.com/documentation/general/guides/authorization-guide/
|
112
|
+
#
|
113
|
+
def exchange_for_session(code)
|
114
|
+
validate_credentials!
|
115
|
+
Spotify::Accounts::Session.from_authorization_code(code)
|
116
|
+
end
|
117
|
+
|
118
|
+
def inspect # :nodoc:
|
119
|
+
"#<%s:0x00%x>" % [self.class.name, (object_id << 1)]
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def validate_credentials! # :nodoc:
|
125
|
+
raise "Missing client id" if @client_id.nil?
|
126
|
+
raise "Missing client secret" if @client_secret.nil?
|
127
|
+
raise "Missing redirect uri" if @redirect_uri.nil?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
class Accounts
|
5
|
+
##
|
6
|
+
# A class representing an access token, with the ability to refresh.
|
7
|
+
#
|
8
|
+
class Session
|
9
|
+
class << self
|
10
|
+
##
|
11
|
+
# Parse the response we collect from the authorization code.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# @session = Spotify::Accounts.from_authorization_code(@accounts, "authorization code here")
|
15
|
+
#
|
16
|
+
# @param [Spotify::Accounts] accounts A valid instance of Spotify::Accounts.
|
17
|
+
# @param [String] code The code provided in the Redirect URI from the Spotify Accounts API.
|
18
|
+
# @return [Spotify::Accounts::Session] access_token An instance of Spotify::Accounts::Session
|
19
|
+
# @see lib/spotify/accounts.rb
|
20
|
+
#
|
21
|
+
def from_authorization_code(accounts, code)
|
22
|
+
params = {
|
23
|
+
client_id: @accounts.instance_variable_get(:@client_id),
|
24
|
+
client_secret: @accounts.instance_variable_get(:@client_secret),
|
25
|
+
redirect_uri: @accounts.instance_variable_get(:@redirect_uri),
|
26
|
+
grant_type: "authorization_code",
|
27
|
+
code: code
|
28
|
+
}
|
29
|
+
request = HTTParty.post("https://accounts.spotify.com/api/token", body: params)
|
30
|
+
response = request.parsed_response.with_indifferent_access
|
31
|
+
raise response[:error_description] if response[:error]
|
32
|
+
new(accounts, response[:access_token], response[:expires_in], response[:refresh_token], response[:scope])
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Set up an instance of Access Token with just a refresh_token.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# @access_token = Spotify::Accounts::Session.from_refresh_token(@accounts, "refresh token here")
|
40
|
+
# @access_token.force_refresh!
|
41
|
+
#
|
42
|
+
# @param [Spotify::Accounts] accounts A valid instance of Spotify::Accounts.
|
43
|
+
# @param [String] refresh_token A valid refresh token. You'll want to store the refresh_token in your database.
|
44
|
+
# @return [Spotify::Accounts::Session] access_token An instance of Spotify::Accounts::Session
|
45
|
+
#
|
46
|
+
def from_refresh_token(accounts, refresh_token)
|
47
|
+
new(accounts, nil, nil, refresh_token, nil)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(accounts, access_token, expires_in, refresh_token, scopes)
|
52
|
+
unless accounts.instance_of?(Spotify::Accounts)
|
53
|
+
raise "You need a valid Spotify::Accounts instance in order to use Spotify authentication."
|
54
|
+
end
|
55
|
+
|
56
|
+
@accounts = accounts
|
57
|
+
@access_token = access_token
|
58
|
+
@expires_in = expires_in
|
59
|
+
@expires_at = expires_in + Time.now.to_i unless expires_in.nil?
|
60
|
+
@refresh_token = refresh_token
|
61
|
+
@scopes = scopes
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :accounts, :access_token, :expires_in, :refresh_token
|
65
|
+
|
66
|
+
##
|
67
|
+
# Converts the space-delimited scope list to a symbolized array.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# @access_token.scopes # => [:"user-read-private", :"user-top-read", ...]
|
71
|
+
#
|
72
|
+
# @return [Array] scopes A symbolized list of scopes.
|
73
|
+
#
|
74
|
+
def scopes
|
75
|
+
return [] if @scopes.nil?
|
76
|
+
@scopes.split(" ").map(&:to_sym)
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Checks if a specific scope has been granted by the user.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# @access_token.contains_scope?("user-read-top")
|
84
|
+
# @access_token.contains_scope?(:"user-read-top")
|
85
|
+
#
|
86
|
+
# @param [String,Symbol] scope The name of the scope you'd like to check. For example, "user-read-private".
|
87
|
+
# @return [TrueClass,FalseClass] scope_included A true/false boolean if the scope is included.
|
88
|
+
#
|
89
|
+
def contains_scope?(scope)
|
90
|
+
scopes.include?(scope.downcase.to_sym)
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# When will the access token expire? Returns nil if no expires_in is defined.
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# @session.expires_at
|
98
|
+
#
|
99
|
+
# @return [Time] time When the access token will expire.
|
100
|
+
#
|
101
|
+
def expires_at
|
102
|
+
return nil if @expires_in.nil?
|
103
|
+
Time.at(@expires_at)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Check if the access token has expired. Returns nil if no expires_in is defined.
|
108
|
+
#
|
109
|
+
# @example
|
110
|
+
# @session.expired?
|
111
|
+
#
|
112
|
+
# @return [TrueClass,FalseClass,NilClass] has_expired Has the access token expired?
|
113
|
+
#
|
114
|
+
def expired?
|
115
|
+
return nil if expires_at.nil?
|
116
|
+
Time.now > expires_at
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Refresh the access token.
|
121
|
+
#
|
122
|
+
# @example
|
123
|
+
# @session.refresh!
|
124
|
+
#
|
125
|
+
# @return [TrueClass,FalseClass] success Have we been able to refresh the access token?
|
126
|
+
#
|
127
|
+
# rubocop:disable AbcSize
|
128
|
+
def refresh!
|
129
|
+
raise "You cannot refresh without a valid refresh_token." if @refresh_token.nil?
|
130
|
+
|
131
|
+
params = {
|
132
|
+
client_id: @accounts.instance_variable_get(:@client_id),
|
133
|
+
client_secret: @accounts.instance_variable_get(:@client_secret),
|
134
|
+
grant_type: "refresh_token",
|
135
|
+
refresh_token: @refresh_token
|
136
|
+
}
|
137
|
+
request = HTTParty.post("https://accounts.spotify.com/api/token", body: params)
|
138
|
+
response = request.parsed_response.with_indifferent_access
|
139
|
+
|
140
|
+
@access_token = response[:access_token]
|
141
|
+
@expires_in = response[:expires_in]
|
142
|
+
@expires_at = response[:expires_in] + Time.now.to_i
|
143
|
+
@scopes = response[:scope]
|
144
|
+
|
145
|
+
true
|
146
|
+
rescue HTTParty::Error
|
147
|
+
false
|
148
|
+
end
|
149
|
+
# rubocop:enable AbcSize
|
150
|
+
|
151
|
+
##
|
152
|
+
# Export to JSON. Designed mostly for iOS, Android, or external use cases.
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# @session.to_json
|
156
|
+
#
|
157
|
+
# @return [String] json The JSON output of the session instance.
|
158
|
+
#
|
159
|
+
def to_json
|
160
|
+
{
|
161
|
+
access_token: @access_token.presence,
|
162
|
+
expires_at: @expires_at.presence,
|
163
|
+
refresh_token: @refresh_token.presence,
|
164
|
+
scopes: scopes
|
165
|
+
}.to_json
|
166
|
+
end
|
167
|
+
|
168
|
+
def inspect # :nodoc:
|
169
|
+
"#<%s:0x00%x>" % [self.class.name, (object_id << 1)]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/spotify/sdk.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require "spotify/sdk/initialization/base"
|
5
|
-
require "spotify/sdk/initialization/oauth_access_token"
|
6
|
-
require "spotify/sdk/initialization/plain_string"
|
7
|
-
require "spotify/sdk/initialization/query_hash"
|
8
|
-
require "spotify/sdk/initialization/query_string"
|
9
|
-
require "spotify/sdk/initialization/url_string"
|
3
|
+
# Scaffolding
|
10
4
|
require "spotify/sdk/base"
|
11
5
|
require "spotify/sdk/model"
|
6
|
+
|
7
|
+
# Components
|
12
8
|
require "spotify/sdk/connect"
|
9
|
+
require "spotify/sdk/me"
|
10
|
+
|
11
|
+
# Models
|
13
12
|
require "spotify/sdk/connect/device"
|
13
|
+
require "spotify/sdk/connect/playback_state"
|
14
|
+
require "spotify/sdk/me/info"
|
15
|
+
require "spotify/sdk/artist"
|
16
|
+
require "spotify/sdk/album"
|
17
|
+
require "spotify/sdk/image"
|
18
|
+
require "spotify/sdk/item"
|
14
19
|
|
15
20
|
module Spotify
|
16
21
|
##
|
@@ -38,72 +43,50 @@ module Spotify
|
|
38
43
|
# @sdk = Spotify::SDK.new("https://localhost:8080/#token=...&expires_in=...")
|
39
44
|
# @sdk = Spotify::SDK.new("token=...&expires_in=...")
|
40
45
|
#
|
41
|
-
# @param [String,
|
46
|
+
# @param [String,Hash,OAuth2::AccessToken] session Any supported object containing an access token.
|
42
47
|
#
|
43
|
-
def initialize(
|
44
|
-
|
45
|
-
@
|
46
|
-
@expires_in = @payload[:expires_in]
|
47
|
-
@refresh_token = @payload[:refresh_token]
|
48
|
-
|
48
|
+
def initialize(session)
|
49
|
+
raise "Invalid Spotify::Accounts::Session object" unless session.instance_of?(Spotify::Accounts::Session)
|
50
|
+
@session = session
|
49
51
|
mount_sdk_components
|
50
52
|
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
#
|
55
|
-
|
56
|
-
# @auth = Spotify::Auth.new({
|
57
|
-
# client_id: "[client id goes here]",
|
58
|
-
# client_secret: "[client secret goes here]",
|
59
|
-
# redirect_uri: "http://localhost"
|
60
|
-
# })
|
61
|
-
#
|
62
|
-
# @sdk = Spotify::SDK.new("access_token_here")
|
63
|
-
# @sdk.oauth2_access_token(@auth) # => #<OAuth2::AccessToken:...>
|
64
|
-
#
|
65
|
-
# @param [Spotify::Auth] client An instance of Spotify::Auth. See example.
|
66
|
-
# @return [OAuth2::AccessToken] An fully qualified instance of OAuth2::AccessToken.
|
67
|
-
#
|
68
|
-
def oauth2_access_token(client)
|
69
|
-
OAuth2::AccessToken.new(client, @access_token, expires_in: @expires_in,
|
70
|
-
refresh_token: @refresh_token)
|
54
|
+
attr_reader :session
|
55
|
+
|
56
|
+
def inspect # :nodoc:
|
57
|
+
"#<%s:0x00%x>" % [self.class.name, (object_id << 1)]
|
71
58
|
end
|
72
59
|
|
73
60
|
##
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
@payload.with_indifferent_access.symbolize_keys
|
90
|
-
end
|
61
|
+
# This is where we mount new SDK components to the Spotify::SDK object.
|
62
|
+
# Simply add a key (this is your identifier) with the value being the object.
|
63
|
+
#
|
64
|
+
# Notes:
|
65
|
+
# - Make sure your SDK component is being loaded at the top of this page.
|
66
|
+
# - You can name your identifier whatever you want:
|
67
|
+
# - This will be what people will use to call your code
|
68
|
+
# - For example: it would be the `connect` in `Spotify::SDK.new(@session).connect`
|
69
|
+
# - We'll call .new on your class, providing one parameter being the instance of this SDK (aka self).
|
70
|
+
# - Make sure to a test for it in spec/lib/spotify/sdk_spec.rb (see how we did it for others)
|
71
|
+
#
|
72
|
+
SDK_COMPONENTS = {
|
73
|
+
connect: Spotify::SDK::Connect,
|
74
|
+
me: Spotify::SDK::Me
|
75
|
+
}.freeze
|
91
76
|
|
92
|
-
|
93
|
-
|
77
|
+
SDK_COMPONENTS.each_key do |component|
|
78
|
+
attr_reader(component)
|
79
|
+
end
|
94
80
|
|
95
81
|
private
|
96
82
|
|
97
83
|
##
|
98
|
-
# This is where we
|
99
|
-
# When mounting a new component, you'll need to do the following:
|
100
|
-
# - Be sure to add a `attr_reader` for it. Developers can't access it otherwise.
|
101
|
-
# - Add a test for it in spec/lib/spotify/sdk_spec.rb (see how we did it for others)
|
102
|
-
#
|
103
|
-
# @return [nil]
|
84
|
+
# This is where we map the SDK component classes to the SDK component vairables.
|
104
85
|
#
|
105
|
-
def mount_sdk_components
|
106
|
-
|
86
|
+
def mount_sdk_components # :nodoc:
|
87
|
+
SDK_COMPONENTS.map do |key, klass|
|
88
|
+
instance_variable_set "@#{key}".to_sym, klass.new(self)
|
89
|
+
end
|
107
90
|
end
|
108
91
|
end
|
109
92
|
end
|