spotify-ruby 0.1.1 → 0.2.1
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 +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
|