spotify-client 1.1.2 → 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 +24 -6
- data/lib/spotify/client.rb +15 -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,
|
|
@@ -47,7 +48,7 @@ module Spotify
|
|
|
47
48
|
headers: {
|
|
48
49
|
'Content-Type' => 'application/json',
|
|
49
50
|
'User-Agent' => "spotify-client/#{Spotify::VERSION} (Ruby)"
|
|
50
|
-
}
|
|
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
|
@@ -86,7 +86,8 @@ module Spotify
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def upload_playlist_cover_image(playlist_id, image_base64_jpeg)
|
|
89
|
-
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' })
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
def album(album_id)
|
|
@@ -165,10 +166,11 @@ module Spotify
|
|
|
165
166
|
# +country_id+ is required. An ISO 3166-1 alpha-2 country code.
|
|
166
167
|
def artist_top_tracks(artist_id, country_id)
|
|
167
168
|
raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/top-tracks')
|
|
168
|
-
run(:get, "/v1/artists/#{artist_id}/top-tracks", [200],
|
|
169
|
+
run(:get, "/v1/artists/#{artist_id}/top-tracks", [200], { market: country_id })
|
|
169
170
|
end
|
|
170
171
|
|
|
171
172
|
def related_artists(artist_id)
|
|
173
|
+
raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/related-artists')
|
|
172
174
|
run(:get, "/v1/artists/#{artist_id}/related-artists", [200])
|
|
173
175
|
end
|
|
174
176
|
|
|
@@ -191,7 +193,7 @@ module Spotify
|
|
|
191
193
|
end
|
|
192
194
|
|
|
193
195
|
def playback_state(params = {})
|
|
194
|
-
run(:get, '/v1/me/player', [200], params)
|
|
196
|
+
run(:get, '/v1/me/player', [200, 204], params)
|
|
195
197
|
end
|
|
196
198
|
|
|
197
199
|
def available_devices
|
|
@@ -245,11 +247,11 @@ module Spotify
|
|
|
245
247
|
end
|
|
246
248
|
|
|
247
249
|
def add_to_library(uris)
|
|
248
|
-
run(:put, '/v1/me/library', [200, 204],
|
|
250
|
+
run(:put, '/v1/me/library', [200, 204], library_query(uris), false)
|
|
249
251
|
end
|
|
250
252
|
|
|
251
253
|
def remove_from_library(uris)
|
|
252
|
-
run(:delete, '/v1/me/library', [200, 204],
|
|
254
|
+
run(:delete, '/v1/me/library', [200, 204], library_query(uris), false)
|
|
253
255
|
end
|
|
254
256
|
|
|
255
257
|
# Generic API helper for forward compatibility with newly added endpoints.
|
|
@@ -276,10 +278,17 @@ module Spotify
|
|
|
276
278
|
|
|
277
279
|
protected
|
|
278
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
|
+
|
|
279
288
|
def raise_endpoint_unavailable_in_development_mode!(endpoint:, replacement: nil)
|
|
280
289
|
return unless development_mode?
|
|
281
290
|
|
|
282
|
-
message = "#{endpoint} is unavailable
|
|
291
|
+
message = "#{endpoint} is unavailable under the configured Spotify Development Mode restrictions."
|
|
283
292
|
message += " Use #{replacement} instead." if replacement
|
|
284
293
|
raise(EndpointUnavailableInDevelopmentMode, message)
|
|
285
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: []
|