spotify-client 1.1.1 → 2.0.0
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/README.md +35 -12
- data/lib/spotify/client/compatibility_api.rb +4 -1
- data/lib/spotify/client/transport.rb +25 -7
- data/lib/spotify/client.rb +16 -6
- data/lib/spotify/exceptions.rb +11 -0
- data/lib/spotify/version.rb +1 -1
- data/spotify-client.gemspec +2 -2
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de40fcc8655fc96d463e86606cfe0caa6191fc5c4920323fa4fa6b38c89aa936
|
|
4
|
+
data.tar.gz: 6048f1d14fe1ad56d500815a498176f1e6eed4e1449a0f873ea142404878f290
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7183df6b108f55a76a6e000928888b2f46dca1bcf9921c527e2150f7a5c75feceaf892539a49560d0d07efe40e8028c6fd5aa6513937d9815a38c04de12e256c
|
|
7
|
+
data.tar.gz: 24b8d34991865ea0b8d8b2c6111961cda0e5cdd8acead3105c130844075f3d6c0f283bfcdce429e58dec52f24c2572b2f41a0e0701982161be710458f8eb3c51
|
data/README.md
CHANGED
|
@@ -27,14 +27,14 @@ gem install spotify-client
|
|
|
27
27
|
|
|
28
28
|
## Supported Ruby Versions
|
|
29
29
|
|
|
30
|
-
The CI matrix runs
|
|
30
|
+
Ruby `3.3` or newer is required. The CI matrix runs against Ruby `3.3`, `3.4`, `4.0`, and `ruby-head`, with both released Excon and its development branch.
|
|
31
31
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
35
|
config = {
|
|
36
36
|
access_token: 'tk',
|
|
37
|
-
app_mode: :development, # optional;
|
|
37
|
+
app_mode: :development, # optional; opt into guards for restricted Development Mode endpoints
|
|
38
38
|
raise_errors: true,
|
|
39
39
|
retries: 0,
|
|
40
40
|
read_timeout: 10,
|
|
@@ -128,30 +128,53 @@ client.request(:get, '/v1/me', [200], { market: 'IT' }) # GET-style hash payload
|
|
|
128
128
|
client.request!(:post, '/v1/some-endpoint', [201], payload, false) # non-GET hash payloads are JSON encoded
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
## Spotify API
|
|
131
|
+
## Spotify API Compatibility
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
The API contracts were checked against Spotify's official documentation on November 15, 2026. The latest listed Web API changelog is July 2026.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
- `
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
| Change | Client behavior |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| [February: Development Mode migration](https://developer.spotify.com/documentation/web-api/references/changes/february-2026) | Playlist helpers use `/v1/me/playlists` and `/v1/playlists/{id}/items`. Library helpers and legacy follow helpers use `/v1/me/library`. |
|
|
138
|
+
| [March: restored external IDs](https://developer.spotify.com/documentation/web-api/references/changes/march-2026) | Album and track responses retain `external_ids` unchanged. |
|
|
139
|
+
| [May: stable account identifier](https://developer.spotify.com/documentation/web-api/references/changes/may-2026) | `me` retains `account_id`. Use this immutable identifier when linking accounts to your service. |
|
|
140
|
+
| [June: refresh-token expiration](https://developer.spotify.com/blog/2026-06-18-refresh-token-expiration) | OAuth remains the caller's responsibility. Refresh tokens expire six months after the original authorization; refreshing does not extend their lifetime. Handle `invalid_grant` by asking the user to authorize again. The change applied to existing apps from July 20, 2026. |
|
|
141
|
+
| [July: shared development quotas](https://developer.spotify.com/documentation/web-api/references/changes/july-2026) | HTTP 429 raises `Spotify::RateLimitError` when errors are enabled, exposing `reason` and `retry_after`. Development Mode Client IDs share their developer account's quota. |
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
Response JSON is returned without filtering fields. Account permissions, quota mode, scopes, and playlist ownership still determine which data Spotify returns.
|
|
144
|
+
|
|
145
|
+
`app_mode: :development` opts into the restricted endpoint contract: `user`, `artist_top_tracks`, and `related_artists` fail before sending a request. Omit this option for integrations that retain access to those endpoints. Spotify [postponed the endpoint-access migration for existing Development Mode integrations](https://developer.spotify.com/blog/2026-02-06-update-on-developer-access-and-platform-security); the February changes do not apply to Extended Quota Mode. `related_artists` is deprecated and only remains usable by eligible integrations.
|
|
146
|
+
|
|
147
|
+
Library save/remove calls follow the [current endpoint reference](https://developer.spotify.com/documentation/web-api/reference/save-library-items): comma-separated `uris` in the query, with at most 40 items per call. Spotify's February migration guide also shows a JSON-body example. Playlist additions accept at most 100 URI strings as a JSON array. Oversized batches raise `Spotify::ImplementationError`; split them into multiple calls.
|
|
148
|
+
|
|
149
|
+
### Rate and quota errors
|
|
150
|
+
|
|
151
|
+
Enable `raise_errors: true` or use `request!` to inspect errors. With the default `raise_errors: false`, non-bang methods continue to return `false` on HTTP errors.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
begin
|
|
155
|
+
client.request!(:get, '/v1/me')
|
|
156
|
+
rescue Spotify::RateLimitError => error
|
|
157
|
+
error.reason # "QUOTA_EXCEEDED" for the July quota response, otherwise nil
|
|
158
|
+
error.retry_after # Spotify's Retry-After header as a string, or nil
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`RateLimitError` inherits from `HTTPError`, so existing rescues still work. With the default `retries: 0`, a 429 response is returned immediately. Configured Excon retries can retry idempotent requests before exposing the final error. The client does not schedule retries from `Retry-After`; callers decide how to handle the supplied reason and retry interval.
|
|
142
163
|
|
|
143
164
|
## Development
|
|
144
165
|
|
|
145
|
-
|
|
166
|
+
Use the Ruby version in `.ruby-version`, install dependencies, and run checks:
|
|
146
167
|
|
|
147
168
|
```bash
|
|
148
169
|
bundle install
|
|
149
170
|
bundle exec rake
|
|
150
171
|
```
|
|
151
172
|
|
|
173
|
+
The suite enables Ruby warnings and requires 100% line coverage overall and per file.
|
|
174
|
+
|
|
152
175
|
## Release
|
|
153
176
|
|
|
154
|
-
|
|
177
|
+
The gem version is defined in `lib/spotify/version.rb`. Push a matching `v`-prefixed tag to trigger the release workflow and publish the gem.
|
|
155
178
|
|
|
156
179
|
## License
|
|
157
180
|
|
|
@@ -45,7 +45,10 @@ module Spotify
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def add_user_tracks_to_playlist(_user_id, playlist_id, uris = [], position = nil)
|
|
48
|
-
|
|
48
|
+
items = Array(uris)
|
|
49
|
+
raise(ImplementationError, 'playlist additions accept at most 100 URIs') if items.length > 100
|
|
50
|
+
|
|
51
|
+
params = { uris: items }
|
|
49
52
|
params.merge!(position: position) if position
|
|
50
53
|
run(:post, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(params), false)
|
|
51
54
|
end
|
|
@@ -14,20 +14,21 @@ module Spotify
|
|
|
14
14
|
|
|
15
15
|
protected
|
|
16
16
|
|
|
17
|
-
def run(verb, path, expected_status_codes, params = {}, idempotent = true)
|
|
18
|
-
run!(verb, path, expected_status_codes, params, idempotent)
|
|
17
|
+
def run(verb, path, expected_status_codes, params = {}, idempotent = true, headers: {})
|
|
18
|
+
run!(verb, path, expected_status_codes, params, idempotent, headers: headers)
|
|
19
19
|
rescue Error => e
|
|
20
20
|
handle_nonbang_error(e)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def run!(verb, path, expected_status_codes, params_or_body = nil, idempotent = true)
|
|
23
|
+
def run!(verb, path, expected_status_codes, params_or_body = nil, idempotent = true, headers: {})
|
|
24
24
|
response = @connection.request(
|
|
25
25
|
build_request_packet(
|
|
26
26
|
verb: verb,
|
|
27
27
|
path: path,
|
|
28
28
|
expected_status_codes: expected_status_codes,
|
|
29
29
|
params_or_body: params_or_body,
|
|
30
|
-
idempotent: idempotent
|
|
30
|
+
idempotent: idempotent,
|
|
31
|
+
headers: headers
|
|
31
32
|
)
|
|
32
33
|
)
|
|
33
34
|
parse_response_body(response)
|
|
@@ -35,7 +36,7 @@ module Spotify
|
|
|
35
36
|
raise map_transport_error(e)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
def build_request_packet(verb:, path:, expected_status_codes:, params_or_body:, idempotent:)
|
|
39
|
+
def build_request_packet(verb:, path:, expected_status_codes:, params_or_body:, idempotent:, headers: {})
|
|
39
40
|
packet = {
|
|
40
41
|
idempotent: idempotent,
|
|
41
42
|
expects: expected_status_codes,
|
|
@@ -46,8 +47,8 @@ module Spotify
|
|
|
46
47
|
retry_limit: @retries,
|
|
47
48
|
headers: {
|
|
48
49
|
'Content-Type' => 'application/json',
|
|
49
|
-
'User-Agent' =>
|
|
50
|
-
}
|
|
50
|
+
'User-Agent' => "spotify-client/#{Spotify::VERSION} (Ruby)"
|
|
51
|
+
}.merge(headers)
|
|
51
52
|
}
|
|
52
53
|
apply_request_payload(packet, params_or_body)
|
|
53
54
|
|
|
@@ -85,12 +86,29 @@ module Spotify
|
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def map_transport_error(error)
|
|
89
|
+
if error.is_a?(Excon::Errors::TooManyRequests)
|
|
90
|
+
return RateLimitError.new("Error: #{error.message}",
|
|
91
|
+
reason: rate_limit_reason(error.response.body),
|
|
92
|
+
retry_after: error.response.headers['Retry-After'])
|
|
93
|
+
end
|
|
94
|
+
|
|
88
95
|
error_class = EXCON_ERROR_MAP.find do |transport_error, _client_error|
|
|
89
96
|
error.is_a?(transport_error)
|
|
90
97
|
end&.last || HTTPError
|
|
91
98
|
|
|
92
99
|
error_class.new("Error: #{error.message}")
|
|
93
100
|
end
|
|
101
|
+
|
|
102
|
+
def rate_limit_reason(body)
|
|
103
|
+
case JSON.parse(body.to_s, symbolize_names: true)
|
|
104
|
+
in { error: { reason: String => reason } }
|
|
105
|
+
reason
|
|
106
|
+
else
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
rescue JSON::ParserError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
94
112
|
end
|
|
95
113
|
end
|
|
96
114
|
end
|
data/lib/spotify/client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'excon'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
6
|
require 'spotify/exceptions'
|
|
7
|
+
require 'spotify/version'
|
|
7
8
|
require 'spotify/client/compatibility_api'
|
|
8
9
|
require 'spotify/client/transport'
|
|
9
10
|
|
|
@@ -85,7 +86,8 @@ module Spotify
|
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def upload_playlist_cover_image(playlist_id, image_base64_jpeg)
|
|
88
|
-
run(:put, "/v1/playlists/#{playlist_id}/images", [200, 202, 204], image_base64_jpeg.to_s, false
|
|
89
|
+
run(:put, "/v1/playlists/#{playlist_id}/images", [200, 202, 204], image_base64_jpeg.to_s, false,
|
|
90
|
+
headers: { 'Content-Type' => 'image/jpeg' })
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def album(album_id)
|
|
@@ -164,10 +166,11 @@ module Spotify
|
|
|
164
166
|
# +country_id+ is required. An ISO 3166-1 alpha-2 country code.
|
|
165
167
|
def artist_top_tracks(artist_id, country_id)
|
|
166
168
|
raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/top-tracks')
|
|
167
|
-
run(:get, "/v1/artists/#{artist_id}/top-tracks", [200],
|
|
169
|
+
run(:get, "/v1/artists/#{artist_id}/top-tracks", [200], { market: country_id })
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
def related_artists(artist_id)
|
|
173
|
+
raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/related-artists')
|
|
171
174
|
run(:get, "/v1/artists/#{artist_id}/related-artists", [200])
|
|
172
175
|
end
|
|
173
176
|
|
|
@@ -190,7 +193,7 @@ module Spotify
|
|
|
190
193
|
end
|
|
191
194
|
|
|
192
195
|
def playback_state(params = {})
|
|
193
|
-
run(:get, '/v1/me/player', [200], params)
|
|
196
|
+
run(:get, '/v1/me/player', [200, 204], params)
|
|
194
197
|
end
|
|
195
198
|
|
|
196
199
|
def available_devices
|
|
@@ -244,11 +247,11 @@ module Spotify
|
|
|
244
247
|
end
|
|
245
248
|
|
|
246
249
|
def add_to_library(uris)
|
|
247
|
-
run(:put, '/v1/me/library', [200, 204],
|
|
250
|
+
run(:put, '/v1/me/library', [200, 204], library_query(uris), false)
|
|
248
251
|
end
|
|
249
252
|
|
|
250
253
|
def remove_from_library(uris)
|
|
251
|
-
run(:delete, '/v1/me/library', [200, 204],
|
|
254
|
+
run(:delete, '/v1/me/library', [200, 204], library_query(uris), false)
|
|
252
255
|
end
|
|
253
256
|
|
|
254
257
|
# Generic API helper for forward compatibility with newly added endpoints.
|
|
@@ -275,10 +278,17 @@ module Spotify
|
|
|
275
278
|
|
|
276
279
|
protected
|
|
277
280
|
|
|
281
|
+
def library_query(uris)
|
|
282
|
+
items = Array(uris)
|
|
283
|
+
raise(ImplementationError, 'library requests accept at most 40 URIs') if items.length > 40
|
|
284
|
+
|
|
285
|
+
{ uris: items.join(',') }
|
|
286
|
+
end
|
|
287
|
+
|
|
278
288
|
def raise_endpoint_unavailable_in_development_mode!(endpoint:, replacement: nil)
|
|
279
289
|
return unless development_mode?
|
|
280
290
|
|
|
281
|
-
message = "#{endpoint} is unavailable
|
|
291
|
+
message = "#{endpoint} is unavailable under the configured Spotify Development Mode restrictions."
|
|
282
292
|
message += " Use #{replacement} instead." if replacement
|
|
283
293
|
raise(EndpointUnavailableInDevelopmentMode, message)
|
|
284
294
|
end
|
data/lib/spotify/exceptions.rb
CHANGED
|
@@ -4,6 +4,17 @@ module Spotify
|
|
|
4
4
|
class Error < StandardError; end
|
|
5
5
|
class AuthenticationError < Error; end
|
|
6
6
|
class HTTPError < Error; end
|
|
7
|
+
|
|
8
|
+
class RateLimitError < HTTPError
|
|
9
|
+
attr_reader :reason, :retry_after
|
|
10
|
+
|
|
11
|
+
def initialize(message, reason: nil, retry_after: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@reason = reason
|
|
14
|
+
@retry_after = retry_after
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
class InsufficientClientScopeError < Error; end
|
|
8
19
|
class BadRequest < Error; end
|
|
9
20
|
class ResourceNotFound < Error; end
|
data/lib/spotify/version.rb
CHANGED
data/spotify-client.gemspec
CHANGED
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.description = 'Lightweight Ruby client for the Spotify Web API with playlist and catalog helpers.'
|
|
17
17
|
spec.homepage = 'https://github.com/icoretech/spotify-client'
|
|
18
18
|
spec.license = 'MIT'
|
|
19
|
-
spec.required_ruby_version = '>= 3.
|
|
19
|
+
spec.required_ruby_version = '>= 3.3'
|
|
20
20
|
|
|
21
21
|
spec.metadata['source_code_uri'] = 'https://github.com/icoretech/spotify-client'
|
|
22
22
|
spec.metadata['bug_tracker_uri'] = 'https://github.com/icoretech/spotify-client/issues'
|
|
@@ -31,5 +31,5 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
]
|
|
32
32
|
spec.require_paths = ['lib']
|
|
33
33
|
|
|
34
|
-
spec.add_dependency 'excon', '>= 1.
|
|
34
|
+
spec.add_dependency 'excon', '>= 1.12', '< 2.0'
|
|
35
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spotify-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Poli
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.
|
|
18
|
+
version: '1.12'
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '2.0'
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '1.
|
|
28
|
+
version: '1.12'
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '2.0'
|
|
@@ -62,14 +62,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
62
62
|
requirements:
|
|
63
63
|
- - ">="
|
|
64
64
|
- !ruby/object:Gem::Version
|
|
65
|
-
version: '3.
|
|
65
|
+
version: '3.3'
|
|
66
66
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
67
|
requirements:
|
|
68
68
|
- - ">="
|
|
69
69
|
- !ruby/object:Gem::Version
|
|
70
70
|
version: '0'
|
|
71
71
|
requirements: []
|
|
72
|
-
rubygems_version: 4.0.
|
|
72
|
+
rubygems_version: 4.0.20
|
|
73
73
|
specification_version: 4
|
|
74
74
|
summary: Ruby client for the Spotify Web API
|
|
75
75
|
test_files: []
|