spotify-client 1.1.0 → 1.1.1
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 +2 -2
- data/lib/spotify/client/compatibility_api.rb +83 -0
- data/lib/spotify/client/transport.rb +96 -0
- data/lib/spotify/client.rb +299 -0
- data/lib/spotify/version.rb +1 -1
- data/lib/spotify-client.rb +4 -1
- data/lib/spotify_client.rb +4 -428
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fba94340f1fab59fe29af2fe56082fe7a957fe49cdd650dc689e681938fa7dc
|
|
4
|
+
data.tar.gz: 88842cffcde50db973f10b4ec60a1214fe4356203bf5c2ebe4826ca93789eaef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7189b3aae25dd0b74d9035e2936adae1cd6bdd5fdc1147531566674b262fa9dacdb40f56f07b1ab65c3ba87934f0ec9a6f57fdf41f44528f3927ae4ddf101014
|
|
7
|
+
data.tar.gz: 3c806b5e89ddf72e7003a1fa30f18dabeb0720c9ac3c8a617a89c0e3677a929e4c5bb8e878a52e4b2216ff00363ba9bb730741ae7942fb5ba37ed925bdaf6a7c
|
data/README.md
CHANGED
|
@@ -124,8 +124,8 @@ client.follow(type, ids)
|
|
|
124
124
|
client.follow_playlist(user_id, playlist_id, is_public = true)
|
|
125
125
|
|
|
126
126
|
# Generic helpers for forward compatibility
|
|
127
|
-
client.request(:get, '/v1/me') #
|
|
128
|
-
client.request!(:post, '/v1/some-endpoint', [201], payload, false)
|
|
127
|
+
client.request(:get, '/v1/me', [200], { market: 'IT' }) # GET-style hash payloads become query params
|
|
128
|
+
client.request!(:post, '/v1/some-endpoint', [201], payload, false) # non-GET hash payloads are JSON encoded
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
## Spotify API Migration Notes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spotify
|
|
4
|
+
class Client
|
|
5
|
+
module CompatibilityAPI
|
|
6
|
+
def user(user_id)
|
|
7
|
+
raise_endpoint_unavailable_in_development_mode!(
|
|
8
|
+
endpoint: 'GET /v1/users/{id}',
|
|
9
|
+
replacement: 'GET /v1/me'
|
|
10
|
+
)
|
|
11
|
+
run(:get, "/v1/users/#{user_id}", [200])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def user_playlists(_user_id = nil)
|
|
15
|
+
run(:get, '/v1/me/playlists', [200])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def user_playlist(_user_id, playlist_id)
|
|
19
|
+
playlist(playlist_id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def user_playlist_tracks(_user_id, playlist_id, params = {})
|
|
23
|
+
tracks = { 'items' => [] }
|
|
24
|
+
path = "/v1/playlists/#{playlist_id}/items"
|
|
25
|
+
query_params = params.dup
|
|
26
|
+
|
|
27
|
+
while path
|
|
28
|
+
response = run(:get, path, [200], query_params)
|
|
29
|
+
return false unless response
|
|
30
|
+
|
|
31
|
+
tracks['items'].concat(response.fetch('items', []))
|
|
32
|
+
tracks.merge!(response.reject { |key, _value| key == 'items' })
|
|
33
|
+
path, query_params = next_page_request(response['next'])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
tracks
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_user_playlist(_user_id, name, is_public = true)
|
|
40
|
+
run(:post, '/v1/me/playlists', [201], JSON.dump(name: name, public: is_public), false)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def change_playlist_details(_user_id, playlist_id, attributes = {})
|
|
44
|
+
run(:put, "/v1/playlists/#{playlist_id}", [200, 204], JSON.dump(attributes), false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_user_tracks_to_playlist(_user_id, playlist_id, uris = [], position = nil)
|
|
48
|
+
params = { uris: Array(uris)[0..99].join(',') }
|
|
49
|
+
params.merge!(position: position) if position
|
|
50
|
+
run(:post, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(params), false)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def remove_user_tracks_from_playlist(_user_id, playlist_id, tracks)
|
|
54
|
+
run(:delete, "/v1/playlists/#{playlist_id}/items", [200], JSON.dump(items: tracks))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def replace_user_tracks_in_playlist(_user_id, playlist_id, tracks)
|
|
58
|
+
run(:put, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(uris: tracks))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def truncate_user_playlist(user_id, playlist_id)
|
|
62
|
+
replace_user_tracks_in_playlist(user_id, playlist_id, [])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def follow(type, ids)
|
|
66
|
+
entity_type = type.to_s.strip
|
|
67
|
+
uris = Array(ids).map do |id|
|
|
68
|
+
raw = id.to_s
|
|
69
|
+
next raw if raw.start_with?('spotify:')
|
|
70
|
+
|
|
71
|
+
raise(ImplementationError, 'type is required when ids are not full Spotify URIs') if entity_type.empty?
|
|
72
|
+
|
|
73
|
+
"spotify:#{entity_type}:#{raw}"
|
|
74
|
+
end
|
|
75
|
+
add_to_library(uris)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def follow_playlist(_user_id, playlist_id, _is_public = true)
|
|
79
|
+
add_to_library(["spotify:playlist:#{playlist_id}"])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Spotify
|
|
6
|
+
class Client
|
|
7
|
+
module Transport
|
|
8
|
+
EXCON_ERROR_MAP = {
|
|
9
|
+
Excon::Errors::NotFound => ResourceNotFound,
|
|
10
|
+
Excon::Errors::BadRequest => BadRequest,
|
|
11
|
+
Excon::Errors::Forbidden => InsufficientClientScopeError,
|
|
12
|
+
Excon::Errors::Unauthorized => AuthenticationError
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
protected
|
|
16
|
+
|
|
17
|
+
def run(verb, path, expected_status_codes, params = {}, idempotent = true)
|
|
18
|
+
run!(verb, path, expected_status_codes, params, idempotent)
|
|
19
|
+
rescue Error => e
|
|
20
|
+
handle_nonbang_error(e)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run!(verb, path, expected_status_codes, params_or_body = nil, idempotent = true)
|
|
24
|
+
response = @connection.request(
|
|
25
|
+
build_request_packet(
|
|
26
|
+
verb: verb,
|
|
27
|
+
path: path,
|
|
28
|
+
expected_status_codes: expected_status_codes,
|
|
29
|
+
params_or_body: params_or_body,
|
|
30
|
+
idempotent: idempotent
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
parse_response_body(response)
|
|
34
|
+
rescue Excon::Errors::Error => e
|
|
35
|
+
raise map_transport_error(e)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_request_packet(verb:, path:, expected_status_codes:, params_or_body:, idempotent:)
|
|
39
|
+
packet = {
|
|
40
|
+
idempotent: idempotent,
|
|
41
|
+
expects: expected_status_codes,
|
|
42
|
+
method: verb,
|
|
43
|
+
path: path,
|
|
44
|
+
read_timeout: @read_timeout,
|
|
45
|
+
write_timeout: @write_timeout,
|
|
46
|
+
retry_limit: @retries,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type' => 'application/json',
|
|
49
|
+
'User-Agent' => 'Spotify Ruby Client'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
apply_request_payload(packet, params_or_body)
|
|
53
|
+
|
|
54
|
+
packet[:headers].merge!('Authorization' => "Bearer #{@access_token}") if !@access_token.nil? && @access_token != ''
|
|
55
|
+
packet
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def apply_request_payload(packet, params_or_body)
|
|
59
|
+
if params_or_body.is_a?(Hash)
|
|
60
|
+
packet[:query] = params_or_body
|
|
61
|
+
else
|
|
62
|
+
packet[:body] = params_or_body
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_response_body(response)
|
|
67
|
+
return {} if response.body.nil? || response.body.empty?
|
|
68
|
+
|
|
69
|
+
::JSON.parse(response.body)
|
|
70
|
+
rescue JSON::ParserError => e
|
|
71
|
+
raise(HTTPError, "Error: #{e.message}")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def next_page_request(next_url)
|
|
75
|
+
return [nil, {}] if next_url.nil? || next_url.empty?
|
|
76
|
+
|
|
77
|
+
uri = URI.parse(next_url)
|
|
78
|
+
[uri.query ? "#{uri.path}?#{uri.query}" : uri.path, {}]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def handle_nonbang_error(error)
|
|
82
|
+
raise error if @raise_errors
|
|
83
|
+
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def map_transport_error(error)
|
|
88
|
+
error_class = EXCON_ERROR_MAP.find do |transport_error, _client_error|
|
|
89
|
+
error.is_a?(transport_error)
|
|
90
|
+
end&.last || HTTPError
|
|
91
|
+
|
|
92
|
+
error_class.new("Error: #{error.message}")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'excon'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require 'spotify/exceptions'
|
|
7
|
+
require 'spotify/client/compatibility_api'
|
|
8
|
+
require 'spotify/client/transport'
|
|
9
|
+
|
|
10
|
+
module Spotify
|
|
11
|
+
class Client
|
|
12
|
+
include CompatibilityAPI
|
|
13
|
+
include Transport
|
|
14
|
+
|
|
15
|
+
BASE_URI = 'https://api.spotify.com'
|
|
16
|
+
|
|
17
|
+
attr_accessor :access_token
|
|
18
|
+
|
|
19
|
+
# Initialize the client.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# client = Spotify::Client.new(:access_token => 'longtoken', retries: 0, raise_errors: true)
|
|
23
|
+
#
|
|
24
|
+
# @param [Hash] configuration.
|
|
25
|
+
def initialize(config = {})
|
|
26
|
+
@access_token = config[:access_token]
|
|
27
|
+
@raise_errors = config[:raise_errors] || false
|
|
28
|
+
@retries = config[:retries] || 0
|
|
29
|
+
@read_timeout = config[:read_timeout] || 10
|
|
30
|
+
@write_timeout = config[:write_timeout] || 10
|
|
31
|
+
@app_mode = config[:app_mode].to_s.strip.downcase
|
|
32
|
+
@connection = Excon.new(BASE_URI, persistent: config[:persistent] || false)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def inspect
|
|
36
|
+
vars = instance_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(', ')
|
|
37
|
+
"<#{self.class}: #{vars}>"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Closes the connection underlying socket.
|
|
41
|
+
# Use when you employ persistent connections and are done with your requests.
|
|
42
|
+
def close_connection
|
|
43
|
+
@connection.reset
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def me
|
|
47
|
+
run(:get, '/v1/me', [200])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def me_tracks
|
|
51
|
+
run(:get, '/v1/me/tracks', [200])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def me_albums(params = {})
|
|
55
|
+
run(:get, '/v1/me/albums', [200], params)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def me_audiobooks(params = {})
|
|
59
|
+
run(:get, '/v1/me/audiobooks', [200], params)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def me_episodes(params = {})
|
|
63
|
+
run(:get, '/v1/me/episodes', [200], params)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def me_shows(params = {})
|
|
67
|
+
run(:get, '/v1/me/shows', [200], params)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# params:
|
|
71
|
+
# - type: Required, The ID type, currently only 'artist' is supported
|
|
72
|
+
# - limit: Optional. The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50.
|
|
73
|
+
# - after: Optional. The last artist ID retrieved from the previous request.
|
|
74
|
+
def me_following(params = {})
|
|
75
|
+
params = params.merge(type: 'artist')
|
|
76
|
+
run(:get, '/v1/me/following', [200], params)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def playlist(playlist_id)
|
|
80
|
+
run(:get, "/v1/playlists/#{playlist_id}", [200])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def playlist_cover_image(playlist_id)
|
|
84
|
+
run(:get, "/v1/playlists/#{playlist_id}/images", [200], {})
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
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
|
+
end
|
|
90
|
+
|
|
91
|
+
def album(album_id)
|
|
92
|
+
run(:get, "/v1/albums/#{album_id}", [200])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def album_tracks(album_id)
|
|
96
|
+
run(:get, "/v1/albums/#{album_id}/tracks", [200])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def albums(album_ids)
|
|
100
|
+
{ 'albums' => Array(album_ids).map { |album_id| album(album_id) } }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def track(track_id)
|
|
104
|
+
run(:get, "/v1/tracks/#{track_id}", [200])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def tracks(track_ids)
|
|
108
|
+
{ 'tracks' => Array(track_ids).map { |track_id| track(track_id) } }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def artist(artist_id)
|
|
112
|
+
run(:get, "/v1/artists/#{artist_id}", [200])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def artists(artist_ids)
|
|
116
|
+
{ 'artists' => Array(artist_ids).map { |artist_id| artist(artist_id) } }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def artist_albums(artist_id)
|
|
120
|
+
run(:get, "/v1/artists/#{artist_id}/albums", [200])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def audiobook(audiobook_id, params = {})
|
|
124
|
+
run(:get, "/v1/audiobooks/#{audiobook_id}", [200], params)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def audiobook_chapters(audiobook_id, params = {})
|
|
128
|
+
run(:get, "/v1/audiobooks/#{audiobook_id}/chapters", [200], params)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def chapter(chapter_id, params = {})
|
|
132
|
+
run(:get, "/v1/chapters/#{chapter_id}", [200], params)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def episode(episode_id, params = {})
|
|
136
|
+
run(:get, "/v1/episodes/#{episode_id}", [200], params)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def show(show_id, params = {})
|
|
140
|
+
run(:get, "/v1/shows/#{show_id}", [200], params)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def show_episodes(show_id, params = {})
|
|
144
|
+
run(:get, "/v1/shows/#{show_id}/episodes", [200], params)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def search(entity, term, options = {})
|
|
148
|
+
unless %i[artist album track].include?(entity.to_sym)
|
|
149
|
+
raise(ImplementationError, "entity needs to be either artist, album or track, got: #{entity}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
options = options.dup
|
|
153
|
+
options[:limit] = [options[:limit].to_i, 10].min if options.key?(:limit)
|
|
154
|
+
|
|
155
|
+
params = {
|
|
156
|
+
q: term.to_s,
|
|
157
|
+
type: entity
|
|
158
|
+
}.merge(options)
|
|
159
|
+
run(:get, '/v1/search', [200], params)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get Spotify catalog information about an artist's top 10 tracks by country.
|
|
163
|
+
#
|
|
164
|
+
# +country_id+ is required. An ISO 3166-1 alpha-2 country code.
|
|
165
|
+
def artist_top_tracks(artist_id, country_id)
|
|
166
|
+
raise_endpoint_unavailable_in_development_mode!(endpoint: 'GET /v1/artists/{id}/top-tracks')
|
|
167
|
+
run(:get, "/v1/artists/#{artist_id}/top-tracks", [200], country: country_id)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def related_artists(artist_id)
|
|
171
|
+
run(:get, "/v1/artists/#{artist_id}/related-artists", [200])
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def me_top(type, params = {})
|
|
175
|
+
valid_types = %w[artists tracks]
|
|
176
|
+
normalized_type = type.to_s
|
|
177
|
+
unless valid_types.include?(normalized_type)
|
|
178
|
+
raise(ImplementationError, "type needs to be one of #{valid_types.join(', ')}, got: #{type}")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
run(:get, "/v1/me/top/#{normalized_type}", [200], params)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def currently_playing(params = {})
|
|
185
|
+
run(:get, '/v1/me/player/currently-playing', [200], params)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def recently_played(params = {})
|
|
189
|
+
run(:get, '/v1/me/player/recently-played', [200], params)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def playback_state(params = {})
|
|
193
|
+
run(:get, '/v1/me/player', [200], params)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def available_devices
|
|
197
|
+
run(:get, '/v1/me/player/devices', [200], {})
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def transfer_playback(device_ids, play = nil)
|
|
201
|
+
body = { device_ids: Array(device_ids) }
|
|
202
|
+
body[:play] = play unless play.nil?
|
|
203
|
+
run(:put, '/v1/me/player', [200, 204], JSON.dump(body), false)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def start_or_resume_playback(payload = {})
|
|
207
|
+
run(:put, '/v1/me/player/play', [200, 204], JSON.dump(payload), false)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def pause_playback(params = {})
|
|
211
|
+
run(:put, '/v1/me/player/pause', [200, 204], params, false)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def skip_to_next(params = {})
|
|
215
|
+
run(:post, '/v1/me/player/next', [200, 204], params, false)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def skip_to_previous(params = {})
|
|
219
|
+
run(:post, '/v1/me/player/previous', [200, 204], params, false)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def seek_to_position(position_ms, params = {})
|
|
223
|
+
run(:put, '/v1/me/player/seek', [200, 204], params.merge(position_ms: position_ms), false)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def set_repeat_mode(state, params = {})
|
|
227
|
+
run(:put, '/v1/me/player/repeat', [200, 204], params.merge(state: state), false)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def set_playback_volume(volume_percent, params = {})
|
|
231
|
+
run(:put, '/v1/me/player/volume', [200, 204], params.merge(volume_percent: volume_percent), false)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def set_shuffle(state, params = {})
|
|
235
|
+
run(:put, '/v1/me/player/shuffle', [200, 204], params.merge(state: state), false)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def playback_queue(params = {})
|
|
239
|
+
run(:get, '/v1/me/player/queue', [200], params)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def add_to_playback_queue(uri, params = {})
|
|
243
|
+
run(:post, '/v1/me/player/queue', [200, 204], params.merge(uri: uri), false)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def add_to_library(uris)
|
|
247
|
+
run(:put, '/v1/me/library', [200, 204], JSON.dump(uris: Array(uris)), false)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def remove_from_library(uris)
|
|
251
|
+
run(:delete, '/v1/me/library', [200, 204], JSON.dump(uris: Array(uris)), false)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Generic API helper for forward compatibility with newly added endpoints.
|
|
255
|
+
def request(verb, path, expected_status_codes = [200], params_or_body = {}, idempotent = true)
|
|
256
|
+
run(
|
|
257
|
+
verb.to_sym,
|
|
258
|
+
path,
|
|
259
|
+
Array(expected_status_codes),
|
|
260
|
+
normalize_generic_request_payload(verb, params_or_body),
|
|
261
|
+
idempotent
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Bang variant that propagates mapped API errors.
|
|
266
|
+
def request!(verb, path, expected_status_codes = [200], params_or_body = {}, idempotent = true)
|
|
267
|
+
run!(
|
|
268
|
+
verb.to_sym,
|
|
269
|
+
path,
|
|
270
|
+
Array(expected_status_codes),
|
|
271
|
+
normalize_generic_request_payload(verb, params_or_body),
|
|
272
|
+
idempotent
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
protected
|
|
277
|
+
|
|
278
|
+
def raise_endpoint_unavailable_in_development_mode!(endpoint:, replacement: nil)
|
|
279
|
+
return unless development_mode?
|
|
280
|
+
|
|
281
|
+
message = "#{endpoint} is unavailable for Spotify Development Mode apps as of March 9, 2026."
|
|
282
|
+
message += " Use #{replacement} instead." if replacement
|
|
283
|
+
raise(EndpointUnavailableInDevelopmentMode, message)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def development_mode?
|
|
287
|
+
@app_mode == 'development' || @app_mode == 'development_mode'
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def normalize_generic_request_payload(verb, params_or_body)
|
|
291
|
+
return params_or_body unless params_or_body.is_a?(Hash)
|
|
292
|
+
|
|
293
|
+
query_verbs = %i[get head options]
|
|
294
|
+
return params_or_body if query_verbs.include?(verb.to_sym)
|
|
295
|
+
|
|
296
|
+
JSON.dump(params_or_body)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
data/lib/spotify/version.rb
CHANGED
data/lib/spotify-client.rb
CHANGED
data/lib/spotify_client.rb
CHANGED
|
@@ -1,430 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
require 'json'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
lib_dir = __dir__
|
|
4
|
+
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
class Client
|
|
8
|
-
BASE_URI = 'https://api.spotify.com'.freeze
|
|
9
|
-
|
|
10
|
-
attr_accessor :access_token
|
|
11
|
-
|
|
12
|
-
# Initialize the client.
|
|
13
|
-
#
|
|
14
|
-
# @example
|
|
15
|
-
# client = Spotify::Client.new(:access_token => 'longtoken', retries: 0, raise_errors: true)
|
|
16
|
-
#
|
|
17
|
-
# @param [Hash] configuration.
|
|
18
|
-
def initialize(config = {})
|
|
19
|
-
@access_token = config[:access_token]
|
|
20
|
-
@raise_errors = config[:raise_errors] || false
|
|
21
|
-
@retries = config[:retries] || 0
|
|
22
|
-
@read_timeout = config[:read_timeout] || 10
|
|
23
|
-
@write_timeout = config[:write_timeout] || 10
|
|
24
|
-
@app_mode = config[:app_mode].to_s.strip.downcase
|
|
25
|
-
@connection = Excon.new(BASE_URI, persistent: config[:persistent] || false)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def inspect
|
|
29
|
-
vars = instance_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(', ')
|
|
30
|
-
"<#{self.class}: #{vars}>"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Closes the connection underlying socket.
|
|
34
|
-
# Use when you employ persistent connections and are done with your requests.
|
|
35
|
-
def close_connection
|
|
36
|
-
@connection.reset
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def me
|
|
40
|
-
run(:get, '/v1/me', [200])
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def me_tracks
|
|
44
|
-
run(:get, '/v1/me/tracks', [200])
|
|
45
|
-
end
|
|
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
|
-
|
|
63
|
-
# params:
|
|
64
|
-
# - type: Required, The ID type, currently only 'artist' is supported
|
|
65
|
-
# - limit: Optional. The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50.
|
|
66
|
-
# - after: Optional. The last artist ID retrieved from the previous request.
|
|
67
|
-
def me_following(params = {})
|
|
68
|
-
params = params.merge(type: 'artist')
|
|
69
|
-
run(:get, '/v1/me/following', [200], params)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def user(user_id)
|
|
73
|
-
raise_endpoint_unavailable_in_development_mode!(
|
|
74
|
-
endpoint: 'GET /v1/users/{id}',
|
|
75
|
-
replacement: 'GET /v1/me'
|
|
76
|
-
)
|
|
77
|
-
run(:get, "/v1/users/#{user_id}", [200])
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def user_playlists(_user_id = nil)
|
|
81
|
-
run(:get, '/v1/me/playlists', [200])
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def user_playlist(_user_id, playlist_id)
|
|
85
|
-
playlist(playlist_id)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def user_playlist_tracks(_user_id, playlist_id, params = {})
|
|
89
|
-
tracks = { 'items' => [] }
|
|
90
|
-
path = "/v1/playlists/#{playlist_id}/items"
|
|
91
|
-
|
|
92
|
-
while path
|
|
93
|
-
response = run(:get, path, [200], params)
|
|
94
|
-
tracks['items'].concat(response.delete('items'))
|
|
95
|
-
tracks.merge!(response)
|
|
96
|
-
|
|
97
|
-
path = response['next']&.gsub(BASE_URI, '')
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
tracks
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Create a playlist for a Spotify user. The playlist will be empty until you add tracks.
|
|
104
|
-
#
|
|
105
|
-
# Requires playlist-modify-public for a public playlist.
|
|
106
|
-
# Requires playlist-modify-private for a private playlist.
|
|
107
|
-
def create_user_playlist(_user_id, name, is_public = true)
|
|
108
|
-
run(:post, '/v1/me/playlists', [201], JSON.dump(name: name, public: is_public), false)
|
|
109
|
-
end
|
|
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
|
-
|
|
115
|
-
# Add an Array of track uris to an existing playlist.
|
|
116
|
-
#
|
|
117
|
-
# Adding tracks to a user's public playlist requires authorization of the playlist-modify-public scope;
|
|
118
|
-
# adding tracks to a private playlist requires the playlist-modify-private scope.
|
|
119
|
-
#
|
|
120
|
-
# client.add_user_tracks_to_playlist(
|
|
121
|
-
# '1181346016', '7i3thJWDtmX04dJhFwYb0x', %w(spotify:track:... spotify:track:...)
|
|
122
|
-
# )
|
|
123
|
-
def add_user_tracks_to_playlist(_user_id, playlist_id, uris = [], position = nil)
|
|
124
|
-
params = { uris: Array(uris)[0..99].join(',') }
|
|
125
|
-
params.merge!(position: position) if position
|
|
126
|
-
run(:post, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(params), false)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Removes tracks from playlist
|
|
130
|
-
#
|
|
131
|
-
# client.remove_user_tracks_from_playlist(
|
|
132
|
-
# '1181346016', '7i3thJWDtmX04dJhFwYb0x', [{ uri: 'spotify:track:...', positions: [0] }]
|
|
133
|
-
# )
|
|
134
|
-
def remove_user_tracks_from_playlist(_user_id, playlist_id, tracks)
|
|
135
|
-
run(:delete, "/v1/playlists/#{playlist_id}/items", [200], JSON.dump(items: tracks))
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Replaces all occurrences of tracks with what's in the playlist
|
|
139
|
-
#
|
|
140
|
-
# client.replace_user_tracks_in_playlist(
|
|
141
|
-
# '1181346016', '7i3thJWDtmX04dJhFwYb0x', %w(spotify:track:... spotify:track:...)
|
|
142
|
-
# )
|
|
143
|
-
def replace_user_tracks_in_playlist(_user_id, playlist_id, tracks)
|
|
144
|
-
run(:put, "/v1/playlists/#{playlist_id}/items", [200, 201], JSON.dump(uris: tracks))
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Removes all tracks in playlist
|
|
148
|
-
#
|
|
149
|
-
# client.truncate_user_playlist('1181346016', '7i3thJWDtmX04dJhFwYb0x')
|
|
150
|
-
def truncate_user_playlist(user_id, playlist_id)
|
|
151
|
-
replace_user_tracks_in_playlist(user_id, playlist_id, [])
|
|
152
|
-
end
|
|
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
|
-
|
|
166
|
-
def album(album_id)
|
|
167
|
-
run(:get, "/v1/albums/#{album_id}", [200])
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def album_tracks(album_id)
|
|
171
|
-
run(:get, "/v1/albums/#{album_id}/tracks", [200])
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def albums(album_ids)
|
|
175
|
-
{ 'albums' => Array(album_ids).map { |album_id| album(album_id) } }
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def track(track_id)
|
|
179
|
-
run(:get, "/v1/tracks/#{track_id}", [200])
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def tracks(track_ids)
|
|
183
|
-
{ 'tracks' => Array(track_ids).map { |track_id| track(track_id) } }
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def artist(artist_id)
|
|
187
|
-
run(:get, "/v1/artists/#{artist_id}", [200])
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def artists(artist_ids)
|
|
191
|
-
{ 'artists' => Array(artist_ids).map { |artist_id| artist(artist_id) } }
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def artist_albums(artist_id)
|
|
195
|
-
run(:get, "/v1/artists/#{artist_id}/albums", [200])
|
|
196
|
-
end
|
|
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
|
-
|
|
222
|
-
def search(entity, term, options = {})
|
|
223
|
-
unless %i[artist album track].include?(entity.to_sym)
|
|
224
|
-
raise(ImplementationError, "entity needs to be either artist, album or track, got: #{entity}")
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
options = options.dup
|
|
228
|
-
options[:limit] = [options[:limit].to_i, 10].min if options.key?(:limit)
|
|
229
|
-
|
|
230
|
-
params = {
|
|
231
|
-
q: term.to_s,
|
|
232
|
-
type: entity
|
|
233
|
-
}.merge(options)
|
|
234
|
-
run(:get, '/v1/search', [200], params)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Get Spotify catalog information about an artist's top 10 tracks by country.
|
|
238
|
-
#
|
|
239
|
-
# +country_id+ is required. An ISO 3166-1 alpha-2 country code.
|
|
240
|
-
def artist_top_tracks(artist_id, country_id)
|
|
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)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def related_artists(artist_id)
|
|
246
|
-
run(:get, "/v1/artists/#{artist_id}/related-artists", [200])
|
|
247
|
-
end
|
|
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
|
-
|
|
329
|
-
# Follow artists or users
|
|
330
|
-
#
|
|
331
|
-
# client.follow('artist', ['0BvkDsjIUla7X0k6CSWh1I'])
|
|
332
|
-
def follow(type, ids)
|
|
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)
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# Follow a playlist
|
|
346
|
-
#
|
|
347
|
-
# client.follow_playlist('lukebryan', '0obRj9nNySESpFelMCLSya')
|
|
348
|
-
def follow_playlist(_user_id, playlist_id, is_public = true)
|
|
349
|
-
_is_public = is_public # kept for backward-compatible signature
|
|
350
|
-
add_to_library(["spotify:playlist:#{playlist_id}"])
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Generic API helper for forward compatibility with newly added endpoints.
|
|
354
|
-
def request(verb, path, expected_status_codes = [200], params_or_body = {}, idempotent = true)
|
|
355
|
-
run(verb.to_sym, path, Array(expected_status_codes), params_or_body, idempotent)
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# Bang variant that propagates mapped API errors.
|
|
359
|
-
def request!(verb, path, expected_status_codes = [200], params_or_body = {}, idempotent = true)
|
|
360
|
-
run!(verb.to_sym, path, Array(expected_status_codes), params_or_body, idempotent)
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
protected
|
|
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
|
-
|
|
377
|
-
def run(verb, path, expected_status_codes, params = {}, idempotent = true)
|
|
378
|
-
run!(verb, path, expected_status_codes, params, idempotent)
|
|
379
|
-
rescue Error => e
|
|
380
|
-
raise e if @raise_errors
|
|
381
|
-
|
|
382
|
-
false
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def run!(verb, path, expected_status_codes, params_or_body = nil, idempotent = true)
|
|
386
|
-
packet = {
|
|
387
|
-
idempotent: idempotent,
|
|
388
|
-
expects: expected_status_codes,
|
|
389
|
-
method: verb,
|
|
390
|
-
path: path,
|
|
391
|
-
read_timeout: @read_timeout,
|
|
392
|
-
write_timeout: @write_timeout,
|
|
393
|
-
retry_limit: @retries,
|
|
394
|
-
headers: {
|
|
395
|
-
'Content-Type' => 'application/json',
|
|
396
|
-
'User-Agent' => 'Spotify Ruby Client'
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if params_or_body.is_a?(Hash)
|
|
400
|
-
packet.merge!(query: params_or_body)
|
|
401
|
-
else
|
|
402
|
-
packet.merge!(body: params_or_body)
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
packet[:headers].merge!('Authorization' => "Bearer #{@access_token}") if !@access_token.nil? && @access_token != ''
|
|
406
|
-
|
|
407
|
-
# puts "\033[31m [Spotify] HTTP Request: #{verb.upcase} #{BASE_URI}#{path} #{packet[:headers].inspect} \e[0m"
|
|
408
|
-
response = @connection.request(packet)
|
|
409
|
-
return {} if response.body.nil? || response.body.empty?
|
|
410
|
-
|
|
411
|
-
::JSON.parse(response.body)
|
|
412
|
-
rescue Excon::Errors::NotFound => e
|
|
413
|
-
raise(ResourceNotFound, "Error: #{e.message}")
|
|
414
|
-
rescue Excon::Errors::BadRequest => e
|
|
415
|
-
raise(BadRequest, "Error: #{e.message}")
|
|
416
|
-
rescue Excon::Errors::Forbidden => e
|
|
417
|
-
raise(InsufficientClientScopeError, "Error: #{e.message}")
|
|
418
|
-
rescue Excon::Errors::Unauthorized => e
|
|
419
|
-
raise(AuthenticationError, "Error: #{e.message}")
|
|
420
|
-
rescue Excon::Errors::Error => e
|
|
421
|
-
# Catch all others errors. Samples:
|
|
422
|
-
#
|
|
423
|
-
# <Excon::Errors::SocketError: Connection refused - connect(2) (Errno::ECONNREFUSED)>
|
|
424
|
-
# <Excon::Errors::InternalServerError: Expected([200, 204, 404]) <=> Actual(500 InternalServerError)>
|
|
425
|
-
# <Excon::Errors::Timeout: read timeout reached>
|
|
426
|
-
# <Excon::Errors::BadGateway: Expected([200]) <=> Actual(502 Bad Gateway)>
|
|
427
|
-
raise(HTTPError, "Error: #{e.message}")
|
|
428
|
-
end
|
|
429
|
-
end
|
|
430
|
-
end
|
|
6
|
+
require 'spotify/client'
|
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.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Poli
|
|
@@ -40,6 +40,9 @@ files:
|
|
|
40
40
|
- LICENSE
|
|
41
41
|
- README.md
|
|
42
42
|
- lib/spotify-client.rb
|
|
43
|
+
- lib/spotify/client.rb
|
|
44
|
+
- lib/spotify/client/compatibility_api.rb
|
|
45
|
+
- lib/spotify/client/transport.rb
|
|
43
46
|
- lib/spotify/exceptions.rb
|
|
44
47
|
- lib/spotify/version.rb
|
|
45
48
|
- lib/spotify_client.rb
|