Skip to content

Commit 847e6c9

Browse files
committed
implement optional rate limit handling
1 parent 9769237 commit 847e6c9

File tree

10 files changed

+298
-68
lines changed

10 files changed

+298
-68
lines changed

src/main/kotlin/com/adamratzman/spotify/Builder.kt

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ fun spotifyApi(block: SpotifyApiBuilderDsl.() -> Unit) = SpotifyApiBuilderDsl().
1616
* Spotify traditional Java style API builder
1717
*/
1818
class SpotifyApiBuilder(
19-
val clientId: String,
20-
val clientSecret: String,
21-
var redirectUri: String? = null,
22-
var authorizationCode: String? = null,
23-
var tokenString: String? = null,
24-
var token: Token? = null,
25-
var useCache: Boolean = true,
26-
var automaticRefresh: Boolean = true
19+
val clientId: String,
20+
val clientSecret: String,
21+
var redirectUri: String? = null,
22+
var authorizationCode: String? = null,
23+
var tokenString: String? = null,
24+
var token: Token? = null,
25+
var useCache: Boolean = true,
26+
var automaticRefresh: Boolean = true,
27+
var retryWhenRateLimited: Boolean = false,
28+
var enableLogger: Boolean = false
2729
) {
2830
/**
2931
* Instantiate the builder with the application [clientId] and [clientSecret]
@@ -92,6 +94,16 @@ class SpotifyApiBuilder(
9294
*/
9395
fun automaticRefresh(automaticRefresh: Boolean) = apply { this.automaticRefresh = automaticRefresh }
9496

97+
/**
98+
* Set whether to block the current thread and wait until the API can retry the request
99+
*/
100+
fun retryWhenRateLimited(retryWhenRateLimited: Boolean) = apply { this.retryWhenRateLimited = retryWhenRateLimited }
101+
102+
/**
103+
* Set whether to enable to the exception logger
104+
*/
105+
fun enableLogger(enableLogger: Boolean) = apply { this.enableLogger = enableLogger }
106+
95107
/**
96108
* Create a [SpotifyAPI] instance with the given [SpotifyApiBuilder] parameters and the type -
97109
* [AuthorizationType.CLIENT] for client authentication, or otherwise [AuthorizationType.APPLICATION]
@@ -120,6 +132,8 @@ class SpotifyApiBuilder(
120132
}
121133

122134
automaticRefresh = this@SpotifyApiBuilder.automaticRefresh
135+
retryWhenRateLimited = this@SpotifyApiBuilder.retryWhenRateLimited
136+
enableLogger = this@SpotifyApiBuilder.enableLogger
123137
}.buildCredentialed()
124138

125139
/**
@@ -139,6 +153,8 @@ class SpotifyApiBuilder(
139153
}
140154

141155
automaticRefresh = this@SpotifyApiBuilder.automaticRefresh
156+
retryWhenRateLimited = this@SpotifyApiBuilder.retryWhenRateLimited
157+
enableLogger = this@SpotifyApiBuilder.enableLogger
142158
}.buildClient()
143159
}
144160

@@ -172,16 +188,29 @@ data class SpotifyCredentials(val clientId: String?, val clientSecret: String?,
172188
* limited time constraint on these before the API automatically refreshes them
173189
*/
174190
class SpotifyUserAuthorizationBuilder(
175-
var authorizationCode: String? = null,
176-
var tokenString: String? = null,
177-
var token: Token? = null
191+
var authorizationCode: String? = null,
192+
var tokenString: String? = null,
193+
var token: Token? = null
178194
)
179195

196+
/**
197+
* Spotify API mutable parameters
198+
*
199+
* @property credentials A holder for application-specific credentials
200+
* @property authentication A holder for authentication methods. At least one needs to be provided in order to create
201+
* a **client** api
202+
* @property useCache Set whether to cache requests. Default: true
203+
* @property automaticRefresh Enable or disable automatic refresh of the Spotify access token
204+
* @property retryWhenRateLimited Set whether to block the current thread and wait until the API can retry the request
205+
* @property enableLogger Set whether to enable to the exception logger
206+
*/
180207
class SpotifyApiBuilderDsl {
181208
private var credentials: SpotifyCredentials = SpotifyCredentials(null, null, null)
182209
private var authentication = SpotifyUserAuthorizationBuilder()
183210
var useCache: Boolean = true
184211
var automaticRefresh: Boolean = true
212+
var retryWhenRateLimited: Boolean = false
213+
var enableLogger: Boolean = false
185214

186215
/**
187216
* A block in which Spotify application credentials (accessible via the Spotify [dashboard](https://developer.spotify.com/dashboard/applications))
@@ -232,7 +261,7 @@ class SpotifyApiBuilderDsl {
232261
return when {
233262
authentication.token != null -> {
234263
SpotifyAppAPI(clientId ?: "not-set", clientSecret
235-
?: "not-set", authentication.token!!, useCache, false)
264+
?: "not-set", authentication.token!!, useCache, false, retryWhenRateLimited, enableLogger)
236265
}
237266
authentication.tokenString != null -> {
238267
SpotifyAppAPI(
@@ -243,13 +272,15 @@ class SpotifyApiBuilderDsl {
243272
60000, null, null
244273
),
245274
useCache,
246-
automaticRefresh
275+
automaticRefresh,
276+
retryWhenRateLimited,
277+
enableLogger
247278
)
248279
}
249280
else -> try {
250281
if (clientId == null || clientSecret == null) throw IllegalArgumentException("Illegal credentials provided")
251282
val token = getCredentialedToken(clientId, clientSecret, null)
252-
SpotifyAppAPI(clientId, clientSecret, token, useCache, automaticRefresh)
283+
SpotifyAppAPI(clientId, clientSecret, token, useCache, automaticRefresh, retryWhenRateLimited, enableLogger)
253284
} catch (e: Exception) {
254285
throw SpotifyException("Invalid credentials provided in the login process", e)
255286
}
@@ -286,9 +317,9 @@ class SpotifyApiBuilderDsl {
286317
* [authorizationCode] or [token] is provided
287318
*/
288319
private fun buildClient(
289-
authorizationCode: String? = null,
290-
tokenString: String? = null,
291-
token: Token? = null
320+
authorizationCode: String? = null,
321+
tokenString: String? = null,
322+
token: Token? = null
292323
): SpotifyClientAPI {
293324
val clientId = credentials.clientId
294325
val clientSecret = credentials.clientSecret
@@ -317,7 +348,9 @@ class SpotifyApiBuilderDsl {
317348
response.body.toObject(null),
318349
automaticRefresh,
319350
redirectUri ?: throw IllegalArgumentException(),
320-
useCache
351+
useCache,
352+
retryWhenRateLimited,
353+
enableLogger
321354
)
322355
} catch (e: Exception) {
323356
throw SpotifyException("Invalid credentials provided in the login process", e)
@@ -328,7 +361,9 @@ class SpotifyApiBuilderDsl {
328361
token,
329362
automaticRefresh,
330363
redirectUri ?: "not-set",
331-
useCache
364+
useCache,
365+
retryWhenRateLimited,
366+
enableLogger
332367
)
333368
tokenString != null -> SpotifyClientAPI(
334369
clientId ?: "not-set",
@@ -342,7 +377,9 @@ class SpotifyApiBuilderDsl {
342377
),
343378
false,
344379
redirectUri ?: "not-set",
345-
useCache
380+
useCache,
381+
retryWhenRateLimited,
382+
enableLogger
346383
)
347384
else -> throw IllegalArgumentException(
348385
"At least one of: authorizationCode, tokenString, or token must be provided " +

src/main/kotlin/com/adamratzman/spotify/SpotifyAPI.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ abstract class SpotifyAPI internal constructor(
6363
val clientSecret: String,
6464
var token: Token,
6565
useCache: Boolean,
66-
var automaticRefresh: Boolean
66+
var automaticRefresh: Boolean,
67+
var retryWhenRateLimited: Boolean,
68+
enableLogger: Boolean
6769
) {
6870
private var refreshFuture: ScheduledFuture<*>? = null
6971

@@ -72,6 +74,8 @@ abstract class SpotifyAPI internal constructor(
7274
if (!useCache && value) refreshFuture = startCacheRefreshRunnable()
7375
else if (useCache && !value) refreshFuture?.cancel(false)
7476

77+
if (!value) clearCache()
78+
7579
field = value
7680
}
7781

@@ -87,7 +91,7 @@ abstract class SpotifyAPI internal constructor(
8791
abstract val tracks: TracksAPI
8892
abstract val following: FollowingAPI
8993

90-
internal val logger = SpotifyLogger(true)
94+
internal val logger = SpotifyLogger(enableLogger)
9195

9296
abstract val klaxon: Klaxon
9397

@@ -157,9 +161,10 @@ class SpotifyAppAPI internal constructor(
157161
clientSecret: String,
158162
token: Token,
159163
useCache: Boolean,
160-
automaticRefresh: Boolean
161-
) :
162-
SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh) {
164+
automaticRefresh: Boolean,
165+
retryWhenRateLimited: Boolean,
166+
enableLogger: Boolean
167+
) : SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh, retryWhenRateLimited, enableLogger) {
163168

164169
override val search: SearchAPI = SearchAPI(this)
165170
override val albums: AlbumAPI = AlbumAPI(this)
@@ -229,8 +234,10 @@ class SpotifyClientAPI internal constructor(
229234
token: Token,
230235
automaticRefresh: Boolean,
231236
var redirectUri: String,
232-
useCache: Boolean
233-
) : SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh) {
237+
useCache: Boolean,
238+
retryWhenRateLimited: Boolean,
239+
enableLogger: Boolean
240+
) : SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh, retryWhenRateLimited, enableLogger) {
234241
override val search: SearchAPI = SearchAPI(this)
235242
override val albums: AlbumAPI = AlbumAPI(this)
236243
override val browse: BrowseAPI = BrowseAPI(this)

src/main/kotlin/com/adamratzman/spotify/SpotifyLogger.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ class SpotifyLogger(var enabled: Boolean) {
2626
}
2727
}
2828

29-
class SpotifyException(message: String, cause: Throwable) : Exception(message, cause)
29+
open class SpotifyException(message: String, cause: Throwable? = null) : Exception(message, cause)

src/main/kotlin/com/adamratzman/spotify/SpotifyScope.kt

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,139 @@ package com.adamratzman.spotify
88
* Each represents a distinct privilege and may be required by one or more endpoints as discussed
99
* on the [Spotify Authorization Documentation](https://developer.spotify.com/documentation/general/guides/scopes/)
1010
*
11-
* @property uri the scope id
11+
* @property uri The scope id
1212
*/
1313
enum class SpotifyScope(val uri: String) {
14+
/**
15+
* Remote control playback of Spotify. This scope is currently available to Spotify iOS and Android App Remote SDKs.
16+
*
17+
* **Visible to users**: Communicate with the Spotify app on your device.
18+
*/
19+
APP_REMOTE_CONTROL("app-remote-control"),
20+
21+
/**
22+
* Read access to user's private playlists.
23+
*
24+
* **Visible to users**: Access your private playlists.
25+
*/
1426
PLAYLIST_READ_PRIVATE("playlist-read-private"),
27+
28+
/**
29+
* Include collaborative playlists when requesting a user's playlists.
30+
*
31+
* **Visible to users**: Access your collaborative playlists.
32+
*/
1533
PLAYLIST_READ_COLLABORATIVE("playlist-read-collaborative"),
34+
35+
/**
36+
* Write access to a user's public playlists.
37+
*
38+
* **Visible to users**: Manage your public playlists.
39+
*/
1640
PLAYLIST_MODIFY_PUBLIC("playlist-modify-public"),
41+
42+
/**
43+
* Write access to a user's private playlists.
44+
*
45+
* **Visible to users**: Manage your private playlists.
46+
*/
1747
PLAYLIST_MODIFY_PRIVATE("playlist-modify-private"),
48+
49+
/**
50+
* Control playback of a Spotify track. This scope is currently available to Spotify Playback SDKs, including the iOS SDK, Android SDK and Web Playback SDK. The user must have a Spotify Premium account.
51+
*
52+
* **Visible to users**: Play music and control playback on your other devices.
53+
*/
54+
STREAMING("streaming"),
55+
56+
/**
57+
* Let the application upload playlist covers and profile images
58+
*
59+
* **Visible to users**: Upload images to personalize your profile or playlist cover
60+
*/
1861
UGC_IMAGE_UPLOAD("ugc-image-upload"),
62+
63+
/**
64+
* Write/delete access to the list of artists and other users that the user follows.
65+
*
66+
* **Visible to users**: Manage who you are following.
67+
*/
1968
USER_FOLLOW_MODIFY("user-follow-modify"),
69+
70+
/**
71+
* Read access to the list of artists and other users that the user follows.
72+
*
73+
* **Visible to users**: Access your followers and who you are following.
74+
*/
2075
USER_FOLLOW_READ("user-follow-read"),
76+
77+
/**
78+
* Read access to a user's "Your Music" library.
79+
*
80+
* **Visible to users**: Access your saved tracks and albums.
81+
*/
2182
USER_LIBRARY_READ("user-library-read"),
83+
84+
/**
85+
* Write/delete access to a user's "Your Music" library.
86+
*
87+
* **Visible to users**: Manage your saved tracks and albums.
88+
*/
2289
USER_LIBRARY_MODIFY("user-library-modify"),
90+
91+
/**
92+
* Write access to a user’s playback state
93+
*
94+
* **Visible to users**: Control playback on your Spotify clients and Spotify Connect devices.
95+
*/
96+
USER_MODIFY_PLAYBACK_STATE("user-modify-playback-state"),
97+
98+
/**
99+
* Read access to user’s subscription details (type of user account).
100+
*
101+
* **Visible to users**: Access your subscription details.
102+
*/
23103
USER_READ_PRIVATE("user-read-private"),
104+
105+
/**
106+
* Read access to the user's birthdate.
107+
*
108+
* **Visible to users**: Receive your birthdate.
109+
*/
24110
USER_READ_BIRTHDATE("user-read-birthdate"),
111+
112+
/**
113+
* Read access to user’s email address.
114+
*
115+
* **Visible to users**: Get your real email address.
116+
*/
25117
USER_READ_EMAIL("user-read-email"),
118+
119+
/**
120+
* Read access to a user's top artists and tracks.
121+
*
122+
* **Visible to users**: Read your top artists and tracks.
123+
*/
26124
USER_TOP_READ("user-top-read"),
125+
126+
/**
127+
* Read access to a user’s player state.
128+
*
129+
* **Visible to users**: Read your currently playing track and Spotify Connect devices information.
130+
*/
27131
USER_READ_PLAYBACK_STATE("user-read-playback-state"),
132+
133+
/**
134+
* Read access to a user’s currently playing track
135+
*
136+
* **Visible to users**: Read your currently playing track
137+
*/
28138
USER_READ_CURRENTLY_PLAYING("user-read-currently-playing"),
139+
140+
/**
141+
* Read access to a user’s recently played tracks.
142+
*
143+
* **Visible to users**: Access your recently played items.
144+
*/
29145
USER_READ_RECENTLY_PLAYED("user-read-recently-played");
30146
}

src/main/kotlin/com/adamratzman/spotify/endpoints/client/ClientPlayerAPI.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/* Spotify Web API - Kotlin Wrapper; MIT License, 2019; Original author: Adam Ratzman */
22
package com.adamratzman.spotify.endpoints.client
33

4-
import com.adamratzman.spotify.http.EndpointBuilder
5-
import com.adamratzman.spotify.http.SpotifyEndpoint
6-
import com.adamratzman.spotify.http.encode
74
import com.adamratzman.spotify.SpotifyAPI
85
import com.adamratzman.spotify.SpotifyRestAction
96
import com.adamratzman.spotify.SpotifyRestActionPaging
7+
import com.adamratzman.spotify.SpotifyScope
8+
import com.adamratzman.spotify.http.EndpointBuilder
9+
import com.adamratzman.spotify.http.SpotifyEndpoint
10+
import com.adamratzman.spotify.http.encode
1011
import com.adamratzman.spotify.models.AlbumURI
1112
import com.adamratzman.spotify.models.ArtistURI
1213
import com.adamratzman.spotify.models.BadRequestException
@@ -56,6 +57,8 @@ class ClientPlayerAPI(api: SpotifyAPI) : SpotifyEndpoint(api) {
5657
/**
5758
* Get tracks from the current user’s recently played tracks.
5859
*
60+
* **Requires** the [SpotifyScope.USER_READ_RECENTLY_PLAYED] scope
61+
*
5962
* @param limit The number of objects to return. Default: 20. Minimum: 1. Maximum: 50.
6063
* @param before The timestamp (retrieved via cursor) **not including**, but before which, tracks will have been played. This can be combined with [limit]
6164
* @param after The timestamp (retrieved via cursor) **not including**, after which, the retrieval starts. This can be combined with [limit]

0 commit comments

Comments
 (0)