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.
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