spotify-client 1.0.1 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa7eb1f393af143301f5f029d8b707c20c314752c8095c069e85980650dcde29
4
- data.tar.gz: 23e2e9669afae1cc5af7459493c14880028b963e11f8689a2d26502f15c852ef
3
+ metadata.gz: 8531ff35eb3c2054c9d4d99b6e97c5e22848e3805dae010d4be01841e3d535a6
4
+ data.tar.gz: f4775af30bd3ad47662b06ba8ebaefdef6b0d30378773e2ec520c54a4c672a36
5
5
  SHA512:
6
- metadata.gz: 6e5ee46bee109d75e9fc34898d711f171d0ef2f2c5bf87bf0a1f07480eb1d14d6ff5edd4dc3d6e82e635fad778c595b501c3165ab5df339351fc444141b18bf4
7
- data.tar.gz: 2296b443e46a7a481a3dfd264909722b9848145f7c21aba107fccd421089ce77cf231b55adcdb1c1beb2539c01b6a660a1370d2dbb50e05fc49d35f4e9a5023c
6
+ metadata.gz: 1892e985e4eff8f8f635e9438fc081832a65a6ac8717d73edaa17ee165219bdfcf29838e33b86ea6f06cf77489bedf418a90d06c2f114f1c5fc343e16615560a
7
+ data.tar.gz: 2ff791f2c9c431305935f27345f1aa4e0001c59eaa132a7d4f1d839b3030ee8d8090aa4ee5be0b8aa75492ec113a243e996b6bd119b81340892e2c593c7cc106
data/README.md CHANGED
@@ -33,6 +33,7 @@ The CI matrix runs this gem against Ruby `3.2`, `3.3`, `3.4`, `4.0`, and `ruby-h
33
33
  ```ruby
34
34
  config = {
35
35
  access_token: 'tk',
36
+ app_mode: :development, # optional; use :development to fail fast on dev-mode restricted endpoints
36
37
  raise_errors: true,
37
38
  retries: 0,
38
39
  read_timeout: 10,
@@ -80,11 +81,12 @@ client.request!(:post, '/v1/some-endpoint', [201], payload, false)
80
81
  Spotify's Web API changed and removed several legacy endpoints in 2026. This gem now uses current routes while keeping backward-compatible method signatures:
81
82
 
82
83
  - Playlist reads/writes use `/v1/me/playlists` and `/v1/playlists/{playlist_id}/*`.
83
- - `follow(type, ids)` keeps the same signature but now targets `/v1/me/library` (the `type` argument is ignored for compatibility).
84
- - `artist_top_tracks` now uses the top-songs route.
84
+ - `follow(type, ids)` now targets `/v1/me/library` using Spotify URIs (`spotify:{type}:{id}`), while still accepting prebuilt URIs.
85
+ - `artist_top_tracks` uses `/v1/artists/{id}/top-tracks`.
86
+ - `user(user_id)` and `artist_top_tracks` rely on endpoints that Spotify marks unavailable for Development Mode apps (still usable for Extended Quota Mode apps).
87
+ - If initialized with `app_mode: :development`, `user(user_id)` and `artist_top_tracks` raise `Spotify::EndpointUnavailableInDevelopmentMode` before making the HTTP request.
85
88
 
86
- - Changelog: [Spotify Web API Changelog](https://developer.spotify.com/documentation/web-api/concepts/changelog)
87
- - Migration guide: [Spotify Web API Migration Guide](https://developer.spotify.com/documentation/web-api/concepts/migration-guide)
89
+ - February 2026 changes: [Spotify Web API Changes](https://developer.spotify.com/documentation/web-api/references/changes/february-2026)
88
90
 
89
91
  ## Development
90
92
 
@@ -1,5 +1,6 @@
1
1
  module Spotify
2
2
  class ImplementationError < StandardError; end
3
+ class EndpointUnavailableInDevelopmentMode < ImplementationError; end
3
4
  class Error < StandardError; end
4
5
  class AuthenticationError < Error; end
5
6
  class HTTPError < Error; end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spotify
4
- VERSION = '1.0.1'
4
+ VERSION = '1.0.2'
5
5
  end
@@ -21,6 +21,7 @@ module Spotify
21
21
  @retries = config[:retries] || 0
22
22
  @read_timeout = config[:read_timeout] || 10
23
23
  @write_timeout = config[:write_timeout] || 10
24
+ @app_mode = config[:app_mode].to_s.strip.downcase
24
25
  @connection = Excon.new(BASE_URI, persistent: config[:persistent] || false)
25
26
  end
26
27
 
@@ -53,6 +54,10 @@ module Spotify
53
54
  end
54
55
 
55
56
  def user(user_id)
57
+ raise_endpoint_unavailable_in_development_mode!(
58
+ endpoint: 'GET /v1/users/{id}',
59
+ replacement: 'GET /v1/me'
60
+ )
56
61
  run(:get, "/v1/users/#{user_id}", [200])
57
62
  end
58
63
 
@@ -66,7 +71,7 @@ module Spotify
66
71
 
67
72
  def user_playlist_tracks(_user_id, playlist_id, params = {})
68
73
  tracks = { 'items' => [] }
69
- path = "/v1/playlists/#{playlist_id}/tracks"
74
+ path = "/v1/playlists/#{playlist_id}/items"
70
75
 
71
76
  while path
72
77
  response = run(:get, path, [200], params)
@@ -107,7 +112,7 @@ module Spotify
107
112
  # '1181346016', '7i3thJWDtmX04dJhFwYb0x', [{ uri: 'spotify:track:...', positions: [0] }]
108
113
  # )
109
114
  def remove_user_tracks_from_playlist(_user_id, playlist_id, tracks)
110
- run(:delete, "/v1/playlists/#{playlist_id}/tracks", [200], JSON.dump(tracks: tracks))
115
+ run(:delete, "/v1/playlists/#{playlist_id}/items", [200], JSON.dump(items: tracks))
111
116
  end
112
117
 
113
118
  # Replaces all occurrences of tracks with what's in the playlist
@@ -116,7 +121,7 @@ module Spotify
116
121
  # '1181346016', '7i3thJWDtmX04dJhFwYb0x', %w(spotify:track:... spotify:track:...)
117
122
  # )
118
123
  def replace_user_tracks_in_playlist(_user_id, playlist_id, tracks)
119
- run(:put, "/v1/playlists/#{playlist_id}/tracks", [200, 201], JSON.dump(uris: tracks))
124
+ run(:put, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(uris: tracks))
120
125
  end
121
126
 
122
127
  # Removes all tracks in playlist
@@ -135,8 +140,7 @@ module Spotify
135
140
  end
136
141
 
137
142
  def albums(album_ids)
138
- params = { ids: Array(album_ids).join(',') }
139
- run(:get, '/v1/albums', [200], params)
143
+ { 'albums' => Array(album_ids).map { |album_id| album(album_id) } }
140
144
  end
141
145
 
142
146
  def track(track_id)
@@ -144,8 +148,7 @@ module Spotify
144
148
  end
145
149
 
146
150
  def tracks(track_ids)
147
- params = { ids: Array(track_ids).join(',') }
148
- run(:get, '/v1/tracks', [200], params)
151
+ { 'tracks' => Array(track_ids).map { |track_id| track(track_id) } }
149
152
  end
150
153
 
151
154
  def artist(artist_id)
@@ -153,8 +156,7 @@ module Spotify
153
156
  end
154
157
 
155
158
  def artists(artist_ids)
156
- params = { ids: Array(artist_ids).join(',') }
157
- run(:get, '/v1/artists', [200], params)
159
+ { 'artists' => Array(artist_ids).map { |artist_id| artist(artist_id) } }
158
160
  end
159
161
 
160
162
  def artist_albums(artist_id)
@@ -166,6 +168,9 @@ module Spotify
166
168
  raise(ImplementationError, "entity needs to be either artist, album or track, got: #{entity}")
167
169
  end
168
170
 
171
+ options = options.dup
172
+ options[:limit] = [options[:limit].to_i, 10].min if options.key?(:limit)
173
+
169
174
  params = {
170
175
  q: term.to_s,
171
176
  type: entity
@@ -177,7 +182,8 @@ module Spotify
177
182
  #
178
183
  # +country_id+ is required. An ISO 3166-1 alpha-2 country code.
179
184
  def artist_top_tracks(artist_id, country_id)
180
- run(:get, "/v1/artists/#{artist_id}/top-songs", [200], market: country_id)
185
+ raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/top-tracks')
186
+ run(:get, "/v1/artists/#{artist_id}/top-tracks", [200], country: country_id)
181
187
  end
182
188
 
183
189
  def related_artists(artist_id)
@@ -188,16 +194,24 @@ module Spotify
188
194
  #
189
195
  # client.follow('artist', ['0BvkDsjIUla7X0k6CSWh1I'])
190
196
  def follow(type, ids)
191
- _type = type # kept for backward-compatible signature
192
- params = { ids: Array(ids).join(',') }
193
- run(:put, '/v1/me/library', [200, 204], params)
197
+ entity_type = type.to_s.strip
198
+ uris = Array(ids).map do |id|
199
+ raw = id.to_s
200
+ next raw if raw.start_with?('spotify:')
201
+
202
+ raise(ImplementationError, 'type is required when ids are not full Spotify URIs') if entity_type.empty?
203
+
204
+ "spotify:#{entity_type}:#{raw}"
205
+ end
206
+ run(:put, '/v1/me/library', [200, 204], JSON.dump(uris: uris), false)
194
207
  end
195
208
 
196
209
  # Follow a playlist
197
210
  #
198
211
  # client.follow_playlist('lukebryan', '0obRj9nNySESpFelMCLSya')
199
212
  def follow_playlist(_user_id, playlist_id, is_public = true)
200
- run(:put, "/v1/playlists/#{playlist_id}/followers", [200, 204], { public: is_public })
213
+ _is_public = is_public # kept for backward-compatible signature
214
+ run(:put, '/v1/me/library', [200, 204], JSON.dump(uris: ["spotify:playlist:#{playlist_id}"]), false)
201
215
  end
202
216
 
203
217
  # Generic API helper for forward compatibility with newly added endpoints.
@@ -212,6 +226,18 @@ module Spotify
212
226
 
213
227
  protected
214
228
 
229
+ def raise_endpoint_unavailable_in_development_mode!(endpoint:, replacement: nil)
230
+ return unless development_mode?
231
+
232
+ message = "#{endpoint} is unavailable for Spotify Development Mode apps as of March 9, 2026."
233
+ message += " Use #{replacement} instead." if replacement
234
+ raise(EndpointUnavailableInDevelopmentMode, message)
235
+ end
236
+
237
+ def development_mode?
238
+ @app_mode == 'development' || @app_mode == 'development_mode'
239
+ end
240
+
215
241
  def run(verb, path, expected_status_codes, params = {}, idempotent = true)
216
242
  run!(verb, path, expected_status_codes, params, idempotent)
217
243
  rescue Error => e
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: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Poli