spotify-client 1.0.1 → 1.1.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 +67 -13
- data/lib/spotify/exceptions.rb +1 -0
- data/lib/spotify/version.rb +1 -1
- data/lib/spotify_client.rb +177 -15
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a6a776c3c8bbec1fba9afe39bf5d91da04789a313496873bd256b41564b0180
|
|
4
|
+
data.tar.gz: '09b9987a2dc0f5dbb7437701d4edc69d58abae3f11b2a3f9cb7ab2c330a88be2'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd4aa7ea96354bdbb0d0dd5b9f9fc93840e67005419bc1ed8c5dada3405798db188c945a56b08e16872b3ab04829a87cdd163e987c72bb1b688c1ee1e77083fe
|
|
7
|
+
data.tar.gz: ae3a9d60e844e7677131c0c3425001d2495fd63c56f28b5d20d4bbc6d364734bb90804a6cd21a41f868460469895b17865de5ac79342d818fa3dc9f5c1f33489
|
data/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Ruby client for the [Spotify Web API](https://developer.spotify.com/documentation/web-api).
|
|
4
4
|
|
|
5
|
-
[](https://github.com/icoretech/spotify-client/actions/workflows/test.yml?query=branch%3Amain)
|
|
6
|
+
[](https://badge.fury.io/rb/spotify-client)
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
@@ -33,6 +34,7 @@ The CI matrix runs this gem against Ruby `3.2`, `3.3`, `3.4`, `4.0`, and `ruby-h
|
|
|
33
34
|
```ruby
|
|
34
35
|
config = {
|
|
35
36
|
access_token: 'tk',
|
|
37
|
+
app_mode: :development, # optional; use :development to fail fast on dev-mode restricted endpoints
|
|
36
38
|
raise_errors: true,
|
|
37
39
|
retries: 0,
|
|
38
40
|
read_timeout: 10,
|
|
@@ -46,31 +48,82 @@ client = Spotify::Client.new(config)
|
|
|
46
48
|
## Public API
|
|
47
49
|
|
|
48
50
|
```ruby
|
|
51
|
+
# User
|
|
49
52
|
client.me
|
|
50
|
-
|
|
53
|
+
|
|
54
|
+
# Library
|
|
55
|
+
client.me_albums(params = {})
|
|
56
|
+
client.me_audiobooks(params = {})
|
|
57
|
+
client.me_episodes(params = {})
|
|
51
58
|
client.me_following
|
|
52
|
-
client.
|
|
53
|
-
client.
|
|
54
|
-
client.
|
|
55
|
-
client.
|
|
59
|
+
client.me_shows(params = {})
|
|
60
|
+
client.me_tracks
|
|
61
|
+
client.add_to_library(uris)
|
|
62
|
+
client.remove_from_library(uris)
|
|
63
|
+
client.user_playlists(user_id = nil) # backward-compatible signature; requests /v1/me/playlists
|
|
56
64
|
client.create_user_playlist(user_id, name, is_public = true)
|
|
65
|
+
client.change_playlist_details(user_id, playlist_id, attributes = {})
|
|
66
|
+
|
|
67
|
+
# Playlist
|
|
68
|
+
client.playlist(playlist_id)
|
|
69
|
+
client.playlist_cover_image(playlist_id)
|
|
70
|
+
client.upload_playlist_cover_image(playlist_id, image_base64_jpeg)
|
|
71
|
+
|
|
72
|
+
# Backward-compatible playlist helpers
|
|
73
|
+
client.user_playlist(user_id, playlist_id) # delegates to playlist(playlist_id)
|
|
74
|
+
client.user_playlist_tracks(user_id, playlist_id, params = {})
|
|
57
75
|
client.add_user_tracks_to_playlist(user_id, playlist_id, uris = [], position = nil)
|
|
58
76
|
client.remove_user_tracks_from_playlist(user_id, playlist_id, tracks)
|
|
59
77
|
client.replace_user_tracks_in_playlist(user_id, playlist_id, tracks)
|
|
60
78
|
client.truncate_user_playlist(user_id, playlist_id)
|
|
79
|
+
|
|
80
|
+
# Metadata
|
|
61
81
|
client.album(album_id)
|
|
62
82
|
client.album_tracks(album_id)
|
|
63
83
|
client.albums(album_ids)
|
|
64
|
-
client.track(track_id)
|
|
65
|
-
client.tracks(track_ids)
|
|
66
84
|
client.artist(artist_id)
|
|
67
85
|
client.artists(artist_ids)
|
|
68
86
|
client.artist_albums(artist_id)
|
|
69
|
-
client.search(entity, term, options = {})
|
|
70
87
|
client.artist_top_tracks(artist_id, country_id)
|
|
88
|
+
client.audiobook(audiobook_id, params = {})
|
|
89
|
+
client.audiobook_chapters(audiobook_id, params = {})
|
|
90
|
+
client.chapter(chapter_id, params = {})
|
|
91
|
+
client.episode(episode_id, params = {})
|
|
92
|
+
client.show(show_id, params = {})
|
|
93
|
+
client.show_episodes(show_id, params = {})
|
|
94
|
+
client.track(track_id)
|
|
95
|
+
client.tracks(track_ids)
|
|
96
|
+
|
|
97
|
+
# Personalisation
|
|
98
|
+
client.me_top(type, params = {}) # type: "artists" or "tracks"
|
|
99
|
+
|
|
100
|
+
# Player
|
|
101
|
+
client.currently_playing(params = {})
|
|
102
|
+
client.recently_played(params = {})
|
|
103
|
+
client.playback_state(params = {})
|
|
104
|
+
client.available_devices
|
|
105
|
+
client.transfer_playback(device_ids, play = nil)
|
|
106
|
+
client.start_or_resume_playback(payload = {})
|
|
107
|
+
client.pause_playback(params = {})
|
|
108
|
+
client.skip_to_next(params = {})
|
|
109
|
+
client.skip_to_previous(params = {})
|
|
110
|
+
client.seek_to_position(position_ms, params = {})
|
|
111
|
+
client.set_repeat_mode(state, params = {})
|
|
112
|
+
client.set_playback_volume(volume_percent, params = {})
|
|
113
|
+
client.set_shuffle(state, params = {})
|
|
114
|
+
client.playback_queue(params = {})
|
|
115
|
+
client.add_to_playback_queue(uri, params = {})
|
|
116
|
+
|
|
117
|
+
# Search
|
|
118
|
+
client.search(entity, term, options = {}) # entity: :artist, :album, :track
|
|
119
|
+
|
|
120
|
+
# Legacy helpers kept for compatibility
|
|
121
|
+
client.user(user_id)
|
|
71
122
|
client.related_artists(artist_id)
|
|
72
123
|
client.follow(type, ids)
|
|
73
124
|
client.follow_playlist(user_id, playlist_id, is_public = true)
|
|
125
|
+
|
|
126
|
+
# Generic helpers for forward compatibility
|
|
74
127
|
client.request(:get, '/v1/me') # generic helper for newer endpoints
|
|
75
128
|
client.request!(:post, '/v1/some-endpoint', [201], payload, false)
|
|
76
129
|
```
|
|
@@ -80,11 +133,12 @@ client.request!(:post, '/v1/some-endpoint', [201], payload, false)
|
|
|
80
133
|
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
134
|
|
|
82
135
|
- Playlist reads/writes use `/v1/me/playlists` and `/v1/playlists/{playlist_id}/*`.
|
|
83
|
-
- `follow(type, ids)`
|
|
84
|
-
- `artist_top_tracks`
|
|
136
|
+
- `follow(type, ids)` now targets `/v1/me/library` using Spotify URIs (`spotify:{type}:{id}`), while still accepting prebuilt URIs.
|
|
137
|
+
- `artist_top_tracks` uses `/v1/artists/{id}/top-tracks`.
|
|
138
|
+
- `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).
|
|
139
|
+
- If initialized with `app_mode: :development`, `user(user_id)` and `artist_top_tracks` raise `Spotify::EndpointUnavailableInDevelopmentMode` before making the HTTP request.
|
|
85
140
|
|
|
86
|
-
-
|
|
87
|
-
- Migration guide: [Spotify Web API Migration Guide](https://developer.spotify.com/documentation/web-api/concepts/migration-guide)
|
|
141
|
+
- February 2026 changes: [Spotify Web API Changes](https://developer.spotify.com/documentation/web-api/references/changes/february-2026)
|
|
88
142
|
|
|
89
143
|
## Development
|
|
90
144
|
|
data/lib/spotify/exceptions.rb
CHANGED
data/lib/spotify/version.rb
CHANGED
data/lib/spotify_client.rb
CHANGED
|
@@ -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
|
|
|
@@ -43,6 +44,22 @@ module Spotify
|
|
|
43
44
|
run(:get, '/v1/me/tracks', [200])
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
def me_albums(params = {})
|
|
48
|
+
run(:get, '/v1/me/albums', [200], params)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def me_audiobooks(params = {})
|
|
52
|
+
run(:get, '/v1/me/audiobooks', [200], params)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def me_episodes(params = {})
|
|
56
|
+
run(:get, '/v1/me/episodes', [200], params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def me_shows(params = {})
|
|
60
|
+
run(:get, '/v1/me/shows', [200], params)
|
|
61
|
+
end
|
|
62
|
+
|
|
46
63
|
# params:
|
|
47
64
|
# - type: Required, The ID type, currently only 'artist' is supported
|
|
48
65
|
# - limit: Optional. The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50.
|
|
@@ -53,6 +70,10 @@ module Spotify
|
|
|
53
70
|
end
|
|
54
71
|
|
|
55
72
|
def user(user_id)
|
|
73
|
+
raise_endpoint_unavailable_in_development_mode!(
|
|
74
|
+
endpoint: 'GET /v1/users/{id}',
|
|
75
|
+
replacement: 'GET /v1/me'
|
|
76
|
+
)
|
|
56
77
|
run(:get, "/v1/users/#{user_id}", [200])
|
|
57
78
|
end
|
|
58
79
|
|
|
@@ -61,12 +82,12 @@ module Spotify
|
|
|
61
82
|
end
|
|
62
83
|
|
|
63
84
|
def user_playlist(_user_id, playlist_id)
|
|
64
|
-
|
|
85
|
+
playlist(playlist_id)
|
|
65
86
|
end
|
|
66
87
|
|
|
67
88
|
def user_playlist_tracks(_user_id, playlist_id, params = {})
|
|
68
89
|
tracks = { 'items' => [] }
|
|
69
|
-
path = "/v1/playlists/#{playlist_id}/
|
|
90
|
+
path = "/v1/playlists/#{playlist_id}/items"
|
|
70
91
|
|
|
71
92
|
while path
|
|
72
93
|
response = run(:get, path, [200], params)
|
|
@@ -87,6 +108,10 @@ module Spotify
|
|
|
87
108
|
run(:post, '/v1/me/playlists', [201], JSON.dump(name: name, public: is_public), false)
|
|
88
109
|
end
|
|
89
110
|
|
|
111
|
+
def change_playlist_details(_user_id, playlist_id, attributes = {})
|
|
112
|
+
run(:put, "/v1/playlists/#{playlist_id}", [200, 204], JSON.dump(attributes), false)
|
|
113
|
+
end
|
|
114
|
+
|
|
90
115
|
# Add an Array of track uris to an existing playlist.
|
|
91
116
|
#
|
|
92
117
|
# Adding tracks to a user's public playlist requires authorization of the playlist-modify-public scope;
|
|
@@ -107,7 +132,7 @@ module Spotify
|
|
|
107
132
|
# '1181346016', '7i3thJWDtmX04dJhFwYb0x', [{ uri: 'spotify:track:...', positions: [0] }]
|
|
108
133
|
# )
|
|
109
134
|
def remove_user_tracks_from_playlist(_user_id, playlist_id, tracks)
|
|
110
|
-
run(:delete, "/v1/playlists/#{playlist_id}/
|
|
135
|
+
run(:delete, "/v1/playlists/#{playlist_id}/items", [200], JSON.dump(items: tracks))
|
|
111
136
|
end
|
|
112
137
|
|
|
113
138
|
# Replaces all occurrences of tracks with what's in the playlist
|
|
@@ -116,7 +141,7 @@ module Spotify
|
|
|
116
141
|
# '1181346016', '7i3thJWDtmX04dJhFwYb0x', %w(spotify:track:... spotify:track:...)
|
|
117
142
|
# )
|
|
118
143
|
def replace_user_tracks_in_playlist(_user_id, playlist_id, tracks)
|
|
119
|
-
run(:put, "/v1/playlists/#{playlist_id}/
|
|
144
|
+
run(:put, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(uris: tracks))
|
|
120
145
|
end
|
|
121
146
|
|
|
122
147
|
# Removes all tracks in playlist
|
|
@@ -126,6 +151,18 @@ module Spotify
|
|
|
126
151
|
replace_user_tracks_in_playlist(user_id, playlist_id, [])
|
|
127
152
|
end
|
|
128
153
|
|
|
154
|
+
def playlist(playlist_id)
|
|
155
|
+
run(:get, "/v1/playlists/#{playlist_id}", [200])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def playlist_cover_image(playlist_id)
|
|
159
|
+
run(:get, "/v1/playlists/#{playlist_id}/images", [200], {})
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def upload_playlist_cover_image(playlist_id, image_base64_jpeg)
|
|
163
|
+
run(:put, "/v1/playlists/#{playlist_id}/images", [200, 202, 204], image_base64_jpeg.to_s, false)
|
|
164
|
+
end
|
|
165
|
+
|
|
129
166
|
def album(album_id)
|
|
130
167
|
run(:get, "/v1/albums/#{album_id}", [200])
|
|
131
168
|
end
|
|
@@ -135,8 +172,7 @@ module Spotify
|
|
|
135
172
|
end
|
|
136
173
|
|
|
137
174
|
def albums(album_ids)
|
|
138
|
-
|
|
139
|
-
run(:get, '/v1/albums', [200], params)
|
|
175
|
+
{ 'albums' => Array(album_ids).map { |album_id| album(album_id) } }
|
|
140
176
|
end
|
|
141
177
|
|
|
142
178
|
def track(track_id)
|
|
@@ -144,8 +180,7 @@ module Spotify
|
|
|
144
180
|
end
|
|
145
181
|
|
|
146
182
|
def tracks(track_ids)
|
|
147
|
-
|
|
148
|
-
run(:get, '/v1/tracks', [200], params)
|
|
183
|
+
{ 'tracks' => Array(track_ids).map { |track_id| track(track_id) } }
|
|
149
184
|
end
|
|
150
185
|
|
|
151
186
|
def artist(artist_id)
|
|
@@ -153,19 +188,45 @@ module Spotify
|
|
|
153
188
|
end
|
|
154
189
|
|
|
155
190
|
def artists(artist_ids)
|
|
156
|
-
|
|
157
|
-
run(:get, '/v1/artists', [200], params)
|
|
191
|
+
{ 'artists' => Array(artist_ids).map { |artist_id| artist(artist_id) } }
|
|
158
192
|
end
|
|
159
193
|
|
|
160
194
|
def artist_albums(artist_id)
|
|
161
195
|
run(:get, "/v1/artists/#{artist_id}/albums", [200])
|
|
162
196
|
end
|
|
163
197
|
|
|
198
|
+
def audiobook(audiobook_id, params = {})
|
|
199
|
+
run(:get, "/v1/audiobooks/#{audiobook_id}", [200], params)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def audiobook_chapters(audiobook_id, params = {})
|
|
203
|
+
run(:get, "/v1/audiobooks/#{audiobook_id}/chapters", [200], params)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def chapter(chapter_id, params = {})
|
|
207
|
+
run(:get, "/v1/chapters/#{chapter_id}", [200], params)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def episode(episode_id, params = {})
|
|
211
|
+
run(:get, "/v1/episodes/#{episode_id}", [200], params)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def show(show_id, params = {})
|
|
215
|
+
run(:get, "/v1/shows/#{show_id}", [200], params)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def show_episodes(show_id, params = {})
|
|
219
|
+
run(:get, "/v1/shows/#{show_id}/episodes", [200], params)
|
|
220
|
+
end
|
|
221
|
+
|
|
164
222
|
def search(entity, term, options = {})
|
|
165
223
|
unless %i[artist album track].include?(entity.to_sym)
|
|
166
224
|
raise(ImplementationError, "entity needs to be either artist, album or track, got: #{entity}")
|
|
167
225
|
end
|
|
168
226
|
|
|
227
|
+
options = options.dup
|
|
228
|
+
options[:limit] = [options[:limit].to_i, 10].min if options.key?(:limit)
|
|
229
|
+
|
|
169
230
|
params = {
|
|
170
231
|
q: term.to_s,
|
|
171
232
|
type: entity
|
|
@@ -177,27 +238,116 @@ module Spotify
|
|
|
177
238
|
#
|
|
178
239
|
# +country_id+ is required. An ISO 3166-1 alpha-2 country code.
|
|
179
240
|
def artist_top_tracks(artist_id, country_id)
|
|
180
|
-
|
|
241
|
+
raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/top-tracks')
|
|
242
|
+
run(:get, "/v1/artists/#{artist_id}/top-tracks", [200], country: country_id)
|
|
181
243
|
end
|
|
182
244
|
|
|
183
245
|
def related_artists(artist_id)
|
|
184
246
|
run(:get, "/v1/artists/#{artist_id}/related-artists", [200])
|
|
185
247
|
end
|
|
186
248
|
|
|
249
|
+
def me_top(type, params = {})
|
|
250
|
+
valid_types = %w[artists tracks]
|
|
251
|
+
normalized_type = type.to_s
|
|
252
|
+
unless valid_types.include?(normalized_type)
|
|
253
|
+
raise(ImplementationError, "type needs to be one of #{valid_types.join(', ')}, got: #{type}")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
run(:get, "/v1/me/top/#{normalized_type}", [200], params)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def currently_playing(params = {})
|
|
260
|
+
run(:get, '/v1/me/player/currently-playing', [200], params)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def recently_played(params = {})
|
|
264
|
+
run(:get, '/v1/me/player/recently-played', [200], params)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def playback_state(params = {})
|
|
268
|
+
run(:get, '/v1/me/player', [200], params)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def available_devices
|
|
272
|
+
run(:get, '/v1/me/player/devices', [200], {})
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def transfer_playback(device_ids, play = nil)
|
|
276
|
+
body = { device_ids: Array(device_ids) }
|
|
277
|
+
body[:play] = play unless play.nil?
|
|
278
|
+
run(:put, '/v1/me/player', [200, 204], JSON.dump(body), false)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def start_or_resume_playback(payload = {})
|
|
282
|
+
run(:put, '/v1/me/player/play', [200, 204], JSON.dump(payload), false)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def pause_playback(params = {})
|
|
286
|
+
run(:put, '/v1/me/player/pause', [200, 204], params, false)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def skip_to_next(params = {})
|
|
290
|
+
run(:post, '/v1/me/player/next', [200, 204], params, false)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def skip_to_previous(params = {})
|
|
294
|
+
run(:post, '/v1/me/player/previous', [200, 204], params, false)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def seek_to_position(position_ms, params = {})
|
|
298
|
+
run(:put, '/v1/me/player/seek', [200, 204], params.merge(position_ms: position_ms), false)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def set_repeat_mode(state, params = {})
|
|
302
|
+
run(:put, '/v1/me/player/repeat', [200, 204], params.merge(state: state), false)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def set_playback_volume(volume_percent, params = {})
|
|
306
|
+
run(:put, '/v1/me/player/volume', [200, 204], params.merge(volume_percent: volume_percent), false)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def set_shuffle(state, params = {})
|
|
310
|
+
run(:put, '/v1/me/player/shuffle', [200, 204], params.merge(state: state), false)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def playback_queue(params = {})
|
|
314
|
+
run(:get, '/v1/me/player/queue', [200], params)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def add_to_playback_queue(uri, params = {})
|
|
318
|
+
run(:post, '/v1/me/player/queue', [200, 204], params.merge(uri: uri), false)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def add_to_library(uris)
|
|
322
|
+
run(:put, '/v1/me/library', [200, 204], JSON.dump(uris: Array(uris)), false)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def remove_from_library(uris)
|
|
326
|
+
run(:delete, '/v1/me/library', [200, 204], JSON.dump(uris: Array(uris)), false)
|
|
327
|
+
end
|
|
328
|
+
|
|
187
329
|
# Follow artists or users
|
|
188
330
|
#
|
|
189
331
|
# client.follow('artist', ['0BvkDsjIUla7X0k6CSWh1I'])
|
|
190
332
|
def follow(type, ids)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
333
|
+
entity_type = type.to_s.strip
|
|
334
|
+
uris = Array(ids).map do |id|
|
|
335
|
+
raw = id.to_s
|
|
336
|
+
next raw if raw.start_with?('spotify:')
|
|
337
|
+
|
|
338
|
+
raise(ImplementationError, 'type is required when ids are not full Spotify URIs') if entity_type.empty?
|
|
339
|
+
|
|
340
|
+
"spotify:#{entity_type}:#{raw}"
|
|
341
|
+
end
|
|
342
|
+
add_to_library(uris)
|
|
194
343
|
end
|
|
195
344
|
|
|
196
345
|
# Follow a playlist
|
|
197
346
|
#
|
|
198
347
|
# client.follow_playlist('lukebryan', '0obRj9nNySESpFelMCLSya')
|
|
199
348
|
def follow_playlist(_user_id, playlist_id, is_public = true)
|
|
200
|
-
|
|
349
|
+
_is_public = is_public # kept for backward-compatible signature
|
|
350
|
+
add_to_library(["spotify:playlist:#{playlist_id}"])
|
|
201
351
|
end
|
|
202
352
|
|
|
203
353
|
# Generic API helper for forward compatibility with newly added endpoints.
|
|
@@ -212,6 +362,18 @@ module Spotify
|
|
|
212
362
|
|
|
213
363
|
protected
|
|
214
364
|
|
|
365
|
+
def raise_endpoint_unavailable_in_development_mode!(endpoint:, replacement: nil)
|
|
366
|
+
return unless development_mode?
|
|
367
|
+
|
|
368
|
+
message = "#{endpoint} is unavailable for Spotify Development Mode apps as of March 9, 2026."
|
|
369
|
+
message += " Use #{replacement} instead." if replacement
|
|
370
|
+
raise(EndpointUnavailableInDevelopmentMode, message)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def development_mode?
|
|
374
|
+
@app_mode == 'development' || @app_mode == 'development_mode'
|
|
375
|
+
end
|
|
376
|
+
|
|
215
377
|
def run(verb, path, expected_status_codes, params = {}, idempotent = true)
|
|
216
378
|
run!(verb, path, expected_status_codes, params, idempotent)
|
|
217
379
|
rescue Error => e
|