spotify-ruby-kev 0.2.5

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.
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotify
4
+ class SDK
5
+ class Artist < Model
6
+ ##
7
+ # Do we have the full information for this artist?
8
+ #
9
+ # @example
10
+ # artist = @sdk.connect.playback.artist
11
+ # artist.full_information? # => false
12
+ #
13
+ # @return [FalseClass,TrueClass] is_full_info Does this contain everything?
14
+ #
15
+ def full_information?
16
+ to_h.key?(:images)
17
+ end
18
+
19
+ ##
20
+ # Get full information for this artist by calling /v1/artists/:id
21
+ #
22
+ # @example
23
+ # artist = @sdk.connect.playback.artist
24
+ # artist.retrieve_full_information! unless artist.full_information?
25
+ #
26
+ # @return [TrueClass] success Always returns true.
27
+ #
28
+ def retrieve_full_information!
29
+ unless full_information?
30
+ parent.send_http_request(:get, "/v1/artists/%s" % id).map do |key, value|
31
+ send("%s=" % key, value)
32
+ end
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ ##
39
+ # Helper method for setting the following status.
40
+ # Requires the `user-follow-modify` scope.
41
+ # If true, PUT /v1/me/following otherwise DELETE /v1/me/following
42
+ #
43
+ # @example
44
+ # @sdk.playback.item.artist.following = true
45
+ # @sdk.playback.item.artist.following = false
46
+ #
47
+ def following=(should_follow)
48
+ raise "#following= must be true or false" unless [true, false].include?(should_follow)
49
+
50
+ should_follow ? follow! : unfollow!
51
+ end
52
+
53
+ ##
54
+ # Follow the artist.
55
+ # Requires the `user-follow-modify` scope.
56
+ # PUT /v1/me/following
57
+ #
58
+ # @example
59
+ # @sdk.playback.item.artist.follow!
60
+ #
61
+ # @return [Spotify::SDK::Artist] self Return the artist object, for chaining methods.
62
+ #
63
+ def follow!
64
+ parent.send_http_request(:put, "/v1/me/following?type=artist&ids=%s" % id, http_options: {expect_nil: true})
65
+ self
66
+ end
67
+
68
+ ##
69
+ # Unfollow the artist.
70
+ # Requires the `user-follow-modify` scope.
71
+ # DELETE /v1/me/following
72
+ #
73
+ # @example
74
+ # @sdk.playback.item.artist.unfollow!
75
+ #
76
+ # @return [Spotify::SDK::Artist] self Return the artist object, for chaining methods.
77
+ #
78
+ def unfollow!
79
+ parent.send_http_request(:delete, "/v1/me/following?type=artist&ids=%s" % id, http_options: {expect_nil: true})
80
+ self
81
+ end
82
+
83
+ ##
84
+ # Display the artist's images. If not obtained, request them from the API.
85
+ #
86
+ # @example
87
+ # artist = @sdk.connect.playback.artist
88
+ # artist.images[0] # => [#<Spotify::SDK::Image>, #<Spotify::SDK::Image>, ...]
89
+ #
90
+ # @return [Array] images Contains a list of images, wrapped in Spotify::SDK::Image
91
+ #
92
+ def images
93
+ retrieve_full_information! unless full_information?
94
+ super.map {|image| Spotify::SDK::Image.new(image, parent) }
95
+ end
96
+
97
+ ##
98
+ # Display the artist's popularity. If not obtained, request them from the API.
99
+ #
100
+ # @example
101
+ # artist = @sdk.connect.playback.artist
102
+ # artist.popularity # => 90
103
+ #
104
+ # @return [Integer] popularity The number of popularity, between 0-100.
105
+ #
106
+ def popularity
107
+ retrieve_full_information! unless full_information?
108
+ super
109
+ end
110
+
111
+ ##
112
+ # Display the artist's genres. If not obtained, request them from the API.
113
+ #
114
+ # @example
115
+ # artist = @sdk.connect.playback.artist
116
+ # artist.genres # => ["hip hop", "pop rap", "rap", ...]
117
+ #
118
+ # @return [Array] genres An array of genres, denoted in strings.
119
+ #
120
+ def genres
121
+ retrieve_full_information! unless full_information?
122
+ super
123
+ end
124
+
125
+ ##
126
+ # Return the Spotify URL for this artist.
127
+ #
128
+ # @example
129
+ # artist = @sdk.connect.playback.artist
130
+ # artist.spotify_url # => "https://open.spotify.com/artist/..."
131
+ #
132
+ # @return [String] spotify_url The URL to open this artist on open.spotify.com
133
+ #
134
+ def spotify_url
135
+ external_urls[:spotify]
136
+ end
137
+
138
+ ##
139
+ # Return the Spotify URI for this artist.
140
+ #
141
+ # @example
142
+ # artist = @sdk.connect.playback.artist
143
+ # artist.spotify_uri # => "spotify:uri:..."
144
+ #
145
+ # @return [String] spotify_uri The URI to open this artist in official apps.
146
+ #
147
+ alias_attribute :spotify_uri, :uri
148
+
149
+ ##
150
+ # Return the followers on Spotify for this artist.
151
+ #
152
+ # @example
153
+ # artist = @sdk.connect.playback.artist
154
+ # artist.followers # => 13913
155
+ #
156
+ # @return [Integer] followers The number of users following this artist.
157
+ #
158
+ def followers
159
+ super[:total]
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotify
4
+ class SDK
5
+ ##
6
+ # For each SDK component, we have a Base class. We're using HTTParty.
7
+ #
8
+ class Base
9
+ include HTTParty
10
+ base_uri "api.spotify.com:443"
11
+
12
+ ##
13
+ # Initiate a Spotify SDK Base component.
14
+ #
15
+ # @example
16
+ # @sdk = Spotify::SDK.new(@session)
17
+ # @auth = Spotify::SDK::Base.new(@sdk)
18
+ #
19
+ # @sdk = Spotify::SDK.new(@session)
20
+ # @sdk.to_hash # => { access_token: ..., expires_at: ... }
21
+ #
22
+ # @param [Spotify::SDK] parent An instance of Spotify::SDK as a reference point.
23
+ #
24
+ def initialize(parent)
25
+ @parent = parent
26
+ @options = {
27
+ headers: {
28
+ Authorization: "Bearer %s" % @parent.session.access_token
29
+ }
30
+ }
31
+ end
32
+
33
+ ##
34
+ # Handle HTTParty responses.
35
+ #
36
+ # @example
37
+ # # Return the Hash from the JSON response.
38
+ # send_http_request(:get, "/v1/me/player/devices", @options)
39
+ #
40
+ # # Return the raw HTTParty::Response object.
41
+ # send_http_request(:get, "/v1/me/player/devices", @options.merge({http_options: { raw: true }}))
42
+ #
43
+ # # Return true for HTTP requests that return a 200 OK with an empty response.
44
+ # send_http_request(:put, "/v1/me/player/pause", @options.merge({http_options: { expect_nil: true }}))
45
+ #
46
+ # @param [Symbol] method The HTTP method you want to perform. Examples are :get, :post, :put, :delete
47
+ # @param [String] endpoint The HTTP endpoint you'd like to call. Example: /v1/me
48
+ # @param [Hash] override_opts Any headers, HTTParty config or application-specific config (see `http_options`)
49
+ # @return [Hash,HTTParty::Response,TrueClass] response The response from the HTTP request.
50
+ #
51
+ # TODO: Address and fix cyclomatic & code complexity issues by Rubocop.
52
+ # rubocop:disable CyclomaticComplexity, PerceivedComplexity, AbcSize
53
+ def send_http_request(method, endpoint, override_opts={})
54
+ opts = {
55
+ raw: false,
56
+ expect_nil: false
57
+ }.merge(override_opts[:http_options].presence || {})
58
+
59
+ httparty = self.class.send(method, endpoint, @options.merge(override_opts))
60
+ response = httparty.parsed_response
61
+ response = response.try(:deep_symbolize_keys) || response
62
+ raise response[:error][:message] if response.is_a?(Hash) && response[:error].present?
63
+ return httparty if opts[:raw] == true
64
+
65
+ response = opts[:expect_nil] ? true : raise("No response returned") if response.nil?
66
+ response
67
+ end
68
+ # rubocop:enable CyclomaticComplexity, PerceivedComplexity, AbcSize
69
+
70
+ def inspect # :nodoc:
71
+ "#<%s:0x00%x>" % [self.class.name, (object_id << 1)]
72
+ end
73
+
74
+ attr_reader :parent
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotify
4
+ class SDK
5
+ class Connect < Base
6
+ ##
7
+ # Get the current playback.
8
+ # GET /v1/me/player
9
+ #
10
+ # @example
11
+ # playback = @sdk.connect.playback
12
+ #
13
+ # @see https://developer.spotify.com/console/get-user-player/
14
+ # @see https://developer.spotify.com/documentation/web-api/reference/player/get-information-about-the-users-current-playback/
15
+ #
16
+ # @param [String] market The market you'd like to request.
17
+ # @param [Hash] override_opts Custom options for HTTParty.
18
+ # @return [Spotify::SDK::Connect::PlaybackState] playback_state Return the playback state object.
19
+ #
20
+ def playback(market="from_token", override_opts={})
21
+ playback_state = send_http_request(:get, "/v1/me/player?market=%s" % market, override_opts)
22
+ Spotify::SDK::Connect::PlaybackState.new(playback_state, self)
23
+ end
24
+
25
+ ##
26
+ # Collect all the user's available devices.
27
+ # GET /v1/me/player/devices
28
+ #
29
+ # @example
30
+ # @sdk.connect.devices # => [#<Spotify::SDK::Connect::Device:...>, ...]
31
+ #
32
+ # @see https://developer.spotify.com/console/get-users-available-devices/
33
+ #
34
+ # @param [Hash] override_opts Custom options for HTTParty.
35
+ # @return [Array] devices A list of all devices.
36
+ #
37
+ def devices(override_opts={})
38
+ response = send_http_request(:get, "/v1/me/player/devices", override_opts)
39
+ response[:devices].map do |device|
40
+ Spotify::SDK::Connect::Device.new(device, self)
41
+ end
42
+ end
43
+
44
+ ##
45
+ # Collect all the active devices.
46
+ #
47
+ # @example
48
+ # @sdk.connect.active_devices # => [#<Spotify::SDK::Connect::Device:...>, ...]
49
+ #
50
+ # @see https://developer.spotify.com/console/get-users-available-devices/
51
+ #
52
+ # @param [Hash] override_opts Custom options for HTTParty.
53
+ # @return [Array] devices A list of all devices that are marked as `is_active`.
54
+ #
55
+ def active_devices(override_opts={})
56
+ devices(override_opts).select(&:active?)
57
+ end
58
+
59
+ ##
60
+ # Collect the first active device.
61
+ #
62
+ # @example
63
+ # @sdk.connect.active_device # => #<Spotify::SDK::Connect::Device:...>
64
+ #
65
+ # @see https://developer.spotify.com/console/get-users-available-devices/
66
+ #
67
+ # @param [Hash] override_opts Custom options for HTTParty.
68
+ # @return [Array,NilClass] device The first device with `is_active`. If no device found, returns `nil`.
69
+ #
70
+ def active_device(override_opts={})
71
+ devices(override_opts).find(&:active?)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotify
4
+ class SDK
5
+ class Connect
6
+ class Device < Model
7
+ ##
8
+ # Get the device's volume.
9
+ #
10
+ # @example
11
+ # device = @sdk.connect.devices[0]
12
+ # device.volume
13
+ #
14
+ # @return [Integer] volume Get the volume. Between 0 and 100.
15
+ #
16
+ alias_attribute :volume, :volume_percent
17
+
18
+ ##
19
+ # Is the device active?
20
+ #
21
+ # @example
22
+ # device = @sdk.connect.devices[0]
23
+ # device.active?
24
+ #
25
+ # @return [Boolean] is_active Bool of whether device is active.
26
+ #
27
+ alias_attribute :active?, :is_active
28
+
29
+ ##
30
+ # Is the device's session private?
31
+ #
32
+ # @example
33
+ # device = @sdk.connect.devices[0]
34
+ # device.private_session?
35
+ #
36
+ # @return [Boolean] is_private_session Bool of whether device has a private session.
37
+ #
38
+ alias_attribute :private_session?, :is_private_session
39
+
40
+ ##
41
+ # Is the device restricted?
42
+ #
43
+ # @example
44
+ # device = @sdk.connect.devices[0]
45
+ # device.restricted?
46
+ #
47
+ # @return [Boolean] is_restricted Bool of whether device is restricted.
48
+ #
49
+ alias_attribute :restricted?, :is_restricted
50
+
51
+ ##
52
+ # Get the currently playing track.
53
+ # Alias to Spotify::SDK::Connect#playback
54
+ #
55
+ # @example
56
+ # device = @sdk.connect.devices[0]
57
+ # device.playback
58
+ #
59
+ # # Same as calling the following:
60
+ # @sdk.connect.playback
61
+ #
62
+ # @see lib/spotify/sdk/connect.rb
63
+ #
64
+ # @return [Spotify::SDK::Connect::PlaybackState] self Return the playback state object.
65
+ #
66
+ def playback
67
+ parent.playback
68
+ end
69
+
70
+ ##
71
+ # Play the currently playing track on device.
72
+ # PUT /v1/me/player/play
73
+ #
74
+ # @example
75
+ # device = @sdk.connect.devices[0]
76
+ #
77
+ # # Play from a playlist, album from a specific index in that list.
78
+ # # For example, play the 9th item on X playlist.
79
+ # device.play!(
80
+ # index: 5,
81
+ # context: "spotify:album:5ht7ItJgpBH7W6vJ5BqpPr",
82
+ # position_ms: 0
83
+ # )
84
+ #
85
+ # # Play any Spotify URI. Albums, artists, tracks, playlists, and more.
86
+ # device.play!(
87
+ # uri: "spotify:track:5MqkZd7a7u7N7hKMqquL2U",
88
+ # position_ms: 0
89
+ # )
90
+ #
91
+ # # Similar to just uri, but you can define the context.
92
+ # # Useful for playing a track that is part of a playlist, and you want the next
93
+ # # songs to play from that particular context.
94
+ # device.play!(
95
+ # uri: "spotify:track:5MqkZd7a7u7N7hKMqquL2U",
96
+ # context: "spotify:album:5ht7ItJgpBH7W6vJ5BqpPr",
97
+ # position_ms: 0
98
+ # )
99
+ #
100
+ # # Play a track, and immediately seek to 60 seconds.
101
+ # device.play!(
102
+ # index: 5,
103
+ # context: "spotify:album:5ht7ItJgpBH7W6vJ5BqpPr",
104
+ # position_ms: 60 * 1000
105
+ # )
106
+ #
107
+ # @see https://developer.spotify.com/console/put-play/
108
+ #
109
+ # @param [Hash] config The play config you'd like to set. See code examples.
110
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
111
+ #
112
+ # rubocop:disable AbcSize
113
+ def play!(config)
114
+ payload = case config.keys
115
+ when %i[index context position_ms]
116
+ {context_uri: config[:context],
117
+ offset: {position: config[:index]},
118
+ position_ms: config[:position_ms]}
119
+ when %i[uri position_ms]
120
+ {uris: [config[:uri]],
121
+ position_ms: config[:position_ms]}
122
+ when %i[uri context position_ms]
123
+ {context_uri: config[:context],
124
+ offset: {uri: config[:uri]},
125
+ position_ms: config[:position_ms]}
126
+ else
127
+ raise <<-ERROR.strip_heredoc.strip
128
+ Unrecognized play instructions.
129
+ See https://www.rubydoc.info/github/bih/spotify-ruby/Spotify/SDK/Connect/Device#play!-instance_method for details.
130
+ ERROR
131
+ end
132
+
133
+ parent.send_http_request(:put, "/v1/me/player/play?device_id=%s" % id, http_options: {expect_nil: true},
134
+ body: payload.to_json)
135
+ self
136
+ end
137
+ # rubocop:enable AbcSize
138
+
139
+ ##
140
+ # Resume the currently playing track on device.
141
+ # PUT /v1/me/player/play
142
+ #
143
+ # @example
144
+ # device = @sdk.connect.devices[0]
145
+ # device.resume!
146
+ #
147
+ # @see https://developer.spotify.com/console/put-play/
148
+ #
149
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
150
+ #
151
+ def resume!
152
+ parent.send_http_request(:put, "/v1/me/player/play?device_id=%s" % id, http_options: {expect_nil: true})
153
+ self
154
+ end
155
+
156
+ ##
157
+ # Pause the currently playing track on device.
158
+ # PUT /v1/me/player/pause
159
+ #
160
+ # @example
161
+ # device = @sdk.connect.devices[0]
162
+ # device.pause!
163
+ #
164
+ # @see https://developer.spotify.com/console/put-pause/
165
+ #
166
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
167
+ #
168
+ def pause!
169
+ parent.send_http_request(:put, "/v1/me/player/pause?device_id=%s" % id, http_options: {expect_nil: true})
170
+ self
171
+ end
172
+
173
+ ##
174
+ # Skip to previous track on device.
175
+ # POST /v1/me/player/previous
176
+ #
177
+ # @example
178
+ # device = @sdk.connect.devices[0]
179
+ # device.previous!
180
+ #
181
+ # @see https://developer.spotify.com/console/put-previous/
182
+ #
183
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
184
+ #
185
+ def previous!
186
+ parent.send_http_request(:post, "/v1/me/player/previous?device_id=%s" % id, http_options: {expect_nil: true})
187
+ self
188
+ end
189
+
190
+ ##
191
+ # Skip to next track on device.
192
+ # POST /v1/me/player/next
193
+ #
194
+ # @example
195
+ # device = @sdk.connect.devices[0]
196
+ # device.next!
197
+ #
198
+ # @see https://developer.spotify.com/console/put-next/
199
+ #
200
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
201
+ #
202
+ def next!
203
+ parent.send_http_request(:post, "/v1/me/player/next?device_id=%s" % id, http_options: {expect_nil: true})
204
+ self
205
+ end
206
+
207
+ ##
208
+ # Set volume for current device.
209
+ # PUT /v1/me/player/volume
210
+ #
211
+ # @example
212
+ # device = @sdk.connect.devices[0]
213
+ # device.change_volume!(30)
214
+ #
215
+ # # or
216
+ #
217
+ # device = @sdk.connect.devices[0]
218
+ # device.volume = 30
219
+ #
220
+ # @see https://developer.spotify.com/console/put-volume/
221
+ #
222
+ # @param [Integer] volume_percent The 0-100 value to change the volume to. 100 is maximum.
223
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
224
+ #
225
+ def change_volume!(volume_percent)
226
+ raise "Must be an integer" unless volume_percent.is_a?(Integer)
227
+
228
+ endpoint = "/v1/me/player/volume?volume_percent=%i&device_id=%s" % [volume_percent, id]
229
+ opts = {http_options: {expect_nil: true}}
230
+ parent.send_http_request(:put, endpoint, opts)
231
+ self
232
+ end
233
+
234
+ alias_method :volume=, :change_volume!
235
+
236
+ ##
237
+ # Seek position (in milliseconds) for the currently playing track on the device.
238
+ # PUT /v1/me/player/seek
239
+ #
240
+ # @example
241
+ # device = @sdk.connect.devices[0]
242
+ # device.seek_ms!(4000)
243
+ #
244
+ # @see https://developer.spotify.com/console/put-seek/
245
+ #
246
+ # @param [Integer] position_ms In milliseconds, where to seek in the current track on device.
247
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
248
+ #
249
+ def seek_ms!(position_ms)
250
+ raise "Must be an integer" unless position_ms.is_a?(Integer)
251
+
252
+ endpoint = "/v1/me/player/seek?position_ms=%i&device_id=%s" % [position_ms, id]
253
+ opts = {http_options: {expect_nil: true}}
254
+ parent.send_http_request(:put, endpoint, opts)
255
+ self
256
+ end
257
+
258
+ alias_method :position_ms=, :seek_ms!
259
+
260
+ ##
261
+ # Set repeat mode for current device.
262
+ # PUT /v1/me/player/repeat
263
+ #
264
+ # @example
265
+ # device = @sdk.connect.devices[0]
266
+ # device.repeat!(:track)
267
+ # device.repeat!(:context)
268
+ # device.repeat!(:off)
269
+ #
270
+ # @see https://developer.spotify.com/console/put-repeat/
271
+ #
272
+ # @param [Boolean] state What to set the repeat state to - :track, :context, or :off
273
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
274
+ #
275
+ def repeat!(state)
276
+ raise "Must be :track, :context, or :off" unless %i[track context off].include?(state)
277
+
278
+ endpoint = "/v1/me/player/repeat?state=%s&device_id=%s" % [state, id]
279
+ opts = {http_options: {expect_nil: true}}
280
+ parent.send_http_request(:put, endpoint, opts)
281
+ self
282
+ end
283
+
284
+ alias_method :repeat=, :repeat!
285
+
286
+ ##
287
+ # Set shuffle for current device.
288
+ # PUT /v1/me/player/shuffle
289
+ #
290
+ # @example
291
+ # device = @sdk.connect.devices[0]
292
+ # device.shuffle!(true)
293
+ #
294
+ # @see https://developer.spotify.com/console/put-shuffle/
295
+ #
296
+ # @param [Boolean] state The true/false of if you'd like to set shuffle on.
297
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
298
+ #
299
+ def shuffle!(state)
300
+ raise "Must be true or false" unless [true, false].include?(state)
301
+
302
+ endpoint = "/v1/me/player/shuffle?state=%s&device_id=%s" % [state, id]
303
+ opts = {http_options: {expect_nil: true}}
304
+ parent.send_http_request(:put, endpoint, opts)
305
+ self
306
+ end
307
+
308
+ alias_method :shuffle=, :shuffle!
309
+
310
+ ##
311
+ # Transfer a user's playback to another device, and continue playing.
312
+ # PUT /v1/me/player
313
+ #
314
+ # @example
315
+ # device = @sdk.connect.devices[0]
316
+ # device.transfer_playback!
317
+ #
318
+ # @see https://developer.spotify.com/console/transfer-a-users-playback/
319
+ #
320
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
321
+ #
322
+ def transfer_playback!
323
+ transfer_playback_method(playing: true)
324
+ self
325
+ end
326
+
327
+ ##
328
+ # Transfer a user's playback to another device, and pause.
329
+ # PUT /v1/me/player
330
+ #
331
+ # @example
332
+ # device = @sdk.connect.devices[0]
333
+ # device.transfer_state!
334
+ #
335
+ # @see https://developer.spotify.com/console/transfer-a-users-playback/
336
+ #
337
+ # @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
338
+ #
339
+ def transfer_state!
340
+ transfer_playback_method(playing: false)
341
+ self
342
+ end
343
+
344
+ private
345
+
346
+ def transfer_playback_method(playing:) # :nodoc:
347
+ override_opts = {
348
+ http_options: {
349
+ expect_nil: true
350
+ },
351
+ body: {
352
+ device_ids: [id],
353
+ play: playing
354
+ }.to_json
355
+ }
356
+
357
+ parent.send_http_request(:put, "/v1/me/player", override_opts)
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end