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.
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 "coveralls/rake/task"
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
- # RDoc
16
+ # YARD
23
17
  # Making all of the code documentable.
24
- RDoc::Task.new do |rdoc|
25
- rdoc.main = "README.md"
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: [:spec, "coveralls:push", :rubocop]
23
+ task ci: %i[spec rubocop]
@@ -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/auth"
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
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spotify/sdk/initialization"
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, Hash, OAuth2::AccessToken] obj Any supported object which contains an access token. See examples.
46
+ # @param [String,Hash,OAuth2::AccessToken] session Any supported object containing an access token.
42
47
  #
43
- def initialize(obj)
44
- @payload = Spotify::SDK::Initialization.detect(obj)
45
- @access_token = @payload[:access_token]
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
- # Helper method to a fully qualified OAuth2::AccessToken instance.
54
- #
55
- # @example
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
- # Obtain a hash containing all of the user's authorization details.
75
- #
76
- # @example
77
- # @auth = Spotify::Auth.new({
78
- # client_id: "[client id goes here]",
79
- # client_secret: "[client secret goes here]",
80
- # redirect_uri: "http://localhost"
81
- # })
82
- #
83
- # @sdk = Spotify::SDK.new("access_token_here")
84
- # @sdk.to_hash # => { access_token: ..., expires_in: ... }
85
- #
86
- # @return [Hash] Containing access_token, expires_in and refresh_token
87
- #
88
- def to_hash
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
- attr_reader :access_token, :expires_in, :refresh_token
93
- attr_reader :connect
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 mount all SDK components to the SDK object.
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
- @connect = Spotify::SDK::Connect.new(self)
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